Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

src/code_room/products.rs

//! BUILDER 2 on the server: a finished Build Thing run lands in the
//! workspace's Mímir.
//!
//! The run's record already reaches the server: `Telemetry.SubmitBuildProvenance`
//! files the `BuildProvenance` (warehouse `build_provenance`), and
//! `Telemetry.SubmitBuildInfo` its §f3 rows (warehouse `build_info`), among them
//! the LINEAGE rows the forge adds at filing time ([`COMMIT_PREFIX`],
//! [`DEP_CRATE`], [`REPO_ROOT`]). When the rows of a successful build arrive the
//! server queues ONE jera job (the workspace's `rooms` lane) that
//!
//! 1. reads both back and makes the run's [`ProductFacts`] ([`facts_of`]);
//! 2. writes them through the consumer's seat ([`CodeRoom::land_products`]):
//!    artifact node + BUILT_FROM edges + provenance in the binary stream's
//!    tables (`mimir_ingest::product`);
//! 3. the BINARY HALF: every artifact readable on this box is analysed exactly
//!    as `nornir bin analyze` does and persisted to the same warehouse tables
//!    (`binary_facts` / `section_sizes` / `symbol_sizes`), keyed by the content
//!    sha256 the artifact node's id carries ([`binary_half`]).
//!
//! The read: `Coderoom.Artifacts` ([`answer`]) — which artifacts were built
//! from crate X / commit Y.

use std::path::{Path, PathBuf};

use anyhow::{Result, bail};
use mimir_ingest::{ArtifactQuery, BinRunRow, ProductArtifact, ProductFacts};
use nornir_build_thing::{BuildInfoRow, BuildProvenance};
use serde_json::{Value, json};

use super::{CodeRoom, Refusal, State};

/// `build_info` provenance row: `commit:<repo>` = the sha the build read of
/// that repo (the recipe's own and every sibling a path dependency lives in).
pub const COMMIT_PREFIX: &str = "commit:";
/// `build_info` dep row name: `name@version` of one crate of the build's
/// dependency closure.
pub const DEP_CRATE: &str = "crate";
/// `build_info` provenance row: the builder's absolute checkout of the recipe
/// repo (artifact paths in the record are relative to it).
pub const REPO_ROOT: &str = "repo_root";

/// Does this batch of `build_info` rows close a SUCCESSFUL build? (its manifest
/// says `outcome = success`): the builds the server lands in Mímir.
pub fn landed_builds(rows: &[BuildInfoRow]) -> Vec<String> {
    let mut ids: Vec<String> = rows
        .iter()
        .filter(|r| r.kind == "provenance" && r.name == "outcome" && r.value == "success")
        .map(|r| r.build_id.clone())
        .collect();
    ids.sort();
    ids.dedup();
    ids
}

/// The run's facts from its record + its `build_info` rows.
pub fn facts_of(record: &BuildProvenance, info: &[BuildInfoRow]) -> Result<ProductFacts> {
    if record.outcome != nornir_build_thing::provenance::Outcome::Success {
        bail!("build {} did not succeed: no product to land", record.id);
    }
    let mine = || info.iter().filter(|r| r.build_id == record.id);
    let mut commits: std::collections::BTreeMap<String, String> = mine()
        .filter(|r| r.kind == "provenance")
        .filter_map(|r| r.name.strip_prefix(COMMIT_PREFIX).map(|repo| (repo.to_string(), r.value.clone())))
        .filter(|(_, sha)| !sha.is_empty())
        .collect();
    if !record.inputs.vcs_rev.is_empty() && record.inputs.vcs_rev != "unknown" {
        commits.entry(record.repo.clone()).or_insert_with(|| record.inputs.vcs_rev.clone());
    }
    let mut crates: Vec<(String, String)> = mine()
        .filter(|r| r.kind == "dep" && r.name == DEP_CRATE)
        .filter_map(|r| r.value.rsplit_once('@').map(|(n, v)| (n.to_string(), v.to_string())))
        .collect();
    crates.sort();
    crates.dedup();
    let artifacts: Vec<ProductArtifact> = record
        .artifacts
        .iter()
        .filter(|a| !a.sha256.is_empty())
        .map(|a| ProductArtifact {
            file: Path::new(&a.path).file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_else(|| a.path.clone()),
            bytes: a.bytes,
            sha256: a.sha256.clone(),
        })
        .collect();
    if artifacts.is_empty() {
        bail!("build {} recorded no artifact", record.id);
    }
    Ok(ProductFacts {
        build_id: record.id.clone(),
        repo: record.repo.clone(),
        variant: record.variant.name.clone(),
        package: record.variant.package.clone(),
        features: record.inputs.features.clone(),
        profile: record.inputs.profile.clone(),
        triple: record.inputs.target.clone().unwrap_or_else(|| record.toolchain.host.clone()),
        toolchain: if record.toolchain.rustc.is_empty() { record.toolchain.image.clone().unwrap_or_default() } else { record.toolchain.rustc.clone() },
        builder: record.builder.clone(),
        built_at: record.built_at.clone(),
        recipe: record.build_thing.as_ref().map(ToString::to_string),
        commits,
        crates,
        artifacts,
        record: serde_json::to_value(record).ok(),
    })
}

