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