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

nordisk/Skidbladnir

src/vendor.rs

//! AIRGAP-RUST (pack side) — `cargo vendor` runner + `.cargo/config` generator.
//!
//! The greenfield piece of the airgapped-Rust bundle: there is **no**
//! source-replacement (`cargo vendor` / `[source]`) anywhere in the constellation
//! yet (design `.nornir/airgap-rust-znippy-design-2026-07-30.md` §1.1), so this
//! module owns no legacy. It is the **connected build-host** half (§5): it runs on
//! a box with network at *pack* time — never on the airgapped target — to turn a
//! seed project into (a) a `vendor/` directory holding every crate's source and
//! (b) a `config.toml` whose `[source]` stanza replaces crates.io with that vendor
//! dir and turns `net.offline` on. Dropped onto the target, `cargo build --offline`
//! then resolves every dependency from `vendor/` with zero egress.
//!
//! Split by testability (the same doctrine as the install matrix): the config
//! **generator** ([`cargo_config_toml`](crate::vendor::cargo_config_toml) / [`rewrite_prefix`](crate::vendor::rewrite_prefix)) is pure and unit-tested
//! offline; the [`run_cargo_vendor`](crate::vendor::run_cargo_vendor) runner shells the ONE accepted external
//! (`cargo` — the zero-shell law admits `qemu` + `cargo`, `product.rs`), so its real
//! round-trip is gated behind an `#[ignore]` test that needs a live registry.

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

use anyhow::{Context, Result};

/// The placeholder the packed `config.toml` carries where the real install prefix
/// goes. `skidbladnir install` rewrites it to the activated generation's absolute
/// path ([`rewrite_prefix`]) so the vendor dir resolves on the target box.
pub const PREFIX_TOKEN: &str = "<PREFIX>";

/// Generate the airgap `.cargo/config.toml` body (design §2.3): crates.io is
/// source-**replaced** by the `vendored-sources` directory, and `net.offline` is
/// hard-on so a build against a crate NOT in the vendor set fails loudly instead of
/// reaching the network. `vendor_directory` is written verbatim into
/// `directory = "…"` — pass [`PREFIX_TOKEN`]-relative (`<PREFIX>/vendor`) at pack
/// time, or a real absolute path when generating for a fixed prefix.
///
/// Deliberately does NOT copy znippy's `target-cpu=native` (host-specific; design
/// §2.3) — the config stays portable across the boxes that receive the bundle.
pub fn cargo_config_toml(vendor_directory: &str) -> String {
    format!(
        "# Auto-generated by the skidbladnir airgap-rust packer.\n\
         # Source-replacement: every crates.io dependency resolves from the vendored\n\
         # crate set below, and `net.offline` refuses any network reach — a build that\n\
         # needs a crate not in `vendor/` fails loudly rather than egressing.\n\
         [source.crates-io]\n\
         replace-with = \"vendored-sources\"\n\
         \n\
         [source.vendored-sources]\n\
         directory = \"{vendor_directory}\"\n\
         \n\
         [net]\n\
         offline = true\n"
    )
}

/// The vendor directory value a packed config carries — `<PREFIX>/vendor` — so the
/// install-time [`rewrite_prefix`] has a single token to replace.
pub fn packed_vendor_directory() -> String {
    format!("{PREFIX_TOKEN}/vendor")
}

/// Rewrite a packed config's [`PREFIX_TOKEN`] to the real activated prefix at
/// install time. `prefix` is the absolute path of the activated generation
/// (`<install-prefix>/current`); every `<PREFIX>` becomes that path, so
/// `directory = "<PREFIX>/vendor"` → `directory = "/…/current/vendor"`.
pub fn rewrite_prefix(config_template: &str, prefix: &Path) -> String {
    config_template.replace(PREFIX_TOKEN, &prefix.to_string_lossy())
}

/// The result of a connected-host `cargo vendor` run.
#[derive(Debug, Clone)]
pub struct VendorOutcome {
    /// The directory the crate sources were written to.
    pub vendor_dir: PathBuf,
    /// The `[source]` stanza `cargo vendor` printed to stdout (informational — the
    /// packer writes its OWN portable [`cargo_config_toml`] instead of this
    /// host-absolute one, but it is captured for provenance / diffing).
    pub printed_stanza: String,
}

/// Locate the `cargo` binary: honour the `CARGO` env var cargo itself sets for a
/// build script / subcommand, else fall back to `cargo` on `PATH`.
fn cargo_bin() -> PathBuf {
    std::env::var_os("CARGO")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("cargo"))
}