/// Where the record's artifacts are on THIS box: `(path, sha256)` of every one
/// that is readable here and still has the recorded bytes (the builder's
/// [`REPO_ROOT`] joins a relative path). A build on another box has none.
pub fn local_artifacts(record: &BuildProvenance, info: &[BuildInfoRow]) -> Vec<(PathBuf, String)> {
    let root = info.iter().find(|r| r.build_id == record.id && r.kind == "provenance" && r.name == REPO_ROOT).map(|r| PathBuf::from(&r.value));
    record
        .artifacts
        .iter()
        .filter_map(|a| {
            let p = Path::new(&a.path);
            let p = if p.is_absolute() { p.to_path_buf() } else { root.as_ref()?.join(p) };
            let same = std::fs::metadata(&p).is_ok_and(|m| m.is_file() && m.len() == a.bytes);
            same.then(|| (p, a.sha256.clone()))
        })
        .collect()
}

/// The binary half: `nornir bin analyze` over every local artifact, persisted to
/// `binary_facts` / `section_sizes` / `symbol_sizes` under its content sha256
/// (the analysis of bytes that changed since filing is skipped, by name).
pub fn binary_half(wh: &crate::warehouse::iceberg::IcebergWarehouse, record: &BuildProvenance, info: &[BuildInfoRow]) -> Vec<Value> {
    let mut out = Vec::new();
    for (path, sha) in local_artifacts(record, info) {
        let r = (|| -> Result<Value> {
            let now = crate::binscan::content_sha256(&path)?;
            if now != sha {
                bail!("{} changed since the build filed it (sha256 {now}, filed {sha})", path.display());
            }
            // the same bytes are the same analysis: a build filed twice (its
            // record and its rows each queue the landing), or the same bytes
            // built again, append no second copy of the rows
            if let Some(h) = wh.query_binary_facts(&sha)? {
                return Ok(json!({ "artifact": path.display().to_string(), "sha256": sha, "already_analysed": true, "total_bytes": h.total_bytes }));
            }
            let report = crate::binscan::analyze(&path, None)?;
            let snap = crate::binscan::persist_report(wh, &report, &record.repo, &record.inputs.vcs_rev, &sha, &path.display().to_string())?;
            Ok(json!({
                "artifact": path.display().to_string(),
                "sha256": sha,
                "snapshot": snap.to_string(),
                "total_bytes": report.header.total_bytes,
                "sections": report.sections.len(),
                "symbols": report.symbols.len(),
                "crates": report.crates.len(),
            }))
        })();
        out.push(r.unwrap_or_else(|e| json!({ "artifact": path.display().to_string(), "error": format!("{e:#}") })));
    }
    out
}

