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

nordisk/gunnar

xtask/tests/the_packaged_crate_names_its_commit.rs

//! **A `cargo install gunnar` binary names the commit it was published from**
//! (lane GUN-VERSION-STAMP, 2026-10-31).
//!
//! `cargo install gunnar` 1.6.0 from crates.io printed `gunnar 1.6.0 (unknown, built
//! 2026-07-13T00:00:00Z)`: a published crate has no `.git`, so `git describe` had nothing to
//! read. cargo writes `.cargo_vcs_info.json` (with `git.sha1`) into every package it makes from
//! a git checkout, and that is the commit the crate was published from.
//!
//! This test does what crates.io does: `cargo package -p gunnar-common`, the package unpacked
//! into a directory outside every git checkout, and a small binary built on the unpacked copy
//! that prints `--version` (the `Cli` of `gunnar_common::cli`, the one `gunnar --version`
//! renders). Then:
//!
//! 1. `--version` names the short sha from the package's `.cargo_vcs_info.json`, never `unknown`;
//! 2. with no `SOURCE_DATE_EPOCH`, it names no date: the package knows no commit date, and a
//!    date the build made up would be a lie (and the wall clock would break reproducible builds);
//! 3. with `SOURCE_DATE_EPOCH` set, it names that date (the reproducible-builds knob);
//! 4. the same binary built on the git checkout keeps its old line: `git describe` and the
//!    commit date.

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

fn root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xtask has a parent")
        .to_path_buf()
}

fn cargo() -> String {
    std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())
}

/// The target directory this test binary was built in (`<target>/debug/deps/<test>`), so the
/// nested builds below reuse their dependencies from one run to the next.
fn target_root() -> PathBuf {
    if let Some(t) = std::env::var_os("CARGO_TARGET_DIR") {
        return PathBuf::from(t);
    }
    let exe = std::env::current_exe().expect("the test binary has a path");
    exe.ancestors()
        .nth(3)
        .expect("<target>/<profile>/deps/<test>")
        .to_path_buf()
}

