//! 真实签名测试包通过 production prepare 编译与执行,并验证源码移除后的行为。 use super::*; use crate::compiler::{build, project::Project}; use std::{fs, path::Path, process::Command}; #[path = "build_fixture.rs"] mod fixture; struct EmptyStore; impl Store for EmptyStore { fn get(&self, _: &str, _: u64) -> Result>, String> { Ok(None) } fn put(&self, _: &[u8]) -> Result<(), String> { panic!("Worker prepare must not write cache") } } fn project(root: &Path) { for (path, source) in [ ("app", "value() (value: Int) { value = port.native() }\n"), ("port", "native() (value: Int) fails dever.api.Error\n"), ("adapter", "external go \"worker/main.go\" {}\n"), ("api", "cmd value = app.value\n"), ] { fixture::write(root, &format!("module/demo/value/{path}.dever"), source); } fixture::write( root, "module/demo/value/worker/main.go", "package main\nimport (\"context\"; _ \"embed\"; \"dever-worker/helper\")\n//go:embed note.txt\nvar note string\nfunc Native(_ context.Context, _ any, _ any) (any, error) { return map[string]any{\"value\":helper.Value()+Target()+int64(len(note))}, nil }\n", ); fixture::write( root, "module/demo/value/worker/helper/value.go", "package helper\nimport \"dever-worker/helper/internal/value\"\nfunc Value() int64 { return value.Get() }\n", ); fixture::write( root, "module/demo/value/worker/helper/internal/value/value.go", "package value\nfunc Get() int64 { return 40 }\n", ); fixture::write( root, "module/demo/value/worker/target_linux.go", "//go:build linux\npackage main\nfunc Target() int64 { return 1 }\n", ); fixture::write( root, "module/demo/value/worker/target_windows.go", "//go:build windows\npackage main\nfunc Target() int64 { return 99 }\n", ); fixture::write(root, "module/demo/value/worker/note.txt", "x"); } #[test] #[ignore = "explicit dev-Pod Go authoring fixture; no product tool fallback"] fn signed_go_pack_compiles_reproducibly_and_runs_native_after_source_removal() { let fixture = fixture::Fixture::new(); let root = fixture.root.join("project"); project(&root); let project = Project::load(&root).unwrap(); let program = project.check().unwrap(); let external = program.external_workers().next().unwrap(); let target = BuildTarget::host().unwrap(); let metadata = library::runtime::metadata(&fixture.signed, "go", target).unwrap(); let mut worker = Worker::from_external(external).unwrap(); worker.runtime = Some(metadata.pack.clone()); let mut lock = Lock::new(vec![]); lock.workers.push(worker); fixture::write(&root, "dever.lock", lock.encode().unwrap()); let prepare = || { crate::compiler::project::dependency::worker::prepare_with_store( &root, &program, target, &fixture.signed, &EmptyStore, ) }; let files = prepare().unwrap(); let repeated = prepare().unwrap(); assert_eq!( files .iter() .map(|file| (&file.path, &file.bytes, file.executable)) .collect::>(), repeated .iter() .map(|file| (&file.path, &file.bytes, file.executable)) .collect::>() ); assert!(!files.iter().any(|file| file.path.contains("/source/") || file.path.contains("/stdlib/") || file.path.ends_with("component.go"))); let binary = build::build_with_resources( &program, &project.sources, "main", &root.join("app"), target, &files, ) .unwrap(); // 工具与 SDK 篡改、闭包缺失和目标被排除都必须在交付 Worker 前失败。 let installation = fixture.root.join("installation"); let release = installation.join("versions/0.0.1"); let signature = release.join("manifest.sig"); let original = fs::read(&signature).unwrap(); fs::write(&signature, "00".repeat(64)).unwrap(); assert!(SignedResources::load(&installation, "0.0.1").is_err()); fs::write(&signature, original).unwrap(); for relative in [ "sdk/go/component.go".into(), format!("runtime/go/{}/runtime.pack", target.name()), format!("runtime/go/{}/manifest.json", target.name()), ] { let path = release.join(relative); let original = fs::read(&path).unwrap(); let mut changed = original.clone(); let middle = changed.len() / 2; changed[middle] ^= 1; fs::write(&path, changed).unwrap(); assert!( prepare() .err() .expect("tampered signed asset was accepted") .contains("signed resource bytes differ") ); fs::write(&path, original).unwrap(); let saved = fixture.root.join("missing-asset"); fs::rename(&path, &saved).unwrap(); assert!(prepare().is_err(), "missing asset used a host fallback"); fs::rename(saved, path).unwrap(); } let main = root.join("module/demo/value/worker/main.go"); let source = fs::read_to_string(&main).unwrap(); fs::write( &main, source.replace("dever-worker/helper", "example.com/unlocked"), ) .unwrap(); assert!( prepare() .err() .expect("unlocked import was accepted") .contains("locked module graph") ); fs::write(&main, format!("//go:build windows\n{source}")).unwrap(); assert!( prepare() .err() .expect("excluded entry was accepted") .contains("excluded by target") ); fs::write(&main, &source).unwrap(); for import in ["internal/abi", "dever-worker/helper/internal/value"] { fs::write( &main, source.replace("_ \"embed\"", &format!("_ \"embed\"; _ {import:?}")), ) .unwrap(); assert!( prepare() .err() .expect("private import was accepted") .contains("Go internal import") ); } fs::write(&main, &source).unwrap(); fixture::write( &root, "module/demo/value/worker/cgo.go", "package main\nimport \"C\"\n", ); assert!(prepare().is_err()); fs::remove_file(root.join("module/demo/value/worker/cgo.go")).unwrap(); let descriptor: Descriptor = serde_json::from_value(fixture.descriptor.clone()).unwrap(); let mut missing = fixture.files.clone(); missing.remove("stdlib/runtime.a"); assert!(descriptor.validate(&missing, target).is_err()); let mut missing = fixture.files.clone(); missing.remove("bin/compile"); assert!(descriptor.validate(&missing, target).is_err()); let mut version = descriptor.clone(); let actual = version.go_version.split_whitespace().nth(2).unwrap(); version.go_version = version.go_version.replace(actual, "go0.0.0"); assert!( version .validate(&fixture.files, target) .unwrap_err() .contains("archive target/compiler version differs") ); let mut wrong = fixture.descriptor.clone(); wrong["goarch"] = "unsupported".into(); assert!( serde_json::from_value::(wrong) .unwrap() .validate(&fixture.files, target) .is_err() ); fs::remove_dir_all(root.join("module")).unwrap(); fs::remove_dir_all(fixture.root.join("installation")).unwrap(); for _ in 0..2 { let output = Command::new(&binary) .env_clear() .args(["demo.value.value", "{}"]) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!( output.stdout, b"{\"code\":0,\"message\":\"ok\",\"data\":42}\n" ); } }