diff --git a/dag/extdeps/rust/cargo.dag b/dag/extdeps/rust/cargo.dag index 8d2ce1c4f2b..db76f1f2239 100644 --- a/dag/extdeps/rust/cargo.dag +++ b/dag/extdeps/rust/cargo.dag @@ -114,6 +114,7 @@ type CargoEnvironmentVariable | CargoTargetDirEnv | CargoBuildJobsEnv | RustcWrapperEnv + | RustflagsEnv // THE NAMES LIVE HERE, THE OPERAND FORM DOES NOT, and an earlier revision of this module got that // wrong. It carried a cargo_environment_assignment rendering "NAME=value" beside these names, @@ -129,6 +130,7 @@ fn cargo_environment_variable_name(variable: CargoEnvironmentVariable) -> NonEmp CargoTargetDirEnv => "CARGO_TARGET_DIR" CargoBuildJobsEnv => "CARGO_BUILD_JOBS" RustcWrapperEnv => "RUSTC_WRAPPER" + RustflagsEnv => "RUSTFLAGS" } } diff --git a/dag/gunbc/repo/repo_self_build.dag b/dag/gunbc/repo/repo_self_build.dag index ad7695e0d1d..fbbf97763ad 100644 --- a/dag/gunbc/repo/repo_self_build.dag +++ b/dag/gunbc/repo/repo_self_build.dag @@ -124,12 +124,29 @@ fn repo_self_test_command() -> String { fn repo_self_clippy_command() -> String { join( argv_words( - command: cargo_clippy_command(extra_args: ["--all-targets"], lint_args: ["-D", "warnings"]) + command: cargo_clippy_command(extra_args: ["--all-targets"], lint_args: repo_self_warning_denial) ), " " ) } +// THAT A WARNING IS AN ERROR IS ONE FACT ABOUT THIS REPOSITORY, SPELLED ONCE. It reaches rustc +// two ways: as the lint arguments after clippy's `--` separator (the row above), and as the +// RUSTFLAGS environment cargo forwards to every rustc it spawns -- the form the required +// v2-native lane sets EXPLICITLY on the emitted compiler crate's build +// (gunbc.witness_v2_native_route NativeRouteEmittedBuild). Until that lane set it, the emitted +// crate was built under whatever RUSTFLAGS the runner's toolchain step happened to export -- the +// setup-rust-toolchain action's DEFAULT input, which this authority never declared -- so the same +// crate compiled clean on a workstation and refused with status 101 in CI, and the receipt's +// "compiled" was true of a runner rather than of the crate. The environment form is the lint +// arguments joined by a space because that is how cargo tokenises RUSTFLAGS (whitespace-split); +// it is derived here from the same row, never re-spelled at the invocation. +data repo_self_warning_denial: List = ["-D", "warnings"] + +fn repo_self_warning_denial_rustflags() -> String { + join(repo_self_warning_denial, " ") +} + // THE DERIVED STAGE0 PARTITION CRATES, WHICH THE PACKAGE-AS-DATA DECISION ABOVE DOES NOT REACH. // repo_self_build_package is one package because that was the whole repository's compiler for as // long as this module existed. The stage0 partition splits that compiler across seven SIBLING diff --git a/dag/gunbc/witness/v2_native_route.dag b/dag/gunbc/witness/v2_native_route.dag index b0336d076e9..d729f15a1db 100644 --- a/dag/gunbc/witness/v2_native_route.dag +++ b/dag/gunbc/witness/v2_native_route.dag @@ -16,6 +16,7 @@ import v2.workflow.compile_door_ledger { } import v2.workflow.floor_expected_red { floor_expected_red_roster } import v2.workflow.floor_route_gap { floor_route_gap_roster } +import gunbc.repo_self_build { repo_self_warning_denial_rustflags } // THE LANE AUTHORITY FOR THE required-v2-native CI JOB (operator authorization 2026-09-09: one // additional required job, modelled beside the workflow authority that projects it — never a @@ -445,6 +446,35 @@ type NativeRouteOldRouteControl | OldRouteFalsifierArmed { falsifier_identity: String } | OldRouteControlAbsent +// THE EMITTED COMPILER'S BUILD, AS A RECEIPT RATHER THAN AS A SIDE EFFECT OF ONE. Before this row +// the lane recorded that the emitted binary EXISTED (its path and content digest) and nothing about +// the compilation that produced it, so "the emitted crate compiled" was true of whatever cargo the +// runner happened to run under whatever RUSTFLAGS the toolchain step exported. Measured: CI's +// toolchain action defaults RUSTFLAGS to `-D warnings`, a workstation exports none, and the same +// crate compiled on one and refused with status 101 on the other with the receipt unable to say +// why. The five facts here are the ones that decide whether one run's "compiled" can be compared +// with another's: the exact argv cargo was spawned with, the RUSTFLAGS the lane SET on that spawn +// (never inherited -- the value is the repository's own row, gunbc.repo_self_build +// repo_self_warning_denial_rustflags, applied at the invocation, on BOTH channels cargo reads), +// the one compiler executable cargo was BOUND to (RUSTC set on the spawn, both wrapper channels +// disabled there -- a receipt naming a compiler cargo was free to replace via config or a +// wrapper would be evidence about a build that did not happen), that executable's +// self-reported identity in its keyed `--version --verbose` form (extdeps.rust.rustc +// rustc_version_verbose, asked from the crate's own directory), cargo's own +// exit status, and the count of `warning` diagnostic headers on its stderr. The count is carried +// so the receipt names it, and admission requires it zero: under the denial a lint arrives as an +// error and stops the build, so a non-zero count on a zero status can only be a warning rustc +// did not own (cargo's own, or a build script's), which is still a warning the receipt would +// otherwise have swallowed. +type NativeRouteEmittedBuild { + cargo_argv: List + rustflags: String + compiler_path: String + rustc_identity: String + exit_status: Int + warning_count: Int +} + // THE TERMINAL OBSERVATION. Every identity the contract names is a field: the tested tree; the // seed used ONLY to prepare the emitted compiler (recorded honestly — this job does not claim // native bootstrap); the emitted closure's identity; the executed binary's path AND content @@ -468,6 +498,7 @@ type NativeRouteReceipt { true_control: Optional malformed_control: NativeRouteMalformedControl old_route_control: NativeRouteOldRouteControl + emitted_build: NativeRouteEmittedBuild } // ADMISSION IS THE CONTRACT, ONE AUTHORITY, ONE CLAUSE PER PREDICATE. The job greens only when @@ -505,6 +536,10 @@ type NativeRouteAdmissionRefusalCause | TrueControlNotObserved | MalformedControlAcceptedByRoute | OldRouteControlNeverInstalled + | EmittedBuildRustcUnrecorded + | EmittedBuildCompilerUnbound + | EmittedBuildWarningsNotDenied + | EmittedBuildNotClean type NativeRouteAdmission = NativeRouteAdmitted @@ -745,6 +780,26 @@ fn native_route_old_route_closed(receipt: NativeRouteReceipt) -> Bool { } } +// The build clauses read the emitted-build receipt and nothing else: rustc named itself; the +// RUSTFLAGS the lane set are exactly the repository's denial row (equality, not containment -- +// a flag string that carries the denial plus something else is a different build environment +// and the receipt must say so); and the build completed with status 0 and no warning header. +fn native_route_emitted_build_rustc_recorded(receipt: NativeRouteReceipt) -> Bool { + receipt.emitted_build.rustc_identity != "" +} + +fn native_route_emitted_build_compiler_bound(receipt: NativeRouteReceipt) -> Bool { + receipt.emitted_build.compiler_path != "" +} + +fn native_route_emitted_build_warnings_denied(receipt: NativeRouteReceipt) -> Bool { + receipt.emitted_build.rustflags == repo_self_warning_denial_rustflags() +} + +fn native_route_emitted_build_clean(receipt: NativeRouteReceipt) -> Bool { + receipt.emitted_build.exit_status == 0 && receipt.emitted_build.warning_count == 0 +} + fn native_route_admission(receipt: NativeRouteReceipt) -> NativeRouteAdmission { let expected_red = floor_expected_red_roster() let route_gap = floor_route_gap_roster() @@ -775,7 +830,11 @@ fn native_route_admission(receipt: NativeRouteReceipt) -> NativeRouteAdmission { NativeRouteClauseCheck { holds: native_route_false_control_holds(receipt: receipt), cause: FalseControlNotObserved }, NativeRouteClauseCheck { holds: native_route_true_control_holds(receipt: receipt), cause: TrueControlNotObserved }, NativeRouteClauseCheck { holds: native_route_malformed_control_holds(receipt: receipt), cause: MalformedControlAcceptedByRoute }, - NativeRouteClauseCheck { holds: native_route_old_route_closed(receipt: receipt), cause: OldRouteControlNeverInstalled } + NativeRouteClauseCheck { holds: native_route_old_route_closed(receipt: receipt), cause: OldRouteControlNeverInstalled }, + NativeRouteClauseCheck { holds: native_route_emitted_build_rustc_recorded(receipt: receipt), cause: EmittedBuildRustcUnrecorded }, + NativeRouteClauseCheck { holds: native_route_emitted_build_compiler_bound(receipt: receipt), cause: EmittedBuildCompilerUnbound }, + NativeRouteClauseCheck { holds: native_route_emitted_build_warnings_denied(receipt: receipt), cause: EmittedBuildWarningsNotDenied }, + NativeRouteClauseCheck { holds: native_route_emitted_build_clean(receipt: receipt), cause: EmittedBuildNotClean } ] let causes = fold_list( xs: checks, @@ -815,6 +874,10 @@ fn native_route_admission_refusal_cause_name(cause: NativeRouteAdmissionRefusalC TrueControlNotObserved => "true_control_not_observed" MalformedControlAcceptedByRoute => "malformed_control_accepted_by_route" OldRouteControlNeverInstalled => "old_route_control_never_installed" + EmittedBuildRustcUnrecorded => "emitted_build_rustc_unrecorded" + EmittedBuildCompilerUnbound => "emitted_build_compiler_unbound" + EmittedBuildWarningsNotDenied => "emitted_build_warnings_not_denied" + EmittedBuildNotClean => "emitted_build_not_clean" } } diff --git a/dag/test/claim/devboot_host_tool_spelling_witness_test.dag b/dag/test/claim/devboot_host_tool_spelling_witness_test.dag index cb7b5664381..257106db4cd 100644 --- a/dag/test/claim/devboot_host_tool_spelling_witness_test.dag +++ b/dag/test/claim/devboot_host_tool_spelling_witness_test.dag @@ -16,6 +16,7 @@ import extdeps.cargo { CargoTargetDirEnv, CargoBuildJobsEnv, RustcWrapperEnv, + RustflagsEnv, cargo_environment_variable_name, } import extdeps.rustup { rustup_home_env_name } @@ -68,6 +69,7 @@ test fn cargo_environment_variable_names_are_the_cited_upstream_spellings() -> B && (cargo_environment_variable_name(variable: CargoTargetDirEnv) as String) == "CARGO_TARGET_DIR" && (cargo_environment_variable_name(variable: CargoBuildJobsEnv) as String) == "CARGO_BUILD_JOBS" && (cargo_environment_variable_name(variable: RustcWrapperEnv) as String) == "RUSTC_WRAPPER" + && (cargo_environment_variable_name(variable: RustflagsEnv) as String) == "RUSTFLAGS" } // THE NAME COMES FROM CARGO, THE OPERAND FORM FROM env(1), AND THE JOIN IS THE ASSERTION. This diff --git a/src/v1/stage0/src/cli_run/native_lane_runner.rs b/src/v1/stage0/src/cli_run/native_lane_runner.rs index ed2a0dc7de9..d688b2001a1 100644 --- a/src/v1/stage0/src/cli_run/native_lane_runner.rs +++ b/src/v1/stage0/src/cli_run/native_lane_runner.rs @@ -89,6 +89,19 @@ struct EmittedPreparation { binary_identity: String, closure_identity: String, seed_identity: String, + build: EmittedBuildObserved, +} + +/// The emitted compiler's build as the receipt records it — mirror of +/// `gunbc.witness_v2_native_route` `NativeRouteEmittedBuild`. Every field is read from the +/// spawn that ran or the toolchain that answered, never composed from what was intended. +struct EmittedBuildObserved { + cargo_argv: Vec, + rustflags: String, + compiler_path: String, + rustc_identity: String, + exit_status: i64, + warning_count: i64, } fn sha256_file(path: &Path) -> Result { @@ -303,6 +316,13 @@ fn prepare_emitted_compiler(source_roots: &[String]) -> Result Result (i64::from(*status), *warning_count as i64), + other => { + return Err(format!( + "V2-NATIVE REFUSAL cause=EmittedCompilerBuildFailed — verdict admitted as compiled \ + is not a completed run: {}", + super::emitted_closure_compile_host::cargo_verdict_summary(other) + )) + } + }; + let build = EmittedBuildObserved { + cargo_argv: invocation.argv, + rustflags: invocation.rustflags, + compiler_path: invocation.compiler_path, + rustc_identity: rustc, + exit_status, + warning_count, + }; + eprintln!( + "required-ci: v2-native emitted crate built — argv={:?} RUSTFLAGS={:?} compiler={} \ + exit_status={exit_status} warning_count={warning_count} rustc={}", + build.cargo_argv, build.rustflags, build.compiler_path, build.rustc_identity + ); let binary_path = workspace.join("target").join("release").join( super::emitted_closure_compile_host::probe_package_name(NATIVE_COMPILE_ENTRY), ); @@ -337,6 +388,7 @@ fn prepare_emitted_compiler(source_roots: &[String]) -> Result Value { + let emitted_build = Value::Record { + type_name: ctx.sym("NativeRouteEmittedBuild"), + fields: Rc::new(vec![ + ( + ctx.sym("cargo_argv"), + super::list_value_from_vec( + preparation + .build + .cargo_argv + .iter() + .map(|word| str_value(word)) + .collect(), + ), + ), + ( + ctx.sym("rustflags"), + str_value(&preparation.build.rustflags), + ), + ( + ctx.sym("compiler_path"), + str_value(&preparation.build.compiler_path), + ), + ( + ctx.sym("rustc_identity"), + str_value(&preparation.build.rustc_identity), + ), + ( + ctx.sym("exit_status"), + Value::Int(preparation.build.exit_status), + ), + ( + ctx.sym("warning_count"), + Value::Int(preparation.build.warning_count), + ), + ]), + }; let universe_values: Vec = universe .iter() .map(|(module, declaration)| identity_value(ctx, module, declaration)) @@ -807,6 +895,7 @@ fn receipt_value( )]), }, ), + (ctx.sym("emitted_build"), emitted_build), ]), } } @@ -818,6 +907,7 @@ fn receipt_value( /// `native_route_admission_summary` of that same value, so the terminal line and the admission /// fold cannot disagree about what was decided. pub fn run_required_v2_native(source_roots: &[String]) -> Result<(), String> { + let lane_started = std::time::Instant::now(); let workspace = super::process_workspace_root(); let tested_tree = std::env::var("GITHUB_SHA").unwrap_or_else(|_| "local".to_string()); @@ -1010,6 +1100,21 @@ pub fn run_required_v2_native(source_roots: &[String]) -> Result<(), String> { )) } }; + // REALIZATION TELEMETRY, ON ITS OWN LINE AND ON NO RECEIPT FIELD: process peak RSS + // (getrusage ru_maxrss, process-scoped — the rustc children cargo spawned are not in it), + // current RSS, the slot cgroup's memory.current/memory.peak (peak spans the runner service's + // lifetime, not this run), and the lane's wall. It re-derives the run's cost; it decides + // nothing. Resource preflight (predicted critical path and memory envelope against + // gunbc.runner_slot_allocation's slot rows) is std.realization cost vocabulary and lands + // with route integration, not here. + let (cgroup_current, cgroup_peak) = super::p1_cohort::p1_cohort_cgroup_memory(); + eprintln!( + "required-ci: v2-native telemetry peak_rss_bytes={:?} rss_bytes={:?} cgroup_current_bytes={cgroup_current:?} \ + cgroup_peak_bytes_slot_lifetime={cgroup_peak:?} wall_s={}", + super::peak_rss_vhwm_bytes(), + super::current_rss_bytes(), + lane_started.elapsed().as_secs() + ); eprintln!("required-ci: v2-native admission {summary_text}"); if admitted { Ok(()) diff --git a/src/v1/stage0/src/emitted_closure_compile_host.rs b/src/v1/stage0/src/emitted_closure_compile_host.rs index b50f808a1e0..48e11acfbf7 100644 --- a/src/v1/stage0/src/emitted_closure_compile_host.rs +++ b/src/v1/stage0/src/emitted_closure_compile_host.rs @@ -40,9 +40,19 @@ //! CLI flag and no required phase, and it is not on the emitted seed's public surface. //! //! WHAT THIS PHASE IS NOT. No baseline, no diagnostic count, no ratchet. Cargo's exit status is -//! the whole verdict; warnings are not errors here. Pinning a diagnostic population measured on -//! the current tree would be the tree-copied oracle DESIGN 5 rejects; an identity-grain debt -//! contract over the emitted population is a separate construction with a separate argument. +//! the whole verdict. Pinning a diagnostic population measured on the current tree would be the +//! tree-copied oracle DESIGN 5 rejects; an identity-grain debt contract over the emitted +//! population is a separate construction with a separate argument. +//! +//! A WARNING IS AN ERROR HERE, AND THE INVOCATION SAYS SO ITSELF. `run_cargo` sets RUSTFLAGS to +//! the repository's own denial row (`gunbc.repo_self_build` `repo_self_warning_denial_rustflags`) +//! on every spawn, so the verdict is the same on a workstation and on a runner. It used to +//! inherit: CI's toolchain step defaults RUSTFLAGS to `-D warnings` and a workstation exports +//! none, so the emitted compiler crate compiled clean locally and refused with status 101 in +//! CI — and the required v2-native lane's receipt could not say which environment its +//! "compiled" was true of. The lane now records the flags, the argv and rustc's identity on its +//! receipt (`gunbc.witness_v2_native_route` `NativeRouteEmittedBuild`) and admission compares +//! the flags against that same row. // CLIPPY ROSTER -- 7 finding(s) this module trips today, listed one lint per line with // its count. Until this commit the generated crate root allowed `clippy::all` plus six @@ -62,7 +72,9 @@ use super::{ ci_layer_roots_authority_content, compile_entry_emission, process_workspace_root, string_list_data_from_ci_layer_roots_source, CompileDisposition, CompileRun, }; -use crate::extdeps_cargo::CargoDependency; +use crate::extdeps_cargo::{ + cargo_environment_variable_name, CargoDependency, CargoEnvironmentVariable, +}; use crate::extdeps_cargo_version::render_cargo_package_header_prefix; use crate::gunbc_stage0_crate_partition_generated::GeneratedPartitionCrateKind; use crate::v1_compiler_stage0_crates::{ @@ -150,11 +162,17 @@ pub enum CargoVerdict { /// alone says only WHERE a fault was reported and never WHAT rustc refused, so a caller /// holding only `probe_line` passes on ANY refusal reported in the attributed file. The /// header is what lets a caller name the error class it expects. + /// `warning_count` is the number of `warning` diagnostic headers on stderr — rustc's + /// `warning: …` and cargo's own alike, counted by the same header scan that governs + /// attribution. Under the denial a rustc lint arrives as an `error` and stops the build, so + /// a non-zero count beside status 0 is a warning nobody denied (cargo's, a build script's); + /// the count is carried so a receipt can name it rather than swallow it. Completed { status: i32, stderr_tail: String, probe_line: Option, probe_diagnostic: Option, + warning_count: usize, }, } @@ -190,6 +208,7 @@ pub fn cargo_verdict_summary(verdict: &CargoVerdict) -> String { stderr_tail, probe_line, probe_diagnostic, + warning_count: _, } => format!( "Completed status={status} diagnostic={} line={} stderr_tail={stderr_tail}", probe_diagnostic.as_deref().unwrap_or("unattributed"), @@ -786,11 +805,204 @@ fn attributed_diagnostic( (None, None) } -/// `build --release` INTO THE WORKSPACE TARGET DIRECTORY, both halves one cost decision: the -/// lane's first step is `cargo build --release -p v1-compiler --bins`, so the seed crate is -/// already compiled there under that profile; a `check` or a private target dir would share no -/// fingerprint and rebuild the whole dependency graph inside a required phase. The baseline arm -/// thus compiles only the emitted crate, and the two further arms are incremental. +/// The number of `warning` diagnostic headers on a cargo stderr, by the same line shape +/// `attributed_diagnostic` reads (a trimmed line starting with `warning`). Pure, for the same +/// reason: the count is receipt content and its scan must be testable without a toolchain. +fn warning_header_count(stderr: &str) -> usize { + stderr + .lines() + .filter(|line| line.trim().starts_with("warning")) + .count() +} + +/// The exact cargo invocation `run_cargo` spawns for a probe crate, as receipt content: the +/// argv (program first), the RUSTFLAGS value the spawn SETS, the one compiler executable cargo +/// is bound to, and that executable's self-reported identity. Built by the one function that +/// also builds the `Command`, so the receipt cannot describe a different spawn than the one +/// that ran. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ProbeCargoInvocation { + pub argv: Vec, + pub rustflags: String, + pub compiler_path: String, + pub rustc_identity: String, +} + +/// Mirror of `gunbc.repo_self_build` `repo_self_warning_denial_rustflags`: the lint arguments +/// `["-D", "warnings"]` joined by a space, the form cargo tokenises RUSTFLAGS in. A seed mirror +/// like every other literal this host carries — the `.dag` row is the authority the required +/// v2-native lane's admission compares the receipt against, so a drift here reds that lane by +/// `emitted_build_warnings_not_denied` rather than passing silently. +pub(crate) const WARNING_DENIAL_RUSTFLAGS: &str = "-D warnings"; + +/// The same denial in cargo's ENCODED form: `CARGO_ENCODED_RUSTFLAGS` is read BEFORE +/// `RUSTFLAGS` and separates arguments with 0x1f. Setting only the plain form would let an +/// ambient encoded value win the build while the receipt still said `-D warnings`. +pub(crate) const WARNING_DENIAL_ENCODED_RUSTFLAGS: &str = "-D\x1fwarnings"; + +/// THE ENVIRONMENT CHANNELS CARGO READS TO CHOOSE FLAGS AND A COMPILER, all of which this +/// construction OWNS (cargo's environment-variables reference, cited by `extdeps.rust.cargo` +/// `cargo_environment_variables_authority`): the two flag channels above; `RUSTC`, which wins +/// over `build.rustc` config; and the two wrapper channels `RUSTC_WRAPPER` / +/// `RUSTC_WORKSPACE_WRAPPER`, which when PRESENT (even empty) short-circuit their +/// `build.rustc-wrapper` / `build.rustc-workspace-wrapper` config — so setting them empty is +/// the one spelling that disables a wrapper from every source. A receipt that recorded +/// `-D warnings` and compiler X while cargo compiled under an ambient encoded flag, a config +/// compiler or a wrapper fronting X would be admission evidence about a build that did not +/// happen (side-chat landing ruling on #11011, 2026-09-11). +const CARGO_ENCODED_RUSTFLAGS_ENV: &str = "CARGO_ENCODED_RUSTFLAGS"; +const RUSTC_ENV: &str = "RUSTC"; +const RUSTC_WRAPPER_ENV: &str = "RUSTC_WRAPPER"; +const RUSTC_WORKSPACE_WRAPPER_ENV: &str = "RUSTC_WORKSPACE_WRAPPER"; + +/// The ONE compiler executable the probe build is bound to: `RUSTC` when the caller's +/// environment names one (cargo's own precedence), else the first `rustc` on PATH — resolved +/// to an absolute path HERE so the identity probe and the build cannot resolve differently. +/// No executable is a typed refusal, never a bare `"rustc"` handed to cargo to resolve again. +fn resolve_probe_compiler() -> Result { + if let Some(named) = std::env::var_os(RUSTC_ENV) { + let path = PathBuf::from(&named); + if path.is_file() { + return Ok(path); + } + return Err(format!( + "ProbeCompilerUnresolved: RUSTC names {} which is not a file", + path.display() + )); + } + let path_var = std::env::var_os("PATH").unwrap_or_default(); + for dir in std::env::split_paths(&path_var) { + let candidate = dir.join("rustc"); + if candidate.is_file() { + return Ok(candidate); + } + } + Err("ProbeCompilerUnresolved: no RUSTC in the environment and no rustc on PATH".to_string()) +} + +/// rustc's self-reported identity in its keyed `--version --verbose` form +/// (`extdeps.rust.rustc` `rustc_version_verbose`): the release line plus every `key: value` +/// line, joined by `; `. Asked of THE BOUND EXECUTABLE, from THE CRATE'S OWN DIRECTORY — a +/// rustup proxy selects its toolchain per working directory, and cargo runs the compiler from +/// the package dir, so a probe from elsewhere could name a different toolchain than the build +/// used. Unreadable is a refusal, not an empty identity. +fn probe_compiler_identity(compiler: &Path, crate_dir: &Path) -> Result { + let output = std::process::Command::new(compiler) + .arg("--version") + .arg("--verbose") + .current_dir(crate_dir) + .output() + .map_err(|e| { + format!( + "RustcIdentityUnreadable: spawning {}: {e}", + compiler.display() + ) + })?; + if !output.status.success() { + return Err(format!( + "RustcIdentityUnreadable: {} --version --verbose exited {}", + compiler.display(), + output.status + )); + } + let text = String::from_utf8_lossy(&output.stdout); + let identity = text + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect::>() + .join("; "); + if identity.is_empty() { + return Err(format!( + "RustcIdentityUnreadable: {} --version --verbose printed nothing", + compiler.display() + )); + } + Ok(identity) +} + +/// The `Command` and its receipt description, from one construction, with `compiler` and its +/// `identity` supplied by the caller that resolved them (so the pure env shape is testable +/// without a toolchain, and the resolving arm is testable separately). Every channel named +/// above is SET on the spawn — never inherited, never left to config: an ambient value would +/// make the verdict a fact about the runner rather than the crate. +fn probe_cargo_command_bound( + crate_dir: &Path, + workspace: &Path, + compiler: &Path, + identity: &str, +) -> (std::process::Command, ProbeCargoInvocation) { + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + let manifest = crate_dir.join("Cargo.toml"); + let argv: Vec = vec![ + cargo.clone(), + "build".to_string(), + "--release".to_string(), + "--manifest-path".to_string(), + manifest.display().to_string(), + ]; + let mut command = std::process::Command::new(&cargo); + command + .args(&argv[1..]) + .env( + cargo_environment_variable_name(CargoEnvironmentVariable::CargoTargetDirEnv), + workspace.join("target"), + ) + .env( + cargo_environment_variable_name(CargoEnvironmentVariable::RustflagsEnv), + WARNING_DENIAL_RUSTFLAGS, + ) + .env( + CARGO_ENCODED_RUSTFLAGS_ENV, + WARNING_DENIAL_ENCODED_RUSTFLAGS, + ) + .env(RUSTC_ENV, compiler) + .env(RUSTC_WRAPPER_ENV, "") + .env(RUSTC_WORKSPACE_WRAPPER_ENV, "") + .current_dir(crate_dir); + ( + command, + ProbeCargoInvocation { + argv, + rustflags: WARNING_DENIAL_RUSTFLAGS.to_string(), + compiler_path: compiler.display().to_string(), + rustc_identity: identity.to_string(), + }, + ) +} + +/// Resolve the compiler, take its identity from the crate's directory, and build the bound +/// spawn. The receipt's compiler is the executable cargo is bound to, by construction. +fn probe_cargo_command( + crate_dir: &Path, + workspace: &Path, +) -> Result<(std::process::Command, ProbeCargoInvocation), String> { + let compiler = resolve_probe_compiler()?; + let identity = probe_compiler_identity(&compiler, crate_dir)?; + Ok(probe_cargo_command_bound( + crate_dir, workspace, &compiler, &identity, + )) +} + +/// The receipt description of the spawn `run_cargo` would make for `crate_dir` — the same +/// construction, without spawning cargo (the compiler identity probe does run). +pub(crate) fn probe_cargo_invocation( + crate_dir: &Path, + workspace: &Path, +) -> Result { + probe_cargo_command(crate_dir, workspace).map(|(_, invocation)| invocation) +} + +/// `build --release` INTO THE WORKSPACE TARGET DIRECTORY, both halves one cost decision: a +/// `check` or a private target dir would share no fingerprint with anything and rebuild the +/// whole dependency graph inside a required phase. RUSTFLAGS is part of cargo's fingerprint, +/// so what the seed build already compiled is reusable here exactly when it was built under +/// the same denial: on CI it was (the toolchain step exports the same `-D warnings`), so the +/// baseline arm compiles only the emitted crate; on a workstation whose seed build inherited +/// no RUSTFLAGS the FIRST probe build recompiles the dependency graph under the denial once, +/// and the further arms — and every later probe in that target dir — are incremental against +/// that. The one-time local cost is the price of the verdict being about the crate rather than +/// about which machine built it. /// /// Phases within one required run are sequential in one process, so nothing else holds cargo's /// lock on that directory. @@ -801,15 +1013,11 @@ pub(crate) fn run_cargo( workspace: &Path, attribution_symbol: &str, ) -> CargoVerdict { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - let mut command = std::process::Command::new(&cargo); - command - .arg("build") - .arg("--release") - .arg("--manifest-path") - .arg(crate_dir.join("Cargo.toml")) - .env("CARGO_TARGET_DIR", workspace.join("target")) - .current_dir(crate_dir); + let (mut command, invocation) = match probe_cargo_command(crate_dir, workspace) { + Ok(bound) => bound, + Err(reason) => return CargoVerdict::NotAttempted { reason }, + }; + let cargo = &invocation.argv[0]; match command.output() { Err(e) => CargoVerdict::DidNotComplete { detail: format!("spawning {cargo} failed: {e}"), @@ -828,6 +1036,7 @@ pub(crate) fn run_cargo( stderr_tail: tail.into_iter().rev().collect::>().join("\n"), probe_line, probe_diagnostic, + warning_count: warning_header_count(&stderr), } } }, @@ -2023,6 +2232,144 @@ mod tests { /// A NON-ZERO COMPLETED VERDICT MUST NAME WHAT RUSTC SAID. The native lane's refusal /// interpolates only this summary; a status-only render is how a cargo failure reached CI /// as `Completed status=101` with the diagnostic sitting unread in the same verdict. + fn env_of(command: &std::process::Command, name: &str) -> Option> { + command + .get_envs() + .find(|(n, _)| *n == std::ffi::OsStr::new(name)) + .map(|(_, v)| v.map(|v| v.to_string_lossy().to_string())) + } + + /// THE SPAWN OWNS EVERY CHANNEL CARGO READS, AND THE RECEIPT DESCRIBES THE SPAWN. The + /// `Command`'s own environment table is read back rather than the receipt trusted: a + /// receipt that said `-D warnings` and compiler X beside a spawn that let cargo read an + /// ambient encoded flag, a config compiler or a wrapper would be the fabricated provenance + /// this construction exists to make unwritable. + #[test] + fn the_probe_cargo_spawn_binds_flags_compiler_and_wrappers_and_the_receipt_names_that_spawn() { + let crate_dir = Path::new("/tmp/probe-crate"); + let workspace = Path::new("/tmp/workspace"); + let compiler = Path::new("/toolchain/bin/rustc"); + let (command, invocation) = + probe_cargo_command_bound(crate_dir, workspace, compiler, "rustc 1.93.0; host: x"); + assert_eq!( + env_of(&command, "RUSTFLAGS"), + Some(Some(WARNING_DENIAL_RUSTFLAGS.to_string())), + "RUSTFLAGS is SET on the spawn, not inherited" + ); + assert_eq!( + env_of(&command, CARGO_ENCODED_RUSTFLAGS_ENV), + Some(Some(WARNING_DENIAL_ENCODED_RUSTFLAGS.to_string())), + "the encoded channel cargo reads FIRST carries the same denial" + ); + assert_eq!( + WARNING_DENIAL_ENCODED_RUSTFLAGS + .split('\x1f') + .collect::>() + .join(" "), + WARNING_DENIAL_RUSTFLAGS, + "the two channels spell one denial" + ); + assert_eq!( + env_of(&command, RUSTC_ENV), + Some(Some(compiler.display().to_string())), + "cargo is bound to the exact executable whose identity the receipt records" + ); + assert_eq!( + env_of(&command, RUSTC_WRAPPER_ENV), + Some(Some(String::new())) + ); + assert_eq!( + env_of(&command, RUSTC_WORKSPACE_WRAPPER_ENV), + Some(Some(String::new())), + "both wrapper channels are present-and-empty, which disables config wrappers too" + ); + assert_eq!( + cargo_environment_variable_name(CargoEnvironmentVariable::RustflagsEnv), + "RUSTFLAGS", + "the spawn's env name is the authority's row, and that row spells cargo's name" + ); + let spawned: Vec = + std::iter::once(command.get_program().to_string_lossy().to_string()) + .chain(command.get_args().map(|a| a.to_string_lossy().to_string())) + .collect(); + assert_eq!( + spawned, invocation.argv, + "the receipt argv is the spawned argv" + ); + assert_eq!(invocation.compiler_path, compiler.display().to_string()); + assert_eq!(invocation.rustc_identity, "rustc 1.93.0; host: x"); + } + + /// RED CONTROLS: conflicting AMBIENT values on every channel are planted in this + /// process's environment and the spawn's table must override each one. Without the + /// binding, cargo would have read the planted values and the receipt would have stayed + /// falsely unchanged. The planted values are removed again so sibling tests are not + /// steered (the harness runs this crate's tests single-threaded, `.cargo/config.toml`). + #[test] + fn planted_ambient_flags_compiler_and_wrappers_cannot_reach_the_probe_build() { + let planted = [ + (CARGO_ENCODED_RUSTFLAGS_ENV, "-C\x1fopt-level=0"), + ("RUSTFLAGS", "-C opt-level=0"), + (RUSTC_ENV, "/planted/other-rustc"), + ("CARGO_BUILD_RUSTC", "/planted/config-rustc"), + (RUSTC_WRAPPER_ENV, "/planted/wrapper"), + ("CARGO_BUILD_RUSTC_WRAPPER", "/planted/config-wrapper"), + (RUSTC_WORKSPACE_WRAPPER_ENV, "/planted/workspace-wrapper"), + ]; + for (name, value) in planted { + std::env::set_var(name, value); + } + let compiler = Path::new("/toolchain/bin/rustc"); + let (command, invocation) = probe_cargo_command_bound( + Path::new("/tmp/probe-crate"), + Path::new("/tmp/workspace"), + compiler, + "rustc 1.93.0", + ); + let resolved_named = resolve_probe_compiler(); + for (name, _) in planted { + std::env::remove_var(name); + } + assert_eq!( + env_of(&command, CARGO_ENCODED_RUSTFLAGS_ENV), + Some(Some(WARNING_DENIAL_ENCODED_RUSTFLAGS.to_string())), + "a planted encoded flag is overridden on the spawn" + ); + assert_eq!( + env_of(&command, "RUSTFLAGS"), + Some(Some(WARNING_DENIAL_RUSTFLAGS.to_string())) + ); + assert_eq!( + env_of(&command, RUSTC_ENV), + Some(Some(compiler.display().to_string())), + "a planted RUSTC is overridden by the bound executable" + ); + assert_eq!( + env_of(&command, RUSTC_WRAPPER_ENV), + Some(Some(String::new())) + ); + assert_eq!( + env_of(&command, RUSTC_WORKSPACE_WRAPPER_ENV), + Some(Some(String::new())) + ); + assert_eq!(invocation.rustflags, WARNING_DENIAL_RUSTFLAGS); + // A planted RUSTC that is not a file is a typed refusal from the resolver, never a + // bare name handed on to cargo. + let refusal = resolved_named.expect_err("a RUSTC naming no file must refuse"); + assert!( + refusal.starts_with("ProbeCompilerUnresolved"), + "typed refusal, got: {refusal}" + ); + } + + #[test] + fn warning_headers_are_counted_and_errors_are_not() { + let stderr = "warning: unused import: `x`\n --> src/a.rs:1:5\nerror[E0308]: mismatched types\n --> src/b.rs:2:1\nwarning: `probe` (lib) generated 1 warning\n"; + assert_eq!(warning_header_count(stderr), 2); + assert_eq!(warning_header_count("error: could not compile"), 0); + assert_eq!(warning_header_count(""), 0); + } + #[test] fn cargo_verdict_summary_renders_the_diagnostic_a_non_zero_run_already_holds() { let attributed = CargoVerdict::Completed { @@ -2030,6 +2377,7 @@ mod tests { stderr_tail: "error: could not compile `probe`".to_string(), probe_line: Some("--> src/fixture.rs:1:1".to_string()), probe_diagnostic: Some("error[E0308]: mismatched types".to_string()), + warning_count: 0, }; let summary = cargo_verdict_summary(&attributed); assert!( @@ -2045,6 +2393,7 @@ mod tests { stderr_tail: "error[E0433]: failed to resolve".to_string(), probe_line: None, probe_diagnostic: None, + warning_count: 0, }; let summary = cargo_verdict_summary(&unattributed); assert!( @@ -2057,6 +2406,7 @@ mod tests { stderr_tail: String::new(), probe_line: None, probe_diagnostic: None, + warning_count: 0, }; assert_eq!(cargo_verdict_summary(&green), "Completed status=0"); } @@ -2422,6 +2772,7 @@ error: could not compile `probe` (lib) due to 1 previous error stderr_tail: String::new(), probe_line: Some("--> src/fixture_probe.rs:13:5".to_string()), probe_diagnostic: diagnostic.map(|value| value.to_string()), + warning_count: 0, }, }; let pair_with = |diagnostic: Option<&str>| FixtureDiscrimination { @@ -2452,6 +2803,7 @@ error: could not compile `probe` (lib) due to 1 previous error stderr_tail: String::new(), probe_line: None, probe_diagnostic: None, + warning_count: 0, }; for mutation in [ MutationVerdict::NotAttempted { diff --git a/src/v1/stage0/src/extdeps_cargo.rs b/src/v1/stage0/src/extdeps_cargo.rs index ac6b547cab0..92f61b6cef9 100644 --- a/src/v1/stage0/src/extdeps_cargo.rs +++ b/src/v1/stage0/src/extdeps_cargo.rs @@ -241,6 +241,7 @@ pub enum CargoEnvironmentVariable { CargoTargetDirEnv, CargoBuildJobsEnv, RustcWrapperEnv, + RustflagsEnv, } pub fn cargo_environment_variable_name(variable: CargoEnvironmentVariable) -> String { @@ -249,6 +250,7 @@ pub fn cargo_environment_variable_name(variable: CargoEnvironmentVariable) -> St CargoEnvironmentVariable::CargoTargetDirEnv => "CARGO_TARGET_DIR".to_string(), CargoEnvironmentVariable::CargoBuildJobsEnv => "CARGO_BUILD_JOBS".to_string(), CargoEnvironmentVariable::RustcWrapperEnv => "RUSTC_WRAPPER".to_string(), + CargoEnvironmentVariable::RustflagsEnv => "RUSTFLAGS".to_string(), } } @@ -303,3 +305,5 @@ pub struct CargoTargetDirEnv; pub struct CargoBuildJobsEnv; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct RustcWrapperEnv; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct RustflagsEnv; diff --git a/src/v2/test/v2_native_route_test.dag b/src/v2/test/v2_native_route_test.dag index d4b5cbe7bdd..e20f87c2172 100644 --- a/src/v2/test/v2_native_route_test.dag +++ b/src/v2/test/v2_native_route_test.dag @@ -20,11 +20,13 @@ import gunbc.witness_v2_native_route { NativeRouteMalformedControl, MalformedSpecimenRefused, MalformedSpecimenAccepted, NativeRouteOldRouteControl, OldRouteWithdrawn, OldRouteFalsifierArmed, OldRouteControlAbsent, NativeRouteReceipt, + NativeRouteEmittedBuild, NativeRouteAdmission, NativeRouteAdmitted, NativeRouteRefused, required_v2_native_job_name, native_route_admission, native_route_admitted, native_route_admission_refused_with, native_route_admission_summary } +import gunbc.repo_self_build { repo_self_warning_denial_rustflags } import v2.compiler.compile { NativeTestVerdict, NativeTestPassed, NativeTestReturnedFalse, NativeTestReturnedOther, NativeTestRefused, @@ -81,6 +83,31 @@ fn file_refusal_row(path: Symbol, head: Symbol, fatal: Symbol) -> NativeTestFile NativeTestFileRefusal { path: path, head_reason: head, fatal_reason: fatal } } +// The emitted build as the lane records it on a clean run: cargo's argv, the denial set by the +// lane (read from the same row admission compares against, so the fixture cannot drift from the +// authority), rustc's identity, status 0, no warning header. +fn clean_emitted_build() -> NativeRouteEmittedBuild { + NativeRouteEmittedBuild { + cargo_argv: ["cargo", "build", "--release", "--manifest-path", "/emitted/Cargo.toml"], + rustflags: repo_self_warning_denial_rustflags(), + compiler_path: "/toolchain/bin/rustc", + rustc_identity: "rustc 1.93.0 (254b59607 2026-01-19); host: aarch64-unknown-linux-gnu", + exit_status: 0, + warning_count: 0 + } +} + +fn emitted_build_with(rustflags: String, rustc_identity: String, exit_status: Int, warning_count: Int) -> NativeRouteEmittedBuild { + NativeRouteEmittedBuild { + cargo_argv: clean_emitted_build().cargo_argv, + rustflags: rustflags, + compiler_path: clean_emitted_build().compiler_path, + rustc_identity: rustc_identity, + exit_status: exit_status, + warning_count: warning_count + } +} + // A receipt with every well-formedness field recorded and both live controls observed; the // universe, module-source index, population and file-refusal populations are the arguments // under test. @@ -107,7 +134,8 @@ fn receipt_over( path: "target/v2-native-lane/malformed-control-root/poison.dag", reason: ^tokenize_lex_e1_unterminated_string }, - old_route_control: OldRouteWithdrawn { withdrawn_executable: "/seed/gunbc" } + old_route_control: OldRouteWithdrawn { withdrawn_executable: "/seed/gunbc" }, + emitted_build: clean_emitted_build() } } @@ -430,7 +458,8 @@ test fn the_wrong_job_name_is_refused() -> Bool { false_control: green_receipt().false_control, true_control: green_receipt().true_control, malformed_control: green_receipt().malformed_control, - old_route_control: green_receipt().old_route_control + old_route_control: green_receipt().old_route_control, + emitted_build: green_receipt().emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -453,7 +482,8 @@ test fn an_unrecorded_tested_tree_is_refused() -> Bool { false_control: green_receipt().false_control, true_control: green_receipt().true_control, malformed_control: green_receipt().malformed_control, - old_route_control: green_receipt().old_route_control + old_route_control: green_receipt().old_route_control, + emitted_build: green_receipt().emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -476,7 +506,8 @@ test fn an_unrecorded_preparation_seed_is_refused() -> Bool { false_control: green_receipt().false_control, true_control: green_receipt().true_control, malformed_control: green_receipt().malformed_control, - old_route_control: green_receipt().old_route_control + old_route_control: green_receipt().old_route_control, + emitted_build: green_receipt().emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -499,7 +530,8 @@ test fn an_unrecorded_emitted_closure_is_refused() -> Bool { false_control: green_receipt().false_control, true_control: green_receipt().true_control, malformed_control: green_receipt().malformed_control, - old_route_control: green_receipt().old_route_control + old_route_control: green_receipt().old_route_control, + emitted_build: green_receipt().emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -523,7 +555,8 @@ test fn an_unrecorded_route_identity_is_refused() -> Bool { false_control: base.false_control, true_control: base.true_control, malformed_control: base.malformed_control, - old_route_control: base.old_route_control + old_route_control: base.old_route_control, + emitted_build: base.emitted_build } let no_identity = NativeRouteReceipt { job: base.job, @@ -539,7 +572,8 @@ test fn an_unrecorded_route_identity_is_refused() -> Bool { false_control: base.false_control, true_control: base.true_control, malformed_control: base.malformed_control, - old_route_control: base.old_route_control + old_route_control: base.old_route_control, + emitted_build: base.emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: no_path), @@ -906,7 +940,8 @@ test fn a_missing_false_control_is_refused() -> Bool { false_control: Absent, true_control: base.true_control, malformed_control: base.malformed_control, - old_route_control: base.old_route_control + old_route_control: base.old_route_control, + emitted_build: base.emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -930,7 +965,8 @@ test fn a_passed_false_control_is_refused() -> Bool { false_control: Present { value: NativeTestPassed }, true_control: base.true_control, malformed_control: base.malformed_control, - old_route_control: base.old_route_control + old_route_control: base.old_route_control, + emitted_build: base.emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -954,7 +990,8 @@ test fn a_missing_true_control_is_refused() -> Bool { false_control: base.false_control, true_control: Absent, malformed_control: base.malformed_control, - old_route_control: base.old_route_control + old_route_control: base.old_route_control, + emitted_build: base.emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -978,7 +1015,8 @@ test fn a_falsified_true_control_is_refused() -> Bool { false_control: base.false_control, true_control: Present { value: NativeTestReturnedFalse }, malformed_control: base.malformed_control, - old_route_control: base.old_route_control + old_route_control: base.old_route_control, + emitted_build: base.emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -1002,7 +1040,8 @@ test fn an_accepted_malformed_specimen_is_refused() -> Bool { false_control: base.false_control, true_control: base.true_control, malformed_control: MalformedSpecimenAccepted, - old_route_control: base.old_route_control + old_route_control: base.old_route_control, + emitted_build: base.emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -1026,7 +1065,8 @@ test fn an_absent_old_route_control_is_refused() -> Bool { false_control: base.false_control, true_control: base.true_control, malformed_control: base.malformed_control, - old_route_control: OldRouteControlAbsent + old_route_control: OldRouteControlAbsent, + emitted_build: base.emitted_build } native_route_admission_refused_with( admission: native_route_admission(receipt: receipt), @@ -1034,6 +1074,88 @@ test fn an_absent_old_route_control_is_refused() -> Bool { ) } +// ── admission: the emitted build ─────────────────────────────────────────────────────────── + +fn receipt_with_build(build: NativeRouteEmittedBuild) -> NativeRouteReceipt { + let base = green_receipt() + NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: base.false_control, + true_control: base.true_control, + malformed_control: base.malformed_control, + old_route_control: base.old_route_control, + emitted_build: build + } +} + +test fn an_unbound_compiler_is_refused() -> Bool { + let base = clean_emitted_build() + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_with_build( + build: NativeRouteEmittedBuild { + cargo_argv: base.cargo_argv, + rustflags: base.rustflags, + compiler_path: "", + rustc_identity: base.rustc_identity, + exit_status: base.exit_status, + warning_count: base.warning_count + } + )), + cause_name: "emitted_build_compiler_unbound" + ) + && !native_route_admission_refused_with( + admission: native_route_admission(receipt: green_receipt()), + cause_name: "emitted_build_compiler_unbound" + ) +} + +test fn an_unrecorded_rustc_identity_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_with_build( + build: emitted_build_with(rustflags: repo_self_warning_denial_rustflags(), rustc_identity: "", exit_status: 0, warning_count: 0) + )), + cause_name: "emitted_build_rustc_unrecorded" + ) +} + +// THE DENIAL IS COMPARED BY EQUALITY: an inherited empty RUSTFLAGS and a RUSTFLAGS carrying the +// denial beside something else are both a different build environment from the one the receipt +// claims, and both refuse by the same name. +test fn rustflags_without_the_exact_denial_are_refused() -> Bool { + let inherited_none = native_route_admission(receipt: receipt_with_build( + build: emitted_build_with(rustflags: "", rustc_identity: "rustc 1.93.0", exit_status: 0, warning_count: 0) + )) + let widened = native_route_admission(receipt: receipt_with_build( + build: emitted_build_with(rustflags: concat(repo_self_warning_denial_rustflags(), " -C opt-level=3"), rustc_identity: "rustc 1.93.0", exit_status: 0, warning_count: 0) + )) + native_route_admission_refused_with(admission: inherited_none, cause_name: "emitted_build_warnings_not_denied") + && native_route_admission_refused_with(admission: widened, cause_name: "emitted_build_warnings_not_denied") + && !native_route_admission_refused_with( + admission: native_route_admission(receipt: green_receipt()), + cause_name: "emitted_build_warnings_not_denied" + ) +} + +test fn a_warning_or_a_non_zero_status_on_the_emitted_build_is_refused() -> Bool { + let warned = native_route_admission(receipt: receipt_with_build( + build: emitted_build_with(rustflags: repo_self_warning_denial_rustflags(), rustc_identity: "rustc 1.93.0", exit_status: 0, warning_count: 1) + )) + let failed = native_route_admission(receipt: receipt_with_build( + build: emitted_build_with(rustflags: repo_self_warning_denial_rustflags(), rustc_identity: "rustc 1.93.0", exit_status: 101, warning_count: 0) + )) + native_route_admission_refused_with(admission: warned, cause_name: "emitted_build_not_clean") + && native_route_admission_refused_with(admission: failed, cause_name: "emitted_build_not_clean") +} + // ── admission: the fold collects every violated clause ───────────────────────────────────── test fn one_red_run_names_every_violated_clause() -> Bool { @@ -1051,7 +1173,8 @@ test fn one_red_run_names_every_violated_clause() -> Bool { false_control: Absent, true_control: Absent, malformed_control: MalformedSpecimenAccepted, - old_route_control: OldRouteControlAbsent + old_route_control: OldRouteControlAbsent, + emitted_build: NativeRouteEmittedBuild { cargo_argv: [], rustflags: "", compiler_path: "", rustc_identity: "", exit_status: 101, warning_count: 2 } }) native_route_admission_refused_with(admission: admission, cause_name: "job_name_mismatch") && native_route_admission_refused_with(admission: admission, cause_name: "tested_tree_unrecorded") @@ -1065,6 +1188,10 @@ test fn one_red_run_names_every_violated_clause() -> Bool { && native_route_admission_refused_with(admission: admission, cause_name: "true_control_not_observed") && native_route_admission_refused_with(admission: admission, cause_name: "malformed_control_accepted_by_route") && native_route_admission_refused_with(admission: admission, cause_name: "old_route_control_never_installed") + && native_route_admission_refused_with(admission: admission, cause_name: "emitted_build_rustc_unrecorded") + && native_route_admission_refused_with(admission: admission, cause_name: "emitted_build_compiler_unbound") + && native_route_admission_refused_with(admission: admission, cause_name: "emitted_build_warnings_not_denied") + && native_route_admission_refused_with(admission: admission, cause_name: "emitted_build_not_clean") } test fn the_admission_summary_names_every_violated_clause() -> Bool { @@ -1082,6 +1209,7 @@ test fn the_admission_summary_names_every_violated_clause() -> Bool { false_control: Absent, true_control: Absent, malformed_control: MalformedSpecimenAccepted, - old_route_control: OldRouteControlAbsent - })) == "refused: job_name_mismatch, tested_tree_unrecorded, preparation_seed_unrecorded, emitted_closure_unrecorded, route_identity_unrecorded, universe_underived, positive_population_empty, required_native_pass_regressed, false_control_not_observed, true_control_not_observed, malformed_control_accepted_by_route, old_route_control_never_installed" + old_route_control: OldRouteControlAbsent, + emitted_build: NativeRouteEmittedBuild { cargo_argv: [], rustflags: "", compiler_path: "", rustc_identity: "", exit_status: 101, warning_count: 2 } + })) == "refused: job_name_mismatch, tested_tree_unrecorded, preparation_seed_unrecorded, emitted_closure_unrecorded, route_identity_unrecorded, universe_underived, positive_population_empty, required_native_pass_regressed, false_control_not_observed, true_control_not_observed, malformed_control_accepted_by_route, old_route_control_never_installed, emitted_build_rustc_unrecorded, emitted_build_compiler_unbound, emitted_build_warnings_not_denied, emitted_build_not_clean" }