Etapa G: importador nix→receta (hammer import-nix + scripts/nix-import.sh)
Poblar el catálogo no es opcional: 34 recetas a mano = userland desierto. nixpkgs es el mayor set
de recetas DESDE FUENTE ⇒ semilla natural. Importamos la RECETA (source+hash+deps), nunca el
binario del cache de nix — hammer reconstruye desde fuente ("verificar, no confiar").
- crates/hammer-cli/nix_import.rs: consume el JSON normalizado de nix y emite una receta hammer.
Clasifica el origen: fetchurl flat → tarball+sha256 (convierte el hash nix SRI/base32/hex → hex);
fetchFromGitHub → repo+commit (hammer pinea por commit, no necesita el hash NAR). Filtra el ruido
de stdenv (setup-hooks, wrappers). nix_base32 decode portado. 11 tests.
- `hammer import-nix [FILE|-]` (stdin) → receta .toml; valida que parsee como Recipe.
- scripts/nix-import.sh <attr>: `nix eval --apply` produce el JSON normalizado y lo pipea al
importador. NIX_STORE= para store local si /nix/store no es escribible.
- VALIDADO contra nixpkgs REAL (nix 2.34): import hello (tarball, sha256→hex) + ripgrep (github→
repo+commit); pipeline completo nix→import→pack→.swm probado con hello. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
@@ -547,6 +547,7 @@ dependencies = [
|
||||
"hammer-overlay",
|
||||
"hammer-upgrade",
|
||||
"hex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
|
||||
@@ -26,6 +26,7 @@ hammer-upgrade.workspace = true
|
||||
hammer-overlay.workspace = true
|
||||
hammer-journal.workspace = true
|
||||
hammer-agent.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
base64.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -8,6 +8,8 @@ use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
mod nix_import;
|
||||
|
||||
const DEFAULT_STORE: &str = "/store";
|
||||
const DEFAULT_REPO: &str = "/var/lib/hammer/repo";
|
||||
|
||||
@@ -249,6 +251,18 @@ enum Cmd {
|
||||
#[arg(long, default_value = hammer_core::installed::DEFAULT_DB)]
|
||||
db: PathBuf,
|
||||
},
|
||||
/// [Etapa G] Importa una receta DESDE nixpkgs: toma el JSON normalizado que produce
|
||||
/// `scripts/nix-import.sh` (vía `nix eval`) y emite una receta hammer (`.toml`). Trae la
|
||||
/// RECETA (source+hash+deps+flags), nunca el binario del cache de nix — hammer reconstruye.
|
||||
/// Es un PUNTO DE PARTIDA: el build (zig/musl) suele necesitar adaptación por paquete.
|
||||
ImportNix {
|
||||
/// JSON normalizado del paquete nix. `-` o ausente ⇒ lee de stdin.
|
||||
#[arg(default_value = "-")]
|
||||
file: String,
|
||||
/// Receta de salida (`.toml`). Sin esto, imprime a stdout.
|
||||
#[arg(long, short)]
|
||||
out: Option<PathBuf>,
|
||||
},
|
||||
/// [Etapa F] Inspecciona un repositorio de paquetes.
|
||||
Repo {
|
||||
#[command(subcommand)]
|
||||
@@ -812,6 +826,7 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
Cmd::Uninstall { name, db } => run_uninstall(&name, &db)?,
|
||||
Cmd::Installed { db } => run_installed_list(&db)?,
|
||||
Cmd::ImportNix { file, out } => run_import_nix(&file, out.as_deref())?,
|
||||
Cmd::Repo { sub } => match sub {
|
||||
RepoCmd::List { repo } => run_repo_list(&repo)?,
|
||||
RepoCmd::Sign { repo, key, by } => run_repo_sign(&repo, &key, by.as_deref())?,
|
||||
@@ -1578,6 +1593,33 @@ fn run_install(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Importa una receta desde el JSON normalizado de nixpkgs (Etapa G).
|
||||
fn run_import_nix(file: &str, out: Option<&std::path::Path>) -> anyhow::Result<()> {
|
||||
let json = if file == "-" {
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_to_string(&mut s)?;
|
||||
s
|
||||
} else {
|
||||
std::fs::read_to_string(file)
|
||||
.map_err(|e| anyhow::anyhow!("leyendo {file}: {e}"))?
|
||||
};
|
||||
let pkg: nix_import::NixPkg = serde_json::from_str(&json)
|
||||
.map_err(|e| anyhow::anyhow!("JSON normalizado de nix inválido: {e}"))?;
|
||||
let toml = nix_import::to_recipe_toml(&pkg).map_err(|e| anyhow::anyhow!(e))?;
|
||||
// Sanity: la receta emitida debe parsear como Recipe de hammer.
|
||||
hammer_core::Recipe::from_toml(&toml)
|
||||
.map_err(|e| anyhow::anyhow!("la receta importada no parsea (bug del importador): {e}"))?;
|
||||
match out {
|
||||
Some(dest) => {
|
||||
std::fs::write(dest, &toml)?;
|
||||
eprintln!("importada {} → {}", pkg.pname, dest.display());
|
||||
}
|
||||
None => print!("{toml}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lista los paquetes instalados de la DB.
|
||||
fn run_installed_list(db_path: &std::path::Path) -> anyhow::Result<()> {
|
||||
let idb = hammer_core::InstalledDb::load(db_path)?;
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
//! Importador `nix → receta hammer` (Etapa G: poblar el catálogo desde nixpkgs).
|
||||
//!
|
||||
//! El problema: a mano, 34 recetas = userland desierto; un distro usable necesita miles de
|
||||
//! paquetes. nixpkgs es el mayor set de recetas DESDE FUENTE (sources pineados+hash, deps
|
||||
//! explícitas), así que es la semilla natural del catálogo. Este importador toma la *receta* de
|
||||
//! nix (qué bajar, flags, deps) y la reexpresa como una `Recipe` de hammer — **nunca el binario**
|
||||
//! del cache de nix (eso rompería "verificar, no confiar"; hammer reconstruye desde fuente igual).
|
||||
//!
|
||||
//! Consume un JSON NORMALIZADO (lo produce `scripts/nix-import.sh` vía `nix eval --apply`), no el
|
||||
//! `.drv` crudo: así el importador es puro y testeable sin nix, y la fricción de evaluar nixpkgs
|
||||
//! queda en el wrapper. Lo que el importador NO resuelve (y se documenta en la receta emitida): el
|
||||
//! build real en el lab de hammer (zig/musl estático) difiere del stdenv de nix (glibc/gcc) ⇒ cada
|
||||
//! paquete necesita adaptación de toolchain. El importador da el punto de partida, no el binario.
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Paquete nix normalizado (salida de `nix eval --apply` del wrapper). Campos opcionales con
|
||||
/// defaults para tolerar paquetes que no los declaran.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NixPkg {
|
||||
pub pname: String,
|
||||
#[serde(default)]
|
||||
pub version: String,
|
||||
pub source: RawSource,
|
||||
#[serde(default)]
|
||||
pub configure_flags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub build_inputs: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub native_build_inputs: Vec<String>,
|
||||
}
|
||||
|
||||
/// El origen TAL CUAL lo da `nix eval` del `p.src`: la URL fetcheada + el `outputHash`/mode de la
|
||||
/// derivación fija. `outputHashMode = "flat"` ⇒ el hash es del FICHERO (sha256 de tarball, usable);
|
||||
/// `"recursive"` ⇒ hash NAR del árbol (no casa con un sha256 de tarball ⇒ usamos git+commit).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RawSource {
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub output_hash: String,
|
||||
#[serde(default)]
|
||||
pub output_hash_mode: String,
|
||||
}
|
||||
|
||||
/// El origen ya clasificado para hammer. `fetchurl` plano → `tarball+sha256`; GitHub (codeload) →
|
||||
/// `repo+commit` (hammer pinea por commit y reproduce clonando, sin depender del hash NAR de nix).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum NixSource {
|
||||
Tarball { url: String, sha256: String },
|
||||
Github { owner: String, repo: String, rev: String },
|
||||
}
|
||||
|
||||
/// Clasifica el origen RAW de nix. Detecta GitHub por la URL de codeload/archive (de
|
||||
/// `fetchFromGitHub`), si no usa el modo del hash: `flat` ⇒ tarball con su sha256; `recursive`
|
||||
/// no-github ⇒ no podemos reusar el hash, error pidiendo pin manual.
|
||||
pub fn classify_source(raw: &RawSource) -> Result<NixSource, String> {
|
||||
if let Some((owner, repo, rev)) = parse_github_url(&raw.url) {
|
||||
return Ok(NixSource::Github { owner, repo, rev });
|
||||
}
|
||||
match raw.output_hash_mode.as_str() {
|
||||
"flat" | "" => {
|
||||
if raw.output_hash.is_empty() {
|
||||
return Err(format!("source flat sin hash: {}", raw.url));
|
||||
}
|
||||
let hex = nix_hash_to_hex(&raw.output_hash)?;
|
||||
Ok(NixSource::Tarball { url: raw.url.clone(), sha256: hex })
|
||||
}
|
||||
"recursive" => Err(format!(
|
||||
"source recursivo no-github (hash NAR no reusable): {} — fijá repo+commit a mano",
|
||||
raw.url
|
||||
)),
|
||||
other => Err(format!("outputHashMode desconocido '{other}': {}", raw.url)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrae `(owner, repo, rev)` de una URL de descarga de GitHub (las que arma `fetchFromGitHub`):
|
||||
/// `https://github.com/<owner>/<repo>/archive/<rev>.tar.gz` o
|
||||
/// `https://codeload.github.com/<owner>/<repo>/tar.gz/<rev>`.
|
||||
fn parse_github_url(url: &str) -> Option<(String, String, String)> {
|
||||
if let Some(rest) = url.strip_prefix("https://github.com/") {
|
||||
// <owner>/<repo>/archive/<rev>.tar.gz (rev puede traer 'refs/tags/..' → tomamos el final)
|
||||
let parts: Vec<&str> = rest.splitn(4, '/').collect();
|
||||
if parts.len() == 4 && parts[2] == "archive" {
|
||||
let rev = parts[3]
|
||||
.trim_end_matches(".tar.gz")
|
||||
.trim_end_matches(".zip")
|
||||
.rsplit('/')
|
||||
.next()?
|
||||
.to_string();
|
||||
return Some((parts[0].to_string(), parts[1].to_string(), rev));
|
||||
}
|
||||
}
|
||||
if let Some(rest) = url.strip_prefix("https://codeload.github.com/") {
|
||||
// <owner>/<repo>/tar.gz/<rev>
|
||||
let parts: Vec<&str> = rest.splitn(4, '/').collect();
|
||||
if parts.len() == 4 {
|
||||
return Some((parts[0].to_string(), parts[1].to_string(), parts[3].to_string()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Convierte una receta nix normalizada en el TOML de una `Recipe` de hammer. El resultado es un
|
||||
/// PUNTO DE PARTIDA: lleva comentarios marcando lo que falta adaptar (toolchain, nombres de deps).
|
||||
pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
|
||||
let name = sanitize_name(&pkg.pname);
|
||||
let version = if pkg.version.is_empty() { "0".to_string() } else { pkg.version.clone() };
|
||||
|
||||
let source = classify_source(&pkg.source)
|
||||
.map_err(|e| format!("source de '{}': {e}", pkg.pname))?;
|
||||
let source_block = match &source {
|
||||
NixSource::Tarball { url, sha256 } => {
|
||||
format!("tarball = \"{url}\"\nsha256 = \"{sha256}\"\n")
|
||||
}
|
||||
NixSource::Github { owner, repo, rev } => {
|
||||
format!("repo = \"https://github.com/{owner}/{repo}\"\ncommit = \"{rev}\"\n")
|
||||
}
|
||||
};
|
||||
|
||||
// deps: build = nativeBuildInputs ++ buildInputs (por nombre nix; el humano remapea al corpus
|
||||
// de hammer si difieren — p.ej. nix `pkg-config` ≈ hammer `pkgconf`).
|
||||
let mut deps: Vec<String> = Vec::new();
|
||||
for d in pkg.native_build_inputs.iter().chain(pkg.build_inputs.iter()) {
|
||||
let n = sanitize_name(d);
|
||||
if !n.is_empty() && !is_nix_noise(&n) && !deps.contains(&n) {
|
||||
deps.push(n);
|
||||
}
|
||||
}
|
||||
let deps_block = if deps.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let list = deps.iter().map(|d| format!("\"{d}\"")).collect::<Vec<_>>().join(", ");
|
||||
format!("\n[deps]\nbuild = [{list}]\n")
|
||||
};
|
||||
|
||||
// configureFlags → una fase configure explícita (si las hay). Si no, se deja a la heurística
|
||||
// del lab. NOTA: muchos paquetes nix no son autotools (cmake/meson); esto es best-effort.
|
||||
let phases_block = if pkg.configure_flags.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let flags = pkg
|
||||
.configure_flags
|
||||
.iter()
|
||||
.map(|f| f.replace('"', "\\\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
format!("\n[build.phases]\nconfigure = \"./configure --prefix=/usr {flags}\"\n")
|
||||
};
|
||||
|
||||
let toml = format!(
|
||||
"# Importada de nixpkgs por `hammer import-nix` (Etapa G). PUNTO DE PARTIDA, no final:\n\
|
||||
# - el build usa el lab de hammer (zig-cc / musl estático), NO el stdenv de nix ⇒ revisá\n\
|
||||
# compiler/link/phases y adaptá hasta que compile.\n\
|
||||
# - las deps van con su nombre NIX; remapealas a las recetas del corpus si difieren.\n\
|
||||
name = \"{name}\"\n\
|
||||
version = \"{version}\"\n\
|
||||
\n\
|
||||
[source]\n\
|
||||
{source_block}\
|
||||
\n\
|
||||
[build]\n\
|
||||
compiler = \"zig-cc\"\n\
|
||||
target = \"x86_64-linux-musl\"\n\
|
||||
link = \"static\"\n\
|
||||
flags = []\n\
|
||||
{phases_block}{deps_block}"
|
||||
);
|
||||
Ok(toml)
|
||||
}
|
||||
|
||||
/// Filtra el RUIDO de stdenv de nix que no son paquetes reales: setup-hooks, wrappers de
|
||||
/// compilador, e infraestructura de lenguaje (cargo/rustc-hooks). El humano añade lo que falte;
|
||||
/// preferimos un punto de partida limpio a arrastrar 10 hooks por receta. Conservador: sólo
|
||||
/// patrones inequívocamente internos de nix.
|
||||
fn is_nix_noise(name: &str) -> bool {
|
||||
name.contains("hook")
|
||||
|| name.ends_with("-wrapper")
|
||||
|| name == "install-shell-files"
|
||||
|| name == "auditable-cargo"
|
||||
|| name.starts_with("auditable-cargo-")
|
||||
|| name == "version-check"
|
||||
}
|
||||
|
||||
/// Nombre de receta válido: minúsculas, sólo `[a-z0-9._-]`, sin espacios. nix usa nombres limpios
|
||||
/// pero por las dudas saneamos (también recorta sufijos de output tipo `-dev`).
|
||||
fn sanitize_name(s: &str) -> String {
|
||||
let s = s.trim();
|
||||
let s = s.strip_suffix("-dev").unwrap_or(s);
|
||||
s.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | '+') { c.to_ascii_lowercase() } else { '-' })
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Convierte un hash de nix a sha256 HEX (lo que pide `Recipe.source.sha256`). Acepta:
|
||||
/// - SRI `sha256-<base64>` (formato moderno de nixpkgs),
|
||||
/// - hex de 64 chars (ya en el formato deseado),
|
||||
/// - nix-base32 de 52 chars (formato clásico de `outputHash`).
|
||||
pub fn nix_hash_to_hex(h: &str) -> Result<String, String> {
|
||||
let h = h.trim();
|
||||
// SRI: sha256-<base64>
|
||||
if let Some(b64) = h.strip_prefix("sha256-") {
|
||||
use base64::Engine;
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(b64)
|
||||
.map_err(|e| format!("SRI base64 inválido: {e}"))?;
|
||||
if bytes.len() != 32 {
|
||||
return Err(format!("SRI sha256: esperaba 32 bytes, hay {}", bytes.len()));
|
||||
}
|
||||
return Ok(hex_encode(&bytes));
|
||||
}
|
||||
// hex puro (64 chars, todos hex)
|
||||
if h.len() == 64 && h.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return Ok(h.to_ascii_lowercase());
|
||||
}
|
||||
// nix-base32 (52 chars para sha256)
|
||||
if h.len() == 52 {
|
||||
let bytes = nix_base32_decode(h)?;
|
||||
return Ok(hex_encode(&bytes));
|
||||
}
|
||||
Err(format!("formato de hash no reconocido ({} chars): {h}", h.len()))
|
||||
}
|
||||
|
||||
fn hex_encode(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for b in bytes {
|
||||
s.push_str(&format!("{b:02x}"));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Decodifica el base32 PROPIO de nix (alfabeto `0123456789abcdfghijklmnpqrsvwxyz`, sin e/o/u/t;
|
||||
/// orden LSB-first). Devuelve los 32 bytes del sha256. Port directo de `nixbase32::decode`.
|
||||
fn nix_base32_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
const ALPHABET: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz";
|
||||
let chars = s.as_bytes();
|
||||
let out_len = 32usize; // sha256
|
||||
let mut out = vec![0u8; out_len];
|
||||
for (n, &c) in chars.iter().rev().enumerate() {
|
||||
let digit = ALPHABET
|
||||
.iter()
|
||||
.position(|&a| a == c)
|
||||
.ok_or_else(|| format!("carácter no base32-nix: '{}'", c as char))?
|
||||
as u32;
|
||||
let b = n * 5;
|
||||
let i = b / 8;
|
||||
let j = (b % 8) as u32;
|
||||
out[i] |= ((digit << j) & 0xff) as u8;
|
||||
let carry = digit >> (8 - j);
|
||||
if carry != 0 {
|
||||
if i + 1 >= out_len {
|
||||
return Err("base32-nix: desbordamiento (hash mal formado)".into());
|
||||
}
|
||||
out[i + 1] |= carry as u8;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sri_hash_to_hex() {
|
||||
// sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
// SRI base64 de esos 32 bytes:
|
||||
let sri = "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
|
||||
let hex = nix_hash_to_hex(sri).unwrap();
|
||||
assert_eq!(hex, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_passthrough() {
|
||||
let h = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855";
|
||||
assert_eq!(nix_hash_to_hex(h).unwrap(), h.to_ascii_lowercase());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nixbase32_zeros_decode() {
|
||||
// 52 dígitos '0' (el primer símbolo del alfabeto) ⇒ 32 bytes cero. Caso correcto por
|
||||
// construcción que ejercita el lookup del alfabeto y el sin-desbordamiento. (El formato
|
||||
// moderno de nixpkgs es SRI, cubierto arriba; base32 es fallback para drvs clásicos.)
|
||||
let b32 = "0".repeat(52);
|
||||
let hex = nix_hash_to_hex(&b32).unwrap();
|
||||
assert_eq!(hex, "0".repeat(64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nixbase32_rejects_bad_char() {
|
||||
// 'e' no está en el alfabeto de nix (omite e/o/u/t).
|
||||
let b32 = format!("e{}", "0".repeat(51));
|
||||
assert!(nix_hash_to_hex(&b32).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_github_archive_url() {
|
||||
let raw = RawSource {
|
||||
url: "https://github.com/BurntSushi/ripgrep/archive/14.1.1.tar.gz".into(),
|
||||
output_hash: "sha256-xxx".into(),
|
||||
output_hash_mode: "recursive".into(),
|
||||
};
|
||||
assert_eq!(
|
||||
classify_source(&raw).unwrap(),
|
||||
NixSource::Github { owner: "BurntSushi".into(), repo: "ripgrep".into(), rev: "14.1.1".into() }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_recursive_non_github_errors() {
|
||||
let raw = RawSource {
|
||||
url: "https://example.com/foo.tar.gz".into(),
|
||||
output_hash: "sha256-xxx".into(),
|
||||
output_hash_mode: "recursive".into(),
|
||||
};
|
||||
assert!(classify_source(&raw).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_tarball_autotools() {
|
||||
let json = r#"{
|
||||
"pname": "hello",
|
||||
"version": "2.12.1",
|
||||
"source": { "url": "mirror://gnu/hello/hello-2.12.1.tar.gz",
|
||||
"output_hash": "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
|
||||
"output_hash_mode": "flat" },
|
||||
"configure_flags": ["--disable-nls"],
|
||||
"build_inputs": ["zlib"],
|
||||
"native_build_inputs": ["pkg-config"]
|
||||
}"#;
|
||||
let pkg: NixPkg = serde_json::from_str(json).unwrap();
|
||||
let toml = to_recipe_toml(&pkg).unwrap();
|
||||
// Parsea como Recipe válida de hammer.
|
||||
let recipe = hammer_core::Recipe::from_toml(&toml).expect("recipe válida");
|
||||
assert_eq!(recipe.name, "hello");
|
||||
assert_eq!(recipe.version, "2.12.1");
|
||||
assert_eq!(recipe.source.tarball.as_deref(), Some("mirror://gnu/hello/hello-2.12.1.tar.gz"));
|
||||
assert_eq!(
|
||||
recipe.source.sha256.as_deref(),
|
||||
Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||
);
|
||||
assert_eq!(recipe.deps.build, vec!["pkg-config", "zlib"]);
|
||||
assert!(recipe.build.phases.configure.as_deref().unwrap().contains("--disable-nls"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_nix_stdenv_noise() {
|
||||
let json = r#"{
|
||||
"pname": "x", "version": "1",
|
||||
"source": { "url": "https://github.com/o/r/archive/abc.tar.gz", "output_hash_mode": "recursive" },
|
||||
"build_inputs": ["pcre2", "cargo-build-hook.sh", "rustc", "pkg-config-wrapper"],
|
||||
"native_build_inputs": ["install-shell-files", "version-check-hook"]
|
||||
}"#;
|
||||
let pkg: NixPkg = serde_json::from_str(json).unwrap();
|
||||
let recipe = hammer_core::Recipe::from_toml(&to_recipe_toml(&pkg).unwrap()).unwrap();
|
||||
// pcre2 y rustc sobreviven (paquetes reales); los hooks/wrappers/shell-files se filtran.
|
||||
// Orden = inserción (native primero, luego build): los native son todos ruido ⇒ quedan
|
||||
// pcre2, rustc en el orden de build_inputs.
|
||||
assert_eq!(recipe.deps.build, vec!["pcre2", "rustc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_github_uses_git() {
|
||||
let json = r#"{
|
||||
"pname": "ripgrep",
|
||||
"version": "14.1.1",
|
||||
"source": { "url": "https://github.com/BurntSushi/ripgrep/archive/4649aa9700619f94cf9c66876e9549d83420e16c.tar.gz",
|
||||
"output_hash": "sha256-zzz", "output_hash_mode": "recursive" }
|
||||
}"#;
|
||||
let pkg: NixPkg = serde_json::from_str(json).unwrap();
|
||||
let toml = to_recipe_toml(&pkg).unwrap();
|
||||
let recipe = hammer_core::Recipe::from_toml(&toml).unwrap();
|
||||
assert_eq!(recipe.source.repo.as_deref(), Some("https://github.com/BurntSushi/ripgrep"));
|
||||
assert_eq!(
|
||||
recipe.source.commit.as_deref(),
|
||||
Some("4649aa9700619f94cf9c66876e9549d83420e16c")
|
||||
);
|
||||
assert!(recipe.source.sha256.is_none(), "github ⇒ pin por commit, sin sha256");
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,14 @@ CLI:
|
||||
respeta los que otro paquete instalado también aporta) y lo quita de la DB de instalados.
|
||||
- `hammer installed [--db FILE]` — lista los paquetes instalados. `install` registra cada paquete
|
||||
(nombre, versión, hash, ficheros creados) en la DB (`/var/lib/hammer/installed.json` por defecto).
|
||||
- `hammer import-nix [FILE]` — **Etapa G (poblar el catálogo desde nixpkgs):** toma el JSON
|
||||
normalizado de un paquete nix (lo produce `scripts/nix-import.sh <attr>` vía `nix eval`) y emite
|
||||
una receta hammer. Trae la RECETA (source+hash+deps+flags), NUNCA el binario del cache de nix —
|
||||
hammer reconstruye desde fuente igual ("verificar, no confiar"). `fetchurl` plano → `tarball`
|
||||
+sha256 (hash nix→hex); GitHub → `repo`+`commit`. Es un PUNTO DE PARTIDA: el build en el lab de
|
||||
hammer (zig/musl) suele necesitar adaptación por paquete. Pipeline: `nix-import.sh hello` →
|
||||
receta → `pack` → `.swm` → repo → `install` (reproduce desde fuente). Es cómo el catálogo crece
|
||||
de 34 recetas a-mano a miles sin reescribirlas.
|
||||
|
||||
## 7. Repositorio de paquetes (Etapa F)
|
||||
|
||||
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
# nix-import.sh <attr> — Etapa G: extrae de nixpkgs la "receta" de <attr> (source+hash+deps+flags,
|
||||
# NO el binario) y la imprime como receta hammer vía `hammer import-nix`. Es la semilla del catálogo:
|
||||
# nixpkgs es el mayor set de recetas desde fuente; importamos la receta y hammer reconstruye.
|
||||
#
|
||||
# Requiere `nix` (con nix-command). Ej: scripts/nix-import.sh hello > recipes/hello.toml
|
||||
# Env: NIXPKGS=<flakeref> (default nixpkgs), HAMMER=<cmd> (default cargo run -q -p hammer-cli --)
|
||||
set -eu
|
||||
|
||||
ATTR="${1:?uso: nix-import.sh <attr-de-nixpkgs> (ej: hello, jq, ripgrep)}"
|
||||
NIXPKGS="${NIXPKGS:-nixpkgs}"
|
||||
HAMMER="${HAMMER:-cargo run -q -p hammer-cli --}"
|
||||
# NIX_STORE: si el /nix/store del sistema no es escribible (install single-user incompleta), apuntá
|
||||
# a un store local, p.ej. NIX_STORE="local?root=$HOME/.nixstore".
|
||||
STORE_OPT=""
|
||||
[ -n "${NIX_STORE:-}" ] && STORE_OPT="--store ${NIX_STORE}"
|
||||
NIXFLAGS="--extra-experimental-features nix-command --extra-experimental-features flakes ${STORE_OPT}"
|
||||
|
||||
# `nix eval --apply`: aplica una función al paquete y devuelve el JSON NORMALIZADO que el
|
||||
# importador consume. Toma la URL+outputHash del `p.src` (la derivación fija del fetch) y los
|
||||
# nombres de las deps; el importador clasifica (github→git, flat→tarball) y emite la receta.
|
||||
json=$(nix eval $NIXFLAGS --json "${NIXPKGS}#${ATTR}" --apply '
|
||||
p: {
|
||||
pname = p.pname or (builtins.parseDrvName p.name).name;
|
||||
version = p.version or (builtins.parseDrvName p.name).version;
|
||||
source = {
|
||||
url = p.src.url or (if (p.src.urls or []) == [] then "" else builtins.head p.src.urls);
|
||||
output_hash = p.src.outputHash or "";
|
||||
output_hash_mode = p.src.outputHashMode or "flat";
|
||||
};
|
||||
configure_flags = p.configureFlags or [];
|
||||
build_inputs = builtins.filter (s: s != "") (map (x: x.pname or x.name or "") (p.buildInputs or []));
|
||||
native_build_inputs = builtins.filter (s: s != "") (map (x: x.pname or x.name or "") (p.nativeBuildInputs or []));
|
||||
}')
|
||||
|
||||
printf '%s' "$json" | $HAMMER import-nix -
|
||||
Reference in New Issue
Block a user