impl CodeRoom {
    /// Land one Build Thing run's facts in this workspace's lake, through the
    /// consumer's seat (it waits for a run in flight; readers never wait).
    pub fn land_products(&self, facts: &ProductFacts) -> Result<Vec<BinRunRow>, Refusal> {
        let landed = self.with_consumer(|log, consumer| {
            log.block_on(consumer.ingest_products(facts))
                .map_err(|e| Refusal::new(State::Internal, format!("code room of `{}`: build {} did not land: {e:#}", self.workspace, facts.build_id)))
        })?;
        // `Viz.Binary` and the lenses that read the built artifacts list them,
        // and a build does not move the room's version: their kept answers are
        // of the room without this build
        self.answers().forget("Viz.Binary");
        for lens in crate::mimir_lens::BUILD_FED {
            self.answers().forget(&format!("Mimir.{lens}"));
        }
        Ok(landed)
    }
}

/// **Builder 2's job**, whole: land one finished Build Thing run (its
/// `build_provenance` record + its `build_info` rows) in the workspace's
/// Mímir — the artifact node, BUILT_FROM and provenance through the consumer's
/// seat ([`CodeRoom::land_products`]), then the binary half ([`binary_half`]).
/// This is THE producer of everything the binary lenses read
/// (`generic_cost`, `binary_drift`, `zombie_features`' bytes).
pub fn land_build(room: &CodeRoom, wh: &crate::warehouse::iceberg::IcebergWarehouse, record: &BuildProvenance, rows: &[BuildInfoRow]) -> Result<Value> {
    let facts = facts_of(record, rows)?;
    let landed = room.land_products(&facts).map_err(|r| anyhow::anyhow!("[{}] {r}", r.state))?;
    let binary = binary_half(wh, record, rows);
    // the binary half lands after the products: what a lens kept in between
    // read the artifact without its sizes
    for lens in crate::mimir_lens::BUILD_FED {
        room.answers().forget(&format!("Mimir.{lens}"));
    }
    Ok(json!({
        "build_id": record.id,
        "repo": facts.repo,
        "variant": facts.variant,
        "artifacts": landed.iter().map(|r| json!({ "owner": r.artifact, "rev": r.rev, "plus": r.rows_plus, "status": r.status })).collect::<Vec<_>>(),
        "crates": facts.crates.len(),
        "commits": facts.commits,
        "binary_half": binary,
    }))
}

/// `Coderoom.Artifacts` — `{ "crate": "name[@version]", "commit": "[repo@]sha",
/// "variant": "…" }` (any subset) → the matching built artifacts. It reads the
/// products, not a room: it answers before any member is consumed (a build can
/// land first), `empty_fact` when nothing matches.
pub fn answer(room: &CodeRoom, req: &Value) -> Result<Value, Refusal> {
    let s = |k: &str| req.get(k).and_then(Value::as_str).map(str::trim).filter(|v| !v.is_empty()).map(str::to_string);
    let q = ArtifactQuery { krate: s("crate"), commit: s("commit"), variant: s("variant") };
    if q.commit.as_deref().is_some_and(|c| c.rsplit('@').next().is_some_and(|sha| sha.len() < 7)) {
        return Err(Refusal::new(State::InvalidArgument, "`commit` needs at least 7 hex digits of the sha"));
    }
    let log = room.log()?;
    let hits = log
        .block_on(async {
            let pinned = log.pin().await?;
            mimir_ingest::product::artifacts(&pinned, &q).await
        })
        .map_err(|e| Refusal::new(State::Internal, format!("read the built artifacts: {e:#}")))?;
    Ok(json!({
        "surface": "Coderoom.Artifacts",
        "state": if hits.is_empty() { "empty_fact" } else { "complete" },
        "query": q,
        "count": hits.len(),
        "artifacts": hits,
        "evidence": { "notes": [
            "builder 2: every artifact a successful Build Thing run filed with this server, from the binary stream's tables under `@build-thing` (artifact node + BUILT_FROM edges)",
            "crates = the build's dependency closure as cargo resolved it; commits = the recipe repo's and every sibling repo a path dependency of the build lives in",
            "the binary half (sizes, symbols) is in the warehouse's binary_facts / section_sizes / symbol_sizes under the artifact's sha256",
        ] },
    }))
}

#[cfg(test)]
mod tests {
    use super::*;
    use nornir_build_thing::build_info::{BuildInfoKind, manifest_rows};

    const SIB_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
    const APP_SHA: &str = "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678";

