bootstrap: builder rootfs — fixes destapados al armarlo con caché real
Al construir el hammer estático musl y armar el builder con --work-cache work, dos problemas reales (cazados antes de llenar el disco): 1. Recursión. --work-cache work --out work/builder-rootfs deja el destino DENTRO de la caché: copiar la caché se tragaba el propio builder a medio armar (work/builder-rootfs/work/builder-rootfs/… sin fondo). Fix: link_or_copy_tree toma un `skip` (path canónico a no descender), que corta el ciclo. 2. Bloat de sources. --work-cache copiaba TODO work/, incluidos los árboles ya materializados de work/sources (4 GB, regenerables). Fix: sólo se embeben work/repos (mirrors git) y work/tarballs (descargas verificadas); de ahí `fetch` rematerializa los sources en la VM. El builder pasa de ~5 GB a ~750 MB. +2 tests (recursión termina y no se auto-copia; sources/ no viaja). Runbook §8c aclara la semántica de --work-cache. El hammer estático musl (target/x86_64-unknown-linux-musl/release/hammer) sale static-pie linked, listo para bootear como PID-algo en la VM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f17a8fa301
commit
83d4b2aa17
@@ -595,7 +595,16 @@ fn recipes_digest(dir: &Path) -> Result<String> {
|
||||
/// Copia recursiva `src`→`dst` preferendo **hardlink** (mismo filesystem, sin coste de bytes) y
|
||||
/// cayendo a copia cuando cruza filesystems. Recrea symlinks tal cual. No hace `chmod`: el builder
|
||||
/// no se sella, así que los hardlinks no deben tocar permisos del origen (p. ej. el `.dev-fs`).
|
||||
fn link_or_copy_tree(src: &Path, dst: &Path) -> Result<()> {
|
||||
///
|
||||
/// `skip` es un path a no descender: imprescindible cuando `dst` vive **dentro** de `src` (p. ej.
|
||||
/// `--work-cache work --out work/builder-rootfs`), donde copiar `src` ciegamente se tragaría el
|
||||
/// propio destino en una recursión infinita. Se compara por path canónico.
|
||||
fn link_or_copy_tree(src: &Path, dst: &Path, skip: Option<&Path>) -> Result<()> {
|
||||
if let Some(skip) = skip {
|
||||
if src.canonicalize().ok().as_deref() == Some(skip) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let meta = std::fs::symlink_metadata(src)?;
|
||||
let ft = meta.file_type();
|
||||
if ft.is_symlink() {
|
||||
@@ -606,7 +615,7 @@ fn link_or_copy_tree(src: &Path, dst: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(dst)?;
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
link_or_copy_tree(&entry.path(), &dst.join(entry.file_name()))?;
|
||||
link_or_copy_tree(&entry.path(), &dst.join(entry.file_name()), skip)?;
|
||||
}
|
||||
} else if ft.is_file() && std::fs::hard_link(src, dst).is_err() {
|
||||
std::fs::copy(src, dst)?;
|
||||
@@ -641,7 +650,7 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
|
||||
spec.toolchain_src.display()
|
||||
)));
|
||||
}
|
||||
link_or_copy_tree(&spec.toolchain_src, &staging.join("toolchain"))?;
|
||||
link_or_copy_tree(&spec.toolchain_src, &staging.join("toolchain"), None)?;
|
||||
|
||||
// 4) Semilla → /store/<hash>-seed-<kind>, tal cual está sellada afuera, para que el `hammer` de
|
||||
// adentro resuelva el toolchain por hash sin re-ingerirla.
|
||||
@@ -657,6 +666,7 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
|
||||
link_or_copy_tree(
|
||||
&seed_dir,
|
||||
&staging.join("store").join(spec.seed_hash.store_dir_name(&seed_name)),
|
||||
None,
|
||||
)?;
|
||||
|
||||
// 5) Binario hammer → /usr/bin/hammer (+x).
|
||||
@@ -675,10 +685,17 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
|
||||
}
|
||||
}
|
||||
|
||||
// 7) Caché de fuentes → /work (opcional, para rebuild offline).
|
||||
// 7) Caché de fetch → /work (opcional, para rebuild offline). Sólo `repos/` (mirrors git) y
|
||||
// `tarballs/` (descargas verificadas): de ahí `fetch` rematerializa los árboles fuente. NO se
|
||||
// copia `sources/` (los árboles ya materializados) — es regenerable y pesado (varios GB). El
|
||||
// `skip = staging` es defensa extra contra `--work-cache work --out work/builder-rootfs`.
|
||||
if let Some(cache) = &spec.work_cache {
|
||||
if cache.is_dir() {
|
||||
link_or_copy_tree(cache, &staging.join("work"))?;
|
||||
let skip = staging.canonicalize().ok();
|
||||
for sub in ["repos", "tarballs"] {
|
||||
let src = cache.join(sub);
|
||||
if src.is_dir() {
|
||||
link_or_copy_tree(&src, &staging.join("work").join(sub), skip.as_deref())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1300,6 +1317,38 @@ mod tests {
|
||||
assert!(env.contains("REF_CONTENT=\n"), "sin referencia ⇒ REF_CONTENT vacío: {env:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_rootfs_with_work_cache_containing_out_dir_terminates() {
|
||||
// Regresión: --work-cache work --out work/builder-rootfs. Copiar la caché no debe tragarse
|
||||
// el propio destino (recursión infinita que llena el disco). El skip lo corta.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (store, mut spec) = builder_fixture(tmp.path());
|
||||
|
||||
// Caché de fetch con un mirror sintético + un árbol sources/ regenerable (no debe viajar).
|
||||
let work = tmp.path().join("work");
|
||||
std::fs::create_dir_all(work.join("repos/musl.git")).unwrap();
|
||||
std::fs::write(work.join("repos/musl.git/HEAD"), b"ref: refs/heads/master\n").unwrap();
|
||||
std::fs::create_dir_all(work.join("sources/musl-abc")).unwrap();
|
||||
std::fs::write(work.join("sources/musl-abc/configure"), b"#!/bin/sh\n").unwrap();
|
||||
spec.work_cache = Some(work.clone());
|
||||
|
||||
// out_dir vive DENTRO de la caché: el caso que disparaba la recursión.
|
||||
let out = work.join("builder-rootfs");
|
||||
let report = builder_rootfs(&spec, &store, &out).unwrap();
|
||||
assert!(report.builder_hash.as_str().starts_with("b3:"));
|
||||
|
||||
// Sólo viajan los mirrors; el destino NO se copió en sí mismo; sources/ NO se embebe.
|
||||
assert!(out.join("work/repos/musl.git/HEAD").is_file(), "los mirrors git se embeben");
|
||||
assert!(
|
||||
!out.join("work/builder-rootfs").exists(),
|
||||
"el out_dir no debe haberse copiado en su propio /work (recursión)",
|
||||
);
|
||||
assert!(
|
||||
!out.join("work/sources").exists(),
|
||||
"sources/ es regenerable y pesado: no debe viajar en el builder",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_rootfs_errors_when_stage1_unsealed() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user