/// Run `cargo vendor` for the seed project at `manifest_dir` (holding its
/// `Cargo.toml` + resolved `Cargo.lock`), writing every crate's source into
/// `vendor_dir`. CONNECTED-host only (needs the registry) — shells the accepted
/// `cargo` external (zero-shell law: `qemu` + `cargo`). Returns the vendor dir +
/// the stanza cargo printed.
///
/// The packer then seals `vendor_dir` into the `.znippy` and writes its OWN
/// portable [`cargo_config_toml`] (`<PREFIX>/vendor`), NOT the host-absolute stanza
/// cargo prints — so the config is relocatable across every target box.
pub fn run_cargo_vendor(manifest_dir: &Path, vendor_dir: &Path) -> Result<VendorOutcome> {
    let manifest = manifest_dir.join("Cargo.toml");
    if !manifest.is_file() {
        anyhow::bail!(
            "cargo vendor: no Cargo.toml at {} (the seed project manifest)",
            manifest.display()
        );
    }
    if let Some(parent) = vendor_dir.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("create vendor parent {}", parent.display()))?;
    }

    // `cargo vendor --manifest-path <dir>/Cargo.toml <vendor_dir>` writes the crate
    // sources and prints the `[source]` stanza on stdout.
    let out = std::process::Command::new(cargo_bin())
        .arg("vendor")
        .arg("--manifest-path")
        .arg(&manifest)
        .arg(vendor_dir)
        .output()
        .with_context(|| "spawn `cargo vendor` (is cargo on PATH?)")?;

    if !out.status.success() {
        anyhow::bail!(
            "cargo vendor failed (exit {:?}): {}",
            out.status.code(),
            String::from_utf8_lossy(&out.stderr).trim()
        );
    }

    Ok(VendorOutcome {
        vendor_dir: vendor_dir.to_path_buf(),
        printed_stanza: String::from_utf8_lossy(&out.stdout).into_owned(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The generated config source-replaces crates.io with the vendor dir and turns
    /// offline hard-on — the two invariants an airgapped build depends on.
    #[test]
    fn config_replaces_crates_io_and_forces_offline() {
        let cfg = cargo_config_toml("/opt/rust-dev/current/vendor");
        assert!(cfg.contains("[source.crates-io]"));
        assert!(cfg.contains("replace-with = \"vendored-sources\""));
        assert!(cfg.contains("[source.vendored-sources]"));
        assert!(cfg.contains("directory = \"/opt/rust-dev/current/vendor\""));
        assert!(cfg.contains("[net]"));
        assert!(cfg.contains("offline = true"));
        // Must NOT carry znippy's host-specific target-cpu=native (design §2.3).
        assert!(!cfg.contains("target-cpu"));
    }

    /// The packed config carries the `<PREFIX>` token, and install-time rewrite turns
    /// it into the real activated-generation path.
    #[test]
    fn prefix_token_round_trips_through_rewrite() {
        let packed = cargo_config_toml(&packed_vendor_directory());
        assert!(packed.contains("directory = \"<PREFIX>/vendor\""));

        let real = rewrite_prefix(&packed, Path::new("/home/dev/.local/rust-dev/current"));
        assert!(
            real.contains("directory = \"/home/dev/.local/rust-dev/current/vendor\""),
            "rewritten: {real}"
        );
        // No stray token survives the rewrite.
        assert!(!real.contains(PREFIX_TOKEN));
    }

    /// The generated config has the three tables in canonical order with exactly one
    /// `directory` line — the structural shape cargo reads (no dup tables, no stray
    /// keys). Asserted by table order rather than adding a `toml` dep to the lean core.
    #[test]
    fn generated_config_has_the_three_tables_in_order() {
        let cfg = cargo_config_toml("/x/vendor");
        let crates_io = cfg.find("[source.crates-io]").expect("crates-io table");
        let vendored = cfg
            .find("[source.vendored-sources]")
            .expect("vendored table");
        let net = cfg.find("[net]").expect("net table");
        assert!(
            crates_io < vendored && vendored < net,
            "tables in canonical order"
        );
        assert_eq!(
            cfg.matches("directory =").count(),
            1,
            "exactly one directory line"
        );
        assert_eq!(
            cfg.matches("[source.vendored-sources]").count(),
            1,
            "no duplicate table"
        );
    }

    /// The REAL `cargo vendor` round-trip needs a live registry (network) + a seed
    /// project's resolved Cargo.lock, neither of which is in-tree — gated honestly
    /// so CI never silently "passes" a vendoring that never ran. Run with
    /// `cargo test -- --ignored run_cargo_vendor_real` on a connected host.
    #[test]
    #[ignore = "needs a connected host + a live crates.io registry (pack-side only)"]
    fn run_cargo_vendor_real() {
        let tmp = tempfile::tempdir().unwrap();
        let proj = tmp.path().join("seed");
        std::fs::create_dir_all(&proj).unwrap();
        std::fs::write(
            proj.join("Cargo.toml"),
            "[package]\nname=\"seed\"\nversion=\"0.0.0\"\nedition=\"2021\"\n\n[dependencies]\nanyhow=\"1\"\n",
        )
        .unwrap();
        std::fs::create_dir_all(proj.join("src")).unwrap();
        std::fs::write(proj.join("src/lib.rs"), "").unwrap();

        let vendor = tmp.path().join("vendor");
        let out = run_cargo_vendor(&proj, &vendor).expect("cargo vendor");
        assert!(
            out.vendor_dir.join("anyhow").exists(),
            "anyhow was vendored"
        );
        assert!(
            out.printed_stanza.contains("[source"),
            "cargo printed a source stanza"
        );
    }
}