    /// A successful build of `app` whose one artifact is a real ELF (`/bin/true`
    /// copied under the builder's checkout), + the forge's lineage rows.
    fn build(root: &Path) -> (BuildProvenance, Vec<BuildInfoRow>) {
        let bin = root.join("target/release/app");
        std::fs::create_dir_all(bin.parent().unwrap()).unwrap();
        std::fs::copy("/bin/true", &bin).unwrap();
        let bytes = std::fs::metadata(&bin).unwrap().len();
        let sha = crate::binscan::content_sha256(&bin).unwrap();
        let record: BuildProvenance = serde_json::from_value(json!({
            "schema": "x", "id": "build-7", "repo": "app",
            "variant": { "name": "app-server", "package": "app" },
            "inputs": { "cargo_args": [], "features": ["tls"], "profile": "release", "vcs_rev": APP_SHA, "vcs_branch": "main", "vcs_dirty": false, "worktree_digest": "clean" },
            "toolchain": { "rustc": "rustc 1.92.0", "host": "x86_64-unknown-linux-gnu", "cargo": "cargo 1.92.0" },
            "host": { "cores": 8, "memory_bytes": 1 },
            "outcome": "success",
            "artifacts": [ { "path": "target/release/app", "bytes": bytes, "sha256": sha } ],
            "built_at": "2026-10-15T20:00:00Z", "builder": "builder@dwarf",
        }))
        .unwrap();
        let mut rows = manifest_rows(&record, 1);
        let row = |k: BuildInfoKind, n: &str, v: &str| BuildInfoRow::new("build-7", 2, k).kv(n, v.to_string());
        rows.push(row(BuildInfoKind::Provenance, REPO_ROOT, &root.display().to_string()));
        rows.push(row(BuildInfoKind::Provenance, &format!("{COMMIT_PREFIX}app"), APP_SHA));
        rows.push(row(BuildInfoKind::Provenance, &format!("{COMMIT_PREFIX}sib"), SIB_SHA));
        for c in ["app@0.1.0", "sib@0.2.0", "serde@1.0.200"] {
            rows.push(row(BuildInfoKind::Dep, DEP_CRATE, c));
        }
        // another build's rows in the same batch are not this one's
        rows.push(BuildInfoRow::new("build-8", 2, BuildInfoKind::Dep).kv(DEP_CRATE, "tokio@1.0.0"));
        (record, rows)
    }

    #[test]
    fn a_filed_build_lands_in_mimir_and_answers_which_artifacts_came_from_a_crate_or_a_commit() {
        let tmp = tempfile::tempdir().unwrap();
        let (record, rows) = build(&tmp.path().join("checkout"));
        assert_eq!(landed_builds(&rows), vec!["build-7".to_string()], "the manifest says success");

        let facts = facts_of(&record, &rows).unwrap();
        assert_eq!(facts.commits.get("sib").map(String::as_str), Some(SIB_SHA));
        assert_eq!(facts.crates.len(), 3, "tokio is build-8's");
        assert_eq!((facts.variant.as_str(), facts.triple.as_str(), facts.features.clone()), ("app-server", "x86_64-unknown-linux-gnu", vec!["tls".to_string()]));

        let room = CodeRoom::new("ws", &tmp.path().join("builds"));
        let landed = room.land_products(&facts).unwrap();
        assert_eq!(landed.len(), 1);
        assert_eq!(landed[0].repo, mimir_ingest::PRODUCTS_REPO);

        let v = answer(&room, &json!({ "crate": "serde" })).unwrap();
        assert_eq!((v["state"].as_str(), v["count"].as_u64()), (Some("complete"), Some(1)), "{v}");
        let hit = &v["artifacts"][0];
        assert_eq!(hit["file"], "app");
        assert!(hit["provenance"].as_str().unwrap().contains("build-7"));
        assert_eq!(answer(&room, &json!({ "commit": &SIB_SHA[..8] })).unwrap()["count"], 1, "built from the sibling's commit");
        assert_eq!(answer(&room, &json!({ "commit": format!("app@{APP_SHA}") })).unwrap()["count"], 1);
        assert_eq!(answer(&room, &json!({ "crate": "tokio" })).unwrap()["state"], "empty_fact");
        assert_eq!(answer(&room, &json!({ "commit": "0123" })).unwrap_err().state, State::InvalidArgument);
    }

