Etapa G capa-de-adaptación #3: traducir mirror:// de nix a URLs concretas

nix resuelve `mirror://<sitio>/...` en eval; un import los deja literales y el curl de hammer no
los entiende. expand_nix_mirror() mapea los comunes (gnu/savannah/kernel/sourceforge/gnome/
apache/xorg/pypi/cpan/debian) a un espejo real; lo no mapeado se deja igual. +1 test.
Validado: import hello → tarball https://ftp.gnu.org/gnu/hello/... (antes mirror://gnu/...).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 08:20:08 -04:00
co-authored by Claude Opus 4.8
parent 8e6c878d3a
commit b66cae91ae
+46 -2
View File
@@ -63,7 +63,7 @@ pub fn classify_source(raw: &RawSource) -> Result<NixSource, String> {
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 })
Ok(NixSource::Tarball { url: expand_nix_mirror(&raw.url), sha256: hex })
}
"recursive" => Err(format!(
"source recursivo no-github (hash NAR no reusable): {} — fijá repo+commit a mano",
@@ -73,6 +73,33 @@ pub fn classify_source(raw: &RawSource) -> Result<NixSource, String> {
}
}
/// Traduce el esquema `mirror://<sitio>/...` de nix a una URL concreta. nix resuelve estos
/// mirrors en tiempo de eval; un import los deja literales y el fetch de hammer (curl) no los
/// entiende. Mapeamos los más comunes a un mirror real; lo no mapeado se deja igual + el usuario
/// elige espejo. (nixpkgs tiene la lista completa en `pkgs/build-support/fetchurl/mirrors.nix`.)
fn expand_nix_mirror(url: &str) -> String {
let Some(rest) = url.strip_prefix("mirror://") else {
return url.to_string();
};
let Some((site, path)) = rest.split_once('/') else {
return url.to_string();
};
let base = match site {
"gnu" => "https://ftp.gnu.org/gnu/",
"savannah" => "https://download.savannah.gnu.org/releases/",
"kernel" => "https://www.kernel.org/pub/",
"sourceforge" => "https://downloads.sourceforge.net/",
"gnome" => "https://download.gnome.org/",
"apache" => "https://dlcdn.apache.org/",
"xorg" => "https://www.x.org/releases/",
"pypi" => "https://files.pythonhosted.org/packages/",
"cpan" => "https://cpan.metacpan.org/",
"debian" => "https://deb.debian.org/debian/",
_ => return url.to_string(), // desconocido: dejamos el mirror:// (el humano resuelve)
};
format!("{base}{path}")
}
/// 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>`.
@@ -335,7 +362,8 @@ mod tests {
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"));
// mirror://gnu/ se traduce a una URL concreta que el fetch de hammer entiende.
assert_eq!(recipe.source.tarball.as_deref(), Some("https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz"));
assert_eq!(
recipe.source.sha256.as_deref(),
Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
@@ -360,6 +388,22 @@ mod tests {
assert_eq!(recipe.deps.build, vec!["pcre2", "rustc"]);
}
#[test]
fn expands_common_nix_mirrors() {
assert_eq!(
expand_nix_mirror("mirror://gnu/hello/hello-2.12.1.tar.gz"),
"https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz"
);
assert_eq!(
expand_nix_mirror("mirror://savannah/foo/foo-1.tar.gz"),
"https://download.savannah.gnu.org/releases/foo/foo-1.tar.gz"
);
// desconocido se deja igual (no rompe, el humano resuelve).
assert_eq!(expand_nix_mirror("mirror://raro/x"), "mirror://raro/x");
// url normal pasa sin tocar.
assert_eq!(expand_nix_mirror("https://x/y.tar.gz"), "https://x/y.tar.gz");
}
#[test]
fn import_github_uses_git() {
let json = r#"{