feat: release Dever 0.1.5 with direct workers and long transactions
This commit is contained in:
1 parent
89629c2197
commit
a900d7e8db
153 files changed
+3676
-3708
No files matched your search
@@ -144,7 +144,7 @@ fn exec_preparation_embeds_every_candidate_with_its_current_bytes() {
|
||||
dever_cli::workers::prepare(root.path(), &program, &input).unwrap()
|
||||
};
|
||||
let resources = prepare();
|
||||
assert_eq!(resources.len(), entries.len() * 2);
|
||||
assert_eq!(resources.len(), entries.len() * 3);
|
||||
for entry in &entries {
|
||||
let manifest = resources
|
||||
.iter()
|
||||
@@ -157,6 +157,11 @@ fn exec_preparation_embeds_every_candidate_with_its_current_bytes() {
|
||||
.unwrap();
|
||||
assert_eq!(worker.bytes, fs::read(root.path().join(entry)).unwrap());
|
||||
assert!(worker.executable);
|
||||
assert!(resources.iter().any(|resource| resource.path
|
||||
== format!(
|
||||
"{}/contract.json",
|
||||
manifest["working_directory"].as_str().unwrap()
|
||||
)));
|
||||
}
|
||||
let mut changed_binary = native_elf::static_executable();
|
||||
changed_binary.extend_from_slice(b"changed");
|
||||
|
||||
@@ -2,8 +2,8 @@ use super::*;
|
||||
use dever_cli::libs::build::{Inputs, Session, pack};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[path = "../../dever-tests/tests/support/sandbox.rs"]
|
||||
pub(super) mod sandbox;
|
||||
#[path = "../../dever-tests/tests/support/process_runtime.rs"]
|
||||
pub(super) mod process_runtime;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -111,12 +111,12 @@ impl Inputs for AuthorInputs {
|
||||
}
|
||||
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
Ok(sandbox::assets(&workspace)
|
||||
Ok(process_runtime::assets(&workspace)
|
||||
.into_iter()
|
||||
.map(|asset| {
|
||||
let _mode = asset.executable;
|
||||
(
|
||||
asset.path.trim_start_matches("sandbox/").into(),
|
||||
asset.path.trim_start_matches("process-runtime/").into(),
|
||||
asset.bytes,
|
||||
)
|
||||
})
|
||||
@@ -195,11 +195,11 @@ pub(super) fn prepare_tool_pack(
|
||||
}
|
||||
files.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
manifest.validate(&files, &descriptor).unwrap();
|
||||
let assets = sandbox::assets(&workspace)
|
||||
let assets = process_runtime::assets(&workspace)
|
||||
.into_iter()
|
||||
.map(|asset| {
|
||||
(
|
||||
asset.path.trim_start_matches("sandbox/").into(),
|
||||
asset.path.trim_start_matches("process-runtime/").into(),
|
||||
asset.bytes,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -338,11 +338,11 @@ impl Inputs for AuthorInputs {
|
||||
}
|
||||
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
Ok(build_tests::sandbox::assets(&workspace)
|
||||
Ok(build_tests::process_runtime::assets(&workspace)
|
||||
.into_iter()
|
||||
.map(|asset| {
|
||||
(
|
||||
asset.path.trim_start_matches("sandbox/").into(),
|
||||
asset.path.trim_start_matches("process-runtime/").into(),
|
||||
asset.bytes,
|
||||
)
|
||||
})
|
||||
@@ -403,7 +403,7 @@ fn resolver<'a>(
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit official npm native source build; network only in registry resolver, never in lifecycle sandbox"]
|
||||
#[ignore = "explicit official npm native source build; network only in registry resolver, never in lifecycle process_runtime"]
|
||||
fn builds_official_npm_addon_with_locked_lifecycle() {
|
||||
let inputs = AuthorInputs::load();
|
||||
let session = Session::new(&inputs);
|
||||
@@ -596,7 +596,7 @@ fn npm_build_output_hard_links_stay_inside_the_exported_tree() {
|
||||
),
|
||||
(
|
||||
"outsidealias",
|
||||
"const fs=require('fs');fs.writeFileSync('/data/build/outside.txt','outside');fs.linkSync('/data/build/outside.txt','alias.txt');",
|
||||
"const fs=require('fs');fs.writeFileSync('../outside.txt','outside');fs.linkSync('../outside.txt','alias.txt');",
|
||||
false,
|
||||
),
|
||||
] {
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Inputs for RuntimeOnly {
|
||||
panic!("archive restoration must not load build extensions")
|
||||
}
|
||||
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
|
||||
panic!("archive restoration must not execute a sandbox")
|
||||
panic!("archive restoration must not execute a process_runtime")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ fn validate_native(
|
||||
) -> Result<(), String> {
|
||||
let wheel_files = files
|
||||
.keys()
|
||||
.filter(|path| !path.starts_with("runtime/") && !path.starts_with("sandbox/"))
|
||||
.filter(|path| !path.starts_with("runtime/") && !path.starts_with("process-runtime/"))
|
||||
.cloned()
|
||||
.collect();
|
||||
dever_cli::workers::python_wheel::validate_native(
|
||||
@@ -38,11 +38,7 @@ fn wheel_console_scripts_use_the_same_private_prefix_and_record_generated_files(
|
||||
.find(|file| file.path == "runtime/bin/sample-tool")
|
||||
.unwrap();
|
||||
assert!(script.executable);
|
||||
assert!(
|
||||
script
|
||||
.bytes
|
||||
.starts_with(b"#!/worker/runtime/bin/python3 -I\n")
|
||||
);
|
||||
assert!(script.bytes.starts_with(b"#!/bin/sh\n"));
|
||||
assert!(String::from_utf8_lossy(&script.bytes).contains("sample.cli"));
|
||||
let record = installed
|
||||
.iter()
|
||||
@@ -280,11 +276,7 @@ fn wheel_installs_all_standard_schemes_and_rewrites_installed_record() {
|
||||
.find(|file| file.path == "runtime/bin/tool")
|
||||
.unwrap();
|
||||
assert!(script.executable);
|
||||
assert!(
|
||||
script
|
||||
.bytes
|
||||
.starts_with(b"#!/worker/runtime/bin/python3 -I\n")
|
||||
);
|
||||
assert!(script.bytes.starts_with(b"#!/bin/sh\n"));
|
||||
let record = files
|
||||
.iter()
|
||||
.find(|file| file.path.ends_with("/RECORD"))
|
||||
@@ -449,7 +441,10 @@ fn native_wheel_checks_target_suffix_and_closed_library_search() {
|
||||
.unwrap_err()
|
||||
.contains("packaged dependency closure")
|
||||
);
|
||||
files.insert("sandbox/lib/libsample.so.1".into(), library.as_slice());
|
||||
files.insert(
|
||||
"process-runtime/lib/libsample.so.1".into(),
|
||||
library.as_slice(),
|
||||
);
|
||||
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
|
||||
let old = files
|
||||
.remove("site-packages/sample/_native.cpython-312-x86_64-linux-gnu.so")
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
reason = "This test crate uses only the shared static ELF fixture"
|
||||
)]
|
||||
mod native_elf;
|
||||
#[path = "../dever-tests/tests/support/sandbox.rs"]
|
||||
mod sandbox;
|
||||
#[path = "../dever-tests/tests/support/process_runtime.rs"]
|
||||
mod process_runtime;
|
||||
#[path = "../dever-tests/tests/support/temp.rs"]
|
||||
mod temp;
|
||||
#[path = "support/wheel.rs"]
|
||||
@@ -32,6 +32,80 @@ use temp::TemporaryDirectory;
|
||||
#[path = "external_workers/command.rs"]
|
||||
mod command;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "external_workers/ffi.rs"]
|
||||
mod ffi;
|
||||
|
||||
#[test]
|
||||
fn native_ffi_workers_package_the_checked_contract_and_recursive_libraries() {
|
||||
let root = TemporaryDirectory::new();
|
||||
let mut sources = SourceMap::default();
|
||||
sources.add(
|
||||
"media/native/port.dever",
|
||||
"triple(value: Int) (output: Int) fails app.NativeError",
|
||||
);
|
||||
sources.add(
|
||||
"media/native/adapter.dever",
|
||||
"external exec \"bin/worker\" {}",
|
||||
);
|
||||
sources.add(
|
||||
"media/native/app.dever",
|
||||
"type NativeError { error Unavailable }\ntriple(value: Int) (output: Int) { output = port.triple(value) }",
|
||||
);
|
||||
sources.add("media/native/api.dever", "cmd triple = app.triple");
|
||||
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
|
||||
let contract = &program.external_worker_contracts()[0];
|
||||
let mut entry = resource(
|
||||
contract.entry.clone(),
|
||||
native_elf::compiler(&["libfixture.so"]),
|
||||
);
|
||||
entry.executable = true;
|
||||
assert!(
|
||||
workers::prepare(root.path(), &program, &[entry.clone()])
|
||||
.unwrap_err()
|
||||
.contains("libfixture.so")
|
||||
);
|
||||
let directory = root
|
||||
.path()
|
||||
.join(&contract.entry)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("lib");
|
||||
fs::create_dir_all(&directory).unwrap();
|
||||
fs::write(
|
||||
directory.join("libfixture.so"),
|
||||
native_elf::library("libfixture.so", &[]),
|
||||
)
|
||||
.unwrap();
|
||||
let mut loader = resource(
|
||||
format!("process-runtime/lib/{}", dever_process::loader_name()),
|
||||
native_elf::static_executable(),
|
||||
);
|
||||
loader.executable = true;
|
||||
let resources = workers::prepare(root.path(), &program, &[entry, loader]).unwrap();
|
||||
let manifest = resources
|
||||
.iter()
|
||||
.find(|file| file.path.ends_with("/contract.json"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&manifest.bytes).unwrap(),
|
||||
contract.sdk_manifest()
|
||||
);
|
||||
assert!(
|
||||
resources
|
||||
.iter()
|
||||
.any(|file| file.path.ends_with("/lib/libfixture.so") && !file.executable)
|
||||
);
|
||||
let launch = resources
|
||||
.iter()
|
||||
.find(|file| file.path.ends_with(".dever-worker.json"))
|
||||
.unwrap();
|
||||
let launch: serde_json::Value = serde_json::from_slice(&launch.bytes).unwrap();
|
||||
assert_eq!(launch["ecosystem"], "exec");
|
||||
assert!(launch["executable"].as_str().unwrap().ends_with("/worker"));
|
||||
assert_eq!(launch["arguments"], json!([]));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
struct OwnedProcessGroup(std::process::Child);
|
||||
|
||||
@@ -93,13 +167,13 @@ fn resource(path: String, bytes: Vec<u8>) -> EmbeddedResource {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_sandboxed_worker(
|
||||
fn prepare_managed_worker(
|
||||
root: &Path,
|
||||
program: &dever_core::hir::Program,
|
||||
mut resources: Vec<EmbeddedResource>,
|
||||
) -> Vec<EmbeddedResource> {
|
||||
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
for asset in sandbox::assets(&workspace) {
|
||||
for asset in process_runtime::assets(&workspace) {
|
||||
let mut file = resource(asset.path, asset.bytes);
|
||||
file.executable = asset.executable;
|
||||
resources.push(file);
|
||||
@@ -108,7 +182,7 @@ fn prepare_sandboxed_worker(
|
||||
generated.extend(
|
||||
resources
|
||||
.into_iter()
|
||||
.filter(|resource| resource.path.starts_with("sandbox/")),
|
||||
.filter(|resource| resource.path.starts_with("process-runtime/")),
|
||||
);
|
||||
generated
|
||||
}
|
||||
@@ -450,7 +524,7 @@ fn cross_target_runtime_rejects_host_binary_even_with_matching_pack_hash() {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
#[ignore = "requires explicit Go runtime pack, official UUID proof/archive and sandbox assets"]
|
||||
#[ignore = "requires explicit Go runtime pack, official UUID proof/archive and process_runtime assets"]
|
||||
fn compiles_go_worker_with_locked_tools_and_runs_without_source_or_go() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
@@ -521,7 +595,7 @@ func Send(_ context.Context, payload any, _ any) (any, error) {
|
||||
assert_eq!(sha(&bytes), artifact.sha256);
|
||||
resources.push(resource(artifact.path.clone(), bytes));
|
||||
}
|
||||
for asset in sandbox::assets(&workspace) {
|
||||
for asset in process_runtime::assets(&workspace) {
|
||||
let mut file = resource(asset.path, asset.bytes);
|
||||
file.executable = asset.executable;
|
||||
resources.push(file);
|
||||
@@ -567,7 +641,7 @@ func Send(_ context.Context, payload any, _ any) (any, error) {
|
||||
for file in generated.iter().chain(
|
||||
resources
|
||||
.iter()
|
||||
.filter(|file| file.path.starts_with("sandbox/")),
|
||||
.filter(|file| file.path.starts_with("process-runtime/")),
|
||||
) {
|
||||
let path = temp.path().join(&file.path);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
@@ -577,14 +651,12 @@ func Send(_ context.Context, payload any, _ any) (any, error) {
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(entry.parent().unwrap()).unwrap();
|
||||
let child = dever_sandbox::Launch {
|
||||
assets: &temp.path().join("sandbox"),
|
||||
let child = dever_process::Launch {
|
||||
assets: &temp.path().join("process-runtime"),
|
||||
worker: &working_directory,
|
||||
executable: &temp.path().join(&binary.path),
|
||||
arguments: &[],
|
||||
working_directory: &working_directory,
|
||||
capabilities: dever_sandbox::Capabilities::default(),
|
||||
grants: &[],
|
||||
}
|
||||
.command()
|
||||
.unwrap()
|
||||
@@ -729,13 +801,13 @@ fn installs_purelib_and_platlib_python_wheels() {
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires the explicit official native-wheel and complete Python runtime fixture at target/python-managed"]
|
||||
fn runs_official_native_python_wheel_in_sandbox() {
|
||||
fn runs_official_native_python_wheel_as_managed_worker() {
|
||||
run_native_python_fixture("python-managed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires explicitly prepared official PEP517 source build at target/source-build"]
|
||||
fn runs_official_python_source_build_in_sandbox() {
|
||||
fn runs_official_python_source_build_as_managed_worker() {
|
||||
run_native_python_fixture("source-build");
|
||||
}
|
||||
|
||||
@@ -820,7 +892,7 @@ fn run_native_python_fixture(fixture: &str) {
|
||||
.collect(),
|
||||
});
|
||||
lock.write_atomic(temp.path()).unwrap();
|
||||
let generated = prepare_sandboxed_worker(temp.path(), &program, resources);
|
||||
let generated = prepare_managed_worker(temp.path(), &program, resources);
|
||||
assert!(
|
||||
generated
|
||||
.iter()
|
||||
@@ -828,14 +900,12 @@ fn run_native_python_fixture(fixture: &str) {
|
||||
&& resource.bytes.starts_with(b"\x7fELF"))
|
||||
);
|
||||
let (deployment, launch) = run_packaged_protocol(&temp, &contract, "pip", &generated);
|
||||
let output = dever_sandbox::Launch {
|
||||
assets: &deployment.join("sandbox"),
|
||||
let output = dever_process::Launch {
|
||||
assets: &deployment.join("process-runtime"),
|
||||
worker: &launch.working_directory,
|
||||
executable: &launch.working_directory.join("runtime/bin/prefix-probe"),
|
||||
arguments: &[],
|
||||
working_directory: &launch.working_directory,
|
||||
capabilities: dever_sandbox::Capabilities::default(),
|
||||
grants: &[],
|
||||
}
|
||||
.command()
|
||||
.unwrap()
|
||||
@@ -877,14 +947,12 @@ fn run_packaged_protocol(
|
||||
ecosystem,
|
||||
)
|
||||
.unwrap();
|
||||
let child = dever_sandbox::Launch {
|
||||
assets: &deployment.join("sandbox"),
|
||||
let child = dever_process::Launch {
|
||||
assets: &deployment.join("process-runtime"),
|
||||
worker: &launch.working_directory,
|
||||
executable: &launch.executable,
|
||||
arguments: &launch.arguments,
|
||||
working_directory: &launch.working_directory,
|
||||
capabilities: dever_sandbox::Capabilities::default(),
|
||||
grants: &[],
|
||||
}
|
||||
.command()
|
||||
.unwrap()
|
||||
@@ -1015,16 +1083,23 @@ fn rejects_python_runtime_without_isolation_flags() {
|
||||
);
|
||||
}
|
||||
|
||||
fn fixture_runtime(name: &str) -> String {
|
||||
let path =
|
||||
std::env::var(name).unwrap_or_else(|_| panic!("set {name} to an absolute runtime path"));
|
||||
assert!(Path::new(&path).is_absolute());
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires the explicit host Node fixture path"]
|
||||
fn runs_javascript_runner_from_explicit_fixture_runtime_path() {
|
||||
run_fixture_runtime("npm", "/root/.nvm/versions/node/v24.15.0/bin/node");
|
||||
run_fixture_runtime("npm", &fixture_runtime("DEVER_TEST_NODE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires the explicit host Python fixture path"]
|
||||
fn runs_python_runner_from_explicit_fixture_runtime_path() {
|
||||
run_fixture_runtime("pip", "/usr/bin/python3.12");
|
||||
run_fixture_runtime("pip", &fixture_runtime("DEVER_TEST_PYTHON"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1042,7 +1117,7 @@ fn binds_commonjs_exports_without_a_registration_table() {
|
||||
&temp,
|
||||
&program,
|
||||
&resources,
|
||||
Some("/root/.nvm/versions/node/v24.15.0/bin/node"),
|
||||
Some(&fixture_runtime("DEVER_TEST_NODE")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1055,7 +1130,7 @@ fn binds_esm_exports_without_a_registration_table() {
|
||||
&temp,
|
||||
&program,
|
||||
&resources,
|
||||
Some("/root/.nvm/versions/node/v24.15.0/bin/node"),
|
||||
Some(&fixture_runtime("DEVER_TEST_NODE")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1070,7 +1145,12 @@ fn imports_packaged_sdk_before_adapter_modules() {
|
||||
"raise RuntimeError('shadowed SDK')\n",
|
||||
)
|
||||
.unwrap();
|
||||
execute_generated_fixture(&temp, &program, &resources, Some("/usr/bin/python3.12"));
|
||||
execute_generated_fixture(
|
||||
&temp,
|
||||
&program,
|
||||
&resources,
|
||||
Some(&fixture_runtime("DEVER_TEST_PYTHON")),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -1080,18 +1160,31 @@ fn compiled_managed_worker_runs_after_source_and_pack_are_removed() {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
let temp = TemporaryDirectory::new();
|
||||
let binary = fs::read("/root/.nvm/versions/node/v24.15.0/bin/node").unwrap();
|
||||
let binary = fs::read(&fixture_runtime("DEVER_TEST_NODE")).unwrap();
|
||||
let (program, resources, sources) =
|
||||
fixture_entry_with_sources(temp.path(), "npm", &binary, None, "worker/main.mjs");
|
||||
let source_entry = program.external_worker_contracts()[0].entry.clone();
|
||||
fs::write(
|
||||
temp.path().join(&source_entry),
|
||||
r#"
|
||||
import { execFileSync } from 'node:child_process';
|
||||
export const send = ({ value }) => {
|
||||
const receipt = Number(execFileSync(process.execPath,
|
||||
['-e', 'process.stdout.write(String(Number(process.argv[1]) + 1))', String(value)],
|
||||
{ encoding: 'utf8', timeout: 3000, maxBuffer: 1024 }));
|
||||
return { receipt };
|
||||
};
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let raw_pack_path = temp.path().join("runtime.pack");
|
||||
fs::write(&raw_pack_path, &resources[0].bytes).unwrap();
|
||||
let raw = resource(resources[0].path.clone(), fs::read(&raw_pack_path).unwrap());
|
||||
let packaged = prepare_sandboxed_worker(temp.path(), &program, vec![raw]);
|
||||
let packaged = prepare_managed_worker(temp.path(), &program, vec![raw]);
|
||||
let native = dever_core::native::compile_project_with_resources(
|
||||
&program,
|
||||
&sources,
|
||||
std::ffi::OsStr::new("/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/rustc"),
|
||||
std::ffi::OsStr::new("rustc"),
|
||||
dever_runtime::config::RuntimeProfile::default(),
|
||||
&packaged,
|
||||
)
|
||||
|
||||
@@ -111,21 +111,16 @@ fn command_preparation_checks_recursive_library_closure_and_loader() {
|
||||
.unwrap_err()
|
||||
.contains("signed target GNU loader")
|
||||
);
|
||||
let loader_name = dever_sandbox::loader_name();
|
||||
let loader_name = dever_process::loader_name();
|
||||
let mut loader = resource(
|
||||
format!("sandbox/lib/{loader_name}"),
|
||||
format!("process-runtime/lib/{loader_name}"),
|
||||
native_elf::static_executable(),
|
||||
);
|
||||
loader.executable = true;
|
||||
let resources = workers::prepare(temp.path(), &program, &[loader]).unwrap();
|
||||
let manifest = command_manifest(&resources);
|
||||
assert!(
|
||||
manifest["executable"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.ends_with("/loader")
|
||||
);
|
||||
assert_eq!(manifest["arguments"][0]["literal"], "--inhibit-cache");
|
||||
assert!(manifest["executable"].as_str().unwrap().ends_with("/tool"));
|
||||
assert_eq!(manifest["arguments"], json!([]));
|
||||
for name in ["libfirst.so", "libsecond.so"] {
|
||||
assert!(resources.iter().any(
|
||||
|resource| resource.path.ends_with(&format!("/lib/{name}")) && !resource.executable
|
||||
@@ -171,13 +166,13 @@ fn command_rejects_host_library_paths_and_signed_library_shadowing() {
|
||||
)
|
||||
.unwrap();
|
||||
let signed = resource(
|
||||
"sandbox/lib/libfirst.so".into(),
|
||||
"process-runtime/lib/libfirst.so".into(),
|
||||
native_elf::library("libfirst.so", &[]),
|
||||
);
|
||||
assert!(
|
||||
workers::prepare(temp.path(), &program, &[signed])
|
||||
.unwrap_err()
|
||||
.contains("overrides a signed sandbox library")
|
||||
.contains("overrides a signed process_runtime library")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
fn sources() -> SourceMap {
|
||||
let mut sources = SourceMap::default();
|
||||
sources.add(
|
||||
"media/native/port.dever",
|
||||
"call(value: Int) (output: Int) fails app.NativeError",
|
||||
);
|
||||
sources.add(
|
||||
"media/native/adapter.dever",
|
||||
"external exec \"bin/worker\" {}",
|
||||
);
|
||||
sources.add(
|
||||
"media/native/app.dever",
|
||||
r#"type NativeError { error Rejected }
|
||||
call(value: Int) (output: Int) { output = port.call(value) }
|
||||
stall() () { output = port.call(-4) }
|
||||
cancel() (output: Int) {
|
||||
output = port.call(7)
|
||||
pending = run(stall())
|
||||
dever.task.sleep(100)
|
||||
stop(pending)
|
||||
}
|
||||
expired() (output: Int) { output = 99 }
|
||||
deadline() (output: Int) {
|
||||
pending = run(call(-4))
|
||||
output = timeout(pending, 100, expired)
|
||||
}"#,
|
||||
);
|
||||
sources.add(
|
||||
"media/native/api.dever",
|
||||
"cmd call = app.call\ncmd cancel = app.cancel\ncmd deadline = app.deadline",
|
||||
);
|
||||
sources
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_c_abi_fixture_checks_in_both_backends() {
|
||||
let sources = sources();
|
||||
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
|
||||
dever_core::native::emit_project(
|
||||
&program,
|
||||
&sources,
|
||||
dever_runtime::config::RuntimeProfile::default(),
|
||||
)
|
||||
.unwrap();
|
||||
dever_core::llvm::emit_application(&program, &sources).unwrap();
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
directory: TemporaryDirectory,
|
||||
worker: PathBuf,
|
||||
executable: PathBuf,
|
||||
hello: serde_json::Value,
|
||||
rejected: String,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn prepare() -> Self {
|
||||
let compiler = PathBuf::from(std::env::var_os("DEVER_FFI_CC").expect("set DEVER_FFI_CC"));
|
||||
let assets = PathBuf::from(
|
||||
std::env::var_os("DEVER_PROCESS_RUNTIME").expect("set DEVER_PROCESS_RUNTIME"),
|
||||
);
|
||||
for path in [&compiler, &assets] {
|
||||
assert!(
|
||||
path.is_absolute(),
|
||||
"fixture inputs must be explicit absolute paths"
|
||||
);
|
||||
}
|
||||
let directory = TemporaryDirectory::new();
|
||||
let sources = sources();
|
||||
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
|
||||
let contract = &program.external_worker_contracts()[0];
|
||||
let rejected = contract.errors.keys().next().unwrap().clone();
|
||||
let worker = directory.path().join(&contract.entry);
|
||||
let libraries = worker.parent().unwrap().join("lib");
|
||||
fs::create_dir_all(&libraries).unwrap();
|
||||
let library_source = directory.path().join("library.c");
|
||||
fs::write(
|
||||
&library_source,
|
||||
r#"#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
long long fixture_call(long long value) {
|
||||
if (value == -3) _exit(73);
|
||||
if (value == -4) {
|
||||
fputs("native-wait\n", stderr);
|
||||
fflush(stderr);
|
||||
sleep(60);
|
||||
}
|
||||
return value * 3;
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let output = Command::new(&compiler)
|
||||
.args([
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-shared",
|
||||
"-fPIC",
|
||||
"-Wl,-soname,libfixture.so",
|
||||
])
|
||||
.arg(&library_source)
|
||||
.arg("-o")
|
||||
.arg(libraries.join("libfixture.so"))
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let adapter_source = directory.path().join("adapter.c");
|
||||
fs::write(&adapter_source, format!(r#"#include "dever_ffi.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
extern long long fixture_call(long long value);
|
||||
static int invoke(void *context, dever_ffi_slice operation, dever_ffi_slice payload, dever_ffi_output *output) {{
|
||||
(void)context;
|
||||
if (operation.size != 4 || memcmp(operation.data, "call", 4) != 0 || payload.size >= 64) return DEVER_FFI_FAULT;
|
||||
char input[64];
|
||||
memcpy(input, payload.data, (size_t)payload.size);
|
||||
input[payload.size] = 0;
|
||||
long long value = 0;
|
||||
if (sscanf(input, "{{\"value\":%lld}}", &value) != 1 || value > 1000000 || value < -1000000) return DEVER_FFI_FAULT;
|
||||
if (value == -1) {{
|
||||
const char *error = "{{\"error\":\"{rejected}\",\"payload\":{{}}}}";
|
||||
memcpy(output->data, error, strlen(error));
|
||||
output->size = strlen(error);
|
||||
return DEVER_FFI_ERROR;
|
||||
}}
|
||||
if (value == -2) return DEVER_FFI_FAULT;
|
||||
int size = snprintf(output->data, (size_t)output->capacity, "{{\"output\":%lld}}", fixture_call(value));
|
||||
if (size < 0 || (uint64_t)size >= output->capacity) return DEVER_FFI_FAULT;
|
||||
output->size = (uint64_t)size;
|
||||
return DEVER_FFI_OK;
|
||||
}}
|
||||
static void closed(void *context) {{ (void)context; fputs("native-closed\n", stderr); }}
|
||||
int main(void) {{
|
||||
const dever_ffi_handler handler = {{DEVER_FFI_ABI_V1, NULL, NULL, invoke, closed}};
|
||||
return dever_ffi_serve(&handler);
|
||||
}}
|
||||
"#)).unwrap();
|
||||
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let output = Command::new(&compiler)
|
||||
.args(["-std=c11", "-Wall", "-Wextra", "-Werror", "-I"])
|
||||
.arg(workspace.join("sdk/c"))
|
||||
.arg(workspace.join("sdk/c/dever_ffi.c"))
|
||||
.arg(&adapter_source)
|
||||
.arg("-L")
|
||||
.arg(&libraries)
|
||||
.args(["-lfixture", "-Wl,-rpath,$ORIGIN/lib", "-o"])
|
||||
.arg(&worker)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let mut supplied = Vec::new();
|
||||
for subdirectory in ["lib"] {
|
||||
for entry in fs::read_dir(assets.join(subdirectory)).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
assert!(entry.file_type().unwrap().is_file());
|
||||
let name = entry.file_name().into_string().unwrap();
|
||||
let path = format!("process-runtime/{subdirectory}/{name}");
|
||||
let input = entry.path();
|
||||
let bytes = fs::read(input).unwrap();
|
||||
supplied.push(EmbeddedResource {
|
||||
sha256: sha(&bytes),
|
||||
executable: dever_process::asset_executable(
|
||||
path.strip_prefix("process-runtime/").unwrap(),
|
||||
),
|
||||
path,
|
||||
bytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut entry = resource(contract.entry.clone(), fs::read(&worker).unwrap());
|
||||
entry.executable = true;
|
||||
supplied.push(entry);
|
||||
let mut packaged = workers::prepare(directory.path(), &program, &supplied).unwrap();
|
||||
let manifest = packaged
|
||||
.iter()
|
||||
.find(|file| file.path.ends_with("/contract.json"))
|
||||
.unwrap();
|
||||
fs::write(
|
||||
worker.parent().unwrap().join("contract.json"),
|
||||
&manifest.bytes,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&manifest.bytes).unwrap(),
|
||||
contract.sdk_manifest()
|
||||
);
|
||||
packaged.extend(
|
||||
supplied
|
||||
.into_iter()
|
||||
.filter(|file| file.path.starts_with("process-runtime/")),
|
||||
);
|
||||
let native = dever_core::native::compile_project_with_resources(
|
||||
&program,
|
||||
&sources,
|
||||
std::ffi::OsStr::new("rustc"),
|
||||
dever_runtime::config::RuntimeProfile::default(),
|
||||
&packaged,
|
||||
)
|
||||
.unwrap();
|
||||
let executable = directory.path().join("application");
|
||||
native.save(&executable).unwrap();
|
||||
fs::create_dir(directory.path().join("config")).unwrap();
|
||||
fs::write(
|
||||
directory.path().join("config/setting.json"),
|
||||
r#"{"adapter":{"media.native":{"use":"default"}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
Self {
|
||||
directory,
|
||||
worker,
|
||||
executable,
|
||||
rejected,
|
||||
hello: json!({"kind":"hello", "version":"dever-component-1", "port":contract.port,
|
||||
"adapter":contract.adapter, "schema":contract.schema, "capabilities":contract.capabilities,
|
||||
"operations":contract.operations, "setting":{}}),
|
||||
}
|
||||
}
|
||||
|
||||
fn capture(
|
||||
&self,
|
||||
command: &mut Command,
|
||||
input: &[u8],
|
||||
limit: Duration,
|
||||
) -> (bool, Vec<u8>, String) {
|
||||
let stdout = self.directory.path().join("stdout");
|
||||
let stderr = self.directory.path().join("stderr");
|
||||
let child = command
|
||||
.env_clear()
|
||||
.process_group(0)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(fs::File::create(&stdout).unwrap())
|
||||
.stderr(fs::File::create(&stderr).unwrap())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut owned = OwnedProcessGroup(child);
|
||||
owned.0.stdin.take().unwrap().write_all(input).unwrap();
|
||||
let mut descendants = Descendants::default();
|
||||
let deadline = Instant::now() + limit;
|
||||
let status = loop {
|
||||
descendants.observe(owned.0.id());
|
||||
if let Some(status) = owned.0.try_wait().unwrap() {
|
||||
break status;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"fixture exceeded its bounded process deadline"
|
||||
);
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
};
|
||||
let cleanup_deadline = Instant::now() + Duration::from_secs(3);
|
||||
loop {
|
||||
let alive = descendants
|
||||
.0
|
||||
.iter()
|
||||
.any(|(pid, started)| process_start(*pid) == Some(*started));
|
||||
if !alive {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < cleanup_deadline,
|
||||
"native Worker descendants were not reaped: {descendants:?}"
|
||||
);
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
(
|
||||
status.success(),
|
||||
fs::read(stdout).unwrap(),
|
||||
fs::read_to_string(stderr).unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Descendants(BTreeMap<u32, u64>);
|
||||
|
||||
impl Descendants {
|
||||
fn observe(&mut self, parent: u32) {
|
||||
let mut pending = vec![parent];
|
||||
pending.extend(
|
||||
self.0.iter().filter_map(|(pid, started)| {
|
||||
(process_start(*pid) == Some(*started)).then_some(*pid)
|
||||
}),
|
||||
);
|
||||
while let Some(parent) = pending.pop() {
|
||||
let children = fs::read_to_string(format!("/proc/{parent}/task/{parent}/children"))
|
||||
.unwrap_or_default();
|
||||
for child in children
|
||||
.split_whitespace()
|
||||
.map(|pid| pid.parse::<u32>().unwrap())
|
||||
{
|
||||
if let Some(started) = process_start(child)
|
||||
&& self.0.insert(child, started).is_none()
|
||||
{
|
||||
pending.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Descendants {
|
||||
fn drop(&mut self) {
|
||||
for (pid, started) in &self.0 {
|
||||
if process_start(*pid) == Some(*started)
|
||||
&& let Some(pid) = rustix::process::Pid::from_raw(*pid as i32)
|
||||
{
|
||||
let _ = rustix::process::kill_process(pid, rustix::process::Signal::KILL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_start(pid: u32) -> Option<u64> {
|
||||
let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
|
||||
stat.rsplit_once(") ")?
|
||||
.1
|
||||
.split_whitespace()
|
||||
.nth(19)?
|
||||
.parse()
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires explicit DEVER_FFI_CC, DEVER_PROCESS_RUNTIME inputs"]
|
||||
fn real_c_abi_packages_compiles_and_checks_the_generated_protocol() {
|
||||
let fixture = Fixture::prepare();
|
||||
for value in [7, -1, -2, -3] {
|
||||
let input = [
|
||||
frame(fixture.hello.clone()),
|
||||
frame(json!({"kind":"call", "id":1, "operation":"call", "payload":{"value":value}})),
|
||||
frame(json!({"kind":"shutdown"})),
|
||||
]
|
||||
.concat();
|
||||
let (success, stdout, stderr) = fixture.capture(
|
||||
Command::new(&fixture.worker).current_dir(fixture.worker.parent().unwrap()),
|
||||
&input,
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
let mut frames = stdout.as_slice();
|
||||
assert_eq!(response(&mut frames).unwrap()["kind"], "ready", "{stderr}");
|
||||
if value >= -1 {
|
||||
assert!(success, "{stderr}");
|
||||
let reply = response(&mut frames).unwrap();
|
||||
if value == 7 {
|
||||
assert_eq!(reply["payload"]["output"], 21);
|
||||
} else {
|
||||
assert_eq!(reply["error"], fixture.rejected);
|
||||
}
|
||||
assert_eq!(response(&mut frames).unwrap()["kind"], "shutdown");
|
||||
assert!(stderr.contains("native-closed"));
|
||||
} else {
|
||||
assert!(!success);
|
||||
assert!(frames.is_empty());
|
||||
assert_eq!(stderr.contains("native-closed"), value == -2);
|
||||
}
|
||||
assert!(frames.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires explicit DEVER_FFI_CC and DEVER_PROCESS_RUNTIME inputs"]
|
||||
fn real_c_abi_direct_worker_errors_deadlines_and_cancellation() {
|
||||
let fixture = Fixture::prepare();
|
||||
fs::remove_dir_all(fixture.worker.parent().unwrap()).unwrap();
|
||||
for (entry, input, expected) in [
|
||||
("call", "{\"value\":7}", Some(21)),
|
||||
("call", "{\"value\":-1}", None),
|
||||
("call", "{\"value\":-2}", None),
|
||||
("call", "{\"value\":-3}", None),
|
||||
("call", "{\"value\":-4}", None),
|
||||
("cancel", "{}", None),
|
||||
("deadline", "{}", None),
|
||||
] {
|
||||
let (success, stdout, stderr) = fixture.capture(
|
||||
Command::new(&fixture.executable)
|
||||
.current_dir(fixture.directory.path())
|
||||
.args([&format!("media.native.{entry}"), input]),
|
||||
&[],
|
||||
Duration::from_secs(45),
|
||||
);
|
||||
if let Some(expected) = expected {
|
||||
assert!(success, "{entry} {input}: {stderr}");
|
||||
let reply: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
|
||||
assert_eq!(reply["code"], 0, "{reply}; {stderr}");
|
||||
assert_eq!(reply["data"], expected, "{reply}; {stderr}");
|
||||
} else {
|
||||
assert!(!success, "{entry} {input}: {stderr}");
|
||||
if input.contains("-1") {
|
||||
assert!(stderr.contains("Rejected"), "{stderr}");
|
||||
}
|
||||
if entry != "call" || input.contains("-4") {
|
||||
assert!(stderr.contains("timed out"), "{stderr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ exports.send = async payload => {
|
||||
assert_eq!(sha(&bytes), artifact.sha256);
|
||||
resources.push(resource(artifact.path.clone(), bytes));
|
||||
}
|
||||
for asset in sandbox::assets(&workspace) {
|
||||
for asset in process_runtime::assets(&workspace) {
|
||||
let mut file = resource(asset.path, asset.bytes);
|
||||
file.executable = asset.executable;
|
||||
resources.push(file);
|
||||
@@ -112,7 +112,7 @@ exports.send = async payload => {
|
||||
generated.extend(
|
||||
resources
|
||||
.into_iter()
|
||||
.filter(|file| file.path.starts_with("sandbox/")),
|
||||
.filter(|file| file.path.starts_with("process-runtime/")),
|
||||
);
|
||||
fs::remove_file(entry).unwrap();
|
||||
run_packaged_protocol(&temp, &contract, "npm", &generated);
|
||||
|
||||
@@ -38,42 +38,31 @@ mod native_objects;
|
||||
mod targets;
|
||||
|
||||
#[test]
|
||||
fn sandbox_bootstrap_requires_static_elf_and_complete_guard_libraries() {
|
||||
let loader = format!("lib/{}", dever_sandbox::loader_name());
|
||||
fn process_runtime_requires_complete_loader_libraries_and_rejects_helpers() {
|
||||
let loader = dever_process::loader_name();
|
||||
let mut files = vec![
|
||||
("bin/bwrap".into(), native_elf::static_executable()),
|
||||
(
|
||||
"bin/guard".into(),
|
||||
native_elf::elf(None, &["libc.so.6"], None),
|
||||
),
|
||||
(
|
||||
loader,
|
||||
native_elf::library(dever_sandbox::loader_name(), &[]),
|
||||
format!("lib/{loader}"),
|
||||
native_elf::library(loader, &["libc.so.6"]),
|
||||
),
|
||||
(
|
||||
"lib/libc.so.6".into(),
|
||||
native_elf::library("libc.so.6", &[]),
|
||||
),
|
||||
];
|
||||
dever_sandbox::validate_assets(&files).unwrap();
|
||||
files[0].1 = native_elf::elf(None, &[], None);
|
||||
dever_process::validate_assets(&files).unwrap();
|
||||
files.push(("bin/bwrap".into(), native_elf::static_executable()));
|
||||
assert!(
|
||||
dever_sandbox::validate_assets(&files)
|
||||
dever_process::validate_assets(&files)
|
||||
.unwrap_err()
|
||||
.contains("static target ELF")
|
||||
.contains("invalid")
|
||||
);
|
||||
files[0].1 = native_elf::library("bwrap", &["libc.so.6"]);
|
||||
assert!(
|
||||
dever_sandbox::validate_assets(&files)
|
||||
.unwrap_err()
|
||||
.contains("static target ELF")
|
||||
);
|
||||
files[0].1 = native_elf::static_executable();
|
||||
files.pop();
|
||||
files.pop();
|
||||
assert!(
|
||||
dever_sandbox::validate_assets(&files)
|
||||
dever_process::validate_assets(&files)
|
||||
.unwrap_err()
|
||||
.contains("sandbox dependency 'lib/libc.so.6' is missing")
|
||||
.contains("lib/libc.so.6")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -354,6 +343,38 @@ fn compiler_libraries_must_form_a_closed_target_and_search_path() {
|
||||
assert!(author.reject().contains("inherited RPATH"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_libraries_follow_the_linked_soname_not_a_distribution_alias() {
|
||||
let mut author = Author::new();
|
||||
let name = "libLLVM-18.so.18.1";
|
||||
fs::write(
|
||||
author.root.join("inputs/core"),
|
||||
native_elf::compiler(&[name, "libc.so.6"]),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
author.root.join("inputs/llvm"),
|
||||
native_elf::library(name, &["libc.so.6"]),
|
||||
)
|
||||
.unwrap();
|
||||
author.settings["core"]["sha256"] =
|
||||
json!(sha256_file(&author.root.join("inputs/core")).unwrap());
|
||||
author.settings["core_libraries"][0]["path"] = json!(name);
|
||||
author.settings["core_libraries"][0]["sha256"] =
|
||||
json!(sha256_file(&author.root.join("inputs/llvm")).unwrap());
|
||||
author.save();
|
||||
let output = author.output("linked-soname");
|
||||
let manifest = packaging::create(&author.root, &output).unwrap();
|
||||
assert!(
|
||||
manifest
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| artifact.path == format!("lib/{name}"))
|
||||
);
|
||||
assert!(!output.join("lib/libLLVM.so.18.1").exists());
|
||||
assert_signed(&author, &output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installation_rechecks_the_signed_native_compiler_closure_before_health_execution() {
|
||||
let author = Author::new();
|
||||
|
||||
@@ -17,8 +17,8 @@ mod daemon;
|
||||
mod inputs;
|
||||
#[path = "../../dever-tests/tests/support/process.rs"]
|
||||
mod process;
|
||||
#[path = "../../dever-tests/tests/support/sandbox.rs"]
|
||||
mod sandbox;
|
||||
#[path = "../../dever-tests/tests/support/process_runtime.rs"]
|
||||
mod process_runtime;
|
||||
use super::temp::TemporaryDirectory;
|
||||
|
||||
#[path = "ecosystem_acceptance.rs"]
|
||||
@@ -147,11 +147,15 @@ fn author_inputs(author: &Path, workspace: &Path) -> Ed25519KeyPair {
|
||||
),
|
||||
);
|
||||
}
|
||||
let compiler = input(author, Path::new(env!("CARGO_BIN_EXE_dever")), "dever");
|
||||
let compiler = input(
|
||||
author,
|
||||
&workspace.join("target/native-release-inputs/bin/dever"),
|
||||
"dever",
|
||||
);
|
||||
let core_libraries = inputs::core_libraries()
|
||||
.into_iter()
|
||||
.map(|(name, source)| {
|
||||
let mut value = input(author, &source, name);
|
||||
let mut value = input(author, &source, &name);
|
||||
value["path"] = json!(name);
|
||||
value
|
||||
})
|
||||
@@ -171,11 +175,11 @@ fn author_inputs(author: &Path, workspace: &Path) -> Ed25519KeyPair {
|
||||
.map(|name| links[*name].clone())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let sandbox = sandbox::assets(workspace).into_iter().map(|asset| {
|
||||
let process_runtime = process_runtime::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()})
|
||||
json!({"source":relative, "path":asset.path.strip_prefix("process-runtime/").unwrap(), "sha256":sha256_file(&author.join(&relative)).unwrap()})
|
||||
}).collect::<Vec<_>>();
|
||||
write(
|
||||
author,
|
||||
@@ -193,7 +197,7 @@ fn author_inputs(author: &Path, workspace: &Path) -> Ed25519KeyPair {
|
||||
"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
|
||||
"end":ordered(&["crtend.o","crtn.o"]), "process_runtime":process_runtime
|
||||
}))
|
||||
.unwrap(),
|
||||
);
|
||||
@@ -211,10 +215,11 @@ fn isolated_os_command(root: &Path) -> 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 isolator = PathBuf::from(
|
||||
std::env::var_os("DEVER_TEST_ISOLATOR")
|
||||
.expect("set an absolute test-only namespace isolator path"),
|
||||
);
|
||||
assert!(isolator.is_absolute());
|
||||
let init = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../target/debug/examples/native-acceptance-init")
|
||||
.canonicalize()
|
||||
@@ -222,10 +227,7 @@ fn isolated_os_namespace(root: &Path) -> Command {
|
||||
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"));
|
||||
let mut command = Command::new(isolator);
|
||||
command
|
||||
.env_clear()
|
||||
.args([
|
||||
@@ -301,6 +303,33 @@ fn application(root: &Path, profile: &str) {
|
||||
"exercise() (okay: Bool) { dever.task.sleep(1)\n okay = true }\n"
|
||||
};
|
||||
write(root, "module/sample/value/app.dever", app);
|
||||
if matches!(profile, "sqlite" | "both") {
|
||||
write(
|
||||
root,
|
||||
"module/sample/value/api.dever",
|
||||
"cmd exercise = app.exercise\nlong cmd stages = app.stages\n",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/sample/value/app.dever",
|
||||
format!(
|
||||
"{app}\nstages() (okay: Bool) {{ id = store()\n dever.task.sleep(1)\n okay = finish(id) }}\ntransaction store() (id: model.id) {{ saved = model.create({{ name = \"stage\" }})\n id = saved.id }}\ntransaction finish(id: model.id) (okay: Bool) {{ count = model.count()\n removed = model.delete(id)\n okay = count == 1 and removed == 1 }}\n"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_profile_projects_have_valid_source_contracts() {
|
||||
for profile in PROFILES {
|
||||
let project = TemporaryDirectory::new();
|
||||
application(project.path(), profile);
|
||||
command(
|
||||
Command::new(env!("CARGO_BIN_EXE_dever"))
|
||||
.arg("check")
|
||||
.arg(project.path()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_response(output: Output) {
|
||||
@@ -489,7 +518,7 @@ fn signed_core_builds_without_host_compiler_libraries_or_tools() {
|
||||
#[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 workspace = inputs::workspace();
|
||||
let profiles = archive_report(&workspace);
|
||||
let author = TemporaryDirectory::new();
|
||||
let key = author_inputs(author.path(), &workspace);
|
||||
@@ -505,7 +534,7 @@ fn optimized_profiles_make_reproducible_signed_release_and_run_through_daemon()
|
||||
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"))
|
||||
Command::new(workspace.join("target/native-release-inputs/bin/native-release"))
|
||||
.arg(author.path())
|
||||
.arg("--output")
|
||||
.arg(&release),
|
||||
@@ -523,10 +552,13 @@ fn optimized_profiles_make_reproducible_signed_release_and_run_through_daemon()
|
||||
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 mut daemon = daemon::TestDaemon::start_with_executable(
|
||||
&layout,
|
||||
&workspace.join("target/native-release-inputs/bin/deverd"),
|
||||
);
|
||||
let projects = TemporaryDirectory::new();
|
||||
let mut binaries = serde_json::Map::new();
|
||||
let mut programs: Vec<(PathBuf, PathBuf)> = Vec::new();
|
||||
let mut programs = Vec::new();
|
||||
for profile in PROFILES {
|
||||
let project = projects.path().join(profile);
|
||||
application(&project, profile);
|
||||
@@ -538,6 +570,15 @@ fn optimized_profiles_make_reproducible_signed_release_and_run_through_daemon()
|
||||
"{}",
|
||||
]),
|
||||
));
|
||||
if matches!(profile, "sqlite" | "both") {
|
||||
assert_response(command(
|
||||
Command::new(&compiler).arg("run").arg(&project).args([
|
||||
"--",
|
||||
"sample.value.stages",
|
||||
"{}",
|
||||
]),
|
||||
));
|
||||
}
|
||||
let executable = project.join("program");
|
||||
command(
|
||||
Command::new(&compiler)
|
||||
@@ -547,7 +588,7 @@ fn optimized_profiles_make_reproducible_signed_release_and_run_through_daemon()
|
||||
.arg(&executable),
|
||||
);
|
||||
binaries.insert(profile.into(), json!({"bytes":fs::metadata(&executable).unwrap().len(),"sha256":sha256_file(&executable).unwrap()}));
|
||||
programs.push((project, executable));
|
||||
programs.push((profile, project, executable));
|
||||
}
|
||||
assert_eq!(cache_status(&layout).unwrap().entries, 4);
|
||||
let exec_project = TemporaryDirectory::new();
|
||||
@@ -595,19 +636,25 @@ fn optimized_profiles_make_reproducible_signed_release_and_run_through_daemon()
|
||||
"managed_run": true, "standalone_run": true,
|
||||
"source_project_removed": true, "response": 7
|
||||
});
|
||||
for (project, executable) in programs {
|
||||
for (profile, project, executable) in programs {
|
||||
fs::remove_dir_all(project.join("module")).unwrap();
|
||||
assert_response(command(
|
||||
Command::new(executable).args(["sample.value.exercise", "{}"]),
|
||||
Command::new(&executable).args(["sample.value.exercise", "{}"]),
|
||||
));
|
||||
if matches!(profile, "sqlite" | "both") {
|
||||
assert_response(command(
|
||||
Command::new(executable).args(["sample.value.stages", "{}"]),
|
||||
));
|
||||
}
|
||||
}
|
||||
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},
|
||||
"checks":{"pack_determinism":true,"signed_install":true,"managed_profiles":4,"standalone_profiles":4,"sqlite_crud":true,"long_staged_transactions":true,"signed_exec_worker":true},
|
||||
"postgres_server":"not_run", "cross_host_source_reproducibility":"not_run"
|
||||
});
|
||||
retain_candidate(&layout, &version, "native", &report);
|
||||
use sha2::Digest;
|
||||
write(
|
||||
&workspace,
|
||||
@@ -615,3 +662,45 @@ fn optimized_profiles_make_reproducible_signed_release_and_run_through_daemon()
|
||||
serde_json::to_vec_pretty(&report).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn retain_candidate(layout: &Layout, version: &Version, name: &str, report: &Value) {
|
||||
let Some(destination) = std::env::var_os("DEVER_NATIVE_ACCEPTANCE_OUTPUT") else {
|
||||
return;
|
||||
};
|
||||
let destination = PathBuf::from(destination);
|
||||
assert!(destination.is_absolute());
|
||||
fs::create_dir_all(&destination).unwrap();
|
||||
let destination = destination.join(name);
|
||||
fs::create_dir(&destination).unwrap();
|
||||
let payload = destination.join("release");
|
||||
let resources = dever_cli::toolchain::SignedResources::load(layout, version).unwrap();
|
||||
let manifest = resources.manifest();
|
||||
for artifact in manifest.artifacts.iter().chain(
|
||||
manifest
|
||||
.extensions
|
||||
.iter()
|
||||
.flat_map(|extension| &extension.artifacts),
|
||||
) {
|
||||
let output = payload.join(&artifact.path);
|
||||
fs::create_dir_all(output.parent().unwrap()).unwrap();
|
||||
fs::copy(resources.path(&artifact.path).unwrap(), &output).unwrap();
|
||||
assert_eq!(sha256_file(&output).unwrap(), artifact.sha256);
|
||||
}
|
||||
for file in ["manifest.json", "manifest.sig"] {
|
||||
fs::copy(
|
||||
layout.versions().join(version.as_str()).join(file),
|
||||
payload.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
fs::copy(
|
||||
layout.state().join("trusted-release-key"),
|
||||
destination.join("test-release.pub"),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
destination.join("verification.json"),
|
||||
serde_json::to_vec_pretty(report).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -64,7 +64,6 @@ fn bootstrap_payload_has_a_separate_signed_complete_loader_closure() {
|
||||
"bootstrap/deverd",
|
||||
"bootstrap/lib/libgcc_s.so.1",
|
||||
"bootstrap/deverd.service",
|
||||
"bootstrap/dever-sandbox",
|
||||
] {
|
||||
let artifact = manifest
|
||||
.artifacts
|
||||
@@ -78,9 +77,12 @@ fn bootstrap_payload_has_a_separate_signed_complete_loader_closure() {
|
||||
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
|
||||
assert!(!release.join("bootstrap/dever-process").exists());
|
||||
assert!(
|
||||
!manifest
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| artifact.path.contains("bwrap") || artifact.path.contains("apparmor"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,6 +176,73 @@ fn signed_installed_versions_keep_their_original_bootstrap_service_template() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_historical_bootstrap_profiles_are_read_only_and_version_bound() {
|
||||
use dever_cli::toolchain::{Artifact, Layout, MachineManager, Version};
|
||||
for release_version in ["0.1.4", "0.1.5"] {
|
||||
let mut author = Author::new();
|
||||
author.settings["version"] = json!(release_version);
|
||||
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(release_version).unwrap();
|
||||
let installed = layout.versions().join(release_version);
|
||||
let mut manifest = packaging::create(&author.root, &installed).unwrap();
|
||||
let profile = installed.join("bootstrap/dever-sandbox");
|
||||
fs::write(&profile, b"historical profile fixture\n").unwrap();
|
||||
manifest.artifacts.push(Artifact {
|
||||
path: "bootstrap/dever-sandbox".into(),
|
||||
bytes: fs::metadata(&profile).unwrap().len(),
|
||||
sha256: sha256_file(&profile).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);
|
||||
if release_version == "0.1.4" {
|
||||
assert_eq!(
|
||||
manager.resolve_core(&version).unwrap(),
|
||||
installed.join("dever-core")
|
||||
);
|
||||
fs::write(profile, b"unsigned change").unwrap();
|
||||
assert!(
|
||||
manager
|
||||
.resolve_core(&version)
|
||||
.unwrap_err()
|
||||
.contains("integrity verification")
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
manager
|
||||
.resolve_core(&version)
|
||||
.unwrap_err()
|
||||
.contains("unsupported artifact")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "root-only installer trust rejection; never touches the actual machine installation"]
|
||||
fn bootstrap_installer_rejects_untrusted_inputs_before_publication() {
|
||||
@@ -313,7 +382,6 @@ fn bootstrap_recovery_is_idempotent_and_rejects_unrecorded_targets() {
|
||||
for directory in [
|
||||
"usr/local/bin",
|
||||
"etc/systemd/system/multi-user.target.wants",
|
||||
"etc/apparmor.d",
|
||||
] {
|
||||
fs::create_dir_all(root.join(directory)).unwrap();
|
||||
}
|
||||
@@ -331,20 +399,12 @@ fn bootstrap_recovery_is_idempotent_and_rejects_unrecorded_targets() {
|
||||
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();
|
||||
@@ -374,8 +434,6 @@ fn bootstrap_recovery_is_idempotent_and_rejects_unrecorded_targets() {
|
||||
// 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"),
|
||||
|
||||
@@ -212,24 +212,13 @@ fn signed_bootstrap_installs_and_serves_two_projects_without_host_libraries() {
|
||||
.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
|
||||
assert!(!image.path().join("etc/apparmor.d/dever-process").exists());
|
||||
assert!(
|
||||
!manifest
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| artifact.path.ends_with("/bin/bwrap")
|
||||
|| artifact.path.ends_with("/bin/guard"))
|
||||
);
|
||||
let launcher = layout.bin().join("dever");
|
||||
let executable = layout.bin().join("deverd");
|
||||
@@ -305,38 +294,6 @@ fn signed_bootstrap_installs_and_serves_two_projects_without_host_libraries() {
|
||||
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(
|
||||
@@ -345,15 +302,6 @@ fn signed_bootstrap_installs_and_serves_two_projects_without_host_libraries() {
|
||||
.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"),
|
||||
|
||||
@@ -98,23 +98,11 @@ fn packaged_project(root: &Path, source: &Path, layout: &Layout) {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires explicit current runtime archive, author C/LLVM inputs and owned daemon/sandbox"]
|
||||
#[ignore = "requires explicit current runtime archive, author C/LLVM inputs and owned daemon/process_runtime"]
|
||||
fn signed_cli_runs_and_packages_ordinary_command_with_private_library() {
|
||||
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let workspace = inputs::workspace();
|
||||
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();
|
||||
@@ -123,7 +111,10 @@ fn signed_cli_runs_and_packages_ordinary_command_with_private_library() {
|
||||
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 mut daemon = daemon::TestDaemon::start_with_executable(
|
||||
&layout,
|
||||
&workspace.join("target/native-release-inputs/bin/deverd"),
|
||||
);
|
||||
let source = TemporaryDirectory::new();
|
||||
project(source.path(), &workspace);
|
||||
command(Command::new(&compiler).arg("check").arg(source.path()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Explicit x86 compiler -> ARM application acceptance; QEMU user mode does
|
||||
//! not establish the namespace/seccomp contract of external Workers.
|
||||
//! not establish external Worker lifecycle behavior on a real target kernel.
|
||||
use super::*;
|
||||
use dever_cli::toolchain::BuildTarget;
|
||||
use dever_cli::toolchain::compilation::{CompileKind, CompileRequest};
|
||||
@@ -281,7 +281,7 @@ fn signed_host_compiler_builds_arm_profiles_and_runs_standalone_under_qemu() {
|
||||
"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"
|
||||
"postgres_server":"not_run", "worker_execution":"requires_arm_kernel_system_emulation"
|
||||
});
|
||||
write(
|
||||
&workspace,
|
||||
|
||||
@@ -15,8 +15,8 @@ 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");
|
||||
fn add_target_process_runtime(author: &Path, workspace: &Path) {
|
||||
let root = workspace.join("target/arm64-cross/process_runtime");
|
||||
let mut files = Vec::new();
|
||||
let mut inputs = Vec::new();
|
||||
for directory in ["bin", "lib"] {
|
||||
@@ -25,16 +25,16 @@ fn add_target_sandbox(author: &Path, workspace: &Path) {
|
||||
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}");
|
||||
let source = format!("inputs/arm-process-runtime/{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();
|
||||
dever_process::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});
|
||||
settings["process_runtime_targets"] = json!({"linux-aarch64":inputs});
|
||||
fs::write(path, serde_json::to_vec(&settings).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ fn prepare_locked_application(
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires explicit ARM runtimes/dependencies/sandbox and native archives; builds with an owned host daemon and exports apps for ARM system QEMU"]
|
||||
#[ignore = "requires explicit ARM runtimes/dependencies/process_runtime 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");
|
||||
}
|
||||
@@ -294,7 +294,7 @@ fn build_guest_apps(ecosystems: &[&str], report: &str) {
|
||||
let author = TemporaryDirectory::new();
|
||||
let key = author_inputs(author.path(), &workspace);
|
||||
add_arm_inputs(author.path(), &workspace);
|
||||
add_target_sandbox(author.path(), &workspace);
|
||||
add_target_process_runtime(author.path(), &workspace);
|
||||
let machine = machine_temporary_directory(&workspace);
|
||||
let layout = Layout::new(machine.path());
|
||||
layout.initialize().unwrap();
|
||||
@@ -349,6 +349,6 @@ fn build_guest_apps(ecosystems: &[&str], report: &str) {
|
||||
"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"
|
||||
"guest_execution":"not_run", "worker_execution":"requires ARM system guest verification"
|
||||
})).unwrap());
|
||||
}
|
||||
@@ -204,6 +204,115 @@ fn ecosystem_inputs(author: &Path, workspace: &Path) {
|
||||
fs::write(config, serde_json::to_vec(&settings).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires explicit native and zero-Lib ecosystem packs; uses an owned root installation and daemon"]
|
||||
fn signed_zero_lib_ecosystems_run_and_build_in_the_current_container() {
|
||||
let workspace = inputs::workspace();
|
||||
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());
|
||||
packaging::create(author.path(), &release).unwrap();
|
||||
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_with_executable(
|
||||
&layout,
|
||||
&workspace.join("target/native-release-inputs/bin/deverd"),
|
||||
);
|
||||
let deployments = TemporaryDirectory::new();
|
||||
let mut programs = Vec::new();
|
||||
for ecosystem in ECOSYSTEMS {
|
||||
let project = TemporaryDirectory::new();
|
||||
let (extension, source) = match ecosystem {
|
||||
"pip" => (
|
||||
"py",
|
||||
"import gzip\nasync def probe(payload, setting):\n return {\"okay\": gzip.decompress(gzip.compress(b'dever')) == b'dever'}\n",
|
||||
),
|
||||
"npm" => (
|
||||
"mjs",
|
||||
"import {createHash} from 'node:crypto';\nexport async function probe() { return {okay:createHash('sha256').update('dever').digest().length === 32}; }\n",
|
||||
),
|
||||
"go" => ("go", include_str!("../../ecosystem-release/probe.go")),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
write(project.path(), "config/setting.json", "{}");
|
||||
write(
|
||||
project.path(),
|
||||
"module/sample/worker/port.dever",
|
||||
"probe() (okay: Bool) fails app.ProbeResult\n",
|
||||
);
|
||||
write(
|
||||
project.path(),
|
||||
"module/sample/worker/app.dever",
|
||||
"type ProbeResult { error Unavailable(message: Text) }\nprobe() (okay: Bool) { okay = port.probe() }\n",
|
||||
);
|
||||
write(
|
||||
project.path(),
|
||||
"module/sample/worker/api.dever",
|
||||
"cmd probe = app.probe\n",
|
||||
);
|
||||
write(
|
||||
project.path(),
|
||||
"module/sample/worker/adapter.dever",
|
||||
format!("external {ecosystem} \"worker/probe.{extension}\" {{}}\n"),
|
||||
);
|
||||
write(
|
||||
project.path(),
|
||||
&format!("module/sample/worker/worker/probe.{extension}"),
|
||||
source,
|
||||
);
|
||||
command(Command::new(&core).arg("check").arg(project.path()));
|
||||
command(Command::new(&core).args(["lib", "add"]).arg(project.path()));
|
||||
assert_response(command(
|
||||
Command::new(&core).arg("run").arg(project.path()).args([
|
||||
"--",
|
||||
"sample.worker.probe",
|
||||
"{}",
|
||||
]),
|
||||
));
|
||||
let destination = deployments.path().join(ecosystem);
|
||||
write(&destination, "config/setting.json", "{}");
|
||||
let program = destination.join("program");
|
||||
command(
|
||||
Command::new(&core)
|
||||
.arg("build")
|
||||
.arg(project.path())
|
||||
.arg("--output")
|
||||
.arg(&program),
|
||||
);
|
||||
programs.push((ecosystem, destination, program));
|
||||
}
|
||||
daemon.stop();
|
||||
for (ecosystem, destination, program) in programs {
|
||||
assert_response(command(
|
||||
Command::new(program)
|
||||
.current_dir(destination)
|
||||
.args(["sample.worker.probe", "{}"]),
|
||||
));
|
||||
eprintln!(
|
||||
"{ecosystem}: signed runtime, managed execution and source-free standalone execution passed"
|
||||
);
|
||||
}
|
||||
retain_candidate(
|
||||
&layout,
|
||||
&version,
|
||||
"ecosystems",
|
||||
&json!({
|
||||
"format":"dever-zero-lib-release-acceptance-v1",
|
||||
"platform":"linux-x86_64", "version":version.as_str(),
|
||||
"checks":{"signed_install":true,"managed_ecosystems":3,"standalone_ecosystems":3,"source_projects_removed":true},
|
||||
"third_party_packages":"not_run", "host_language_absence":"not_verified",
|
||||
"gpu":"not_run", "public_release":"not_published"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn application(root: &Path, ecosystem: &str) {
|
||||
let (extension, source) = match ecosystem {
|
||||
"pip" => ("py", include_str!("../../ecosystem-release/probe.py")),
|
||||
@@ -389,10 +498,10 @@ fn signed_ecosystems_build_and_run_without_system_language_installations() {
|
||||
nonroot_standalone(&isolated.path().join(ecosystem), &programs[ecosystem]),
|
||||
);
|
||||
}
|
||||
write(&workspace, "target/closure-sandbox-ecosystem-release-acceptance.json", serde_json::to_vec_pretty(&json!({
|
||||
write(&workspace, "target/closure-process_runtime-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},
|
||||
"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,"direct_workers":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"
|
||||
|
||||
@@ -298,10 +298,8 @@ fn go_cross_pack_binds_host_tools_and_arm_standard_library_separately() {
|
||||
|
||||
#[test]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn sandbox_cross_assets_preserve_loader_mode_and_validate_deployment_target() {
|
||||
fn process_runtime_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", &[]),
|
||||
@@ -314,12 +312,12 @@ fn sandbox_cross_assets_preserve_loader_mode_and_validate_deployment_target() {
|
||||
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();
|
||||
dever_process::validate_assets_for_target(&files, "linux-aarch64").unwrap();
|
||||
let mut author = super::targets::cross_author();
|
||||
let inputs = files
|
||||
.iter()
|
||||
.map(|(path, bytes)| {
|
||||
let source = format!("inputs/arm-sandbox/{path}");
|
||||
let source = format!("inputs/arm-process-runtime/{path}");
|
||||
let file = author.root.join(&source);
|
||||
fs::create_dir_all(file.parent().unwrap()).unwrap();
|
||||
fs::write(&file, bytes).unwrap();
|
||||
@@ -327,18 +325,16 @@ fn sandbox_cross_assets_preserve_loader_mode_and_validate_deployment_target() {
|
||||
json!({"source":source,"path":path,"sha256":sha256_file(&file).unwrap()})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
author.settings["sandbox_targets"] = json!({"linux-aarch64":inputs});
|
||||
author.settings["process_runtime_targets"] = json!({"linux-aarch64":inputs});
|
||||
author.save();
|
||||
let output = author.output("cross-sandbox");
|
||||
let output = author.output("cross-process_runtime");
|
||||
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))
|
||||
fs::metadata(output.join("process-runtime/linux-aarch64").join(path))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
@@ -349,7 +345,7 @@ fn sandbox_cross_assets_preserve_loader_mode_and_validate_deployment_target() {
|
||||
}
|
||||
files[1].1[18..20].copy_from_slice(&62u16.to_le_bytes());
|
||||
assert!(
|
||||
dever_sandbox::validate_assets_for_target(&files, "linux-aarch64")
|
||||
dever_process::validate_assets_for_target(&files, "linux-aarch64")
|
||||
.unwrap_err()
|
||||
.contains("incompatible target")
|
||||
);
|
||||
|
||||
@@ -2,6 +2,45 @@ use super::*;
|
||||
use dever_cli::toolchain::runtime_pack::RuntimePack;
|
||||
use dever_runtime::config::RuntimeProfile;
|
||||
|
||||
#[test]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn target_resource_namespaces_are_bound_to_the_signed_release_version() {
|
||||
use dever_cli::toolchain::{
|
||||
Artifact, Extension, ExtensionKind, ReleaseManifest, Version, validate_catalog,
|
||||
};
|
||||
let mut manifest = ReleaseManifest::new(
|
||||
Version::parse("0.1.4").unwrap(),
|
||||
vec![Artifact {
|
||||
path: "dever-core".into(),
|
||||
bytes: 1,
|
||||
sha256: "0".repeat(64),
|
||||
}],
|
||||
);
|
||||
manifest.extensions.push(Extension {
|
||||
kind: ExtensionKind::Target,
|
||||
target: BuildTarget::LinuxAarch64,
|
||||
artifacts: vec![Artifact {
|
||||
path: "sandbox/linux-aarch64/lib/libc.so.6".into(),
|
||||
bytes: 1,
|
||||
sha256: "0".repeat(64),
|
||||
}],
|
||||
});
|
||||
validate_catalog(&manifest).unwrap();
|
||||
manifest.version = Version::parse("0.1.5").unwrap();
|
||||
assert!(
|
||||
validate_catalog(&manifest)
|
||||
.unwrap_err()
|
||||
.contains("resource namespace")
|
||||
);
|
||||
manifest.extensions[0].artifacts[0].path = "process-runtime/linux-aarch64/lib/libc.so.6".into();
|
||||
validate_catalog(&manifest).unwrap();
|
||||
manifest.version = Version::parse("0.1.4").unwrap();
|
||||
assert!(
|
||||
validate_catalog(&manifest)
|
||||
.unwrap_err()
|
||||
.contains("resource namespace")
|
||||
);
|
||||
}
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub(super) fn cross_author() -> Author {
|
||||
let mut author = Author::new();
|
||||
|
||||
@@ -31,7 +31,11 @@ impl Project {
|
||||
MachineManager::new(layout.clone())
|
||||
.install_requested("1.0.0")
|
||||
.unwrap();
|
||||
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
|
||||
fs::copy(
|
||||
fixture_binary(env!("CARGO_BIN_EXE_dever-launcher")),
|
||||
layout.bin().join("dever"),
|
||||
)
|
||||
.unwrap();
|
||||
let root = temporary.path().join("project");
|
||||
fs::create_dir_all(root.join("module")).unwrap();
|
||||
fs::create_dir(root.join("config")).unwrap();
|
||||
@@ -122,7 +126,9 @@ fn project_skill_preserves_user_bytes_permissions_and_updates_only_its_block() {
|
||||
let suffix = "\r\n# 自定义后文\r\n原样保留";
|
||||
fs::write(
|
||||
&agents,
|
||||
format!("{prefix}<!-- dever-language:start -->\n旧规则\n<!-- dever-language:end -->{suffix}"),
|
||||
format!(
|
||||
"{prefix}<!-- dever-language:start -->\n旧规则\n<!-- dever-language:end -->{suffix}"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fixture.succeeds();
|
||||
|
||||
@@ -18,6 +18,18 @@ use sha2::{Digest, Sha256};
|
||||
|
||||
static NEXT_TEMPORARY: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn fixture_binary(cargo_path: &str) -> PathBuf {
|
||||
match std::env::var_os("DEVER_NATIVE_ACCEPTANCE_WORKSPACE") {
|
||||
Some(root) => {
|
||||
let root = PathBuf::from(root);
|
||||
assert!(root.is_absolute());
|
||||
root.join("target/native-release-inputs/bin")
|
||||
.join(Path::new(cargo_path).file_name().unwrap())
|
||||
}
|
||||
None => cargo_path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[path = "shared_toolchain_extensions.rs"]
|
||||
mod extensions;
|
||||
@@ -61,7 +73,7 @@ struct TestDaemon(Child);
|
||||
#[cfg(unix)]
|
||||
impl TestDaemon {
|
||||
fn start(layout: &Layout) -> Self {
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_deverd"))
|
||||
let child = Command::new(fixture_binary(env!("CARGO_BIN_EXE_deverd")))
|
||||
.env_clear()
|
||||
.args(["--root", layout.root().to_str().unwrap()])
|
||||
.spawn()
|
||||
@@ -228,6 +240,77 @@ fn all_supported_platforms_have_one_machine_entry_and_service_contract() {
|
||||
assert!(platform_contract("freebsd").is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn signed_installation_uses_declared_modes_under_restrictive_umask() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
if std::env::var_os("DEVER_TEST_RESTRICTIVE_UMASK").is_none() {
|
||||
let output = Command::new("/bin/sh")
|
||||
.args(["-c", "umask 077; exec \"$@\"", "dever-umask-probe"])
|
||||
.arg(std::env::current_exe().unwrap())
|
||||
.args([
|
||||
"--exact",
|
||||
"signed_installation_uses_declared_modes_under_restrictive_umask",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("DEVER_TEST_RESTRICTIVE_UMASK", "1")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
let temporary = TemporaryDirectory::new();
|
||||
let layout = Layout::new(temporary.path().join("machine"));
|
||||
layout.initialize().unwrap();
|
||||
let key = signing_key();
|
||||
fs::write(
|
||||
layout.state().join("trusted-release-key"),
|
||||
hex(key.public_key().as_ref()),
|
||||
)
|
||||
.unwrap();
|
||||
fs::set_permissions(
|
||||
layout.state().join("trusted-release-key"),
|
||||
fs::Permissions::from_mode(0o644),
|
||||
)
|
||||
.unwrap();
|
||||
write_release(
|
||||
&layout,
|
||||
&key,
|
||||
"1.0.0",
|
||||
&temporary.path().join("dispatch"),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
MachineManager::new(layout.clone())
|
||||
.install_requested("1.0.0")
|
||||
.unwrap();
|
||||
for directory in [
|
||||
layout.bin(),
|
||||
layout.versions(),
|
||||
layout.versions().join("1.0.0"),
|
||||
layout
|
||||
.versions()
|
||||
.join("1.0.0/skills/dever-language/assets/template"),
|
||||
] {
|
||||
assert_eq!(
|
||||
fs::metadata(directory).unwrap().permissions().mode() & 0o777,
|
||||
0o755
|
||||
);
|
||||
}
|
||||
fs::set_permissions(layout.state(), fs::Permissions::from_mode(0o700)).unwrap();
|
||||
layout.initialize().unwrap();
|
||||
assert_eq!(
|
||||
fs::metadata(layout.state()).unwrap().permissions().mode() & 0o777,
|
||||
0o700
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
|
||||
@@ -310,7 +393,11 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
|
||||
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
|
||||
"skill 1.0.0\n"
|
||||
);
|
||||
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
|
||||
fs::copy(
|
||||
fixture_binary(env!("CARGO_BIN_EXE_dever-launcher")),
|
||||
layout.bin().join("dever"),
|
||||
)
|
||||
.unwrap();
|
||||
let active_project = temporary.path().join("active-project");
|
||||
fs::create_dir_all(active_project.join("module")).unwrap();
|
||||
fs::create_dir(active_project.join("config")).unwrap();
|
||||
@@ -510,7 +597,11 @@ fn installed_launcher_is_one_shared_entry_for_multiple_projects() {
|
||||
.unwrap();
|
||||
|
||||
let launcher = layout.bin().join("dever");
|
||||
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), &launcher).unwrap();
|
||||
fs::copy(
|
||||
fixture_binary(env!("CARGO_BIN_EXE_dever-launcher")),
|
||||
&launcher,
|
||||
)
|
||||
.unwrap();
|
||||
let version = Command::new(&launcher)
|
||||
.env_clear()
|
||||
.arg("version")
|
||||
@@ -721,7 +812,7 @@ fn deverd_rejects_a_second_instance_and_recovers_its_socket_after_exit() {
|
||||
layout.initialize().unwrap();
|
||||
let mut daemon = TestDaemon::start(&layout);
|
||||
let socket_inode = fs::symlink_metadata(layout.service_socket()).unwrap().ino();
|
||||
let second = Command::new(env!("CARGO_BIN_EXE_deverd"))
|
||||
let second = Command::new(fixture_binary(env!("CARGO_BIN_EXE_deverd")))
|
||||
.args(["--root", layout.root().to_str().unwrap()])
|
||||
.output()
|
||||
.unwrap();
|
||||
@@ -737,7 +828,7 @@ fn deverd_rejects_a_second_instance_and_recovers_its_socket_after_exit() {
|
||||
assert!(layout.service_socket().exists());
|
||||
let lock = layout.state().join("deverd.lock");
|
||||
fs::set_permissions(&lock, fs::Permissions::from_mode(0o644)).unwrap();
|
||||
let insecure = Command::new(env!("CARGO_BIN_EXE_deverd"))
|
||||
let insecure = Command::new(fixture_binary(env!("CARGO_BIN_EXE_deverd")))
|
||||
.args(["--root", layout.root().to_str().unwrap()])
|
||||
.output()
|
||||
.unwrap();
|
||||
@@ -1097,7 +1188,11 @@ fn active_skill_is_readable_by_other_machine_users_without_install_lock_access()
|
||||
.install_requested("1.0.0")
|
||||
.unwrap();
|
||||
let launcher = layout.bin().join("dever");
|
||||
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), &launcher).unwrap();
|
||||
fs::copy(
|
||||
fixture_binary(env!("CARGO_BIN_EXE_dever-launcher")),
|
||||
&launcher,
|
||||
)
|
||||
.unwrap();
|
||||
fs::set_permissions(&launcher, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let output = Command::new(&launcher)
|
||||
.args(["skill", "path"])
|
||||
@@ -1193,7 +1288,11 @@ fn deverd_uses_kernel_credentials_for_other_machine_users() {
|
||||
layout.initialize().unwrap();
|
||||
let _daemon = TestDaemon::start(&layout);
|
||||
let launcher = layout.bin().join("dever");
|
||||
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), &launcher).unwrap();
|
||||
fs::copy(
|
||||
fixture_binary(env!("CARGO_BIN_EXE_dever-launcher")),
|
||||
&launcher,
|
||||
)
|
||||
.unwrap();
|
||||
fs::set_permissions(&launcher, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let status = Command::new(&launcher)
|
||||
.args(["cache", "status"])
|
||||
|
||||
@@ -8,7 +8,11 @@ pub struct TestDaemon(Child);
|
||||
|
||||
impl TestDaemon {
|
||||
pub fn start(layout: &Layout) -> Self {
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_deverd"))
|
||||
Self::start_with_executable(layout, std::path::Path::new(env!("CARGO_BIN_EXE_deverd")))
|
||||
}
|
||||
|
||||
pub fn start_with_executable(layout: &Layout, executable: &std::path::Path) -> Self {
|
||||
let child = Command::new(executable)
|
||||
.env_clear()
|
||||
.current_dir(layout.root())
|
||||
.args(["--root", "."])
|
||||
|
||||
@@ -1,67 +1,48 @@
|
||||
//! Explicit Linux author inputs shared by opt-in native fixtures.
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const LLVM_LIBRARY: &str = "/usr/lib/llvm-18/lib/libLLVM.so.1";
|
||||
pub fn workspace() -> PathBuf {
|
||||
let root = std::env::var_os("DEVER_NATIVE_ACCEPTANCE_WORKSPACE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."));
|
||||
assert!(root.is_absolute());
|
||||
root
|
||||
}
|
||||
|
||||
pub fn core_libraries() -> Vec<(&'static str, PathBuf)> {
|
||||
let mut inputs = vec![("libLLVM.so.18.1", PathBuf::from(LLVM_LIBRARY))];
|
||||
for name in [
|
||||
"libstdc++.so.6",
|
||||
"libgcc_s.so.1",
|
||||
"libz.so.1",
|
||||
"libffi.so.8",
|
||||
"libedit.so.2",
|
||||
"libzstd.so.1",
|
||||
"libtinfo.so.6",
|
||||
"libxml2.so.2",
|
||||
"libbsd.so.0",
|
||||
"libmd.so.0",
|
||||
"libicuuc.so.74",
|
||||
"libicudata.so.74",
|
||||
"liblzma.so.5",
|
||||
] {
|
||||
inputs.push((name, Path::new("/usr/lib/x86_64-linux-gnu").join(name)));
|
||||
}
|
||||
inputs
|
||||
.into_iter()
|
||||
.map(|(name, path)| {
|
||||
(
|
||||
name,
|
||||
path.canonicalize()
|
||||
.expect("prepare the explicit GNU compiler-library inputs"),
|
||||
)
|
||||
fn prepared_root() -> PathBuf {
|
||||
workspace().join("target/native-release-inputs")
|
||||
}
|
||||
|
||||
pub fn core_libraries() -> Vec<(String, PathBuf)> {
|
||||
let mut inputs = std::fs::read_dir(prepared_root().join("core-libraries"))
|
||||
.expect("prepare the explicit GNU compiler-library closure")
|
||||
.map(|entry| {
|
||||
let entry = entry.unwrap();
|
||||
assert!(entry.file_type().unwrap().is_file());
|
||||
(entry.file_name().into_string().unwrap(), entry.path())
|
||||
})
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!inputs.is_empty(), "compiler-library closure is empty");
|
||||
inputs.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
inputs
|
||||
}
|
||||
|
||||
pub fn native_inputs() -> Vec<(&'static str, PathBuf)> {
|
||||
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
vec![
|
||||
(
|
||||
"runtime.a",
|
||||
workspace.join("target/native-runtime-abi/debug/libdever_backend_bridge.a"),
|
||||
),
|
||||
("crt1.o", "/usr/lib/x86_64-linux-gnu/crt1.o".into()),
|
||||
("crti.o", "/usr/lib/x86_64-linux-gnu/crti.o".into()),
|
||||
(
|
||||
"crtbeginT.o",
|
||||
"/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginT.o".into(),
|
||||
),
|
||||
(
|
||||
"crtend.o",
|
||||
"/usr/lib/gcc/x86_64-linux-gnu/13/crtend.o".into(),
|
||||
),
|
||||
("crtn.o", "/usr/lib/x86_64-linux-gnu/crtn.o".into()),
|
||||
("libc.a", "/usr/lib/x86_64-linux-gnu/libc.a".into()),
|
||||
("libm.a", "/usr/lib/x86_64-linux-gnu/libm-2.39.a".into()),
|
||||
("libmvec.a", "/usr/lib/x86_64-linux-gnu/libmvec.a".into()),
|
||||
(
|
||||
"libgcc.a",
|
||||
"/usr/lib/gcc/x86_64-linux-gnu/13/libgcc.a".into(),
|
||||
),
|
||||
(
|
||||
"libgcc_eh.a",
|
||||
"/usr/lib/gcc/x86_64-linux-gnu/13/libgcc_eh.a".into(),
|
||||
),
|
||||
let native = prepared_root().join("native");
|
||||
[
|
||||
"runtime.a",
|
||||
"crt1.o",
|
||||
"crti.o",
|
||||
"crtbeginT.o",
|
||||
"crtend.o",
|
||||
"crtn.o",
|
||||
"libc.a",
|
||||
"libm.a",
|
||||
"libmvec.a",
|
||||
"libgcc.a",
|
||||
"libgcc_eh.a",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|name| (name, native.join(name)))
|
||||
.collect()
|
||||
}
|
||||
Reference in new issue
Block a user