    /// `Viz.Binary` lists what Build Thing runs filed (`@build-thing`, no
    /// consumer offset — the room never sees it): with a room that links no
    /// artifact, a filed build is what the Binary pane shows, its sizes and
    /// hardening from the server's analysis; before it lands the answer says
    /// no build filed one either.
    #[test]
    fn viz_binary_shows_a_filed_build_when_the_room_links_no_artifact() {
        let tmp = tempfile::tempdir().unwrap();
        let (record, rows) = build(&tmp.path().join("checkout"));
        let ws = crate::code_room::reader::fixture::fresh_tiny();
        let wh = crate::warehouse::iceberg::IcebergWarehouse::open(&tmp.path().join("wh")).unwrap();
        let reach = crate::code_room::Reach::new(&ws.handle, &wh);
        let ask = |p: Value| crate::code_room::reader::wired::answer("Viz.Binary", &reach, &p);

        let r = ask(json!({})).unwrap_err();
        assert_eq!(r.state, State::NotConsumed, "{}", r.note);
        assert!(r.note.contains("no Build Thing run has filed an artifact"), "{}", r.note);

        let facts = facts_of(&record, &rows).unwrap();
        ws.handle.land_products(&facts).unwrap();
        assert_eq!(binary_half(&wh, &record, &rows).len(), 1);

        let v = ask(json!({})).unwrap_or_else(|r| panic!("[{}] {}", r.state, r.note));
        // partial: the room half (repo `v`, consumed without the binary stream) is the named gap
        assert_eq!((v["state"].as_str(), v["source"].as_str()), (Some("partial"), Some("build")), "{v}");
        assert_eq!(v["evidence"]["missing"][0]["input"], "binary stream", "{v}");
        let arts = v["artifacts"].as_array().unwrap();
        assert_eq!(arts.len(), 1, "{v}");
        assert_eq!((arts[0]["source"].as_str(), arts[0]["binary"].as_str(), arts[0]["variant"].as_str()), (Some("build"), Some("app"), Some("app-server")));
        assert_eq!(arts[0]["sha256"], record.artifacts[0].sha256);
        assert!(v["checksec"]["pie"].is_boolean() && v["checksec"]["total_bytes"].as_i64().unwrap_or(0) > 0, "hardening measured: {v}");
        assert_eq!(v["artifact"], arts[0]["artifact"]);
        // asked by file name, and by a sha prefix
        assert_eq!(ask(json!({ "artifact": "app" })).unwrap()["source"], "build");
        assert_eq!(ask(json!({ "artifact": &record.artifacts[0].sha256[..12] })).unwrap()["source"], "build");
    }

    #[test]
    fn the_binary_half_is_bin_analyze_under_the_artifacts_sha_and_only_for_bytes_on_this_box() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path().join("checkout");
        let (record, rows) = build(&root);
        let wh = crate::warehouse::iceberg::IcebergWarehouse::open(&tmp.path().join("wh")).unwrap();
        let out = binary_half(&wh, &record, &rows);
        assert_eq!(out.len(), 1, "{out:?}");
        assert!(out[0].get("error").is_none(), "{out:?}");
        let sha = &record.artifacts[0].sha256;
        let (header, sections, _) = crate::binscan::load_analysis(&wh, sha).unwrap().expect("persisted under its sha");
        assert_eq!(header.repo, "app");
        assert!(header.total_bytes > 0 && !sections.is_empty());

        // a build on another box: nothing here to read, nothing analysed
        let elsewhere: Vec<BuildInfoRow> = rows.iter().filter(|r| r.name != REPO_ROOT).cloned().collect();
        assert!(local_artifacts(&record, &elsewhere).is_empty());
        // bytes that changed since filing are not the artifact
        std::fs::write(root.join("target/release/app"), b"other").unwrap();
        assert!(local_artifacts(&record, &rows).is_empty(), "size differs: not the filed artifact");

        let mut failed = record.clone();
        failed.outcome = nornir_build_thing::provenance::Outcome::Failed;
        assert!(facts_of(&failed, &rows).is_err());
    }
}