From 910db267efe193465c1666c5de55b736c4cd3cf7 Mon Sep 17 00:00:00 2001 From: sergio Date: Sun, 21 Jun 2026 07:07:32 -0400 Subject: [PATCH] =?UTF-8?q?Etapa=20F=20paqueter=C3=ADa=20#6:=20repo=20sobr?= =?UTF-8?q?e=20red=20=E2=80=94=20`install=20--repo=20URL`=20(HTTP/HTTPS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El repo son ficheros estáticos (index.json + .swm) ⇒ cualquier servidor estático lo sirve. - hammer-cli: `RepoSource` {Local(path) | Http(url)}. `install --repo` ahora acepta path o URL. Para HTTP: lee index.json por GET, materializa un repo LOCAL temporal bajando el índice + los .swm del cierre de deps (curl, vía download::fetch_url_bytes), y de ahí el flujo es IDÉNTICO al local (resolución de deps, verificación de release/firma/base, reproduce + hidrata). tempfile pasa a dep normal de hammer-cli. - Validado E2E: server HTTP estático + install openssh vía http:// → "release: trusted" (índice firmado bajado por red) → "repo: bajados 3 .swm" (cierre openssh+zlib+openssl) → resuelve del temporal. (El proxy del sandbox exige NO_PROXY para localhost; el código es correcto.) 31 verde. Co-Authored-By: Claude Opus 4.8 --- crates/hammer-cli/Cargo.toml | 1 + crates/hammer-cli/src/main.rs | 88 ++++++++++++++++++++++++++++++++--- docs/06-swm-format.md | 21 +++++---- 3 files changed, 95 insertions(+), 15 deletions(-) diff --git a/crates/hammer-cli/Cargo.toml b/crates/hammer-cli/Cargo.toml index 4fd8b428..99a24f7a 100644 --- a/crates/hammer-cli/Cargo.toml +++ b/crates/hammer-cli/Cargo.toml @@ -32,6 +32,7 @@ anyhow.workspace = true clap.workspace = true tracing.workspace = true tracing-subscriber.workspace = true +tempfile.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs index 8742d190..478b6cd9 100644 --- a/crates/hammer-cli/src/main.rs +++ b/crates/hammer-cli/src/main.rs @@ -208,9 +208,9 @@ enum Cmd { Install { /// Nombre del paquete a instalar (clave del índice del repo). name: String, - /// Directorio del repositorio (con `index.json`). + /// Repositorio: un directorio local (con `index.json`) o una URL HTTP(S) (`http(s)://…`). #[arg(long, default_value = DEFAULT_REPO)] - repo: PathBuf, + repo: String, /// Re-rootea las mutaciones bajo este prefix (tests/staging) en vez de abrir overlay. #[arg(long)] prefix: Option, @@ -1347,14 +1347,79 @@ fn run_pack( Ok(()) } +/// De dónde viene un repo: un directorio local, o una URL HTTP(S) (el repo son ficheros +/// estáticos — `index.json` + los `.swm` —, así que cualquier servidor estático lo sirve). +enum RepoSource { + Local(PathBuf), + Http(String), // base sin barra final +} + +impl RepoSource { + fn parse(s: &str) -> RepoSource { + if s.starts_with("http://") || s.starts_with("https://") { + RepoSource::Http(s.trim_end_matches('/').to_string()) + } else { + RepoSource::Local(PathBuf::from(s)) + } + } + + fn label(&self) -> String { + match self { + RepoSource::Local(d) => d.display().to_string(), + RepoSource::Http(b) => b.clone(), + } + } + + /// Lee el índice del repo (local: `index.json`; http: `GET /index.json`). + fn read_index(&self) -> anyhow::Result { + match self { + RepoSource::Local(d) => Ok(hammer_core::RepoIndex::load(d)?), + RepoSource::Http(base) => { + let url = format!("{base}/index.json"); + let bytes = hammer_build::download::fetch_url_bytes(&url)?; + serde_json::from_slice(&bytes) + .map_err(|e| anyhow::anyhow!("índice remoto {url}: {e}")) + } + } + } + + /// Materializa un directorio de repo LOCAL del que el resto del flujo lee los `.swm`. Para + /// `Local` es el propio directorio; para `Http` baja `index.json` + los `.swm` de `files` a un + /// dir temporal (que el caller mantiene vivo) ⇒ el camino de instalación es idéntico. + fn materialize( + &self, + index: &hammer_core::RepoIndex, + files: &[&str], + tmp_holder: &mut Option, + ) -> anyhow::Result { + match self { + RepoSource::Local(d) => Ok(d.clone()), + RepoSource::Http(base) => { + let tmp = tempfile::tempdir()?; + let dir = tmp.path().to_path_buf(); + index.save(&dir)?; // index.json (con firma) re-cargable idéntico + for f in files { + let url = format!("{base}/{f}"); + let bytes = hammer_build::download::fetch_url_bytes(&url)?; + std::fs::write(dir.join(f), bytes)?; + } + eprintln!("repo: bajados {} .swm de {base}", files.len()); + *tmp_holder = Some(tmp); // mantener vivo hasta el final del install + Ok(dir) + } + } + } +} + /// Instala un paquete por nombre desde un repo (Etapa F): resuelve el nombre en el índice, /// verifica firma (con `--trust`) y delega en el camino de `apply` (reproduce + hidrata). Nunca -/// corre un binario ajeno — sólo reconstruye desde fuente y compara. +/// corre un binario ajeno — sólo reconstruye desde fuente y compara. `repo` puede ser un +/// directorio local o una URL HTTP(S). #[allow(clippy::too_many_arguments)] fn run_install( store_path: &str, name: &str, - repo_dir: &std::path::Path, + repo: &str, prefix: Option<&std::path::Path>, skip_source_patch: bool, base_ref: Option<&std::path::Path>, @@ -1362,7 +1427,8 @@ fn run_install( state_root: Option<&std::path::Path>, db_path: &std::path::Path, ) -> anyhow::Result<()> { - let index = hammer_core::RepoIndex::load(repo_dir)?; + let source = RepoSource::parse(repo); + let index = source.read_index()?; // Verificación del RELEASE (firma del catálogo entero): si el índice está firmado, una firma // mala = índice manipulado ⇒ abortamos antes de tocar nada. Sin firma se informa; con --trust @@ -1386,10 +1452,20 @@ fn run_install( let avail: Vec<&str> = index.packages.iter().map(|p| p.name.as_str()).collect(); anyhow::anyhow!( "paquete '{name}' no está en el repo {}. Disponibles: {}", - repo_dir.display(), + source.label(), if avail.is_empty() { "(ninguno)".into() } else { avail.join(", ") } ) })?; + + // Materializamos un repo LOCAL con los `.swm` que vamos a tocar (el target + su cierre de + // deps). Para un repo local es el propio directorio; para HTTP, baja esos `.swm` a un temporal. + // De ahí en más el flujo es idéntico (lee ficheros de `repo_dir`). + let closure = index.resolve_closure(name)?; + let needed: Vec<&str> = closure.iter().map(|p| p.file.as_str()).collect(); + let mut _tmp_repo: Option = None; + let repo_dir = source.materialize(&index, &needed, &mut _tmp_repo)?; + let repo_dir = repo_dir.as_path(); + let swm_path = hammer_core::RepoIndex::swm_path(repo_dir, entry); eprintln!( "install {} {} ← {}", diff --git a/docs/06-swm-format.md b/docs/06-swm-format.md index 7f3d55bb..2d8f1741 100644 --- a/docs/06-swm-format.md +++ b/docs/06-swm-format.md @@ -137,12 +137,15 @@ CLI: `strip_components`, y las `deps` por nombre; los patches de la receta viajan inline (concatenados). Si la receta declara build-deps, `pack` recuerda publicarlas en el mismo repo. Con `--repo DIR` **publica** en un repositorio (ver abajo) en vez de (o además de) `--out`. -- `hammer install [--repo DIR]` — **consume** del repositorio: resuelve `nombre` en el - índice, verifica la firma (con `--trust DIR`) y la base (con `--base-ref`), **resuelve el cierre - transitivo de build-deps** desde el índice (orden topológico) poblando un catálogo de recetas, y - delega en el camino de `apply` (reproduce el `source_patch` desde fuente, con sus deps - materializadas en el sandbox, + hidrata). Nunca corre un binario ajeno. `--prefix`/ - `--skip-source-patch` para staging y dry-run de schema. +- `hammer install [--repo DIR|URL]` — **consume** del repositorio: resuelve `nombre` en + el índice, verifica la firma del release y la del `.swm` (con `--trust DIR`) y la base (con + `--base-ref`), **resuelve el cierre transitivo de build-deps** desde el índice (orden + topológico) poblando un catálogo de recetas, y delega en el camino de `apply` (reproduce el + `source_patch` desde fuente, con sus deps materializadas en el sandbox, + hidrata). Nunca corre + un binario ajeno. Registra el paquete en la DB de instalados (`--db`). `--repo` admite un + directorio local **o una URL HTTP(S)** (el repo son ficheros estáticos: `index.json` + los + `.swm`; con URL baja el índice + el cierre de deps a un temporal y procede igual). + `--prefix`/`--skip-source-patch` para staging y dry-run de schema. - `hammer repo list [--repo DIR]` — lista el catálogo (`/index.json`). - `hammer repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras publicar (cada `pack --repo` invalida la firma del release). @@ -159,9 +162,9 @@ Un **repo** es un directorio con los `.swm` + un `index.json` que mapea `nombre diffeable). Un `.swm` no lleva identidad propia (es un manifiesto de mutación, no "el paquete X"); la identidad la asigna el repo al publicar — el índice es el namespace. `pack --repo` publica con el nombre/versión de la receta (upsert idempotente por nombre; una versión nueva -retira el `.swm` huérfano). El transporte del repo (filesystem / sshfs / mirror) es ortogonal: -ver `hammer-mirror` para el CAS del store. Tipos en `hammer-core::repo` (`RepoIndex`, -`PackageEntry`). +retira el `.swm` huérfano). El repo son ficheros estáticos, así que `install --repo URL` lo +consume por HTTP(S) (cualquier servidor estático sirve; baja `index.json` + el cierre de deps a +un temporal). Tipos en `hammer-core::repo` (`RepoIndex`, `PackageEntry`). **Dependencias.** Un `source_patch` lleva sus build-deps por NOMBRE (las de la receta original); la `PackageEntry` las espeja para resolver el grafo sin abrir cada `.swm`. `install `