feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
1 parent 70fa73cdc2
commit ac458ffa7f
960 files changed
+221319 -1163

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
use super::{Action, BuildTarget, OsString, arguments_for};
#[test]
fn build_target_is_explicit_or_defaults_to_host() {
for options in [
vec!["--output", "program", "--target", "linux-aarch64"],
vec!["--target", "linux-aarch64", "--output", "program"],
vec!["--output", "program"],
] {
let explicit = options.contains(&"--target");
let arguments: Vec<_> = [vec!["build", "project"], options]
.concat()
.into_iter()
.map(OsString::from)
.collect();
let (root, action) = arguments_for(&arguments).unwrap();
assert_eq!(root.to_str(), Some("project"));
let Action::Build { output, target } = action else {
panic!("expected build");
};
assert_eq!(output.to_str(), Some("program"));
assert_eq!(
target,
if explicit {
BuildTarget::LinuxAarch64
} else {
BuildTarget::host().unwrap()
}
);
}
}
#[test]
fn build_rejects_missing_unknown_duplicate_and_misplaced_options() {
for options in [
vec![],
vec!["--output"],
vec!["--target"],
vec!["--target", "linux-aarch64"],
vec!["--output", "program", "--target", "arm64"],
vec![
"--output",
"program",
"--target",
"linux-aarch64",
"--target",
"linux-aarch64",
],
vec!["--output", "program", "--output", "second"],
vec!["--output", "--target", "linux-aarch64"],
vec!["--output", "program", "--unknown"],
vec!["--output", "program", "extra"],
vec!["--output", ""],
] {
let arguments: Vec<_> = [vec!["build", "project"], options]
.concat()
.into_iter()
.map(OsString::from)
.collect();
assert!(arguments_for(&arguments).is_err(), "accepted {arguments:?}");
}
for command in ["run", "test", "check"] {
let arguments = [command, "project", "--target", "linux-aarch64"].map(OsString::from);
assert!(arguments_for(&arguments).is_err());
}
}
#[test]
fn lib_target_keeps_specs_and_rejects_option_drift() {
let arguments = [
"lib",
"add",
"project",
"pip:demo",
"--target",
"linux-aarch64",
]
.map(OsString::from);
let (_, action) = arguments_for(&arguments).unwrap();
let Action::Lib { specs, target, .. } = action else {
panic!("expected lib");
};
assert_eq!(specs, ["pip:demo"]);
assert_eq!(target, BuildTarget::LinuxAarch64);
for options in [
vec!["--target"],
vec!["--target", "arm64"],
vec!["--unknown"],
vec!["--target", "linux-aarch64", "--target", "linux-aarch64"],
] {
let arguments: Vec<_> = [vec!["lib", "update", "project"], options]
.concat()
.into_iter()
.map(OsString::from)
.collect();
assert!(arguments_for(&arguments).is_err());
}
}
+451
View File
@@ -0,0 +1,451 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
#[path = "../dever-tests/tests/support/process.rs"]
pub mod process;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "support/llvm_pack.rs"]
pub mod llvm_pack;
static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0);
struct TemporaryProject(PathBuf);
impl TemporaryProject {
fn new() -> Self {
let suffix = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
let root =
std::env::temp_dir().join(format!("dever-cms-project-{}-{suffix}", std::process::id()));
fs::create_dir(&root).unwrap();
Self(root)
}
fn copy_from(source: &Path) -> Self {
let project = Self::new();
copy_directory(&source.join("config"), &project.path().join("config"));
copy_directory(&source.join("module"), &project.path().join("module"));
let tests = source.join("test");
if tests.exists() {
copy_directory(&tests, &project.path().join("test"));
}
fs::create_dir(project.path().join("data")).unwrap();
project
}
fn package(source: &Path, executable: &Path) -> Self {
let project = Self::new();
copy_directory(&source.join("config"), &project.path().join("config"));
fs::copy(executable, project.path().join("cms-app")).unwrap();
project
}
fn path(&self) -> &Path {
&self.0
}
fn block_deployment_database(&self) {
// Package metadata needs valid JSON. A directory as the configured
// database proves tests use their own SQLite configuration instead.
fs::create_dir(self.path().join("data/deployment.db")).unwrap();
fs::write(
self.path().join("config/setting.json"),
r#"{"database":{"default":{"type":"sqlite","path":"data/deployment.db"}}}"#,
)
.unwrap();
}
}
impl Drop for TemporaryProject {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn copy_directory(source: &Path, target: &Path) {
fs::create_dir(target).unwrap();
for entry in fs::read_dir(source).unwrap() {
let entry = entry.unwrap();
let source = entry.path();
let target = target.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_directory(&source, &target);
} else {
fs::copy(source, target).unwrap();
}
}
}
fn dever(project: &Path, action: &str, arguments: &[&str]) -> Output {
dever_with_pid(project, action, arguments).1
}
fn dever_with_pid(project: &Path, action: &str, arguments: &[&str]) -> (u32, Output) {
output_with_pid(
Command::new(env!("CARGO_BIN_EXE_dever"))
.arg(action)
.arg(project)
.args(arguments),
)
}
fn output_with_pid(command: &mut Command) -> (u32, Output) {
let capture = TemporaryProject::new();
let stdout = capture.path().join("stdout");
let stderr = capture.path().join("stderr");
let mut child = command
.env_clear()
.stdout(Stdio::from(fs::File::create(&stdout).unwrap()))
.stderr(Stdio::from(fs::File::create(&stderr).unwrap()))
.spawn()
.unwrap();
let pid = child.id();
let status = process::wait(&mut child, Duration::from_secs(180)).unwrap();
(
pid,
Output {
status,
stdout: fs::read(stdout).unwrap(),
stderr: fs::read(stderr).unwrap(),
},
)
}
fn assert_test_artifacts_removed(pid: u32) {
let test_prefix = format!("dever-application-test-{pid}-");
let build_prefix = format!("dever-build-{pid}-");
let leftovers = fs::read_dir(std::env::temp_dir())
.unwrap()
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.filter(|name| name.starts_with(&test_prefix) || name.starts_with(&build_prefix))
.collect::<Vec<_>>();
assert!(
leftovers.is_empty(),
"temporary test artifacts remain: {leftovers:?}"
);
}
fn assert_success(output: &Output) {
assert!(
output.status.success(),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(unix)]
#[test]
fn clean_removes_only_stale_owned_dever_artifacts() {
use std::os::unix::fs::{MetadataExt, symlink};
use std::time::{SystemTime, UNIX_EPOCH};
let project = TemporaryProject::new();
let pid = u64::from(std::process::id()) + 1_000_000;
let suffix = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
let stale = std::env::temp_dir().join(format!("dever-build-{pid}-{suffix}"));
fs::create_dir(&stale).unwrap();
let owner = fs::metadata(&stale).unwrap().uid();
fs::write(
stale.join(".dever-owner"),
format!(
"dever-build-owner-v1\ncreated=1\npid={pid}\nowner={owner}\ncompiler={}\n",
env!("CARGO_PKG_VERSION"),
),
)
.unwrap();
fs::write(stale.join("program"), b"stale").unwrap();
let fresh_pid = pid + 1;
let fresh = std::env::temp_dir().join(format!("dever-build-{fresh_pid}-{suffix}"));
fs::create_dir(&fresh).unwrap();
let created = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
fs::write(
fresh.join(".dever-owner"),
format!(
"dever-build-owner-v1\ncreated={created}\npid={fresh_pid}\nowner={owner}\ncompiler={}\n",
env!("CARGO_PKG_VERSION"),
),
).unwrap();
let unknown = std::env::temp_dir().join(format!("dever-build-{}-{suffix}", pid + 2));
fs::create_dir(&unknown).unwrap();
let link = std::env::temp_dir().join(format!("dever-build-{}-{suffix}", pid + 3));
symlink(&unknown, &link).unwrap();
let mut active_process = Command::new("/bin/sleep").arg("60").spawn().unwrap();
let active_pid = u64::from(active_process.id());
let active = std::env::temp_dir().join(format!("dever-build-{active_pid}-{suffix}"));
fs::create_dir(&active).unwrap();
fs::write(
active.join(".dever-owner"),
format!(
"dever-build-owner-v1\ncreated=1\npid={active_pid}\nowner={owner}\ncompiler={}\n",
env!("CARGO_PKG_VERSION"),
),
)
.unwrap();
fs::write(active.join("program"), b"active").unwrap();
let legacy = project.path().join(format!(".dever-run-{pid}"));
fs::write(&legacy, b"stale legacy").unwrap();
let legacy_file = fs::OpenOptions::new().write(true).open(&legacy).unwrap();
legacy_file
.set_times(
fs::FileTimes::new().set_modified(UNIX_EPOCH + std::time::Duration::from_secs(1)),
)
.unwrap();
let fresh_legacy = project.path().join(format!(".dever-run-{}", pid + 1));
fs::write(&fresh_legacy, b"fresh legacy").unwrap();
let active_legacy = project.path().join(format!(".dever-run-{active_pid}"));
fs::write(&active_legacy, b"active legacy").unwrap();
let active_legacy_file = fs::OpenOptions::new()
.write(true)
.open(&active_legacy)
.unwrap();
active_legacy_file
.set_times(
fs::FileTimes::new().set_modified(UNIX_EPOCH + std::time::Duration::from_secs(1)),
)
.unwrap();
let explicit = project.path().join("application");
fs::write(&explicit, b"explicit output").unwrap();
let output = dever(project.path(), "clean", &[]);
active_process.kill().unwrap();
active_process.wait().unwrap();
assert_success(&output);
assert!(
String::from_utf8_lossy(&output.stdout).starts_with("cleaned 1 directories, 3 files, ")
);
assert!(!stale.exists());
assert!(fresh.exists());
assert!(unknown.exists());
assert!(link.symlink_metadata().is_ok());
assert!(active.exists());
assert!(!legacy.exists());
assert!(fresh_legacy.exists());
assert!(active_legacy.exists());
assert!(explicit.exists());
fs::remove_dir_all(fresh).unwrap();
fs::remove_file(link).unwrap();
fs::remove_dir_all(unknown).unwrap();
fs::remove_dir_all(active).unwrap();
}
#[test]
fn cms_project_checks_runs_and_builds_from_an_isolated_copy() {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
for format in ["dever", "md"] {
let project = TemporaryProject::copy_from(&workspace.join("examples/cms").join(format));
let settings_path = project.path().join("config/setting.json");
let production_settings = fs::read(&settings_path).unwrap();
project.block_deployment_database();
for _ in 0..2 {
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
assert_cms_tests(&output);
assert_test_artifacts_removed(pid);
}
fs::write(&settings_path, production_settings).unwrap();
fs::write(project.path().join("test/ignored.dever"), "not valid Dever").unwrap();
assert_success(&dever(project.path(), "check", &[]));
for _ in 0..2 {
assert_health_stdout(&dever(
project.path(),
"run",
&["--", "system.health.ping", "{}"],
));
}
let executable = build(project.path(), "cms-app");
let package = TemporaryProject::package(project.path(), &executable);
assert!(!package.path().join("module").exists());
for _ in 0..2 {
let output = output_with_pid(
Command::new(package.path().join("cms-app"))
.args(["system.health.ping", "{}"])
.current_dir(package.path()),
)
.1;
assert_health_stdout(&output);
}
assert!(fs::read_dir(project.path()).unwrap().all(|entry| {
!entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with(".dever-run-")
}));
}
}
#[test]
fn project_without_tests_reports_zero_and_succeeds() {
let project = TemporaryProject::new();
fs::create_dir(project.path().join("module")).unwrap();
fs::write(project.path().join("module/main.dever"), "main() () {}").unwrap();
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
assert_success(&output);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"running 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 not run\n"
);
assert_test_artifacts_removed(pid);
}
#[test]
fn mixed_suite_initializes_database_only_for_database_cases() {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let project = TemporaryProject::copy_from(&workspace.join("examples/cms/dever"));
project.block_deployment_database();
let health = project.path().join("test/system/health");
fs::create_dir_all(&health).unwrap();
fs::write(
health.join("ping.dever"),
"ping() () { assert_eq(\"ready\", \"ready\") }\n",
)
.unwrap();
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
assert_success(&output);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"running 6 tests\ntest content/category/lifecycle ... ok\ntest news/article/publish ... ok\ntest system/health/ping ... ok\ntest user/account/bootstrap ... ok\ntest user/account/validation ... ok\ntest user/session/lifecycle ... ok\n\ntest result: ok. 6 passed; 0 failed; 0 not run\n"
);
assert_test_artifacts_removed(pid);
}
#[test]
fn failed_test_reports_captured_output_and_does_not_stop_later_tests() {
let project = TemporaryProject::new();
fs::create_dir(project.path().join("module")).unwrap();
fs::create_dir_all(project.path().join("test/sample/case")).unwrap();
fs::write(project.path().join("module/main.dever"), "main() () {}").unwrap();
fs::write(
project.path().join("test/sample/case/fails.dever"),
r#"fails() () {
blocking(dever.io.println("failed output"))
assert_eq(1, 2)
}
"#,
)
.unwrap();
fs::write(
project.path().join("test/sample/case/passes.dever"),
r#"passes() () {
blocking(dever.io.println("hidden successful output"))
assert(true)
}
"#,
)
.unwrap();
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
assert!(!output.status.success());
assert!(output.stderr.is_empty());
let stdout = String::from_utf8_lossy(&output.stdout);
let failed = stdout.find("test sample/case/fails ... FAILED").unwrap();
let passed = stdout.find("test sample/case/passes ... ok").unwrap();
assert!(failed < passed, "{stdout}");
assert!(
stdout.contains("---- stdout ----\nfailed output\n"),
"{stdout}"
);
assert!(stdout.contains("---- stderr ----"), "{stdout}");
assert!(
stdout.contains("assert_eq failed: actual = 1, expected = 2"),
"{stdout}"
);
assert!(!stdout.contains("hidden successful output"), "{stdout}");
assert!(
stdout.ends_with("test result: FAILED. 1 passed; 1 failed; 0 not run\n"),
"{stdout}"
);
assert_test_artifacts_removed(pid);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn suite_compilation_failure_marks_every_discovered_case_failed() {
let project = TemporaryProject::new();
fs::create_dir(project.path().join("module")).unwrap();
fs::create_dir_all(project.path().join("test/sample/case")).unwrap();
fs::write(project.path().join("module/main.dever"), "main() () {}").unwrap();
fs::write(
project.path().join("test/sample/case/first.dever"),
"first() () { assert(true) }\n",
)
.unwrap();
fs::write(
project.path().join("test/sample/case/second.dever"),
"second() () { assert(true) }\n",
)
.unwrap();
let toolchain = TemporaryProject::new();
let compiler = llvm_pack::compiler(
Path::new(env!("CARGO_BIN_EXE_dever")),
toolchain.path(),
"dever",
);
let (pid, output) = output_with_pid(Command::new(compiler).arg("test").arg(project.path()));
assert!(!output.status.success());
assert!(output.stderr.is_empty());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("test sample/case/first ... FAILED"),
"{stdout}"
);
assert!(
stdout.contains("test sample/case/second ... FAILED"),
"{stdout}"
);
assert_eq!(
stdout.matches("cannot inspect runtime pack").count(),
2,
"{stdout}"
);
assert!(
stdout.ends_with("test result: FAILED. 0 passed; 2 failed; 0 not run\n"),
"{stdout}"
);
assert_test_artifacts_removed(pid);
assert!(!toolchain.path().join("cache").exists());
}
fn build(project: &Path, name: &str) -> PathBuf {
let executable = project.join(name);
let build = dever(
project,
"build",
&["--output", executable.to_str().unwrap()],
);
assert_success(&build);
assert!(executable.is_file());
executable
}
fn assert_health_stdout(output: &Output) {
assert_success(output);
let body: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(body["code"], 0);
assert_eq!(body["message"], "ok");
assert_eq!(body["data"]["status"], "ready");
}
fn assert_cms_tests(output: &Output) {
assert_success(output);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"running 5 tests\ntest content/category/lifecycle ... ok\ntest news/article/publish ... ok\ntest user/account/bootstrap ... ok\ntest user/account/validation ... ok\ntest user/session/lifecycle ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 not run\n"
);
}
File diff suppressed because it is too large. Load diff
+468
View File
@@ -0,0 +1,468 @@
use super::*;
use dever_cli::libs::build::{Inputs, Session, pack};
use serde::{Deserialize, Serialize};
#[path = "../../dever-tests/tests/support/sandbox.rs"]
pub(super) mod sandbox;
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
runtime_file: String,
build: pack::Descriptor,
build_file: String,
}
struct AuthorInputs {
root: PathBuf,
settings: Settings,
}
#[test]
fn signed_build_pack_binds_runtime_layout_tools_and_archive_digest() {
let runtime = registry_runtime(Ecosystem::Pip);
let mut descriptor = pack::Descriptor {
format: "dever-registry-build-v1".into(),
ecosystem: Ecosystem::Pip,
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: None,
sha256: "0".repeat(64),
};
descriptor.validate(&runtime, None).unwrap();
let manifest = pack::Manifest {
format: "dever-lib-build-v1".into(),
ecosystem: Ecosystem::Pip,
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: None,
executables: ["sh", "cc", "c++", "as", "ld", "ar", "ranlib", "make"]
.into_iter()
.map(|name| format!("rootfs/usr/bin/{name}"))
.collect(),
};
let mut files = manifest
.executables
.iter()
.map(|path| (path.clone(), b"#!/bin/sh\nexit 0\n".to_vec()))
.collect::<Vec<_>>();
files.push((
"dever-build.json".into(),
serde_json::to_vec(&manifest).unwrap(),
));
manifest.validate(&files, &descriptor).unwrap();
let mut changed = descriptor.clone();
changed.runtime_sha256 = "f".repeat(64);
assert!(changed.validate(&runtime, None).is_err());
assert!(manifest.validate(&files, &changed).is_err());
let mut changed = files.clone();
changed.push(("rootfs/etc/ld.so.preload".into(), b"injected".to_vec()));
assert!(manifest.validate(&changed, &descriptor).is_err());
let mut changed = manifest.clone();
changed.executables.push("rootfs/usr/bin/absent".into());
assert!(changed.validate(&files, &descriptor).is_err());
let mut archive = tar::Builder::new(flate2::write::GzEncoder::new(
Vec::new(),
flate2::Compression::default(),
));
for (path, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
archive
.append_data(&mut header, path, bytes.as_slice())
.unwrap();
}
let bytes = archive.into_inner().unwrap().finish().unwrap();
descriptor.sha256 = digest(&bytes);
pack::unpack(&bytes, &descriptor).unwrap();
let mut changed = bytes;
changed[0] ^= 1;
assert!(pack::unpack(&changed, &descriptor).is_err());
}
impl Inputs for AuthorInputs {
fn diagnostic(&self, output: &[u8]) {
eprintln!("{}", String::from_utf8_lossy(output));
}
fn runtime(&self, ecosystem: Ecosystem) -> Result<(RegistryRuntime, Vec<u8>), String> {
if ecosystem != Ecosystem::Pip {
return Err("fixture only supplies Python".into());
}
let bytes = fs::read(self.root.join(&self.settings.runtime_file))
.map_err(|error| error.to_string())?;
if digest(&bytes) != self.settings.runtime.pack.sha256 {
return Err("author runtime drift".into());
}
Ok((self.settings.runtime.clone(), bytes))
}
fn tools(&self, ecosystem: Ecosystem) -> Result<(pack::Descriptor, Vec<u8>), String> {
if ecosystem != Ecosystem::Pip {
return Err("fixture only supplies Python".into());
}
Ok((
self.settings.build.clone(),
fs::read(self.root.join(&self.settings.build_file))
.map_err(|error| error.to_string())?,
))
}
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
Ok(sandbox::assets(&workspace)
.into_iter()
.map(|asset| {
let _mode = asset.executable;
(
asset.path.trim_start_matches("sandbox/").into(),
asset.bytes,
)
})
.collect())
}
}
#[test]
#[ignore = "explicit author operation: assembles previously hashed native-build-inputs; no network or host execution"]
fn prepare_source_build_tool_fixture() {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let runtime: RegistryRuntime = serde_json::from_value(
serde_json::from_slice::<serde_json::Value>(
&fs::read(workspace.join("target/python-managed/config/setting.json")).unwrap(),
)
.unwrap()["runtime"]
.clone(),
)
.unwrap();
let bytes = fs::read(workspace.join("target/python-managed/runtime.pack")).unwrap();
assert_eq!(digest(&bytes), runtime.pack.sha256);
drop(bytes);
let root = workspace.join("target/source-build");
let descriptor = prepare_tool_pack(Ecosystem::Pip, &runtime, None, &root);
fs::write(
root.join("config/setting.json"),
serde_json::to_vec_pretty(&Settings {
runtime,
runtime_file: "../python-managed/runtime.pack".into(),
build: descriptor,
build_file: "build.pack".into(),
})
.unwrap(),
)
.unwrap();
}
pub(super) fn prepare_tool_pack(
ecosystem: Ecosystem,
runtime: &RegistryRuntime,
python: Option<&RegistryRuntime>,
root: &std::path::Path,
) -> pack::Descriptor {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let inputs = workspace.join("target/native-build-inputs");
let document: serde_json::Value =
serde_json::from_slice(&fs::read(inputs.join("inputs.json")).unwrap()).unwrap();
let mut descriptor = pack::Descriptor {
format: "dever-registry-build-v1".into(),
ecosystem: ecosystem.clone(),
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: python.map(|runtime| runtime.pack.sha256.clone()),
sha256: "0".repeat(64),
};
descriptor.validate(runtime, python).unwrap();
let manifest = pack::Manifest {
format: "dever-lib-build-v1".into(),
ecosystem: ecosystem.clone(),
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: descriptor.python_runtime_sha256.clone(),
executables: serde_json::from_value(document[ecosystem.as_str()]["executables"].clone())
.unwrap(),
};
let mut files: Vec<(String, Vec<u8>)> = vec![(
"dever-build.json".into(),
serde_json::to_vec(&manifest).unwrap(),
)];
for input in document[ecosystem.as_str()]["files"].as_array().unwrap() {
let source = inputs.join(input["source"].as_str().unwrap());
assert!(fs::symlink_metadata(&source).unwrap().is_file());
let bytes = fs::read(&source).unwrap();
assert_eq!(digest(&bytes), input["sha256"].as_str().unwrap());
files.push((input["path"].as_str().unwrap().into(), bytes));
}
files.sort_by(|left, right| left.0.cmp(&right.0));
manifest.validate(&files, &descriptor).unwrap();
let assets = sandbox::assets(&workspace)
.into_iter()
.map(|asset| {
(
asset.path.trim_start_matches("sandbox/").into(),
asset.bytes,
)
})
.collect::<Vec<_>>();
pack::validate_tools(&manifest, &files, &assets).unwrap();
fs::create_dir_all(root.join("config")).unwrap();
let encoder = flate2::GzBuilder::new()
.mtime(0)
.operating_system(255)
.write(
fs::File::create(root.join("build.pack")).unwrap(),
flate2::Compression::default(),
);
let mut archive = tar::Builder::new(encoder);
for (path, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(if manifest.executables.contains(&path) {
0o755
} else {
0o644
});
header.set_uid(0);
header.set_gid(0);
header.set_mtime(0);
header.set_cksum();
archive
.append_data(&mut header, path, bytes.as_slice())
.unwrap();
}
archive.into_inner().unwrap().finish().unwrap();
descriptor.sha256 = digest(&fs::read(root.join("build.pack")).unwrap());
descriptor
}
struct SourceOnly(dever_cli::libs::HttpRegistry);
#[test]
fn incompatible_sdist_candidate_backtracks_before_loading_build_tools() {
let mut registry = LocalRegistry(BTreeMap::new());
python_release(&mut registry, "app", "1.0.0", &[], &["backend>=1"]);
python_release(&mut registry, "backend", "1.0.0", &[], &[]);
python_release(&mut registry, "backend", "2.0.0", &[], &[]);
let path = (Ecosystem::Pip, "/pypi/backend/2.0.0/json".into());
let mut release: serde_json::Value = serde_json::from_slice(&registry.0[&path]).unwrap();
release["urls"] = serde_json::json!([{"packagetype":"sdist","requires_python":">=3.13","filename":"backend-2.0.0.tar.gz","url":"must-not-fetch","digests":{"sha256":"0".repeat(64)}}]);
registry
.0
.insert(path, serde_json::to_vec(&release).unwrap());
let lock = python_lock(&registry, &["pip:app@1.0.0"]).unwrap();
assert!(
lock.libs
.iter()
.any(|lib| lib.spec.key() == "pip:backend@1.0.0")
);
}
fn backend_fixture(mutate: bool) -> LocalRegistry {
let mut registry = LocalRegistry(BTreeMap::new());
python_release(&mut registry, "buildhelper", "1.0.0", &[], &[]);
registry.0.insert(
(Ecosystem::Pip, "/pypi/sample/json".into()),
serde_json::to_vec(&serde_json::json!({"releases":{"1.0.0":[]}})).unwrap(),
);
let helper = wheel_fixture::pack(
"buildhelper",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("buildhelper", "1.0.0", &[], &[]),
&[
(
"buildhelper.py",
b"def cli():\n print('isolated-cli')\n".to_vec(),
),
(
"buildhelper-1.0.0.dist-info/entry_points.txt",
b"[console_scripts]\nbuild-probe = buildhelper:cli\n".to_vec(),
),
],
);
let key = (Ecosystem::Pip, "/pypi/buildhelper/1.0.0/json".into());
let mut release: serde_json::Value = serde_json::from_slice(&registry.0[&key]).unwrap();
let url = release["urls"][0]["url"].as_str().unwrap().to_owned();
release["urls"][0]["digests"]["sha256"] = serde_json::json!(digest(&helper));
registry
.0
.insert(key, serde_json::to_vec(&release).unwrap());
registry.0.insert((Ecosystem::Pip, url), helper);
let original = wheel_fixture::metadata("sample", "1.0.0", &[], &[]);
let changed = format!("{original}Summary: changed by wheel hook\n");
let wheel = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
if mutate { &changed } else { &original },
&[],
);
let backend = format!(
r#"import os, shutil, subprocess
from pathlib import Path
def get_requires_for_build_wheel(config_settings=None):
try:
import buildhelper
return []
except ImportError:
return ['buildhelper==1.0.0']
def prepare_metadata_for_build_wheel(directory, config_settings=None):
assert subprocess.check_output(['build-probe'], text=True).strip() == 'isolated-cli'
metadata = Path(directory) / 'sample-1.0.0.dist-info'
metadata.mkdir()
shutil.copy('original-metadata', metadata / 'METADATA')
os.chdir('/')
return metadata.name
def build_wheel(directory, config_settings=None, metadata_directory=None):
assert Path.cwd().name == 'source', 'previous hook leaked its cwd'
if {mutate}:
shutil.copy('changed-metadata', Path(metadata_directory) / 'METADATA')
name = 'sample-1.0.0-py3-none-any.whl'
shutil.copy('result.whl', Path(directory) / name)
return name
"#,
mutate = if mutate { "True" } else { "False" }
);
let sourcefiles = vec![
(
"sample-1.0.0/pyproject.toml",
b"[build-system]\nrequires = []\nbuild-backend = 'backend'\nbackend-path = ['.']\n"
.to_vec(),
),
("sample-1.0.0/backend.py", backend.into_bytes()),
("sample-1.0.0/original-metadata", original.into_bytes()),
("sample-1.0.0/changed-metadata", changed.into_bytes()),
("sample-1.0.0/result.whl", wheel),
];
let mut tar = tar::Builder::new(flate2::write::GzEncoder::new(
Vec::new(),
flate2::Compression::default(),
));
for (name, bytes) in sourcefiles {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, name, bytes.as_slice())
.unwrap();
}
let source = tar.into_inner().unwrap().finish().unwrap();
registry.0.insert((Ecosystem::Pip,"/pypi/sample/1.0.0/json".into()),serde_json::to_vec(&serde_json::json!({"info":{},"urls":[{"packagetype":"sdist","filename":"sample-1.0.0.tar.gz","url":"sample-source","digests":{"sha256":digest(&source)}}]})).unwrap());
registry
.0
.insert((Ecosystem::Pip, "sample-source".into()), source);
registry
}
#[test]
#[ignore = "isolated offline PEP517 contracts; requires explicit target/source-build tool/runtime fixture"]
fn pep517_hooks_keep_requirements_cli_and_metadata_boundaries() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/source-build");
let settings: Settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let inputs = AuthorInputs { root, settings };
for mutate in [false, true] {
let session = Session::new(&inputs);
let transport = backend_fixture(mutate);
let store = FixtureArtifactStore::default();
let resolver = RegistryResolver {
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Pip, inputs.settings.runtime.clone())]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
build: Some(&session),
};
let result = dever_cli::libs::LibResolver::resolve(
&resolver,
&["pip:sample@1.0.0".parse().unwrap()],
);
if mutate {
assert!(result.unwrap_err().contains("metadata differs"));
continue;
}
let lock = result.unwrap();
dever_cli::libs::doctor(&lock).unwrap();
assert_eq!(lock.builds[0].dynamic_requires, vec!["buildhelper==1.0.0"]);
for mutation in 0..3 {
let mut altered = lock.clone();
match mutation {
0 => altered.builds[0].tools.sha256 = "not-a-hash".into(),
1 => altered.builds[0].dynamic_requires = vec!["buildhelper>=2".into()],
_ => *altered.builds[0].dependencies = LockFile::new(vec![]).unwrap(),
}
altered.libs[0].build = Some(altered.builds[0].identity().unwrap());
assert!(
dever_cli::libs::doctor(&altered).is_err(),
"accepted contradictory receipt {mutation}"
);
}
}
}
impl RegistryTransport for SourceOnly {
fn get(&self, ecosystem: Ecosystem, path: &str, limit: usize) -> Result<Vec<u8>, String> {
let bytes = self.0.get(ecosystem, path, limit)?;
// Explicit author acceptance forces this one package's official sdist.
// Backend requirements still use their ordinary authenticated wheels.
if path == "/pypi/simplejson/3.20.1/json" {
let mut document: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|error| error.to_string())?;
document["urls"]
.as_array_mut()
.ok_or("official release lacks files")?
.retain(|file| file["packagetype"] == "sdist");
serde_json::to_vec(&document).map_err(|error| error.to_string())
} else {
Ok(bytes)
}
}
}
#[test]
#[ignore = "explicit official PyPI source build: downloads fixed source/build requirements and executes signed managed build tools"]
fn builds_official_python_source_with_isolated_pep517() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/source-build");
let settings: Settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let inputs = AuthorInputs {
root: root.clone(),
settings,
};
let session = Session::new(&inputs);
let transport = SourceOnly(dever_cli::libs::HttpRegistry::official());
let store = FixtureArtifactStore::default();
let resolver = RegistryResolver {
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Pip, inputs.settings.runtime.clone())]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
build: Some(&session),
};
let requested = vec!["pip:simplejson@3.20.1".parse().unwrap()];
let lock = dever_cli::libs::LibResolver::resolve(&resolver, &requested).unwrap();
dever_cli::libs::doctor(&lock).unwrap();
assert_eq!(lock.builds.len(), 1);
assert!(!lock.builds[0].dependencies.libs.is_empty());
let artifact = &lock
.libs
.iter()
.find(|lib| lib.spec.name == "simplejson")
.unwrap()
.artifacts[0];
let wheel = store
.verify_exact(&artifact.sha256, artifact.bytes, &artifact.target)
.unwrap();
let mut archive = zip::ZipArchive::new(Cursor::new(wheel)).unwrap();
assert!((0..archive.len()).any(|index| {
let file = archive.by_index(index).unwrap();
file.name().contains("_speedups.") && file.name().ends_with(".so")
}));
assert_eq!(
lock,
dever_cli::libs::LibResolver::resolve(&resolver, &requested).unwrap(),
"same session must replay one exact built artifact"
);
publish_fixture_artifacts(&root.join("dependency"), &lock, &store);
let mut altered = lock.clone();
altered.builds[0].frontend = "0".repeat(64);
assert!(dever_cli::libs::doctor(&altered).is_err());
}
+691
View File
@@ -0,0 +1,691 @@
use super::*;
use dever_cli::libs::{LibResolver, LockedWorker, npm, resolve_workers};
pub(super) fn archive(
name: &str,
version: &str,
mut manifest: serde_json::Value,
extra: &[(&str, Vec<u8>)],
) -> Vec<u8> {
manifest["name"] = name.into();
manifest["version"] = version.into();
let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
let mut writer = tar::Builder::new(encoder);
for (name, bytes) in [
(
"package/package.json",
serde_json::to_vec(&manifest).unwrap(),
),
(
"package/index.js",
format!("module.exports = '{name}/{version}';\n").into_bytes(),
),
]
.into_iter()
.chain(extra.iter().map(|(name, bytes)| (*name, bytes.clone())))
{
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
writer
.append_data(&mut header, name, bytes.as_slice())
.unwrap();
}
writer.into_inner().unwrap().finish().unwrap()
}
pub(super) fn release(
registry: &mut LocalRegistry,
name: &str,
version: &str,
manifest: serde_json::Value,
extra: &[(&str, Vec<u8>)],
) {
let archive = archive(name, version, manifest.clone(), extra);
let index_path = (Ecosystem::Npm, format!("/{name}"));
let mut index = registry
.0
.get(&index_path)
.map(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).unwrap())
.unwrap_or(serde_json::json!({"versions":{}}));
index["versions"][version] = serde_json::json!({});
registry
.0
.insert(index_path, serde_json::to_vec(&index).unwrap());
let mut document = manifest;
document["name"] = name.into();
document["version"] = version.into();
let url = format!("https://mock/{name}/{version}.tgz");
document["dist"] = serde_json::json!({"tarball":url,"integrity":format!("sha512-{}", base64::engine::general_purpose::STANDARD.encode(Sha512::digest(&archive)))});
registry.0.insert(
(Ecosystem::Npm, format!("/{name}/{version}")),
serde_json::to_vec(&document).unwrap(),
);
registry.0.insert((Ecosystem::Npm, url), archive);
}
fn resolved(registry: &LocalRegistry, requested: &[&str]) -> (LockFile, FixtureArtifactStore) {
let store = FixtureArtifactStore::default();
let lock = local_resolver(registry, &store)
.resolve(
&requested
.iter()
.map(|spec| spec.parse().unwrap())
.collect::<Vec<_>>(),
)
.unwrap();
doctor(&lock).unwrap();
assert_eq!(LockFile::decode(&lock.encode().unwrap()).unwrap(), lock);
(lock, store)
}
fn files(lock: &LockFile, store: &FixtureArtifactStore, index: usize) -> BTreeMap<String, Vec<u8>> {
let archives = lock
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
npm::files(&lock.npm[index], lock, |lib| {
Ok(archives[&lib.spec].as_slice())
})
.unwrap()
.into_iter()
.collect()
}
#[test]
fn nested_versions_and_shared_packages_keep_distinct_peer_contexts() {
let mut registry = LocalRegistry(BTreeMap::new());
for (name, dependencies) in [
("left", serde_json::json!({"plugin":"1.0.0","host":"1.0.0"})),
(
"right",
serde_json::json!({"plugin":"1.0.0","host":"2.0.0"}),
),
] {
release(
&mut registry,
name,
"1.0.0",
serde_json::json!({"dependencies":dependencies}),
&[],
);
}
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"peerDependencies":{"host":">=1.0.0 <3.0.0"}}),
&[],
);
for version in ["1.0.0", "2.0.0"] {
release(&mut registry, "host", version, serde_json::json!({}), &[]);
}
let (lock, store) = resolved(&registry, &["npm:left@1.0.0", "npm:right@1.0.0"]);
assert_eq!(lock.libs.len(), 5);
let files = files(&lock, &store, 0);
for (side, version) in [("left", "1.0.0"), ("right", "2.0.0")] {
let node = lock.npm[0]
.instances
.iter()
.find(|node| node.path == format!("node_modules/{side}/node_modules/plugin"))
.unwrap();
assert_eq!(
node.edges[0].target.as_deref(),
Some(format!("node_modules/{side}/node_modules/host").as_str())
);
assert!(
String::from_utf8(
files[&format!("node_modules/{side}/node_modules/host/index.js")].clone()
)
.unwrap()
.contains(version)
);
}
}
#[test]
fn optional_dependencies_override_regular_edges_and_record_target_or_absence() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"dependencies":{"child":"1.0.0"},"optionalDependencies":{"child":"2.0.0","missing":"*","darwin":"*","musl":"*"}}),
&[],
);
release(&mut registry, "child", "2.0.0", serde_json::json!({}), &[]);
release(
&mut registry,
"darwin",
"1.0.0",
serde_json::json!({"os":["darwin"]}),
&[],
);
release(
&mut registry,
"musl",
"1.0.0",
serde_json::json!({"os":["linux"],"libc":["musl"]}),
&[],
);
let (lock, store) = resolved(&registry, &["npm:root@1.0.0"]);
assert_eq!(lock.libs.len(), 2);
let root = lock.npm[0]
.instances
.iter()
.find(|node| node.spec.name == "root")
.unwrap();
assert_eq!(
root.edges
.iter()
.find(|edge| edge.name == "child")
.unwrap()
.kind,
npm::Kind::Optional
);
for name in ["darwin", "musl"] {
assert_eq!(
root.edges
.iter()
.find(|edge| edge.name == name)
.unwrap()
.omission,
Some(npm::Omission::Target)
);
}
assert_eq!(
root.edges
.iter()
.find(|edge| edge.name == "missing")
.unwrap()
.omission,
Some(npm::Omission::Unavailable)
);
assert!(
!files(&lock, &store, 0)
.keys()
.any(|path| path.contains("darwin") || path.contains("musl"))
);
}
#[test]
fn optional_integrity_and_protocol_failures_are_never_omissions() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"optionalDependencies":{"child":"*"}}),
&[],
);
release(&mut registry, "child", "1.0.0", serde_json::json!({}), &[]);
registry.0.insert(
(Ecosystem::Npm, "https://mock/child/1.0.0.tgz".into()),
b"tampered".to_vec(),
);
let store = FixtureArtifactStore::default();
assert!(
local_resolver(&registry, &store)
.resolve(&["npm:root@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("integrity mismatch")
);
release(
&mut registry,
"child",
"1.0.0",
serde_json::json!({"dependencies":{"other":"not-a-semver"}}),
&[],
);
assert!(
local_resolver(&registry, &store)
.resolve(&["npm:root@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("invalid npm range")
);
}
#[test]
fn required_peers_auto_install_and_backtrack_but_optional_peers_do_not() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"dependencies":{"plugin":"*","host":"1.0.0"}}),
&[],
);
release(
&mut registry,
"plugin",
"2.0.0",
serde_json::json!({"peerDependencies":{"host":"2.0.0"}}),
&[],
);
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"peerDependencies":{"host":"1.0.0","absent":"*"},"peerDependenciesMeta":{"absent":{"optional":true}}}),
&[],
);
for version in ["1.0.0", "2.0.0"] {
release(&mut registry, "host", version, serde_json::json!({}), &[]);
}
let (lock, _) = resolved(&registry, &["npm:root@1.0.0"]);
let plugin = lock.npm[0]
.instances
.iter()
.find(|node| node.spec.name == "plugin")
.unwrap();
assert_eq!(plugin.spec.version, "1.0.0");
assert_eq!(
plugin
.edges
.iter()
.find(|edge| edge.name == "absent")
.unwrap()
.omission,
Some(npm::Omission::OptionalPeerAbsent)
);
let (lock, _) = resolved(&registry, &["npm:plugin@1.0.0"]);
assert!(
lock.npm[0]
.instances
.iter()
.any(|node| node.path == "node_modules/host")
);
let store = FixtureArtifactStore::default();
assert!(
local_resolver(&registry, &store)
.resolve(&[
"npm:plugin@2.0.0".parse().unwrap(),
"npm:host@1.0.0".parse().unwrap()
])
.is_err()
);
}
#[test]
fn bundled_transitive_packages_are_bound_to_parent_and_never_downloaded() {
let mut registry = LocalRegistry(BTreeMap::new());
let extra = vec![
(
"package/node_modules/b/package.json",
br#"{"name":"b","version":"1.0.0","dependencies":{"c":"2.0.0"}}"#.to_vec(),
),
(
"package/node_modules/b/index.js",
b"module.exports = require('c');".to_vec(),
),
(
"package/node_modules/c/package.json",
br#"{"name":"c","version":"2.0.0"}"#.to_vec(),
),
(
"package/node_modules/c/index.js",
b"module.exports = 42;".to_vec(),
),
];
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({"dependencies":{"b":"1.0.0"},"bundleDependencies":["b"]}),
&extra,
);
let (lock, store) = resolved(&registry, &["npm:root@1.0.0"]);
assert_eq!(lock.libs.len(), 1);
assert_eq!(lock.npm[0].instances.len(), 3);
assert_eq!(
files(&lock, &store, 0)["node_modules/root/node_modules/c/index.js"],
b"module.exports = 42;"
);
release(
&mut registry,
"root",
"1.0.0",
serde_json::json!({}),
&extra,
);
assert!(
local_resolver(&registry, &store)
.resolve(&["npm:root@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("undeclared")
);
}
#[test]
fn worker_root_sets_keep_separate_npm_environments_and_detect_lock_tampering() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"peerDependencies":{"host":"*"}}),
&[],
);
for version in ["1.0.0", "2.0.0"] {
release(&mut registry, "host", version, serde_json::json!({}), &[]);
}
let store = FixtureArtifactStore::default();
let worker = |adapter: &str, version: &str| LockedWorker {
port: "sample.worker".into(),
adapter: adapter.into(),
ecosystem: "npm".into(),
runtime: Some(registry_runtime(Ecosystem::Npm).pack),
entry: format!("worker/{adapter}.mjs"),
schema: "checked-port".into(),
capabilities: vec![],
operations: vec!["read".into()],
libs: vec![
"npm:plugin@1.0.0".parse().unwrap(),
format!("npm:host@{version}").parse().unwrap(),
],
};
let lock = resolve_workers(
&local_resolver(&registry, &store),
&[],
vec![worker("first", "1.0.0"), worker("second", "2.0.0")],
)
.unwrap();
assert_eq!(lock.npm.len(), 2);
for index in 0..2 {
assert!(files(&lock, &store, index).contains_key("node_modules/plugin/index.js"));
}
let mut changed = lock.clone();
changed.npm[0].instances[0].path = "../escape".into();
assert!(doctor(&changed).is_err());
let mut changed = lock.clone();
changed.npm[0]
.instances
.iter_mut()
.find(|node| node.spec.name == "plugin")
.unwrap()
.edges[0]
.target = Some("node_modules/missing".into());
assert!(doctor(&changed).is_err());
let mut changed = lock;
changed.npm[0]
.instances
.iter_mut()
.find(|node| node.spec.name == "plugin")
.unwrap()
.edges
.clear();
let archives = changed
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
assert!(
npm::files(&changed.npm[0], &changed, |lib| Ok(
archives[&lib.spec].as_slice()
))
.unwrap_err()
.contains("differs from package.json")
);
}
#[test]
fn bundled_instances_cannot_switch_to_another_parents_same_version_package() {
let mut registry = LocalRegistry(BTreeMap::new());
for name in ["a", "c"] {
release(
&mut registry,
name,
"1.0.0",
serde_json::json!({"dependencies":{"b":"1.0.0"},"bundleDependencies":["b"]}),
&[
(
"package/node_modules/b/package.json",
br#"{"name":"b","version":"1.0.0"}"#.to_vec(),
),
(
"package/node_modules/b/index.js",
format!("module.exports = '{name}';").into_bytes(),
),
],
);
}
release(&mut registry, "b", "1.0.0", serde_json::json!({}), &[]);
let (original, store) = resolved(&registry, &["npm:a@1.0.0", "npm:c@1.0.0", "npm:b@1.0.0"]);
let archives = original
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
for (archive, prefix) in [
("npm:c@1.0.0", "package/node_modules/b/"),
("npm:b@1.0.0", "package/"),
] {
let mut changed = original.clone();
changed.npm[0]
.instances
.iter_mut()
.find(|node| node.path == "node_modules/a/node_modules/b")
.unwrap()
.source = npm::Source {
archive: archive.parse().unwrap(),
prefix: prefix.into(),
};
assert!(
doctor(&changed)
.unwrap_err()
.contains("parent archive installation")
);
assert!(
npm::files(&changed.npm[0], &changed, |lib| Ok(
archives[&lib.spec].as_slice()
))
.unwrap_err()
.contains("parent archive installation")
);
// Removing the locked layout cannot hide the original archive binding.
for node in &mut changed.npm[0].instances {
node.bundled.clear();
}
assert!(
npm::files(&changed.npm[0], &changed, |lib| Ok(
archives[&lib.spec].as_slice()
))
.is_err()
);
}
}
#[test]
fn production_and_optional_dependencies_override_same_named_peers() {
let mut registry = LocalRegistry(BTreeMap::new());
release(&mut registry, "host", "2.0.0", serde_json::json!({}), &[]);
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"dependencies":{"host":"2.0.0"},"peerDependencies":{"host":"1.0.0"}}),
&[],
);
let (lock, _) = resolved(&registry, &["npm:plugin@1.0.0", "npm:host@2.0.0"]);
let plugin = lock.npm[0]
.instances
.iter()
.find(|node| node.spec.name == "plugin")
.unwrap();
assert_eq!(plugin.edges.len(), 1);
assert_eq!(plugin.edges[0].kind, npm::Kind::Dependency);
assert_eq!(
plugin.edges[0].target.as_deref(),
Some("node_modules/plugin/node_modules/host")
);
release(
&mut registry,
"plugin",
"1.0.0",
serde_json::json!({"optionalDependencies":{"absent":"*"},"peerDependencies":{"absent":"1.0.0"}}),
&[],
);
let (lock, _) = resolved(&registry, &["npm:plugin@1.0.0"]);
assert_eq!(lock.npm[0].instances[0].edges.len(), 1);
assert_eq!(lock.npm[0].instances[0].edges[0].kind, npm::Kind::Optional);
assert_eq!(
lock.npm[0].instances[0].edges[0].omission,
Some(npm::Omission::Unavailable)
);
}
#[test]
fn removing_a_root_prunes_its_entire_dependency_cycle() {
for bundled in [false, true] {
let mut registry = LocalRegistry(BTreeMap::new());
release(&mut registry, "z", "1.0.0", serde_json::json!({}), &[]);
let b = serde_json::json!({"name":"b","version":"1.0.0","dependencies":{"a":"1.0.0"}});
let mut a = serde_json::json!({"dependencies":{"b":"1.0.0"}});
let extra = if bundled {
a["bundleDependencies"] = serde_json::json!(["b"]);
vec![(
"package/node_modules/b/package.json",
serde_json::to_vec(&b).unwrap(),
)]
} else {
release(&mut registry, "b", "1.0.0", b, &[]);
vec![]
};
release(&mut registry, "a", "1.0.0", a, &extra);
let (lock, _) = resolved(&registry, &["npm:a@1.0.0", "npm:z@1.0.0"]);
let root = TemporaryDirectory::new();
fs::create_dir(root.path().join("config")).unwrap();
fs::write(
root.path().join("config/setting.json"),
br#"{"lib":["npm:a@1.0.0","npm:z@1.0.0"]}"#,
)
.unwrap();
lock.write_atomic(root.path()).unwrap();
dever_cli::libs::execute("remove", root.path(), &["npm:a@1.0.0".into()]).unwrap();
let retained =
LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
assert_eq!(retained.libs.len(), 1);
assert_eq!(retained.libs[0].spec.name, "z");
assert_eq!(retained.npm[0].instances.len(), 1);
doctor(&retained).unwrap();
}
}
#[test]
fn removing_a_dependency_still_required_by_a_remaining_root_is_rejected() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"a",
"1.0.0",
serde_json::json!({"dependencies":{"b":"1.0.0"}}),
&[],
);
release(&mut registry, "b", "1.0.0", serde_json::json!({}), &[]);
let (lock, _) = resolved(&registry, &["npm:a@1.0.0", "npm:b@1.0.0"]);
let root = TemporaryDirectory::new();
fs::create_dir(root.path().join("config")).unwrap();
let setting = br#"{"lib":["npm:a@1.0.0","npm:b@1.0.0"]}"#;
fs::write(root.path().join("config/setting.json"), setting).unwrap();
lock.write_atomic(root.path()).unwrap();
let before = fs::read(root.path().join("dever.lock")).unwrap();
assert!(
dever_cli::libs::execute("remove", root.path(), &["npm:b@1.0.0".into()])
.unwrap_err()
.contains("required by")
);
assert_eq!(fs::read(root.path().join("dever.lock")).unwrap(), before);
assert_eq!(
fs::read(root.path().join("config/setting.json")).unwrap(),
setting
);
}
#[test]
fn repeated_archive_instances_are_bounded_before_materialization_clones() {
let mut registry = LocalRegistry(BTreeMap::new());
release(
&mut registry,
"leaf",
"1.0.0",
serde_json::json!({}),
&[("package/payload.js", vec![b'x'; 2 * 1024 * 1024])],
);
let mut roots = Vec::new();
for number in 0..129 {
let name = format!("root-{number}");
release(
&mut registry,
&name,
"1.0.0",
serde_json::json!({"dependencies":{"leaf":"1.0.0"}}),
&[],
);
roots.push(format!("npm:{name}@1.0.0"));
}
let (lock, store) = resolved(
&registry,
&roots.iter().map(String::as_str).collect::<Vec<_>>(),
);
assert_eq!(lock.npm[0].instances.len(), 258);
let archives = lock
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
assert!(
npm::files(
&lock.npm[0],
&lock,
|lib| Ok(archives[&lib.spec].as_slice())
)
.unwrap_err()
.contains("tree file/byte budget")
);
}
@@ -0,0 +1,590 @@
use super::*;
use dever_cli::libs::build::{Inputs, NativeRejection, Receipt, Session, pack};
use dever_cli::libs::{LibResolver, npm};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
runtime_file: String,
python: RegistryRuntime,
python_file: String,
build: pack::Descriptor,
build_file: String,
}
struct AuthorInputs {
root: PathBuf,
settings: Settings,
}
#[test]
fn npm_lifecycle_output_preserves_locked_identity_and_instance_ownership() {
let environment = npm::Environment::single("npm:app@1.0.0".parse().unwrap());
let files = vec![
(
"node_modules/app/package.json".into(),
br#"{"name":"app","version":"1.0.0"}"#.to_vec(),
),
(
"node_modules/app/generated.js".into(),
b"module.exports=42;".to_vec(),
),
];
npm::validate_built_files(&environment, &files).unwrap();
let mut changed = files.clone();
changed[0].1 =
br#"{"name":"app","version":"1.0.0","dependencies":{"injected":"1.0.0"}}"#.to_vec();
assert!(npm::validate_built_files(&environment, &changed).is_err());
let mut changed = files.clone();
changed.push(("node_modules/injected/index.js".into(), vec![]));
assert!(npm::validate_built_files(&environment, &changed).is_err());
let mut changed = files.clone();
changed.push(("node_modules/app/../escape".into(), vec![]));
assert!(npm::validate_built_files(&environment, &changed).is_err());
assert!(npm::validate_built_files(&environment, &files[1..]).is_err());
}
#[test]
fn npm_registry_install_hooks_do_not_run_publishing_lifecycle() {
let metadata = |script: &str| {
vec![(
"node_modules/app/package.json".into(),
serde_json::to_vec(&serde_json::json!({"scripts":{script:"node command.js"}})).unwrap(),
)]
};
for script in ["preinstall", "install", "postinstall"] {
assert!(npm::requires_build(&metadata(script)).unwrap());
}
for script in ["prepare", "prepack", "prepublish"] {
assert!(!npm::requires_build(&metadata(script)).unwrap());
}
assert!(
npm::requires_build(&[("node_modules/app/binding.gyp".into(), b"{}".to_vec())]).unwrap()
);
}
fn locked_installation_fixture() -> (LockFile, Vec<u8>) {
let mut registry = LocalRegistry(BTreeMap::new());
npm_tests::release(
&mut registry,
"app",
"1.0.0",
serde_json::json!({}),
&[(
"package/prebuilds/foreign/addon.node",
b"foreign-object".to_vec(),
)],
);
npm_tests::release(&mut registry, "other", "1.0.0", serde_json::json!({}), &[]);
let store = FixtureArtifactStore::default();
let mut lock = local_resolver(&registry, &store)
.resolve(&[
"npm:app@1.0.0".parse().unwrap(),
"npm:other@1.0.0".parse().unwrap(),
])
.unwrap();
let sources = lock
.libs
.iter()
.flat_map(|lib| lib.artifacts.clone())
.collect();
let archives = lock
.libs
.iter()
.map(|lib| {
(
lib.spec.clone(),
store
.verify_exact(
&lib.artifacts[0].sha256,
lib.artifacts[0].bytes,
&lib.artifacts[0].target,
)
.unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
let files = npm::build_files(
&lock.npm[0],
&lock,
|lib| Ok(archives[&lib.spec].as_slice()),
)
.unwrap();
let mut writer = tar::Builder::new(flate2::write::GzEncoder::new(
Vec::new(),
flate2::Compression::default(),
));
for (path, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
writer
.append_data(&mut header, path, bytes.as_slice())
.unwrap();
}
let bytes = writer.into_inner().unwrap().finish().unwrap();
let runtime = registry_runtime(Ecosystem::Npm);
let python = registry_runtime(Ecosystem::Pip);
let hash = digest(&bytes);
let receipt = Receipt {
ecosystem: Ecosystem::Npm,
roots: lock.npm[0].roots.clone(),
sources,
graph: Some(lock.npm[0].clone()),
runtime: runtime.pack.clone(),
auxiliary_runtime: Some(python.pack.clone()),
native_entries: Default::default(),
native_rejections: BTreeMap::from([(
"node_modules/app/prebuilds/foreign/addon.node".into(),
NativeRejection::Format,
)]),
python_markers: None,
tools: pack::Descriptor {
format: "dever-registry-build-v1".into(),
ecosystem: Ecosystem::Npm,
target: runtime.target.clone(),
runtime_sha256: runtime.pack.sha256.clone(),
python_runtime_sha256: Some(python.pack.sha256),
sha256: "a".repeat(64),
},
frontend: digest(
&[
include_bytes!("../../../crates/dever-cli/src/libs/build/npm.js").as_slice(),
include_bytes!("../../../crates/dever-cli/src/libs/build/npm/probe.cjs").as_slice(),
]
.concat(),
),
python: None,
dynamic_requires: vec![],
dependencies: Box::new(LockFile::new(vec![]).unwrap()),
config_settings: BTreeMap::new(),
output: dever_cli::libs::LockedArtifact {
target: runtime.target,
path: format!("build/npm/{hash}/installation.tgz"),
bytes: bytes.len() as u64,
sha256: hash,
},
};
lock.npm[0].build = Some(receipt.identity().unwrap());
lock.builds.push(receipt);
doctor(&lock).unwrap();
(lock, bytes)
}
#[test]
fn npm_expanded_outputs_are_pruned_only_when_no_raw_consumer_remains() {
use dever_cli::libs::{LockedWorker, prune_expanded_resources};
use dever_core::native::EmbeddedResource;
let (mut lock, bytes) = locked_installation_fixture();
let roots = lock.npm[0].roots.clone();
let other = vec!["npm:other@1.0.0".parse().unwrap()];
lock.npm = npm::retain(&lock, &[roots.clone(), other.clone()]).unwrap();
let output = &lock.builds[0].output;
let managed = LockedWorker {
port: "sample.worker".into(),
adapter: "default".into(),
ecosystem: "npm".into(),
runtime: Some(registry_runtime(Ecosystem::Npm).pack),
entry: "worker/main.cjs".into(),
schema: "checked".into(),
capabilities: vec![],
operations: vec!["probe".into()],
libs: roots.clone(),
};
let resource = EmbeddedResource {
path: output.path.clone(),
bytes,
sha256: output.sha256.clone(),
executable: false,
};
let mut resources = vec![resource.clone()];
prune_expanded_resources(&lock, std::slice::from_ref(&managed), &[], &mut resources).unwrap();
assert!(
resources.is_empty(),
"expanded installation archive leaked into program"
);
// A projected environment shares its original build output with the full
// Worker. Retention follows current consumers, not the receipt's old roots.
for declared in [roots, other] {
let mut resources = vec![resource.clone()];
prune_expanded_resources(
&lock,
std::slice::from_ref(&managed),
&declared,
&mut resources,
)
.unwrap();
assert_eq!(resources.len(), 1);
}
let mut exec = managed.clone();
exec.ecosystem = "exec".into();
exec.runtime = None;
let mut resources = vec![resource];
prune_expanded_resources(&lock, &[managed, exec], &[], &mut resources).unwrap();
assert_eq!(resources.len(), 1);
}
#[test]
fn npm_native_admission_accounts_every_addon_and_prunes_unsupported_objects() {
let (lock, bytes) = locked_installation_fixture();
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
assert!(files.iter().all(|(path, _, _)| !path.ends_with(".node")));
let mut missing = lock.clone();
missing.builds[0].native_rejections.clear();
missing.npm[0].build = Some(missing.builds[0].identity().unwrap());
assert!(
dever_cli::libs::build::npm_installation(&missing, &missing.npm[0], &bytes)
.unwrap_err()
.contains("exact addon files")
);
let mut corrupted = bytes.clone();
corrupted[10] ^= 1;
assert!(dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &corrupted).is_err());
for change in ["tools", "graph", "runtime"] {
let mut changed = lock.clone();
match change {
"tools" => changed.builds[0].tools.sha256 = "not-a-hash".into(),
"graph" => changed.builds[0].graph.as_mut().unwrap().instances.clear(),
_ => changed.builds[0].runtime.sha256 = "b".repeat(64),
}
changed.npm[0].build = Some(changed.builds[0].identity().unwrap());
assert!(
doctor(&changed).is_err(),
"accepted internally inconsistent {change}"
);
}
}
#[test]
fn npm_locked_output_projects_retained_roots_without_rerunning_hooks() {
let (lock, bytes) = locked_installation_fixture();
let project = TemporaryDirectory::new();
fs::create_dir(project.path().join("config")).unwrap();
fs::write(
project.path().join("config/setting.json"),
br#"{"lib":["npm:app@1.0.0","npm:other@1.0.0"]}"#,
)
.unwrap();
lock.write_atomic(project.path()).unwrap();
dever_cli::libs::execute("remove", project.path(), &["npm:app@1.0.0".into()]).unwrap();
let retained = LockFile::decode(&fs::read(project.path().join("dever.lock")).unwrap()).unwrap();
doctor(&retained).unwrap();
let files =
dever_cli::libs::build::npm_installation(&retained, &retained.npm[0], &bytes).unwrap();
assert_eq!(retained.builds, lock.builds);
assert!(
files
.iter()
.all(|(path, _, _)| path.starts_with("node_modules/other/"))
);
}
impl AuthorInputs {
fn load() -> Self {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/npm-build");
let settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
Self { root, settings }
}
}
impl Inputs for AuthorInputs {
fn runtime(&self, ecosystem: Ecosystem) -> Result<(RegistryRuntime, Vec<u8>), String> {
let (runtime, path) = match ecosystem {
Ecosystem::Pip => (&self.settings.python, &self.settings.python_file),
Ecosystem::Npm => (&self.settings.runtime, &self.settings.runtime_file),
_ => return Err("fixture only supplies Node and auxiliary Python".into()),
};
let bytes = fs::read(self.root.join(path)).map_err(|error| error.to_string())?;
if digest(&bytes) != runtime.pack.sha256 {
return Err("author runtime drift".into());
}
Ok((runtime.clone(), bytes))
}
fn tools(&self, ecosystem: Ecosystem) -> Result<(pack::Descriptor, Vec<u8>), String> {
if ecosystem != Ecosystem::Npm {
return Err("fixture only supplies npm tools".into());
}
Ok((
self.settings.build.clone(),
fs::read(self.root.join(&self.settings.build_file))
.map_err(|error| error.to_string())?,
))
}
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
Ok(build_tests::sandbox::assets(&workspace)
.into_iter()
.map(|asset| {
(
asset.path.trim_start_matches("sandbox/").into(),
asset.bytes,
)
})
.collect())
}
fn diagnostic(&self, output: &[u8]) {
eprintln!("{}", String::from_utf8_lossy(output));
}
}
#[test]
#[ignore = "explicit author operation: assembles hashed npm/compiler/header inputs; no network or host execution"]
fn prepare_npm_build_tool_fixture() {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let runtime = |name: &str| {
let root = workspace.join(format!("target/{name}-managed"));
let config: serde_json::Value =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let runtime: RegistryRuntime = serde_json::from_value(config["runtime"].clone()).unwrap();
assert_eq!(
digest(&fs::read(root.join("runtime.pack")).unwrap()),
runtime.pack.sha256
);
runtime
};
let node = runtime("npm");
let python = runtime("python");
let root = workspace.join("target/npm-build");
let build = build_tests::prepare_tool_pack(Ecosystem::Npm, &node, Some(&python), &root);
let settings = Settings {
runtime: node,
runtime_file: "../npm-managed/runtime.pack".into(),
python,
python_file: "../python-managed/runtime.pack".into(),
build,
build_file: "build.pack".into(),
};
fs::write(
root.join("config/setting.json"),
serde_json::to_vec_pretty(&settings).unwrap(),
)
.unwrap();
}
fn resolver<'a>(
inputs: &AuthorInputs,
session: &'a Session<'a>,
transport: &'a dyn RegistryTransport,
store: &'a FixtureArtifactStore,
) -> RegistryResolver<'a> {
RegistryResolver {
transport,
store,
runtimes: BTreeMap::from([(Ecosystem::Npm, inputs.settings.runtime.clone())]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
build: Some(session),
}
}
#[test]
#[ignore = "explicit official npm native source build; network only in registry resolver, never in lifecycle sandbox"]
fn builds_official_npm_addon_with_locked_lifecycle() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
let transport = dever_cli::libs::HttpRegistry::official();
let store = FixtureArtifactStore::default();
let resolver = resolver(&inputs, &session, &transport, &store);
let roots = vec!["npm:bufferutil@4.0.9".parse().unwrap()];
let lock = resolver.resolve(&roots).unwrap();
doctor(&lock).unwrap();
assert_eq!(lock.builds.len(), 1);
let receipt = &lock.builds[0];
assert!(
receipt
.native_entries
.contains("node_modules/bufferutil/build/Release/bufferutil.node")
);
let bytes = store
.verify_exact(
&receipt.output.sha256,
receipt.output.bytes,
&receipt.output.target,
)
.unwrap();
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
assert!(files.iter().any(
|(path, bytes, _)| path.ends_with("/build/Release/bufferutil.node")
&& bytes.starts_with(b"\x7fELF")
));
assert_eq!(lock, resolver.resolve(&roots).unwrap());
publish_fixture_artifacts(&inputs.root.join("dependency"), &lock, &store);
}
#[test]
#[ignore = "explicit managed npm hooks; uses local registry and real signed author runtime/tools"]
fn npm_lifecycle_preserves_optional_failures_bins_and_offline_remove() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
let mut transport = LocalRegistry(BTreeMap::new());
npm_tests::release(&mut transport, "helper", "1.0.0", serde_json::json!({"bin":{"build-probe":"cli.js"}}), &[("package/cli.js", b"#!/usr/bin/env node\nrequire('fs').writeFileSync('generated.js','module.exports = 42;');\n".to_vec())]);
npm_tests::release(
&mut transport,
"broken",
"1.0.0",
serde_json::json!({"dependencies":{"cycle":"1.0.0"},"scripts":{"install":"node -e 'process.exit(7)'"}}),
&[],
);
npm_tests::release(
&mut transport,
"cycle",
"1.0.0",
serde_json::json!({"dependencies":{"broken":"1.0.0"}}),
&[],
);
npm_tests::release(
&mut transport,
"app",
"1.0.0",
serde_json::json!({"dependencies":{"helper":"1.0.0"},"optionalDependencies":{"broken":"1.0.0"},"scripts":{"postinstall":"build-probe"}}),
&[],
);
npm_tests::release(&mut transport, "other", "1.0.0", serde_json::json!({}), &[]);
let store = FixtureArtifactStore::default();
let resolver = resolver(&inputs, &session, &transport, &store);
let lock = resolver
.resolve(&[
"npm:app@1.0.0".parse().unwrap(),
"npm:other@1.0.0".parse().unwrap(),
])
.unwrap();
doctor(&lock).unwrap();
assert!(
!lock
.libs
.iter()
.any(|lib| matches!(lib.spec.name.as_str(), "broken" | "cycle"))
);
assert!(
lock.npm[0]
.instances
.iter()
.flat_map(|node| &node.edges)
.any(|edge| edge.omission == Some(npm::Omission::BuildFailure))
);
let receipt = &lock.builds[0];
let bytes = store
.verify_exact(
&receipt.output.sha256,
receipt.output.bytes,
&receipt.output.target,
)
.unwrap();
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
assert!(
files
.iter()
.any(|(path, bytes, _)| path == "node_modules/app/generated.js"
&& bytes == b"module.exports = 42;")
);
assert!(
resolver
.resolve(&["npm:broken@1.0.0".parse().unwrap()])
.unwrap_err()
.contains("failed")
);
let project = TemporaryDirectory::new();
fs::create_dir(project.path().join("config")).unwrap();
fs::write(
project.path().join("config/setting.json"),
br#"{"lib":["npm:app@1.0.0","npm:other@1.0.0"]}"#,
)
.unwrap();
lock.write_atomic(project.path()).unwrap();
dever_cli::libs::execute("remove", project.path(), &["npm:app@1.0.0".into()]).unwrap();
let retained = LockFile::decode(&fs::read(project.path().join("dever.lock")).unwrap()).unwrap();
doctor(&retained).unwrap();
let files =
dever_cli::libs::build::npm_installation(&retained, &retained.npm[0], &bytes).unwrap();
assert!(
files
.iter()
.all(|(path, _, _)| path.starts_with("node_modules/other/"))
);
}
#[test]
#[ignore = "explicit managed npm hook: sparse scratch overflow and child cleanup; no network"]
fn npm_build_scratch_budget_kills_owned_hook_and_cleans_stage() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
let mut transport = LocalRegistry(BTreeMap::new());
npm_tests::release(&mut transport,"oversized","1.0.0",serde_json::json!({"scripts":{"install":"node fill.js"}}),&[("package/fill.js",b"const fs=require('fs');fs.writeFileSync('large',Buffer.alloc(1));fs.truncateSync('large',600*1024*1024);setInterval(()=>{},1000);".to_vec())]);
let store = FixtureArtifactStore::default();
let resolver = resolver(&inputs, &session, &transport, &store);
let owned_stages = || {
fs::read_dir(std::env::temp_dir())
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.filter(|name| name.starts_with(&format!("dever-lib-build-{}-", std::process::id())))
.collect::<std::collections::BTreeSet<_>>()
};
let before = owned_stages();
let error = resolver
.resolve(&["npm:oversized@1.0.0".parse().unwrap()])
.unwrap_err();
assert!(error.contains("scratch budget"), "{error}");
assert_eq!(
owned_stages(),
before,
"failed hook leaked its owned staging tree"
);
}
#[test]
#[ignore = "explicit managed npm hook: internal output aliases are flattened; aliases outside output are rejected"]
fn npm_build_output_hard_links_stay_inside_the_exported_tree() {
let inputs = AuthorInputs::load();
let session = Session::new(&inputs);
for (name, source, accepted) in [
(
"insidealias",
"const fs=require('fs');fs.writeFileSync('original.txt','same-bytes');fs.linkSync('original.txt','alias.txt');",
true,
),
(
"outsidealias",
"const fs=require('fs');fs.writeFileSync('/data/build/outside.txt','outside');fs.linkSync('/data/build/outside.txt','alias.txt');",
false,
),
] {
let mut transport = LocalRegistry(BTreeMap::new());
npm_tests::release(
&mut transport,
name,
"1.0.0",
serde_json::json!({"scripts":{"install":"node aliases.js"}}),
&[("package/aliases.js", source.as_bytes().to_vec())],
);
let store = FixtureArtifactStore::default();
let result =
resolver(&inputs, &session, &transport, &store)
.resolve(&[format!("npm:{name}@1.0.0").parse().unwrap()]);
if accepted {
let lock = result.unwrap();
doctor(&lock).unwrap();
let receipt = &lock.builds[0];
let bytes = store
.verify_exact(
&receipt.output.sha256,
receipt.output.bytes,
&receipt.output.target,
)
.unwrap();
let files =
dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
for leaf in ["original.txt", "alias.txt"] {
assert!(files.iter().any(|(path, bytes, _)| path
== &format!("node_modules/{name}/{leaf}")
&& bytes == b"same-bytes"));
}
} else {
assert!(result.unwrap_err().contains("hard-link alias outside"));
}
}
}
+754
View File
@@ -0,0 +1,754 @@
use super::*;
use dever_cli::libs::sumdb::{self, ChecksumDatabase, Evidence, Verifier};
use ring::signature::{Ed25519KeyPair, KeyPair};
fn key() -> Ed25519KeyPair {
Ed25519KeyPair::from_seed_unchecked(&[47; 32]).unwrap()
}
pub(super) fn verifier() -> Verifier {
let mut encoded = vec![1];
encoded.extend(key().public_key().as_ref());
let mut hash = Sha256::new();
hash.update(b"dever-sumdb-test\n");
hash.update(&encoded);
let id = hash.finalize();
Verifier::new(&format!(
"dever-sumdb-test+{}+{}",
id[..4]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
base64::engine::general_purpose::STANDARD.encode(encoded)
))
.unwrap()
}
fn signed(size: usize, root: [u8; 32]) -> String {
let text = format!(
"go.sum database tree\n{size}\n{}\n",
base64::engine::general_purpose::STANDARD.encode(root)
);
let mut hash = Sha256::new();
hash.update(b"dever-sumdb-test\n");
hash.update([1]);
hash.update(key().public_key().as_ref());
let mut signature = hash.finalize()[..4].to_vec();
signature.extend(key().sign(text.as_bytes()).as_ref());
format!(
"{text}\n— dever-sumdb-test {}\n",
base64::engine::general_purpose::STANDARD.encode(signature)
)
}
fn leaf(record: &[u8]) -> [u8; 32] {
let mut hash = Sha256::new();
hash.update([0]);
hash.update(record);
hash.finalize().into()
}
fn root(hashes: &[[u8; 32]]) -> [u8; 32] {
if hashes.is_empty() {
return Sha256::digest([]).into();
}
if hashes.len() == 1 {
return hashes[0];
}
let split = hashes.len().next_power_of_two() / 2;
let mut hash = Sha256::new();
hash.update([1]);
hash.update(root(&hashes[..split]));
hash.update(root(&hashes[split..]));
hash.finalize().into()
}
fn unescape(path: &str) -> String {
let mut chars = path.chars();
let mut output = String::new();
while let Some(ch) = chars.next() {
output.push(if ch == '!' {
chars.next().unwrap().to_ascii_uppercase()
} else {
ch
});
}
output
}
pub(super) fn seal(registry: &mut LocalRegistry) {
registry
.0
.retain(|(_, path), _| !path.starts_with("https://sum.golang.org/"));
let records = registry
.0
.iter()
.filter_map(|((ecosystem, path), bytes)| {
if *ecosystem != Ecosystem::Go || !path.ends_with(".mod") {
return None;
}
let (module, version) = path.strip_prefix('/')?.split_once("/@v/v")?;
let version = version.strip_suffix(".mod")?;
let module = unescape(module);
let archive = registry.0.get(&(
Ecosystem::Go,
format!("{}zip", path.strip_suffix("mod").unwrap()),
))?;
let record = format!(
"{module} v{version} {}\n{module} v{version}/go.mod {}\n",
sumdb::zip_hash(&module, version, archive).unwrap(),
sumdb::mod_hash(bytes)
);
Some((
format!(
"/lookup/{}@v{version}",
path.strip_prefix('/')
.unwrap()
.split("/@v/")
.next()
.unwrap()
),
record,
))
})
.collect::<Vec<_>>();
let hashes = records
.iter()
.map(|(_, record)| leaf(record.as_bytes()))
.collect::<Vec<_>>();
let checkpoint = signed(records.len(), root(&hashes));
registry.0.insert(
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
checkpoint.as_bytes().to_vec(),
);
for (id, (path, record)) in records.iter().enumerate() {
registry.0.insert(
(Ecosystem::Go, format!("https://sum.golang.org{path}")),
format!("{id}\n{record}\n{checkpoint}").into_bytes(),
);
}
// Fixtures deliberately remain below one tile. Larger trees use official
// captured vectors, not a second production tile-address implementation.
assert!(hashes.len() < 256);
if !hashes.is_empty() {
registry.0.insert(
(
Ecosystem::Go,
format!("https://sum.golang.org/tile/8/0/000.p/{}", hashes.len()),
),
hashes.iter().flatten().copied().collect(),
);
}
}
pub(super) struct Transport<'a> {
pub registry: &'a dyn RegistryTransport,
pub checksums: &'a LocalRegistry,
}
impl RegistryTransport for Transport<'_> {
fn get(&self, ecosystem: Ecosystem, path: &str, limit: usize) -> Result<Vec<u8>, String> {
if path.starts_with("https://sum.golang.org/") {
self.checksums.get(ecosystem, path, limit)
} else {
self.registry.get(ecosystem, path, limit)
}
}
}
fn resolved(
registry: &LocalRegistry,
previous: Option<String>,
roots: &[&str],
) -> Result<LockFile, String> {
let cache = FixtureArtifactStore::default();
let mut resolver = local_resolver(registry, &cache);
resolver.go_sumdb = ChecksumDatabase::new(verifier(), previous)?;
dever_cli::libs::LibResolver::resolve(
&resolver,
&roots
.iter()
.map(|root| root.parse().unwrap())
.collect::<Vec<_>>(),
)
}
#[test]
fn authenticated_modules_replay_offline_and_reject_content_or_proof_drift() {
let registry = local_registry();
let lock = resolved(&registry, None, &["go:example.com/a@1.0.0"]).unwrap();
let evidence = &lock.go_sumdb[0];
let checked = evidence.verify(&verifier()).unwrap();
let archive = &registry.0[&(Ecosystem::Go, "/example.com/a/@v/v1.0.0.zip".into())];
checked
.verify_zip("example.com/a", "1.0.0", archive)
.unwrap();
assert!(
evidence
.verify(&Verifier::official())
.unwrap_err()
.contains("trusted signature")
);
let mut changed = evidence.clone();
changed.records[0].go_mod.push_str("// changed\n");
assert!(changed.verify(&verifier()).unwrap_err().contains("go.mod"));
let mut changed = evidence.clone();
changed.records[0].module = "wrong.example/a".into();
assert!(
changed
.verify(&verifier())
.unwrap_err()
.contains("different module/version")
);
let mut changed = evidence.clone();
changed.records[0].inclusion[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
changed
.verify(&verifier())
.unwrap_err()
.contains("inclusion proof")
);
let mut changed = evidence.clone();
changed.checkpoint = changed
.checkpoint
.replace("go.sum database tree", "go.sum database free");
assert!(
changed
.verify(&verifier())
.unwrap_err()
.contains("signature verification")
);
assert!(
checked
.verify_zip(
"example.com/a",
"1.0.0",
&zip_file("example.com/a@v1.0.0/changed.go")
)
.unwrap_err()
.contains("sumdb h1")
);
}
#[test]
fn checkpoint_updates_require_consistency_and_never_move_backwards() {
let mut first = local_registry();
first
.0
.remove(&(Ecosystem::Go, "/example.com/b/@v/v1.2.0.mod".into()));
first.0.insert(
(Ecosystem::Go, "/example.com/a/@v/v1.0.0.mod".into()),
b"module example.com/a\n".to_vec(),
);
seal(&mut first);
let old = resolved(&first, None, &["go:example.com/a@1.0.0"]).unwrap();
let checkpoint = old.go_sumdb[0].verify(&verifier()).unwrap().checkpoint;
let mut next = first;
next.0.insert(
(Ecosystem::Go, "/example.com/z/@v/v1.0.0.mod".into()),
b"module example.com/z\n".to_vec(),
);
next.0.insert(
(Ecosystem::Go, "/example.com/z/@v/v1.0.0.zip".into()),
zip_file("example.com/z@v1.0.0/z.go"),
);
seal(&mut next);
let new = resolved(&next, Some(checkpoint.clone()), &["go:example.com/z@1.0.0"]).unwrap();
let current = new.go_sumdb[0].verify(&verifier()).unwrap().checkpoint;
assert!(resolved(&next, Some(current.clone()), &["go:example.com/z@1.0.0"]).is_ok());
next.0.insert(
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
checkpoint.into_bytes(),
);
assert!(
resolved(&next, Some(current), &["go:example.com/a@1.0.0"])
.unwrap_err()
.contains("rollback")
);
let mut fork = new.go_sumdb[0].clone();
fork.consistency[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
fork.verify(&verifier())
.unwrap_err()
.contains("conflicting history")
);
}
#[test]
fn sumdb_verifier_key_and_upstream_signed_note_vectors() {
let note = "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n";
let evidence = Evidence {
previous: None,
checkpoint: note.into(),
consistency: vec![],
records: vec![],
};
evidence.verify(&Verifier::official()).unwrap();
assert!(evidence.verify(&verifier()).is_err());
assert!(Verifier::new(&sumdb::OFFICIAL_VERIFIER.replace("033de0ae", "033de0af")).is_err());
}
#[test]
fn official_large_tree_inclusion_and_consistency_vectors_verify_offline() {
// Captured from sum.golang.org/lookup/github.com/google/uuid@v1.6.0
// and its immutable height-8 tiles on 2026-10-02. Hashes were assembled
// independently of this Rust implementation. Both notes retain Google's
// original Ed25519 signatures; no fixture signer can mint these vectors.
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
let trusted = Verifier::official();
let verified = evidence.verify(&trusted).unwrap();
assert!(verified.contains("github.com/google/uuid", "1.6.0"));
assert_eq!(
sumdb::mod_hash(b"module github.com/google/uuid\n"),
"h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo="
);
for index in 0..evidence.records[0].inclusion.len() {
let mut changed = evidence.clone();
changed.records[0].inclusion[index] =
base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
changed.verify(&trusted).is_err(),
"accepted corrupt inclusion hash {index}"
);
}
for index in 0..evidence.consistency.len() {
let mut changed = evidence.clone();
changed.consistency[index] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
assert!(
changed.verify(&trusted).is_err(),
"accepted corrupt consistency hash {index}"
);
}
}
#[test]
fn go_h1_ignores_zip_order_and_compression_and_preserves_module_case() {
let archive = |reverse, compression| {
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut files = vec![
("a.go", b"package a\n".as_slice()),
("LICENSE", b"fixture license\n".as_slice()),
];
if reverse {
files.reverse();
}
for (name, content) in files {
writer
.start_file(
format!("example.com/Mixed@v1.0.0/{name}"),
zip::write::SimpleFileOptions::default().compression_method(compression),
)
.unwrap();
writer.write_all(content).unwrap();
}
writer.finish().unwrap().into_inner()
};
let first = archive(false, zip::CompressionMethod::Stored);
let second = archive(true, zip::CompressionMethod::Deflated);
assert_ne!(digest(&first), digest(&second));
assert_eq!(
sumdb::zip_hash("example.com/Mixed", "1.0.0", &first).unwrap(),
sumdb::zip_hash("example.com/Mixed", "1.0.0", &second).unwrap()
);
let mut registry = LocalRegistry(BTreeMap::from([
(
(Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.mod".into()),
b"module example.com/Mixed\n".to_vec(),
),
(
(Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.zip".into()),
first,
),
]));
seal(&mut registry);
let lock = resolved(&registry, None, &["go:example.com/Mixed@1.0.0"]).unwrap();
assert_eq!(lock.libs[0].spec.name, "example.com/Mixed");
assert!(
lock.go_sumdb[0]
.verify(&verifier())
.unwrap()
.contains("example.com/Mixed", "1.0.0")
);
}
#[test]
fn every_go_mod_consulted_by_mvs_is_authenticated_even_when_not_selected() {
let mut registry = LocalRegistry(BTreeMap::new());
for (name, version, source) in [
(
"a",
"1.0.0",
"module example.com/a\nrequire (\nexample.com/b v1.0.0\nexample.com/c v1.0.0\n)\n",
),
("b", "1.0.0", "module example.com/b\n"),
("b", "2.0.0", "module example.com/b\n"),
(
"c",
"1.0.0",
"module example.com/c\nrequire example.com/b v2.0.0\n",
),
] {
registry.0.insert(
(
Ecosystem::Go,
format!("/example.com/{name}/@v/v{version}.mod"),
),
source.as_bytes().to_vec(),
);
registry.0.insert(
(
Ecosystem::Go,
format!("/example.com/{name}/@v/v{version}.zip"),
),
zip_file(&format!("example.com/{name}@v{version}/{name}.go")),
);
}
seal(&mut registry);
let lock = resolved(&registry, None, &["go:example.com/a@1.0.0"]).unwrap();
assert!(
!lock
.libs
.iter()
.any(|lib| lib.spec.key() == "go:example.com/b@1.0.0")
);
assert!(
lock.go_sumdb[0]
.records
.iter()
.any(|record| record.module == "example.com/b" && record.version == "1.0.0")
);
registry
.0
.get_mut(&(Ecosystem::Go, "/example.com/b/@v/v1.0.0.mod".into()))
.unwrap()
.extend(b"// modified discarded metadata\n");
let cache = FixtureArtifactStore::default();
let resolver = local_resolver(&registry, &cache);
assert!(
dever_cli::libs::LibResolver::resolve(
&resolver,
&["go:example.com/a@1.0.0".parse().unwrap()]
)
.unwrap_err()
.contains("go.mod does not match")
);
for ((_, path), bytes) in &registry.0 {
if path.ends_with(".zip") {
assert!(!cache.has(&digest(bytes), "linux-x86_64").unwrap());
}
}
}
#[test]
fn go_hashzip_includes_empty_directory_entries() {
let archive = |directory| {
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
writer
.start_file(
"example.com/a@v1.0.0/a.go",
zip::write::SimpleFileOptions::default(),
)
.unwrap();
writer.write_all(b"package a\n").unwrap();
if directory {
writer
.add_directory(
"example.com/a@v1.0.0/empty/",
zip::write::SimpleFileOptions::default(),
)
.unwrap();
}
writer.finish().unwrap().into_inner()
};
// Independent Hash1 vectors: sorted "sha256(content) filename\n" records,
// including the directory's trailing slash and SHA-256 of empty content.
assert_eq!(
sumdb::zip_hash("example.com/a", "1.0.0", &archive(false)).unwrap(),
"h1:VEUcYKYLbKBYq0eA/VEJ3mMQBzi4jjWpusjuduGJcBQ="
);
assert_eq!(
sumdb::zip_hash("example.com/a", "1.0.0", &archive(true)).unwrap(),
"h1:y9ZWKWCmVsKnLxdFkIVoxr4MyXyHgBUvp1hgBzZ+IfE="
);
}
#[test]
fn project_updates_and_removing_last_lib_preserve_official_checkpoint_anchor() {
let root = TemporaryDirectory::new();
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
let mut lock = LockFile::new(Vec::new()).unwrap();
lock.go_sumdb.push(evidence.clone());
lock.write_atomic(root.path()).unwrap();
let spec = vec!["pip:fixture@1.0.0".into()];
for command in ["add", "update", "remove"] {
dever_cli::libs::execute(command, root.path(), &spec).unwrap();
let lock = LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
assert_eq!(lock.go_sumdb, vec![evidence.clone()]);
let current = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap();
let previous = evidence.previous.clone().unwrap();
// A subsequent online resolution cannot accept the older official tree.
let mut registry = local_registry();
registry.0.insert(
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
previous.into_bytes(),
);
let cache = FixtureArtifactStore::default();
let mut resolver = local_resolver(&registry, &cache);
resolver.go_sumdb = ChecksumDatabase::official(Some(current.checkpoint)).unwrap();
assert!(
dever_cli::libs::LibResolver::resolve(
&resolver,
&["go:example.com/a@1.0.0".parse().unwrap()]
)
.unwrap_err()
.contains("rollback")
);
}
}
#[test]
fn lock_publication_rejects_offline_byte_and_element_overflow_before_replacement() {
let root = TemporaryDirectory::new();
let original = LockFile::new(Vec::new()).unwrap();
original.write_atomic(root.path()).unwrap();
let path = root.path().join("dever.lock");
let before = fs::read(&path).unwrap();
let receipt = Evidence {
previous: None,
checkpoint: "x".into(),
consistency: Vec::new(),
records: Vec::new(),
};
for overflow in [true, false] {
let mut lock = original.clone();
let mut receipt = receipt.clone();
if overflow {
receipt.checkpoint = "x".repeat(dever_runtime::wire::MAX_BYTES);
} else {
receipt.consistency = vec!["x".into(); dever_runtime::wire::MAX_ELEMENTS];
}
lock.go_sumdb.push(receipt);
assert!(lock.encode().unwrap_err().contains("invalid dever.lock"));
assert!(lock.write_atomic(root.path()).is_err());
assert_eq!(fs::read(&path).unwrap(), before);
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1);
}
}
#[test]
fn truncated_surplus_and_out_of_bounds_sumdb_proofs_are_rejected() {
let original: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
let trusted = Verifier::official();
let mut changed = original.clone();
changed.records[0].inclusion.pop();
assert!(changed.verify(&trusted).is_err());
let mut changed = original.clone();
changed.records[0]
.inclusion
.push(base64::engine::general_purpose::STANDARD.encode([0; 32]));
assert!(changed.verify(&trusted).is_err());
for id in ["66321505", "09223372036854775807", "9223372036854775808"] {
let mut changed = original.clone();
let (_, suffix) = changed.records[0].lookup.split_once('\n').unwrap();
changed.records[0].lookup = format!("{id}\n{suffix}");
assert!(changed.verify(&trusted).is_err());
}
let mut changed = original.clone();
changed.checkpoint.pop();
assert!(changed.verify(&trusted).is_err());
let mut disconnected = original.clone();
disconnected.previous = None;
disconnected.consistency.clear();
assert!(
sumdb::verify_chain(&[original, disconnected], &trusted)
.unwrap_err()
.contains("not connected")
);
}
#[cfg(unix)]
#[test]
fn lib_and_package_mutations_share_a_stable_nonblocking_project_lock() {
use std::os::unix::fs::OpenOptionsExt;
let root = TemporaryDirectory::new();
let requested = vec!["pip:fixture@1.0.0".into()];
dever_cli::libs::execute("add", root.path(), &requested).unwrap();
let lock_path = root.path().join("dever.lock");
let config_path = root.path().join("config/setting.json");
let before_lock = fs::read(&lock_path).unwrap();
let before_config = fs::read(&config_path).unwrap();
let guard = fs::OpenOptions::new()
.read(true)
.write(true)
.mode(0o600)
.open(root.path().join(".dever-project.lock"))
.unwrap();
fs2::FileExt::try_lock_exclusive(&guard).unwrap();
for command in ["add", "update", "remove"] {
assert!(
dever_cli::libs::execute(command, root.path(), &requested)
.unwrap_err()
.contains("mutation is busy")
);
assert!(
dever_cli::packages::execute(command, root.path(), &[])
.unwrap_err()
.contains("mutation is busy")
);
struct UnusedTransport;
impl dever_cli::packages::PackageTransport for UnusedTransport {
fn get(&self, _: &str, _: usize) -> Result<Vec<u8>, String> {
panic!("busy mutation must not contact a registry")
}
}
assert!(
dever_cli::packages::execute_with(
command,
root.path(),
&[],
&UnusedTransport,
&FixtureArtifactStore::default()
)
.unwrap_err()
.contains("mutation is busy")
);
}
assert_eq!(fs::read(&lock_path).unwrap(), before_lock);
assert_eq!(fs::read(&config_path).unwrap(), before_config);
drop(guard);
dever_cli::libs::execute("update", root.path(), &requested).unwrap();
assert_eq!(fs::read(&lock_path).unwrap(), before_lock);
}
#[cfg(unix)]
#[test]
fn project_mutation_rejects_linked_or_unprotected_lock_paths() {
use std::os::unix::fs::{PermissionsExt, symlink};
let root = TemporaryDirectory::new();
let outside = TemporaryDirectory::new();
let target = outside.path().join("untouched");
fs::write(&target, "keep").unwrap();
let path = root.path().join(".dever-project.lock");
symlink(&target, &path).unwrap();
let requested = vec!["pip:fixture@1.0.0".into()];
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("without symlinks")
);
assert_eq!(fs::read(&target).unwrap(), b"keep");
fs::remove_file(&path).unwrap();
fs::hard_link(&target, &path).unwrap();
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("private and unlinked")
);
fs::remove_file(&path).unwrap();
fs::write(&path, "").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o666)).unwrap();
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("private and unlinked")
);
fs::set_permissions(root.path(), fs::Permissions::from_mode(0o777)).unwrap();
assert!(
dever_cli::libs::execute("add", root.path(), &requested)
.unwrap_err()
.contains("not group/other writable")
);
}
#[test]
fn independent_go_workers_merge_connected_offline_evidence() {
use dever_cli::libs::{LockedWorker, resolve_workers};
let registry = local_registry();
let cache = FixtureArtifactStore::default();
let resolver = local_resolver(&registry, &cache);
let worker = |adapter: &str, spec: &str| LockedWorker {
port: "sample.worker".into(),
adapter: adapter.into(),
ecosystem: "go".into(),
runtime: Some(registry_runtime(Ecosystem::Go).pack),
entry: format!("worker/{adapter}.go"),
schema: "checked-port".into(),
capabilities: vec![],
operations: vec!["read".into()],
libs: vec![spec.parse().unwrap()],
};
let lock = resolve_workers(
&resolver,
&[],
vec![
worker("first", "go:example.com/a@1.0.0"),
worker("second", "go:example.com/b@1.2.0"),
],
)
.unwrap();
assert_eq!(lock.go_sumdb.len(), 2);
assert_eq!(
lock.go_sumdb[1].previous.as_ref(),
Some(&lock.go_sumdb[0].verify(&verifier()).unwrap().checkpoint)
);
let decoded = LockFile::decode(&lock.encode().unwrap()).unwrap();
let verified = sumdb::verify_chain(&decoded.go_sumdb, &verifier()).unwrap();
assert!(verified.contains("example.com/a", "1.0.0"));
assert!(verified.contains("example.com/b", "1.2.0"));
verify_locked_artifacts(&decoded, &cache, "linux-x86_64").unwrap();
}
#[test]
#[ignore = "author operation: explicitly fetches official Go proxy/sumdb; requires target/go-managed/config/setting.json and runtime.pack"]
fn prepare_official_go_dependency_fixture() {
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
}
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/go-managed");
let settings: Settings = serde_json::from_slice(
&fs::read(root.join("config/setting.json"))
.expect("prepare the explicit fixture settings first"),
)
.unwrap();
let pack =
fs::read(root.join("runtime.pack")).expect("prepare the explicit Go runtime pack first");
assert_eq!(
settings.runtime.pack.sha256,
digest(&pack),
"configured runtime does not match runtime.pack"
);
assert_eq!(
settings.runtime.target,
dever_cli::toolchain::platform_identity()
);
let output = root.join("dependency");
let lock_path = output.join("dever.lock");
let previous = if lock_path.exists() {
let lock = LockFile::decode(&fs::read(&lock_path).unwrap()).unwrap();
dever_cli::libs::doctor(&lock).unwrap();
let verified = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap();
(!verified.checkpoint.is_empty()).then_some(verified.checkpoint)
} else {
None
};
let target = settings.runtime.target.clone();
let store = FixtureArtifactStore::default();
let transport = dever_cli::libs::HttpRegistry::official();
let resolver = RegistryResolver {
build: None,
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Go, settings.runtime)]),
go_sumdb: ChecksumDatabase::official(previous).unwrap(),
};
let lock = dever_cli::libs::LibResolver::resolve(
&resolver,
&["go:github.com/google/uuid@1.6.0".parse().unwrap()],
)
.unwrap();
// The same official verifier and offline artifact checks protect normal builds.
dever_cli::libs::doctor(&lock).unwrap();
dever_cli::libs::verify_locked_artifacts(&lock, &store, &target).unwrap();
publish_fixture_artifacts(&output, &lock, &store);
}
@@ -0,0 +1,68 @@
{
"previous": "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n",
"checkpoint": "go.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
"consistency": [
"3OyjS9XS6r41pxaFWGnW/WSmX7b1ik3nupquDNipdZ8=",
"zX5IsB02+IutT35ka/MwY7gFiPSAlihfnt2cu6smUjw=",
"Qc0CqO89A+/onGz1u4ZsDoMOfRIgPZpayRGTETOv8kk=",
"JhdrRO/y8Iu+MDeUupFG0uM4+odAADMjVH0mOgLOA/c=",
"lOgywJvTdrBWol0uphM6ZulvAebwJGNlekm8MIwiwKM=",
"FRf0N51Fw+8eSLHEa/e/AsnDway0EkKo4jI0E1Ne7dk=",
"Ak0gyC+RS6/ahVBCBuSBDA1wFN9mgq3acIbYm9uJHQ4=",
"FFJ8WrKTTQgCjTuAl5CukBdjDaQ7opSdHBpNSolRYWw=",
"rdNcd+MUjUxzlX/nlVGeQP5BkkZyTT89sn/yfg0AXQ0=",
"gJzOh5y5rP1BILUrFJw+aGjod840PErDnjy4T0BoUE4=",
"7I8BPI+ADxeJmu19mQxO+xt6bNhg3Ueq1j2dIfOppwA=",
"urhcUF9QOyTRAYRjJjsRHjHdwIc+BxfiQ6a+NuYeC6E=",
"p9eGEUHXeKLFnrIPB7VXHHMuwtaBnvHG4CYwgC0wq+w=",
"Gcry0ScaNfFmQujPMT95RQ3rbzKDSynQsL546C8CkfU=",
"dIpym+JRT/+z9/tlhpuIRyv+uoWGqgND/MHw0f1MtqA=",
"cQrKLJIBbPoWDnRnbGfAEMfpsau0h+Aw3aqDOJ5LAVU=",
"C46761Ui7jL+lf4vRXBlveoxuFS6tykAd6mMEfNttPE=",
"45Kz4HcZ9iZ84ySMHCy21d4vd2MvibvfNMzYm0hKYRs=",
"4JZTJ+4VUtLMTEs11jn4GmCxoz56Lm5HDwkYmkmAWlw=",
"2hJV4h1jZ2YujjyXUw+7YYCwLXGYZy+ondhRUgxKmXI=",
"lrn/kRfgYdYSDxgPdSQ8/GnZw6u/joKu9++AR+Saa5M=",
"hnm9+kaHQdhHWrAcyrvY5fbv8QB+GwVjRMFeobtTg8A=",
"rPFmKL/vD6Edsk/0DmpvDa/nMOqWgjfpPGRjBUL9yKs=",
"2IzbyezmwwGZZPJ276ejAQZiAnar+RcjAUU3MEMtbIA=",
"k3kfr9AECmkV+UA3/D6EMqaBDr67aubL5hXGsvJ+Nng=",
"/WnQtddEqG03mbcU1XID1iws1nlKHVkJcaREWknCOiE=",
"xJ20xXM4QItt5vxFOtolZ5l18p7ej1+RimNkgWmoRFM="
],
"records": [{
"module": "github.com/google/uuid",
"version": "1.6.0",
"go_mod": "module github.com/google/uuid\n",
"lookup": "22152757\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\n\ngo.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
"inclusion": [
"vYU6vRfhcMyd5/U04n+AYg52mm3BQrjwyN+27saiSnU=",
"syWiyNbj6gybIdLKoAFd7xF3qNltS7OhQjtp4y8rCFg=",
"Zqpa9zJsOg/sI/mv0UkXO9d7gvPHLLjMAKnZCPzeiNM=",
"tGbcXa1AQlYBIm5gM2KDy8JvdAFDizj4lUop18Eehxs=",
"/w9FG1Sae/omKxZdOvj719sT1rCow3tlP8VNo0fDQtE=",
"dR6Bk+gh/hyJx7bu65HfhQdfuvT/OdVv/G7RUsOc26Y=",
"fRC86/2mWWtFWNPz/Vou6f7hdyvRLHFPo9KaOlVOVmc=",
"jJS2b3XVP2sst4CS5srpnF34agHpiluU63h6UQyFPDc=",
"cOL2+zAXTEOaAiyvmCrL3Ned6soAg08CJJJ2VW3+a18=",
"In5NzPUzIAZfV6bnUL11Bkm4Aqoit/ZG7gJ8nxOdB2w=",
"r+8IH0m7Ig3BS+uwCVVG1CZT0I/Rro3bbSUVYcSrN2Q=",
"wOgTamt00bdokE1Nbv3aHYu2um06V1xdLFEmOBjt0GI=",
"21woobbbCvogV/ZXACdAozfVAFjRv5xMID/iLcryHzw=",
"5+o2gn9/zVod+MYr4UuoeMzuz68vt/hA5+9H4YZaVME=",
"040B+RrLQeFCJhPU69kzoKr4BTqhSwdrnYEVhknZ+I0=",
"ioI6UByPBEr2lEZB0dI6pCXgcNss6UkKxm+TYAADujg=",
"HOJ0dLK4hnl0tFeu1tmT7TRrNKRIbi/T8KI/2s4DXDs=",
"BBcHtB4mS19jHff/LQTNI/+WCVBZ1Up52gNqyoln71U=",
"Oz/OlF1Lj7wkqqE15p324ZSX1XHcnLzDSMKNtUx6rwY=",
"ofpmTAcvFvNzlwjZnsrMqeG4iIBCP2lt5wIusvZ2gG4=",
"IRpVS0aap6n0qb02jyOy6ikkOPxdFIQtWwckaGF2MAQ=",
"liRusDUG3TanNZLGkfjb58ppX/6d40M7X2s3wptjxMw=",
"ROjUzjXHDN0Yo/I6EeqHhpszFgIs7FCABVeQniliPTk=",
"NTjIv8CJqIIGrOHhNQWSP9FV6XEPL6uGA4lmMfNKGJg=",
"bwqUQG2wJXGxo1QLA2CFb3chH17xvlqXe6eQy43p0HY=",
"bqa+4xGvzGcI/PYne2fXfUvdULgy8o3zQmD13q/gu1Y="
],
"consistency": []
}]
}
+531
View File
@@ -0,0 +1,531 @@
use super::*;
use dever_cli::workers::python_wheel::Wheel;
fn validate_native(
target: &str,
suffixes: &[String],
interpreter: &str,
files: &BTreeMap<String, &[u8]>,
) -> Result<(), String> {
let wheel_files = files
.keys()
.filter(|path| !path.starts_with("runtime/") && !path.starts_with("sandbox/"))
.cloned()
.collect();
dever_cli::workers::python_wheel::validate_native(
target,
suffixes,
interpreter,
files,
&wheel_files,
)
}
fn spec() -> LibSpec {
"pip:sample@1.0.0".parse().unwrap()
}
#[test]
fn wheel_console_scripts_use_the_same_private_prefix_and_record_generated_files() {
let bytes=wheel_fixture::pack("sample","1.0.0","Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample","1.0.0",&[],&[]),&[("sample-1.0.0.dist-info/entry_points.txt",b"[console_scripts]\nsample-tool = sample.cli:main\n[gui_scripts]\nsample-gui = sample.gui:App.run [desktop]\n".to_vec())]);
let installed = Wheel::parse(&spec(), None, &tags(), &bytes)
.unwrap()
.install("runtime/bin/python3", "3.12")
.unwrap();
let script = installed
.iter()
.find(|file| file.path == "runtime/bin/sample-tool")
.unwrap();
assert!(script.executable);
assert!(
script
.bytes
.starts_with(b"#!/worker/runtime/bin/python3 -I\n")
);
assert!(String::from_utf8_lossy(&script.bytes).contains("sample.cli"));
let record = installed
.iter()
.find(|file| file.path.ends_with(".dist-info/RECORD"))
.unwrap();
assert!(String::from_utf8_lossy(&record.bytes).contains("../../../bin/sample-tool,sha256="));
let collision = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[
(
"sample-1.0.0.dist-info/entry_points.txt",
b"[console_scripts]\ntool = sample:main\n".to_vec(),
),
("sample-1.0.0.data/scripts/tool", b"#!python\n".to_vec()),
],
);
assert!(
Wheel::parse(&spec(), None, &tags(), &collision)
.unwrap()
.install("runtime/bin/python3", "3.12")
.is_err()
);
}
fn tags() -> Vec<String> {
vec!["py3-none-any".into()]
}
fn valid() -> Vec<u8> {
pure_wheel("sample", "1.0.0", &[], &[])
}
fn unpack(bytes: &[u8]) -> Vec<(String, Vec<u8>)> {
use std::io::Read;
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
(0..archive.len())
.map(|index| {
let mut file = archive.by_index(index).unwrap();
let name = file.name().to_owned();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
(name, bytes)
})
.collect()
}
#[test]
fn wheel_identity_and_all_compressed_filename_tags_are_bound_to_metadata() {
Wheel::parse(
&spec(),
Some("sample-1.0.0-py3-none-any.whl"),
&tags(),
&valid(),
)
.unwrap();
for filename in [
"other-1.0.0-py3-none-any.whl",
"sample-2.0.0-py3-none-any.whl",
"sample-1.0.0-py2.py3-none-any.whl",
"sample-1.0.0-1-py3-none-any.whl",
] {
assert!(
Wheel::parse(&spec(), Some(filename), &tags(), &valid()).is_err(),
"{filename}"
);
}
let compressed = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py2-none-any\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[],
);
Wheel::parse(
&spec(),
Some("sample-1.0.0-py2.py3-none-any.whl"),
&tags(),
&compressed,
)
.unwrap();
assert!(
Wheel::parse(
&spec(),
None,
&["cp312-cp312-linux_x86_64".into()],
&valid()
)
.is_err()
);
let forged_metadata = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("other", "1.0.0", &[], &[]),
&[],
);
assert!(Wheel::parse(&spec(), None, &tags(), &forged_metadata).is_err());
let hidden_binary = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[("sample/native.so", elf(62, None, None))],
);
assert!(Wheel::parse(&spec(), None, &tags(), &hidden_binary).is_err());
}
#[test]
#[ignore = "author operation: explicitly fetches PyPI simplejson; requires target/python-managed/config/setting.json and runtime.pack"]
fn prepare_official_native_python_fixture() {
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Settings {
runtime: RegistryRuntime,
}
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/python-managed");
let settings: Settings =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
let pack = fs::read(root.join("runtime.pack")).unwrap();
assert_eq!(digest(&pack), settings.runtime.pack.sha256);
assert_eq!(
settings.runtime.target,
dever_cli::toolchain::platform_identity()
);
let target = settings.runtime.target.clone();
let store = FixtureArtifactStore::default();
let transport = dever_cli::libs::HttpRegistry::official();
let resolver = RegistryResolver {
build: None,
transport: &transport,
store: &store,
runtimes: BTreeMap::from([(Ecosystem::Pip, settings.runtime)]),
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
};
let lock = dever_cli::libs::LibResolver::resolve(
&resolver,
&["pip:simplejson@3.20.1".parse().unwrap()],
)
.unwrap();
dever_cli::libs::doctor(&lock).unwrap();
dever_cli::libs::verify_locked_artifacts(&lock, &store, &target).unwrap();
let artifact = &lock.libs[0].artifacts[0];
let wheel = store
.verify_exact(&artifact.sha256, artifact.bytes, &target)
.unwrap();
assert!(
unpack(&wheel)
.iter()
.any(|(path, bytes)| path.contains("_speedups.")
&& path.ends_with(".so")
&& bytes.starts_with(b"\x7fELF")),
"fixture must use native speedups, not the optional pure-Python fallback"
);
publish_fixture_artifacts(&root.join("dependency"), &lock, &store);
}
#[test]
fn wheel_record_requires_exact_files_hashes_sizes_and_unique_csv_paths() {
for mutation in 0..6 {
let mut files = unpack(&valid());
match mutation {
0 => files
.iter_mut()
.find(|(path, _)| path.ends_with("__init__.py"))
.unwrap()
.1
.push(b'!'),
1 => files.push(("sample/unlisted.py".into(), vec![b'x'])),
2 => {
files.retain(|(path, _)| !path.ends_with("__init__.py"));
}
_ => {
let record = &mut files
.iter_mut()
.find(|(path, _)| path.ends_with("/RECORD"))
.unwrap()
.1;
let mut content = String::from_utf8(record.clone()).unwrap();
match mutation {
3 => content = content.replace("sha256=", "sha1="),
4 => content.push_str("sample-1.0.0.dist-info/RECORD,,\n"),
5 => content.push_str("../outside.py,sha256=abc,1\n"),
_ => unreachable!(),
}
*record = content.into_bytes();
}
}
assert!(
Wheel::parse(&spec(), None, &tags(), &wheel_fixture::zip(&files)).is_err(),
"mutation {mutation}"
);
}
}
#[test]
fn wheel_installs_all_standard_schemes_and_rewrites_installed_record() {
let bytes = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[
("sample-1.0.0.data/purelib/plain.py", b"plain".to_vec()),
(
"sample-1.0.0.data/platlib/platform.py",
b"platform".to_vec(),
),
(
"sample-1.0.0.data/scripts/tool",
b"#!python\nprint('ok')\n".to_vec(),
),
("sample-1.0.0.data/headers/public.h", b"header".to_vec()),
(
"sample-1.0.0.data/data/share/sample/schema.json",
b"{}".to_vec(),
),
("sample/name,with\"quotes.py", b"quoted".to_vec()),
],
);
let files = Wheel::parse(&spec(), None, &tags(), &bytes)
.unwrap()
.install("runtime/bin/python3", "3.12")
.unwrap();
for path in [
"runtime/lib/python3.12/site-packages/plain.py",
"runtime/lib/python3.12/site-packages/platform.py",
"runtime/include/python3.12/sample/public.h",
"runtime/share/sample/schema.json",
"runtime/lib/python3.12/site-packages/sample/name,with\"quotes.py",
] {
assert!(files.iter().any(|file| file.path == path), "{path}");
}
let script = files
.iter()
.find(|file| file.path == "runtime/bin/tool")
.unwrap();
assert!(script.executable);
assert!(
script
.bytes
.starts_with(b"#!/worker/runtime/bin/python3 -I\n")
);
let record = files
.iter()
.find(|file| file.path.ends_with("/RECORD"))
.unwrap();
let rows = csv::ReaderBuilder::new()
.has_headers(false)
.from_reader(record.bytes.as_slice())
.records()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let installed_script = rows
.iter()
.find(|row| &row[0] == "../../../bin/tool")
.unwrap();
assert_eq!(
installed_script[1].to_owned(),
format!(
"sha256={}",
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(&script.bytes))
)
);
}
#[test]
fn wheel_rejects_scheme_collisions_and_uses_artifact_dependencies() {
let bytes = wheel_fixture::pack(
"sample",
"1.0.0",
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
&[
("same.py", vec![b'a']),
("sample-1.0.0.data/platlib/same.py", vec![b'b']),
],
);
assert!(
Wheel::parse(&spec(), None, &tags(), &bytes)
.unwrap()
.install("runtime/bin/python3", "3.12")
.is_err()
);
let mut registry = local_registry();
let release = registry
.0
.get_mut(&(Ecosystem::Pip, "/pypi/a/1.0.0/json".into()))
.unwrap();
let mut document: serde_json::Value = serde_json::from_slice(release).unwrap();
document["info"]["requires_dist"] = serde_json::json!([]);
*release = serde_json::to_vec(&document).unwrap();
let lock = python_lock(&registry, &["pip:a@1.0.0"]).unwrap();
assert_eq!(dependency_keys(&lock, "pip:a@1.0.0"), ["pip:b@1.1.0"]);
}
#[test]
fn solver_backtracks_on_selected_wheel_metadata_and_its_actual_extras() {
let mut registry = LocalRegistry(BTreeMap::new());
python_release(
&mut registry,
"root",
"1.0.0",
&[],
&["a[x] >=1", "b ==1.0.0"],
);
python_release(
&mut registry,
"a",
"2.0.0",
&["x"],
&["b ==2.0.0; extra == 'x'"],
);
python_release(
&mut registry,
"a",
"1.0.0",
&["x"],
&["b ==1.0.0; extra == 'x'"],
);
python_release(&mut registry, "b", "2.0.0", &[], &[]);
python_release(&mut registry, "b", "1.0.0", &[], &[]);
for version in ["1.0.0", "2.0.0"] {
let json = registry
.0
.get_mut(&(Ecosystem::Pip, format!("/pypi/a/{version}/json")))
.unwrap();
let mut release: serde_json::Value = serde_json::from_slice(json).unwrap();
release["info"] = serde_json::json!({"requires_dist":null,"provides_extra":null});
*json = serde_json::to_vec(&release).unwrap();
}
let lock = python_lock(&registry, &["pip:root@1.0.0"]).unwrap();
assert!(
lock.libs
.iter()
.any(|lib| lib.spec.key() == "pip:a[x]@1.0.0")
);
assert!(!lock.libs.iter().any(|lib| lib.spec.version == "2.0.0"));
assert_eq!(dependency_keys(&lock, "pip:a[x]@1.0.0"), ["pip:b@1.0.0"]);
}
/// ELF64 protocol fixture with PT_LOAD and optional PT_DYNAMIC/DT_STRTAB.
/// It is parsed only; no executable bytes are launched by this test.
fn elf(machine: u16, needed: Option<&str>, rpath: Option<&str>) -> Vec<u8> {
let mut bytes = vec![0u8; 1024];
bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
bytes[16..18].copy_from_slice(&3u16.to_le_bytes());
bytes[18..20].copy_from_slice(&machine.to_le_bytes());
bytes[20..24].copy_from_slice(&1u32.to_le_bytes());
bytes[32..40].copy_from_slice(&64u64.to_le_bytes());
bytes[52..54].copy_from_slice(&64u16.to_le_bytes());
bytes[54..56].copy_from_slice(&56u16.to_le_bytes());
bytes[56..58].copy_from_slice(&2u16.to_le_bytes());
bytes[64..68].copy_from_slice(&1u32.to_le_bytes());
bytes[96..104].copy_from_slice(&1024u64.to_le_bytes());
bytes[104..112].copy_from_slice(&1024u64.to_le_bytes());
bytes[120..124].copy_from_slice(&2u32.to_le_bytes());
bytes[128..136].copy_from_slice(&176u64.to_le_bytes());
bytes[136..144].copy_from_slice(&176u64.to_le_bytes());
let mut strings = vec![0];
let mut dynamic = vec![];
for (tag, value) in [(1u64, needed), (15u64, rpath)] {
if let Some(value) = value {
dynamic.push((tag, strings.len() as u64));
strings.extend(value.bytes());
strings.push(0);
}
}
dynamic.extend([(5, 512), (10, strings.len() as u64), (0, 0)]);
bytes[152..160].copy_from_slice(&((dynamic.len() * 16) as u64).to_le_bytes());
bytes[160..168].copy_from_slice(&((dynamic.len() * 16) as u64).to_le_bytes());
for (index, (tag, value)) in dynamic.into_iter().enumerate() {
bytes[176 + index * 16..184 + index * 16].copy_from_slice(&tag.to_le_bytes());
bytes[184 + index * 16..192 + index * 16].copy_from_slice(&value.to_le_bytes());
}
bytes[512..512 + strings.len()].copy_from_slice(&strings);
bytes
}
#[test]
fn native_wheel_checks_target_suffix_and_closed_library_search() {
let runtime = elf(62, None, None);
let library = elf(62, None, None);
let native = elf(62, Some("libsample.so.1"), Some("$ORIGIN/../sample.libs"));
let suffixes = vec![
".cpython-312-x86_64-linux-gnu.so".into(),
".abi3.so".into(),
".so".into(),
];
let mut files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
(
"site-packages/sample/_native.cpython-312-x86_64-linux-gnu.so".into(),
native.as_slice(),
),
(
"site-packages/sample.libs/libsample.so.1".into(),
library.as_slice(),
),
]);
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
files.remove("site-packages/sample.libs/libsample.so.1");
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains("packaged dependency closure")
);
files.insert("sandbox/lib/libsample.so.1".into(), library.as_slice());
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
let old = files
.remove("site-packages/sample/_native.cpython-312-x86_64-linux-gnu.so")
.unwrap();
files.insert(
"site-packages/sample/_native.cpython-311-x86_64-linux-gnu.so".into(),
old,
);
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains("suffix")
);
for (machine, rpath, message) in [
(183, "$ORIGIN", "target"),
(62, "/usr/lib", "$ORIGIN"),
(62, "$ORIGIN/../../../..", "escapes"),
] {
let rejected = elf(machine, None, Some(rpath));
let files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
("site-packages/sample/native.so".into(), rejected.as_slice()),
]);
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains(message)
);
}
}
#[test]
fn native_runpath_outranks_ancestor_rpath_and_helpers_keep_import_context() {
let runtime = elf(62, None, Some("$ORIGIN/../lib"));
let shared = elf(62, None, None);
let wrong_target = elf(183, None, None);
let mut extension = elf(
62,
Some("libsample.so.1"),
Some("$ORIGIN/../../runtime/alternate"),
);
// Replace DT_RPATH with DT_RUNPATH in the independently encoded ELF vector.
for offset in (176..256).step_by(16) {
if u64::from_le_bytes(extension[offset..offset + 8].try_into().unwrap()) == 15 {
extension[offset..offset + 8].copy_from_slice(&29u64.to_le_bytes());
}
}
let suffixes = vec![".cpython-312-x86_64-linux-gnu.so".into()];
let files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
("runtime/lib/libsample.so.1".into(), shared.as_slice()),
(
"runtime/alternate/libsample.so.1".into(),
wrong_target.as_slice(),
),
(
"site-packages/pkg/mod.cpython-312-x86_64-linux-gnu.so".into(),
extension.as_slice(),
),
]);
assert!(
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
.unwrap_err()
.contains("target")
);
let extension = elf(62, Some("liba.so.1"), Some("$ORIGIN/../vendor"));
let helper = elf(62, Some("libb.so.1"), None);
let files = BTreeMap::from([
("runtime/bin/python3".into(), runtime.as_slice()),
(
"site-packages/pkg/mod.cpython-312-x86_64-linux-gnu.so".into(),
extension.as_slice(),
),
("site-packages/vendor/liba.so.1".into(), helper.as_slice()),
("site-packages/vendor/libb.so.1".into(), shared.as_slice()),
]);
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,204 @@
use super::*;
use dever_cli::toolchain::BuildTarget;
fn command_fixture(root: &Path, binary: &[u8]) -> dever_core::hir::Program {
let mut sources = SourceMap::default();
sources.add(
"media/tool/port.dever",
"execute(args: List<Text>) (output: dever.process.Output) fails dever.process.Error",
);
sources.add(
"media/tool/adapter.dever",
"external command \"bin/tool\" {}\n",
);
sources.add("media/tool/app.dever", "execute() (code: Int) {\n output = port.execute([\"a b\", \"$(literal)\"])\n code = output.code\n}\n");
sources.add("media/tool/api.dever", "cmd execute = app.execute\n");
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
let entry = &program.external_command_contracts()[0].entry;
fs::create_dir_all(root.join(entry).parent().unwrap()).unwrap();
fs::write(root.join(entry), binary).unwrap();
program
}
fn command_manifest(resources: &[EmbeddedResource]) -> serde_json::Value {
let manifest = resources
.iter()
.find(|resource| resource.path.ends_with(".dever-command.json"))
.unwrap();
serde_json::from_slice(&manifest.bytes).unwrap()
}
#[test]
fn command_preparation_needs_no_ecosystem_lock_and_keeps_target_identity() {
for (target, machine) in [
(BuildTarget::LinuxX86_64, 62u16),
(BuildTarget::LinuxAarch64, 183u16),
] {
let temp = TemporaryDirectory::new();
let mut binary = native_elf::static_executable();
binary[18..20].copy_from_slice(&machine.to_le_bytes());
let program = command_fixture(temp.path(), &binary);
assert!(program.external_worker_contracts().is_empty());
assert!(
dever_cli::libs::prepare_program(temp.path(), &program, target)
.unwrap()
.is_none()
);
let resources =
workers::prepare_for_target(temp.path(), &program, &[], target, &[]).unwrap();
let manifest = command_manifest(&resources);
assert_eq!(manifest["format"], "dever-command-launch-v1");
assert_eq!(manifest["arguments"], json!([]));
assert_eq!(manifest["ecosystem"], "command");
let executable = resources
.iter()
.find(|resource| Some(resource.path.as_str()) == manifest["executable"].as_str())
.unwrap();
assert!(executable.executable);
assert_eq!(executable.bytes, binary);
let other = if target == BuildTarget::LinuxX86_64 {
BuildTarget::LinuxAarch64
} else {
BuildTarget::LinuxX86_64
};
assert!(
workers::prepare_for_target(temp.path(), &program, &[], other, &[])
.unwrap_err()
.contains("differs from target")
);
assert!(!temp.path().join("dever.lock").exists());
}
}
#[test]
fn command_preparation_checks_recursive_library_closure_and_loader() {
let temp = TemporaryDirectory::new();
let program = command_fixture(temp.path(), &native_elf::compiler(&["libfirst.so"]));
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("libfirst.so")
);
let lib = temp.path().join("module/media/tool/bin/lib");
fs::create_dir_all(&lib).unwrap();
fs::write(
lib.join("libfirst.so"),
native_elf::library("libfirst.so", &["libsecond.so"]),
)
.unwrap();
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("libsecond.so")
);
fs::write(
lib.join("libsecond.so"),
native_elf::library("libwrong.so", &[]),
)
.unwrap();
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("SONAME")
);
fs::write(
lib.join("libsecond.so"),
native_elf::library("libsecond.so", &[]),
)
.unwrap();
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("signed target GNU loader")
);
let loader_name = dever_sandbox::loader_name();
let mut loader = resource(
format!("sandbox/lib/{loader_name}"),
native_elf::static_executable(),
);
loader.executable = true;
let resources = workers::prepare(temp.path(), &program, &[loader]).unwrap();
let manifest = command_manifest(&resources);
assert!(
manifest["executable"]
.as_str()
.unwrap()
.ends_with("/loader")
);
assert_eq!(manifest["arguments"][0]["literal"], "--inhibit-cache");
for name in ["libfirst.so", "libsecond.so"] {
assert!(resources.iter().any(
|resource| resource.path.ends_with(&format!("/lib/{name}")) && !resource.executable
));
}
}
#[test]
fn command_rejects_host_library_paths_and_signed_library_shadowing() {
let temp = TemporaryDirectory::new();
let program = command_fixture(
temp.path(),
&native_elf::elf(None, &["libfirst.so"], Some("/usr/lib")),
);
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("$ORIGIN")
);
let entry = &program.external_command_contracts()[0].entry;
for path in ["$ORIGIN/../data/grant", "${ORIGIN}/../lib"] {
fs::write(
temp.path().join(entry),
native_elf::elf(None, &[], Some(path)),
)
.unwrap();
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("escapes its packaged tree")
);
}
fs::write(
temp.path().join(entry),
native_elf::compiler(&["libfirst.so"]),
)
.unwrap();
let lib = temp.path().join("module/media/tool/bin/lib");
fs::create_dir_all(&lib).unwrap();
fs::write(
lib.join("libfirst.so"),
native_elf::library("libfirst.so", &[]),
)
.unwrap();
let signed = resource(
"sandbox/lib/libfirst.so".into(),
native_elf::library("libfirst.so", &[]),
);
assert!(
workers::prepare(temp.path(), &program, &[signed])
.unwrap_err()
.contains("overrides a signed sandbox library")
);
}
#[cfg(unix)]
#[test]
fn command_rejects_symlinks_and_library_subdirectories() {
let temp = TemporaryDirectory::new();
let program = command_fixture(temp.path(), &native_elf::static_executable());
let lib = temp.path().join("module/media/tool/bin/lib");
fs::create_dir_all(&lib).unwrap();
std::os::unix::fs::symlink("../tool", lib.join("libalias.so")).unwrap();
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("regular library files")
);
fs::remove_file(lib.join("libalias.so")).unwrap();
fs::create_dir(lib.join("hidden")).unwrap();
assert!(
workers::prepare(temp.path(), &program, &[])
.unwrap_err()
.contains("regular library files")
);
}
@@ -0,0 +1,119 @@
use super::*;
#[test]
#[ignore = "requires the explicit official native npm build fixture at target/npm-build"]
fn runs_official_npm_addon_from_offline_resources_and_threads() {
let temp = TemporaryDirectory::new();
let program = dever_core::check(&worker_sources(
"npm",
"worker/main.cjs",
"lib \"bufferutil@4.0.9\"",
))
.unwrap();
let contract = program.external_worker_contracts().remove(0);
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let prepared = workspace.join("target/npm-build");
let config: serde_json::Value =
serde_json::from_slice(&fs::read(prepared.join("config/setting.json")).unwrap()).unwrap();
let pack = fs::read(prepared.join(config["runtime_file"].as_str().unwrap())).unwrap();
let mut lock =
LockFile::decode(&fs::read(prepared.join("dependency/dever.lock")).unwrap()).unwrap();
let runtime = lock.libs[0].runtime.clone();
assert_eq!(sha(&pack), runtime.sha256);
let rejected = lock.builds[0]
.native_rejections
.keys()
.cloned()
.collect::<Vec<_>>();
assert!(
!rejected.is_empty(),
"official multi-platform prebuilds must be classified"
);
let entry = temp.path().join(&contract.entry);
fs::create_dir_all(entry.parent().unwrap()).unwrap();
let adapter = r#"const addon = require('bufferutil/build/Release/bufferutil.node');
const {Worker} = require('node:worker_threads');
const path = require('node:path');
const rejected = __REJECTED__.map(name => path.join(__dirname, '..', name));
exports.send = async payload => {
await new Promise((resolve, reject) => {
const worker = new Worker(`const {parentPort,workerData}=require('node:worker_threads'); const fs=require('node:fs'); parentPort.postMessage(workerData.every(path=>!fs.existsSync(path)));`, {eval:true,workerData:rejected});
worker.once('message', ok => ok ? resolve() : reject(new Error('unverified addon was installed')));
worker.once('error', reject);
});
const input=Buffer.from([payload.value]), output=Buffer.alloc(1), mask=Buffer.from([1,2,3,4]);
addon.mask(input,mask,output,0,1); addon.unmask(output,mask);
return {receipt:output[0]+1};
};
"#.replace("__REJECTED__",&serde_json::to_string(&rejected).unwrap());
fs::write(&entry, adapter).unwrap();
let mut resources = vec![resource(
format!(
"lib/runtime/npm/{}/runtime.pack",
dever_cli::toolchain::platform_identity()
),
pack,
)];
for library in &mut lock.libs {
library.schema = contract.schema.clone();
}
for artifact in lock
.libs
.iter()
.flat_map(|lib| &lib.artifacts)
.chain(lock.builds.iter().map(|receipt| &receipt.output))
{
let bytes = fs::read(prepared.join("dependency/artifacts").join(&artifact.sha256)).unwrap();
assert_eq!(sha(&bytes), artifact.sha256);
resources.push(resource(artifact.path.clone(), bytes));
}
for asset in sandbox::assets(&workspace) {
let mut file = resource(asset.path, asset.bytes);
file.executable = asset.executable;
resources.push(file);
}
lock.workers.push(LockedWorker {
port: contract.port.clone(),
adapter: contract.adapter.clone(),
ecosystem: "npm".into(),
runtime: Some(runtime),
entry: contract.entry.clone(),
schema: contract.schema.clone(),
capabilities: contract.capabilities.clone(),
operations: contract.operations.clone(),
libs: contract
.libs
.iter()
.map(|spec| spec.parse().unwrap())
.collect(),
});
lock.write_atomic(temp.path()).unwrap();
let mut generated = workers::prepare(temp.path(), &program, &resources).unwrap();
let again = workers::prepare(temp.path(), &program, &resources).unwrap();
assert_eq!(
generated
.iter()
.map(|file| (&file.path, &file.sha256))
.collect::<Vec<_>>(),
again
.iter()
.map(|file| (&file.path, &file.sha256))
.collect::<Vec<_>>()
);
assert!(
generated
.iter()
.any(|file| file.path.ends_with("/build/Release/bufferutil.node")
&& file.bytes.starts_with(b"\x7fELF"))
);
for rejected in &rejected {
assert!(!generated.iter().any(|file| file.path.ends_with(rejected)));
}
generated.extend(
resources
.into_iter()
.filter(|file| file.path.starts_with("sandbox/")),
);
fs::remove_file(entry).unwrap();
run_packaged_protocol(&temp, &contract, "npm", &generated);
}
+642
View File
@@ -0,0 +1,642 @@
//! Actual default CLI boundary. Native cases consume an explicitly prepared
//! runtime archive; ordinary negative checks never start a compiler subprocess.
#![cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::time::Duration;
use serde_json::{Value, json};
#[path = "support/llvm_pack.rs"]
mod pack;
#[path = "../dever-tests/tests/support/process.rs"]
mod process;
#[path = "../dever-tests/tests/support/temp.rs"]
mod temp;
use temp::TemporaryDirectory;
const APP: &str = r#"type Outcome { error Denied(message: Text, labels: List<Text?>) }
echo(name: Text) (value: Text) { dever.task.sleep(1)
value = name }
divide(divisor: Int) (value: Int) { value = 42 // divisor }
deny() (value: Text) { fail(Outcome.Denied("owned denial", ["nested label", null])) }
"#;
fn write(root: &Path, relative: &str, source: &str) {
let path = root.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, source).unwrap();
}
fn project(root: &Path) {
write(root, "module/sample/echo/app.dever", APP);
write(
root,
"module/sample/echo/api.dever",
"cmd echo = app.echo\ncmd divide = app.divide\ncmd deny = app.deny",
);
write(root, "config/setting.json", "{}");
}
fn compiler(root: &Path, name: &str) -> PathBuf {
pack::compiler(Path::new(env!("CARGO_BIN_EXE_dever")), root, name)
}
fn output(command: &mut Command) -> Output {
let capture = TemporaryDirectory::new();
let stdout = capture.path().join("stdout");
let stderr = capture.path().join("stderr");
command
.stdout(Stdio::from(fs::File::create(&stdout).unwrap()))
.stderr(Stdio::from(fs::File::create(&stderr).unwrap()));
let status = process::status(command, Duration::from_secs(180)).unwrap();
Output {
status,
stdout: fs::read(stdout).unwrap(),
stderr: fs::read(stderr).unwrap(),
}
}
fn cli(compiler: &Path, project: &Path, action: &str, args: &[&str]) -> Output {
output(
Command::new(compiler)
.env_clear()
.arg(action)
.arg(project)
.args(args),
)
}
fn success(output: &Output) {
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn failure(output: &Output, expected: &str) {
assert!(!output.status.success(), "unexpected success");
let message = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
message.contains(expected),
"expected {expected:?}: {message}"
);
}
#[test]
fn missing_pack_fails_without_host_tools_and_check_and_empty_test_need_no_pack() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let help = output(Command::new(&compiler).env_clear().arg("--help"));
success(&help);
assert!(String::from_utf8_lossy(&help.stdout).contains("dever run <project-root>"));
let project_dir = TemporaryDirectory::new();
project(project_dir.path());
success(&cli(&compiler, project_dir.path(), "check", &[]));
let tests = cli(&compiler, project_dir.path(), "test", &[]);
success(&tests);
assert!(String::from_utf8_lossy(&tests.stdout).contains("0 passed; 0 failed"));
failure(
&cli(
&compiler,
project_dir.path(),
"run",
&["--", "sample.echo.echo", "{\"name\":\"Ada\"}"],
),
"runtime pack",
);
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn cross_build_requires_the_selected_pack_even_when_host_pack_exists() {
let root = TemporaryDirectory::new();
let compiler = compiler(root.path(), "dever");
pack::manifest(root.path(), false);
let project_dir = TemporaryDirectory::new();
project(project_dir.path());
let executable = root.path().join("arm-application");
failure(
&cli(
&compiler,
project_dir.path(),
"build",
&[
"--target",
"linux-aarch64",
"--output",
executable.to_str().unwrap(),
],
),
"runtime/linux-aarch64",
);
assert!(!executable.exists());
assert!(!root.path().join("cache").exists());
}
#[test]
fn managed_cores_never_fall_back_to_private_packs_or_cache() {
let root = TemporaryDirectory::new();
let project_dir = TemporaryDirectory::new();
project(project_dir.path());
for (directory, diagnostic) in [
(
root.path().join("standalone"),
"managed compiler must be installed",
),
(
root.path().join("versions/0.1.0"),
"cannot inspect deverd state directory",
),
] {
let compiler = compiler(&directory, "dever-core");
failure(&cli(&compiler, project_dir.path(), "run", &[]), diagnostic);
assert!(!directory.join("cache").exists());
}
let installed = root.path().join("versions/0.1.0/dever-core");
let worker = output(
Command::new(installed)
.env_clear()
.current_dir(project_dir.path())
.arg("--dever-compile-worker"),
);
failure(&worker, "service-owned native cache staging directory");
assert!(!project_dir.path().join("program").exists());
}
#[test]
fn pack_contract_rejects_drift_duplicate_paths_and_undeclared_link_inputs() {
let root = TemporaryDirectory::new();
let compiler = compiler(root.path(), "dever");
let original = pack::manifest(root.path(), false);
let project_dir = TemporaryDirectory::new();
project(project_dir.path());
let mutations: &[(&str, Value, &str)] = &[
("/format", json!("future"), "does not match"),
("/compiler_version", json!("99.0.0"), "does not match"),
("/abi", json!(99), "does not match"),
(
"/target",
json!("aarch64-unknown-linux-gnu"),
"does not match",
),
("/profiles/base", Value::Null, "missing the required 'base'"),
(
"/profiles/base/runtime",
json!("missing.a"),
"undeclared or duplicate",
),
(
"/profiles/base/libraries/0",
json!("runtime.a"),
"undeclared or duplicate",
),
(
"/files/0/path",
json!("../runtime.a"),
"normalized relative",
),
(
"/files/0/path",
json!("nested//runtime.a"),
"normalized relative",
),
(
"/files/0/path",
json!("C:\\runtime.a"),
"normalized relative",
),
(
"/files/1/path",
json!("runtime.a"),
"duplicate runtime file",
),
("/files/0/bytes", json!(9), "SHA-256 verification"),
(
"/files/0/sha256",
json!("0".repeat(64)),
"SHA-256 verification",
),
];
for (pointer, value, expected) in mutations {
let mut manifest = original.clone();
*manifest.pointer_mut(pointer).unwrap() = value.clone();
pack::write_manifest(root.path(), &manifest);
failure(&cli(&compiler, project_dir.path(), "run", &[]), expected);
assert!(!root.path().join("cache").exists());
}
let text = serde_json::to_string(&original).unwrap();
fs::write(
pack::pack_root(root.path()).join("manifest.json"),
text.replacen("\"abi\":1", "\"abi\":1,\"abi\":1", 1),
)
.unwrap();
failure(
&cli(&compiler, project_dir.path(), "run", &[]),
"duplicate field",
);
}
#[test]
fn pack_rejects_symlinks_and_disguised_linker_scripts() {
use std::os::unix::fs::symlink;
let root = TemporaryDirectory::new();
let compiler = compiler(root.path(), "dever");
let mut manifest = pack::manifest(root.path(), false);
let project_dir = TemporaryDirectory::new();
project(project_dir.path());
let runtime = pack::pack_root(root.path()).join("runtime.a");
let outside = root.path().join("outside.a");
fs::rename(&runtime, &outside).unwrap();
symlink(&outside, &runtime).unwrap();
failure(
&cli(&compiler, project_dir.path(), "run", &[]),
"must be a real file",
);
fs::remove_file(&runtime).unwrap();
for content in [b"INPUT(/usr/lib/forbidden.a)".as_slice(), b"!<thin>\n"] {
fs::write(&runtime, content).unwrap();
manifest["files"][0]["bytes"] = json!(content.len());
manifest["files"][0]["sha256"] = json!(pack::digest(&runtime));
pack::write_manifest(root.path(), &manifest);
failure(
&cli(&compiler, project_dir.path(), "run", &[]),
"linker scripts and thin archives are forbidden",
);
}
assert!(!root.path().join("cache").exists());
}
fn cached_programs(root: &Path) -> Vec<PathBuf> {
let mut programs = fs::read_dir(root.join("cache/native"))
.unwrap()
.map(|entry| entry.unwrap().path().join("program"))
.filter(|path| path.is_file())
.collect::<Vec<_>>();
programs.sort();
programs
}
#[test]
#[ignore = "requires explicitly prepared full runtime archive and Linux CRT files; serial offline CLI execution"]
fn default_llvm_runs_builds_moves_and_reuses_cache_without_host_toolchain() {
let root = TemporaryDirectory::new();
let compiler = compiler(root.path(), "dever");
pack::manifest(root.path(), true);
let project_dir = TemporaryDirectory::new();
project(project_dir.path());
let args = ["--", "sample.echo.echo", "{\"name\":\"你好 LLVM\"}"];
let marker = root.path().join("forbidden-tool-was-started");
for name in ["rustc", "cargo", "cc"] {
use std::os::unix::fs::PermissionsExt;
let path = root.path().join(name);
fs::write(
&path,
format!(
"#!/bin/sh\nprintf forbidden > '{}'\nexit 99\n",
marker.display()
),
)
.unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).unwrap();
}
let run = output(
Command::new(&compiler)
.env_clear()
.env("PATH", root.path())
.env("RUSTC", root.path().join("rustc"))
.env("CARGO", root.path().join("cargo"))
.env("CC", root.path().join("cc"))
.arg("run")
.arg(project_dir.path())
.args(args),
);
success(&run);
assert!(
!marker.exists(),
"LLVM compilation must not start a host compiler"
);
let response: Value = serde_json::from_slice(&run.stdout).unwrap();
assert_eq!(response["data"], "你好 LLVM");
let cached = cached_programs(root.path());
assert_eq!(cached.len(), 1);
let modified = fs::metadata(&cached[0]).unwrap().modified().unwrap();
let binary = project_dir.path().join("standalone");
success(&cli(
&compiler,
project_dir.path(),
"build",
&["--output", binary.to_str().unwrap()],
));
assert_eq!(
fs::metadata(&cached[0]).unwrap().modified().unwrap(),
modified
);
let first_digest = pack::digest(&binary);
failure(
&cli(
&compiler,
project_dir.path(),
"build",
&["--output", binary.to_str().unwrap()],
),
"cannot save executable",
);
assert_eq!(pack::digest(&binary), first_digest);
let moved = TemporaryDirectory::new();
let relocated = moved.path().join("application");
fs::rename(&binary, &relocated).unwrap();
write(moved.path(), "config/setting.json", "{}");
success(&output(
Command::new(&relocated).env_clear().args(&args[1..]),
));
{
use std::os::fd::OwnedFd;
use std::os::unix::net::UnixStream;
let (reader, writer) = UnixStream::pair().unwrap();
drop(reader);
let stderr = moved.path().join("closed-stdout-error");
let status = process::status(
Command::new(&relocated)
.env_clear()
.args(&args[1..])
.stdout(Stdio::from(OwnedFd::from(writer)))
.stderr(Stdio::from(fs::File::create(&stderr).unwrap())),
Duration::from_secs(30),
)
.unwrap();
assert_eq!(
status.code(),
Some(1),
"closed stdout must return a checked failure, not SIGPIPE"
);
let error = fs::read_to_string(stderr).unwrap();
assert!(
error.contains("Broken pipe") && error.contains(".dever"),
"{error}"
);
}
let denied = cli(
&compiler,
project_dir.path(),
"run",
&["--", "sample.echo.deny", "{}"],
);
failure(&denied, "owned denial");
assert!(String::from_utf8_lossy(&denied.stderr).contains("nested label"));
assert!(String::from_utf8_lossy(&denied.stderr).contains("app.dever"));
failure(
&cli(
&compiler,
project_dir.path(),
"run",
&["--", "sample.echo.echo", "{}"],
),
"name",
);
let failure_output = cli(
&compiler,
project_dir.path(),
"run",
&["--", "sample.echo.divide", "{\"divisor\":0}"],
);
failure(&failure_output, "division");
assert!(String::from_utf8_lossy(&failure_output.stderr).contains("app.dever"));
// Corrupt only the fixture-owned cache copy, never a hard-linked runtime.
fs::write(&cached[0], b"corrupt cached executable").unwrap();
failure(
&cli(&compiler, project_dir.path(), "run", &args),
"incomplete or modified",
);
assert_eq!(fs::read(&cached[0]).unwrap(), b"corrupt cached executable");
write(
project_dir.path(),
"module/sample/echo/app.dever",
&APP.replace("value = name", "value = name + \"!\""),
);
let changed = cli(&compiler, project_dir.path(), "run", &args);
success(&changed);
assert_eq!(
serde_json::from_slice::<Value>(&changed.stdout).unwrap()["data"],
"你好 LLVM!"
);
assert_eq!(cached_programs(root.path()).len(), 2);
// Even a byte-level manifest change has its own exact pack identity.
let manifest = pack::pack_root(root.path()).join("manifest.json");
let mut bytes = fs::read(&manifest).unwrap();
bytes.push(b'\n');
fs::write(&manifest, bytes).unwrap();
success(&cli(&compiler, project_dir.path(), "run", &args));
assert_eq!(cached_programs(root.path()).len(), 3);
let markdown = TemporaryDirectory::new();
write(markdown.path(), "config/setting.json", "{}");
write(
markdown.path(),
"module/system/health/app.dever.md",
include_str!("../../examples/cms/md/module/system/health/app.dever.md"),
);
write(
markdown.path(),
"module/system/health/api.dever.md",
include_str!("../../examples/cms/md/module/system/health/api.dever.md"),
);
let response = cli(
&compiler,
markdown.path(),
"run",
&["--", "system.health.ping", "{}"],
);
success(&response);
assert_eq!(
serde_json::from_slice::<Value>(&response.stdout).unwrap()["data"]["status"],
"ready"
);
assert_eq!(cached_programs(root.path()).len(), 4);
let object = pack::pack_root(root.path()).join("crt1.o");
let mut corrupt = fs::read(&object).unwrap();
corrupt[16] ^= 1;
// Break this fixture's hard link before mutation; the host CRT is immutable.
fs::remove_file(&object).unwrap();
fs::write(&object, corrupt).unwrap();
failure(
&cli(&compiler, project_dir.path(), "run", &args),
"SHA-256 verification",
);
assert_eq!(cached_programs(root.path()).len(), 4);
}
#[test]
#[ignore = "requires explicitly prepared runtime archive and component-fixture; no downloads or language SDK"]
fn unsigned_llvm_run_and_build_reject_exec_workers() {
let root = TemporaryDirectory::new();
let compiler = compiler(root.path(), "dever");
pack::manifest(root.path(), true);
let project_dir = TemporaryDirectory::new();
let directory = project_dir.path();
write(
directory,
"module/notification/delivery/app.dever",
"read() (value: Int) { value = port.read() }",
);
write(
directory,
"module/notification/delivery/port.dever",
"read() (value: Int) fails dever.time.SleepResult",
);
write(
directory,
"module/notification/delivery/adapter.dever",
"setting { mode: Text }\nexternal exec \"worker\" {}",
);
write(
directory,
"module/notification/delivery/api.dever",
"cmd read = app.read",
);
let settings = r#"{"adapter":{"notification.delivery":{"setting":{"mode":"embedded"}}}}"#;
write(directory, "config/setting.json", settings);
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../target/native-release-inputs/component-fixture");
assert!(
fixture.is_file(),
"prepare the immutable component-fixture in target/native-release-inputs"
);
fs::copy(
fixture,
directory.join("module/notification/delivery/worker"),
)
.unwrap();
let args = ["--", "notification.delivery.read", "{}"];
let run = cli(&compiler, directory, "run", &args);
let binary = directory.join("packaged");
let build = cli(
&compiler,
directory,
"build",
&["--output", binary.to_str().unwrap()],
);
for rejected in [run, build] {
assert!(!rejected.status.success());
assert!(rejected.stdout.is_empty());
assert_eq!(
String::from_utf8(rejected.stderr).unwrap().trim(),
"real Lib providers require a signed managed Dever release"
);
}
assert!(!binary.exists());
assert!(!root.path().join("cache").exists());
}
#[test]
#[ignore = "requires explicitly prepared full runtime archive; isolated SQLite and case-local fake processes"]
fn llvm_test_preserves_case_isolation_fakes_and_failure_continuation() {
let root = TemporaryDirectory::new();
let compiler = compiler(root.path(), "dever");
pack::manifest(root.path(), true);
let project_dir = TemporaryDirectory::new();
let directory = project_dir.path();
write(
directory,
"module/sample/account/model.dever",
"type Account { name: Text }",
);
write(
directory,
"module/sample/account/app.dever",
"create(name: Text) (count: Int) { row = model.create({ name = name })\n count = model.count() }",
);
write(
directory,
"module/notification/mail/app.dever",
"read() (value: Int) { value = port.read() }",
);
write(
directory,
"module/notification/mail/port.dever",
"read() (value: Int) fails dever.time.SleepResult",
);
write(
directory,
"module/notification/mail/adapter.dever",
"external exec \"worker/must-not-start\" {}",
);
write(
directory,
"config/setting.json",
r#"{"database":{"default":{"type":"sqlite","path":"deployment.db"}}}"#,
);
// The CLI reads project dependency metadata from valid JSON, but test
// execution must use its generated database, never this unusable directory.
fs::create_dir(directory.join("deployment.db")).unwrap();
for name in ["first", "second"] {
write(
directory,
&format!("test/sample/account/{name}.dever"),
&format!("{name}() () {{ assert_eq(app.create(\"Ada\"), 1) }}"),
);
}
write(
directory,
"test/notification/mail/blocking.dever",
"blocking() () { dever.time.sleep(1)\n assert_eq(app.read(), 11) }\nport.read() (value: Int) { value = 11 }",
);
write(
directory,
"test/notification/mail/suspending.dever",
"suspending() () { assert_eq(app.read(), 23) }\nport.read() (value: Int) { dever.task.sleep(1)\n value = 23 }",
);
write(
directory,
"test/sample/account/failure.dever",
"failure() () { assert_eq(1, 2) }",
);
write(
directory,
"test/sample/account/business.dever",
"type Outcome { error Denied(message: Text) }\nbusiness() () { fail(Outcome.Denied(\"test business denial\")) }",
);
let result = cli(&compiler, directory, "test", &[]);
failure(&result, "4 passed; 2 failed; 0 not run");
let report = String::from_utf8_lossy(&result.stdout);
assert!(
report.contains("test sample/account/second ... ok"),
"{report}"
);
assert!(report.contains("failure.dever"), "{report}");
assert!(
report.contains("assert_eq failed: actual = 1, expected = 2"),
"{report}"
);
assert!(report.contains("test business denial"), "{report}");
assert!(!directory.join("data").exists());
assert_eq!(cached_programs(root.path()).len(), 1);
let suite = &cached_programs(root.path())[0];
for args in [
vec![],
vec![""],
vec!["-1"],
vec!["+0"],
vec![" 0"],
vec!["0 "],
vec!["18446744073709551616"],
vec!["6"],
vec!["0", "extra"],
] {
let rejected = output(Command::new(suite).env_clear().args(args));
failure(&rejected, "test program requires a valid case index");
assert_eq!(
rejected.stderr,
b"test program requires a valid case index\n"
);
}
}
+542
View File
@@ -0,0 +1,542 @@
//! Authoring contracts use tiny synthetic payloads; real link/install checks are opt-in.
#![cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
use std::collections::BTreeMap;
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
use std::path::{Path, PathBuf};
use dever_cli::toolchain::{BuildTarget, packaging, platform_identity, runtime_pack, sha256_file};
use ring::rand::SystemRandom;
use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
use serde_json::{Value, json};
#[path = "../dever-tests/tests/support/temp.rs"]
mod temp;
use temp::TemporaryDirectory;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "native_release/acceptance.rs"]
mod acceptance;
#[path = "native_release/ecosystems.rs"]
mod ecosystems;
#[cfg(target_os = "linux")]
#[path = "native_release/bootstrap.rs"]
mod bootstrap;
#[path = "support/native_elf.rs"]
mod native_elf;
#[path = "support/native_objects.rs"]
mod native_objects;
#[path = "native_release/targets.rs"]
mod targets;
#[test]
fn sandbox_bootstrap_requires_static_elf_and_complete_guard_libraries() {
let loader = format!("lib/{}", dever_sandbox::loader_name());
let mut files = vec![
("bin/bwrap".into(), native_elf::static_executable()),
(
"bin/guard".into(),
native_elf::elf(None, &["libc.so.6"], None),
),
(
loader,
native_elf::library(dever_sandbox::loader_name(), &[]),
),
(
"lib/libc.so.6".into(),
native_elf::library("libc.so.6", &[]),
),
];
dever_sandbox::validate_assets(&files).unwrap();
files[0].1 = native_elf::elf(None, &[], None);
assert!(
dever_sandbox::validate_assets(&files)
.unwrap_err()
.contains("static target ELF")
);
files[0].1 = native_elf::library("bwrap", &["libc.so.6"]);
assert!(
dever_sandbox::validate_assets(&files)
.unwrap_err()
.contains("static target ELF")
);
files[0].1 = native_elf::static_executable();
files.pop();
assert!(
dever_sandbox::validate_assets(&files)
.unwrap_err()
.contains("sandbox dependency 'lib/libc.so.6' is missing")
);
}
struct Author {
directory: TemporaryDirectory,
root: PathBuf,
settings: Value,
public_key: Vec<u8>,
}
impl Author {
fn new() -> Self {
let directory = TemporaryDirectory::new();
let root = directory.path().join("author");
fs::create_dir_all(root.join("config")).unwrap();
fs::create_dir(root.join("inputs")).unwrap();
let key = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
fs::write(root.join("signing.pk8"), key.as_ref()).unwrap();
fs::set_permissions(root.join("signing.pk8"), fs::Permissions::from_mode(0o600)).unwrap();
let public_key = Ed25519KeyPair::from_pkcs8(key.as_ref())
.unwrap()
.public_key()
.as_ref()
.to_vec();
let input = |name: &str, bytes: &[u8]| {
let source = format!("inputs/{name}");
fs::write(root.join(&source), bytes).unwrap();
json!({ "source": source, "sha256": sha256_file(&root.join(source)).unwrap() })
};
let core = input(
"core",
&native_elf::compiler(&["libLLVM.so.18.1", "libc.so.6"]),
);
let mut llvm = input(
"llvm",
&native_elf::library("libLLVM.so.18.1", &["libc.so.6"]),
);
llvm["path"] = json!("libLLVM.so.18.1");
let mut profiles = json!({});
for name in ["base", "sqlite", "postgres", "both"] {
profiles[name] = input(
name,
&native_objects::archive(BuildTarget::host().unwrap(), name),
);
}
let link_input = |name: &str, bytes: &[u8]| {
let mut value = input(name, bytes);
value["path"] = json!(name);
value
};
let start = link_input(
"start.o",
&native_objects::object(BuildTarget::host().unwrap()),
);
let end = link_input(
"end.o",
&native_objects::object(BuildTarget::host().unwrap()),
);
let library = link_input("system.a", b"!<arch>\n");
let skill = link_input(
"SKILL.md",
b"---\nname: dever-language\ndescription: Develop Dever applications.\n---\n",
);
let settings = json!({
"format": "dever-native-release-input-v1",
"version": env!("CARGO_PKG_VERSION"),
"target": format!("{}-unknown-linux-gnu", std::env::consts::ARCH),
"signing_key": "signing.pk8", "core": core, "core_libraries": [llvm],
"skill": [skill],
"profiles": profiles, "start": [start], "libraries": [library], "end": [end],
});
Self {
directory,
root,
settings,
public_key,
}
}
fn save(&self) {
fs::write(
self.root.join("config/setting.json"),
serde_json::to_vec(&self.settings).unwrap(),
)
.unwrap();
}
fn output(&self, name: &str) -> PathBuf {
self.directory.path().join(name)
}
fn machine(&self) -> dever_cli::toolchain::Layout {
let layout = dever_cli::toolchain::Layout::new(self.output("machine"));
layout.initialize().unwrap();
let key_path = layout.state().join("trusted-release-key");
fs::write(
&key_path,
self.public_key
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
fs::set_permissions(key_path, fs::Permissions::from_mode(0o644)).unwrap();
layout
}
fn reject(&self) -> String {
self.save();
let output = self.output("rejected");
let error = packaging::create(&self.root, &output).unwrap_err();
assert!(!output.exists());
assert_eq!(
fs::read_dir(self.directory.path()).unwrap().count(),
1,
"staging leaked: {error}"
);
assert!(
!error.contains("signing.pk8"),
"private key location leaked: {error}"
);
error
}
}
fn contents(directory: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
fn visit(root: &Path, directory: &Path, files: &mut BTreeMap<PathBuf, Vec<u8>>) {
for entry in fs::read_dir(directory).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if entry.file_type().unwrap().is_dir() {
visit(root, &path, files);
} else {
files.insert(
path.strip_prefix(root).unwrap().to_owned(),
fs::read(path).unwrap(),
);
}
}
}
let mut files = BTreeMap::new();
visit(directory, directory, &mut files);
files
}
fn assert_signed(author: &Author, release: &Path) {
let encoded = fs::read_to_string(release.join("manifest.sig")).unwrap();
let (pairs, remainder) = encoded.trim().as_bytes().as_chunks::<2>();
assert!(remainder.is_empty());
let signature = pairs
.iter()
.map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
.collect::<Vec<_>>();
UnparsedPublicKey::new(&ED25519, &author.public_key)
.verify(
&fs::read(release.join("manifest.json")).unwrap(),
&signature,
)
.unwrap();
}
#[test]
fn release_skill_rejects_missing_entry_and_oversized_files() {
let mut author = Author::new();
author.settings["skill"] = json!([]);
author.save();
assert!(packaging::create(&author.root, &author.output("missing")).is_err());
assert!(!author.output("missing").exists());
let mut author = Author::new();
fs::write(
author.root.join("inputs/SKILL.md"),
vec![b'x'; 1024 * 1024 + 1],
)
.unwrap();
author.settings["skill"][0]["sha256"] =
json!(sha256_file(&author.root.join("inputs/SKILL.md")).unwrap());
author.save();
assert!(
packaging::create(&author.root, &author.output("large"))
.unwrap_err()
.contains("limit")
);
}
#[test]
fn identical_inputs_produce_identical_signed_independent_releases() {
let author = Author::new();
author.save();
let first = author.output("first");
let second = author.output("second");
let manifest = packaging::create(&author.root, &first).unwrap();
assert_eq!(packaging::create(&author.root, &second).unwrap(), manifest);
let before = contents(&first);
assert_eq!(contents(&second), before);
assert_eq!(before.len(), manifest.artifacts.len() + 2);
assert!(
manifest
.artifacts
.windows(2)
.all(|pair| pair[0].path < pair[1].path)
);
for artifact in &manifest.artifacts {
assert_eq!(
sha256_file(&first.join(&artifact.path)).unwrap(),
artifact.sha256
);
assert_eq!(fs::metadata(first.join(&artifact.path)).unwrap().nlink(), 1);
}
assert_signed(&author, &first);
let runtime = runtime_pack::validate(
&first,
env!("CARGO_PKG_VERSION"),
BuildTarget::host().unwrap(),
)
.unwrap();
assert_eq!(runtime.len() + 3, manifest.artifacts.len());
let metadata: Value = serde_json::from_slice(
&before[&PathBuf::from(format!("runtime/{}/manifest.json", platform_identity()))],
)
.unwrap();
for profile in ["base", "sqlite", "postgres", "both"] {
assert_eq!(
metadata["profiles"][profile]["runtime"],
format!("archives/{profile}.a")
);
}
fs::write(author.root.join("inputs/base"), b"changed source").unwrap();
assert_eq!(contents(&first), before);
assert_eq!(contents(&second), before);
}
#[test]
fn compiler_libraries_must_form_a_closed_target_and_search_path() {
let invalid_libraries = [
(
native_elf::library("libLLVM.so.18.1", &["libz.so.1"]),
"not supplied",
),
(
native_elf::library("different.so", &["libc.so.6"]),
"SONAME",
),
(
native_elf::elf(Some("libLLVM.so.18.1"), &[], Some("/usr/lib")),
"search path",
),
(
native_elf::elf(Some("libLLVM.so.18.1"), &[], Some("$ORIGIN/../../lib")),
"search path",
),
(
native_elf::library("libLLVM.so.18.1", &["/usr/lib/libz.so.1"]),
"plain library",
),
];
for (bytes, diagnostic) in invalid_libraries {
let mut author = Author::new();
fs::write(author.root.join("inputs/llvm"), bytes).unwrap();
author.settings["core_libraries"][0]["sha256"] =
json!(sha256_file(&author.root.join("inputs/llvm")).unwrap());
assert!(author.reject().contains(diagnostic));
}
let mut author = Author::new();
let mut bytes = native_elf::compiler(&["libLLVM.so.18.1"]);
bytes[18..20].copy_from_slice(&0u16.to_le_bytes());
fs::write(author.root.join("inputs/core"), bytes).unwrap();
author.settings["core"]["sha256"] =
json!(sha256_file(&author.root.join("inputs/core")).unwrap());
assert!(author.reject().contains("incompatible target"));
let mut author = Author::new();
author.settings["core_libraries"] = json!([]);
assert!(author.reject().contains("not supplied"));
let mut author = Author::new();
let bytes = native_elf::elf(None, &["libLLVM.so.18.1"], Some("/usr/lib"));
fs::write(author.root.join("inputs/core"), bytes).unwrap();
author.settings["core"]["sha256"] =
json!(sha256_file(&author.root.join("inputs/core")).unwrap());
assert!(author.reject().contains("inherited RPATH"));
}
#[test]
fn installation_rechecks_the_signed_native_compiler_closure_before_health_execution() {
let author = Author::new();
author.save();
let layout = author.machine();
let release = layout.downloads().join(env!("CARGO_PKG_VERSION"));
let mut manifest = packaging::create(&author.root, &release).unwrap();
manifest
.artifacts
.retain(|artifact| artifact.path != "lib/libLLVM.so.18.1");
fs::remove_file(release.join("lib/libLLVM.so.18.1")).unwrap();
let encoded = serde_json::to_vec(&manifest).unwrap();
let key =
Ed25519KeyPair::from_pkcs8(&fs::read(author.root.join("signing.pk8")).unwrap()).unwrap();
fs::write(release.join("manifest.json"), &encoded).unwrap();
fs::write(
release.join("manifest.sig"),
key.sign(&encoded)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
let error = dever_cli::toolchain::MachineManager::new(layout.clone())
.install_requested(env!("CARGO_PKG_VERSION"))
.unwrap_err();
assert!(error.contains("not supplied"), "{error}");
assert!(!layout.versions().join(env!("CARGO_PKG_VERSION")).exists());
}
#[test]
fn native_core_resolution_rejects_unsigned_loader_search_entries() {
for entry in [
"lib/libc.so.6",
"lib/glibc-hwcaps/x86-64-v3/libLLVM.so.18.1",
] {
let author = Author::new();
author.save();
let layout = author.machine();
let version = dever_cli::toolchain::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let release = layout.versions().join(version.as_str());
packaging::create(&author.root, &release).unwrap();
let extra = release.join(entry);
fs::create_dir_all(extra.parent().unwrap()).unwrap();
fs::write(extra, b"unsigned loader input").unwrap();
let error = dever_cli::toolchain::MachineManager::new(layout)
.resolve_core(&version)
.unwrap_err();
assert!(
error.contains("unsigned file, directory or link"),
"{error}"
);
}
}
#[test]
fn rejects_invalid_configuration_and_rolls_back_partial_payloads() {
let changes = [
("/format", json!("unknown")),
("/target", json!("x86_64-pc-windows-msvc")),
("/version", json!("1.0")),
("/unexpected", json!(true)),
("/core/unexpected", json!(true)),
("/core/source", json!("../outside")),
("/core/source", json!("/etc/passwd")),
("/core/source", json!("inputs/./core")),
("/core/source", json!("inputs/missing")),
("/profiles/base/sha256", json!("0".repeat(64))),
("/profiles/base/sha256", json!("A".repeat(64))),
("/profiles/postgres", Value::Null),
("/start", json!([])),
("/libraries", json!([])),
("/end", json!([])),
("/end/0/path", json!("start.o")),
("/end/0/path", json!("archives/base.a")),
("/end/0/path", json!("manifest.json")),
("/end/0/path", json!("../../escape.o")),
("/end/0/path", json!("end.txt")),
];
for (pointer, value) in changes {
let mut author = Author::new();
if let Some(slot) = author.settings.pointer_mut(pointer) {
*slot = value;
} else if pointer == "/unexpected" {
author.settings["unexpected"] = value;
} else {
author.settings["core"]["unexpected"] = value;
}
author.reject();
}
let mut author = Author::new();
author.settings["profiles"]
.as_object_mut()
.unwrap()
.remove("sqlite");
author.reject();
for bytes in [b"!<thin>\n".as_slice(), b"linker script".as_slice()] {
let mut author = Author::new();
fs::write(author.root.join("inputs/base"), bytes).unwrap();
author.settings["profiles"]["base"]["sha256"] =
json!(sha256_file(&author.root.join("inputs/base")).unwrap());
assert!(author.reject().contains("self-contained archive"));
}
}
#[test]
fn private_key_and_symlink_inputs_never_enter_the_release() {
let author = Author::new();
fs::set_permissions(
author.root.join("signing.pk8"),
fs::Permissions::from_mode(0o644),
)
.unwrap();
assert!(author.reject().contains("permissions"));
let mut author = Author::new();
symlink("signing.pk8", author.root.join("key-link")).unwrap();
author.settings["signing_key"] = json!("key-link");
author.reject();
let mut author = Author::new();
symlink("inputs/core", author.root.join("payload-link")).unwrap();
author.settings["core"]["source"] = json!("payload-link");
author.reject();
let mut author = Author::new();
symlink("inputs", author.root.join("directory-link")).unwrap();
author.settings["core"]["source"] = json!("directory-link/core");
author.reject();
let mut author = Author::new();
fs::copy(
author.root.join("signing.pk8"),
author.root.join("inputs/key-copy"),
)
.unwrap();
author.settings["core"] = json!({ "source":"inputs/key-copy", "sha256":sha256_file(&author.root.join("inputs/key-copy")).unwrap() });
assert!(author.reject().contains("cannot be included"));
}
#[test]
fn existing_outputs_are_preserved_and_concurrent_creation_has_one_winner() {
let author = Author::new();
author.save();
let existing = author.output("existing");
fs::create_dir(&existing).unwrap();
fs::write(existing.join("keep"), b"original").unwrap();
let before = contents(&existing);
assert!(packaging::create(&author.root, &existing).is_err());
assert_eq!(contents(&existing), before);
let empty = author.output("empty");
fs::create_dir(&empty).unwrap();
assert!(packaging::create(&author.root, &empty).is_err());
assert!(empty.is_dir());
let link = author.output("link");
symlink(&existing, &link).unwrap();
assert!(packaging::create(&author.root, &link).is_err());
assert_eq!(contents(&existing), before);
let output = author.output("concurrent");
let outcomes = std::thread::scope(|scope| {
let first = scope.spawn(|| packaging::create(&author.root, &output));
let second = scope.spawn(|| packaging::create(&author.root, &output));
[first.join().unwrap(), second.join().unwrap()]
});
assert_eq!(outcomes.iter().filter(|result| result.is_ok()).count(), 1);
runtime_pack::validate(
&output,
env!("CARGO_PKG_VERSION"),
BuildTarget::host().unwrap(),
)
.unwrap();
assert_eq!(fs::read_dir(author.directory.path()).unwrap().count(), 5);
}
#[test]
fn duplicate_fields_and_invalid_private_keys_fail_before_publication() {
let author = Author::new();
author.save();
let config = author.root.join("config/setting.json");
let original = fs::read_to_string(&config).unwrap();
let duplicate = original.replacen('{', "{\"signing_key\":\"signing.pk8\",", 1);
fs::write(&config, duplicate).unwrap();
let error = packaging::create(&author.root, &author.output("duplicate")).unwrap_err();
assert!(error.contains("invalid author configuration"));
assert!(!error.contains("signing.pk8"));
assert_eq!(fs::read_dir(author.directory.path()).unwrap().count(), 1);
fs::write(author.root.join("signing.pk8"), b"invalid private key").unwrap();
assert!(author.reject().contains("PKCS#8"));
}
@@ -0,0 +1,567 @@
//! Explicit Linux acceptance using four separately built optimized archives.
use std::fs;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::time::Duration;
use dever_cli::toolchain::{Layout, MachineManager, Version, cache_status, packaging, sha256_file};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
use serde::Deserialize;
use serde_json::{Value, json};
#[path = "../support/daemon.rs"]
mod daemon;
#[path = "../support/llvm_inputs.rs"]
mod inputs;
#[path = "../../dever-tests/tests/support/process.rs"]
mod process;
#[path = "../../dever-tests/tests/support/sandbox.rs"]
mod sandbox;
use super::temp::TemporaryDirectory;
#[path = "ecosystem_acceptance.rs"]
mod ecosystems;
#[path = "bootstrap_acceptance.rs"]
mod bootstrap;
#[path = "cross_acceptance.rs"]
mod cross;
#[path = "command_acceptance.rs"]
mod command_acceptance;
const PROFILES: [&str; 4] = ["base", "sqlite", "postgres", "both"];
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct AcceptanceSettings {
machine_temporary_root: Option<PathBuf>,
}
fn machine_temporary_root(workspace: &Path) -> Result<Option<PathBuf>, String> {
let path = workspace.join("target/native-release-inputs/config/setting.json");
let bytes = match fs::read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(format!("cannot read acceptance settings: {error}")),
};
let settings: AcceptanceSettings = serde_json::from_slice(&bytes)
.map_err(|error| format!("invalid acceptance settings: {error}"))?;
if let Some(root) = &settings.machine_temporary_root
&& (!root.is_absolute()
|| root
.components()
.any(|part| matches!(part, std::path::Component::ParentDir)))
{
return Err("machine_temporary_root must be an absolute path without '..'".into());
}
Ok(settings.machine_temporary_root)
}
fn machine_temporary_directory(workspace: &Path) -> TemporaryDirectory {
match machine_temporary_root(workspace).expect("valid native release acceptance settings") {
Some(root) => TemporaryDirectory::new_in(&root),
None => TemporaryDirectory::new(),
}
}
#[test]
fn acceptance_machine_directory_uses_only_explicit_author_settings() {
let workspace = TemporaryDirectory::new();
assert_eq!(machine_temporary_root(workspace.path()).unwrap(), None);
let config = "target/native-release-inputs/config/setting.json";
write(
workspace.path(),
config,
br#"{"machine_temporary_root":"relative"}"#,
);
assert!(
machine_temporary_root(workspace.path())
.unwrap_err()
.contains("absolute path")
);
write(
workspace.path(),
config,
br#"{"machine_temporary_root":"/tmp","unknown":true}"#,
);
assert!(
machine_temporary_root(workspace.path())
.unwrap_err()
.contains("unknown field")
);
let selected = TemporaryDirectory::new();
write(
workspace.path(),
config,
serde_json::to_vec(&json!({"machine_temporary_root":selected.path()})).unwrap(),
);
let directory = machine_temporary_directory(workspace.path());
assert_eq!(directory.path().parent(), Some(selected.path()));
assert!(directory.path().is_dir());
let path = directory.path().to_owned();
drop(directory);
assert!(!path.exists());
assert!(selected.path().is_dir());
}
fn write(root: &Path, relative: &str, bytes: impl AsRef<[u8]>) {
let path = root.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, bytes).unwrap();
}
fn input(author: &Path, source: &Path, name: &str) -> Value {
let relative = format!("inputs/{name}");
let path = author.join(&relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
// This is immutable test input staging, never a published release artifact.
fs::hard_link(source, &path).unwrap();
json!({"source":relative,"sha256":sha256_file(&path).unwrap()})
}
fn author_inputs(author: &Path, workspace: &Path) -> Ed25519KeyPair {
let document = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
let key = Ed25519KeyPair::from_pkcs8(document.as_ref()).unwrap();
use std::io::Write;
fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(author.join("signing.pk8"))
.unwrap()
.write_all(document.as_ref())
.unwrap();
let mut profiles = serde_json::Map::new();
for profile in PROFILES {
let name = format!("{profile}.a");
profiles.insert(
profile.into(),
input(
author,
&workspace.join("target/native-release-inputs").join(&name),
&name,
),
);
}
let compiler = input(author, Path::new(env!("CARGO_BIN_EXE_dever")), "dever");
let core_libraries = inputs::core_libraries()
.into_iter()
.map(|(name, source)| {
let mut value = input(author, &source, name);
value["path"] = json!(name);
value
})
.collect::<Vec<_>>();
let mut links = serde_json::Map::new();
for (name, source) in inputs::native_inputs()
.into_iter()
.filter(|(name, _)| *name != "runtime.a")
{
let mut entry = input(author, &source, name);
entry["path"] = json!(name);
links.insert(name.into(), entry);
}
let ordered = |names: &[&str]| {
names
.iter()
.map(|name| links[*name].clone())
.collect::<Vec<_>>()
};
let sandbox = sandbox::assets(workspace).into_iter().map(|asset| {
let relative = format!("inputs/{}", asset.path);
write(author, &relative, &asset.bytes);
fs::set_permissions(author.join(&relative), fs::Permissions::from_mode(if asset.executable { 0o755 } else { 0o644 })).unwrap();
json!({"source":relative, "path":asset.path.strip_prefix("sandbox/").unwrap(), "sha256":sha256_file(&author.join(&relative)).unwrap()})
}).collect::<Vec<_>>();
write(
author,
"inputs/SKILL.md",
b"---\nname: dever-language\ndescription: Dever acceptance skill.\n---\n",
);
let skill = json!({"source":"inputs/SKILL.md", "path":"SKILL.md", "sha256":sha256_file(&author.join("inputs/SKILL.md")).unwrap()});
write(
author,
"config/setting.json",
serde_json::to_vec(&json!({
"format":"dever-native-release-input-v1", "version":env!("CARGO_PKG_VERSION"),
"target":"x86_64-unknown-linux-gnu", "signing_key":"signing.pk8",
"core":compiler, "core_libraries":core_libraries, "profiles":profiles,
"skill":[skill],
"start":ordered(&["crt1.o","crti.o","crtbeginT.o"]),
"libraries":ordered(&["libc.a","libm.a","libmvec.a","libgcc.a","libgcc_eh.a"]),
"end":ordered(&["crtend.o","crtn.o"]), "sandbox":sandbox
}))
.unwrap(),
);
key
}
fn command(command: &mut Command) -> Output {
command_with_timeout(command, Duration::from_secs(190))
}
fn isolated_os_command(root: &Path) -> Command {
let mut command = isolated_os_namespace(root);
command.args(["--", "/fixture-proc-init"]);
command
}
fn isolated_os_namespace(root: &Path) -> Command {
let assets = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../target/sandbox-inputs/assets")
.canonicalize()
.expect("prepare the explicit Linux isolation assets");
let init = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../target/debug/examples/native-acceptance-init")
.canonicalize()
.expect("build the explicit static native-acceptance-init example");
let bytes = fs::read(&init).unwrap();
let executable = goblin::elf::Elf::parse(&bytes).unwrap();
assert!(executable.interpreter.is_none() && executable.libraries.is_empty());
// A plain chroot cannot create a nested user namespace. Use a real mount
// namespace root so the program can retain its normal Worker sandbox.
dever_sandbox::validate_bwrap(&fs::read(assets.join("bin/bwrap")).unwrap()).unwrap();
let mut command = Command::new(assets.join("bin/bwrap"));
command
.env_clear()
.args([
"--unshare-all",
"--die-with-parent",
"--new-session",
"--bind",
])
.arg(root)
.args(["/", "--proc", "/proc", "--dev", "/dev", "--chdir", "/"])
.arg("--ro-bind")
.arg(init)
.arg("/fixture-proc-init");
command
}
fn command_with_timeout(command: &mut Command, timeout: Duration) -> Output {
let capture = TemporaryDirectory::new();
let stdout = capture.path().join("stdout");
let stderr = capture.path().join("stderr");
let status = process::status(
command
.env_clear()
.stdout(Stdio::from(fs::File::create(&stdout).unwrap()))
.stderr(Stdio::from(fs::File::create(&stderr).unwrap())),
timeout,
)
.unwrap();
let output = Output {
status,
stdout: fs::read(stdout).unwrap(),
stderr: fs::read(stderr).unwrap(),
};
assert!(
output.status.success(),
"{:?}: {}\n{}",
command,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
output
}
fn application(root: &Path, profile: &str) {
let sqlite = json!({"type":"sqlite","path":"data/owned.sqlite","max_connections":1});
let postgres =
json!({"type":"postgres","url":"postgres://fixture@127.0.0.1:1/unused", "tls":"disabled"});
let settings = match profile {
"base" => json!({}),
"sqlite" => json!({"database":{"default":sqlite}}),
"postgres" => json!({"database":{"default":postgres}}),
"both" => json!({"database":{"default":sqlite,"unused":postgres}}),
_ => unreachable!(),
};
write(
root,
"config/setting.json",
serde_json::to_vec(&settings).unwrap(),
);
write(
root,
"module/sample/value/api.dever",
"cmd exercise = app.exercise\n",
);
let app = if matches!(profile, "sqlite" | "both") {
write(
root,
"module/sample/value/model.dever",
"type Value { name: Text(1, 32) }\n",
);
"exercise() (okay: Bool) { saved = model.create({ name = \"pack\" })\n count = model.count()\n removed = model.delete(saved.id)\n okay = count == 1 and removed == 1 }\n"
} else {
"exercise() (okay: Bool) { dever.task.sleep(1)\n okay = true }\n"
};
write(root, "module/sample/value/app.dever", app);
}
fn assert_response(output: Output) {
assert_response_data(output, json!(true));
}
fn assert_response_data(output: Output, expected: Value) {
let value: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(value["code"], 0);
assert_eq!(value["data"], expected);
}
fn exec_application(root: &Path, workspace: &Path) {
for (name, source) in [
("app", "read() (value: Int) { value = port.read() }"),
("port", "read() (value: Int) fails dever.time.SleepResult"),
(
"adapter",
"setting { mode: Text }\nexternal exec \"worker\" {}",
),
("api", "cmd read = app.read"),
] {
write(
root,
&format!("module/notification/delivery/{name}.dever"),
source,
);
}
write(
root,
"config/setting.json",
br#"{"adapter":{"notification.delivery":{"setting":{"mode":"embedded"}}}}"#,
);
let fixture = workspace.join("target/native-release-inputs/component-fixture");
assert!(
fixture.is_file(),
"prepare the immutable component-fixture in target/native-release-inputs"
);
fs::copy(fixture, root.join("module/notification/delivery/worker")).unwrap();
}
fn install_release(layout: &Layout, version: &Version, key: &Ed25519KeyPair) {
let public_key = key
.public_key()
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
fs::write(layout.state().join("trusted-release-key"), public_key).unwrap();
fs::set_permissions(
layout.state().join("trusted-release-key"),
fs::Permissions::from_mode(0o644),
)
.unwrap();
MachineManager::new(layout.clone())
.install_requested(version.as_str())
.unwrap();
}
fn archive_report(workspace: &Path) -> Value {
let mut profiles = serde_json::Map::new();
for profile in PROFILES {
let path = workspace
.join("target/native-release-inputs")
.join(format!("{profile}.a"));
let output = command(
Command::new("/usr/bin/nm")
.args(["--defined-only", "--demangle"])
.arg(&path),
);
let symbols = String::from_utf8(output.stdout).unwrap();
assert_eq!(
symbols.contains(" sqlite3_open"),
matches!(profile, "sqlite" | "both"),
"{profile} SQLite driver"
);
assert_eq!(
symbols.contains("tokio_postgres"),
matches!(profile, "postgres" | "both"),
"{profile} PostgreSQL driver"
);
assert!(!symbols.contains("LLVMContextCreate"));
profiles.insert(profile.into(), json!({"bytes":fs::metadata(&path).unwrap().len(),"sha256":sha256_file(&path).unwrap()}));
}
json!(profiles)
}
#[test]
#[ignore = "requires prepared optimized archives and root for an owned GNU ABI root"]
fn signed_core_builds_without_host_compiler_libraries_or_tools() {
assert_eq!(
rustix::process::geteuid().as_raw(),
0,
"explicit owned namespace acceptance requires root"
);
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let author = TemporaryDirectory::new();
author_inputs(author.path(), &workspace);
let release = machine_temporary_directory(&workspace);
let payload = release.path().join("release");
let manifest = packaging::create(author.path(), &payload).unwrap();
let isolated = machine_temporary_directory(&workspace);
ecosystems::minimal_os(isolated.path());
let compiler_root = isolated.path().join("compiler");
for artifact in &manifest.artifacts {
let relative = if artifact.path == "dever-core" {
"dever"
} else {
&artifact.path
};
let destination = compiler_root.join(relative);
fs::create_dir_all(destination.parent().unwrap()).unwrap();
// Immutable signed inputs; no host directory is mounted into the root.
fs::hard_link(payload.join(&artifact.path), destination).unwrap();
}
application(&isolated.path().join("project"), "sqlite");
let invoke = |arguments: &[&str]| command(isolated_os_command(isolated.path()).args(arguments));
invoke(&["/compiler/dever", "check", "/project"]);
invoke(&[
"/compiler/dever",
"build",
"/project",
"--output",
"/project/program",
]);
fs::remove_dir_all(isolated.path().join("project/module")).unwrap();
assert_response(invoke(&["/project/program", "sample.value.exercise", "{}"]));
write(&workspace, "target/closure-core-loader-acceptance.json", serde_json::to_vec_pretty(&json!({
"platform":"linux-x86_64", "os_baseline":"GNU libc 2.39 and its named companion libraries",
"core_libraries":manifest.artifacts.iter().filter(|artifact| artifact.path.starts_with("lib/")).collect::<Vec<_>>(),
"checks":{"signed_payload":true,"check_in_minimal_root":true,"sqlite_build_in_minimal_root":true,"source_removed_execution":true},
"host_compiler_libraries":false,"host_language_tools":false
})).unwrap());
}
#[test]
#[ignore = "requires four optimized archives and component-fixture; starts only an owned daemon, SQLite and exec Worker fixtures"]
fn optimized_profiles_make_reproducible_signed_release_and_run_through_daemon() {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let profiles = archive_report(&workspace);
let author = TemporaryDirectory::new();
let key = author_inputs(author.path(), &workspace);
let machine = machine_temporary_directory(&workspace);
let layout = Layout::new(machine.path());
layout.initialize().unwrap();
let first = machine.path().join("first");
let expected = packaging::create(author.path(), &first).unwrap();
let manifest = fs::read(first.join("manifest.json")).unwrap();
let signature = fs::read(first.join("manifest.sig")).unwrap();
// Remove only this test's first completed copy before making the second.
fs::remove_dir_all(&first).unwrap();
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let release = layout.downloads().join(version.as_str());
command(
Command::new(workspace.join("target/debug/examples/native-release"))
.arg(author.path())
.arg("--output")
.arg(&release),
);
assert_eq!(
serde_json::from_slice::<dever_cli::toolchain::ReleaseManifest>(
&fs::read(release.join("manifest.json")).unwrap()
)
.unwrap(),
expected
);
assert_eq!(fs::read(release.join("manifest.json")).unwrap(), manifest);
assert_eq!(fs::read(release.join("manifest.sig")).unwrap(), signature);
install_release(&layout, &version, &key);
fs::remove_dir_all(&release).unwrap();
drop(author);
let compiler = layout.versions().join(version.as_str()).join("dever-core");
let mut daemon = daemon::TestDaemon::start(&layout);
let projects = TemporaryDirectory::new();
let mut binaries = serde_json::Map::new();
let mut programs: Vec<(PathBuf, PathBuf)> = Vec::new();
for profile in PROFILES {
let project = projects.path().join(profile);
application(&project, profile);
command(Command::new(&compiler).arg("check").arg(&project));
assert_response(command(
Command::new(&compiler).arg("run").arg(&project).args([
"--",
"sample.value.exercise",
"{}",
]),
));
let executable = project.join("program");
command(
Command::new(&compiler)
.arg("build")
.arg(&project)
.arg("--output")
.arg(&executable),
);
binaries.insert(profile.into(), json!({"bytes":fs::metadata(&executable).unwrap().len(),"sha256":sha256_file(&executable).unwrap()}));
programs.push((project, executable));
}
assert_eq!(cache_status(&layout).unwrap().entries, 4);
let exec_project = TemporaryDirectory::new();
let exec_source = exec_project.path().to_owned();
exec_application(&exec_source, &workspace);
assert_response_data(
command(Command::new(&compiler).arg("run").arg(&exec_source).args([
"--",
"notification.delivery.read",
"{}",
])),
json!(7),
);
let standalone = TemporaryDirectory::new();
let exec_program = standalone.path().join("application");
fs::create_dir(standalone.path().join("config")).unwrap();
fs::copy(
exec_source.join("config/setting.json"),
standalone.path().join("config/setting.json"),
)
.unwrap();
command(
Command::new(&compiler)
.arg("build")
.arg(&exec_source)
.arg("--output")
.arg(&exec_program),
);
assert_eq!(cache_status(&layout).unwrap().entries, 5);
drop(exec_project);
assert!(!exec_source.exists());
daemon.stop();
assert_response_data(
command(
Command::new(&exec_program)
.current_dir(standalone.path())
.args(["notification.delivery.read", "{}"]),
),
json!(7),
);
assert!(!standalone.path().join("module").exists());
let exec = json!({
"bytes": fs::metadata(&exec_program).unwrap().len(),
"sha256": sha256_file(&exec_program).unwrap(),
"managed_run": true, "standalone_run": true,
"source_project_removed": true, "response": 7
});
for (project, executable) in programs {
fs::remove_dir_all(project.join("module")).unwrap();
assert_response(command(
Command::new(executable).args(["sample.value.exercise", "{}"]),
));
}
let report = json!({
"format":"dever-native-release-acceptance-v1", "platform":"linux-x86_64",
"version":version.as_str(), "profiles":profiles,"programs":binaries,"exec":exec,
"manifest_sha256":format!("{:x}", sha2::Sha256::digest(&manifest)),
"checks":{"pack_determinism":true,"signed_install":true,"managed_profiles":4,"standalone_profiles":4,"sqlite_crud":true,"signed_exec_worker":true},
"postgres_server":"not_run", "cross_host_source_reproducibility":"not_run"
});
use sha2::Digest;
write(
&workspace,
"target/closure-native-release-acceptance.json",
serde_json::to_vec_pretty(&report).unwrap(),
);
}
@@ -0,0 +1,410 @@
//! Bootstrap package validation and recovery use only owned fixture directories.
use super::*;
use dever_cli::toolchain::bootstrap;
use sha2::{Digest, Sha256};
#[test]
fn bootstrap_service_recovery_orders_actions_around_file_restoration() {
use bootstrap::RecoveryAction::{Reload, Restart, Stop};
for active in [false, true] {
for loaded in [false, true] {
assert_eq!(
bootstrap::recovery_actions(false, active, loaded),
(vec![], vec![])
);
}
}
assert_eq!(
bootstrap::recovery_actions(true, false, false),
(vec![], vec![Reload])
);
assert_eq!(
bootstrap::recovery_actions(true, false, true),
(vec![Stop], vec![Reload])
);
assert_eq!(
bootstrap::recovery_actions(true, true, true),
(vec![Stop], vec![Reload, Restart])
);
assert_eq!(
bootstrap::recovery_actions(true, true, false),
(vec![], vec![Reload, Restart])
);
}
fn add_bootstrap(author: &mut Author) {
let input = |name: &str, bytes: &[u8]| {
let relative = format!("inputs/{name}");
fs::write(author.root.join(&relative), bytes).unwrap();
json!({"source":relative,"sha256":sha256_file(&author.root.join(&relative)).unwrap()})
};
let launcher = input(
"dever",
&native_elf::compiler(&["libgcc_s.so.1", "libc.so.6"]),
);
let daemon = input(
"deverd",
&native_elf::compiler(&["libgcc_s.so.1", "libc.so.6"]),
);
let mut gcc = input("gcc", &native_elf::library("libgcc_s.so.1", &["libc.so.6"]));
gcc["path"] = json!("libgcc_s.so.1");
author.settings["bootstrap"] = json!({"launcher":launcher,"daemon":daemon,"libraries":[gcc]});
}
#[test]
fn bootstrap_payload_has_a_separate_signed_complete_loader_closure() {
let mut author = Author::new();
add_bootstrap(&mut author);
author.save();
let release = author.output("complete");
let manifest = packaging::create(&author.root, &release).unwrap();
assert_signed(&author, &release);
for path in [
"bootstrap/dever",
"bootstrap/deverd",
"bootstrap/lib/libgcc_s.so.1",
"bootstrap/deverd.service",
"bootstrap/dever-sandbox",
] {
let artifact = manifest
.artifacts
.iter()
.find(|artifact| artifact.path == path)
.unwrap();
assert_eq!(artifact.sha256, sha256_file(&release.join(path)).unwrap());
}
let unit = fs::read_to_string(release.join("bootstrap/deverd.service")).unwrap();
assert!(unit.contains("ExecStart=/opt/dever/bin/deverd --root /opt/dever"));
assert!(unit.contains("RuntimeDirectory=dever"));
assert!(!unit.contains("Environment"));
assert!(!unit.contains("PATH"));
assert_eq!(
fs::read_to_string(release.join("bootstrap/dever-sandbox")).unwrap(),
dever_sandbox::APPARMOR_PROFILE
);
}
#[test]
fn bootstrap_authoring_rejects_missing_libraries_wrong_sonames_and_search_paths() {
let mut author = Author::new();
add_bootstrap(&mut author);
author.settings["bootstrap"]["libraries"] = json!([]);
assert!(author.reject().contains("libgcc_s.so.1"));
let mut author = Author::new();
add_bootstrap(&mut author);
fs::write(
author.root.join("inputs/gcc"),
native_elf::library("libwrong.so", &["libc.so.6"]),
)
.unwrap();
author.settings["bootstrap"]["libraries"][0]["sha256"] =
json!(sha256_file(&author.root.join("inputs/gcc")).unwrap());
assert!(author.reject().contains("SONAME"));
let mut author = Author::new();
add_bootstrap(&mut author);
fs::write(
author.root.join("inputs/dever"),
native_elf::elf(None, &["libgcc_s.so.1"], Some("/usr/lib")),
)
.unwrap();
author.settings["bootstrap"]["launcher"]["sha256"] =
json!(sha256_file(&author.root.join("inputs/dever")).unwrap());
assert!(author.reject().contains("RPATH"));
}
#[test]
fn signed_installed_versions_keep_their_original_bootstrap_service_template() {
use dever_cli::toolchain::{Layout, MachineManager, Version};
let mut author = Author::new();
add_bootstrap(&mut author);
author.save();
let machine = TemporaryDirectory::new();
let layout = Layout::new(machine.path().join("machine"));
layout.initialize().unwrap();
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let installed = layout.versions().join(version.as_str());
let mut manifest = packaging::create(&author.root, &installed).unwrap();
let service = installed.join("bootstrap/deverd.service");
let current = fs::read_to_string(&service).unwrap();
let old = current.replace("RestartSec=1", "RestartSec=2");
assert_ne!(current, old);
fs::write(&service, old).unwrap();
let artifact = manifest
.artifacts
.iter_mut()
.find(|artifact| artifact.path == "bootstrap/deverd.service")
.unwrap();
artifact.bytes = fs::metadata(&service).unwrap().len();
artifact.sha256 = sha256_file(&service).unwrap();
let encoded = serde_json::to_vec(&manifest).unwrap();
let key =
Ed25519KeyPair::from_pkcs8(&fs::read(author.root.join("signing.pk8")).unwrap()).unwrap();
fs::write(installed.join("manifest.json"), &encoded).unwrap();
fs::write(
installed.join("manifest.sig"),
key.sign(&encoded)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
fs::write(
layout.state().join("trusted-release-key"),
author
.public_key
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
let manager = MachineManager::new(layout);
assert_eq!(
manager.resolve_core(&version).unwrap(),
installed.join("dever-core")
);
fs::write(service, b"unsigned modification").unwrap();
assert!(
manager
.resolve_core(&version)
.unwrap_err()
.contains("integrity verification")
);
}
#[test]
#[ignore = "root-only installer trust rejection; never touches the actual machine installation"]
fn bootstrap_installer_rejects_untrusted_inputs_before_publication() {
assert_eq!(rustix::process::geteuid().as_raw(), 0);
assert!(
bootstrap::install(Path::new("."))
.unwrap_err()
.contains("absolute paths")
);
assert!(
bootstrap::recover(Path::new("."))
.unwrap_err()
.contains("absolute paths")
);
let mut author = Author::new();
add_bootstrap(&mut author);
author.save();
let package = author.output("release");
packaging::create(&author.root, &package).unwrap();
let installer = TemporaryDirectory::new();
let system = TemporaryDirectory::new();
let key = installer.path().join("trusted.pub");
fs::write(
&key,
author
.public_key
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
fs::create_dir(installer.path().join("config")).unwrap();
let save = |key: &Path, activate: bool| {
fs::write(installer.path().join("config/setting.json"), serde_json::to_vec(&json!({"release":package,"system_root":system.path(),"trusted_key":key,"activate_service":activate})).unwrap()).unwrap()
};
save(&key, true);
assert!(
bootstrap::install(installer.path())
.unwrap_err()
.contains("actual system root")
);
let inside = package.join("trusted.pub");
fs::copy(&key, &inside).unwrap();
save(&inside, false);
assert!(
bootstrap::install(installer.path())
.unwrap_err()
.contains("independently")
);
save(&key, false);
fs::set_permissions(&key, fs::Permissions::from_mode(0o666)).unwrap();
assert!(
bootstrap::install(installer.path())
.unwrap_err()
.contains("root-owned")
);
fs::set_permissions(&key, fs::Permissions::from_mode(0o644)).unwrap();
let signature = fs::read(package.join("manifest.sig")).unwrap();
fs::write(package.join("manifest.sig"), "00".repeat(64)).unwrap();
assert!(
bootstrap::install(installer.path())
.unwrap_err()
.contains("signature verification")
);
fs::write(package.join("manifest.sig"), signature).unwrap();
// The key is pinned before the intentionally non-executable synthetic core
// fails its health check. Empty/partial temporary files model interrupted
// writes without injecting product failpoints or touching real services.
let state = system.path().join("opt/dever/state");
let temporary_key = state.join(".trusted-release-key.next");
let pinned_key = state.join("trusted-release-key");
for partial in ["", "abc"] {
fs::write(&temporary_key, partial).unwrap();
let error = bootstrap::install(installer.path()).unwrap_err();
assert!(!error.contains("temporary release key"), "{error}");
assert_eq!(
fs::read_to_string(&pinned_key).unwrap().trim(),
fs::read_to_string(&key).unwrap()
);
assert!(!temporary_key.exists());
fs::remove_file(&pinned_key).unwrap();
}
fs::write(&pinned_key, "11".repeat(32)).unwrap();
assert!(
bootstrap::install(installer.path())
.unwrap_err()
.contains("pinned release key")
);
assert_eq!(fs::read_to_string(&pinned_key).unwrap(), "11".repeat(32));
fs::remove_file(&pinned_key).unwrap();
fs::write(package.join("bootstrap/lib/libgcc_s.so.1"), b"tampered").unwrap();
assert!(
bootstrap::install(installer.path())
.unwrap_err()
.contains("integrity verification")
);
fs::remove_file(package.join("bootstrap/lib/libgcc_s.so.1")).unwrap();
assert!(bootstrap::install(installer.path()).is_err());
assert!(!system.path().join("usr/local/bin/dever").exists());
assert!(
!system
.path()
.join("opt/dever/state/active-version")
.exists()
);
}
// Independent construction of a one-node journal identity lets recovery tests
// simulate interruption at publication boundaries without product failpoints.
fn node_hash(kind: &str, bytes: &[u8]) -> String {
let mut hash = Sha256::new();
hash.update(0u64.to_le_bytes());
hash.update(
if kind == "directory" {
0o755u32
} else {
0o644u32
}
.to_le_bytes(),
);
hash.update(kind.as_bytes());
if kind == "file" {
hash.update((bytes.len() as u64).to_le_bytes());
hash.update(bytes);
}
format!("{:x}", hash.finalize())
}
#[test]
#[ignore = "root-only crash-recovery fixture; all journal targets are below an owned image root"]
fn bootstrap_recovery_is_idempotent_and_rejects_unrecorded_targets() {
assert_eq!(rustix::process::geteuid().as_raw(), 0);
let system = TemporaryDirectory::new();
let root = system.path();
let layout = dever_cli::toolchain::Layout::new(root.join("opt/dever"));
layout.initialize().unwrap();
for directory in [
"usr/local/bin",
"etc/systemd/system/multi-user.target.wants",
"etc/apparmor.d",
] {
fs::create_dir_all(root.join(directory)).unwrap();
}
let service = root.join("etc/systemd/system/deverd.service");
fs::write(&service, b"new service").unwrap();
let entry = root.join("usr/local/bin/dever");
symlink("/opt/dever/bin/dever", &entry).unwrap();
let enable_stage = root
.join("etc/systemd/system/multi-user.target.wants/.dever-bootstrap-deverd.service.next");
symlink("../deverd.service", &enable_stage).unwrap();
let active_stage = layout.state().join(".dever-bootstrap-active-version.next");
fs::write(&active_stage, b"new active").unwrap();
let sentinel = root.join("etc/systemd/system/unrelated.service");
fs::write(&sentinel, b"keep").unwrap();
let link_hash =
|target: &str| format!("{:x}", Sha256::digest(format!("link\0{target}").as_bytes()));
let journal = json!({"format":"dever-bootstrap-transaction-v1","phase":"prepared","service_was_active":null,"service_start_requested":false,"changes":[
{"target":"sandbox","before":null,"after":node_hash("directory",b"")},
{"target":"apparmor","before":null,"after":node_hash("file",b"new profile")},
{"target":"bin","before":node_hash("directory",b""),"after":node_hash("directory",b"")},
{"target":"service","before":null,"after":node_hash("file",b"new service")},
{"target":"entry","before":null,"after":link_hash("/opt/dever/bin/dever")},
{"target":"enable","before":null,"after":link_hash("../deverd.service")},
{"target":"active","before":null,"after":node_hash("file",b"new active")}
]});
fs::create_dir(root.join("opt/dever/.dever-bootstrap-sandbox.next")).unwrap();
fs::write(
root.join("etc/apparmor.d/.dever-bootstrap-dever-sandbox.next"),
b"new profile",
)
.unwrap();
let path = layout.state().join("bootstrap-install.json");
fs::write(&path, serde_json::to_vec(&journal).unwrap()).unwrap();
let mut invalid = journal.clone();
invalid["changes"][0]["target"] = json!("../../unrelated");
fs::write(&path, serde_json::to_vec(&invalid).unwrap()).unwrap();
assert!(bootstrap::recover(root).is_err());
assert_eq!(fs::read(&sentinel).unwrap(), b"keep");
fs::write(&path, serde_json::to_vec(&journal).unwrap()).unwrap();
fs::write(&service, b"not this transaction").unwrap();
assert!(
bootstrap::recover(root)
.unwrap_err()
.contains("outside this transaction")
);
assert_eq!(fs::read(&service).unwrap(), b"not this transaction");
fs::write(&service, b"new service").unwrap();
bootstrap::recover(root).unwrap();
bootstrap::recover(root).unwrap();
assert!(!service.exists());
assert!(fs::symlink_metadata(&entry).is_err());
assert!(!active_stage.exists());
assert!(fs::symlink_metadata(&enable_stage).is_err());
assert_eq!(fs::read(&sentinel).unwrap(), b"keep");
assert!(!path.exists());
// A committed publication cleans only its old staging copies; it must not
// undo the new active version, including after a failed cleanup retry.
fs::write(&service, b"new service").unwrap();
symlink("/opt/dever/bin/dever", &entry).unwrap();
fs::create_dir(root.join("opt/dever/sandbox")).unwrap();
fs::write(root.join("etc/apparmor.d/dever-sandbox"), b"new profile").unwrap();
symlink(
"../deverd.service",
root.join("etc/systemd/system/multi-user.target.wants/deverd.service"),
)
.unwrap();
fs::write(layout.state().join("active-version"), b"new active").unwrap();
fs::write(&active_stage, b"old active").unwrap();
let mut committed = journal;
committed["phase"] = json!("committed");
fs::write(&path, serde_json::to_vec(&committed).unwrap()).unwrap();
fs::write(&service, b"unexpected modification").unwrap();
assert!(
bootstrap::recover(root)
.unwrap_err()
.contains("committed bootstrap destination changed")
);
assert!(path.exists());
assert_eq!(
fs::read(layout.state().join("active-version")).unwrap(),
b"new active"
);
fs::write(&service, b"new service").unwrap();
bootstrap::recover(root).unwrap();
bootstrap::recover(root).unwrap();
assert_eq!(
fs::read(layout.state().join("active-version")).unwrap(),
b"new active"
);
assert!(!active_stage.exists());
assert!(!path.exists());
assert_eq!(fs::read(sentinel).unwrap(), b"keep");
}
@@ -0,0 +1,476 @@
//! Real bootstrap installation and execution; no global service is changed.
use super::*;
use std::os::unix::fs::{MetadataExt, chown};
use std::os::unix::process::CommandExt;
use std::process::Child;
struct Daemon(Child);
impl Drop for Daemon {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn wait_socket(daemon: &mut Daemon, socket: &Path) {
for _ in 0..200 {
assert!(
daemon.0.try_wait().unwrap().is_none(),
"installed daemon exited before becoming ready"
);
if socket.exists() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("installed daemon did not create its socket");
}
/// Clients share the daemon's PID namespace so SO_PEERCRED can identify both
/// peers. Separate bwrap invocations would hide the daemon PID from clients.
struct IsolatedDaemon {
process: Daemon,
namespace_pid: u32,
namespace: std::os::fd::OwnedFd,
root: PathBuf,
stopped: bool,
}
impl IsolatedDaemon {
fn start(root: &Path) -> Self {
let information = TemporaryDirectory::new();
let path = information.path().join("namespace.json");
let output = fs::File::create(&path).unwrap();
let mut process = Daemon(
isolated_os_namespace(root)
.args([
"--info-fd",
"1",
"--",
"/fixture-proc-init",
"/opt/dever/bin/deverd",
"--root",
"/opt/dever",
])
.stdout(Stdio::from(output))
.stderr(Stdio::inherit())
.spawn()
.unwrap(),
);
wait_socket(&mut process, &root.join("run/dever/deverd.sock"));
assert!(fs::metadata(&path).unwrap().len() <= 4096);
let info: Value = serde_json::from_slice(&fs::read(path).unwrap()).unwrap();
let namespace_pid = u32::try_from(info["child-pid"].as_u64().unwrap()).unwrap();
assert_ne!(namespace_pid, 0);
let namespace = rustix::process::pidfd_open(
rustix::process::Pid::from_raw(i32::try_from(namespace_pid).unwrap()).unwrap(),
rustix::process::PidfdFlags::empty(),
)
.unwrap();
Self {
process,
namespace_pid,
namespace,
root: root.to_owned(),
stopped: false,
}
}
fn client(&mut self) -> Command {
assert!(self.process.0.try_wait().unwrap().is_none());
let target = fs::metadata(format!("/proc/{}/root", self.namespace_pid)).unwrap();
let expected = fs::metadata(&self.root).unwrap();
assert_eq!(
(target.dev(), target.ino()),
(expected.dev(), expected.ino())
);
let mut command = Command::new("/usr/bin/nsenter");
command.env_clear().args([
"--target",
&self.namespace_pid.to_string(),
"--all",
"--root",
"--wdns=/",
"--",
]);
command
}
fn stop(&mut self) -> Result<(), String> {
if self.stopped {
return Ok(());
}
// Target the pinned namespace init, not a reusable numeric PID. Its
// termination also kills its namespace descendants; waiting only for
// the outer bwrap process does not establish daemon lock release.
match rustix::process::pidfd_send_signal(&self.namespace, rustix::process::Signal::KILL) {
Ok(()) | Err(rustix::io::Errno::SRCH) => {}
Err(error) => return Err(format!("cannot stop owned namespace: {error}")),
}
let expected = fs::metadata(&self.root).map_err(|error| error.to_string())?;
let lock = fs::File::open(self.root.join("opt/dever/state/deverd.lock"))
.map_err(|error| error.to_string())?;
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
let wrapper_exited = self
.process
.0
.try_wait()
.map_err(|error| error.to_string())?
.is_some();
let namespace_exited = match fs::metadata(format!("/proc/{}/root", self.namespace_pid))
{
Ok(current) => (current.dev(), current.ino()) != (expected.dev(), expected.ino()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => true,
Err(error) => return Err(format!("cannot inspect owned namespace exit: {error}")),
};
if wrapper_exited && namespace_exited {
match fs2::FileExt::try_lock_exclusive(&lock) {
Ok(()) => {
self.stopped = true;
return Ok(());
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
Err(error) => {
return Err(format!("cannot check owned daemon lock release: {error}"));
}
}
}
if std::time::Instant::now() >= deadline {
return Err("owned namespace or daemon lock remained live after 10 seconds".into());
}
std::thread::sleep(Duration::from_millis(20));
}
}
}
impl Drop for IsolatedDaemon {
fn drop(&mut self) {
// Cleanup must not cause a second panic while unwinding a failed
// assertion. Normal stage transitions explicitly check stop().
if let Err(error) = self.stop() {
eprintln!("bootstrap fixture cleanup failed: {error}");
}
}
}
fn add_inputs(author: &Path) {
let launcher = input(
author,
Path::new(env!("CARGO_BIN_EXE_dever-launcher")),
"bootstrap/dever",
);
let daemon = input(
author,
Path::new(env!("CARGO_BIN_EXE_deverd")),
"bootstrap/deverd",
);
let mut gcc = input(
author,
Path::new("/lib/x86_64-linux-gnu/libgcc_s.so.1"),
"bootstrap/libgcc_s.so.1",
);
gcc["path"] = json!("libgcc_s.so.1");
let path = author.join("config/setting.json");
let mut settings: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
settings["bootstrap"] = json!({"launcher":launcher,"daemon":daemon,"libraries":[gcc]});
fs::write(path, serde_json::to_vec(&settings).unwrap()).unwrap();
}
#[test]
#[ignore = "requires optimized author archives and Linux root; installs only into an owned image root"]
fn signed_bootstrap_installs_and_serves_two_projects_without_host_libraries() {
assert_eq!(rustix::process::geteuid().as_raw(), 0);
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let author = TemporaryDirectory::new();
let key = author_inputs(author.path(), &workspace);
add_inputs(author.path());
let downloaded = machine_temporary_directory(&workspace);
let release = downloaded.path().join("release");
let manifest = packaging::create(author.path(), &release).unwrap();
let image = machine_temporary_directory(&workspace);
ecosystems::minimal_os(image.path());
let installer = TemporaryDirectory::new();
let public = installer.path().join("release.pub");
fs::write(
&public,
key.public_key()
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
write(installer.path(), "config/setting.json", serde_json::to_vec(&json!({
"release":release,"system_root":image.path(),"trusted_key":public,"activate_service":false
})).unwrap());
// This author-built installer is the independently trusted first program;
// no executable from an unchecked downloaded directory starts as root.
command(
Command::new(env!("CARGO_BIN_EXE_dever-launcher"))
.arg("--dever-install")
.arg(installer.path()),
);
let layout = Layout::new(image.path().join("opt/dever"));
let sandbox_artifact = manifest
.artifacts
.iter()
.find(|artifact| artifact.path == "sandbox/linux-x86_64/bin/bwrap")
.unwrap();
let sandbox_store = layout.root().join("sandbox");
let sandbox_helper = sandbox_store.join(&sandbox_artifact.sha256).join("bwrap");
assert_eq!(
sha256_file(&sandbox_helper).unwrap(),
sandbox_artifact.sha256
);
assert_eq!(
fs::metadata(&sandbox_helper).unwrap().mode() & 0o7777,
0o755
);
assert_eq!(
fs::read_to_string(image.path().join("etc/apparmor.d/dever-sandbox")).unwrap(),
dever_sandbox::APPARMOR_PROFILE
);
let launcher = layout.bin().join("dever");
let executable = layout.bin().join("deverd");
assert_eq!(
fs::read_link(image.path().join("usr/local/bin/dever")).unwrap(),
Path::new("/opt/dever/bin/dever")
);
assert_eq!(
fs::read_link(
image
.path()
.join("etc/systemd/system/multi-user.target.wants/deverd.service")
)
.unwrap(),
Path::new("../deverd.service")
);
let unit = fs::read_to_string(image.path().join("etc/systemd/system/deverd.service")).unwrap();
assert!(unit.contains("ExecStart=/opt/dever/bin/deverd --root /opt/dever"));
assert!(unit.contains("User=root"));
for file in [
&launcher,
&executable,
&layout.bin().join("lib/libgcc_s.so.1"),
&layout.state().join("trusted-release-key"),
&image.path().join("etc/systemd/system/deverd.service"),
] {
let metadata = fs::metadata(file).unwrap();
assert_eq!(metadata.uid(), 0);
assert_eq!(metadata.mode() & 0o022, 0);
}
let active = fs::read(layout.state().join("active-version")).unwrap();
let launcher_hash = sha256_file(&launcher).unwrap();
// Model an older, correctly signed bootstrap service template. Its exact
// signed bytes establish ownership; only the incoming payload must match
// the current installer's supported service template.
let unit_path = image.path().join("etc/systemd/system/deverd.service");
let current_unit = fs::read_to_string(&unit_path).unwrap();
let old_unit = current_unit.replace("RestartSec=1", "RestartSec=2");
assert_ne!(old_unit, current_unit);
fs::write(layout.bin().join("deverd.service"), &old_unit).unwrap();
let mut old_manifest: Value =
serde_json::from_slice(&fs::read(layout.bin().join("manifest.json")).unwrap()).unwrap();
let service_artifact = old_manifest["artifacts"]
.as_array_mut()
.unwrap()
.iter_mut()
.find(|artifact| artifact["path"] == "bootstrap/deverd.service")
.unwrap();
service_artifact["bytes"] = json!(old_unit.len());
service_artifact["sha256"] = json!(sha256_file(&layout.bin().join("deverd.service")).unwrap());
let encoded = serde_json::to_vec(&old_manifest).unwrap();
fs::write(
layout.bin().join("manifest.sig"),
key.sign(&encoded)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
fs::write(layout.bin().join("manifest.json"), encoded).unwrap();
fs::write(&unit_path, b"unrelated system unit").unwrap();
let rejected = Command::new(env!("CARGO_BIN_EXE_dever-launcher"))
.env_clear()
.arg("--dever-install")
.arg(installer.path())
.output()
.unwrap();
assert!(!rejected.status.success());
assert!(String::from_utf8_lossy(&rejected.stderr).contains("unrelated system service"));
assert_eq!(
fs::read(layout.state().join("active-version")).unwrap(),
active
);
fs::write(&unit_path, old_unit).unwrap();
// Retain a different, correctly signed static helper identity through an
// upgrade. ELF trailing padding keeps this executable's actual behavior.
let mut previous_helper = fs::read(&sandbox_helper).unwrap();
previous_helper.push(0);
let previous_hash = dever_sandbox::validate_bwrap(&previous_helper).unwrap();
let previous_root = sandbox_store.join(&previous_hash);
fs::create_dir(&previous_root).unwrap();
fs::write(previous_root.join("bwrap"), &previous_helper).unwrap();
fs::set_permissions(
previous_root.join("bwrap"),
fs::Permissions::from_mode(0o755),
)
.unwrap();
let mut previous_manifest = manifest.clone();
let previous_artifact = previous_manifest
.artifacts
.iter_mut()
.find(|artifact| artifact.path == sandbox_artifact.path)
.unwrap();
previous_artifact.sha256 = previous_hash.clone();
previous_artifact.bytes = previous_helper.len() as u64;
let previous_encoded = serde_json::to_vec(&previous_manifest).unwrap();
fs::write(
previous_root.join("manifest.sig"),
key.sign(&previous_encoded)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
fs::write(previous_root.join("manifest.json"), previous_encoded).unwrap();
// Retry is supported, but a later damaged package cannot damage the
// published launcher or compiler selection.
command(
Command::new(env!("CARGO_BIN_EXE_dever-launcher"))
.arg("--dever-install")
.arg(installer.path()),
);
assert_eq!(fs::read_to_string(&unit_path).unwrap(), current_unit);
assert_eq!(
sha256_file(&sandbox_helper).unwrap(),
sandbox_artifact.sha256
);
assert_eq!(
sha256_file(&previous_root.join("bwrap")).unwrap(),
previous_hash
);
assert_eq!(fs::read_dir(&sandbox_store).unwrap().count(), 2);
let before_bad_install = fs::read(layout.state().join("active-version")).unwrap();
fs::write(
release.join("bootstrap/lib/libgcc_s.so.1"),
b"changed after signing",
)
.unwrap();
let failed = Command::new(env!("CARGO_BIN_EXE_dever-launcher"))
.env_clear()
.arg("--dever-install")
.arg(installer.path())
.output()
.unwrap();
assert!(!failed.status.success());
assert_eq!(
fs::read(layout.state().join("active-version")).unwrap(),
before_bad_install
);
assert_eq!(sha256_file(&launcher).unwrap(), launcher_hash);
assert!(!active.is_empty());
drop(downloaded);
drop(author);
drop(installer);
for name in ["first", "second"] {
application(&image.path().join(name), "base");
}
let mut daemon = IsolatedDaemon::start(image.path());
for name in ["first", "second"] {
command(
daemon
.client()
.args(["/usr/local/bin/dever", "check", &format!("/{name}")]),
);
assert_response(command(daemon.client().args([
"/usr/local/bin/dever",
"run",
&format!("/{name}"),
"--",
"sample.value.exercise",
"{}",
])));
command(daemon.client().args([
"/usr/local/bin/dever",
"build",
&format!("/{name}"),
"--output",
&format!("/{name}/program"),
]));
}
let status = command(
daemon
.client()
.args(["/usr/local/bin/dever", "cache", "status"]),
);
assert_eq!(
serde_json::from_slice::<Value>(&status.stdout).unwrap()["entries"],
1
);
daemon
.stop()
.expect("isolated daemon and namespace fully stopped");
drop(daemon);
// Namespace-root execution above proves the complete loader closure. These
// clients retain real distinct kernel UIDs, without a single-UID userns
// translating file ownership to overflow IDs.
fs::set_permissions(image.path(), fs::Permissions::from_mode(0o755)).unwrap();
let mut daemon = Daemon(
Command::new(&executable)
.env_clear()
.args(["--root", layout.root().to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.unwrap(),
);
wait_socket(&mut daemon, &layout.service_socket());
for (name, uid) in [("first", 65533), ("second", 65534)] {
let project = image.path().join(name);
chown(&project, Some(uid), Some(uid)).unwrap();
assert_response(command(
Command::new(&launcher)
.uid(uid)
.gid(uid)
.arg("run")
.arg(&project)
.args(["--", "sample.value.exercise", "{}"]),
));
let denied = Command::new(&launcher)
.uid(uid)
.gid(uid)
.env_clear()
.args(["use", manifest.version.as_str()])
.output()
.unwrap();
assert!(!denied.status.success());
assert_eq!(
fs::read(layout.state().join("active-version")).unwrap(),
before_bad_install
);
}
assert_eq!(cache_status(&layout).unwrap().entries, 1);
drop(daemon);
for name in ["first", "second"] {
fs::remove_dir_all(image.path().join(name).join("module")).unwrap();
}
fs::remove_dir_all(layout.root()).unwrap();
for name in ["first", "second"] {
assert_response(command(isolated_os_command(image.path()).args([
&format!("/{name}/program"),
"sample.value.exercise",
"{}",
])));
}
write(&workspace, "target/closure-linux-bootstrap-acceptance.json", serde_json::to_vec_pretty(&json!({
"format":"dever-linux-bootstrap-acceptance-v1","platform":"linux-x86_64",
"bootstrap":manifest.artifacts.iter().filter(|artifact| artifact.path.starts_with("bootstrap/")).collect::<Vec<_>>(),
"checks":{"signed_first_install":true,"stable_public_entry":true,"root_owned_systemd_assets":true,"two_projects_in_minimal_os_root":true,"real_distinct_uids":true,"shared_compile_entries":1,"failed_install_preserves_old_entry":true,"source_and_machine_removed_execution":true},
"global_service_actions":"not_run","public_release":"not_published"
})).unwrap());
}
@@ -0,0 +1,205 @@
//! Owned signed CLI and relocated output; the tool is an ordinary dynamic ELF.
use super::*;
use dever_cli::libs::LockFile;
use dever_cli::packages::LockedPackage;
use dever_cli::toolchain::artifact_put;
use sha2::{Digest, Sha256};
use std::io::{Cursor, Write};
fn project(root: &Path, workspace: &Path) {
write(
root,
"module/media/tool/port.dever",
"execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error\n",
);
write(
root,
"module/media/tool/adapter.dever",
"external command \"bin/tool\" {}\n",
);
write(
root,
"module/media/tool/app.dever",
r#"execute() (passed: Bool) {
input = dever.bytes.from_ints([0, 255, 128, 10])
output = port.execute(["a b", "$(literal);*"], input)
passed = output.code == 23 and output.stdout == input and dever.bytes.to_text(output.stderr) == "ordinary command"
}
"#,
);
write(
root,
"module/media/tool/api.dever",
"cmd execute = app.execute\n",
);
write(root, "config/setting.json", "{}");
let directory = root.join("module/media/tool/bin");
fs::create_dir_all(directory.join("lib")).unwrap();
command(
Command::new("/usr/bin/cc")
.arg("-B/usr/bin/")
.args(["-shared", "-fPIC", "-Wl,-soname,libcommand.so", "-o"])
.arg(directory.join("lib/libcommand.so"))
.arg(workspace.join("test/dever-cli-tests/support/command_library.c")),
);
command(
Command::new("/usr/bin/cc")
.arg("-B/usr/bin/")
.arg(workspace.join("test/dever-cli-tests/support/command_tool.c"))
.arg("-L")
.arg(directory.join("lib"))
.arg("-lcommand")
.arg("-o")
.arg(directory.join("tool")),
);
}
fn packaged_project(root: &Path, source: &Path, layout: &Layout) {
let manifest = br#"{"format":"dever-package-v1","name":"media","version":"1.0.0","dependencies":{},"lib":[]}"#;
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
zip.start_file(
"dever-package.json",
zip::write::SimpleFileOptions::default(),
)
.unwrap();
zip.write_all(manifest).unwrap();
for file in [
"port.dever",
"adapter.dever",
"app.dever",
"api.dever",
"bin/tool",
"bin/lib/libcommand.so",
] {
let name = format!("module/media/tool/{file}");
zip.start_file(&name, zip::write::SimpleFileOptions::default())
.unwrap();
zip.write_all(&fs::read(source.join(name)).unwrap())
.unwrap();
}
let archive = zip.finish().unwrap().into_inner();
let receipt = artifact_put(layout, &archive).unwrap();
fs::create_dir_all(root.join("module")).unwrap();
write(
root,
"config/setting.json",
r#"{"package":{"registry":"https://packages.example.test","use":["media@1.0.0"]}}"#,
);
let mut lock = LockFile::new(Vec::new()).unwrap();
lock.packages.push(LockedPackage {
name: "media".into(),
version: "1.0.0".into(),
sha256: receipt.sha256,
bytes: archive.len() as u64,
dependencies: Vec::new(),
manifest_sha256: format!("{:x}", Sha256::digest(manifest)),
});
lock.write_atomic(root).unwrap();
}
#[test]
#[ignore = "requires explicit current runtime archive, author C/LLVM inputs and owned daemon/sandbox"]
fn signed_cli_runs_and_packages_ordinary_command_with_private_library() {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let author = TemporaryDirectory::new();
let key = author_inputs(author.path(), &workspace);
// This test validates the command boundary, not release-profile trimming.
// Use the explicitly rebuilt archive containing the current wire ABI.
let current = input(
author.path(),
&workspace.join("target/native-runtime-abi/debug/libdever_backend_bridge.a"),
"command-base.a",
);
let settings_path = author.path().join("config/setting.json");
let mut settings: Value = serde_json::from_slice(&fs::read(&settings_path).unwrap()).unwrap();
settings["profiles"]["base"] = current;
fs::write(settings_path, serde_json::to_vec(&settings).unwrap()).unwrap();
let machine = machine_temporary_directory(&workspace);
let layout = Layout::new(machine.path());
layout.initialize().unwrap();
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let release = layout.downloads().join(version.as_str());
packaging::create(author.path(), &release).unwrap();
install_release(&layout, &version, &key);
let compiler = layout.versions().join(version.as_str()).join("dever-core");
let mut daemon = daemon::TestDaemon::start(&layout);
let source = TemporaryDirectory::new();
project(source.path(), &workspace);
command(Command::new(&compiler).arg("check").arg(source.path()));
assert_response(command(
Command::new(&compiler).arg("run").arg(source.path()).args([
"--",
"media.tool.execute",
"{}",
]),
));
assert!(!source.path().join("dever.lock").exists());
let deployment = TemporaryDirectory::new();
let executable = deployment.path().join("program");
command(
Command::new(&compiler)
.arg("build")
.arg(source.path())
.arg("--output")
.arg(&executable),
);
assert_eq!(cache_status(&layout).unwrap().entries, 1);
let package_source = TemporaryDirectory::new();
packaged_project(package_source.path(), source.path(), &layout);
assert_response(command(
Command::new(&compiler)
.arg("run")
.arg(package_source.path())
.args(["--", "media.tool.execute", "{}"]),
));
let package_executable = deployment.path().join("package-program");
command(
Command::new(&compiler)
.arg("build")
.arg(package_source.path())
.arg("--output")
.arg(&package_executable),
);
write(deployment.path(), "config/setting.json", "{}");
daemon.stop();
drop(source);
drop(package_source);
drop(author);
drop(machine);
assert_response(command(
Command::new(&executable)
.args(["media.tool.execute", "{}"])
.current_dir(deployment.path()),
));
assert_response(command(
Command::new(&package_executable)
.args(["media.tool.execute", "{}"])
.current_dir(deployment.path()),
));
let cache = deployment.path().join("data/cache/lib");
let bundle = fs::read_dir(cache).unwrap().next().unwrap().unwrap().path();
let commands = bundle.join("commands");
let tree = fs::read_dir(commands)
.unwrap()
.next()
.unwrap()
.unwrap()
.path();
assert!(tree.join("lib/libcommand.so").is_file());
fs::write(tree.join("lib/libcommand.so"), b"tampered").unwrap();
let capture = deployment.path().join("tamper.stderr");
let status = process::status(
Command::new(&executable)
.env_clear()
.current_dir(deployment.path())
.args(["media.tool.execute", "{}"])
.stdout(Stdio::null())
.stderr(Stdio::from(fs::File::create(&capture).unwrap())),
Duration::from_secs(10),
)
.unwrap();
assert!(!status.success());
assert!(fs::read_to_string(capture).unwrap().contains("resource"));
}
@@ -0,0 +1,291 @@
//! Explicit x86 compiler -> ARM application acceptance; QEMU user mode does
//! not establish the namespace/seccomp contract of external Workers.
use super::*;
use dever_cli::toolchain::BuildTarget;
use dever_cli::toolchain::compilation::{CompileKind, CompileRequest};
use dever_core::source::SourceMap;
use dever_runtime::config::Settings;
use std::io::{Read, Seek, SeekFrom, Write};
#[path = "cross_ecosystems.rs"]
mod ecosystems;
fn add_arm_inputs(author: &Path, workspace: &Path) {
let prepared = workspace.join("target/arm64-cross");
let mut profiles = serde_json::Map::new();
for profile in PROFILES {
let name = format!("{profile}.a");
profiles.insert(
profile.into(),
input(
author,
&prepared.join("archives").join(&name),
&format!("arm/{name}"),
),
);
}
let libc = prepared.join("tools/usr/aarch64-linux-gnu/lib");
let gcc = prepared.join("tools/usr/lib/gcc-cross/aarch64-linux-gnu/13");
let links = |root: &Path, names: &[&str]| -> Vec<Value> {
names
.iter()
.map(|name| {
let mut entry = input(author, &root.join(name), &format!("arm/{name}"));
entry["path"] = json!(name);
entry
})
.collect()
};
let start = [
links(&libc, &["crt1.o", "crti.o"]),
links(&gcc, &["crtbeginT.o"]),
]
.concat();
let libraries = [
links(&libc, &["libc.a", "libm-2.39.a", "libmvec.a"]),
links(&gcc, &["libgcc.a", "libgcc_eh.a"]),
]
.concat();
let end = [links(&gcc, &["crtend.o"]), links(&libc, &["crtn.o"])].concat();
let path = author.join("config/setting.json");
let mut settings: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
settings["native_targets"] = json!({"linux-aarch64": {"profiles": profiles, "start":start, "libraries":libraries, "end":end}});
fs::write(path, serde_json::to_vec(&settings).unwrap()).unwrap();
}
fn build(compiler: &Path, project: &Path, output: &Path, target: BuildTarget) {
command(
Command::new(compiler)
.arg("build")
.arg(project)
.args(["--target", target.platform(), "--output"])
.arg(output),
);
let bytes = fs::read(output).unwrap();
let elf = goblin::elf::Elf::parse(&bytes).unwrap();
assert!(elf.is_64 && elf.little_endian);
assert!(elf.interpreter.is_none() && elf.libraries.is_empty());
assert_eq!(
elf.header.e_machine,
match target {
BuildTarget::LinuxX86_64 => goblin::elf::header::EM_X86_64,
BuildTarget::LinuxAarch64 => goblin::elf::header::EM_AARCH64,
}
);
}
fn request(project: &Path, target: BuildTarget) -> CompileRequest {
let sources = SourceMap::load_with_packages(&project.join("module"), None, &[]).unwrap();
let bindings = Settings::load_project(project)
.unwrap()
.compilation_bindings();
CompileRequest::new(
env!("CARGO_PKG_VERSION").into(),
CompileKind::Application,
&sources,
Some(bindings),
&[],
target,
)
.unwrap()
}
fn private_compiler(
installed: &Path,
destination: &Path,
manifest: &dever_cli::toolchain::ReleaseManifest,
) -> PathBuf {
for artifact in &manifest.artifacts {
if artifact.path != "dever-core"
&& !artifact.path.starts_with("lib/")
&& !artifact.path.starts_with("runtime/linux-aarch64/")
{
continue;
}
let relative = if artifact.path == "dever-core" {
"dever"
} else {
&artifact.path
};
let output = destination.join(relative);
fs::create_dir_all(output.parent().unwrap()).unwrap();
fs::hard_link(installed.join(&artifact.path), output).unwrap();
}
destination.join("dever")
}
fn reject_tampered_cache_hit(layout: &Layout, archive: &Path, request: &CompileRequest) {
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(archive)
.unwrap();
let mut original = [0; 1];
file.read_exact(&mut original).unwrap();
file.seek(SeekFrom::Start(0)).unwrap();
file.write_all(&[original[0] ^ 1]).unwrap();
file.sync_all().unwrap();
let rejected = dever_cli::toolchain::compile(layout, request);
file.seek(SeekFrom::Start(0)).unwrap();
file.write_all(&original).unwrap();
file.sync_all().unwrap();
assert!(rejected.unwrap_err().contains("integrity verification"));
}
fn export_http_application(compiler: &Path, projects: &Path, workspace: &Path) -> Value {
let project = projects.join("http");
for (path, source) in [
(
"user/account/model.dever",
"global type Account { name: Text(1, 32) }",
),
(
"platform/tenant/model.dever",
"global type Tenant { name: Text(1, 32) }",
),
(
"user/account/app.dever",
"type Identity { id: Text\nuser_id: user.account.model.id?\ntenant_id: platform.tenant.model.id? }\nverify(claims: dever.auth.Claims) (identity: Identity) { identity = Identity { id = claims.subject\nuser_id = null\ntenant_id = null } }",
),
(
"sample/http/app.dever",
"hello() (message: Text) { message = \"arm-http\" }",
),
("sample/http/api.dever", "public get hello = app.hello"),
] {
write(&project, &format!("module/{path}"), source);
}
let settings = serde_json::to_vec(&json!({
"http":{"host":"127.0.0.1","port":18080},
"runtime":{"mode":"api","shutdown_ms":2000}, "log":{"level":"error"},
"database":{"default":{"type":"sqlite","path":"data/http.sqlite","max_connections":1}},
"auth":{"providers":{"session":{"verify":"user.account.verify","jwtSecret":"cross-acceptance-only-test-key-2026"}}},
"sites":{"test":{"path":"","auth":"session"}}
})).unwrap();
write(&project, "config/setting.json", &settings);
let program = project.join("application");
build(compiler, &project, &program, BuildTarget::LinuxAarch64);
let exported = workspace.join("target/arm64-cross/guest/apps/http");
write(&exported, "config/setting.json", settings);
fs::copy(&program, exported.join("application")).unwrap();
json!({"sha256":sha256_file(&program).unwrap(), "profile":"sqlite", "url":"http://127.0.0.1:18080/sample/http/hello", "expected_data":"arm-http", "execution":"pending_guest_validation"})
}
#[test]
#[ignore = "requires prepared x86/ARM four-profile runtime archives and private QEMU; starts only an owned daemon"]
fn signed_host_compiler_builds_arm_profiles_and_runs_standalone_under_qemu() {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.unwrap();
let emulator = workspace.join("target/arm64-cross/tools/usr/bin/qemu-aarch64-static");
assert!(
emulator.is_file(),
"prepare the task-owned static QEMU emulator"
);
let author = TemporaryDirectory::new();
let key = author_inputs(author.path(), &workspace);
add_arm_inputs(author.path(), &workspace);
let machine = machine_temporary_directory(&workspace);
let layout = Layout::new(machine.path());
layout.initialize().unwrap();
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let release = layout.downloads().join(version.as_str());
let manifest = packaging::create(author.path(), &release).unwrap();
assert_eq!(manifest.platform, BuildTarget::LinuxX86_64.platform());
install_release(&layout, &version, &key);
fs::remove_dir_all(&release).unwrap();
drop(author);
let compiler = layout.versions().join(version.as_str()).join("dever-core");
let mut daemon = daemon::TestDaemon::start(&layout);
let projects = TemporaryDirectory::new();
let standalone = TemporaryDirectory::new();
let mut programs = Vec::new();
let mut evidence = serde_json::Map::new();
for profile in PROFILES {
let project = projects.path().join(profile);
application(&project, profile);
let destination = standalone.path().join(profile);
fs::create_dir_all(destination.join("config")).unwrap();
fs::copy(
project.join("config/setting.json"),
destination.join("config/setting.json"),
)
.unwrap();
let executable = destination.join("application");
build(&compiler, &project, &executable, BuildTarget::LinuxAarch64);
let before = cache_status(&layout).unwrap().entries;
let repeated = project.join("repeated");
build(&compiler, &project, &repeated, BuildTarget::LinuxAarch64);
assert_eq!(
sha256_file(&executable).unwrap(),
sha256_file(&repeated).unwrap()
);
assert_eq!(cache_status(&layout).unwrap().entries, before);
evidence.insert(profile.into(), json!({"sha256":sha256_file(&executable).unwrap(), "bytes":fs::metadata(&executable).unwrap().len(), "cache_hit":true}));
programs.push((destination, executable));
}
assert_eq!(cache_status(&layout).unwrap().entries, 4);
let base_project = projects.path().join("base");
let host = standalone.path().join("base/host-application");
build(&compiler, &base_project, &host, BuildTarget::LinuxX86_64);
assert_response(command(
Command::new(&host).args(["sample.value.exercise", "{}"]),
));
assert_eq!(cache_status(&layout).unwrap().entries, 5);
let private = private_compiler(
compiler.parent().unwrap(),
&machine.path().join("private"),
&manifest,
);
let private_directory = standalone.path().join("private-base");
write(&private_directory, "config/setting.json", b"{}");
let private_output = private_directory.join("application");
build(
&private,
&base_project,
&private_output,
BuildTarget::LinuxAarch64,
);
programs.push((private_directory, private_output));
// Use the same canonical request to prove integrity is checked before a hit.
let arm_request = request(&base_project, BuildTarget::LinuxAarch64);
assert_eq!(
dever_cli::toolchain::compile(&layout, &arm_request).unwrap(),
fs::read(&programs[0].1).unwrap()
);
let archive = compiler
.parent()
.unwrap()
.join("runtime/linux-aarch64/archives/base.a");
reject_tampered_cache_hit(&layout, &archive, &arm_request);
assert_eq!(cache_status(&layout).unwrap().entries, 5);
let http = export_http_application(&compiler, projects.path(), &workspace);
assert_eq!(cache_status(&layout).unwrap().entries, 6);
daemon.stop();
drop(projects);
drop(machine);
for (directory, executable) in programs {
assert!(!directory.join("module").exists());
assert_response(command(
Command::new(&emulator)
.arg(executable)
.current_dir(directory)
.args(["sample.value.exercise", "{}"]),
));
}
let report = json!({
"format":"dever-cross-application-acceptance-v1", "host":"linux-x86_64", "target":"linux-aarch64",
"programs":evidence, "http":http, "qemu_sha256":sha256_file(&emulator).unwrap(),
"checks":{"signed_install":true,"profiles":4,"private_arm_build":true,"standalone_after_source_and_machine_removal":true,"sqlite_crud":true,"cache_entries":6,"cache_target_isolation":true,"tampered_target_cache_hit_rejected":true},
"postgres_server":"not_run", "worker_sandbox":"requires_arm_kernel_system_emulation"
});
write(
&workspace,
"target/arm64-cross/compile-acceptance.json",
serde_json::to_vec_pretty(&report).unwrap(),
);
}
@@ -0,0 +1,348 @@
//! Produce real standalone ARM Worker applications for full-system acceptance.
use super::*;
use dever_cli::libs::{
self, LibSpec, LockFile, LockedArtifact, LockedLib, LockedWorker, RegistryRuntime,
};
use dever_cli::toolchain::{Artifact, ReleaseManifest};
use dever_core::hir::ExternalWorkerContract;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
const ECOSYSTEMS: [&str; 3] = ["pip", "npm", "go"];
const TARGET: BuildTarget = BuildTarget::LinuxAarch64;
fn digest(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
fn add_target_sandbox(author: &Path, workspace: &Path) {
let root = workspace.join("target/arm64-cross/sandbox");
let mut files = Vec::new();
let mut inputs = Vec::new();
for directory in ["bin", "lib"] {
for entry in fs::read_dir(root.join(directory)).unwrap() {
let entry = entry.unwrap();
assert!(entry.file_type().unwrap().is_file());
let relative = format!("{directory}/{}", entry.file_name().to_str().unwrap());
let bytes = fs::read(entry.path()).unwrap();
let source = format!("inputs/arm-sandbox/{relative}");
write(author, &source, &bytes);
inputs.push(json!({"source":source,"path":relative,"sha256":digest(&bytes)}));
files.push((relative, bytes));
}
}
dever_sandbox::validate_assets_for_target(&files, TARGET.platform()).unwrap();
let path = author.join("config/setting.json");
let mut settings: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
settings["sandbox_targets"] = json!({"linux-aarch64":inputs});
fs::write(path, serde_json::to_vec(&settings).unwrap()).unwrap();
}
// Sign the exact prepared archives: their identities also bind each Lib lock.
// As in the native ecosystem fixture, this is an explicit private test author.
fn sign_target_runtimes(
release: &Path,
workspace: &Path,
key: &Ed25519KeyPair,
ecosystems: &[&str],
) -> BTreeMap<String, RegistryRuntime> {
let mut manifest: ReleaseManifest =
serde_json::from_slice(&fs::read(release.join("manifest.json")).unwrap()).unwrap();
let mut runtimes = BTreeMap::new();
for &ecosystem in ecosystems {
let prepared = workspace
.join("target/arm64-cross/ecosystems")
.join(ecosystem);
let settings: Value =
serde_json::from_slice(&fs::read(prepared.join("config/setting.json")).unwrap())
.unwrap();
let runtime: RegistryRuntime = serde_json::from_value(settings["runtime"].clone()).unwrap();
assert_eq!(runtime.target, TARGET.platform());
let pack = fs::read(prepared.join("runtime.pack")).unwrap();
assert_eq!(digest(&pack), runtime.pack.sha256);
let prefix = format!("runtime/{ecosystem}/{}", TARGET.platform());
write(release, &format!("{prefix}/runtime.pack"), pack);
write(
release,
&format!("{prefix}/manifest.json"),
serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":runtime}))
.unwrap(),
);
for leaf in ["manifest.json", "runtime.pack"] {
let path = format!("{prefix}/{leaf}");
assert!(
!manifest
.artifacts
.iter()
.any(|artifact| artifact.path == path)
);
manifest.artifacts.push(Artifact {
bytes: fs::metadata(release.join(&path)).unwrap().len(),
sha256: sha256_file(&release.join(&path)).unwrap(),
path,
});
}
runtimes.insert(ecosystem.to_owned(), runtime);
}
manifest
.artifacts
.sort_by(|left, right| left.path.cmp(&right.path));
let encoded = serde_json::to_vec(&manifest).unwrap();
fs::write(release.join("manifest.json"), &encoded).unwrap();
fs::write(
release.join("manifest.sig"),
key.sign(&encoded)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
runtimes
}
fn application(root: &Path, ecosystem: &str) -> ExternalWorkerContract {
let (extension, library, source) = match ecosystem {
"pip" => (
"py",
"six@1.17.0",
include_str!("../../ecosystem-release/arm64/probe.py"),
),
"npm" => (
"mjs",
"is-number@7.0.0",
include_str!("../../ecosystem-release/arm64/probe.mjs"),
),
"go" => (
"go",
"github.com/google/uuid@1.6.0",
include_str!("../../ecosystem-release/arm64/probe.go"),
),
_ => unreachable!(),
};
write(root, "config/setting.json", "{}\n");
write(
root,
"module/sample/worker/port.dever",
"probe() (okay: Bool) fails app.ProbeResult\nreject() (okay: Bool) fails app.ProbeResult\n",
);
write(
root,
"module/sample/worker/app.dever",
"type ProbeResult { error Unavailable(message: Text) }\nprobe() (okay: Bool) { okay = port.probe() }\nreject() (okay: Bool) { okay = port.reject() }\n",
);
write(
root,
"module/sample/worker/api.dever",
"cmd probe = app.probe\ncmd reject = app.reject\n",
);
write(
root,
"module/sample/worker/adapter.dever",
format!("external {ecosystem} \"worker/probe.{extension}\" {{ lib \"{library}\" }}\n"),
);
let sources = SourceMap::load(&root.join("module")).unwrap();
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
let contract = program.external_worker_contracts().remove(0);
write(root, &contract.entry, source);
contract
}
fn pure_package_lock(
workspace: &Path,
contract: &ExternalWorkerContract,
runtime: &RegistryRuntime,
) -> (LockFile, BTreeMap<String, Vec<u8>>) {
let spec: LibSpec = contract.libs[0].parse().unwrap();
let (filename, extension) = match contract.ecosystem.as_str() {
"pip" => ("six-1.17.0-py2.py3-none-any.whl", "whl"),
"npm" => ("is-number-7.0.0.tgz", "tgz"),
_ => unreachable!(),
};
let bytes = fs::read(
workspace
.join("target/arm64-cross/dependencies")
.join(filename),
)
.unwrap();
let sha256 = digest(&bytes);
let artifact = LockedArtifact {
target: TARGET.platform().into(),
path: format!(
"lib/{}/{}/{}/archive.{extension}",
contract.ecosystem,
digest(spec.distribution_name().as_bytes()),
spec.version
),
bytes: bytes.len() as u64,
sha256: sha256.clone(),
};
let mut lock = LockFile::new(vec![LockedLib {
spec: spec.clone(),
dependencies: vec![],
runtime: runtime.pack.clone(),
artifacts: vec![artifact],
schema: contract.schema.clone(),
build: None,
}])
.unwrap();
if contract.ecosystem == "npm" {
lock.npm.push(libs::npm::Environment::single(spec));
}
(lock, BTreeMap::from([(sha256, bytes)]))
}
fn go_package_lock(
workspace: &Path,
runtime: &RegistryRuntime,
) -> (LockFile, BTreeMap<String, Vec<u8>>) {
let prepared = workspace.join("target/go-managed/dependency");
let mut lock = LockFile::decode(&fs::read(prepared.join("dever.lock")).unwrap()).unwrap();
libs::doctor(&lock).unwrap();
assert!(lock.builds.is_empty() && lock.workers.is_empty());
assert_eq!(lock.libs.len(), 1);
assert_eq!(lock.libs[0].spec.key(), "go:github.com/google/uuid@1.6.0");
let authenticated =
libs::sumdb::verify_chain(&lock.go_sumdb, &libs::sumdb::Verifier::official()).unwrap();
let mut archives = BTreeMap::new();
for library in &mut lock.libs {
// Go module ZIPs are source-only; keep the verified source and sumdb
// evidence unchanged while selecting the new runtime and build target.
library.runtime = runtime.pack.clone();
for artifact in &mut library.artifacts {
let bytes = fs::read(prepared.join("artifacts").join(&artifact.sha256)).unwrap();
assert_eq!(digest(&bytes), artifact.sha256);
assert_eq!(bytes.len() as u64, artifact.bytes);
authenticated
.verify_zip(&library.spec.name, &library.spec.version, &bytes)
.unwrap();
artifact.target = TARGET.platform().into();
archives.insert(artifact.sha256.clone(), bytes);
}
}
(lock, archives)
}
fn prepare_locked_application(
project: &Path,
workspace: &Path,
layout: &Layout,
contract: ExternalWorkerContract,
runtime: &RegistryRuntime,
) -> LockFile {
let (mut lock, archives) = if contract.ecosystem == "go" {
go_package_lock(workspace, runtime)
} else {
pure_package_lock(workspace, &contract, runtime)
};
for (sha256, bytes) in archives {
let receipt = dever_cli::toolchain::artifact_put(layout, &bytes).unwrap();
assert_eq!(receipt.sha256, sha256);
assert_eq!(receipt.bytes, bytes.len() as u64);
}
lock.workers.push(LockedWorker {
libs: contract
.libs
.iter()
.map(|value| value.parse().unwrap())
.collect(),
port: contract.port,
adapter: contract.adapter,
ecosystem: contract.ecosystem,
runtime: Some(runtime.pack.clone()),
entry: contract.entry,
schema: contract.schema,
capabilities: contract.capabilities,
operations: contract.operations,
});
libs::doctor(&lock).unwrap();
lock.write_atomic(project).unwrap();
lock
}
#[test]
#[ignore = "requires explicit ARM runtimes/dependencies/sandbox and native archives; builds with an owned host daemon and exports apps for ARM system QEMU"]
fn signed_arm_ecosystems_build_offline_standalone_apps_for_system_guest() {
build_guest_apps(&ECOSYSTEMS, "ecosystem-build-acceptance.json");
}
#[test]
#[ignore = "requires explicit ARM inputs; rebuilds only Python for SDK shutdown regression under the ARM guest"]
fn signed_arm_python_worker_rebuilds_with_current_sdk() {
build_guest_apps(&["pip"], "python-build-acceptance.json");
}
fn build_guest_apps(ecosystems: &[&str], report: &str) {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.unwrap();
let destination = workspace.join("target/arm64-cross/guest/apps");
fs::create_dir_all(&destination).unwrap();
for &ecosystem in ecosystems {
assert!(
!destination.join(ecosystem).exists(),
"guest app already exists; preserve or remove the prior owned fixture explicitly"
);
}
let author = TemporaryDirectory::new();
let key = author_inputs(author.path(), &workspace);
add_arm_inputs(author.path(), &workspace);
add_target_sandbox(author.path(), &workspace);
let machine = machine_temporary_directory(&workspace);
let layout = Layout::new(machine.path());
layout.initialize().unwrap();
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let release = layout.downloads().join(version.as_str());
packaging::create(author.path(), &release).unwrap();
let runtimes = sign_target_runtimes(&release, &workspace, &key, ecosystems);
install_release(&layout, &version, &key);
fs::remove_dir_all(&release).unwrap();
drop(author);
let compiler = layout.versions().join(version.as_str()).join("dever-core");
let mut daemon = daemon::TestDaemon::start(&layout);
let projects = TemporaryDirectory::new();
let mut programs = serde_json::Map::new();
for &ecosystem in ecosystems {
let project = projects.path().join(ecosystem);
let contract = application(&project, ecosystem);
let lock = prepare_locked_application(
&project,
&workspace,
&layout,
contract,
&runtimes[ecosystem],
);
command(
Command::new(&compiler)
.args(["lib", "doctor"])
.arg(&project)
.args(["--target", TARGET.platform()]),
);
let output = destination.join(ecosystem);
fs::create_dir(&output).unwrap();
write(&output, "config/setting.json", "{}\n");
let executable = output.join("program");
build(&compiler, &project, &executable, TARGET);
programs.insert(ecosystem.into(), json!({
"path":format!("{ecosystem}/program"), "sha256":sha256_file(&executable).unwrap(),
"bytes":fs::metadata(&executable).unwrap().len(), "runtime":runtimes[ecosystem].pack,
"libs":lock.libs.iter().map(|lib| lib.spec.key()).collect::<Vec<_>>(),
"artifacts":lock.libs.iter().flat_map(|lib| &lib.artifacts).collect::<Vec<_>>()
}));
}
assert_eq!(
cache_status(&layout).unwrap().entries,
ecosystems.len() as u64
);
daemon.stop();
drop(projects);
drop(machine);
write(&workspace, &format!("target/arm64-cross/{report}"), serde_json::to_vec_pretty(&json!({
"format":"dever-cross-ecosystem-build-v1", "target":TARGET.platform(), "programs":programs,
"checks":{"signed_runtime_install":true,"offline_lib_doctor":ecosystems.len(),"actual_cli_arm_build":ecosystems.len(),"source_and_build_machine_removed":true},
"guest_probe":{"arguments":["sample.worker.probe","{}"],"expected_data":true},
"guest_failure":{"arguments":["sample.worker.reject","{}"],"expected":"runtime fault; next fresh probe still succeeds; no remaining Worker"},
"guest_execution":"not_run", "sandbox_execution":"requires full ARM kernel guest"
})).unwrap());
}
@@ -0,0 +1,398 @@
//! Explicit signed-release acceptance with no host language trees in the final root.
use super::*;
use std::os::unix::fs::{MetadataExt, chown};
const ECOSYSTEMS: [&str; 3] = ["pip", "npm", "go"];
const STANDALONE_UID: u32 = 65534;
fn nonroot_standalone(source: &Path, expected_program: &Value) -> Value {
let deployment = TemporaryDirectory::new_in(Path::new("/tmp"));
let program = deployment.path().join("program");
let config = deployment.path().join("config");
let settings = config.join("setting.json");
fs::create_dir(&config).unwrap();
fs::copy(source.join("program"), &program).unwrap();
fs::copy(source.join("config/setting.json"), &settings).unwrap();
let identity = json!({
"bytes": fs::metadata(&program).unwrap().len(),
"sha256": sha256_file(&program).unwrap(),
});
assert_eq!(&identity, expected_program);
for (path, mode) in [
(program.as_path(), 0o700),
(config.as_path(), 0o700),
(settings.as_path(), 0o600),
(deployment.path(), 0o700),
] {
fs::set_permissions(path, fs::Permissions::from_mode(mode)).unwrap();
chown(path, Some(STANDALONE_UID), Some(STANDALONE_UID)).unwrap();
}
// Only the standalone executable and private config cross this boundary.
// The actual non-root process must extract its own embedded Worker bundle.
assert!(!deployment.path().join("data").exists());
let output = command(
Command::new("/usr/bin/setpriv")
.arg(format!("--reuid={STANDALONE_UID}"))
.arg(format!("--regid={STANDALONE_UID}"))
.args([
"--clear-groups",
"--inh-caps=-all",
"--ambient-caps=-all",
"--",
])
.arg(&program)
.args(["sample.worker.probe", "{}"])
.current_dir(deployment.path())
.stdin(Stdio::null()),
);
eprintln!(
"standalone UID {STANDALONE_UID} {}: {}\n{}",
source.display(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_response(output);
assert_eq!(
fs::metadata(deployment.path().join("data/cache/lib"))
.unwrap()
.uid(),
STANDALONE_UID
);
json!({"passed": true, "uid": STANDALONE_UID, "program": identity})
}
fn dependency_fixture(workspace: &Path, ecosystem: &str) -> PathBuf {
workspace.join(match ecosystem {
"pip" => "target/source-build",
"npm" => "target/npm-build",
_ => unreachable!(),
})
}
/// The fixtures were built against these exact pack bytes. The test author
/// signs those inputs, rather than rewriting receipts for a recompressed pack.
fn sign_build_runtimes(
release: &Path,
workspace: &Path,
key: &Ed25519KeyPair,
) -> dever_cli::toolchain::ReleaseManifest {
use dever_cli::libs::{LockFile, RegistryRuntime, RuntimePack};
let mut manifest: dever_cli::toolchain::ReleaseManifest =
serde_json::from_slice(&fs::read(release.join("manifest.json")).unwrap()).unwrap();
for ecosystem in ["pip", "npm"] {
let fixture = dependency_fixture(workspace, ecosystem);
let settings: Value =
serde_json::from_slice(&fs::read(fixture.join("config/setting.json")).unwrap())
.unwrap();
let runtime: RegistryRuntime = serde_json::from_value(settings["runtime"].clone()).unwrap();
assert_eq!(runtime.target, "linux-x86_64");
let pack = fixture.join(settings["runtime_file"].as_str().unwrap());
assert_eq!(sha256_file(&pack).unwrap(), runtime.pack.sha256);
let lock =
LockFile::decode(&fs::read(fixture.join("dependency/dever.lock")).unwrap()).unwrap();
assert!(
!lock.builds.is_empty(),
"acceptance requires actual source build receipts"
);
for receipt in &lock.builds {
assert_eq!(receipt.runtime, runtime.pack);
if let Some(python) = &receipt.auxiliary_runtime {
let python_config: Value = serde_json::from_slice(
&fs::read(dependency_fixture(workspace, "pip").join("config/setting.json"))
.unwrap(),
)
.unwrap();
assert_eq!(
*python,
serde_json::from_value::<RuntimePack>(python_config["runtime"]["pack"].clone())
.unwrap()
);
}
}
let prefix = format!("runtime/{ecosystem}/linux-x86_64");
fs::copy(pack, release.join(&prefix).join("runtime.pack")).unwrap();
fs::write(
release.join(&prefix).join("manifest.json"),
serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":runtime}))
.unwrap(),
)
.unwrap();
for suffix in ["runtime.pack", "manifest.json"] {
let name = format!("{prefix}/{suffix}");
let artifact = manifest
.artifacts
.iter_mut()
.find(|artifact| artifact.path == name)
.unwrap();
artifact.bytes = fs::metadata(release.join(&name)).unwrap().len();
artifact.sha256 = sha256_file(&release.join(&name)).unwrap();
}
}
let encoded = serde_json::to_vec(&manifest).unwrap();
fs::write(release.join("manifest.json"), &encoded).unwrap();
fs::write(
release.join("manifest.sig"),
key.sign(&encoded)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
)
.unwrap();
manifest
}
fn import_built_dependencies(project: &Path, fixture: &Path, layout: &Layout) {
use dever_cli::libs::{LockFile, LockedWorker, build};
let mut lock =
LockFile::decode(&fs::read(fixture.join("dependency/dever.lock")).unwrap()).unwrap();
for artifact in lock
.libs
.iter()
.flat_map(|lib| &lib.artifacts)
.chain(build::npm_outputs(&lock).map(|receipt| &receipt.output))
{
let bytes = fs::read(fixture.join("dependency/artifacts").join(&artifact.sha256)).unwrap();
let stored = dever_cli::toolchain::artifact_put(layout, &bytes).unwrap();
assert_eq!(stored.sha256, artifact.sha256);
assert_eq!(stored.bytes, artifact.bytes);
}
let sources = dever_core::source::SourceMap::load(&project.join("module")).unwrap();
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
let contract = program.external_worker_contracts().remove(0);
let runtime = lock.libs[0].runtime.clone();
assert!(lock.libs.iter().all(|lib| lib.runtime == runtime));
assert!(lock.workers.is_empty());
lock.workers.push(LockedWorker {
port: contract.port,
adapter: contract.adapter,
ecosystem: contract.ecosystem,
runtime: Some(runtime),
entry: contract.entry,
schema: contract.schema,
capabilities: contract.capabilities,
operations: contract.operations,
libs: contract
.libs
.iter()
.map(|spec| spec.parse().unwrap())
.collect(),
});
dever_cli::libs::doctor(&lock).unwrap();
lock.write_atomic(project).unwrap();
}
fn ecosystem_inputs(author: &Path, workspace: &Path) {
let prepared = workspace.join("target/ecosystem-release-inputs/prepared");
let fragment: Value =
serde_json::from_slice(&fs::read(prepared.join("config/setting.json")).unwrap()).unwrap();
for ecosystem in ECOSYSTEMS {
for entry in fragment["runtimes"][ecosystem]["files"].as_array().unwrap() {
let relative = entry["source"].as_str().unwrap();
let output = author.join(relative);
fs::create_dir_all(output.parent().unwrap()).unwrap();
// Author staging only; the maker materializes its own immutable pack.
fs::hard_link(prepared.join(relative), output).unwrap();
}
}
let config = author.join("config/setting.json");
let mut settings: Value = serde_json::from_slice(&fs::read(&config).unwrap()).unwrap();
settings["runtimes"] = fragment["runtimes"].clone();
fs::write(config, serde_json::to_vec(&settings).unwrap()).unwrap();
}
fn application(root: &Path, ecosystem: &str) {
let (extension, source) = match ecosystem {
"pip" => ("py", include_str!("../../ecosystem-release/probe.py")),
"npm" => ("mjs", include_str!("../../ecosystem-release/probe.mjs")),
"go" => ("go", include_str!("../../ecosystem-release/probe.go")),
_ => unreachable!(),
};
write(root, "config/setting.json", "{}\n");
write(
root,
"module/sample/worker/port.dever",
"probe() (okay: Bool) fails app.ProbeResult\n",
);
write(
root,
"module/sample/worker/app.dever",
"type ProbeResult { error Unavailable(message: Text) }\nprobe() (okay: Bool) { okay = port.probe() }\n",
);
write(
root,
"module/sample/worker/api.dever",
"cmd probe = app.probe\n",
);
write(
root,
"module/sample/worker/adapter.dever",
format!(
"external {ecosystem} \"worker/probe.{extension}\" {{\n{}\n}}\n",
match ecosystem {
"pip" => "lib \"simplejson@3.20.1\"",
"npm" => "lib \"bufferutil@4.0.9\"",
"go" => "",
_ => unreachable!(),
}
),
);
let sources = dever_core::source::SourceMap::load(&root.join("module")).unwrap();
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
let entry = &program.external_worker_contracts()[0].entry;
write(root, entry, source);
}
pub(super) fn minimal_os(root: &Path) {
// This root contains only the named Linux OS ABI libraries. No host language
// executable, standard library, PATH, package manager or source tree is visible.
for name in [
"libc.so.6",
"libdl.so.2",
"libm.so.6",
"libpthread.so.0",
"librt.so.1",
"libutil.so.1",
"libresolv.so.2",
"ld-linux-x86-64.so.2",
] {
let source = Path::new("/lib/x86_64-linux-gnu").join(name);
let output = root.join("lib/x86_64-linux-gnu").join(name);
fs::create_dir_all(output.parent().unwrap()).unwrap();
fs::copy(source, output).unwrap();
}
fs::create_dir(root.join("lib64")).unwrap();
fs::copy(
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
root.join("lib64/ld-linux-x86-64.so.2"),
)
.unwrap();
fs::create_dir(root.join("tmp")).unwrap();
fs::create_dir(root.join("proc")).unwrap();
fs::create_dir(root.join("dev")).unwrap();
// The outer namespace supplies only its own proc and synthetic device tree.
assert!(!root.join("usr").exists());
}
#[test]
#[ignore = "requires prepared pinned ecosystem inputs and root for owned namespace; no existing services"]
fn signed_ecosystems_build_and_run_without_system_language_installations() {
assert_eq!(
rustix::process::geteuid().as_raw(),
0,
"explicit no-host acceptance requires the owned root namespace fixture"
);
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let projects = TemporaryDirectory::new();
for ecosystem in ECOSYSTEMS {
application(&projects.path().join(ecosystem), ecosystem);
}
let author = TemporaryDirectory::new();
let key = author_inputs(author.path(), &workspace);
ecosystem_inputs(author.path(), &workspace);
let machine = machine_temporary_directory(&workspace);
let layout = Layout::new(machine.path());
layout.initialize().unwrap();
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let release = layout.downloads().join(version.as_str());
command_with_timeout(
Command::new(workspace.join("target/debug/examples/native-release"))
.arg(author.path())
.arg("--output")
.arg(&release),
// Debug gzip compresses complete language distributions; this is author
// preparation, independent of the service's bounded compile deadline.
Duration::from_secs(900),
);
let signed = sign_build_runtimes(&release, &workspace, &key);
let mut runtimes = serde_json::Map::new();
for ecosystem in ECOSYSTEMS {
let path = format!("runtime/{ecosystem}/linux-x86_64/runtime.pack");
let artifact = signed
.artifacts
.iter()
.find(|artifact| artifact.path == path)
.unwrap();
runtimes.insert(
ecosystem.into(),
json!({"bytes":artifact.bytes,"sha256":artifact.sha256}),
);
}
install_release(&layout, &version, &key);
fs::remove_dir_all(&release).unwrap();
drop(author);
let core = layout.versions().join(version.as_str()).join("dever-core");
let mut daemon = daemon::TestDaemon::start(&layout);
let isolated = TemporaryDirectory::new();
minimal_os(isolated.path());
let mut programs = serde_json::Map::new();
for ecosystem in ECOSYSTEMS {
let project = projects.path().join(ecosystem);
if ecosystem == "go" {
command(Command::new(&core).args(["lib", "add"]).arg(&project));
} else {
import_built_dependencies(
&project,
&dependency_fixture(&workspace, ecosystem),
&layout,
);
}
let lock =
dever_cli::libs::LockFile::decode(&fs::read(project.join("dever.lock")).unwrap())
.unwrap();
assert_eq!(lock.workers.len(), 1);
assert_eq!(lock.libs.is_empty(), ecosystem == "go");
assert_eq!(lock.builds.is_empty(), ecosystem == "go");
assert_eq!(
lock.workers[0].runtime.as_ref().unwrap().sha256,
runtimes[ecosystem]["sha256"].as_str().unwrap()
);
command(Command::new(&core).arg("check").arg(&project));
assert_response(command(
Command::new(&core)
.arg("run")
.arg(&project)
.args(["--", "sample.worker.probe", "{}"]),
));
let destination = isolated.path().join(ecosystem);
fs::create_dir(&destination).unwrap();
write(&destination, "config/setting.json", "{}\n");
let executable = destination.join("program");
command(
Command::new(&core)
.arg("build")
.arg(&project)
.arg("--output")
.arg(&executable),
);
programs.insert(ecosystem.into(), json!({"bytes":fs::metadata(&executable).unwrap().len(),"sha256":sha256_file(&executable).unwrap()}));
}
assert_eq!(cache_status(&layout).unwrap().entries, 3);
daemon.stop();
drop(machine);
drop(projects);
for ecosystem in ECOSYSTEMS {
assert_response(command(
isolated_os_command(isolated.path())
.arg(format!("/{ecosystem}/program"))
.args(["sample.worker.probe", "{}"]),
));
}
let mut standalone_nonroot = serde_json::Map::new();
for ecosystem in ECOSYSTEMS {
standalone_nonroot.insert(
ecosystem.into(),
nonroot_standalone(&isolated.path().join(ecosystem), &programs[ecosystem]),
);
}
write(&workspace, "target/closure-sandbox-ecosystem-release-acceptance.json", serde_json::to_vec_pretty(&json!({
"format":"dever-ecosystem-release-acceptance-v1", "platform":"linux-x86_64",
"runtimes":runtimes,"programs":programs,"standalone_nonroot":standalone_nonroot,
"checks":{"signed_install":true,"locked_runtimes":3,"managed_check_run_build":3,"standalone_isolated_root":3,"standalone_nonroot":3,"shared_compile_entries":3,"python_stdlib_paths_in_bundle":true,"os_sandbox":true,"offline_build_receipt_replay":true},
"os_baseline":"explicit Linux GNU OS libraries and named device nodes inside an owned root",
"runtime_pack_provenance":"test author signed the exact pip/npm packs bound to the completed source builds; maker determinism is verified separately",
"third_party_packages":{"pip":"simplejson@3.20.1 _speedups C extension","npm":"bufferutil@4.0.9 build/Release addon"}, "other_platforms":"not_run", "public_release":"not_published"
})).unwrap());
}
@@ -0,0 +1,429 @@
//! Synthetic runtime closures exercise the production maker without host interpreters.
use std::io::Read;
use super::*;
fn add_file(author: &mut Author, ecosystem: &str, path: &str, bytes: &[u8]) {
let source = format!("inputs/{ecosystem}/{path}");
let file = author.root.join(&source);
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(&file, bytes).unwrap();
author.settings["runtimes"][ecosystem]["files"]
.as_array_mut()
.unwrap()
.push(json!({"source": source, "path": path, "sha256": sha256_file(&file).unwrap()}));
}
fn set_manifest(author: &mut Author, ecosystem: &str, change: impl FnOnce(&mut Value)) {
let input = author.settings["runtimes"][ecosystem]["files"]
.as_array_mut()
.unwrap()
.iter_mut()
.find(|input| input["path"] == "dever-runtime.json")
.unwrap();
let path = author.root.join(input["source"].as_str().unwrap());
let mut manifest: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
change(&mut manifest);
fs::write(&path, serde_json::to_vec(&manifest).unwrap()).unwrap();
input["sha256"] = json!(sha256_file(&path).unwrap());
}
fn runtime_author() -> Author {
let mut author = Author::new();
for (ecosystem, name, version) in [
("pip", "cpython", "3.12.0"),
("npm", "node", "24.0.0"),
("go", "go", "1.25.0"),
] {
author.settings["runtimes"][ecosystem] = json!({"name":name,"version":version,"files":[]});
let mut descriptor = json!({"format":"dever-worker-runtime-v1","ecosystem":ecosystem,"target":platform_identity()});
if ecosystem == "go" {
let arch = if std::env::consts::ARCH == "x86_64" {
"amd64"
} else {
"arm64"
};
descriptor["build"] = json!({"host":platform_identity(),"compiler":"bin/compile","linker":"bin/link","analyzer":"bin/analyze","stdlib_importcfg":"stdlib/importcfg","goos":"linux","goarch":arch,"go_version":format!("go version go1.25.0 linux/{arch}")});
for path in ["bin/compile", "bin/link", "bin/analyze"] {
add_file(
&mut author,
ecosystem,
path,
&native_elf::static_executable(),
);
}
let mut importcfg = String::new();
for name in ["runtime", "context", "encoding/json"] {
let path = format!("stdlib/{name}.a");
let object = format!("go object linux {arch} go1.25.0 fixture\n");
let archive = format!(
"!<arch>\n{:<16}{:<12}{:<6}{:<6}{:<8}{:<10}`\n{object}",
"__.PKGDEF",
0,
0,
0,
"644",
object.len()
);
add_file(&mut author, ecosystem, &path, archive.as_bytes());
importcfg.push_str(&format!("packagefile {name}={path}\n"));
}
add_file(
&mut author,
ecosystem,
"stdlib/importcfg",
importcfg.as_bytes(),
);
} else {
descriptor["executable"] = json!(format!("bin/{name}"));
descriptor["arguments"] = if ecosystem == "pip" {
json!(["-I", "-S", "-B"])
} else {
json!([])
};
add_file(
&mut author,
ecosystem,
&format!("bin/{name}"),
&native_elf::static_executable(),
);
}
add_file(
&mut author,
ecosystem,
"dever-runtime.json",
&serde_json::to_vec(&descriptor).unwrap(),
);
}
author.settings["runtimes"]["pip"]["python_markers"] = json!({
"implementation_name":"cpython","implementation_version":"3.12.0","os_name":"posix",
"platform_machine":std::env::consts::ARCH,"platform_python_implementation":"CPython",
"platform_release":"","platform_system":"Linux","platform_version":"",
"python_full_version":"3.12.0","python_version":"3.12","sys_platform":"linux"
});
author.settings["runtimes"]["pip"]["python_wheel_tags"] = json!(["py3-none-any"]);
let markers = author.settings["runtimes"]["pip"]["python_markers"].clone();
set_manifest(&mut author, "pip", |manifest| {
manifest["python_markers"] = markers;
manifest["python_wheel_tags"] = json!(["py3-none-any"]);
manifest["python_extension_suffixes"] = json!([".abi3.so", ".so"]);
});
author
}
#[test]
fn ecosystem_archives_are_deterministic_and_signed_with_computed_identities() {
let mut author = runtime_author();
author.save();
let first = author.output("first");
let manifest = packaging::create(&author.root, &first).unwrap();
// Input order and filesystem mode are not archive metadata.
for ecosystem in ["pip", "npm", "go"] {
author.settings["runtimes"][ecosystem]["files"]
.as_array_mut()
.unwrap()
.reverse();
}
fs::set_permissions(
author.root.join("inputs/npm/bin/node"),
fs::Permissions::from_mode(0o700),
)
.unwrap();
author.save();
let second = author.output("second");
assert_eq!(packaging::create(&author.root, &second).unwrap(), manifest);
assert_eq!(contents(&first), contents(&second));
assert_signed(&author, &first);
for ecosystem in ["pip", "npm", "go"] {
let prefix = format!("runtime/{ecosystem}/{}", platform_identity());
let metadata_path = format!("{prefix}/manifest.json");
let pack_path = format!("{prefix}/runtime.pack");
let metadata: Value =
serde_json::from_slice(&fs::read(first.join(&metadata_path)).unwrap()).unwrap();
assert_eq!(metadata["format"], "dever-registry-runtime-v1");
assert_eq!(
metadata["runtime"]["pack"]["sha256"],
sha256_file(&first.join(&pack_path)).unwrap()
);
for path in [&metadata_path, &pack_path] {
let signed = manifest
.artifacts
.iter()
.find(|artifact| &artifact.path == path)
.unwrap();
assert_eq!(signed.sha256, sha256_file(&first.join(path)).unwrap());
}
let bytes = fs::read(first.join(&pack_path)).unwrap();
let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(bytes.as_slice()));
let mut names = Vec::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap().to_str().unwrap().to_owned();
assert_eq!(entry.header().mtime().unwrap(), 0);
assert_eq!(entry.header().uid().unwrap(), 0);
assert_eq!(entry.header().gid().unwrap(), 0);
assert_eq!(
entry.header().mode().unwrap(),
if path.starts_with("bin/") {
0o755
} else {
0o644
}
);
let mut actual = Vec::new();
entry.read_to_end(&mut actual).unwrap();
assert_eq!(
actual,
fs::read(author.root.join(format!("inputs/{ecosystem}/{path}"))).unwrap()
);
names.push(path);
}
assert!(names.windows(2).all(|pair| pair[0] < pair[1]));
}
let before = contents(&first);
fs::write(author.root.join("inputs/npm/bin/node"), b"changed").unwrap();
assert_eq!(contents(&first), before);
}
#[test]
fn rejects_incomplete_or_mismatched_runtime_descriptors() {
for ecosystem in ["pip", "npm", "go"] {
for field in ["format", "ecosystem", "target"] {
let mut author = runtime_author();
set_manifest(&mut author, ecosystem, |manifest| {
manifest[field] = json!("wrong")
});
assert!(author.reject().contains("identity"));
}
let mut author = runtime_author();
set_manifest(&mut author, ecosystem, |manifest| {
manifest["unknown"] = json!(true)
});
assert!(author.reject().contains("manifest"));
let mut author = runtime_author();
author.settings["runtimes"][ecosystem]["files"]
.as_array_mut()
.unwrap()
.retain(|input| input["path"] != "dever-runtime.json");
assert!(author.reject().contains("dever-runtime.json"));
}
for (ecosystem, field, value) in [
("pip", "arguments", json!(["-I", "-B"])),
("npm", "executable", json!("bin/missing")),
("npm", "arguments", json!(["source.js"])),
("go", "executable", json!("bin/compile")),
("go", "build", Value::Null),
] {
let mut author = runtime_author();
set_manifest(&mut author, ecosystem, |manifest| manifest[field] = value);
author.reject();
}
for (field, value) in [
("goarch", "unknown"),
("compiler", "bin/missing"),
("stdlib_importcfg", "missing"),
] {
let mut author = runtime_author();
set_manifest(&mut author, "go", |manifest| {
manifest["build"][field] = json!(value)
});
author.reject();
}
let mut author = runtime_author();
author.settings["runtimes"]["go"]["files"]
.as_array_mut()
.unwrap()
.retain(|input| input["path"] != "stdlib/context.a");
assert!(author.reject().contains("archive"));
}
#[test]
fn go_cross_pack_binds_host_tools_and_arm_standard_library_separately() {
for wrong_stdlib in [false, true] {
let mut author = runtime_author();
set_manifest(&mut author, "go", |manifest| {
manifest["target"] = json!("linux-aarch64");
manifest["build"]["goarch"] = json!("arm64");
});
for input in author.settings["runtimes"]["go"]["files"]
.as_array_mut()
.unwrap()
{
if !input["path"].as_str().unwrap().ends_with(".a") {
continue;
}
let path = author.root.join(input["source"].as_str().unwrap());
let architecture = if wrong_stdlib { "amd64" } else { "arm64" };
let object = format!("go object linux {architecture} go1.25.0 fixture\n");
let archive = format!(
"!<arch>\n{:<16}{:<12}{:<6}{:<6}{:<8}{:<10}`\n{object}",
"__.PKGDEF",
0,
0,
0,
"644",
object.len()
);
fs::write(&path, archive).unwrap();
input["sha256"] = json!(sha256_file(&path).unwrap());
}
let input = author.settings["runtimes"]
.as_object_mut()
.unwrap()
.remove("go")
.unwrap();
author.settings["ecosystem_targets"] = json!({"linux-aarch64":{"go":input}});
author.save();
let result = packaging::create(&author.root, &author.output("cross-go"));
if wrong_stdlib {
assert!(
result
.unwrap_err()
.contains("differs from its target or compiler version")
);
} else {
let manifest = result.unwrap();
assert!(
manifest
.artifacts
.iter()
.any(|file| file.path == "runtime/go/linux-aarch64/runtime.pack")
);
}
}
}
#[test]
fn sandbox_cross_assets_preserve_loader_mode_and_validate_deployment_target() {
let mut files = vec![
("bin/bwrap".into(), native_elf::static_executable()),
("bin/guard".into(), native_elf::static_executable()),
(
"lib/ld-linux-aarch64.so.1".into(),
native_elf::library("ld-linux-aarch64.so.1", &[]),
),
(
"lib/libc.so.6".into(),
native_elf::library("libc.so.6", &[]),
),
];
for (_, bytes) in &mut files {
bytes[18..20].copy_from_slice(&183u16.to_le_bytes());
}
dever_sandbox::validate_assets_for_target(&files, "linux-aarch64").unwrap();
let mut author = Author::new();
let inputs = files
.iter()
.map(|(path, bytes)| {
let source = format!("inputs/arm-sandbox/{path}");
let file = author.root.join(&source);
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(&file, bytes).unwrap();
fs::set_permissions(&file, fs::Permissions::from_mode(0o600)).unwrap();
json!({"source":source,"path":path,"sha256":sha256_file(&file).unwrap()})
})
.collect::<Vec<_>>();
author.settings["sandbox_targets"] = json!({"linux-aarch64":inputs});
author.save();
let output = author.output("cross-sandbox");
packaging::create(&author.root, &output).unwrap();
for (path, mode) in [
("bin/bwrap", 0o755),
("bin/guard", 0o755),
("lib/ld-linux-aarch64.so.1", 0o755),
("lib/libc.so.6", 0o644),
] {
assert_eq!(
fs::metadata(output.join("sandbox/linux-aarch64").join(path))
.unwrap()
.permissions()
.mode()
& 0o777,
mode,
"incorrect packaged mode for {path}"
);
}
files[1].1[18..20].copy_from_slice(&62u16.to_le_bytes());
assert!(
dever_sandbox::validate_assets_for_target(&files, "linux-aarch64")
.unwrap_err()
.contains("incompatible target")
);
}
#[test]
fn rejects_invalid_registry_metadata_and_unknown_ecosystems() {
for (ecosystem, field, value) in [
("pip", "python_markers", Value::Null),
("pip", "python_wheel_tags", json!([])),
("pip", "python_wheel_tags", json!(["invalid"])),
(
"pip",
"python_wheel_tags",
json!(["py3-none-any", "py3-none-any"]),
),
("npm", "python_wheel_tags", json!(["py3-none-any"])),
("npm", "name", json!("")),
("npm", "name", json!("../node")),
("go", "version", json!("latest")),
("go", "unknown", json!(true)),
] {
let mut author = runtime_author();
author.settings["runtimes"][ecosystem][field] = value;
author.reject();
}
let mut author = runtime_author();
author.settings["runtimes"]["npm"]["python_markers"] =
author.settings["runtimes"]["pip"]["python_markers"].clone();
author.reject();
let mut author = runtime_author();
author.settings["runtimes"]["exec"] = author.settings["runtimes"]["npm"].clone();
author.reject();
let author = runtime_author();
author.save();
let path = author.root.join("config/setting.json");
let config = fs::read_to_string(&path).unwrap();
let duplicate = config.replacen("\"runtimes\":{", "\"runtimes\":{\"npm\":null,", 1);
assert_ne!(duplicate, config);
fs::write(path, duplicate).unwrap();
assert!(
packaging::create(&author.root, &author.output("duplicate"))
.unwrap_err()
.contains("invalid author configuration")
);
assert_eq!(fs::read_dir(author.directory.path()).unwrap().count(), 1);
}
#[test]
fn rejects_runtime_path_conflicts_drift_and_private_key_aliases() {
for path in [
"../outside",
"bin",
"bin/node/child",
"runtime.pack",
"bin/node",
] {
let mut author = runtime_author();
let mut input = author.settings["runtimes"]["npm"]["files"][0].clone();
input["path"] = json!(path);
author.settings["runtimes"]["npm"]["files"]
.as_array_mut()
.unwrap()
.push(input);
author.reject();
}
let author = runtime_author();
fs::write(author.root.join("inputs/npm/bin/node"), b"changed").unwrap();
assert!(author.reject().contains("SHA-256"));
let mut author = self::runtime_author();
let alias = author.root.join("inputs/key-alias");
fs::hard_link(author.root.join("signing.pk8"), &alias).unwrap();
author.settings["runtimes"]["npm"]["files"].as_array_mut().unwrap().push(json!({"source":"inputs/key-alias","path":"secrets/key","sha256":sha256_file(&alias).unwrap()}));
assert!(author.reject().contains("cannot be included"));
let mut author = self::runtime_author();
symlink("node", author.root.join("inputs/npm/bin/link")).unwrap();
author.settings["runtimes"]["npm"]["files"][0]["source"] = json!("inputs/npm/bin/link");
author.reject();
}
@@ -0,0 +1,178 @@
use super::*;
use dever_cli::toolchain::runtime_pack::RuntimePack;
use dever_runtime::config::RuntimeProfile;
#[cfg(target_arch = "x86_64")]
fn cross_author() -> Author {
let mut author = Author::new();
let target = BuildTarget::LinuxAarch64;
let input = |name: &str, bytes: Vec<u8>| {
let source = format!("inputs/arm-{name}");
fs::write(author.root.join(&source), bytes).unwrap();
json!({"source":source, "sha256":sha256_file(&author.root.join(source)).unwrap()})
};
let mut profiles = json!({});
for profile in ["base", "sqlite", "postgres", "both"] {
profiles[profile] = input(profile, native_objects::archive(target, profile));
}
let link = |name: &str, bytes| {
let mut artifact = input(name, bytes);
artifact["path"] = json!(name);
artifact
};
author.settings["native_targets"] = json!({"linux-aarch64": {
"profiles":profiles,
"start":[link("start.o", native_objects::object(target))],
"libraries":[link("system.a", native_objects::archive(target, "system.o"))],
"end":[link("end.o", native_objects::object(target))],
}});
author
}
#[test]
fn target_names_and_serde_are_exact() {
for (name, target, triple) in [
(
"linux-x86_64",
BuildTarget::LinuxX86_64,
"x86_64-unknown-linux-gnu",
),
(
"linux-aarch64",
BuildTarget::LinuxAarch64,
"aarch64-unknown-linux-gnu",
),
] {
assert_eq!(name.parse::<BuildTarget>().unwrap(), target);
assert_eq!(target.platform(), name);
assert_eq!(target.triple(), triple);
assert_eq!(
serde_json::to_string(&target).unwrap(),
format!("\"{name}\"")
);
}
for invalid in [
"",
"arm64",
"Linux-aarch64",
"linux-arm64",
"aarch64-unknown-linux-gnu",
"linux-aarch64 ",
] {
assert!(invalid.parse::<BuildTarget>().is_err());
assert!(serde_json::from_value::<BuildTarget>(json!(invalid)).is_err());
}
}
#[test]
#[cfg(target_arch = "x86_64")]
fn host_core_packages_both_native_targets_with_separate_profiles_and_signed_closures() {
let author = cross_author();
author.save();
let output = author.directory.path().join("cross-release");
let manifest = packaging::create(&author.root, &output).unwrap();
assert_eq!(manifest.platform, "linux-x86_64");
assert_signed(&author, &output);
for target in [BuildTarget::LinuxX86_64, BuildTarget::LinuxAarch64] {
let closure = runtime_pack::validate(&output, env!("CARGO_PKG_VERSION"), target).unwrap();
for input in closure {
assert!(
manifest
.artifacts
.iter()
.any(|artifact| artifact.path == input.path
&& artifact.sha256 == input.sha256
&& artifact.bytes == input.bytes)
);
}
for profile in [
RuntimeProfile {
sqlite: false,
postgres: false,
},
RuntimeProfile {
sqlite: true,
postgres: false,
},
RuntimeProfile {
sqlite: false,
postgres: true,
},
RuntimeProfile {
sqlite: true,
postgres: true,
},
] {
assert_eq!(
RuntimePack::load(&output, profile, target).unwrap().target,
target
);
}
}
let profile = RuntimeProfile {
sqlite: false,
postgres: false,
};
let arm = RuntimePack::load(&output, profile, BuildTarget::LinuxAarch64).unwrap();
let host = RuntimePack::load(&output, profile, BuildTarget::LinuxX86_64).unwrap();
assert_ne!(arm.identity, host.identity);
fs::write(
output.join("runtime/linux-aarch64/archives/base.a"),
b"tampered",
)
.unwrap();
assert!(RuntimePack::load(&output, profile, BuildTarget::LinuxAarch64).is_err());
RuntimePack::load(&output, profile, BuildTarget::LinuxX86_64).unwrap();
}
#[test]
#[cfg(target_arch = "x86_64")]
fn target_packs_reject_wrong_architecture_and_duplicate_host_declarations() {
for (slot, source) in [
("/native_targets/linux-aarch64/start/0", "start.o"),
("/native_targets/linux-aarch64/profiles/base", "base"),
] {
let mut author = cross_author();
let replacement = if source.ends_with(".o") {
native_objects::object(BuildTarget::LinuxX86_64)
} else {
native_objects::archive(BuildTarget::LinuxX86_64, "wrong.o")
};
fs::write(
author.root.join(format!("inputs/arm-{source}")),
replacement,
)
.unwrap();
author.settings.pointer_mut(slot).unwrap()["sha256"] =
json!(sha256_file(&author.root.join(format!("inputs/arm-{source}"))).unwrap());
assert!(
author
.reject()
.contains("relocatable ELF object for linux-aarch64")
);
}
let mut author = cross_author();
author.settings["native_targets"]["linux-x86_64"] =
author.settings["native_targets"]["linux-aarch64"].clone();
assert!(author.reject().contains("duplicate release destination"));
}
#[test]
#[cfg(target_arch = "x86_64")]
fn missing_target_pack_never_uses_host_inputs() {
let author = Author::new();
author.save();
let output = author.directory.path().join("host-release");
packaging::create(&author.root, &output).unwrap();
let error = RuntimePack::load(
&output,
RuntimeProfile {
sqlite: false,
postgres: false,
},
BuildTarget::LinuxAarch64,
)
.err()
.unwrap();
assert!(error.contains("linux-aarch64"), "{error}");
}
+142
View File
@@ -0,0 +1,142 @@
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use dever_core::source::SourceMap;
static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0);
struct Workspace(PathBuf);
impl Workspace {
fn new() -> Self {
let sequence = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("dever-new-test-{}-{sequence}", std::process::id()));
fs::create_dir(&path).unwrap();
Self(path)
}
}
impl Drop for Workspace {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn both_formats_create_checked_formatted_projects_with_real_application_tests() {
let workspace = Workspace::new();
for markdown in [false, true] {
let root = workspace
.0
.join(if markdown { "markdown" } else { "plain" });
dever_cli::project::create(&root, markdown).unwrap();
let sources = SourceMap::load_project(&root.join("module"), &root.join("test")).unwrap();
let result = dever_core::check(&sources);
assert!(result.is_ok(), "{result:?}");
assert_eq!(sources.files().len(), 3);
for source in sources.files() {
let formatted = dever_core::format::format(source).unwrap();
assert_eq!(formatted, source.text(), "{}", source.path().display());
}
let settings: serde_json::Value =
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
assert_eq!(settings["log"]["level"], "info");
assert!(root.join("AGENTS.md").is_file());
assert!(!root.join("module/main.dever").exists());
}
}
#[test]
fn existing_directories_files_and_symlinks_are_preserved() {
let workspace = Workspace::new();
let directory = workspace.0.join("existing");
fs::create_dir(&directory).unwrap();
assert!(dever_cli::project::create(&directory, false).is_err());
assert_eq!(fs::read_dir(&directory).unwrap().count(), 0);
let file = workspace.0.join("file");
fs::write(&file, "keep me").unwrap();
assert!(dever_cli::project::create(&file, false).is_err());
assert_eq!(fs::read_to_string(&file).unwrap(), "keep me");
#[cfg(unix)]
{
let link = workspace.0.join("link");
std::os::unix::fs::symlink(workspace.0.join("absent"), &link).unwrap();
assert!(dever_cli::project::create(&link, true).is_err());
assert!(fs::symlink_metadata(link).unwrap().is_symlink());
}
assert!(fs::read_dir(&workspace.0).unwrap().all(|entry| {
!entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with(".dever-new-")
}));
}
#[test]
fn concurrent_creation_publishes_one_complete_project_without_staging_leftovers() {
let workspace = Workspace::new();
let root = workspace.0.join("project");
let barrier = std::sync::Barrier::new(2);
let results = std::thread::scope(|scope| {
let workers: Vec<_> = [false, true]
.into_iter()
.map(|markdown| {
let root = &root;
let barrier = &barrier;
scope.spawn(move || {
barrier.wait();
dever_cli::project::create(root, markdown)
})
})
.collect();
workers
.into_iter()
.map(|worker| worker.join().unwrap())
.collect::<Vec<_>>()
});
assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
let sources = SourceMap::load_project(&root.join("module"), &root.join("test")).unwrap();
assert!(dever_core::check(&sources).is_ok());
assert_eq!(sources.files().len(), 3);
assert_eq!(fs::read_dir(&workspace.0).unwrap().count(), 1);
}
#[test]
fn command_creates_before_reading_project_configuration_and_rejects_bad_arguments() {
let workspace = Workspace::new();
let output = Command::new(env!("CARGO_BIN_EXE_dever"))
.env_clear()
.current_dir(&workspace.0)
.args(["new", "app", "--markdown"])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
workspace
.0
.join("app/module/hello/greeting/api.dever.md")
.is_file()
);
for arguments in [
vec!["new"],
vec!["new", "bad", "--unknown"],
vec!["new", "--markdown"],
] {
let output = Command::new(env!("CARGO_BIN_EXE_dever"))
.env_clear()
.current_dir(&workspace.0)
.args(arguments)
.output()
.unwrap();
assert!(!output.status.success());
}
assert!(!workspace.0.join("bad").exists());
}
+961
View File
@@ -0,0 +1,961 @@
use std::collections::BTreeMap;
use std::fs;
#[cfg(unix)]
use std::io::Read;
use std::io::{Cursor, Write};
#[cfg(unix)]
use std::path::Path;
#[cfg(unix)]
use std::process::Command;
use dever_cli::libs::{ArtifactStore, FixtureArtifactStore, LockFile};
#[cfg(unix)]
use dever_cli::libs::{LockedWorker, RuntimePack};
#[cfg(unix)]
use dever_cli::packages::LockedPackage;
use dever_cli::packages::{
PackageTransport, execute_with, owned_files_with, owned_worker_bytes_with, owns_worker,
sources_with,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use dever_cli::toolchain::cache_status;
#[cfg(unix)]
use dever_cli::toolchain::{
Artifact, Layout, MachineManager, ReleaseManifest, Version, artifact_get, artifact_put,
};
#[cfg(unix)]
use dever_cli::workers;
#[cfg(unix)]
use dever_core::native::EmbeddedResource;
#[cfg(unix)]
use ring::rand::SystemRandom;
#[cfg(unix)]
use ring::signature::{Ed25519KeyPair, KeyPair};
use sha2::{Digest, Sha256};
#[path = "../dever-tests/tests/support/temp.rs"]
mod temp;
#[cfg(unix)]
#[path = "support/daemon.rs"]
mod daemon;
#[cfg(unix)]
use daemon::TestDaemon;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "support/llvm_pack.rs"]
mod llvm_pack;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "packages/managed_compile.rs"]
mod managed_compile;
use temp::TemporaryDirectory;
struct Registry(BTreeMap<String, Vec<u8>>);
impl PackageTransport for Registry {
fn get(&self, path: &str, limit: usize) -> Result<Vec<u8>, String> {
let bytes = self
.0
.get(path)
.ok_or_else(|| format!("missing registry path {path}"))?;
if bytes.len() > limit {
return Err("registry response exceeds limit".into());
}
Ok(bytes.clone())
}
}
fn archive(
name: &str,
version: &str,
dependencies: serde_json::Value,
libs: &[&str],
source: &str,
) -> Vec<u8> {
archive_with_sources(name, version, dependencies, libs, source, &[])
}
fn archive_with_sources(
name: &str,
version: &str,
dependencies: serde_json::Value,
libs: &[&str],
source: &str,
additional_sources: &[(&str, &str)],
) -> Vec<u8> {
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
for (path, bytes) in [
(
"dever-package.json".to_owned(),
serde_json::to_vec(&serde_json::json!({
"format": "dever-package-v1", "name": name, "version": version,
"dependencies": dependencies, "lib": libs,
}))
.unwrap(),
),
(
format!("module/{name}/value/app.dever"),
source.as_bytes().to_vec(),
),
] {
writer
.start_file(path, zip::write::SimpleFileOptions::default())
.unwrap();
writer.write_all(&bytes).unwrap();
}
for (path, source) in additional_sources {
writer
.start_file(*path, zip::write::SimpleFileOptions::default())
.unwrap();
writer.write_all(source.as_bytes()).unwrap();
}
writer.finish().unwrap().into_inner()
}
fn digest(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
fn registry(archives: &[(&str, &str, Vec<u8>)]) -> Registry {
let mut paths = BTreeMap::new();
let mut versions = BTreeMap::<String, Vec<serde_json::Value>>::new();
for (name, version, bytes) in archives {
versions
.entry((*name).into())
.or_default()
.push(serde_json::json!({
"version": version, "sha256": digest(bytes), "bytes": bytes.len(),
}));
paths.insert(format!("/v1/packages/{name}/{version}.zip"), bytes.clone());
}
for (name, versions) in versions {
paths.insert(
format!("/v1/packages/{name}/index.json"),
serde_json::to_vec(&serde_json::json!({
"format": "dever-package-index-v1", "name": name, "versions": versions,
}))
.unwrap(),
);
}
Registry(paths)
}
fn project() -> TemporaryDirectory {
let root = TemporaryDirectory::new();
fs::create_dir_all(root.path().join("config")).unwrap();
fs::create_dir_all(root.path().join("module/portal/value")).unwrap();
fs::write(
root.path().join("config/setting.json"),
r#"{"package":{"registry":"https://packages.example.test","use":[]}}"#,
)
.unwrap();
fs::write(
root.path().join("module/portal/value/app.dever"),
"value() (result: Text) { result = storefront.value.value() }\n",
)
.unwrap();
fs::write(
root.path().join("module/portal/value/api.dever"),
"cmd show = app.value\n",
)
.unwrap();
fs::create_dir_all(root.path().join("test/portal/value")).unwrap();
fs::write(
root.path().join("test/portal/value/smoke.dever"),
"smoke() () { assert_eq(portal.value.value(), \"two\") }\n",
)
.unwrap();
root
}
#[test]
fn package_add_resolves_transitive_versions_and_feeds_the_common_lib_lock() {
let root = project();
let registry = registry(&[
(
"storefront",
"1.0.0",
archive(
"storefront",
"1.0.0",
serde_json::json!({"catalog":"^1.0.0"}),
&[],
"value() (result: Text) { result = catalog.value.value() }\n",
),
),
(
"catalog",
"1.0.0",
archive(
"catalog",
"1.0.0",
serde_json::json!({}),
&["pip:fixture@1.0.0"],
"value() (result: Text) { result = \"one\" }\n",
),
),
(
"catalog",
"1.2.0",
archive(
"catalog",
"1.2.0",
serde_json::json!({}),
&["pip:fixture@1.0.0"],
"value() (result: Text) { result = \"two\" }\n",
),
),
(
"catalog",
"2.0.0",
archive(
"catalog",
"2.0.0",
serde_json::json!({}),
&[],
"value() (result: Text) { result = \"wrong\" }\n",
),
),
]);
let store = FixtureArtifactStore::default();
execute_with(
"add",
root.path(),
&["storefront@^1.0.0".into()],
&registry,
&store,
)
.unwrap();
let lock = LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
assert_eq!(
lock.packages
.iter()
.map(|package| (package.name.as_str(), package.version.as_str()))
.collect::<Vec<_>>(),
[("catalog", "1.2.0"), ("storefront", "1.0.0")]
);
assert_eq!(lock.libs.len(), 1);
assert_eq!(lock.libs[0].spec.key(), "pip:fixture@1.0.0");
assert!(owns_worker(root.path(), "worker/catalog/tool").unwrap());
assert!(owns_worker(root.path(), "module/catalog/value/worker/main.py").unwrap());
assert!(!owns_worker(root.path(), "module/portal/value/worker/main.py").unwrap());
let files = owned_files_with(root.path(), &store).unwrap();
assert!(String::from_utf8_lossy(&files["module/catalog/value/app.dever"]).contains("two"));
assert!(execute_with("doctor", root.path(), &[], &registry, &store).is_ok());
let mut damaged = lock.clone();
damaged.packages[0].manifest_sha256 = "0".repeat(64);
damaged.write_atomic(root.path()).unwrap();
assert!(
owned_files_with(root.path(), &store)
.unwrap_err()
.contains("manifest changed")
);
}
#[test]
fn package_resolution_rejects_conflicts_without_changing_project_configuration() {
let root = project();
let before = fs::read(root.path().join("config/setting.json")).unwrap();
let registry = registry(&[
(
"storefront",
"1.0.0",
archive(
"storefront",
"1.0.0",
serde_json::json!({"catalog":"^2.0.0"}),
&[],
"value() (result: Text) { result = catalog.value.value() }\n",
),
),
(
"catalog",
"1.0.0",
archive(
"catalog",
"1.0.0",
serde_json::json!({}),
&[],
"value() (result: Text) { result = \"one\" }\n",
),
),
]);
let store = FixtureArtifactStore::default();
let error = execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&store,
)
.unwrap_err();
assert!(error.contains("no Package version satisfies"), "{error}");
assert_eq!(
fs::read(root.path().join("config/setting.json")).unwrap(),
before
);
assert!(!root.path().join("dever.lock").exists());
}
#[test]
fn package_archive_rejects_paths_owned_by_another_component() {
let root = project();
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
for (path, bytes) in [
(
"dever-package.json",
r#"{"format":"dever-package-v1","name":"storefront","version":"1.0.0"}"#,
),
(
"module/portal/value/app.dever",
"value() (result: Text) { result = \"bad\" }",
),
] {
writer
.start_file(path, zip::write::SimpleFileOptions::default())
.unwrap();
writer.write_all(bytes.as_bytes()).unwrap();
}
let registry = registry(&[("storefront", "1.0.0", writer.finish().unwrap().into_inner())]);
let store = FixtureArtifactStore::default();
let error = execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&store,
)
.unwrap_err();
assert!(error.contains("unowned path"), "{error}");
}
#[test]
fn package_rejects_local_component_even_when_file_paths_do_not_overlap() {
let root = project();
fs::create_dir_all(root.path().join("module/storefront/other")).unwrap();
fs::write(
root.path().join("module/storefront/other/app.dever"),
"value() (result: Text) { result = \"local\" }\n",
)
.unwrap();
let registry = registry(&[(
"storefront",
"1.0.0",
archive(
"storefront",
"1.0.0",
serde_json::json!({}),
&[],
"value() (result: Text) { result = \"package\" }\n",
),
)]);
let store = FixtureArtifactStore::default();
let error = execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&store,
)
.unwrap_err();
assert!(error.contains("collides with a local component"), "{error}");
assert!(!root.path().join("dever.lock").exists());
}
#[test]
fn package_only_project_loads_without_a_local_module_directory() {
let root = TemporaryDirectory::new();
fs::create_dir(root.path().join("config")).unwrap();
fs::write(
root.path().join("config/setting.json"),
r#"{"package":{"registry":"https://packages.example.test","use":[]}}"#,
)
.unwrap();
let registry = registry(&[(
"catalog",
"1.0.0",
archive(
"catalog",
"1.0.0",
serde_json::json!({}),
&[],
"value() (result: Text) { result = \"ready\" }\n",
),
)]);
let store = FixtureArtifactStore::default();
execute_with(
"add",
root.path(),
&["catalog@1.0.0".into()],
&registry,
&store,
)
.unwrap();
let packages = sources_with(root.path(), &store).unwrap();
let sources = dever_core::source::SourceMap::load_with_packages(
&root.path().join("module"),
None,
&packages,
)
.unwrap();
assert_eq!(sources.files().len(), 1);
dever_core::check(&sources).unwrap();
}
#[test]
fn locked_package_worker_never_falls_back_to_a_local_file() {
let root = project();
let registry = registry(&[(
"storefront",
"1.0.0",
archive(
"storefront",
"1.0.0",
serde_json::json!({}),
&[],
"value() (result: Text) { result = \"two\" }\n",
),
)]);
let store = FixtureArtifactStore::default();
execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&store,
)
.unwrap();
let local = root.path().join("worker/storefront/tool");
fs::create_dir_all(local.parent().unwrap()).unwrap();
fs::write(&local, b"local fallback must never execute").unwrap();
assert!(owns_worker(root.path(), "worker/storefront/tool").unwrap());
assert!(owns_worker(root.path(), "module/storefront/value/worker/main.py").unwrap());
assert!(!owns_worker(root.path(), "worker/portal/tool").unwrap());
let error = owned_worker_bytes_with(root.path(), "worker/storefront/tool", &store).unwrap_err();
assert!(error.contains("missing from its locked archive"), "{error}");
assert_eq!(
owned_worker_bytes_with(root.path(), "worker/portal/tool", &store).unwrap(),
None
);
}
#[cfg(unix)]
#[test]
fn managed_package_worker_requires_its_locked_entry_and_rejects_local_collision() {
let root = project();
let (layout, executable) = signed_core(root.path(), &std::env::current_exe().unwrap(), false);
let mut daemon = TestDaemon::start(&layout);
let package_archive = archive_with_sources(
"storefront",
"1.0.0",
serde_json::json!({}),
&[],
"type SendResult { error Unavailable(message: Text) }\nvalue() (result: Text) { result = \"two\" }\nsend(value: Int) (receipt: Int) { receipt = port.send(value) }\n",
&[
(
"module/storefront/value/port.dever",
"send(value: Int) (receipt: Int) fails app.SendResult\n",
),
(
"module/storefront/value/adapter.dever",
"external pip \"worker/main.py\" { }\n",
),
],
);
fs::write(
root.path().join("config/setting.json"),
r#"{"package":{"registry":"https://packages.example.test","use":["storefront@1.0.0"]}}"#,
)
.unwrap();
let store = ServiceStore(layout.clone());
let package_sha256 = store.publish(&package_archive, "package").unwrap();
let mut zip = zip::ZipArchive::new(Cursor::new(&package_archive)).unwrap();
let mut manifest = Vec::new();
zip.by_name("dever-package.json")
.unwrap()
.read_to_end(&mut manifest)
.unwrap();
let mut lock = LockFile::new(Vec::new()).unwrap();
lock.packages.push(LockedPackage {
name: "storefront".into(),
version: "1.0.0".into(),
sha256: package_sha256,
bytes: package_archive.len() as u64,
dependencies: Vec::new(),
manifest_sha256: digest(&manifest),
});
lock.write_atomic(root.path()).unwrap();
let package_sources = dever_cli::packages::sources_with(root.path(), &store).unwrap();
let sources = dever_core::source::SourceMap::load_with_packages(
&root.path().join("module"),
None,
&package_sources,
)
.unwrap();
let program = dever_core::check(&sources).unwrap();
let contract = program.external_worker_contracts().remove(0);
assert_eq!(contract.entry, "module/storefront/value/worker/main.py");
assert!(owns_worker(root.path(), &contract.entry).unwrap());
let target = dever_cli::toolchain::platform_identity();
let metadata = serde_json::to_vec(&serde_json::json!({
"format": "dever-worker-runtime-v1", "ecosystem": "pip", "target": target,
"executable": "bin/python3", "arguments": ["-I", "-S", "-B"],
"python_wheel_tags": ["py3-none-any"],
"python_extension_suffixes": [".cpython-312-x86_64-linux-gnu.so", ".abi3.so", ".so"],
"python_markers": {
"implementation_name":"cpython", "implementation_version":"3.12.0", "os_name":"posix",
"platform_machine":"x86_64", "platform_python_implementation":"CPython", "platform_release":"",
"platform_system":"Linux", "platform_version":"", "python_full_version":"3.12.0",
"python_version":"3.12", "sys_platform":"linux"
},
}))
.unwrap();
let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::none());
let mut tar = tar::Builder::new(encoder);
for (path, bytes) in [
("dever-runtime.json", metadata.as_slice()),
("bin/python3", b"fixture-interpreter".as_slice()),
] {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o755);
header.set_cksum();
tar.append_data(&mut header, path, bytes).unwrap();
}
let runtime_bytes = tar.into_inner().unwrap().finish().unwrap();
let runtime_sha256 = digest(&runtime_bytes);
let runtime = RuntimePack {
name: "fixture-pip".into(),
version: "1".into(),
sha256: runtime_sha256.clone(),
};
lock.workers.push(LockedWorker {
port: contract.port,
adapter: contract.adapter,
ecosystem: contract.ecosystem,
runtime: Some(runtime),
entry: contract.entry.clone(),
schema: contract.schema,
capabilities: contract.capabilities,
operations: contract.operations,
libs: Vec::new(),
});
lock.write_atomic(root.path()).unwrap();
fs::write(root.path().join("test-runtime.pack"), runtime_bytes).unwrap();
let output = Command::new(executable)
.args([
"--exact",
"installed_managed_package_worker_guard",
"--ignored",
"--nocapture",
])
.current_dir(root.path())
.output()
.unwrap();
assert!(
output.status.success(),
"signed child test failed: {}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
daemon.stop();
}
#[cfg(unix)]
#[test]
#[ignore = "run only from the signed private fixture"]
fn installed_managed_package_worker_guard() {
let root = std::env::current_dir().unwrap();
let package_sources = dever_cli::packages::sources(&root).unwrap();
let sources = dever_core::source::SourceMap::load_with_packages(
&root.join("module"),
None,
&package_sources,
)
.unwrap();
let program = dever_core::check(&sources).unwrap();
let entry = &program.external_worker_contracts()[0].entry;
assert_eq!(entry, "module/storefront/value/worker/main.py");
assert!(owns_worker(&root, entry).unwrap());
let runtime_bytes = fs::read(root.join("test-runtime.pack")).unwrap();
let resources = [EmbeddedResource {
path: format!(
"lib/runtime/pip/{}/runtime.pack",
dever_cli::toolchain::platform_identity()
),
sha256: digest(&runtime_bytes),
bytes: runtime_bytes,
executable: false,
}];
let error = workers::prepare(&root, &program, &resources).unwrap_err();
assert!(
error.contains("locked Package Worker entry") && error.contains("missing"),
"{error}"
);
let local = root.join(entry);
fs::create_dir_all(local.parent().unwrap()).unwrap();
fs::write(local, b"local fallback must never execute").unwrap();
let error = workers::prepare(&root, &program, &resources).unwrap_err();
assert!(error.contains("collides with a local component"), "{error}");
}
#[test]
fn declared_package_worker_requires_a_lock_before_local_fallback() {
let root = project();
fs::write(
root.path().join("config/setting.json"),
r#"{"package":{"registry":"https://packages.example.test","use":["storefront@1.0.0"]}}"#,
)
.unwrap();
let error = owns_worker(root.path(), "worker/storefront/tool").unwrap_err();
assert!(error.contains("dever.lock"), "{error}");
}
#[test]
fn removing_a_package_keeps_locked_versions_without_registry_requests() {
let root = project();
let registry = registry(&[
(
"storefront",
"1.0.0",
archive(
"storefront",
"1.0.0",
serde_json::json!({"catalog":"^1.0.0"}),
&[],
"value() (result: Text) { result = catalog.value.value() }\n",
),
),
(
"catalog",
"1.2.0",
archive(
"catalog",
"1.2.0",
serde_json::json!({}),
&["pip:fixture@1.0.0"],
"value() (result: Text) { result = \"two\" }\n",
),
),
]);
let store = FixtureArtifactStore::default();
execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&store,
)
.unwrap();
execute_with(
"add",
root.path(),
&["catalog@^1.0.0".into()],
&registry,
&store,
)
.unwrap();
fs::write(
root.path().join("module/portal/value/app.dever"),
"value() (result: Text) { result = catalog.value.value() }\n",
)
.unwrap();
let offline = Registry(BTreeMap::new());
execute_with(
"remove",
root.path(),
&["storefront".into()],
&offline,
&store,
)
.unwrap();
let lock = LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
assert_eq!(lock.packages.len(), 1);
assert_eq!(
(
lock.packages[0].name.as_str(),
lock.packages[0].version.as_str()
),
("catalog", "1.2.0")
);
assert_eq!(lock.libs[0].spec.key(), "pip:fixture@1.0.0");
assert!(
!String::from_utf8_lossy(&fs::read(root.path().join("config/setting.json")).unwrap())
.contains("storefront@")
);
fs::write(
root.path().join("module/portal/value/app.dever"),
"value() (result: Text) { result = \"two\" }\n",
)
.unwrap();
execute_with("remove", root.path(), &["catalog".into()], &offline, &store).unwrap();
let settings: serde_json::Value =
serde_json::from_slice(&fs::read(root.path().join("config/setting.json")).unwrap())
.unwrap();
assert_eq!(
settings["package"]["registry"],
"https://packages.example.test"
);
assert_eq!(settings["package"]["use"], serde_json::json!([]));
execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&store,
)
.unwrap();
assert_eq!(
LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap())
.unwrap()
.packages
.len(),
2
);
}
#[cfg(unix)]
#[test]
fn package_update_restores_original_setting_bytes_when_lock_write_fails() {
use std::os::unix::fs::symlink;
let root = project();
let registry = registry(&[(
"storefront",
"1.0.0",
archive(
"storefront",
"1.0.0",
serde_json::json!({}),
&[],
"value() (result: Text) { result = \"two\" }\n",
),
)]);
let store = FixtureArtifactStore::default();
execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&store,
)
.unwrap();
let settings = fs::read(root.path().join("config/setting.json")).unwrap();
let lock_path = root.path().join("dever.lock");
let saved = root.path().join("saved.lock");
fs::rename(&lock_path, &saved).unwrap();
symlink(&saved, &lock_path).unwrap();
let error = execute_with("update", root.path(), &[], &registry, &store).unwrap_err();
assert!(error.contains("symbolic link"), "{error}");
assert_eq!(
fs::read(root.path().join("config/setting.json")).unwrap(),
settings
);
assert_eq!(fs::read(&saved).unwrap(), fs::read(&lock_path).unwrap());
}
#[cfg(unix)]
struct ServiceStore(Layout);
#[cfg(unix)]
impl ArtifactStore for ServiceStore {
fn has(&self, _: &str, _: &str) -> Result<bool, String> {
Err("exact length required".into())
}
fn publish(&self, bytes: &[u8], _: &str) -> Result<String, String> {
Ok(artifact_put(&self.0, bytes)?.sha256)
}
fn get(&self, _: &str, _: &str) -> Result<Option<Vec<u8>>, String> {
Err("exact length required".into())
}
fn get_exact(&self, sha256: &str, bytes: u64, _: &str) -> Result<Option<Vec<u8>>, String> {
artifact_get(&self.0, sha256, bytes).map(Some)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn installed_dever(root: &Path) -> (Layout, std::path::PathBuf) {
signed_core(root, Path::new(env!("CARGO_BIN_EXE_dever")), true)
}
#[cfg(unix)]
fn signed_core(
root: &Path,
source_executable: &Path,
health_checked_install: bool,
) -> (Layout, std::path::PathBuf) {
use std::os::unix::fs::PermissionsExt;
let layout = Layout::new(root.join("machine"));
layout.initialize().unwrap();
let key_bytes = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
let key = Ed25519KeyPair::from_pkcs8(key_bytes.as_ref()).unwrap();
fs::write(
layout.state().join("trusted-release-key"),
format!("{}\n", hex(key.public_key().as_ref())),
)
.unwrap();
let release = if health_checked_install {
layout.downloads().join("0.1.0")
} else {
layout.versions().join("0.1.0")
};
fs::create_dir(&release).unwrap();
let core = release.join("dever-core");
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if health_checked_install {
llvm_pack::compiler(source_executable, &release, "dever-core");
} else {
fs::copy(source_executable, &core).unwrap();
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fs::copy(source_executable, &core).unwrap();
fs::set_permissions(&core, fs::Permissions::from_mode(0o755)).unwrap();
let bytes = fs::read(&core).unwrap();
let artifacts = vec![Artifact {
path: "dever-core".into(),
bytes: bytes.len() as u64,
sha256: digest(&bytes),
}];
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
let artifacts = if health_checked_install {
artifacts
.into_iter()
.chain(native_release_inputs(&release))
.collect()
} else {
artifacts
};
let manifest = ReleaseManifest::new(Version::parse("0.1.0").unwrap(), artifacts);
let manifest_bytes = serde_json::to_vec(&manifest).unwrap();
fs::write(release.join("manifest.json"), &manifest_bytes).unwrap();
fs::write(
release.join("manifest.sig"),
format!("{}\n", hex(key.sign(&manifest_bytes).as_ref())),
)
.unwrap();
let manager = MachineManager::new(layout.clone());
if health_checked_install {
manager.install_requested("0.1.0").unwrap();
} else {
manager
.resolve_core(&Version::parse("0.1.0").unwrap())
.unwrap();
}
(layout.clone(), layout.versions().join("0.1.0/dever-core"))
}
#[cfg(unix)]
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
/// Explicit author inputs included in an ephemeral signature; these are not
/// the four trimmed runtime profiles of a formal distribution.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn native_release_inputs(root: &Path) -> Vec<Artifact> {
let document = llvm_pack::manifest(root, true);
let prefix = format!("runtime/{}", dever_cli::toolchain::platform_identity());
let paths = document["files"]
.as_array()
.unwrap()
.iter()
.map(|file| format!("{prefix}/{}", file["path"].as_str().unwrap()))
.chain([format!("{prefix}/manifest.json")])
.chain(
fs::read_dir(root.join("lib"))
.unwrap()
.map(|entry| format!("lib/{}", entry.unwrap().file_name().to_str().unwrap())),
);
paths
.map(|path| {
let file = root.join(&path);
Artifact {
path,
bytes: fs::metadata(&file).unwrap().len(),
sha256: dever_cli::toolchain::sha256_file(&file).unwrap(),
}
})
.collect()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn assert_cli_success(executable: &Path, arguments: &[&str]) -> std::process::Output {
let output = Command::new(executable)
.env_clear()
.args(arguments)
.output()
.unwrap();
assert!(
output.status.success(),
"{} exited {}\nstdout: {}\nstderr: {}",
arguments.join(" "),
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
output
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires explicitly prepared native runtime archive and owned daemon"]
fn installed_package_passes_check_test_run_build_and_standalone_execution() {
let root = project();
let (layout, executable) = installed_dever(root.path());
let mut daemon = TestDaemon::start(&layout);
let registry = registry(&[
(
"storefront",
"1.0.0",
archive(
"storefront",
"1.0.0",
serde_json::json!({"catalog":"1.2.0"}),
&[],
"value() (result: Text) { result = catalog.value.value() }\n",
),
),
(
"catalog",
"1.2.0",
archive(
"catalog",
"1.2.0",
serde_json::json!({}),
&[],
"value() (result: Text) { result = \"two\" }\n",
),
),
]);
execute_with(
"add",
root.path(),
&["storefront@1.0.0".into()],
&registry,
&ServiceStore(layout.clone()),
)
.unwrap();
let project = root.path().to_str().unwrap();
assert_cli_success(&executable, &["check", project]);
assert_cli_success(&executable, &["test", project]);
let run = assert_cli_success(
&executable,
&["run", project, "--", "portal.value.show", "{}"],
);
assert!(String::from_utf8_lossy(&run.stdout).contains("two"));
let output = root.path().join("standalone");
assert_cli_success(
&executable,
&["build", project, "--output", output.to_str().unwrap()],
);
daemon.stop();
let standalone = assert_cli_success(&output, &["portal.value.show", "{}"]);
assert!(String::from_utf8_lossy(&standalone.stdout).contains("two"));
}
@@ -0,0 +1,456 @@
//! Actual managed compilation, using ephemeral signing keys and an owned daemon.
use super::*;
use dever_cli::toolchain::compilation::{CompileKind, CompileRequest};
use dever_cli::toolchain::{BuildTarget, CacheStore, compile};
use dever_core::source::SourceMap;
use dever_runtime::config::Settings;
use std::io::Read;
use std::os::unix::net::UnixStream;
use std::time::{Duration, Instant};
fn source_project() -> TemporaryDirectory {
let root = project();
fs::write(
root.path().join("module/portal/value/app.dever"),
"value() (result: Text) { result = \"two\" }\n",
)
.unwrap();
root
}
fn request(root: &Path, kind: CompileKind) -> CompileRequest {
let sources = SourceMap::load_with_packages(
&root.join("module"),
if kind == CompileKind::TestSuite {
Some(root.join("test"))
} else {
None
}
.as_deref(),
&[],
)
.unwrap();
let bindings = (kind == CompileKind::Application)
.then(|| Settings::load_project(root).unwrap().compilation_bindings());
CompileRequest::new(
"0.1.0".into(),
kind,
&sources,
bindings,
&[],
BuildTarget::host().unwrap(),
)
.unwrap()
}
#[test]
fn compilation_snapshot_excludes_secrets_and_rejects_ambiguous_or_unsafe_inputs() {
let root = source_project();
fs::write(root.path().join("config/setting.json"), serde_json::to_vec(&serde_json::json!({
"database":{"default":{"type":"sqlite","path":"private-database-marker.sqlite"}},
"auth":{"providers":{"session":{"verify":"user.account.verify",
"jwtSecret":"private-jwt-marker-long-enough-for-hmac-2026","ttlSeconds":3600,"cookie":"secret-cookie-name"}}},
"sites":{"admin":{"path":"admin","auth":"session","origin":"https://private-origin.example"}}
})).unwrap()).unwrap();
let original = request(root.path(), CompileKind::Application);
let encoded = original.encode().unwrap();
let text = std::str::from_utf8(&encoded).unwrap();
for secret in [
"private-database-marker",
"private-jwt-marker",
"secret-cookie-name",
"private-origin",
root.path().to_str().unwrap(),
] {
assert!(!text.contains(secret), "compile input leaked {secret}");
}
assert!(text.contains("user.account.verify"));
assert!(text.contains("sqlite"));
CompileRequest::decode(&encoded).unwrap();
let duplicate = text.replace("\"databases\":", "\"databases\":{},\"databases\":");
assert!(CompileRequest::decode(duplicate.as_bytes()).is_err());
for path in ["/tmp/read-me.dever", "../main.dever", "portal/value/app.rs"] {
let mut invalid = original.clone();
invalid.sources[0].logical_path = path.into();
assert!(invalid.encode().is_err(), "accepted source path {path}");
}
let mut reserved = original.clone();
reserved.sources[0].logical_path = "dever/auth/app.dever".into();
assert!(
dever_core::check(&reserved.source_map().unwrap()).is_err(),
"reserved standard namespace bypassed checking"
);
let mut invalid = original.clone();
invalid.sources.push(invalid.sources[0].clone());
assert!(invalid.encode().is_err());
let mut invalid = original;
invalid.sources[0].is_test = true;
assert!(invalid.encode().is_err());
let suite = request(root.path(), CompileKind::TestSuite);
assert!(suite.bindings.is_none());
assert!(
suite
.source_map()
.unwrap()
.files()
.iter()
.any(|source| source.is_test())
);
}
#[test]
fn compilation_target_is_required_canonical_and_part_of_cache_identity() {
let root = source_project();
let mut input = request(root.path(), CompileKind::Application);
input.target = BuildTarget::LinuxX86_64;
let host = input.encode().unwrap();
input.target = BuildTarget::LinuxAarch64;
let arm = input.encode().unwrap();
assert_ne!(host, arm);
assert_eq!(
CompileRequest::decode(&arm).unwrap().target,
BuildTarget::LinuxAarch64
);
let text = std::str::from_utf8(&arm).unwrap();
for invalid in [
text.replace("\"target\":\"linux-aarch64\",", ""),
text.replace("linux-aarch64", "aarch64-unknown-linux-gnu"),
text.replace("\"target\":", "\"target\":\"linux-x86_64\",\"target\":"),
] {
assert!(CompileRequest::decode(invalid.as_bytes()).is_err());
}
let mut suite = request(root.path(), CompileKind::TestSuite);
suite.target = if BuildTarget::host().unwrap() == BuildTarget::LinuxX86_64 {
BuildTarget::LinuxAarch64
} else {
BuildTarget::LinuxX86_64
};
assert!(suite.encode().unwrap_err().contains("host target"));
}
#[test]
#[ignore = "requires explicitly prepared native archive; starts an owned daemon"]
fn trusted_compilation_cache_requires_sources_and_revalidates_signed_pack() {
let root = source_project();
let (layout, _) = installed_dever(root.path());
let _daemon = TestDaemon::start(&layout);
let input = request(root.path(), CompileKind::Application);
let started = Instant::now();
let first = compile(&layout, &input).unwrap();
let cold_seconds = started.elapsed().as_secs_f64();
let initial_cache = cache_status(&layout).unwrap();
assert_eq!(initial_cache.entries, 1);
let started = Instant::now();
assert_eq!(compile(&layout, &input).unwrap(), first);
let hit_seconds = started.elapsed().as_secs_f64();
assert_eq!(cache_status(&layout).unwrap().entries, 1);
assert!(artifact_get(&layout, &digest(&first), first.len() as u64).is_err());
let mut invalid = input.clone();
invalid
.sources
.iter_mut()
.find(|source| source.logical_path.ends_with("app.dever"))
.unwrap()
.text = "value() (result: Text) { result = missing_function() }".into();
let error = compile(&layout, &invalid).unwrap_err();
assert!(error.contains("missing_function"), "{error}");
assert_eq!(cache_status(&layout).unwrap().entries, 1);
assert_eq!(compile(&layout, &input).unwrap(), first);
let mut changed = input.clone();
changed
.sources
.iter_mut()
.find(|source| source.logical_path.ends_with("app.dever"))
.unwrap()
.text = "value() (result: Text) { result = \"three\" }".into();
// 两个独立客户端同时提交同一份新源码,必须收敛到一个已验证产物。
let barrier = std::sync::Barrier::new(2);
let concurrent = std::thread::scope(|scope| {
let clients: Vec<_> = (0..2)
.map(|_| {
scope.spawn(|| {
barrier.wait();
let started = Instant::now();
let bytes = compile(&layout, &changed).unwrap();
(bytes, started.elapsed().as_secs_f64())
})
})
.collect();
clients
.into_iter()
.map(|client| client.join().unwrap())
.collect::<Vec<_>>()
});
assert_eq!(concurrent[0].0, concurrent[1].0);
assert_ne!(concurrent[0].0, first);
let shared_cache = cache_status(&layout).unwrap();
assert_eq!(shared_cache.entries, 2);
// The worker compiles the suite; only the caller runs its failing case.
fs::write(
root.path().join("test/portal/value/smoke.dever"),
"smoke() () { assert_eq(1, 2) }",
)
.unwrap();
let suite = compile(&layout, &request(root.path(), CompileKind::TestSuite)).unwrap();
let program = dever_core::native::NativeProgram::from_verified_bytes(&suite).unwrap();
assert!(
!Command::new(program.executable())
.arg("0")
.env_clear()
.output()
.unwrap()
.status
.success()
);
assert_eq!(cache_status(&layout).unwrap().entries, 3);
// A valid signature over an incomplete list must not authorize unsigned
// link inputs, even when the native manifest still names those inputs.
let version_root = layout.versions().join("0.1.0");
let trusted_key = fs::read(layout.state().join("trusted-release-key")).unwrap();
let signed_manifest = fs::read(version_root.join("manifest.json")).unwrap();
let signature = fs::read(version_root.join("manifest.sig")).unwrap();
let mut manifest: ReleaseManifest = serde_json::from_slice(&signed_manifest).unwrap();
manifest
.artifacts
.retain(|artifact| !artifact.path.ends_with("/crt1.o"));
let key_bytes = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
let key = Ed25519KeyPair::from_pkcs8(key_bytes.as_ref()).unwrap();
let bytes = serde_json::to_vec(&manifest).unwrap();
fs::write(
layout.state().join("trusted-release-key"),
hex(key.public_key().as_ref()),
)
.unwrap();
fs::write(version_root.join("manifest.json"), &bytes).unwrap();
fs::write(
version_root.join("manifest.sig"),
hex(key.sign(&bytes).as_ref()),
)
.unwrap();
assert!(
compile(&layout, &input)
.unwrap_err()
.contains("not covered by the signed release")
);
fs::write(layout.state().join("trusted-release-key"), trusted_key).unwrap();
fs::write(version_root.join("manifest.json"), signed_manifest).unwrap();
fs::write(version_root.join("manifest.sig"), signature).unwrap();
let pack = layout.versions().join("0.1.0/runtime/linux-x86_64/crt1.o");
fs::write(pack, b"changed signed link input").unwrap();
let error = compile(&layout, &input).unwrap_err();
assert!(error.contains("integrity"), "{error}");
assert_eq!(cache_status(&layout).unwrap().entries, 3);
assert_eq!(
fs::read_dir(layout.native_cache().join("staging"))
.unwrap()
.count(),
0
);
assert_eq!(
fs::read_dir(layout.native_cache().join("operations"))
.unwrap()
.count(),
0
);
let report = serde_json::json!({
"format": "dever-shared-compile-measurement-v1",
"scope": "owned signed compiler and daemon; client wall time includes verification and IPC; warm OS caches",
"cold_seconds": cold_seconds,
"cache_hit_seconds": hit_seconds,
"artifact_bytes": first.len(),
"artifact_sha256": digest(&first),
"initial_cache": initial_cache,
"concurrent_clients": concurrent.len(),
"concurrent_miss_seconds": concurrent.iter().map(|(_, seconds)| seconds).collect::<Vec<_>>(),
"concurrent_same_artifact": true,
"shared_cache": shared_cache,
});
fs::write(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../target/closure-shared-compile-measurement.json"),
serde_json::to_vec_pretty(&report).unwrap(),
)
.unwrap();
}
#[test]
#[ignore = "requires root and prepared archive; only owned clients change UID"]
fn different_machine_users_share_compilation_without_private_cache() {
use std::os::unix::fs::PermissionsExt;
use std::os::unix::process::CommandExt;
assert_eq!(rustix::process::geteuid().as_raw(), 0);
let root = source_project();
let other = source_project();
// Only these owned fixtures are shared; the usual temporary root is 0700.
fs::set_permissions(root.path(), fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(other.path(), fs::Permissions::from_mode(0o755)).unwrap();
let (layout, core) = installed_dever(root.path());
let _daemon = TestDaemon::start(&layout);
assert_cli_success(
&core,
&[
"run",
root.path().to_str().unwrap(),
"--",
"portal.value.show",
"{}",
],
);
assert_eq!(cache_status(&layout).unwrap().entries, 1);
let result = Command::new(&core)
.uid(65534)
.gid(65534)
.env_clear()
.args([
"run",
other.path().to_str().unwrap(),
"--",
"portal.value.show",
"{}",
])
.output()
.unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert!(String::from_utf8_lossy(&result.stdout).contains("two"));
assert_eq!(cache_status(&layout).unwrap().entries, 1);
assert!(!core.parent().unwrap().join("cache").exists());
assert!(!other.path().join("cache").exists());
}
fn frame(stream: &mut UnixStream) -> serde_json::Value {
let mut size = [0; 4];
stream.read_exact(&mut size).unwrap();
let mut bytes = vec![0; u32::from_be_bytes(size) as usize];
stream.read_exact(&mut bytes).unwrap();
serde_json::from_slice(&bytes).unwrap()
}
fn upload(layout: &Layout, input: &CompileRequest) -> UnixStream {
let payload = input.encode().unwrap();
let mut stream = UnixStream::connect(layout.service_socket()).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let token = fs::read_to_string(layout.service_token()).unwrap();
let control = serde_json::to_vec(&serde_json::json!({"token":token.trim(),
"operation":{"compile":{"bytes":payload.len()}},"installed":[]}))
.unwrap();
stream
.write_all(&(control.len() as u32).to_be_bytes())
.unwrap();
stream.write_all(&control).unwrap();
assert_eq!(frame(&mut stream)["ok"], true);
stream.write_all(&payload).unwrap();
stream
}
fn wait_for<T>(mode: &str, phase: &str, mut action: impl FnMut() -> Option<T>) -> T {
let started = Instant::now();
loop {
if let Some(value) = action() {
return value;
}
assert!(
started.elapsed() < Duration::from_secs(10),
"owned compilation mode={mode} timed out waiting for {phase}"
);
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
#[ignore = "requires prepared archive, explicit author cc/LLVM inputs; supervises an owned signed sleeper"]
fn cancellation_disconnect_and_version_lease_reap_worker_and_staging() {
let root = source_project();
let sleeper = root.path().join("signed-sleeper");
let source = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test/dever-cli-tests/support/compiler_sleeper.c");
let status = Command::new("/usr/bin/cc")
.env_clear()
.args([
"-B/usr/bin/",
"-O2",
"-Wl,--disable-new-dtags,-rpath,$ORIGIN/lib,--no-as-needed",
"-L/usr/lib/llvm-18/lib",
"-lLLVM-18",
"-lstdc++",
"-lz",
"-lgcc_s",
])
.arg(source)
.arg("-o")
.arg(&sleeper)
.status()
.unwrap();
assert!(
status.success(),
"explicit native sleeper preparation failed"
);
let (layout, _) = signed_core(root.path(), &sleeper, true);
let _daemon = TestDaemon::start(&layout);
for mode in ["cancel", "disconnect", "deadline"] {
let mut stream = upload(&layout, &request(root.path(), CompileKind::Application));
let pid = wait_for(mode, "worker PID publication", || {
fs::read_dir(layout.native_cache().join("staging"))
.ok()?
.filter_map(Result::ok)
.find_map(|entry| {
fs::read_to_string(entry.path().join("worker.pid"))
.ok()?
.parse::<i32>()
.ok()
.filter(|pid| *pid > 0)
.map(|pid| pid.to_string())
})
});
let lock = fs::OpenOptions::new()
.read(true)
.write(true)
.open(layout.state().join("install.lock"))
.unwrap();
assert!(
fs2::FileExt::try_lock_exclusive(&lock).is_err(),
"active compiler version was not leased"
);
assert!(
CacheStore::new(&layout)
.clean(&Default::default())
.unwrap_err()
.contains("busy")
);
if mode == "cancel" {
stream.write_all(&[1]).unwrap();
let response = frame(&mut stream);
assert_eq!(response["ok"], false);
assert!(response["error"].as_str().unwrap().contains("cancel"));
} else if mode == "deadline" {
stream
.set_read_timeout(Some(Duration::from_secs(190)))
.unwrap();
let response = frame(&mut stream);
assert_eq!(response["ok"], false);
assert!(response["error"].as_str().unwrap().contains("deadline"));
}
drop(stream);
wait_for(mode, "worker exit and staging/operation cleanup", || {
let staging = fs::read_dir(layout.native_cache().join("staging"))
.ok()?
.count();
let operations = fs::read_dir(layout.native_cache().join("operations"))
.ok()?
.count();
(staging == 0 && operations == 0 && !Path::new("/proc").join(&pid).exists())
.then_some(())
});
fs2::FileExt::try_lock_exclusive(&lock).unwrap();
fs2::FileExt::unlock(&lock).unwrap();
assert_eq!(cache_status(&layout).unwrap().entries, 0);
}
}
+289
View File
@@ -0,0 +1,289 @@
use super::super::release::Artifact;
use super::*;
use sha2::{Digest, Sha256};
fn manifest() -> ReleaseManifest {
ReleaseManifest::new(
Version::parse("1.2.3").unwrap(),
vec![Artifact {
path: "dever-core".into(),
bytes: 4,
sha256: super::super::release::hex(&Sha256::digest(b"core")),
}],
)
}
fn archive(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut archive = tar::Builder::new(Vec::new());
for (name, contents) in entries {
let mut header = tar::Header::new_ustar();
header.set_size(contents.len() as u64);
header.set_mode(0o644);
header.set_cksum();
archive.append_data(&mut header, name, *contents).unwrap();
}
archive.into_inner().unwrap()
}
#[test]
fn release_archive_accepts_only_the_exact_signed_regular_files() {
let root = Download::new(&std::env::temp_dir()).unwrap();
unpack(
archive(&[("dever-core", b"core")]).as_slice(),
&root.0,
&manifest(),
)
.unwrap();
assert_eq!(fs::read(root.0.join("dever-core")).unwrap(), b"core");
for entries in [
vec![
("dever-core", b"core".as_slice()),
("extra", b"x".as_slice()),
],
vec![
("dever-core", b"core".as_slice()),
("dever-core", b"core".as_slice()),
],
vec![("dever-core", b"bad".as_slice())],
vec![],
] {
let root = Download::new(&std::env::temp_dir()).unwrap();
assert!(unpack(archive(&entries).as_slice(), &root.0, &manifest()).is_err());
}
}
#[test]
fn release_archive_rejects_links_and_extension_headers() {
for kind in [
tar::EntryType::Symlink,
tar::EntryType::Link,
tar::EntryType::XGlobalHeader,
] {
let root = Download::new(&std::env::temp_dir()).unwrap();
let mut archive = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_ustar();
header.set_path("dever-core").unwrap();
header.set_entry_type(kind);
header.set_size(0);
header.set_mode(0o644);
header.set_link_name("/outside").unwrap();
header.set_cksum();
archive.append(&header, io::empty()).unwrap();
assert!(
unpack(
archive.into_inner().unwrap().as_slice(),
&root.0,
&manifest()
)
.is_err()
);
assert!(!root.0.join("dever-core").exists());
}
}
#[test]
fn signed_catalog_paths_and_total_size_are_bounded_before_extraction() {
for path in [
"/absolute",
"../outside",
"a/../b",
"./file",
"a//b",
"a\\b",
"C:drive",
"manifest.json",
"manifest.sig",
] {
let mut manifest = manifest();
manifest.artifacts[0].path = path.into();
assert!(validate_catalog(&manifest).is_err(), "{path}");
}
let mut excessive = manifest();
excessive.artifacts[0].bytes = RELEASE_LIMIT + 1;
assert!(validate_catalog(&excessive).is_err());
let mut duplicate = manifest();
duplicate.artifacts.push(duplicate.artifacts[0].clone());
assert!(validate_catalog(&duplicate).is_err());
}
#[test]
fn official_redirects_reject_http_credentials_ports_and_foreign_hosts() {
let current = "https://github.com/shemic/dever-main/releases/latest/download/a";
assert!(
redirect(
current,
"https://release-assets.githubusercontent.com/asset?token=opaque"
)
.is_ok()
);
assert!(redirect(current, "/shemic/dever-main/releases/download/v1.2.3/a").is_ok());
for address in [
"http://github.com/a",
"https://evil.example/a",
"https://github.com.evil.example/a",
"https://user@github.com/a",
"https://github.com:444/a",
"https://github.com/a#fragment",
] {
assert!(redirect(current, address).is_err(), "{address}");
}
}
#[test]
fn official_asset_names_are_versioned_and_platform_specific() {
let source = ReleaseSource::official();
assert_eq!(
source.asset("latest", "manifest.json"),
format!(
"{OFFICIAL_RELEASES}/latest/download/dever-{}.manifest.json",
platform_identity()
)
);
assert_eq!(
source.asset("1.2.3", "tar.gz"),
format!(
"{OFFICIAL_RELEASES}/download/v1.2.3/dever-{}.tar.gz",
platform_identity()
)
);
}
fn signed_release() -> (ReleaseManifest, Vec<u8>, Vec<u8>, Vec<u8>) {
use ring::signature::{Ed25519KeyPair, KeyPair};
let pair = Ed25519KeyPair::from_seed_unchecked(&[37; 32]).unwrap();
let mut manifest = manifest();
manifest.artifacts.push(Artifact {
path: "skills/dever-language/SKILL.md".into(),
bytes: 5,
sha256: super::super::release::hex(&Sha256::digest(b"skill")),
});
let bytes = serde_json::to_vec(&manifest).unwrap();
let signature = super::super::release::hex(pair.sign(&bytes).as_ref()).into_bytes();
(
manifest,
bytes,
signature,
pair.public_key().as_ref().to_vec(),
)
}
fn server(responses: Vec<Vec<u8>>) -> (ReleaseSource, std::thread::JoinHandle<()>) {
use std::io::BufRead;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let base = format!("http://{}/releases", listener.local_addr().unwrap());
let worker = std::thread::spawn(move || {
let deadline = Instant::now() + Duration::from_secs(5);
for response in responses {
let mut stream = loop {
match listener.accept() {
Ok((stream, _)) => break stream,
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
assert!(
Instant::now() < deadline,
"expected release request did not arrive"
);
std::thread::sleep(Duration::from_millis(5));
}
Err(error) => panic!("{error}"),
}
};
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let mut reader = io::BufReader::new(stream.try_clone().unwrap());
let mut request = String::new();
reader.read_line(&mut request).unwrap();
assert!(request.starts_with("GET /releases/"));
loop {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
if line == "\r\n" {
break;
}
}
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
response.len()
)
.unwrap();
stream.write_all(&response).unwrap();
}
});
(ReleaseSource::new(base), worker)
}
fn compressed_release(core: &[u8]) -> Vec<u8> {
let bytes = archive(&[
("dever-core", core),
("skills/dever-language/SKILL.md", b"skill"),
]);
let mut gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
gzip.write_all(&bytes).unwrap();
gzip.finish().unwrap()
}
#[test]
fn explicit_download_verifies_the_complete_bundle_before_publishing_catalog() {
let root = Download::new(&std::env::temp_dir()).unwrap();
let layout = Layout::new(root.0.join("machine"));
layout.initialize().unwrap();
let (manifest, bytes, signature, key) = signed_release();
fs::write(
layout.state().join("trusted-release-key"),
super::super::release::hex(&key),
)
.unwrap();
let (source, worker) = server(vec![bytes, signature, compressed_release(b"core")]);
let version = prepare_from(&layout, "latest", &source).unwrap();
worker.join().unwrap();
assert_eq!(version, manifest.version);
let installed = layout.downloads().join(version.as_str());
MachineManager::verify_package(&installed, Some(&version), &key).unwrap();
assert_eq!(
fs::read(installed.join("skills/dever-language/SKILL.md")).unwrap(),
b"skill"
);
assert_eq!(
fs::read_to_string(layout.downloads().join("latest")).unwrap(),
"1.2.3\n"
);
assert_eq!(fs::read_dir(layout.staging()).unwrap().count(), 0);
}
#[test]
fn bad_signatures_and_corrupt_payloads_preserve_the_previous_catalog_and_active_version() {
for bad_signature in [true, false] {
let root = Download::new(&std::env::temp_dir()).unwrap();
let layout = Layout::new(root.0.join("machine"));
layout.initialize().unwrap();
let (_, bytes, signature, key) = signed_release();
fs::write(
layout.state().join("trusted-release-key"),
super::super::release::hex(&key),
)
.unwrap();
fs::write(layout.downloads().join("latest"), "0.9.0\n").unwrap();
let active = layout.state().join("active-version");
super::super::release::append_active_version(&active, &Version::parse("0.9.0").unwrap())
.unwrap();
let previous = fs::read(&active).unwrap();
let responses = if bad_signature {
vec![bytes, vec![b'0'; 128]]
} else {
vec![bytes, signature, compressed_release(b"evil")]
};
let (source, worker) = server(responses);
let error = prepare_from(&layout, "latest", &source).unwrap_err();
worker.join().unwrap();
assert!(error.contains("verification"), "{error}");
assert_eq!(fs::read(&active).unwrap(), previous);
assert_eq!(
fs::read_to_string(layout.downloads().join("latest")).unwrap(),
"0.9.0\n"
);
assert!(!layout.downloads().join("1.2.3").exists());
assert_eq!(fs::read_dir(layout.staging()).unwrap().count(), 0);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,2 @@
/* Deliberately separate DT_NEEDED dependency for native command packaging. */
int command_value(void) { return 42; }
@@ -0,0 +1,19 @@
#include <stdio.h>
#include <string.h>
extern int command_value(void);
int main(int argc, char **argv) {
const unsigned char expected[] = {0, 255, 128, 10};
unsigned char input[sizeof(expected)];
if (argc != 3 || strcmp(argv[1], "a b") != 0 ||
strcmp(argv[2], "$(literal);*") != 0 || command_value() != 42)
return 90;
if (fread(input, 1, sizeof(input), stdin) != sizeof(input) ||
memcmp(input, expected, sizeof(input)) != 0 || getchar() != EOF)
return 91;
if (fwrite(input, 1, sizeof(input), stdout) != sizeof(input) ||
fputs("ordinary command", stderr) == EOF)
return 92;
return 23;
}
@@ -0,0 +1,18 @@
/* Native author fixture for compile cancellation and deadline supervision. */
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
int main(int argc, char **argv) {
if (argc == 2 && strcmp(argv[1], "--dever-health") == 0) return 0;
FILE *pid = fopen("worker.pid", "w");
if (!pid) return 2;
if (fprintf(pid, "%ld", (long)getpid()) < 0 || fclose(pid) != 0) return 3;
struct timespec remaining = {300, 0};
while (nanosleep(&remaining, &remaining) != 0) {
if (errno != EINTR) return 4;
}
return 0;
}
+42
View File
@@ -0,0 +1,42 @@
//! Lifetime of an explicitly requested test-owned shared compiler service.
use std::process::{Child, Command};
use std::time::Duration;
use dever_cli::toolchain::{Layout, cache_status};
pub struct TestDaemon(Child);
impl TestDaemon {
pub fn start(layout: &Layout) -> Self {
let child = Command::new(env!("CARGO_BIN_EXE_deverd"))
.env_clear()
.current_dir(layout.root())
.args(["--root", "."])
.spawn()
.unwrap();
let mut daemon = Self(child);
for _ in 0..100 {
if cache_status(layout).is_ok() {
return daemon;
}
assert!(
daemon.0.try_wait().unwrap().is_none(),
"test deverd exited early"
);
std::thread::sleep(Duration::from_millis(10));
}
panic!("test deverd did not become ready");
}
pub fn stop(&mut self) {
self.0.kill().unwrap();
self.0.wait().unwrap();
}
}
impl Drop for TestDaemon {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
@@ -0,0 +1,67 @@
//! Explicit Linux author inputs shared by opt-in native fixtures.
use std::path::{Path, PathBuf};
pub const LLVM_LIBRARY: &str = "/usr/lib/llvm-18/lib/libLLVM.so.1";
pub fn core_libraries() -> Vec<(&'static str, PathBuf)> {
let mut inputs = vec![("libLLVM.so.18.1", PathBuf::from(LLVM_LIBRARY))];
for name in [
"libstdc++.so.6",
"libgcc_s.so.1",
"libz.so.1",
"libffi.so.8",
"libedit.so.2",
"libzstd.so.1",
"libtinfo.so.6",
"libxml2.so.2",
"libbsd.so.0",
"libmd.so.0",
"libicuuc.so.74",
"libicudata.so.74",
"liblzma.so.5",
] {
inputs.push((name, Path::new("/usr/lib/x86_64-linux-gnu").join(name)));
}
inputs
.into_iter()
.map(|(name, path)| {
(
name,
path.canonicalize()
.expect("prepare the explicit GNU compiler-library inputs"),
)
})
.collect()
}
pub fn native_inputs() -> Vec<(&'static str, PathBuf)> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
vec![
(
"runtime.a",
workspace.join("target/native-runtime-abi/debug/libdever_backend_bridge.a"),
),
("crt1.o", "/usr/lib/x86_64-linux-gnu/crt1.o".into()),
("crti.o", "/usr/lib/x86_64-linux-gnu/crti.o".into()),
(
"crtbeginT.o",
"/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginT.o".into(),
),
(
"crtend.o",
"/usr/lib/gcc/x86_64-linux-gnu/13/crtend.o".into(),
),
("crtn.o", "/usr/lib/x86_64-linux-gnu/crtn.o".into()),
("libc.a", "/usr/lib/x86_64-linux-gnu/libc.a".into()),
("libm.a", "/usr/lib/x86_64-linux-gnu/libm-2.39.a".into()),
("libmvec.a", "/usr/lib/x86_64-linux-gnu/libmvec.a".into()),
(
"libgcc.a",
"/usr/lib/gcc/x86_64-linux-gnu/13/libgcc.a".into(),
),
(
"libgcc_eh.a",
"/usr/lib/gcc/x86_64-linux-gnu/13/libgcc_eh.a".into(),
),
]
}
+128
View File
@@ -0,0 +1,128 @@
//! Explicit test inputs: no downloads, recursive builds or host tool lookup.
use std::fs;
use std::path::{Path, PathBuf};
use dever_cli::toolchain::sha256_file as hash_file;
use serde_json::{Value, json};
#[path = "llvm_inputs.rs"]
mod inputs;
#[cfg(test)]
#[path = "native_objects.rs"]
mod native_objects;
use inputs::{core_libraries, native_inputs};
#[cfg(test)]
pub fn compiler(source: &Path, root: &Path, name: &str) -> PathBuf {
fs::create_dir_all(root).unwrap();
let path = root.join(name);
fs::hard_link(source, &path).unwrap();
compiler_library(root).unwrap();
path
}
pub fn pack_root(root: &Path) -> PathBuf {
root.join("runtime")
.join(dever_cli::toolchain::platform_identity())
}
#[cfg(test)]
pub fn manifest(root: &Path, real: bool) -> Value {
let destination = pack_root(root);
fs::create_dir_all(&destination).unwrap();
let sources = native_inputs();
let mut files = Vec::new();
for (name, source) in sources {
let output = destination.join(name);
if real {
assert!(
source.is_file(),
"prepare the explicit native archive and CRT fixture first: {}",
source.display()
);
// Only immutable reads follow. Corruption cases use synthetic files.
fs::hard_link(source, &output).unwrap();
} else {
fs::write(
&output,
if name.ends_with(".a") {
native_objects::archive(
dever_cli::toolchain::BuildTarget::host().unwrap(),
"fixture.o",
)
} else {
native_objects::object(dever_cli::toolchain::BuildTarget::host().unwrap())
},
)
.unwrap();
}
files.push(json!({"path":name, "bytes":fs::metadata(&output).unwrap().len(), "sha256":digest(&output)}));
}
let document = document(files);
write_manifest(root, &document);
document
}
fn document(files: Vec<Value>) -> Value {
let profile = json!({
"start":["crt1.o","crti.o","crtbeginT.o"], "runtime":"runtime.a",
"libraries":["libc.a","libm.a","libmvec.a","libgcc.a","libgcc_eh.a"],
"end":["crtend.o","crtn.o"]
});
json!({
"format":"dever-native-runtime-v1", "compiler_version":env!("CARGO_PKG_VERSION"),
"abi":dever_backend_bridge::RUNTIME_ABI_VERSION,
"platform":dever_cli::toolchain::platform_identity(),
"target":"x86_64-unknown-linux-gnu",
"profiles":{"base":profile,"sqlite":profile,"postgres":profile,"both":profile},
"files":files
})
}
fn compiler_library(root: &Path) -> Result<(), String> {
let libraries = root.join("lib");
create_or_check_directory(&libraries)?;
for (name, source) in core_libraries() {
let target = libraries.join(name);
match fs::symlink_metadata(&target) {
Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => {
if hash_file(&target)? != hash_file(&source)? {
return Err(
"existing compiler library differs from the explicit author SDK".into(),
);
}
}
Ok(_) => return Err("compiler library must be a real file".into()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
fs::hard_link(source, target).map_err(|error| error.to_string())?;
}
Err(error) => return Err(error.to_string()),
}
}
Ok(())
}
fn create_or_check_directory(path: &Path) -> Result<(), String> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
Ok(_) => Err(format!("'{}' must be a real directory", path.display())),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(path).map_err(|error| error.to_string())
}
Err(error) => Err(error.to_string()),
}
}
#[cfg(test)]
pub fn write_manifest(root: &Path, manifest: &Value) {
fs::write(
pack_root(root).join("manifest.json"),
serde_json::to_vec(manifest).unwrap(),
)
.unwrap();
}
#[cfg(test)]
pub fn digest(path: &Path) -> String {
hash_file(path).unwrap()
}
@@ -0,0 +1,41 @@
//! Test-only static entry for the already pivoted no-host acceptance namespace.
#[cfg(target_os = "linux")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use rustix::mount::{MountFlags, mount};
use rustix::thread::{self, CapabilitySet};
use std::os::unix::process::CommandExt;
let mut arguments = std::env::args_os().skip(1);
let executable = arguments.next().ok_or("missing acceptance executable")?;
if !std::path::Path::new(&executable).is_absolute() {
return Err("acceptance executable must be absolute".into());
}
// The root-only harness keeps bwrap's original namespace capabilities.
// Reject ordinary host execution before touching any mount.
if !thread::no_new_privs()?
|| !thread::capabilities(None)?
.effective
.contains(CapabilitySet::SYS_ADMIN)
{
return Err("acceptance init requires its restricted fixture namespace".into());
}
// bwrap's readonly proc submounts otherwise prevent a nested user
// namespace from mounting its own proc. Replace only this private proc.
mount(
"proc",
"/proc",
"proc",
MountFlags::NOSUID | MountFlags::NODEV | MountFlags::NOEXEC,
None,
)?;
Err(std::process::Command::new(executable)
.args(arguments)
.env_clear()
.exec()
.into())
}
#[cfg(not(target_os = "linux"))]
fn main() -> Result<(), &'static str> {
Err("native acceptance isolation requires Linux")
}
@@ -0,0 +1,90 @@
//! Small ELF metadata fixtures; execution acceptance uses real author inputs.
pub fn compiler(needed: &[&str]) -> Vec<u8> {
elf(None, needed, Some("$ORIGIN/lib"))
}
pub fn library(name: &str, needed: &[&str]) -> Vec<u8> {
elf(Some(name), needed, None)
}
pub fn static_executable() -> Vec<u8> {
let mut bytes = elf(None, &[], None);
// Change the synthetic PT_INTERP entry to PT_NULL. Execution tests use the
// real static author input; this fixture tests only ELF admission rules.
bytes[64 + 2 * 56..68 + 2 * 56].copy_from_slice(&0u32.to_le_bytes());
bytes
}
pub fn elf(soname: Option<&str>, needed: &[&str], rpath: Option<&str>) -> Vec<u8> {
let mut strings = vec![0u8];
let mut string = |text: &str| {
let offset = strings.len() as u64;
strings.extend_from_slice(text.as_bytes());
strings.push(0);
offset
};
let mut dynamic = needed
.iter()
.map(|name| (1u64, string(name)))
.collect::<Vec<_>>();
if let Some(name) = soname {
dynamic.push((14, string(name)));
}
if let Some(path) = rpath {
dynamic.push((15, string(path)));
}
let interpreter = if cfg!(target_arch = "aarch64") {
b"/lib/ld-linux-aarch64.so.1\0".as_slice()
} else {
b"/lib64/ld-linux-x86-64.so.2\0".as_slice()
};
let header_count: u16 = if soname.is_some() { 2 } else { 3 };
let string_offset = 64 + usize::from(header_count) * 56;
let interpreter_offset = string_offset + strings.len();
let dynamic_offset = (interpreter_offset + interpreter.len() + 7) & !7;
dynamic.extend([
(5, string_offset as u64),
(10, strings.len() as u64),
(0, 0),
]);
let length = dynamic_offset + dynamic.len() * 16;
let mut bytes = vec![0; length];
bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
bytes[16..18].copy_from_slice(&3u16.to_le_bytes());
let machine = if cfg!(target_arch = "aarch64") {
183u16
} else {
62u16
};
bytes[18..20].copy_from_slice(&machine.to_le_bytes());
bytes[20..24].copy_from_slice(&1u32.to_le_bytes());
bytes[32..40].copy_from_slice(&64u64.to_le_bytes());
bytes[52..54].copy_from_slice(&64u16.to_le_bytes());
bytes[54..56].copy_from_slice(&56u16.to_le_bytes());
bytes[56..58].copy_from_slice(&header_count.to_le_bytes());
let mut segment = |index: usize, kind: u32, offset: usize, size: usize| {
let start = 64 + index * 56;
bytes[start..start + 4].copy_from_slice(&kind.to_le_bytes());
bytes[start + 4..start + 8].copy_from_slice(&4u32.to_le_bytes());
for at in [8, 16, 24] {
bytes[start + at..start + at + 8].copy_from_slice(&(offset as u64).to_le_bytes());
}
for at in [32, 40] {
bytes[start + at..start + at + 8].copy_from_slice(&(size as u64).to_le_bytes());
}
bytes[start + 48..start + 56].copy_from_slice(&1u64.to_le_bytes());
};
segment(0, 1, 0, length);
segment(1, 2, dynamic_offset, dynamic.len() * 16);
if soname.is_none() {
segment(2, 3, interpreter_offset, interpreter.len());
}
bytes[string_offset..interpreter_offset].copy_from_slice(&strings);
bytes[interpreter_offset..interpreter_offset + interpreter.len()].copy_from_slice(interpreter);
for (index, (tag, value)) in dynamic.into_iter().enumerate() {
let start = dynamic_offset + index * 16;
bytes[start..start + 8].copy_from_slice(&tag.to_le_bytes());
bytes[start + 8..start + 16].copy_from_slice(&value.to_le_bytes());
}
bytes
}
@@ -0,0 +1,29 @@
//! Small architecture-bearing link inputs for pack validation tests.
use dever_cli::toolchain::BuildTarget;
pub fn object(target: BuildTarget) -> Vec<u8> {
let mut bytes = vec![0; 64];
bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
bytes[16..18].copy_from_slice(&1_u16.to_le_bytes());
let machine: u16 = match target {
BuildTarget::LinuxX86_64 => 62,
BuildTarget::LinuxAarch64 => 183,
};
bytes[18..20].copy_from_slice(&machine.to_le_bytes());
bytes
}
pub fn archive(target: BuildTarget, member: &str) -> Vec<u8> {
let object = object(target);
let header = format!(
"{:<16}{:<12}{:<6}{:<6}{:<8}{:<10}`\n",
format!("{member}/"),
0,
0,
0,
"644",
object.len()
);
assert_eq!(header.len(), 60);
[b"!<arch>\n".as_slice(), header.as_bytes(), &object].concat()
}
+81
View File
@@ -0,0 +1,81 @@
use std::io::{Cursor, Write};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};
pub fn metadata(name: &str, version: &str, requires: &[&str], extras: &[&str]) -> String {
let mut text = format!("Metadata-Version: 2.4\nName: {name}\nVersion: {version}\n");
for requirement in requires {
text.push_str(&format!("Requires-Dist: {requirement}\n"));
}
for extra in extras {
text.push_str(&format!("Provides-Extra: {extra}\n"));
}
text
}
pub fn pack(
name: &str,
version: &str,
wheel: &str,
metadata: &str,
extra: &[(&str, Vec<u8>)],
) -> Vec<u8> {
let info = format!("{}-{version}.dist-info", name.replace('-', "_"));
let mut files = vec![
(format!("{info}/WHEEL"), wheel.as_bytes().to_vec()),
(format!("{info}/METADATA"), metadata.as_bytes().to_vec()),
];
files.extend(
extra
.iter()
.map(|(path, bytes)| ((*path).to_owned(), bytes.clone())),
);
let mut record = csv::WriterBuilder::new()
.has_headers(false)
.from_writer(Vec::new());
for (path, bytes) in &files {
record
.write_record([
path.clone(),
format!("sha256={}", URL_SAFE_NO_PAD.encode(Sha256::digest(bytes))),
bytes.len().to_string(),
])
.unwrap();
}
let path = format!("{info}/RECORD");
record
.write_record([path.clone(), String::new(), String::new()])
.unwrap();
files.push((path, record.into_inner().unwrap()));
zip(&files)
}
pub fn zip(files: &[(String, Vec<u8>)]) -> Vec<u8> {
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
for (path, bytes) in files {
writer
.start_file(path, zip::write::SimpleFileOptions::default())
.unwrap();
writer.write_all(bytes).unwrap();
}
writer.finish().unwrap().into_inner()
}
pub fn markers() -> pep508_rs::MarkerEnvironment {
pep508_rs::MarkerEnvironmentBuilder {
implementation_name: "cpython",
implementation_version: "3.12.0",
os_name: "posix",
platform_machine: "x86_64",
platform_python_implementation: "CPython",
platform_release: "",
platform_system: "Linux",
platform_version: "",
python_full_version: "3.12.0",
python_version: "3.12",
sys_platform: "linux",
}
.try_into()
.unwrap()
}