From 0ebfcb92320fb643e3fda6bd52923ab0da41b242 Mon Sep 17 00:00:00 2001 From: Drew Bradford <16455249+DrewBradfordXYZ@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:17:22 -0500 Subject: [PATCH] The dev-build guard recognises the test binaries Rust 1.100's cargo builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since Rust 1.100, cargo runs test binaries from its build-dir layout, /debug/build///out/, not /debug/deps/. dev_build() looked only at the binary's own folder (or the one above deps/), so under 1.100 the nebula binary's tests no longer looked like a cargo build and dev_builds_are_refused_without_force failed. CI on every PR went red the day the runners picked 1.100.0 up. The path rule is now a pure function, in_cargo_target, which also accepts the out/ ← ← ← build/ ← chain. A new test feeds it each layout and a few paths it must refuse, so it no longer depends on which cargo runs it. What `nebula upgrade` refuses is unchanged: a binary in /release or /debug. Checked: cargo +1.100.0 fmt --check, clippy -D warnings and test --workspace (1,938 passed) are clean, and the upgrade tests pass on 1.97.1 too. Co-Authored-By: Claude Opus 5.5 --- crates/nebula/src/upgrade.rs | 57 +++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/crates/nebula/src/upgrade.rs b/crates/nebula/src/upgrade.rs index b32b65fb..8c897894 100644 --- a/crates/nebula/src/upgrade.rs +++ b/crates/nebula/src/upgrade.rs @@ -288,20 +288,34 @@ fn stage_script(url: &str, dir: &Path) -> Result { } /// The running binary when it lives in a cargo build dir (`target/release`, -/// `target/debug`, or the `deps/` dir test binaries run from). Symlinks +/// `target/debug`, or where cargo runs test binaries from). Symlinks /// resolve first: the usual dev setup is a `~/.cargo/bin/nebula` symlink /// pointing into `target/release`, and it's that symlink an upgrade replaces. fn dev_build() -> Option { let exe = std::env::current_exe().ok()?; let exe = std::fs::canonicalize(&exe).unwrap_or(exe); - let dir = exe.parent()?; - let dir = if dir.file_name()? == "deps" { - dir.parent()? - } else { - dir + in_cargo_target(&exe).then_some(exe) +} + +/// Does `exe` sit where cargo puts what it builds? Directly in a profile +/// dir (`/release/nebula`), in its `deps/` (test binaries before +/// Rust 1.100), or in the build-dir layout cargo uses since 1.100, which +/// runs test binaries from `/build///out/`. +fn in_cargo_target(exe: &Path) -> bool { + let Some(mut dir) = exe.parent() else { + return false; }; - let is_target_dir = matches!(dir.file_name()?.to_str()?, "debug" | "release"); - is_target_dir.then_some(exe) + let name = |d: &Path| d.file_name().and_then(|n| n.to_str()).map(str::to_owned); + if name(dir).as_deref() == Some("deps") { + dir = dir.parent().unwrap_or(dir); + } else if name(dir).as_deref() == Some("out") { + // out/ ← / ← / ← build/ ← / + let build = dir.ancestors().nth(3); + if let Some(build) = build.filter(|b| name(b).as_deref() == Some("build")) { + dir = build.parent().unwrap_or(build); + } + } + matches!(name(dir).as_deref(), Some("debug" | "release")) } #[cfg(test)] @@ -502,10 +516,33 @@ mod tests { } } + /// Every place cargo puts a binary reads as a dev build, whichever + /// cargo runs the tests; an installed binary does not. + #[test] + fn cargo_target_dirs_are_recognised() { + for exe in [ + "/src/nebula/target/release/nebula", + "/src/nebula/target/debug/nebula", + "/src/nebula/target/debug/deps/nebula-81def4ed18659372", + "/src/nebula/target/debug/build/nebula/81def4ed18659372/out/nebula-81def4ed18659372", + ] { + assert!(in_cargo_target(Path::new(exe)), "{exe}"); + } + for exe in [ + "/Users/me/.cargo/bin/nebula", + "/usr/local/bin/nebula", + "/opt/nebula/out/nebula", + "/src/nebula/target/debug/build/nebula-1/out/build-script-build", + "nebula", + ] { + assert!(!in_cargo_target(Path::new(exe)), "{exe}"); + } + } + #[test] fn dev_builds_are_refused_without_force() { - // Cargo runs this binary from /debug/deps, so the guard sees - // exactly what it would see for a symlinked target/release build. + // Cargo runs this binary from inside /debug, so the guard + // sees what it would see for a symlinked target/release build. assert!( dev_build().is_some(), "test binary should look like a cargo build"