Files
core/test/rust/native/dependency/npm/build.rs
T
2026-11-03 03:32:07 +00:00

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);
}