[pitboss] phase 19: Track E.3 — Docker backend + nyx-image-builder + pinned digests

This commit is contained in:
pitboss 2026-05-15 11:03:31 -05:00
parent 6ca9bddedb
commit 7ca0c053f5
9 changed files with 1412 additions and 0 deletions

141
build.rs
View file

@ -9,6 +9,12 @@ fn main() {
// the file (the include never actually compiles on non-Linux).
emit_seccomp_policy();
// Phase 19 (Track E.3): emit the IMAGE_DIGESTS table from
// tools/image-builder/images.toml. The runtime side (src/dynamic/
// toolchain.rs) `include!`s the generated file unconditionally so
// every host build has the same pinned-digest catalogue.
emit_image_digests();
// Only relevant when the serve feature is active.
if std::env::var("CARGO_FEATURE_SERVE").is_err() {
return;
@ -283,3 +289,138 @@ fn store_allow(policy: &mut SeccompPolicy, section: Option<&str>, key: &str, val
fn escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
// ── Phase 19 (Track E.3) — image digest codegen ──────────────────────────────
const IMAGE_CATALOGUE_PATH: &str = "tools/image-builder/images.toml";
/// Parse `tools/image-builder/images.toml` and emit two tables to
/// `$OUT_DIR/image_digests.rs`:
///
/// pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = …;
/// pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = …;
///
/// `IMAGE_DIGESTS` keys are toolchain IDs (`python-3.11`, …) and values are
/// `<base>@sha256:…` strings ready to hand to `docker pull`. An empty digest
/// in `images.toml` is treated as "not yet pinned" and the entry is omitted
/// from `IMAGE_DIGESTS`; `IMAGE_BASES` always carries the unpinned reference
/// so `docker.rs` can fall back to a tag pull when no digest is recorded.
fn emit_image_digests() {
println!("cargo:rerun-if-changed={}", IMAGE_CATALOGUE_PATH);
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR must be set by cargo");
let out_path = Path::new(&out_dir).join("image_digests.rs");
let toml_text = match std::fs::read_to_string(IMAGE_CATALOGUE_PATH) {
Ok(s) => s,
Err(_) => {
// Missing catalogue (fresh checkout without the file) — emit
// empty maps so the runtime include still compiles.
std::fs::write(
&out_path,
"/// generated empty IMAGE_DIGESTS — images.toml missing\n\
pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = \
phf::phf_map! {};\n\
pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = \
phf::phf_map! {};\n",
)
.expect("write empty image digests stub");
return;
}
};
let entries = parse_image_catalogue(&toml_text);
let mut out = String::new();
out.push_str("// generated by build.rs from tools/image-builder/images.toml — do not edit\n\n");
// IMAGE_DIGESTS: only entries with a non-empty digest survive.
out.push_str("pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = phf::phf_map! {\n");
for e in &entries {
if e.digest.is_empty() {
continue;
}
let pinned = format!("{}@{}", e.base, e.digest);
out.push_str(&format!(
" \"{}\" => \"{}\",\n",
escape(&e.toolchain_id),
escape(&pinned),
));
}
out.push_str("};\n\n");
// IMAGE_BASES: every entry, digest stripped. Used by docker.rs when no
// digest is pinned yet so a `docker pull <base>` is still possible.
out.push_str("pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = phf::phf_map! {\n");
for e in &entries {
out.push_str(&format!(
" \"{}\" => \"{}\",\n",
escape(&e.toolchain_id),
escape(&e.base),
));
}
out.push_str("};\n");
std::fs::write(&out_path, out).expect("write image_digests.rs");
}
#[derive(Default)]
struct ImageEntry {
toolchain_id: String,
base: String,
digest: String,
}
/// Tiny TOML parser scoped to the `[[image]] toolchain_id = …` shape used
/// by `images.toml`. Only the three fields we consume here are extracted;
/// the rest of each entry (`toolchain`, `packages`) is ignored.
fn parse_image_catalogue(src: &str) -> Vec<ImageEntry> {
let mut entries: Vec<ImageEntry> = Vec::new();
let mut current: Option<ImageEntry> = None;
for raw_line in src.lines() {
let line = strip_comment(raw_line).trim();
if line.is_empty() {
continue;
}
if line == "[[image]]" {
if let Some(prev) = current.take() {
if !prev.toolchain_id.is_empty() {
entries.push(prev);
}
}
current = Some(ImageEntry::default());
continue;
}
if line.starts_with("[[") || line.starts_with('[') {
// Any other section ends accumulation.
if let Some(prev) = current.take() {
if !prev.toolchain_id.is_empty() {
entries.push(prev);
}
}
continue;
}
let Some(slot) = current.as_mut() else { continue };
let Some((key, value)) = line.split_once('=') else { continue };
let key = key.trim();
let value = value.trim().trim_matches('"').trim_matches('\'');
match key {
"toolchain_id" => slot.toolchain_id = value.to_owned(),
"base" => slot.base = value.to_owned(),
"digest" => slot.digest = value.to_owned(),
_ => {}
}
}
if let Some(prev) = current.take() {
if !prev.toolchain_id.is_empty() {
entries.push(prev);
}
}
entries
}