164 lines
7.4 KiB
Rust
164 lines
7.4 KiB
Rust
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);
|
|
}
|