Caché persistente de zig + bootstrap reproducible del .dev-fs
- `BuildConfig.cache_root` (override `HAMMER_CACHE`): bindea `/cache` al sandbox y exporta `ZIG_GLOBAL_CACHE_DIR=/cache/zig`. Evita ~30s de recompilación de musl en cada build. - `scripts/bootstrap-devfs.sh`: idempotente, descarga+verifica alpine-minirootfs 3.23.4 y zig 0.16.0 con sha256 fijo, instala build tools en el rootfs vía apk bajo bwrap. - `make_tree_read_only`: solo archivos regulares pierden `w`; los directorios conservan 0o755 para no estorbar GC ni `rm -rf` administrativo. La inmutabilidad estricta del store se delega al mount RO de la distro propia. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
736e73e2ef
commit
e53877a532
@@ -16,12 +16,15 @@ pub struct BuildConfig {
|
|||||||
/// patch+compile, y `out` por build). Debe estar en el mismo filesystem que el store
|
/// patch+compile, y `out` por build). Debe estar en el mismo filesystem que el store
|
||||||
/// para que el `seal` final sea un rename atómico.
|
/// para que el `seal` final sea un rename atómico.
|
||||||
pub work_root: PathBuf,
|
pub work_root: PathBuf,
|
||||||
|
/// Directorio host persistente que se bindea como `/cache` en el sandbox; alberga la
|
||||||
|
/// caché global de `zig` (objetos de musl, libc, etc.) entre builds. `None` desactiva.
|
||||||
|
pub cache_root: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildConfig {
|
impl BuildConfig {
|
||||||
/// Defaults relativos al store: `.dev-fs` y `work/` como hermanos del store.
|
/// Defaults relativos al store: `.dev-fs` y `work/` como hermanos del store.
|
||||||
/// `store_root = /…/store` → rootfs = `/…/.dev-fs/alpine`, zig = `/…/.dev-fs/tools/zig`,
|
/// `store_root = /…/store` → rootfs = `/…/.dev-fs/alpine`, zig = `/…/.dev-fs/tools/zig`,
|
||||||
/// work = `/…/work`.
|
/// work = `/…/work`, cache = `/…/.dev-fs/cache`.
|
||||||
pub fn defaults_for_store(store_root: &Path) -> Self {
|
pub fn defaults_for_store(store_root: &Path) -> Self {
|
||||||
let project_root = store_root
|
let project_root = store_root
|
||||||
.parent()
|
.parent()
|
||||||
@@ -31,10 +34,12 @@ impl BuildConfig {
|
|||||||
rootfs: project_root.join(".dev-fs/alpine"),
|
rootfs: project_root.join(".dev-fs/alpine"),
|
||||||
zig_dir: project_root.join(".dev-fs/tools/zig"),
|
zig_dir: project_root.join(".dev-fs/tools/zig"),
|
||||||
work_root: project_root.join("work"),
|
work_root: project_root.join("work"),
|
||||||
|
cache_root: Some(project_root.join(".dev-fs/cache")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lee overrides desde entorno (`HAMMER_ROOTFS`, `HAMMER_ZIG`, `HAMMER_WORK`).
|
/// Lee overrides desde entorno (`HAMMER_ROOTFS`, `HAMMER_ZIG`, `HAMMER_WORK`,
|
||||||
|
/// `HAMMER_CACHE`). `HAMMER_CACHE=""` (vacío) desactiva la caché.
|
||||||
pub fn from_env_or_defaults(store_root: &Path) -> Self {
|
pub fn from_env_or_defaults(store_root: &Path) -> Self {
|
||||||
let mut cfg = Self::defaults_for_store(store_root);
|
let mut cfg = Self::defaults_for_store(store_root);
|
||||||
if let Ok(v) = std::env::var("HAMMER_ROOTFS") {
|
if let Ok(v) = std::env::var("HAMMER_ROOTFS") {
|
||||||
@@ -46,6 +51,9 @@ impl BuildConfig {
|
|||||||
if let Ok(v) = std::env::var("HAMMER_WORK") {
|
if let Ok(v) = std::env::var("HAMMER_WORK") {
|
||||||
cfg.work_root = PathBuf::from(v);
|
cfg.work_root = PathBuf::from(v);
|
||||||
}
|
}
|
||||||
|
if let Ok(v) = std::env::var("HAMMER_CACHE") {
|
||||||
|
cfg.cache_root = if v.is_empty() { None } else { Some(PathBuf::from(v)) };
|
||||||
|
}
|
||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,11 +55,15 @@ pub fn build(
|
|||||||
// monolítico que pide nuestra estrategia de hidratación primaria.
|
// monolítico que pide nuestra estrategia de hidratación primaria.
|
||||||
env.push(("LDFLAGS".into(), "-static".into()));
|
env.push(("LDFLAGS".into(), "-static".into()));
|
||||||
}
|
}
|
||||||
|
if let Some(cache) = &cfg.cache_root {
|
||||||
|
std::fs::create_dir_all(cache.join("zig"))?;
|
||||||
|
}
|
||||||
let sb = Sandbox {
|
let sb = Sandbox {
|
||||||
rootfs: cfg.rootfs.clone(),
|
rootfs: cfg.rootfs.clone(),
|
||||||
zig_dir: cfg.zig_dir.clone(),
|
zig_dir: cfg.zig_dir.clone(),
|
||||||
src_dir: src_tree.clone(),
|
src_dir: src_tree.clone(),
|
||||||
out_dir: out_dir.clone(),
|
out_dir: out_dir.clone(),
|
||||||
|
cache_dir: cfg.cache_root.clone(),
|
||||||
env,
|
env,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ pub struct Sandbox {
|
|||||||
pub zig_dir: PathBuf,
|
pub zig_dir: PathBuf,
|
||||||
pub src_dir: PathBuf,
|
pub src_dir: PathBuf,
|
||||||
pub out_dir: PathBuf,
|
pub out_dir: PathBuf,
|
||||||
|
/// Si Some, se bindea RW como `/cache` y se setea `ZIG_GLOBAL_CACHE_DIR=/cache/zig`.
|
||||||
|
/// Persistir esta caché entre builds evita que `zig cc` recompile musl cada vez (~30s).
|
||||||
|
pub cache_dir: Option<PathBuf>,
|
||||||
/// Vars de entorno extra inyectadas además del set por defecto.
|
/// Vars de entorno extra inyectadas además del set por defecto.
|
||||||
pub env: Vec<(String, String)>,
|
pub env: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
@@ -70,8 +73,14 @@ impl Sandbox {
|
|||||||
"--die-with-parent".into(),
|
"--die-with-parent".into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if let Some(cache) = &self.cache_dir {
|
||||||
|
args.push("--bind".into());
|
||||||
|
args.push(cache.display().to_string());
|
||||||
|
args.push("/cache".into());
|
||||||
|
}
|
||||||
|
|
||||||
// Entorno mínimo y predecible.
|
// Entorno mínimo y predecible.
|
||||||
let defaults: Vec<(&str, &str)> = vec![
|
let mut defaults: Vec<(&str, &str)> = vec![
|
||||||
("PATH", "/opt/zig:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"),
|
("PATH", "/opt/zig:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"),
|
||||||
("HOME", "/tmp"),
|
("HOME", "/tmp"),
|
||||||
("DESTDIR", "/out"),
|
("DESTDIR", "/out"),
|
||||||
@@ -82,6 +91,10 @@ impl Sandbox {
|
|||||||
("LC_ALL", "C"),
|
("LC_ALL", "C"),
|
||||||
("LANG", "C"),
|
("LANG", "C"),
|
||||||
];
|
];
|
||||||
|
if self.cache_dir.is_some() {
|
||||||
|
defaults.push(("ZIG_GLOBAL_CACHE_DIR", "/cache/zig"));
|
||||||
|
defaults.push(("ZIG_LOCAL_CACHE_DIR", "/cache/zig"));
|
||||||
|
}
|
||||||
for (k, v) in defaults {
|
for (k, v) in defaults {
|
||||||
args.push("--setenv".into());
|
args.push("--setenv".into());
|
||||||
args.push(k.into());
|
args.push(k.into());
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ fn build_autotools_hello_end_to_end() {
|
|||||||
rootfs: project.join(".dev-fs/alpine"),
|
rootfs: project.join(".dev-fs/alpine"),
|
||||||
zig_dir: project.join(".dev-fs/tools/zig"),
|
zig_dir: project.join(".dev-fs/tools/zig"),
|
||||||
work_root: tempfile::tempdir().unwrap().keep(),
|
work_root: tempfile::tempdir().unwrap().keep(),
|
||||||
|
cache_root: Some(project.join(".dev-fs/cache")),
|
||||||
};
|
};
|
||||||
if skip_if_no_layout(&cfg) {
|
if skip_if_no_layout(&cfg) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ fn build_hello_end_to_end() {
|
|||||||
rootfs: rootfs.clone(),
|
rootfs: rootfs.clone(),
|
||||||
zig_dir: zig_dir.clone(),
|
zig_dir: zig_dir.clone(),
|
||||||
work_root,
|
work_root,
|
||||||
|
// Reusa la caché global del dev — la primera corrida tras `cargo clean` paga
|
||||||
|
// ~30s recompilando musl; las siguientes terminan en segundos.
|
||||||
|
cache_root: Some(project.join(".dev-fs/cache")),
|
||||||
};
|
};
|
||||||
if skip_if_no_layout(&cfg) {
|
if skip_if_no_layout(&cfg) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -105,8 +105,11 @@ impl Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hace todo el árbol read-only. Archivos pierden `w` para todos los usuarios; directorios
|
/// Quita los bits de escritura de cada archivo regular del árbol. Los directorios mantienen
|
||||||
/// mantienen permisos suficientes para listarse y atravesarse, pero pierden `w`.
|
/// sus permisos originales (típicamente 0o755): eso es suficiente para garantizar
|
||||||
|
/// "contenido inmutable" sin estorbar al GC o a operaciones administrativas como `rm -rf`.
|
||||||
|
/// La inmutabilidad estricta del store entero (impedir crear archivos *nuevos* dentro de un
|
||||||
|
/// dir sellado) se aplicará vía mount read-only en el track de distro propia.
|
||||||
fn make_tree_read_only(root: &Path) -> crate::Result<()> {
|
fn make_tree_read_only(root: &Path) -> crate::Result<()> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
@@ -114,18 +117,13 @@ fn make_tree_read_only(root: &Path) -> crate::Result<()> {
|
|||||||
let meta = std::fs::symlink_metadata(path)?;
|
let meta = std::fs::symlink_metadata(path)?;
|
||||||
let ft = meta.file_type();
|
let ft = meta.file_type();
|
||||||
if ft.is_symlink() {
|
if ft.is_symlink() {
|
||||||
// No tocamos symlinks (chmod sigue el link en muchas plataformas; no merece la pena).
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if ft.is_dir() {
|
if ft.is_dir() {
|
||||||
for entry in std::fs::read_dir(path)? {
|
for entry in std::fs::read_dir(path)? {
|
||||||
walk(&entry?.path())?;
|
walk(&entry?.path())?;
|
||||||
}
|
}
|
||||||
// Quitar bits de escritura del directorio (lectura+ejec sí, para poder atravesar).
|
} else if ft.is_file() {
|
||||||
let mut perms = meta.permissions();
|
|
||||||
perms.set_mode(perms.mode() & !0o222);
|
|
||||||
std::fs::set_permissions(path, perms)?;
|
|
||||||
} else {
|
|
||||||
let mut perms = meta.permissions();
|
let mut perms = meta.permissions();
|
||||||
perms.set_mode(perms.mode() & !0o222);
|
perms.set_mode(perms.mode() & !0o222);
|
||||||
std::fs::set_permissions(path, perms)?;
|
std::fs::set_permissions(path, perms)?;
|
||||||
|
|||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bootstrap-devfs.sh — Prepara .dev-fs/ con todo lo que hammer-build necesita:
|
||||||
|
# 1. Rootfs Alpine minirootfs como base hermética del sandbox.
|
||||||
|
# 2. Compilador zig oficial bajo .dev-fs/tools/zig (symlink versionado).
|
||||||
|
# 3. Build tools (make, autoconf, automake, m4, patch, coreutils, libtool,
|
||||||
|
# pkgconf, bash) instaladas dentro del rootfs con apk.
|
||||||
|
# 4. Directorios de caché y work para builds posteriores.
|
||||||
|
#
|
||||||
|
# Idempotente: cada paso se salta si ya hizo. Reinvocar tras un cambio de
|
||||||
|
# versión actualiza in-place.
|
||||||
|
#
|
||||||
|
# Variables (override por entorno):
|
||||||
|
# ALPINE_BRANCH rama de Alpine (default v3.23)
|
||||||
|
# ALPINE_VER versión exacta del tarball (default 3.23.4)
|
||||||
|
# ALPINE_SHA256 sha256 del tarball (verificado obligatorio)
|
||||||
|
# ZIG_VER versión de zig (default 0.16.0)
|
||||||
|
# ZIG_SHA256 sha256 del tarball zig
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# ./scripts/bootstrap-devfs.sh # bootstrap completo
|
||||||
|
# ./scripts/bootstrap-devfs.sh --skip-apk # sólo rootfs + zig
|
||||||
|
# ./scripts/bootstrap-devfs.sh --force-zig # re-descarga zig
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ALPINE_BRANCH="${ALPINE_BRANCH:-v3.23}"
|
||||||
|
ALPINE_VER="${ALPINE_VER:-3.23.4}"
|
||||||
|
ALPINE_SHA256="${ALPINE_SHA256:-85498865362aa7ebececa0d725a2f2e4db7ac4e4b2850b8df21645afa0d03ee3}"
|
||||||
|
ZIG_VER="${ZIG_VER:-0.16.0}"
|
||||||
|
ZIG_SHA256="${ZIG_SHA256:-70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00}"
|
||||||
|
|
||||||
|
SKIP_APK=0
|
||||||
|
FORCE_ZIG=0
|
||||||
|
FORCE_ROOTFS=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--skip-apk) SKIP_APK=1 ;;
|
||||||
|
--force-zig) FORCE_ZIG=1 ;;
|
||||||
|
--force-rootfs) FORCE_ROOTFS=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
|
||||||
|
exit 0 ;;
|
||||||
|
*)
|
||||||
|
echo "argumento desconocido: $arg" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
DEVFS="$REPO_ROOT/.dev-fs"
|
||||||
|
ROOTFS="$DEVFS/alpine"
|
||||||
|
TOOLS="$DEVFS/tools"
|
||||||
|
ZIG_DIR="$TOOLS/zig-x86_64-linux-$ZIG_VER"
|
||||||
|
ZIG_LINK="$TOOLS/zig"
|
||||||
|
CACHE="$DEVFS/cache"
|
||||||
|
WORK="$REPO_ROOT/work"
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||||
|
ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
|
||||||
|
|
||||||
|
mkdir -p "$DEVFS" "$TOOLS" "$CACHE" "$WORK"
|
||||||
|
|
||||||
|
# --- 1. Rootfs Alpine ---------------------------------------------------------
|
||||||
|
if [[ $FORCE_ROOTFS -eq 1 ]] && [[ -d "$ROOTFS" ]]; then
|
||||||
|
log "rootfs: --force-rootfs ⇒ borro $ROOTFS"
|
||||||
|
rm -rf "$ROOTFS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$ROOTFS/bin/busybox" ]]; then
|
||||||
|
ok "rootfs Alpine ya presente en $ROOTFS"
|
||||||
|
else
|
||||||
|
log "rootfs: descargando alpine-minirootfs $ALPINE_VER"
|
||||||
|
tar="$DEVFS/alpine-minirootfs-$ALPINE_VER.tar.gz"
|
||||||
|
url="https://dl-cdn.alpinelinux.org/alpine/$ALPINE_BRANCH/releases/x86_64/alpine-minirootfs-$ALPINE_VER-x86_64.tar.gz"
|
||||||
|
curl -fsSL "$url" -o "$tar"
|
||||||
|
echo "$ALPINE_SHA256 $tar" | sha256sum -c -
|
||||||
|
mkdir -p "$ROOTFS"
|
||||||
|
tar -xzf "$tar" -C "$ROOTFS"
|
||||||
|
rm -f "$tar"
|
||||||
|
ok "rootfs Alpine $ALPINE_VER extraído"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2. Compilador zig --------------------------------------------------------
|
||||||
|
if [[ $FORCE_ZIG -eq 1 ]] && [[ -e "$ZIG_DIR" ]]; then
|
||||||
|
log "zig: --force-zig ⇒ borro $ZIG_DIR"
|
||||||
|
rm -rf "$ZIG_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -x "$ZIG_LINK/zig" ]]; then
|
||||||
|
ok "zig $ZIG_VER ya presente en $ZIG_LINK"
|
||||||
|
else
|
||||||
|
log "zig: descargando $ZIG_VER"
|
||||||
|
tar="$TOOLS/zig.tar.xz"
|
||||||
|
url="https://ziglang.org/download/$ZIG_VER/zig-x86_64-linux-$ZIG_VER.tar.xz"
|
||||||
|
curl -fsSL "$url" -o "$tar"
|
||||||
|
echo "$ZIG_SHA256 $tar" | sha256sum -c -
|
||||||
|
tar -xJf "$tar" -C "$TOOLS"
|
||||||
|
rm -f "$tar"
|
||||||
|
ln -sfn "zig-x86_64-linux-$ZIG_VER" "$ZIG_LINK"
|
||||||
|
ok "zig $ZIG_VER instalado, symlink $ZIG_LINK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3. Build tools en el rootfs (apk add) ------------------------------------
|
||||||
|
if [[ $SKIP_APK -eq 1 ]]; then
|
||||||
|
log "apk: --skip-apk ⇒ saltando build tools"
|
||||||
|
else
|
||||||
|
NEEDED="make autoconf automake m4 patch coreutils libtool pkgconf bash"
|
||||||
|
missing=""
|
||||||
|
for pkg in $NEEDED; do
|
||||||
|
# apk info -e devuelve el paquete si está instalado, vacío si no.
|
||||||
|
if ! bwrap --bind "$ROOTFS" / --proc /proc --dev /dev --unshare-all \
|
||||||
|
--setenv PATH /usr/bin:/usr/sbin:/bin:/sbin \
|
||||||
|
/sbin/apk info -e "$pkg" >/dev/null 2>&1; then
|
||||||
|
missing="$missing $pkg"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "$missing" ]]; then
|
||||||
|
log "apk: instalando paquetes faltantes:$missing"
|
||||||
|
bwrap --bind "$ROOTFS" / --proc /proc --dev /dev \
|
||||||
|
--ro-bind /etc/resolv.conf /etc/resolv.conf --share-net \
|
||||||
|
/sbin/apk update >/dev/null
|
||||||
|
bwrap --bind "$ROOTFS" / --proc /proc --dev /dev \
|
||||||
|
--ro-bind /etc/resolv.conf /etc/resolv.conf --share-net \
|
||||||
|
/sbin/apk add $missing
|
||||||
|
fi
|
||||||
|
ok "build tools presentes en el rootfs ($NEEDED)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 4. Resumen ---------------------------------------------------------------
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
bootstrap-devfs.sh — OK
|
||||||
|
rootfs: $ROOTFS
|
||||||
|
zig: $ZIG_LINK ($("$ZIG_LINK/zig" version))
|
||||||
|
cache: $CACHE
|
||||||
|
work: $WORK
|
||||||
|
|
||||||
|
Variables que la CLI ya recoge por defecto (override con env vars):
|
||||||
|
HAMMER_ROOTFS = $ROOTFS
|
||||||
|
HAMMER_ZIG = $ZIG_LINK
|
||||||
|
HAMMER_CACHE = $CACHE # vacío ("") desactiva la caché
|
||||||
|
HAMMER_WORK = $WORK
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user