feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -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}");
|
||||
}
|
||||
Reference in new issue
Block a user