diff --git a/.gitignore b/.gitignore index 91ba39f4804..1f23bc064a9 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,3 @@ src/v2/test/claim/manual/sg2_scratch_probe.dag # Regenerable; not committed. Whitelist: ci.yml, .gitignore, .md docs (ROADMAP.md, DESIGN.md, docs/plans/). /.github/fleet-runner-deploy.manifest /provisioning/srv3/user-data -/provisioning/srv3/dnsmasq.conf diff --git a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag index 0897a73b72b..d9c2f070376 100644 --- a/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag +++ b/dsl/extdeps/bmc/webui/nbd_proxy_serve.dag @@ -20,12 +20,17 @@ import extdeps.languages.bash.program { serialize_bash, } import extdeps.bmc.webui.nbd_proxy { - extdeps_external_authority_anchor, nbd_proxy_role_assignment, NbdServer, } +import extdeps.uri { Uri, Https } -data nbd_proxy_serve_transport_authority: ExternalAuthority = extdeps_external_authority_anchor +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "github.com/openbmc/jsnbd" + } +} data bmcweb_session_token_env_ref: NonEmptyStr = concat("$", "BMCWEB_SESSION_TOKEN") as NonEmptyStr diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index a5a5d0ac296..0ab07ecd0c1 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -72,6 +72,19 @@ pub const UNIFIED_CLAIM_VERIFICATION_MODULE: &str = "v2.std.verification"; pub const BOOL_WITNESS_CLAIM_TYPE: &str = "BoolWitnessClaim"; pub const NODE_CORPUS_TYPE: &str = "NodeCorpus"; +// cargo's build-output dir (a `target` dir beside a Cargo.toml) is realization +// output, not source: a corpus copy materialized under it (e.g. +// target/func_env_semantic_baseline_corpus/dsl/**) must never enter a module +// index alongside the tree it was copied from. A source root passed FROM +// inside target/ is still walked — only descent into the output dir is refused. +pub(crate) fn is_cargo_target_output_dir( + parent: &std::path::Path, + child: &std::path::Path, +) -> bool { + child.file_name().and_then(|n| n.to_str()) == Some("target") + && parent.join("Cargo.toml").is_file() +} + fn collect_dag_files(dir: &std::path::Path, files: &mut Vec) { let mut entries: Vec<_> = std::fs::read_dir(dir) .unwrap_or_else(|e| panic!("failed to read dir {:?}: {}", dir, e)) @@ -81,6 +94,9 @@ fn collect_dag_files(dir: &std::path::Path, files: &mut Vec) for entry in entries { let path = entry.path(); if path.is_dir() { + if is_cargo_target_output_dir(dir, &path) { + continue; + } collect_dag_files(&path, files); } else if path.extension().map(|e| e == "dag").unwrap_or(false) { files.push(path); @@ -174,6 +190,14 @@ pub fn build_module_path_index(source_roots: &[String]) -> HashMap ModuleSourceIndex { .unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e)); if let Some(module_path) = extract_module_path(&content) { let rel_path = path.to_string_lossy().to_string(); + if let Some(existing) = index.get(&module_path) { + if existing.path != rel_path { + panic!( + "module-path collision: module '{}' is declared by both '{}' and '{}' — one module, one authority (DESIGN §3); silent last-root-wins shadowing broke the floor (extdeps.shell, 2026-07-01) — de-fork or rename one side", + module_path, existing.path, rel_path + ); + } + } index.insert( module_path, Rc::new(v1_compiler_compile::SourceFile { @@ -2876,6 +2908,9 @@ pub(crate) fn collect_dag_files_tolerant(dir: &Path, out: &mut Vec) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { + if is_cargo_target_output_dir(dir, &path) { + continue; + } collect_dag_files_tolerant(&path, out); } else if path.extension().and_then(|e| e.to_str()) == Some("dag") { out.push(path); @@ -8533,9 +8568,77 @@ mod module_path_index_tests { } #[test] - fn co_root_overlay_last_root_wins_on_duplicate_module_path() { + fn extdeps_shell_resolves_to_the_dsl_authority() { let path = source_path_for_module_path("extdeps.shell".to_string()); - assert_eq!(path, "src/v2/extdeps/shell.dag"); + assert_eq!(path, "dsl/extdeps/shell/shell.dag"); + } + + #[test] + fn duplicate_module_path_across_roots_refuses_loudly() { + let dir = std::env::temp_dir().join(format!( + "gunbc-module-collision-wall-{}", + std::process::id() + )); + let root_a = dir.join("root_a"); + let root_b = dir.join("root_b"); + std::fs::create_dir_all(&root_a).unwrap(); + std::fs::create_dir_all(&root_b).unwrap(); + std::fs::write(root_a.join("m.dag"), "module collision.example\n").unwrap(); + std::fs::write(root_b.join("m.dag"), "module collision.example\n").unwrap(); + let roots = vec![ + root_a.to_string_lossy().into_owned(), + root_b.to_string_lossy().into_owned(), + ]; + // RED control: same module declared in two files refuses loudly. + let refused = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + super::build_module_index(&roots) + })) + .is_err(); + // GREEN control: distinct modules build fine. + std::fs::write(root_b.join("m.dag"), "module collision.other\n").unwrap(); + let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + super::build_module_index(&roots) + })) + .is_ok(); + std::fs::remove_dir_all(&dir).ok(); + assert!(refused, "collision must refuse loudly, not shadow silently"); + assert!(built, "distinct modules must still index"); + } + + #[test] + fn cargo_target_dir_output_never_enters_the_module_index() { + let dir = + std::env::temp_dir().join(format!("gunbc-target-dir-exclusion-{}", std::process::id())); + let root = dir.join("root"); + let baseline = root.join("target").join("baseline_corpus"); + std::fs::create_dir_all(&baseline).unwrap(); + std::fs::write(root.join("m.dag"), "module corpus.example\n").unwrap(); + std::fs::write(baseline.join("m.dag"), "module corpus.example\n").unwrap(); + let roots = vec![root.to_string_lossy().into_owned()]; + // With a Cargo.toml beside it, target/ is build output: the corpus + // copy is skipped and the source file indexes alone (the CI regression: + // target/func_env_semantic_baseline_corpus tripped the collision wall). + std::fs::write(root.join("Cargo.toml"), "[package]\n").unwrap(); + let indexed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + super::build_module_index(&roots) + })); + // RED control: without Cargo.toml the same layout is two source files + // declaring one module — the wall must still refuse. + std::fs::remove_file(root.join("Cargo.toml")).unwrap(); + let refused = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + super::build_module_index(&roots) + })) + .is_err(); + std::fs::remove_dir_all(&dir).ok(); + let index = indexed.expect("cargo target output must be excluded, not collide"); + assert!( + index.contains_key("corpus.example"), + "the source-tree declaration must still index" + ); + assert!( + refused, + "a plain (non-cargo) target dir is source like any other — collision must refuse" + ); } #[test] diff --git a/src/v1/tests/src/dependency_pool_index_compile_test.rs b/src/v1/tests/src/dependency_pool_index_compile_test.rs index 85b57424789..fa51cee364e 100644 --- a/src/v1/tests/src/dependency_pool_index_compile_test.rs +++ b/src/v1/tests/src/dependency_pool_index_compile_test.rs @@ -83,21 +83,31 @@ fn primary_precedence_within_root_duplicate_module_path_panics() { rm_rf(&out); } +// The live-tree extdeps.shell collision fixture is gone (the src/v2 fork was +// deleted — one module, one authority, DESIGN §3), so the strict-mode RED +// control synthesizes its own cross-root duplicate instead of depending on a +// defect persisting in the real corpus. #[test] -fn strict_dependency_pool_index_panics_on_cross_root_extdeps_shell_collision() { +fn strict_dependency_pool_index_panics_on_cross_root_collision() { let Some(gunbc) = gunbc_bin() else { eprintln!("skipping: release gunbc binary not found"); return; }; let ws = workspace_root(); + let root_a = temp_dir("strict-cross-root-a"); + let root_b = temp_dir("strict-cross-root-b"); + fs::write( + root_a.join("dup.dag"), + "module duplicate.across.roots\nfn a() -> Int { 1 }\n", + ) + .expect("write root_a dup.dag"); + fs::write( + root_b.join("dup.dag"), + "module duplicate.across.roots\nfn b() -> Int { 2 }\n", + ) + .expect("write root_b dup.dag"); let out = temp_dir("strict-cross-root-out"); - let output = compile_with_roots( - &gunbc, - &ws, - &[&ws.join("dsl"), &ws.join("src/v2")], - "strict", - &out, - ); + let output = compile_with_roots(&gunbc, &ws, &[&root_a, &root_b], "strict", &out); let combined = format!( "{}{}", String::from_utf8_lossy(&output.stdout), @@ -105,13 +115,35 @@ fn strict_dependency_pool_index_panics_on_cross_root_extdeps_shell_collision() { ); assert!( !output.status.success(), - "strict mode must panic on cross-root extdeps.shell collision" + "strict mode must panic on a cross-root duplicate module path; got success" + ); + assert!( + combined.contains("duplicate module path"), + "expected duplicate module path panic; got:\n{combined}" + ); + // GREEN control: the same fixture under primary-precedence indexes + // first-root-wins instead of refusing at the index layer. + let out_pp = temp_dir("strict-cross-root-pp-out"); + let output_pp = compile_with_roots( + &gunbc, + &ws, + &[&root_a, &root_b], + "primary-precedence", + &out_pp, + ); + let combined_pp = format!( + "{}{}", + String::from_utf8_lossy(&output_pp.stdout), + String::from_utf8_lossy(&output_pp.stderr) ); assert!( - combined.contains("extdeps.shell"), - "expected extdeps.shell collision diagnostic; got:\n{combined}" + !combined_pp.contains("duplicate module path"), + "primary-precedence must not refuse the cross-root duplicate at the index layer; got:\n{combined_pp}" ); + rm_rf(&root_a); + rm_rf(&root_b); rm_rf(&out); + rm_rf(&out_pp); } #[test] diff --git a/src/v2/extdeps/shell.dag b/src/v2/extdeps/shell.dag deleted file mode 100644 index c24c04ed9d6..00000000000 --- a/src/v2/extdeps/shell.dag +++ /dev/null @@ -1,45 +0,0 @@ -module extdeps.shell - -import std.types { NonEmptyStr } -import extdeps.external_authority { ExternalAuthority } -import extdeps.uri { Uri, Https } - -data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { - uri: Uri { - scheme: Https - locator: "pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html" - } -} - -service shell.Env { - operation Get { - input { name: NonEmptyStr } - output { value: String? from "stdout" } - readonly - transport shell { argv: ["printenv", "{name}"] } - exit { - 0 => Unit - 1 => Unit "Variable not set" - } - } -} - -service shell.Exec { - operation Run { - input { script: String } - output { - exit_code: Int from "exit_code" - success: Bool from "exit_success" - stdout: String from "stdout" - stderr: String from "stderr" - } - transport shell { argv: ["sh", "-c", "{script}"] } - exit { - 0 => Unit - nonzero => String "Script failed" - } - mock_response { - 0 => { exit_code: 0, exit_success: true, stdout: "(mock)", stderr: "" } "Mock shell execution" - } - } -}