Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,163 @@
|
||||
use super::{Fixture, tools};
|
||||
use dever::compiler::project::dependency::{
|
||||
Lock,
|
||||
library::{command, npm},
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{collections::BTreeMap, fs, os::unix::fs::DirBuilderExt};
|
||||
|
||||
#[test]
|
||||
fn npm_signed_hooks_native_addon_and_missing_output_replay_are_real() {
|
||||
let mut fixture = Fixture::new();
|
||||
tools::prepare(&mut fixture);
|
||||
let script=br#"const fs=require('node:fs');const path=require('node:path');const child=require('node:child_process');
|
||||
const tools=process.env.PATH.split(':').find(p=>p.endsWith('/rootfs/usr/bin'));
|
||||
if(!tools)throw Error('signed tools PATH missing');
|
||||
if((fs.statSync('executable.cjs').mode&0o111)===0)throw Error('archive executable bit lost');
|
||||
if((fs.statSync('addon.c').mode&0o111)!==0)throw Error('archive executable bit invented');
|
||||
if(fs.readFileSync(path.join(path.dirname(process.execPath),'..','shared.txt'),'utf8')!=='shared runtime fixture')throw Error('shared runtime path changed');
|
||||
if(child.execFileSync(path.join(tools,'fixture-node'),{encoding:'utf8'})!=='signed script')throw Error('signed script did not execute');
|
||||
fs.mkdirSync('build/Release',{recursive:true});
|
||||
child.execFileSync(path.join(tools,'cc'),['-B'+tools+'/','-fno-use-linker-plugin','-fno-lto','-shared','-fPIC','-nostdlib','addon.c','-o','build/Release/addon.node'],{stdio:'inherit'});
|
||||
fs.writeFileSync('generated.js','module.exports=42;');
|
||||
"#;
|
||||
let source=br#"typedef void* napi_env;typedef void* napi_value;typedef unsigned long size_t;
|
||||
extern int napi_create_string_utf8(napi_env,const char*,size_t,napi_value*);
|
||||
__attribute__((visibility("default"))) napi_value napi_register_module_v1(napi_env env,napi_value exports){napi_value result;napi_create_string_utf8(env,"signed addon",12,&result);return result;}
|
||||
"#;
|
||||
fixture.package(json!({"name":"built","version":"1.0.0","optionalDependencies":{"unavailable":"1.0.0"},"scripts":{"install":"node build.cjs","prepare":"node must-not-run.cjs"}}),BTreeMap::from([("package/build.cjs".into(),script.to_vec()),("package/addon.c".into(),source.to_vec()),("package/executable.cjs".into(),b"#!/usr/bin/env node\n".to_vec())]));
|
||||
fixture.package(json!({"name":"unavailable","version":"1.0.0","scripts":{"install":"node -e 'process.exit(7)'"},"bin":{"fixture-bin":"index.js"}}), BTreeMap::new());
|
||||
let project = fixture.directory.0.join("project");
|
||||
fs::DirBuilder::new().mode(0o700).create(&project).unwrap();
|
||||
fs::create_dir(project.join("config")).unwrap();
|
||||
fs::write(project.join("config/setting.json"), "{\"lib\":[]}").unwrap();
|
||||
command::execute_with(
|
||||
"add",
|
||||
&project,
|
||||
&["npm:built@1.0.0".into()],
|
||||
fixture.target,
|
||||
&fixture,
|
||||
&fixture,
|
||||
Some(&fixture.signed),
|
||||
)
|
||||
.unwrap();
|
||||
let lock_file = fs::read_to_string(project.join("dever.lock")).unwrap();
|
||||
let setting = fs::read(project.join("config/setting.json")).unwrap();
|
||||
fixture.lock = Lock::decode(&lock_file).unwrap();
|
||||
let files = fixture.install().unwrap();
|
||||
assert_eq!(
|
||||
files["node_modules/built/generated.js"],
|
||||
b"module.exports=42;"
|
||||
);
|
||||
assert!(files["node_modules/built/build/Release/addon.node"].starts_with(b"\x7fELF"));
|
||||
assert_eq!(fixture.graph().instances.len(), 1);
|
||||
assert_eq!(
|
||||
fixture.graph().instances[0].edges[0].omission,
|
||||
Some(npm::Omission::BuildFailure)
|
||||
);
|
||||
assert_eq!(fixture.lock.libs.len(), 1);
|
||||
let receipt = &fixture.lock.builds[0];
|
||||
assert_eq!(
|
||||
receipt["native_entries"],
|
||||
json!(["node_modules/built/build/Release/addon.node"])
|
||||
);
|
||||
let output = receipt["output"]["sha256"].as_str().unwrap().to_owned();
|
||||
let original = fixture.cache.borrow_mut().remove(&output).unwrap();
|
||||
let sources = receipt["sources"].as_array().unwrap();
|
||||
assert_eq!(sources.len(), 2);
|
||||
let mut expected_urls = vec![];
|
||||
for source in sources {
|
||||
fixture
|
||||
.cache
|
||||
.borrow_mut()
|
||||
.remove(source["sha256"].as_str().unwrap())
|
||||
.unwrap();
|
||||
expected_urls.push(source["source"]["locator"].as_str().unwrap().to_owned());
|
||||
}
|
||||
let lock_before = fixture.lock.encode().unwrap();
|
||||
let calls = fixture.calls.borrow().len();
|
||||
command::execute_with(
|
||||
"install",
|
||||
&project,
|
||||
&[],
|
||||
fixture.target,
|
||||
&fixture,
|
||||
&fixture,
|
||||
Some(&fixture.signed),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fixture.cache.borrow()[&output], original);
|
||||
assert_eq!(fixture.lock.encode().unwrap(), lock_before);
|
||||
assert_eq!(
|
||||
fs::read_to_string(project.join("dever.lock")).unwrap(),
|
||||
lock_file
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(project.join("config/setting.json")).unwrap(),
|
||||
setting
|
||||
);
|
||||
let mut fetched = fixture.calls.borrow()[calls..].to_vec();
|
||||
expected_urls.sort();
|
||||
fetched.sort();
|
||||
assert_eq!(fetched, expected_urls);
|
||||
nondeterministic_replay_is_rejected(&mut fixture);
|
||||
failed_native_build_is_not_published(&mut fixture);
|
||||
}
|
||||
|
||||
fn nondeterministic_replay_is_rejected(fixture: &mut Fixture) {
|
||||
fixture.lock = Lock::new(vec![]);
|
||||
fixture.package(json!({"name":"unstable","version":"1.0.0","scripts":{"install":"node -e \"require('node:fs').writeFileSync('nonce',require('node:crypto').randomBytes(16))\""}}), BTreeMap::new());
|
||||
fixture.resolve(&["npm:unstable@1.0.0"]).unwrap();
|
||||
let mut lock = fixture.lock.clone();
|
||||
fixture.reads.borrow_mut().clear();
|
||||
npm::prepare(&mut lock, fixture.target, &fixture.signed, fixture).unwrap();
|
||||
for lib in &lock.libs {
|
||||
let artifact = &lib.artifacts[0];
|
||||
let reads = fixture.reads.borrow();
|
||||
let source_reads = reads
|
||||
.iter()
|
||||
.filter(|(hash, _)| hash == &artifact.sha256)
|
||||
.collect::<Vec<_>>();
|
||||
eprintln!(
|
||||
"npm build: fixture={} bytes={} reads={}",
|
||||
artifact.sha256,
|
||||
artifact.bytes,
|
||||
source_reads.len()
|
||||
);
|
||||
assert_eq!(source_reads.len(), 1);
|
||||
assert_eq!(source_reads[0].1 as u64, artifact.bytes);
|
||||
}
|
||||
fixture.lock = lock;
|
||||
let digest = fixture.lock.builds[0]["output"]["sha256"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
fixture.cache.borrow_mut().remove(&digest).unwrap();
|
||||
let before = fixture.lock.encode().unwrap();
|
||||
let cache_entries = fixture.cache.borrow().len();
|
||||
let error = npm::restore(
|
||||
&fixture.lock,
|
||||
&fixture.graph(),
|
||||
fixture.target,
|
||||
&fixture.signed,
|
||||
fixture,
|
||||
fixture,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("not byte reproducible"), "{error}");
|
||||
assert_eq!(fixture.cache.borrow().len(), cache_entries);
|
||||
assert_eq!(fixture.lock.encode().unwrap(), before);
|
||||
}
|
||||
|
||||
fn failed_native_build_is_not_published(fixture: &mut Fixture) {
|
||||
fixture.lock = Lock::new(vec![]);
|
||||
fixture.package(json!({"name":"broken","version":"1.0.0","scripts":{"install":"node -e \"const fs=require('node:fs');fs.mkdirSync('build/Release',{recursive:true});fs.writeFileSync('build/Release/broken.node','invalid addon');\""}}), BTreeMap::new());
|
||||
fixture.resolve(&["npm:broken@1.0.0"]).unwrap();
|
||||
let mut lock = fixture.lock.clone();
|
||||
let before = lock.encode().unwrap();
|
||||
let cache_entries = fixture.cache.borrow().len();
|
||||
let error = npm::prepare(&mut lock, fixture.target, &fixture.signed, fixture).unwrap_err();
|
||||
assert!(error.contains("Lib build failed"), "{error}");
|
||||
assert_eq!(lock.encode().unwrap(), before);
|
||||
assert_eq!(fixture.cache.borrow().len(), cache_entries);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
use super::super::super::Directory;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use dever::compiler::{
|
||||
project::dependency::{
|
||||
Lock, Store,
|
||||
library::{self, Spec, registry::Transport, runtime},
|
||||
worker::signed::SignedResources,
|
||||
},
|
||||
toolchain::BuildTarget,
|
||||
};
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use serde_json::{Value, json};
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
use std::{cell::RefCell, collections::BTreeMap, io::Cursor};
|
||||
|
||||
pub(super) fn hash(bytes: &[u8]) -> String {
|
||||
format!("{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
|
||||
pub(super) fn archive(files: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
|
||||
let gzip = flate2::GzBuilder::new()
|
||||
.mtime(0)
|
||||
.write(Vec::new(), flate2::Compression::fast());
|
||||
let mut builder = tar::Builder::new(gzip);
|
||||
for (path, bytes) in files {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_mode(if bytes.starts_with(b"#!") {
|
||||
0o755
|
||||
} else {
|
||||
0o644
|
||||
});
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_cksum();
|
||||
builder
|
||||
.append_data(&mut header, path, Cursor::new(bytes))
|
||||
.unwrap();
|
||||
}
|
||||
builder.into_inner().unwrap().finish().unwrap()
|
||||
}
|
||||
|
||||
pub(super) struct Fixture {
|
||||
pub directory: Directory,
|
||||
pub documents: BTreeMap<String, Vec<u8>>,
|
||||
pub cache: RefCell<BTreeMap<String, Vec<u8>>>,
|
||||
pub reads: RefCell<Vec<(String, usize)>>,
|
||||
pub calls: RefCell<Vec<String>>,
|
||||
pub metadata: runtime::Metadata,
|
||||
pub target: BuildTarget,
|
||||
pub signed: SignedResources,
|
||||
pub lock: Lock,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
pub fn new() -> Self {
|
||||
let directory = Directory::new();
|
||||
let target = BuildTarget::host().unwrap();
|
||||
let metadata = json!({"pack":{"name":"node","version":"22.1.0","sha256":hash(b"node pack")},"target":target.name(),"python_markers":null,"python_wheel_tags":[],"npm_libc":"glibc"});
|
||||
let signed = signed(
|
||||
&directory,
|
||||
&BTreeMap::from([(
|
||||
format!("runtime/npm/{}/manifest.json", target.name()),
|
||||
serde_json::to_vec(
|
||||
&json!({"format":"dever-registry-runtime-v1","runtime":metadata}),
|
||||
)
|
||||
.unwrap(),
|
||||
)]),
|
||||
);
|
||||
Self {
|
||||
directory,
|
||||
documents: BTreeMap::new(),
|
||||
cache: RefCell::new(BTreeMap::new()),
|
||||
reads: RefCell::new(vec![]),
|
||||
calls: RefCell::new(vec![]),
|
||||
metadata: serde_json::from_value(metadata).unwrap(),
|
||||
target,
|
||||
signed,
|
||||
lock: Lock::new(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn package(&mut self, document: Value, extra: BTreeMap<String, Vec<u8>>) {
|
||||
let name = document["name"].as_str().unwrap();
|
||||
let version = document["version"].as_str().unwrap();
|
||||
let base = format!("https://registry.npmjs.org/{name}");
|
||||
let locator = format!("{base}/-/archive-{version}.tgz");
|
||||
let mut files = extra;
|
||||
files.insert(
|
||||
"package/package.json".into(),
|
||||
serde_json::to_vec(&document).unwrap(),
|
||||
);
|
||||
files.insert("package/index.js".into(), b"module.exports = 42;".to_vec());
|
||||
let bytes = archive(&files);
|
||||
let mut metadata = document.clone();
|
||||
metadata["dist"] = json!({"tarball":locator,"integrity":format!("sha512-{}",STANDARD.encode(Sha512::digest(&bytes)))});
|
||||
let mut index: Value = self
|
||||
.documents
|
||||
.get(&base)
|
||||
.map(|bytes| serde_json::from_slice(bytes).unwrap())
|
||||
.unwrap_or_else(|| json!({"name":name,"versions":{}}));
|
||||
index["versions"][version] = metadata.clone();
|
||||
self.documents
|
||||
.insert(base.clone(), serde_json::to_vec(&index).unwrap());
|
||||
self.documents.insert(
|
||||
format!("{base}/{version}"),
|
||||
serde_json::to_vec(&metadata).unwrap(),
|
||||
);
|
||||
self.documents.insert(locator, bytes);
|
||||
}
|
||||
|
||||
pub fn resolve(&mut self, roots: &[&str]) -> Result<(), String> {
|
||||
let specs = roots
|
||||
.iter()
|
||||
.map(|value| value.parse::<Spec>())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut lock = self.lock.clone();
|
||||
library::npm::resolve(&specs, self.target, &self.metadata, self, self, &mut lock)?;
|
||||
self.lock = lock;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn graph(&self) -> library::npm::Environment {
|
||||
serde_json::from_value(self.lock.npm[0].clone()).unwrap()
|
||||
}
|
||||
|
||||
pub fn install(&self) -> Result<BTreeMap<String, Vec<u8>>, String> {
|
||||
library::npm::install(
|
||||
&self.lock,
|
||||
&self.graph().roots,
|
||||
self.target,
|
||||
self,
|
||||
&self.signed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for Fixture {
|
||||
fn get(&self, url: &str, limit: usize) -> Result<Vec<u8>, String> {
|
||||
library::registry::official(url)?;
|
||||
self.calls.borrow_mut().push(url.into());
|
||||
let bytes = self
|
||||
.documents
|
||||
.get(url)
|
||||
.ok_or_else(|| "registry returned HTTP 404".to_owned())?;
|
||||
assert!(bytes.len() <= limit);
|
||||
Ok(bytes.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for Fixture {
|
||||
fn get(&self, digest: &str, _: u64) -> Result<Option<Vec<u8>>, String> {
|
||||
let bytes = self.cache.borrow().get(digest).cloned();
|
||||
self.reads
|
||||
.borrow_mut()
|
||||
.push((digest.into(), bytes.as_ref().map_or(0, Vec::len)));
|
||||
Ok(bytes)
|
||||
}
|
||||
fn put(&self, bytes: &[u8]) -> Result<(), String> {
|
||||
self.cache.borrow_mut().insert(hash(bytes), bytes.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn signed(directory: &Directory, files: &BTreeMap<String, Vec<u8>>) -> SignedResources {
|
||||
let root = directory.0.join("signed");
|
||||
let key = Ed25519KeyPair::from_seed_unchecked(&[72; 32]).unwrap();
|
||||
let hex = |bytes: &[u8]| {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>()
|
||||
};
|
||||
let write = |path: &str, bytes: &[u8]| {
|
||||
let path = root.join(path);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, bytes).unwrap();
|
||||
};
|
||||
write(
|
||||
"state/trusted-release-key",
|
||||
hex(key.public_key().as_ref()).as_bytes(),
|
||||
);
|
||||
let mut artifacts = vec![json!({"path":"base.txt","bytes":4,"sha256":hash(b"base")})];
|
||||
let target = BuildTarget::host().unwrap();
|
||||
for (path, bytes) in files {
|
||||
artifacts.push(json!({"path":path,"bytes":bytes.len(),"sha256":hash(bytes)}));
|
||||
write(&format!("versions/0.0.1/{path}"), bytes);
|
||||
}
|
||||
write("versions/0.0.1/base.txt", b"base");
|
||||
let catalog=serde_json::to_vec(&json!({"format":"dever-release-v2","version":"0.0.1","platform":target.name(),"artifacts":artifacts,"extensions":[]})).unwrap();
|
||||
write("versions/0.0.1/manifest.json", &catalog);
|
||||
write(
|
||||
"versions/0.0.1/manifest.sig",
|
||||
hex(key.sign(&catalog).as_ref()).as_bytes(),
|
||||
);
|
||||
SignedResources::load(&root, "0.0.1").unwrap()
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
use super::Fixture;
|
||||
use dever::compiler::project::dependency::library::npm::{self, Kind, Omission};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn npm_scoped_metadata_uses_strict_official_url_policy() {
|
||||
let mut fixture = Fixture::new();
|
||||
fixture.package(
|
||||
json!({"name":"app","version":"1.0.0","dependencies":{"@types/node":"^22"}}),
|
||||
BTreeMap::new(),
|
||||
);
|
||||
fixture.package(
|
||||
json!({"name":"@types/node","version":"22.10.0"}),
|
||||
BTreeMap::new(),
|
||||
);
|
||||
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
||||
for address in [
|
||||
"https://registry.npmjs.org/@types/node",
|
||||
"https://registry.npmjs.org/@types/node/22.10.0",
|
||||
"https://registry.npmjs.org/@types/node/-/archive-22.10.0.tgz",
|
||||
] {
|
||||
assert!(fixture.calls.borrow().iter().any(|url| url == address));
|
||||
}
|
||||
let graph = fixture.graph();
|
||||
let node = graph
|
||||
.instances
|
||||
.iter()
|
||||
.find(|node| node.spec.name == "@types/node")
|
||||
.unwrap();
|
||||
let files = fixture.install().unwrap();
|
||||
assert_eq!(
|
||||
files[&format!("{}/index.js", node.path)],
|
||||
b"module.exports = 42;"
|
||||
);
|
||||
let lib = fixture
|
||||
.lock
|
||||
.libs
|
||||
.iter()
|
||||
.find(|lib| lib.spec.name == "@types/node")
|
||||
.unwrap();
|
||||
let mut source = lib.artifacts[0].source.clone().unwrap();
|
||||
for locator in [
|
||||
"https://registry.npmjs.org/@types%2fnode/-/archive-22.10.0.tgz",
|
||||
"https://registry.npmjs.org/@types%2Fnode/-/archive-22.10.0.tgz",
|
||||
"https://registry.npmjs.org/@types/node/%2e%2e/archive-22.10.0.tgz",
|
||||
] {
|
||||
source.locator = locator.into();
|
||||
assert!(source.validate(&lib.spec).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_nested_instances_keep_peer_context_and_install_offline() {
|
||||
let mut fixture = Fixture::new();
|
||||
for document in [
|
||||
json!({"name":"left","version":"1.0.0","dependencies":{"plugin":"1.0.0","engine":"1.0.0"}}),
|
||||
json!({"name":"right","version":"1.0.0","dependencies":{"plugin":"1.0.0","engine":"2.0.0"}}),
|
||||
json!({"name":"plugin","version":"1.0.0","peerDependencies":{"engine":">=1 <3"}}),
|
||||
json!({"name":"engine","version":"1.0.0"}),
|
||||
json!({"name":"engine","version":"2.0.0"}),
|
||||
] {
|
||||
fixture.package(document, BTreeMap::new());
|
||||
}
|
||||
fixture
|
||||
.resolve(&["npm:left@1.0.0", "npm:right@1.0.0"])
|
||||
.unwrap();
|
||||
let graph = fixture.graph();
|
||||
for root in ["left", "right"] {
|
||||
let plugin = graph
|
||||
.instances
|
||||
.iter()
|
||||
.find(|node| node.path == format!("node_modules/{root}/node_modules/plugin"))
|
||||
.unwrap();
|
||||
assert_eq!(plugin.edges[0].kind, Kind::Peer);
|
||||
assert_eq!(
|
||||
plugin.edges[0].target.as_deref(),
|
||||
Some(format!("node_modules/{root}/node_modules/engine").as_str())
|
||||
);
|
||||
}
|
||||
fixture.documents.clear();
|
||||
let calls = fixture.calls.borrow().len();
|
||||
let files = fixture.install().unwrap();
|
||||
assert_eq!(files.len(), 12);
|
||||
assert_eq!(fixture.calls.borrow().len(), calls);
|
||||
assert_eq!(fixture.lock.libs.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_peer_conflict_backtracks_versions_and_incompatible_roots_fail_atomically() {
|
||||
let mut fixture = Fixture::new();
|
||||
for document in [
|
||||
json!({"name":"app","version":"1.0.0","dependencies":{"plugin":"*","engine":"1.0.0"}}),
|
||||
json!({"name":"plugin","version":"2.0.0","peerDependencies":{"engine":"2.0.0"}}),
|
||||
json!({"name":"plugin","version":"1.0.0","peerDependencies":{"engine":"1.0.0"}}),
|
||||
json!({"name":"engine","version":"1.0.0"}),
|
||||
json!({"name":"engine","version":"2.0.0"}),
|
||||
] {
|
||||
fixture.package(document, BTreeMap::new());
|
||||
}
|
||||
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
||||
assert_eq!(
|
||||
fixture
|
||||
.graph()
|
||||
.instances
|
||||
.iter()
|
||||
.find(|node| node.spec.name == "plugin")
|
||||
.unwrap()
|
||||
.spec
|
||||
.version,
|
||||
"1.0.0"
|
||||
);
|
||||
let before = fixture.lock.encode().unwrap();
|
||||
assert!(
|
||||
fixture
|
||||
.resolve(&["npm:engine@1.0.0", "npm:engine@2.0.0"])
|
||||
.unwrap_err()
|
||||
.contains("incompatible")
|
||||
);
|
||||
assert_eq!(fixture.lock.encode().unwrap(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_optional_target_and_absent_peer_are_explicit_but_integrity_errors_are_fatal() {
|
||||
let mut fixture = Fixture::new();
|
||||
fixture.package(json!({"name":"app","version":"1.0.0","optionalDependencies":{"wrong":"*","missing":"*"},"peerDependencies":{"peer":"*"},"peerDependenciesMeta":{"peer":{"optional":true}}}),BTreeMap::new());
|
||||
fixture.package(
|
||||
json!({"name":"wrong","version":"1.0.0","os":["!linux"]}),
|
||||
BTreeMap::new(),
|
||||
);
|
||||
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
||||
let graph = fixture.graph();
|
||||
let edges = &graph.instances[0].edges;
|
||||
for (name, reason) in [
|
||||
("wrong", Omission::Target),
|
||||
("missing", Omission::Unavailable),
|
||||
("peer", Omission::OptionalPeerAbsent),
|
||||
] {
|
||||
assert_eq!(
|
||||
edges
|
||||
.iter()
|
||||
.find(|edge| edge.name == name)
|
||||
.unwrap()
|
||||
.omission,
|
||||
Some(reason)
|
||||
);
|
||||
}
|
||||
fixture.install().unwrap();
|
||||
let before = fixture.lock.encode().unwrap();
|
||||
assert!(
|
||||
fixture
|
||||
.resolve(&["npm:missing@1.0.0"])
|
||||
.unwrap_err()
|
||||
.contains("no npm version")
|
||||
);
|
||||
assert_eq!(fixture.lock.encode().unwrap(), before);
|
||||
fixture.documents.insert(
|
||||
"https://registry.npmjs.org/wrong/-/archive-1.0.0.tgz".into(),
|
||||
b"corrupt optional archive".to_vec(),
|
||||
);
|
||||
assert!(
|
||||
fixture
|
||||
.resolve(&["npm:app@1.0.0"])
|
||||
.unwrap_err()
|
||||
.contains("integrity")
|
||||
);
|
||||
assert_eq!(fixture.lock.encode().unwrap(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_one_version_cannot_change_bytes_between_worker_groups() {
|
||||
let mut fixture = Fixture::new();
|
||||
let metadata = json!({"name":"shared","version":"1.0.0"});
|
||||
fixture.package(metadata.clone(), BTreeMap::new());
|
||||
fixture.resolve(&["npm:shared@1.0.0"]).unwrap();
|
||||
let before = fixture.lock.encode().unwrap();
|
||||
fixture.package(
|
||||
metadata,
|
||||
BTreeMap::from([("package/changed.js".into(), b"different bytes".to_vec())]),
|
||||
);
|
||||
let error = fixture.resolve(&["npm:shared@1.0.0"]).unwrap_err();
|
||||
assert!(error.contains("artifact differs"), "{error}");
|
||||
assert_eq!(fixture.lock.encode().unwrap(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_bundled_packages_have_one_owner_and_tampered_manifest_or_graph_fails() {
|
||||
let mut fixture = Fixture::new();
|
||||
fixture.package(json!({"name":"app","version":"1.0.0","dependencies":{"tiny":"1.0.0"},"bundleDependencies":["tiny"]}),BTreeMap::from([
|
||||
("package/node_modules/tiny/package.json".into(),serde_json::to_vec(&json!({"name":"tiny","version":"1.0.0"})).unwrap()),
|
||||
("package/node_modules/tiny/index.js".into(),b"module.exports=7".to_vec()),
|
||||
]));
|
||||
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
||||
assert_eq!(fixture.lock.libs.len(), 1);
|
||||
assert_eq!(fixture.install().unwrap().len(), 4);
|
||||
let original = fixture.lock.npm[0].clone();
|
||||
fixture.lock.npm[0]["instances"][0]["edges"][0]["requirement"] = json!("2.0.0");
|
||||
assert!(fixture.install().unwrap_err().contains("range"));
|
||||
fixture.lock.npm[0] = original;
|
||||
let mut graph = fixture.graph();
|
||||
graph.instances[1].source.prefix = "package/".into();
|
||||
assert!(
|
||||
npm::validate(&graph, &fixture.lock)
|
||||
.unwrap_err()
|
||||
.contains("owned")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_registry_and_archive_metadata_must_agree_and_urls_are_official() {
|
||||
let mut fixture = Fixture::new();
|
||||
fixture.package(json!({"name":"app","version":"1.0.0"}), BTreeMap::new());
|
||||
let address = "https://registry.npmjs.org/app/1.0.0";
|
||||
let original = fixture.documents[address].clone();
|
||||
let mut metadata: serde_json::Value = serde_json::from_slice(&original).unwrap();
|
||||
metadata["dependencies"] = json!({"hidden":"1.0.0"});
|
||||
fixture
|
||||
.documents
|
||||
.insert(address.into(), serde_json::to_vec(&metadata).unwrap());
|
||||
assert!(
|
||||
fixture
|
||||
.resolve(&["npm:app@1.0.0"])
|
||||
.unwrap_err()
|
||||
.contains("metadata")
|
||||
);
|
||||
let mut metadata: serde_json::Value = serde_json::from_slice(&original).unwrap();
|
||||
metadata["dist"]["tarball"] = json!("https://attacker.invalid/app.tgz");
|
||||
fixture
|
||||
.documents
|
||||
.insert(address.into(), serde_json::to_vec(&metadata).unwrap());
|
||||
assert!(
|
||||
fixture
|
||||
.resolve(&["npm:app@1.0.0"])
|
||||
.unwrap_err()
|
||||
.contains("official")
|
||||
);
|
||||
assert!(
|
||||
!fixture
|
||||
.calls
|
||||
.borrow()
|
||||
.iter()
|
||||
.any(|url| url.contains("attacker"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_cycles_close_without_infinite_nested_copies_and_missing_build_receipt_fails() {
|
||||
let mut fixture = Fixture::new();
|
||||
fixture.package(
|
||||
json!({"name":"alpha","version":"1.0.0","dependencies":{"beta":"1.0.0"}}),
|
||||
BTreeMap::new(),
|
||||
);
|
||||
fixture.package(
|
||||
json!({"name":"beta","version":"1.0.0","dependencies":{"alpha":"1.0.0"}}),
|
||||
BTreeMap::new(),
|
||||
);
|
||||
fixture.resolve(&["npm:alpha@1.0.0"]).unwrap();
|
||||
assert_eq!(fixture.graph().instances.len(), 2);
|
||||
assert_eq!(fixture.install().unwrap().len(), 4);
|
||||
fixture.package(
|
||||
json!({"name":"hook","version":"1.0.0","scripts":{"install":"must not run"}}),
|
||||
BTreeMap::new(),
|
||||
);
|
||||
fixture.resolve(&["npm:hook@1.0.0"]).unwrap();
|
||||
let graph = fixture
|
||||
.lock
|
||||
.npm
|
||||
.iter()
|
||||
.find(|value| value["roots"][0]["name"] == "hook")
|
||||
.unwrap()
|
||||
.clone();
|
||||
fixture.lock.npm = vec![graph];
|
||||
assert!(fixture.install().unwrap_err().contains("lifecycle"));
|
||||
fixture.lock.npm[0]["build"] = json!("0".repeat(64));
|
||||
assert!(fixture.install().unwrap_err().contains("receipt"));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//! 注入 registry、签名 catalog 与真实 tgz;不运行宿主 Node/npm,不访问公网。
|
||||
|
||||
mod build;
|
||||
mod fixture;
|
||||
mod graph;
|
||||
mod process;
|
||||
mod receipt;
|
||||
mod tools;
|
||||
|
||||
use fixture::Fixture;
|
||||
@@ -0,0 +1,98 @@
|
||||
//! 共享执行器回归使用 Pod 内的真实子进程,不依赖线上服务。
|
||||
|
||||
use super::super::super::Directory;
|
||||
use dever::compiler::project::dependency::library::build::process;
|
||||
use std::{fs, path::Path, process::Command};
|
||||
|
||||
fn child_gone(path: &Path) {
|
||||
let pid = fs::read_to_string(path).unwrap();
|
||||
assert!(
|
||||
!Path::new("/proc").join(pid.trim()).exists(),
|
||||
"child {pid} was not reaped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_build_output_overflow_drains_pipes_and_reaps_its_group() {
|
||||
let directory = Directory::new();
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.current_dir(&directory.0).args(["-c", "echo $$ > parent; /bin/sleep 300 & echo $! > child; /usr/bin/head -c 33554432 /dev/zero; wait"]);
|
||||
let error = process::run(command, &directory.0).unwrap_err();
|
||||
assert!(error.contains("16 MiB"), "{error}");
|
||||
child_gone(&directory.0.join("parent"));
|
||||
child_gone(&directory.0.join("child"));
|
||||
assert!(
|
||||
fs::read_dir(&directory.0)
|
||||
.unwrap()
|
||||
.all(|entry| entry.unwrap().metadata().unwrap().len() < 32)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_build_success_reaps_descendants_holding_output_open() {
|
||||
let directory = Directory::new();
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command
|
||||
.current_dir(&directory.0)
|
||||
.args(["-c", "/bin/sleep 300 & echo $! > child; exit 0"]);
|
||||
assert!(process::run(command, &directory.0).unwrap().success());
|
||||
child_gone(&directory.0.join("child"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_build_scratch_budget_kills_and_reaps_before_returning() {
|
||||
let directory = Directory::new();
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.current_dir(&directory.0).args(["-c", "echo $$ > parent; /bin/sleep 300 & echo $! > child; /usr/bin/truncate -s 536870913 excess; wait"]);
|
||||
let error = process::run(command, &directory.0).unwrap_err();
|
||||
assert!(error.contains("512 MiB"), "{error}");
|
||||
child_gone(&directory.0.join("parent"));
|
||||
child_gone(&directory.0.join("child"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_build_probe_distinguishes_unloadable_exit_and_signal_from_budget_errors() {
|
||||
let directory = Directory::new();
|
||||
for script in ["exit 10", "ulimit -c 0; kill -SEGV $$"] {
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", script]);
|
||||
let status = process::status(command, &directory.0).unwrap();
|
||||
assert!(!status.success());
|
||||
}
|
||||
let mut command = Command::new("/bin/sh");
|
||||
command.args(["-c", "exit 10"]);
|
||||
assert!(
|
||||
process::run(command, &directory.0)
|
||||
.unwrap_err()
|
||||
.contains("Lib build failed")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_build_script_header_cannot_select_host_tools_or_inject_arguments() {
|
||||
let fixture = super::Fixture::new();
|
||||
for header in [
|
||||
"#!/bin/bash",
|
||||
"#!/usr/bin/env node --eval unsafe",
|
||||
"#!/tmp/untrusted",
|
||||
] {
|
||||
let mut stage = dever::compiler::project::dependency::library::build::stage::Stage::new(
|
||||
&fixture.signed,
|
||||
)
|
||||
.unwrap();
|
||||
stage
|
||||
.write(
|
||||
"input/rootfs/usr/bin/script",
|
||||
format!("{header}\nexit 0\n").into_bytes(),
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let tools = ["rootfs/usr/bin/script".to_owned()].into_iter().collect();
|
||||
assert!(
|
||||
stage
|
||||
.prepare_tools(&tools)
|
||||
.unwrap_err()
|
||||
.contains("signed interpreter")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
use super::{
|
||||
Fixture,
|
||||
fixture::{archive, hash, signed},
|
||||
};
|
||||
use dever::compiler::project::dependency::{
|
||||
Lock, Store,
|
||||
library::{
|
||||
Spec,
|
||||
lock::{Artifact, Library, Runtime},
|
||||
npm::Environment,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
// Fixture 按既有 v6 协议编码,独立于实现中的私有 Receipt 类型。
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Inputs {
|
||||
format: String,
|
||||
toolchain: String,
|
||||
libs: Vec<Library>,
|
||||
workers: Vec<Value>,
|
||||
npm: Vec<Environment>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Tools {
|
||||
format: String,
|
||||
ecosystem: String,
|
||||
target: String,
|
||||
runtime_sha256: String,
|
||||
python_runtime_sha256: Option<String>,
|
||||
sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Receipt {
|
||||
ecosystem: String,
|
||||
roots: Vec<Spec>,
|
||||
sources: Vec<Artifact>,
|
||||
graph: Option<Environment>,
|
||||
runtime: Runtime,
|
||||
auxiliary_runtime: Option<Runtime>,
|
||||
native_entries: BTreeSet<String>,
|
||||
native_rejections: BTreeMap<String, String>,
|
||||
python_markers: Option<Value>,
|
||||
tools: Tools,
|
||||
frontend: String,
|
||||
python: Option<Value>,
|
||||
dynamic_requires: Vec<String>,
|
||||
inputs: Inputs,
|
||||
config_settings: BTreeMap<String, Value>,
|
||||
output: Artifact,
|
||||
}
|
||||
|
||||
fn prepare(fixture: &mut Fixture, addons: bool) -> Receipt {
|
||||
let node_pack = archive(&BTreeMap::from([(
|
||||
"fixture/node.txt".into(),
|
||||
b"author fixture runtime".to_vec(),
|
||||
)]));
|
||||
let python_pack = archive(&BTreeMap::from([(
|
||||
"fixture/python.txt".into(),
|
||||
b"author fixture auxiliary runtime".to_vec(),
|
||||
)]));
|
||||
let build_pack = archive(&BTreeMap::from([(
|
||||
"fixture/tools.txt".into(),
|
||||
b"already prepared output; no hooks run".to_vec(),
|
||||
)]));
|
||||
fixture.metadata.pack.sha256 = hash(&node_pack);
|
||||
let mut extra = BTreeMap::new();
|
||||
if addons {
|
||||
extra.insert(
|
||||
"package/foreign.node".into(),
|
||||
b"prebuilt foreign format".to_vec(),
|
||||
);
|
||||
}
|
||||
fixture.package(
|
||||
json!({"name":"built","version":"1.0.0","scripts":{"install":"must never run"}}),
|
||||
extra,
|
||||
);
|
||||
fixture.resolve(&["npm:built@1.0.0"]).unwrap();
|
||||
let graph = fixture.graph();
|
||||
let mut output = BTreeMap::from([
|
||||
(
|
||||
"node_modules/built/package.json".into(),
|
||||
serde_json::to_vec(
|
||||
&json!({"name":"built","version":"1.0.0","scripts":{"install":"must never run"}}),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
(
|
||||
"node_modules/built/generated.js".into(),
|
||||
b"module.exports=42".to_vec(),
|
||||
),
|
||||
]);
|
||||
if addons {
|
||||
output.insert(
|
||||
"node_modules/built/foreign.node".into(),
|
||||
b"prebuilt foreign format".to_vec(),
|
||||
);
|
||||
}
|
||||
let bytes = archive(&output);
|
||||
fixture.put(&bytes).unwrap();
|
||||
let python = Runtime {
|
||||
name: "python".into(),
|
||||
version: "3.12.1".into(),
|
||||
sha256: hash(&python_pack),
|
||||
};
|
||||
let tools = Tools {
|
||||
format: "dever-registry-build-v1".into(),
|
||||
ecosystem: "npm".into(),
|
||||
target: fixture.target.name().into(),
|
||||
runtime_sha256: fixture.metadata.pack.sha256.clone(),
|
||||
python_runtime_sha256: Some(python.sha256.clone()),
|
||||
sha256: hash(&build_pack),
|
||||
};
|
||||
let target = fixture.target.name();
|
||||
let files=BTreeMap::from([
|
||||
(format!("runtime/npm/{target}/runtime.pack"),node_pack),
|
||||
(format!("runtime/npm/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":fixture.metadata.pack,"target":target,"python_markers":null,"python_wheel_tags":[],"npm_libc":"glibc"}})).unwrap()),
|
||||
(format!("runtime/pip/{target}/runtime.pack"),python_pack),
|
||||
(format!("runtime/pip/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":python,"target":target,"python_markers":{"implementation_name":"cpython","implementation_version":"3.12.1","os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":"3.12.1","python_version":"3.12","sys_platform":"linux"},"python_wheel_tags":["py3-none-any"]}})).unwrap()),
|
||||
(format!("build/npm/{target}/build.pack"),build_pack),
|
||||
(format!("build/npm/{target}/manifest.json"),serde_json::to_vec(&tools).unwrap()),
|
||||
]);
|
||||
fixture.signed = signed(&fixture.directory, &files);
|
||||
Receipt {
|
||||
ecosystem: "npm".into(),
|
||||
roots: graph.roots.clone(),
|
||||
sources: fixture
|
||||
.lock
|
||||
.libs
|
||||
.iter()
|
||||
.flat_map(|lib| lib.artifacts.clone())
|
||||
.collect(),
|
||||
graph: Some(graph),
|
||||
runtime: fixture.metadata.pack.clone(),
|
||||
auxiliary_runtime: Some(python),
|
||||
native_entries: BTreeSet::new(),
|
||||
native_rejections: if addons {
|
||||
BTreeMap::from([("node_modules/built/foreign.node".into(), "format".into())])
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
},
|
||||
python_markers: None,
|
||||
tools,
|
||||
frontend: "cfe67ebbc939762cd4ba7328943bf97a72d06a4b79071f056ebd8e5ab566a721".into(),
|
||||
python: None,
|
||||
dynamic_requires: vec![],
|
||||
inputs: serde_json::from_value(serde_json::to_value(&fixture.lock).unwrap()).unwrap(),
|
||||
config_settings: BTreeMap::new(),
|
||||
output: Artifact {
|
||||
target: target.into(),
|
||||
path: format!("build/npm/{}/installation.tgz", hash(&bytes)),
|
||||
bytes: bytes.len() as u64,
|
||||
sha256: hash(&bytes),
|
||||
source: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn attach(fixture: &mut Fixture, receipt: &Receipt) {
|
||||
fixture.lock.npm[0]["build"] = json!(hash(&serde_json::to_vec(receipt).unwrap()));
|
||||
fixture.lock.builds = vec![serde_json::to_value(receipt).unwrap()];
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_lifecycle_receipt_consumes_signed_inputs_without_network_or_hooks() {
|
||||
let mut fixture = Fixture::new();
|
||||
let receipt = prepare(&mut fixture, false);
|
||||
attach(&mut fixture, &receipt);
|
||||
fixture.documents.clear();
|
||||
let calls = fixture.calls.borrow().len();
|
||||
let files = fixture.install().unwrap();
|
||||
assert_eq!(
|
||||
files["node_modules/built/generated.js"],
|
||||
b"module.exports=42"
|
||||
);
|
||||
assert_eq!(fixture.calls.borrow().len(), calls);
|
||||
let text = fixture.lock.encode().unwrap();
|
||||
let reparsed = Lock::decode(&text).unwrap();
|
||||
fixture.lock = reparsed;
|
||||
fixture.install().unwrap();
|
||||
fixture.lock.builds[0]["frontend"] = json!("0".repeat(64));
|
||||
assert!(fixture.install().unwrap_err().contains("receipt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_native_receipt_accounts_for_all_addons_and_never_admits_failed_build() {
|
||||
let mut fixture = Fixture::new();
|
||||
let mut receipt = prepare(&mut fixture, true);
|
||||
attach(&mut fixture, &receipt);
|
||||
assert!(
|
||||
!fixture
|
||||
.install()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.any(|path| path.ends_with(".node"))
|
||||
);
|
||||
receipt.native_rejections.clear();
|
||||
attach(&mut fixture, &receipt);
|
||||
assert!(fixture.install().unwrap_err().contains("admission"));
|
||||
receipt
|
||||
.native_entries
|
||||
.insert("node_modules/built/foreign.node".into());
|
||||
attach(&mut fixture, &receipt);
|
||||
assert!(fixture.install().unwrap_err().contains("ELF"));
|
||||
receipt.native_entries.clear();
|
||||
receipt
|
||||
.native_rejections
|
||||
.insert("node_modules/built/new.node".into(), "unloadable".into());
|
||||
attach(&mut fixture, &receipt);
|
||||
assert!(fixture.install().unwrap_err().contains("admission"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_replay_rejects_malformed_binding_before_fetching_or_executing() {
|
||||
let mut fixture = Fixture::new();
|
||||
let mut receipt = prepare(&mut fixture, false);
|
||||
receipt.tools.runtime_sha256 = "0".repeat(64);
|
||||
attach(&mut fixture, &receipt);
|
||||
fixture.cache.borrow_mut().clear();
|
||||
let calls = fixture.calls.borrow().len();
|
||||
let error = dever::compiler::project::dependency::library::npm::restore(
|
||||
&fixture.lock,
|
||||
&fixture.graph(),
|
||||
fixture.target,
|
||||
&fixture.signed,
|
||||
&fixture,
|
||||
&fixture,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("binding differs"), "{error}");
|
||||
assert_eq!(fixture.calls.borrow().len(), calls);
|
||||
assert!(fixture.cache.borrow().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_cached_receipt_rejects_tampering_but_needs_no_build_tools() {
|
||||
let mut fixture = Fixture::new();
|
||||
let receipt = prepare(&mut fixture, false);
|
||||
attach(&mut fixture, &receipt);
|
||||
let original = fixture.cache.borrow()[&receipt.output.sha256].clone();
|
||||
fixture
|
||||
.cache
|
||||
.borrow_mut()
|
||||
.insert(receipt.output.sha256.clone(), b"tampered".to_vec());
|
||||
assert!(fixture.install().unwrap_err().contains("digest"));
|
||||
fixture
|
||||
.cache
|
||||
.borrow_mut()
|
||||
.insert(receipt.output.sha256.clone(), original);
|
||||
std::fs::remove_file(fixture.directory.0.join(format!(
|
||||
"signed/versions/0.0.1/build/npm/{}/build.pack",
|
||||
fixture.target.name()
|
||||
)))
|
||||
.unwrap();
|
||||
fixture.install().unwrap();
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! 只在开发 Pod 执行:读取已安装工具,组装临时签名输入;不安装或激活软件。
|
||||
|
||||
use super::{
|
||||
Fixture,
|
||||
fixture::{archive, hash, signed},
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{collections::BTreeMap, fs, path::Path, process::Command};
|
||||
|
||||
pub(super) fn prepare(fixture: &mut Fixture) {
|
||||
let target = fixture.target.name();
|
||||
let mut assets = BTreeMap::new();
|
||||
let mut dependencies = BTreeMap::new();
|
||||
let node = Path::new("/usr/local/bin/node");
|
||||
let python = Path::new("/usr/bin/python3");
|
||||
let node_bytes = fs::read(node).expect("Node must already exist in the development Pod");
|
||||
let python_bytes = fs::read(python).expect("Python must already exist in the development Pod");
|
||||
libraries(node, &mut dependencies);
|
||||
libraries(python, &mut dependencies);
|
||||
let markers = json!({"implementation_name":"cpython","implementation_version":"3.13.0","os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":"3.13.0","python_version":"3.13","sys_platform":"linux"});
|
||||
let node_pack=archive(&BTreeMap::from([
|
||||
("bin/node".into(),node_bytes),
|
||||
("shared.txt".into(),b"shared runtime fixture".to_vec()),
|
||||
("dever-runtime.json".into(),serde_json::to_vec(&json!({"format":"dever-worker-runtime-v1","ecosystem":"npm","target":target,"executable":"bin/node","arguments":[],"build":null,"python_wheel_tags":[],"python_extension_suffixes":[],"python_markers":null})).unwrap()),
|
||||
]));
|
||||
let python_pack=archive(&BTreeMap::from([
|
||||
("bin/python3".into(),python_bytes),
|
||||
("shared.txt".into(),b"shared runtime fixture".to_vec()),
|
||||
("dever-runtime.json".into(),serde_json::to_vec(&json!({"format":"dever-worker-runtime-v1","ecosystem":"pip","target":target,"executable":"bin/python3","arguments":["-I","-S","-B"],"build":null,"python_wheel_tags":["py3-none-any"],"python_extension_suffixes":[".cpython-313-x86_64-linux-gnu.so"],"python_markers":markers})).unwrap()),
|
||||
]));
|
||||
fixture.metadata.pack.sha256 = hash(&node_pack);
|
||||
fixture.metadata.pack.version = "24.21.0".into();
|
||||
let python_identity = json!({"name":"python","version":"3.13.0","sha256":hash(&python_pack)});
|
||||
let mut tools = BTreeMap::new();
|
||||
let mut executables = Vec::new();
|
||||
for (name, path) in [
|
||||
("sh", "/bin/sh"),
|
||||
("cc", "/usr/bin/cc"),
|
||||
("c++", "/usr/bin/c++"),
|
||||
("as", "/usr/bin/as"),
|
||||
("ld", "/usr/bin/ld"),
|
||||
("ar", "/usr/bin/ar"),
|
||||
("ranlib", "/usr/bin/ranlib"),
|
||||
("make", "/usr/bin/make"),
|
||||
("cc1", "/usr/libexec/gcc/x86_64-linux-gnu/14/cc1"),
|
||||
("collect2", "/usr/libexec/gcc/x86_64-linux-gnu/14/collect2"),
|
||||
] {
|
||||
let path = Path::new(path);
|
||||
libraries(path, &mut dependencies);
|
||||
let destination = format!("rootfs/usr/bin/{name}");
|
||||
tools.insert(destination.clone(), fs::read(path).unwrap());
|
||||
executables.push(destination);
|
||||
}
|
||||
files(
|
||||
Path::new("/usr/local/lib/node_modules/npm"),
|
||||
"npm/frontend/node_modules/npm/",
|
||||
&mut tools,
|
||||
);
|
||||
tools.insert(
|
||||
"rootfs/usr/bin/fixture-node".into(),
|
||||
b"#!/usr/bin/env node\nprocess.stdout.write('signed script');\n".to_vec(),
|
||||
);
|
||||
executables.push("rootfs/usr/bin/fixture-node".into());
|
||||
tools.insert("dever-build.json".into(),serde_json::to_vec(&json!({"format":"dever-lib-build-v1","ecosystem":"npm","target":target,"runtime_sha256":fixture.metadata.pack.sha256,"python_runtime_sha256":python_identity["sha256"],"executables":executables})).unwrap());
|
||||
let tools_pack = archive(&tools);
|
||||
assets.insert(format!("build/npm/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-build-v1","ecosystem":"npm","target":target,"runtime_sha256":fixture.metadata.pack.sha256,"python_runtime_sha256":python_identity["sha256"],"sha256":hash(&tools_pack)})).unwrap());
|
||||
assets.insert(format!("build/npm/{target}/build.pack"), tools_pack);
|
||||
assets.insert(format!("runtime/npm/{target}/runtime.pack"), node_pack);
|
||||
assets.insert(format!("runtime/pip/{target}/runtime.pack"), python_pack);
|
||||
assets.insert(format!("runtime/npm/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":fixture.metadata.pack,"target":target,"python_markers":null,"python_wheel_tags":[],"npm_libc":"glibc"}})).unwrap());
|
||||
assets.insert(format!("runtime/pip/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":python_identity,"target":target,"python_markers":markers,"python_wheel_tags":["py3-none-any"]}})).unwrap());
|
||||
for (name, bytes) in dependencies {
|
||||
assets.insert(format!("process-runtime/{target}/lib/{name}"), bytes);
|
||||
}
|
||||
fixture.signed = signed(&fixture.directory, &assets);
|
||||
}
|
||||
|
||||
fn files(root: &Path, prefix: &str, output: &mut BTreeMap<String, Vec<u8>>) {
|
||||
for entry in fs::read_dir(root).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let name = entry.file_name().into_string().unwrap();
|
||||
let kind = entry.file_type().unwrap();
|
||||
if kind.is_dir() {
|
||||
files(&entry.path(), &format!("{prefix}{name}/"), output);
|
||||
} else if kind.is_file() {
|
||||
output.insert(format!("{prefix}{name}"), fs::read(entry.path()).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn libraries(program: &Path, files: &mut BTreeMap<String, Vec<u8>>) {
|
||||
let output = Command::new("/usr/bin/ldd").arg(program).output().unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"ldd fixture input {}",
|
||||
program.display()
|
||||
);
|
||||
for path in String::from_utf8(output.stdout)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.filter(|value| value.starts_with('/'))
|
||||
{
|
||||
let path = Path::new(path);
|
||||
let name = path.file_name().unwrap().to_str().unwrap().to_owned();
|
||||
let bytes = fs::read(path).unwrap();
|
||||
if let Some(previous) = files.insert(name, bytes.clone()) {
|
||||
assert_eq!(previous, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user