tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1182Shell 87Rust 63Markdown 541592 Sutichaykuna

Etapa F paquetería #6: repo sobre red — `install --repo URL` (HTTP/HTTPS)

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 <noreply@anthropic.com>

910db267efe193465c1666c5de55b736c4cd3cf7 · ruwaq sergio · (4 killa ñawpaq) · tayta 9d1b2d4a7 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 7

crates/hammer-cli/src/main.rs

3 qillqakuna · +95 −15

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<PathBuf>,
@@ -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 <base>/index.json`).
+    fn read_index(&self) -> anyhow::Result<hammer_core::RepoIndex> {
+        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<tempfile::TempDir>,
+    ) -> anyhow::Result<PathBuf> {
+        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<tempfile::TempDir> = 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 <nombre> [--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 <nombre> [--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 (`<repo>/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 <nombre>`