fn run(cmd: &mut Command, what: &str) -> String {
    let out = cmd
        .output()
        .unwrap_or_else(|e| panic!("{what}: could not run: {e}"));
    assert!(
        out.status.success(),
        "{what} failed ({}):\n{}\n{}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8_lossy(&out.stdout).into_owned()
}

/// The `git.sha1` of a `.cargo_vcs_info.json`, read by hand (xtask has no JSON crate).
fn vcs_sha1(json: &str) -> String {
    let at = json.find("\"sha1\"").expect("the vcs info names git.sha1");
    let rest = &json[at + "\"sha1\"".len()..];
    let open = rest.find('"').expect("a quoted sha1");
    let rest = &rest[open + 1..];
    let sha = &rest[..rest.find('"').expect("a closed sha1")];
    assert!(
        sha.len() == 40 && sha.bytes().all(|b| b.is_ascii_hexdigit()),
        "git.sha1 is a 40-hex commit id: {sha:?}"
    );
    sha.to_owned()
}

/// A small binary on `gunnar_common` at `dep`, printing what `gunnar --version` prints.
fn version_probe(dir: &Path, dep: &Path) {
    std::fs::create_dir_all(dir.join("src")).unwrap();
    std::fs::write(
        dir.join("Cargo.toml"),
        format!(
            "[package]\nname = \"version-probe\"\nversion = \"0.0.0\"\nedition = \"2021\"\npublish = false\n\n\
             [dependencies]\ngunnar-common = {{ path = {:?}, default-features = false }}\nclap = \"4\"\n\n[workspace]\n",
            dep.display().to_string()
        ),
    )
    .unwrap();
    std::fs::write(
        dir.join("src/main.rs"),
        "use clap::Parser;\n\
         fn main() {\n\
         \x20   match gunnar_common::cli::Cli::try_parse_from([\"gunnar\", \"--version\"]) {\n\
         \x20       Err(e) => print!(\"{e}\"),\n\
         \x20       Ok(_) => panic!(\"--version parsed as a command\"),\n\
         \x20   }\n\
         }\n",
    )
    .unwrap();
    // The workspace's lock: the probe builds the versions the workspace builds.
    std::fs::copy(root().join("Cargo.lock"), dir.join("Cargo.lock")).unwrap();
}

/// `--version` of the probe at `dir`, built into `target` with `sde` as `SOURCE_DATE_EPOCH`.
fn probe_version(dir: &Path, target: &Path, sde: Option<&str>) -> String {
    let mut cmd = Command::new(cargo());
    cmd.args(["run", "--quiet", "--manifest-path"])
        .arg(dir.join("Cargo.toml"))
        .arg("--target-dir")
        .arg(target)
        .current_dir(dir)
        // A builder's ref wins over everything (build_ref.rs); this test is about the package.
        .env_remove("GUNNAR_BUILD_REF")
        .env_remove("NORDISK_BUILD_REF")
        .env_remove("SOURCE_DATE_EPOCH");
    if let Some(sde) = sde {
        cmd.env("SOURCE_DATE_EPOCH", sde);
    }
    run(&mut cmd, "the version probe").trim().to_owned()
}

#[test]
fn a_packaged_gunnar_names_the_commit_it_was_published_from() {
    let work = target_root().join("packaged-version");
    let pkg_target = work.join("package-target");
    let probe_target = work.join("probe-target");

    // 1. What crates.io gets: the package of gunnar-common, with its .cargo_vcs_info.json.
    run(
        Command::new(cargo())
            .args([
                "package",
                "-p",
                "gunnar-common",
                "--no-verify",
                "--allow-dirty",
            ])
            .arg("--target-dir")
            .arg(&pkg_target)
            .current_dir(root()),
        "cargo package -p gunnar-common",
    );
    let version = {
        let manifest =
            std::fs::read_to_string(root().join("crates/gunnar-common/Cargo.toml")).unwrap();
        manifest
            .lines()
            .find_map(|l| l.trim().strip_prefix("version = \""))
            .and_then(|v| v.strip_suffix('"'))
            .expect("gunnar-common has a version")
            .to_owned()
    };
    let crate_file = pkg_target.join(format!("package/gunnar-common-{version}.crate"));

    // 2. Unpacked OUTSIDE every git checkout: crates.io's copy has no `.git` above it either.
    let outside = std::env::temp_dir().join(format!(
        "gunnar-packaged-version-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    std::fs::create_dir_all(&outside).unwrap();
    run(
        Command::new("tar")
            .arg("-xzf")
            .arg(&crate_file)
            .arg("-C")
            .arg(&outside),
        "unpacking the package",
    );
    let unpacked = outside.join(format!("gunnar-common-{version}"));
    let in_git = Command::new("git")
        .args(["rev-parse", "--git-dir"])
        .current_dir(&unpacked)
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false);
    assert!(
        !in_git,
        "{} is inside a git checkout; the test needs a temp dir outside one",
        unpacked.display()
    );
    let vcs = std::fs::read_to_string(unpacked.join(".cargo_vcs_info.json"))
        .expect("cargo package wrote .cargo_vcs_info.json");
    let sha = vcs_sha1(&vcs);
    let short = &sha[..7];

    // 3. The packaged copy, built with no SOURCE_DATE_EPOCH: the sha, and no date at all.
    let probe = outside.join("probe");
    version_probe(&probe, &unpacked);
    let line = probe_version(&probe, &probe_target, None);
    assert!(
        line.contains(short),
        "a packaged gunnar must name its commit {short} (from .cargo_vcs_info.json): {line:?}"
    );
    assert!(
        !line.contains("unknown"),
        "a packaged gunnar must not say `unknown`: {line:?}"
    );
    assert!(
        !line.contains("built"),
        "with no SOURCE_DATE_EPOCH a packaged gunnar knows no date and must name none: {line:?}"
    );
    assert_eq!(
        line,
        format!(
            "gunnar {version} ({}{})",
            short,
            if vcs.contains("\"dirty\": true") {
                "-modified"
            } else {
                ""
            }
        )
    );

    // 4. With SOURCE_DATE_EPOCH, that date and no other.
    let line = probe_version(&probe, &probe_target, Some("1783900800"));
    assert!(
        line.contains(short) && line.ends_with(", built 2026-07-13T00:00:00Z)"),
        "with SOURCE_DATE_EPOCH the packaged gunnar names that date: {line:?}"
    );

    // 5. The git checkout keeps its line: `git describe` and the commit's date.
    let describe = run(
        Command::new("git")
            .args([
                "-c",
                "safe.directory=*",
                "-c",
                "core.checkStat=minimal",
                "describe",
                "--tags",
                "--always",
                "--dirty=-modified",
                "--abbrev=7",
            ])
            .current_dir(root()),
        "git describe",
    );
    let checkout_probe = outside.join("checkout-probe");
    version_probe(&checkout_probe, &root().join("crates/gunnar-common"));
    let line = probe_version(&checkout_probe, &probe_target, None);
    assert!(
        line.starts_with(&format!("gunnar {version} ({}, built ", describe.trim())),
        "a git checkout keeps `git describe` and the commit date: {line:?}"
    );
    assert!(!line.contains("unknown"), "{line:?}");

    let _ = std::fs::remove_dir_all(&outside);
}