feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,468 @@
|
||||
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;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Settings {
|
||||
runtime: RegistryRuntime,
|
||||
runtime_file: String,
|
||||
build: pack::Descriptor,
|
||||
build_file: String,
|
||||
}
|
||||
|
||||
struct AuthorInputs {
|
||||
root: PathBuf,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_build_pack_binds_runtime_layout_tools_and_archive_digest() {
|
||||
let runtime = registry_runtime(Ecosystem::Pip);
|
||||
let mut descriptor = pack::Descriptor {
|
||||
format: "dever-registry-build-v1".into(),
|
||||
ecosystem: Ecosystem::Pip,
|
||||
target: runtime.target.clone(),
|
||||
runtime_sha256: runtime.pack.sha256.clone(),
|
||||
python_runtime_sha256: None,
|
||||
sha256: "0".repeat(64),
|
||||
};
|
||||
descriptor.validate(&runtime, None).unwrap();
|
||||
let manifest = pack::Manifest {
|
||||
format: "dever-lib-build-v1".into(),
|
||||
ecosystem: Ecosystem::Pip,
|
||||
target: runtime.target.clone(),
|
||||
runtime_sha256: runtime.pack.sha256.clone(),
|
||||
python_runtime_sha256: None,
|
||||
executables: ["sh", "cc", "c++", "as", "ld", "ar", "ranlib", "make"]
|
||||
.into_iter()
|
||||
.map(|name| format!("rootfs/usr/bin/{name}"))
|
||||
.collect(),
|
||||
};
|
||||
let mut files = manifest
|
||||
.executables
|
||||
.iter()
|
||||
.map(|path| (path.clone(), b"#!/bin/sh\nexit 0\n".to_vec()))
|
||||
.collect::<Vec<_>>();
|
||||
files.push((
|
||||
"dever-build.json".into(),
|
||||
serde_json::to_vec(&manifest).unwrap(),
|
||||
));
|
||||
manifest.validate(&files, &descriptor).unwrap();
|
||||
let mut changed = descriptor.clone();
|
||||
changed.runtime_sha256 = "f".repeat(64);
|
||||
assert!(changed.validate(&runtime, None).is_err());
|
||||
assert!(manifest.validate(&files, &changed).is_err());
|
||||
let mut changed = files.clone();
|
||||
changed.push(("rootfs/etc/ld.so.preload".into(), b"injected".to_vec()));
|
||||
assert!(manifest.validate(&changed, &descriptor).is_err());
|
||||
let mut changed = manifest.clone();
|
||||
changed.executables.push("rootfs/usr/bin/absent".into());
|
||||
assert!(changed.validate(&files, &descriptor).is_err());
|
||||
let mut archive = tar::Builder::new(flate2::write::GzEncoder::new(
|
||||
Vec::new(),
|
||||
flate2::Compression::default(),
|
||||
));
|
||||
for (path, bytes) in files {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
archive
|
||||
.append_data(&mut header, path, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
let bytes = archive.into_inner().unwrap().finish().unwrap();
|
||||
descriptor.sha256 = digest(&bytes);
|
||||
pack::unpack(&bytes, &descriptor).unwrap();
|
||||
let mut changed = bytes;
|
||||
changed[0] ^= 1;
|
||||
assert!(pack::unpack(&changed, &descriptor).is_err());
|
||||
}
|
||||
|
||||
impl Inputs for AuthorInputs {
|
||||
fn diagnostic(&self, output: &[u8]) {
|
||||
eprintln!("{}", String::from_utf8_lossy(output));
|
||||
}
|
||||
|
||||
fn runtime(&self, ecosystem: Ecosystem) -> Result<(RegistryRuntime, Vec<u8>), String> {
|
||||
if ecosystem != Ecosystem::Pip {
|
||||
return Err("fixture only supplies Python".into());
|
||||
}
|
||||
let bytes = fs::read(self.root.join(&self.settings.runtime_file))
|
||||
.map_err(|error| error.to_string())?;
|
||||
if digest(&bytes) != self.settings.runtime.pack.sha256 {
|
||||
return Err("author runtime drift".into());
|
||||
}
|
||||
Ok((self.settings.runtime.clone(), bytes))
|
||||
}
|
||||
fn tools(&self, ecosystem: Ecosystem) -> Result<(pack::Descriptor, Vec<u8>), String> {
|
||||
if ecosystem != Ecosystem::Pip {
|
||||
return Err("fixture only supplies Python".into());
|
||||
}
|
||||
Ok((
|
||||
self.settings.build.clone(),
|
||||
fs::read(self.root.join(&self.settings.build_file))
|
||||
.map_err(|error| error.to_string())?,
|
||||
))
|
||||
}
|
||||
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
Ok(sandbox::assets(&workspace)
|
||||
.into_iter()
|
||||
.map(|asset| {
|
||||
let _mode = asset.executable;
|
||||
(
|
||||
asset.path.trim_start_matches("sandbox/").into(),
|
||||
asset.bytes,
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit author operation: assembles previously hashed native-build-inputs; no network or host execution"]
|
||||
fn prepare_source_build_tool_fixture() {
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let runtime: RegistryRuntime = serde_json::from_value(
|
||||
serde_json::from_slice::<serde_json::Value>(
|
||||
&fs::read(workspace.join("target/python-managed/config/setting.json")).unwrap(),
|
||||
)
|
||||
.unwrap()["runtime"]
|
||||
.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let bytes = fs::read(workspace.join("target/python-managed/runtime.pack")).unwrap();
|
||||
assert_eq!(digest(&bytes), runtime.pack.sha256);
|
||||
drop(bytes);
|
||||
let root = workspace.join("target/source-build");
|
||||
let descriptor = prepare_tool_pack(Ecosystem::Pip, &runtime, None, &root);
|
||||
fs::write(
|
||||
root.join("config/setting.json"),
|
||||
serde_json::to_vec_pretty(&Settings {
|
||||
runtime,
|
||||
runtime_file: "../python-managed/runtime.pack".into(),
|
||||
build: descriptor,
|
||||
build_file: "build.pack".into(),
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(super) fn prepare_tool_pack(
|
||||
ecosystem: Ecosystem,
|
||||
runtime: &RegistryRuntime,
|
||||
python: Option<&RegistryRuntime>,
|
||||
root: &std::path::Path,
|
||||
) -> pack::Descriptor {
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let inputs = workspace.join("target/native-build-inputs");
|
||||
let document: serde_json::Value =
|
||||
serde_json::from_slice(&fs::read(inputs.join("inputs.json")).unwrap()).unwrap();
|
||||
let mut descriptor = pack::Descriptor {
|
||||
format: "dever-registry-build-v1".into(),
|
||||
ecosystem: ecosystem.clone(),
|
||||
target: runtime.target.clone(),
|
||||
runtime_sha256: runtime.pack.sha256.clone(),
|
||||
python_runtime_sha256: python.map(|runtime| runtime.pack.sha256.clone()),
|
||||
sha256: "0".repeat(64),
|
||||
};
|
||||
descriptor.validate(runtime, python).unwrap();
|
||||
let manifest = pack::Manifest {
|
||||
format: "dever-lib-build-v1".into(),
|
||||
ecosystem: ecosystem.clone(),
|
||||
target: runtime.target.clone(),
|
||||
runtime_sha256: runtime.pack.sha256.clone(),
|
||||
python_runtime_sha256: descriptor.python_runtime_sha256.clone(),
|
||||
executables: serde_json::from_value(document[ecosystem.as_str()]["executables"].clone())
|
||||
.unwrap(),
|
||||
};
|
||||
let mut files: Vec<(String, Vec<u8>)> = vec![(
|
||||
"dever-build.json".into(),
|
||||
serde_json::to_vec(&manifest).unwrap(),
|
||||
)];
|
||||
for input in document[ecosystem.as_str()]["files"].as_array().unwrap() {
|
||||
let source = inputs.join(input["source"].as_str().unwrap());
|
||||
assert!(fs::symlink_metadata(&source).unwrap().is_file());
|
||||
let bytes = fs::read(&source).unwrap();
|
||||
assert_eq!(digest(&bytes), input["sha256"].as_str().unwrap());
|
||||
files.push((input["path"].as_str().unwrap().into(), bytes));
|
||||
}
|
||||
files.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
manifest.validate(&files, &descriptor).unwrap();
|
||||
let assets = sandbox::assets(&workspace)
|
||||
.into_iter()
|
||||
.map(|asset| {
|
||||
(
|
||||
asset.path.trim_start_matches("sandbox/").into(),
|
||||
asset.bytes,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pack::validate_tools(&manifest, &files, &assets).unwrap();
|
||||
fs::create_dir_all(root.join("config")).unwrap();
|
||||
let encoder = flate2::GzBuilder::new()
|
||||
.mtime(0)
|
||||
.operating_system(255)
|
||||
.write(
|
||||
fs::File::create(root.join("build.pack")).unwrap(),
|
||||
flate2::Compression::default(),
|
||||
);
|
||||
let mut archive = tar::Builder::new(encoder);
|
||||
for (path, bytes) in files {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(if manifest.executables.contains(&path) {
|
||||
0o755
|
||||
} else {
|
||||
0o644
|
||||
});
|
||||
header.set_uid(0);
|
||||
header.set_gid(0);
|
||||
header.set_mtime(0);
|
||||
header.set_cksum();
|
||||
archive
|
||||
.append_data(&mut header, path, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
archive.into_inner().unwrap().finish().unwrap();
|
||||
descriptor.sha256 = digest(&fs::read(root.join("build.pack")).unwrap());
|
||||
descriptor
|
||||
}
|
||||
|
||||
struct SourceOnly(dever_cli::libs::HttpRegistry);
|
||||
|
||||
#[test]
|
||||
fn incompatible_sdist_candidate_backtracks_before_loading_build_tools() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
python_release(&mut registry, "app", "1.0.0", &[], &["backend>=1"]);
|
||||
python_release(&mut registry, "backend", "1.0.0", &[], &[]);
|
||||
python_release(&mut registry, "backend", "2.0.0", &[], &[]);
|
||||
let path = (Ecosystem::Pip, "/pypi/backend/2.0.0/json".into());
|
||||
let mut release: serde_json::Value = serde_json::from_slice(®istry.0[&path]).unwrap();
|
||||
release["urls"] = serde_json::json!([{"packagetype":"sdist","requires_python":">=3.13","filename":"backend-2.0.0.tar.gz","url":"must-not-fetch","digests":{"sha256":"0".repeat(64)}}]);
|
||||
registry
|
||||
.0
|
||||
.insert(path, serde_json::to_vec(&release).unwrap());
|
||||
let lock = python_lock(®istry, &["pip:app@1.0.0"]).unwrap();
|
||||
assert!(
|
||||
lock.libs
|
||||
.iter()
|
||||
.any(|lib| lib.spec.key() == "pip:backend@1.0.0")
|
||||
);
|
||||
}
|
||||
|
||||
fn backend_fixture(mutate: bool) -> LocalRegistry {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
python_release(&mut registry, "buildhelper", "1.0.0", &[], &[]);
|
||||
registry.0.insert(
|
||||
(Ecosystem::Pip, "/pypi/sample/json".into()),
|
||||
serde_json::to_vec(&serde_json::json!({"releases":{"1.0.0":[]}})).unwrap(),
|
||||
);
|
||||
let helper = wheel_fixture::pack(
|
||||
"buildhelper",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("buildhelper", "1.0.0", &[], &[]),
|
||||
&[
|
||||
(
|
||||
"buildhelper.py",
|
||||
b"def cli():\n print('isolated-cli')\n".to_vec(),
|
||||
),
|
||||
(
|
||||
"buildhelper-1.0.0.dist-info/entry_points.txt",
|
||||
b"[console_scripts]\nbuild-probe = buildhelper:cli\n".to_vec(),
|
||||
),
|
||||
],
|
||||
);
|
||||
let key = (Ecosystem::Pip, "/pypi/buildhelper/1.0.0/json".into());
|
||||
let mut release: serde_json::Value = serde_json::from_slice(®istry.0[&key]).unwrap();
|
||||
let url = release["urls"][0]["url"].as_str().unwrap().to_owned();
|
||||
release["urls"][0]["digests"]["sha256"] = serde_json::json!(digest(&helper));
|
||||
registry
|
||||
.0
|
||||
.insert(key, serde_json::to_vec(&release).unwrap());
|
||||
registry.0.insert((Ecosystem::Pip, url), helper);
|
||||
let original = wheel_fixture::metadata("sample", "1.0.0", &[], &[]);
|
||||
let changed = format!("{original}Summary: changed by wheel hook\n");
|
||||
let wheel = wheel_fixture::pack(
|
||||
"sample",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
if mutate { &changed } else { &original },
|
||||
&[],
|
||||
);
|
||||
let backend = format!(
|
||||
r#"import os, shutil, subprocess
|
||||
from pathlib import Path
|
||||
def get_requires_for_build_wheel(config_settings=None):
|
||||
try:
|
||||
import buildhelper
|
||||
return []
|
||||
except ImportError:
|
||||
return ['buildhelper==1.0.0']
|
||||
def prepare_metadata_for_build_wheel(directory, config_settings=None):
|
||||
assert subprocess.check_output(['build-probe'], text=True).strip() == 'isolated-cli'
|
||||
metadata = Path(directory) / 'sample-1.0.0.dist-info'
|
||||
metadata.mkdir()
|
||||
shutil.copy('original-metadata', metadata / 'METADATA')
|
||||
os.chdir('/')
|
||||
return metadata.name
|
||||
def build_wheel(directory, config_settings=None, metadata_directory=None):
|
||||
assert Path.cwd().name == 'source', 'previous hook leaked its cwd'
|
||||
if {mutate}:
|
||||
shutil.copy('changed-metadata', Path(metadata_directory) / 'METADATA')
|
||||
name = 'sample-1.0.0-py3-none-any.whl'
|
||||
shutil.copy('result.whl', Path(directory) / name)
|
||||
return name
|
||||
"#,
|
||||
mutate = if mutate { "True" } else { "False" }
|
||||
);
|
||||
let sourcefiles = vec![
|
||||
(
|
||||
"sample-1.0.0/pyproject.toml",
|
||||
b"[build-system]\nrequires = []\nbuild-backend = 'backend'\nbackend-path = ['.']\n"
|
||||
.to_vec(),
|
||||
),
|
||||
("sample-1.0.0/backend.py", backend.into_bytes()),
|
||||
("sample-1.0.0/original-metadata", original.into_bytes()),
|
||||
("sample-1.0.0/changed-metadata", changed.into_bytes()),
|
||||
("sample-1.0.0/result.whl", wheel),
|
||||
];
|
||||
let mut tar = tar::Builder::new(flate2::write::GzEncoder::new(
|
||||
Vec::new(),
|
||||
flate2::Compression::default(),
|
||||
));
|
||||
for (name, bytes) in sourcefiles {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
tar.append_data(&mut header, name, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
let source = tar.into_inner().unwrap().finish().unwrap();
|
||||
registry.0.insert((Ecosystem::Pip,"/pypi/sample/1.0.0/json".into()),serde_json::to_vec(&serde_json::json!({"info":{},"urls":[{"packagetype":"sdist","filename":"sample-1.0.0.tar.gz","url":"sample-source","digests":{"sha256":digest(&source)}}]})).unwrap());
|
||||
registry
|
||||
.0
|
||||
.insert((Ecosystem::Pip, "sample-source".into()), source);
|
||||
registry
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "isolated offline PEP517 contracts; requires explicit target/source-build tool/runtime fixture"]
|
||||
fn pep517_hooks_keep_requirements_cli_and_metadata_boundaries() {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/source-build");
|
||||
let settings: Settings =
|
||||
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
|
||||
let inputs = AuthorInputs { root, settings };
|
||||
for mutate in [false, true] {
|
||||
let session = Session::new(&inputs);
|
||||
let transport = backend_fixture(mutate);
|
||||
let store = FixtureArtifactStore::default();
|
||||
let resolver = RegistryResolver {
|
||||
transport: &transport,
|
||||
store: &store,
|
||||
runtimes: BTreeMap::from([(Ecosystem::Pip, inputs.settings.runtime.clone())]),
|
||||
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
|
||||
build: Some(&session),
|
||||
};
|
||||
let result = dever_cli::libs::LibResolver::resolve(
|
||||
&resolver,
|
||||
&["pip:sample@1.0.0".parse().unwrap()],
|
||||
);
|
||||
if mutate {
|
||||
assert!(result.unwrap_err().contains("metadata differs"));
|
||||
continue;
|
||||
}
|
||||
let lock = result.unwrap();
|
||||
dever_cli::libs::doctor(&lock).unwrap();
|
||||
assert_eq!(lock.builds[0].dynamic_requires, vec!["buildhelper==1.0.0"]);
|
||||
for mutation in 0..3 {
|
||||
let mut altered = lock.clone();
|
||||
match mutation {
|
||||
0 => altered.builds[0].tools.sha256 = "not-a-hash".into(),
|
||||
1 => altered.builds[0].dynamic_requires = vec!["buildhelper>=2".into()],
|
||||
_ => *altered.builds[0].dependencies = LockFile::new(vec![]).unwrap(),
|
||||
}
|
||||
altered.libs[0].build = Some(altered.builds[0].identity().unwrap());
|
||||
assert!(
|
||||
dever_cli::libs::doctor(&altered).is_err(),
|
||||
"accepted contradictory receipt {mutation}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl RegistryTransport for SourceOnly {
|
||||
fn get(&self, ecosystem: Ecosystem, path: &str, limit: usize) -> Result<Vec<u8>, String> {
|
||||
let bytes = self.0.get(ecosystem, path, limit)?;
|
||||
// Explicit author acceptance forces this one package's official sdist.
|
||||
// Backend requirements still use their ordinary authenticated wheels.
|
||||
if path == "/pypi/simplejson/3.20.1/json" {
|
||||
let mut document: serde_json::Value =
|
||||
serde_json::from_slice(&bytes).map_err(|error| error.to_string())?;
|
||||
document["urls"]
|
||||
.as_array_mut()
|
||||
.ok_or("official release lacks files")?
|
||||
.retain(|file| file["packagetype"] == "sdist");
|
||||
serde_json::to_vec(&document).map_err(|error| error.to_string())
|
||||
} else {
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit official PyPI source build: downloads fixed source/build requirements and executes signed managed build tools"]
|
||||
fn builds_official_python_source_with_isolated_pep517() {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/source-build");
|
||||
let settings: Settings =
|
||||
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
|
||||
let inputs = AuthorInputs {
|
||||
root: root.clone(),
|
||||
settings,
|
||||
};
|
||||
let session = Session::new(&inputs);
|
||||
let transport = SourceOnly(dever_cli::libs::HttpRegistry::official());
|
||||
let store = FixtureArtifactStore::default();
|
||||
let resolver = RegistryResolver {
|
||||
transport: &transport,
|
||||
store: &store,
|
||||
runtimes: BTreeMap::from([(Ecosystem::Pip, inputs.settings.runtime.clone())]),
|
||||
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
|
||||
build: Some(&session),
|
||||
};
|
||||
let requested = vec!["pip:simplejson@3.20.1".parse().unwrap()];
|
||||
let lock = dever_cli::libs::LibResolver::resolve(&resolver, &requested).unwrap();
|
||||
dever_cli::libs::doctor(&lock).unwrap();
|
||||
assert_eq!(lock.builds.len(), 1);
|
||||
assert!(!lock.builds[0].dependencies.libs.is_empty());
|
||||
let artifact = &lock
|
||||
.libs
|
||||
.iter()
|
||||
.find(|lib| lib.spec.name == "simplejson")
|
||||
.unwrap()
|
||||
.artifacts[0];
|
||||
let wheel = store
|
||||
.verify_exact(&artifact.sha256, artifact.bytes, &artifact.target)
|
||||
.unwrap();
|
||||
let mut archive = zip::ZipArchive::new(Cursor::new(wheel)).unwrap();
|
||||
assert!((0..archive.len()).any(|index| {
|
||||
let file = archive.by_index(index).unwrap();
|
||||
file.name().contains("_speedups.") && file.name().ends_with(".so")
|
||||
}));
|
||||
assert_eq!(
|
||||
lock,
|
||||
dever_cli::libs::LibResolver::resolve(&resolver, &requested).unwrap(),
|
||||
"same session must replay one exact built artifact"
|
||||
);
|
||||
publish_fixture_artifacts(&root.join("dependency"), &lock, &store);
|
||||
let mut altered = lock.clone();
|
||||
altered.builds[0].frontend = "0".repeat(64);
|
||||
assert!(dever_cli::libs::doctor(&altered).is_err());
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
use super::*;
|
||||
use dever_cli::libs::{LibResolver, LockedWorker, npm, resolve_workers};
|
||||
|
||||
pub(super) fn archive(
|
||||
name: &str,
|
||||
version: &str,
|
||||
mut manifest: serde_json::Value,
|
||||
extra: &[(&str, Vec<u8>)],
|
||||
) -> Vec<u8> {
|
||||
manifest["name"] = name.into();
|
||||
manifest["version"] = version.into();
|
||||
let encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
let mut writer = tar::Builder::new(encoder);
|
||||
for (name, bytes) in [
|
||||
(
|
||||
"package/package.json",
|
||||
serde_json::to_vec(&manifest).unwrap(),
|
||||
),
|
||||
(
|
||||
"package/index.js",
|
||||
format!("module.exports = '{name}/{version}';\n").into_bytes(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(extra.iter().map(|(name, bytes)| (*name, bytes.clone())))
|
||||
{
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
writer
|
||||
.append_data(&mut header, name, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
writer.into_inner().unwrap().finish().unwrap()
|
||||
}
|
||||
|
||||
pub(super) fn release(
|
||||
registry: &mut LocalRegistry,
|
||||
name: &str,
|
||||
version: &str,
|
||||
manifest: serde_json::Value,
|
||||
extra: &[(&str, Vec<u8>)],
|
||||
) {
|
||||
let archive = archive(name, version, manifest.clone(), extra);
|
||||
let index_path = (Ecosystem::Npm, format!("/{name}"));
|
||||
let mut index = registry
|
||||
.0
|
||||
.get(&index_path)
|
||||
.map(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).unwrap())
|
||||
.unwrap_or(serde_json::json!({"versions":{}}));
|
||||
index["versions"][version] = serde_json::json!({});
|
||||
registry
|
||||
.0
|
||||
.insert(index_path, serde_json::to_vec(&index).unwrap());
|
||||
let mut document = manifest;
|
||||
document["name"] = name.into();
|
||||
document["version"] = version.into();
|
||||
let url = format!("https://mock/{name}/{version}.tgz");
|
||||
document["dist"] = serde_json::json!({"tarball":url,"integrity":format!("sha512-{}", base64::engine::general_purpose::STANDARD.encode(Sha512::digest(&archive)))});
|
||||
registry.0.insert(
|
||||
(Ecosystem::Npm, format!("/{name}/{version}")),
|
||||
serde_json::to_vec(&document).unwrap(),
|
||||
);
|
||||
registry.0.insert((Ecosystem::Npm, url), archive);
|
||||
}
|
||||
|
||||
fn resolved(registry: &LocalRegistry, requested: &[&str]) -> (LockFile, FixtureArtifactStore) {
|
||||
let store = FixtureArtifactStore::default();
|
||||
let lock = local_resolver(registry, &store)
|
||||
.resolve(
|
||||
&requested
|
||||
.iter()
|
||||
.map(|spec| spec.parse().unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
doctor(&lock).unwrap();
|
||||
assert_eq!(LockFile::decode(&lock.encode().unwrap()).unwrap(), lock);
|
||||
(lock, store)
|
||||
}
|
||||
|
||||
fn files(lock: &LockFile, store: &FixtureArtifactStore, index: usize) -> BTreeMap<String, Vec<u8>> {
|
||||
let archives = lock
|
||||
.libs
|
||||
.iter()
|
||||
.map(|lib| {
|
||||
(
|
||||
lib.spec.clone(),
|
||||
store
|
||||
.verify_exact(
|
||||
&lib.artifacts[0].sha256,
|
||||
lib.artifacts[0].bytes,
|
||||
&lib.artifacts[0].target,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
npm::files(&lock.npm[index], lock, |lib| {
|
||||
Ok(archives[&lib.spec].as_slice())
|
||||
})
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_versions_and_shared_packages_keep_distinct_peer_contexts() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
for (name, dependencies) in [
|
||||
("left", serde_json::json!({"plugin":"1.0.0","host":"1.0.0"})),
|
||||
(
|
||||
"right",
|
||||
serde_json::json!({"plugin":"1.0.0","host":"2.0.0"}),
|
||||
),
|
||||
] {
|
||||
release(
|
||||
&mut registry,
|
||||
name,
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":dependencies}),
|
||||
&[],
|
||||
);
|
||||
}
|
||||
release(
|
||||
&mut registry,
|
||||
"plugin",
|
||||
"1.0.0",
|
||||
serde_json::json!({"peerDependencies":{"host":">=1.0.0 <3.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
for version in ["1.0.0", "2.0.0"] {
|
||||
release(&mut registry, "host", version, serde_json::json!({}), &[]);
|
||||
}
|
||||
let (lock, store) = resolved(®istry, &["npm:left@1.0.0", "npm:right@1.0.0"]);
|
||||
assert_eq!(lock.libs.len(), 5);
|
||||
let files = files(&lock, &store, 0);
|
||||
for (side, version) in [("left", "1.0.0"), ("right", "2.0.0")] {
|
||||
let node = lock.npm[0]
|
||||
.instances
|
||||
.iter()
|
||||
.find(|node| node.path == format!("node_modules/{side}/node_modules/plugin"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
node.edges[0].target.as_deref(),
|
||||
Some(format!("node_modules/{side}/node_modules/host").as_str())
|
||||
);
|
||||
assert!(
|
||||
String::from_utf8(
|
||||
files[&format!("node_modules/{side}/node_modules/host/index.js")].clone()
|
||||
)
|
||||
.unwrap()
|
||||
.contains(version)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_dependencies_override_regular_edges_and_record_target_or_absence() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(
|
||||
&mut registry,
|
||||
"root",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"child":"1.0.0"},"optionalDependencies":{"child":"2.0.0","missing":"*","darwin":"*","musl":"*"}}),
|
||||
&[],
|
||||
);
|
||||
release(&mut registry, "child", "2.0.0", serde_json::json!({}), &[]);
|
||||
release(
|
||||
&mut registry,
|
||||
"darwin",
|
||||
"1.0.0",
|
||||
serde_json::json!({"os":["darwin"]}),
|
||||
&[],
|
||||
);
|
||||
release(
|
||||
&mut registry,
|
||||
"musl",
|
||||
"1.0.0",
|
||||
serde_json::json!({"os":["linux"],"libc":["musl"]}),
|
||||
&[],
|
||||
);
|
||||
let (lock, store) = resolved(®istry, &["npm:root@1.0.0"]);
|
||||
assert_eq!(lock.libs.len(), 2);
|
||||
let root = lock.npm[0]
|
||||
.instances
|
||||
.iter()
|
||||
.find(|node| node.spec.name == "root")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
root.edges
|
||||
.iter()
|
||||
.find(|edge| edge.name == "child")
|
||||
.unwrap()
|
||||
.kind,
|
||||
npm::Kind::Optional
|
||||
);
|
||||
for name in ["darwin", "musl"] {
|
||||
assert_eq!(
|
||||
root.edges
|
||||
.iter()
|
||||
.find(|edge| edge.name == name)
|
||||
.unwrap()
|
||||
.omission,
|
||||
Some(npm::Omission::Target)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
root.edges
|
||||
.iter()
|
||||
.find(|edge| edge.name == "missing")
|
||||
.unwrap()
|
||||
.omission,
|
||||
Some(npm::Omission::Unavailable)
|
||||
);
|
||||
assert!(
|
||||
!files(&lock, &store, 0)
|
||||
.keys()
|
||||
.any(|path| path.contains("darwin") || path.contains("musl"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_integrity_and_protocol_failures_are_never_omissions() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(
|
||||
&mut registry,
|
||||
"root",
|
||||
"1.0.0",
|
||||
serde_json::json!({"optionalDependencies":{"child":"*"}}),
|
||||
&[],
|
||||
);
|
||||
release(&mut registry, "child", "1.0.0", serde_json::json!({}), &[]);
|
||||
registry.0.insert(
|
||||
(Ecosystem::Npm, "https://mock/child/1.0.0.tgz".into()),
|
||||
b"tampered".to_vec(),
|
||||
);
|
||||
let store = FixtureArtifactStore::default();
|
||||
assert!(
|
||||
local_resolver(®istry, &store)
|
||||
.resolve(&["npm:root@1.0.0".parse().unwrap()])
|
||||
.unwrap_err()
|
||||
.contains("integrity mismatch")
|
||||
);
|
||||
release(
|
||||
&mut registry,
|
||||
"child",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"other":"not-a-semver"}}),
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
local_resolver(®istry, &store)
|
||||
.resolve(&["npm:root@1.0.0".parse().unwrap()])
|
||||
.unwrap_err()
|
||||
.contains("invalid npm range")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_peers_auto_install_and_backtrack_but_optional_peers_do_not() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(
|
||||
&mut registry,
|
||||
"root",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"plugin":"*","host":"1.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
release(
|
||||
&mut registry,
|
||||
"plugin",
|
||||
"2.0.0",
|
||||
serde_json::json!({"peerDependencies":{"host":"2.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
release(
|
||||
&mut registry,
|
||||
"plugin",
|
||||
"1.0.0",
|
||||
serde_json::json!({"peerDependencies":{"host":"1.0.0","absent":"*"},"peerDependenciesMeta":{"absent":{"optional":true}}}),
|
||||
&[],
|
||||
);
|
||||
for version in ["1.0.0", "2.0.0"] {
|
||||
release(&mut registry, "host", version, serde_json::json!({}), &[]);
|
||||
}
|
||||
let (lock, _) = resolved(®istry, &["npm:root@1.0.0"]);
|
||||
let plugin = lock.npm[0]
|
||||
.instances
|
||||
.iter()
|
||||
.find(|node| node.spec.name == "plugin")
|
||||
.unwrap();
|
||||
assert_eq!(plugin.spec.version, "1.0.0");
|
||||
assert_eq!(
|
||||
plugin
|
||||
.edges
|
||||
.iter()
|
||||
.find(|edge| edge.name == "absent")
|
||||
.unwrap()
|
||||
.omission,
|
||||
Some(npm::Omission::OptionalPeerAbsent)
|
||||
);
|
||||
let (lock, _) = resolved(®istry, &["npm:plugin@1.0.0"]);
|
||||
assert!(
|
||||
lock.npm[0]
|
||||
.instances
|
||||
.iter()
|
||||
.any(|node| node.path == "node_modules/host")
|
||||
);
|
||||
let store = FixtureArtifactStore::default();
|
||||
assert!(
|
||||
local_resolver(®istry, &store)
|
||||
.resolve(&[
|
||||
"npm:plugin@2.0.0".parse().unwrap(),
|
||||
"npm:host@1.0.0".parse().unwrap()
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_transitive_packages_are_bound_to_parent_and_never_downloaded() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
let extra = vec![
|
||||
(
|
||||
"package/node_modules/b/package.json",
|
||||
br#"{"name":"b","version":"1.0.0","dependencies":{"c":"2.0.0"}}"#.to_vec(),
|
||||
),
|
||||
(
|
||||
"package/node_modules/b/index.js",
|
||||
b"module.exports = require('c');".to_vec(),
|
||||
),
|
||||
(
|
||||
"package/node_modules/c/package.json",
|
||||
br#"{"name":"c","version":"2.0.0"}"#.to_vec(),
|
||||
),
|
||||
(
|
||||
"package/node_modules/c/index.js",
|
||||
b"module.exports = 42;".to_vec(),
|
||||
),
|
||||
];
|
||||
release(
|
||||
&mut registry,
|
||||
"root",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"b":"1.0.0"},"bundleDependencies":["b"]}),
|
||||
&extra,
|
||||
);
|
||||
let (lock, store) = resolved(®istry, &["npm:root@1.0.0"]);
|
||||
assert_eq!(lock.libs.len(), 1);
|
||||
assert_eq!(lock.npm[0].instances.len(), 3);
|
||||
assert_eq!(
|
||||
files(&lock, &store, 0)["node_modules/root/node_modules/c/index.js"],
|
||||
b"module.exports = 42;"
|
||||
);
|
||||
release(
|
||||
&mut registry,
|
||||
"root",
|
||||
"1.0.0",
|
||||
serde_json::json!({}),
|
||||
&extra,
|
||||
);
|
||||
assert!(
|
||||
local_resolver(®istry, &store)
|
||||
.resolve(&["npm:root@1.0.0".parse().unwrap()])
|
||||
.unwrap_err()
|
||||
.contains("undeclared")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_root_sets_keep_separate_npm_environments_and_detect_lock_tampering() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(
|
||||
&mut registry,
|
||||
"plugin",
|
||||
"1.0.0",
|
||||
serde_json::json!({"peerDependencies":{"host":"*"}}),
|
||||
&[],
|
||||
);
|
||||
for version in ["1.0.0", "2.0.0"] {
|
||||
release(&mut registry, "host", version, serde_json::json!({}), &[]);
|
||||
}
|
||||
let store = FixtureArtifactStore::default();
|
||||
let worker = |adapter: &str, version: &str| LockedWorker {
|
||||
port: "sample.worker".into(),
|
||||
adapter: adapter.into(),
|
||||
ecosystem: "npm".into(),
|
||||
runtime: Some(registry_runtime(Ecosystem::Npm).pack),
|
||||
entry: format!("worker/{adapter}.mjs"),
|
||||
schema: "checked-port".into(),
|
||||
capabilities: vec![],
|
||||
operations: vec!["read".into()],
|
||||
libs: vec![
|
||||
"npm:plugin@1.0.0".parse().unwrap(),
|
||||
format!("npm:host@{version}").parse().unwrap(),
|
||||
],
|
||||
};
|
||||
let lock = resolve_workers(
|
||||
&local_resolver(®istry, &store),
|
||||
&[],
|
||||
vec![worker("first", "1.0.0"), worker("second", "2.0.0")],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(lock.npm.len(), 2);
|
||||
for index in 0..2 {
|
||||
assert!(files(&lock, &store, index).contains_key("node_modules/plugin/index.js"));
|
||||
}
|
||||
let mut changed = lock.clone();
|
||||
changed.npm[0].instances[0].path = "../escape".into();
|
||||
assert!(doctor(&changed).is_err());
|
||||
let mut changed = lock.clone();
|
||||
changed.npm[0]
|
||||
.instances
|
||||
.iter_mut()
|
||||
.find(|node| node.spec.name == "plugin")
|
||||
.unwrap()
|
||||
.edges[0]
|
||||
.target = Some("node_modules/missing".into());
|
||||
assert!(doctor(&changed).is_err());
|
||||
let mut changed = lock;
|
||||
changed.npm[0]
|
||||
.instances
|
||||
.iter_mut()
|
||||
.find(|node| node.spec.name == "plugin")
|
||||
.unwrap()
|
||||
.edges
|
||||
.clear();
|
||||
let archives = changed
|
||||
.libs
|
||||
.iter()
|
||||
.map(|lib| {
|
||||
(
|
||||
lib.spec.clone(),
|
||||
store
|
||||
.verify_exact(
|
||||
&lib.artifacts[0].sha256,
|
||||
lib.artifacts[0].bytes,
|
||||
&lib.artifacts[0].target,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert!(
|
||||
npm::files(&changed.npm[0], &changed, |lib| Ok(
|
||||
archives[&lib.spec].as_slice()
|
||||
))
|
||||
.unwrap_err()
|
||||
.contains("differs from package.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_instances_cannot_switch_to_another_parents_same_version_package() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
for name in ["a", "c"] {
|
||||
release(
|
||||
&mut registry,
|
||||
name,
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"b":"1.0.0"},"bundleDependencies":["b"]}),
|
||||
&[
|
||||
(
|
||||
"package/node_modules/b/package.json",
|
||||
br#"{"name":"b","version":"1.0.0"}"#.to_vec(),
|
||||
),
|
||||
(
|
||||
"package/node_modules/b/index.js",
|
||||
format!("module.exports = '{name}';").into_bytes(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
release(&mut registry, "b", "1.0.0", serde_json::json!({}), &[]);
|
||||
let (original, store) = resolved(®istry, &["npm:a@1.0.0", "npm:c@1.0.0", "npm:b@1.0.0"]);
|
||||
let archives = original
|
||||
.libs
|
||||
.iter()
|
||||
.map(|lib| {
|
||||
(
|
||||
lib.spec.clone(),
|
||||
store
|
||||
.verify_exact(
|
||||
&lib.artifacts[0].sha256,
|
||||
lib.artifacts[0].bytes,
|
||||
&lib.artifacts[0].target,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for (archive, prefix) in [
|
||||
("npm:c@1.0.0", "package/node_modules/b/"),
|
||||
("npm:b@1.0.0", "package/"),
|
||||
] {
|
||||
let mut changed = original.clone();
|
||||
changed.npm[0]
|
||||
.instances
|
||||
.iter_mut()
|
||||
.find(|node| node.path == "node_modules/a/node_modules/b")
|
||||
.unwrap()
|
||||
.source = npm::Source {
|
||||
archive: archive.parse().unwrap(),
|
||||
prefix: prefix.into(),
|
||||
};
|
||||
assert!(
|
||||
doctor(&changed)
|
||||
.unwrap_err()
|
||||
.contains("parent archive installation")
|
||||
);
|
||||
assert!(
|
||||
npm::files(&changed.npm[0], &changed, |lib| Ok(
|
||||
archives[&lib.spec].as_slice()
|
||||
))
|
||||
.unwrap_err()
|
||||
.contains("parent archive installation")
|
||||
);
|
||||
// Removing the locked layout cannot hide the original archive binding.
|
||||
for node in &mut changed.npm[0].instances {
|
||||
node.bundled.clear();
|
||||
}
|
||||
assert!(
|
||||
npm::files(&changed.npm[0], &changed, |lib| Ok(
|
||||
archives[&lib.spec].as_slice()
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_and_optional_dependencies_override_same_named_peers() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(&mut registry, "host", "2.0.0", serde_json::json!({}), &[]);
|
||||
release(
|
||||
&mut registry,
|
||||
"plugin",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"host":"2.0.0"},"peerDependencies":{"host":"1.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
let (lock, _) = resolved(®istry, &["npm:plugin@1.0.0", "npm:host@2.0.0"]);
|
||||
let plugin = lock.npm[0]
|
||||
.instances
|
||||
.iter()
|
||||
.find(|node| node.spec.name == "plugin")
|
||||
.unwrap();
|
||||
assert_eq!(plugin.edges.len(), 1);
|
||||
assert_eq!(plugin.edges[0].kind, npm::Kind::Dependency);
|
||||
assert_eq!(
|
||||
plugin.edges[0].target.as_deref(),
|
||||
Some("node_modules/plugin/node_modules/host")
|
||||
);
|
||||
release(
|
||||
&mut registry,
|
||||
"plugin",
|
||||
"1.0.0",
|
||||
serde_json::json!({"optionalDependencies":{"absent":"*"},"peerDependencies":{"absent":"1.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
let (lock, _) = resolved(®istry, &["npm:plugin@1.0.0"]);
|
||||
assert_eq!(lock.npm[0].instances[0].edges.len(), 1);
|
||||
assert_eq!(lock.npm[0].instances[0].edges[0].kind, npm::Kind::Optional);
|
||||
assert_eq!(
|
||||
lock.npm[0].instances[0].edges[0].omission,
|
||||
Some(npm::Omission::Unavailable)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_a_root_prunes_its_entire_dependency_cycle() {
|
||||
for bundled in [false, true] {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(&mut registry, "z", "1.0.0", serde_json::json!({}), &[]);
|
||||
let b = serde_json::json!({"name":"b","version":"1.0.0","dependencies":{"a":"1.0.0"}});
|
||||
let mut a = serde_json::json!({"dependencies":{"b":"1.0.0"}});
|
||||
let extra = if bundled {
|
||||
a["bundleDependencies"] = serde_json::json!(["b"]);
|
||||
vec![(
|
||||
"package/node_modules/b/package.json",
|
||||
serde_json::to_vec(&b).unwrap(),
|
||||
)]
|
||||
} else {
|
||||
release(&mut registry, "b", "1.0.0", b, &[]);
|
||||
vec![]
|
||||
};
|
||||
release(&mut registry, "a", "1.0.0", a, &extra);
|
||||
let (lock, _) = resolved(®istry, &["npm:a@1.0.0", "npm:z@1.0.0"]);
|
||||
let root = TemporaryDirectory::new();
|
||||
fs::create_dir(root.path().join("config")).unwrap();
|
||||
fs::write(
|
||||
root.path().join("config/setting.json"),
|
||||
br#"{"lib":["npm:a@1.0.0","npm:z@1.0.0"]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
lock.write_atomic(root.path()).unwrap();
|
||||
dever_cli::libs::execute("remove", root.path(), &["npm:a@1.0.0".into()]).unwrap();
|
||||
let retained =
|
||||
LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
|
||||
assert_eq!(retained.libs.len(), 1);
|
||||
assert_eq!(retained.libs[0].spec.name, "z");
|
||||
assert_eq!(retained.npm[0].instances.len(), 1);
|
||||
doctor(&retained).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_a_dependency_still_required_by_a_remaining_root_is_rejected() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(
|
||||
&mut registry,
|
||||
"a",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"b":"1.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
release(&mut registry, "b", "1.0.0", serde_json::json!({}), &[]);
|
||||
let (lock, _) = resolved(®istry, &["npm:a@1.0.0", "npm:b@1.0.0"]);
|
||||
let root = TemporaryDirectory::new();
|
||||
fs::create_dir(root.path().join("config")).unwrap();
|
||||
let setting = br#"{"lib":["npm:a@1.0.0","npm:b@1.0.0"]}"#;
|
||||
fs::write(root.path().join("config/setting.json"), setting).unwrap();
|
||||
lock.write_atomic(root.path()).unwrap();
|
||||
let before = fs::read(root.path().join("dever.lock")).unwrap();
|
||||
assert!(
|
||||
dever_cli::libs::execute("remove", root.path(), &["npm:b@1.0.0".into()])
|
||||
.unwrap_err()
|
||||
.contains("required by")
|
||||
);
|
||||
assert_eq!(fs::read(root.path().join("dever.lock")).unwrap(), before);
|
||||
assert_eq!(
|
||||
fs::read(root.path().join("config/setting.json")).unwrap(),
|
||||
setting
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_archive_instances_are_bounded_before_materialization_clones() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
release(
|
||||
&mut registry,
|
||||
"leaf",
|
||||
"1.0.0",
|
||||
serde_json::json!({}),
|
||||
&[("package/payload.js", vec![b'x'; 2 * 1024 * 1024])],
|
||||
);
|
||||
let mut roots = Vec::new();
|
||||
for number in 0..129 {
|
||||
let name = format!("root-{number}");
|
||||
release(
|
||||
&mut registry,
|
||||
&name,
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"leaf":"1.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
roots.push(format!("npm:{name}@1.0.0"));
|
||||
}
|
||||
let (lock, store) = resolved(
|
||||
®istry,
|
||||
&roots.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(lock.npm[0].instances.len(), 258);
|
||||
let archives = lock
|
||||
.libs
|
||||
.iter()
|
||||
.map(|lib| {
|
||||
(
|
||||
lib.spec.clone(),
|
||||
store
|
||||
.verify_exact(
|
||||
&lib.artifacts[0].sha256,
|
||||
lib.artifacts[0].bytes,
|
||||
&lib.artifacts[0].target,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert!(
|
||||
npm::files(
|
||||
&lock.npm[0],
|
||||
&lock,
|
||||
|lib| Ok(archives[&lib.spec].as_slice())
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("tree file/byte budget")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
use super::*;
|
||||
use dever_cli::libs::build::{Inputs, NativeRejection, Receipt, Session, pack};
|
||||
use dever_cli::libs::{LibResolver, npm};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Settings {
|
||||
runtime: RegistryRuntime,
|
||||
runtime_file: String,
|
||||
python: RegistryRuntime,
|
||||
python_file: String,
|
||||
build: pack::Descriptor,
|
||||
build_file: String,
|
||||
}
|
||||
|
||||
struct AuthorInputs {
|
||||
root: PathBuf,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_lifecycle_output_preserves_locked_identity_and_instance_ownership() {
|
||||
let environment = npm::Environment::single("npm:app@1.0.0".parse().unwrap());
|
||||
let files = vec![
|
||||
(
|
||||
"node_modules/app/package.json".into(),
|
||||
br#"{"name":"app","version":"1.0.0"}"#.to_vec(),
|
||||
),
|
||||
(
|
||||
"node_modules/app/generated.js".into(),
|
||||
b"module.exports=42;".to_vec(),
|
||||
),
|
||||
];
|
||||
npm::validate_built_files(&environment, &files).unwrap();
|
||||
let mut changed = files.clone();
|
||||
changed[0].1 =
|
||||
br#"{"name":"app","version":"1.0.0","dependencies":{"injected":"1.0.0"}}"#.to_vec();
|
||||
assert!(npm::validate_built_files(&environment, &changed).is_err());
|
||||
let mut changed = files.clone();
|
||||
changed.push(("node_modules/injected/index.js".into(), vec![]));
|
||||
assert!(npm::validate_built_files(&environment, &changed).is_err());
|
||||
let mut changed = files.clone();
|
||||
changed.push(("node_modules/app/../escape".into(), vec![]));
|
||||
assert!(npm::validate_built_files(&environment, &changed).is_err());
|
||||
assert!(npm::validate_built_files(&environment, &files[1..]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_registry_install_hooks_do_not_run_publishing_lifecycle() {
|
||||
let metadata = |script: &str| {
|
||||
vec![(
|
||||
"node_modules/app/package.json".into(),
|
||||
serde_json::to_vec(&serde_json::json!({"scripts":{script:"node command.js"}})).unwrap(),
|
||||
)]
|
||||
};
|
||||
for script in ["preinstall", "install", "postinstall"] {
|
||||
assert!(npm::requires_build(&metadata(script)).unwrap());
|
||||
}
|
||||
for script in ["prepare", "prepack", "prepublish"] {
|
||||
assert!(!npm::requires_build(&metadata(script)).unwrap());
|
||||
}
|
||||
assert!(
|
||||
npm::requires_build(&[("node_modules/app/binding.gyp".into(), b"{}".to_vec())]).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
fn locked_installation_fixture() -> (LockFile, Vec<u8>) {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
npm_tests::release(
|
||||
&mut registry,
|
||||
"app",
|
||||
"1.0.0",
|
||||
serde_json::json!({}),
|
||||
&[(
|
||||
"package/prebuilds/foreign/addon.node",
|
||||
b"foreign-object".to_vec(),
|
||||
)],
|
||||
);
|
||||
npm_tests::release(&mut registry, "other", "1.0.0", serde_json::json!({}), &[]);
|
||||
let store = FixtureArtifactStore::default();
|
||||
let mut lock = local_resolver(®istry, &store)
|
||||
.resolve(&[
|
||||
"npm:app@1.0.0".parse().unwrap(),
|
||||
"npm:other@1.0.0".parse().unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
let sources = lock
|
||||
.libs
|
||||
.iter()
|
||||
.flat_map(|lib| lib.artifacts.clone())
|
||||
.collect();
|
||||
let archives = lock
|
||||
.libs
|
||||
.iter()
|
||||
.map(|lib| {
|
||||
(
|
||||
lib.spec.clone(),
|
||||
store
|
||||
.verify_exact(
|
||||
&lib.artifacts[0].sha256,
|
||||
lib.artifacts[0].bytes,
|
||||
&lib.artifacts[0].target,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let files = npm::build_files(
|
||||
&lock.npm[0],
|
||||
&lock,
|
||||
|lib| Ok(archives[&lib.spec].as_slice()),
|
||||
)
|
||||
.unwrap();
|
||||
let mut writer = tar::Builder::new(flate2::write::GzEncoder::new(
|
||||
Vec::new(),
|
||||
flate2::Compression::default(),
|
||||
));
|
||||
for (path, bytes) in files {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
writer
|
||||
.append_data(&mut header, path, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
let bytes = writer.into_inner().unwrap().finish().unwrap();
|
||||
let runtime = registry_runtime(Ecosystem::Npm);
|
||||
let python = registry_runtime(Ecosystem::Pip);
|
||||
let hash = digest(&bytes);
|
||||
let receipt = Receipt {
|
||||
ecosystem: Ecosystem::Npm,
|
||||
roots: lock.npm[0].roots.clone(),
|
||||
sources,
|
||||
graph: Some(lock.npm[0].clone()),
|
||||
runtime: runtime.pack.clone(),
|
||||
auxiliary_runtime: Some(python.pack.clone()),
|
||||
native_entries: Default::default(),
|
||||
native_rejections: BTreeMap::from([(
|
||||
"node_modules/app/prebuilds/foreign/addon.node".into(),
|
||||
NativeRejection::Format,
|
||||
)]),
|
||||
python_markers: None,
|
||||
tools: pack::Descriptor {
|
||||
format: "dever-registry-build-v1".into(),
|
||||
ecosystem: Ecosystem::Npm,
|
||||
target: runtime.target.clone(),
|
||||
runtime_sha256: runtime.pack.sha256.clone(),
|
||||
python_runtime_sha256: Some(python.pack.sha256),
|
||||
sha256: "a".repeat(64),
|
||||
},
|
||||
frontend: digest(
|
||||
&[
|
||||
include_bytes!("../../../crates/dever-cli/src/libs/build/npm.js").as_slice(),
|
||||
include_bytes!("../../../crates/dever-cli/src/libs/build/npm/probe.cjs").as_slice(),
|
||||
]
|
||||
.concat(),
|
||||
),
|
||||
python: None,
|
||||
dynamic_requires: vec![],
|
||||
dependencies: Box::new(LockFile::new(vec![]).unwrap()),
|
||||
config_settings: BTreeMap::new(),
|
||||
output: dever_cli::libs::LockedArtifact {
|
||||
target: runtime.target,
|
||||
path: format!("build/npm/{hash}/installation.tgz"),
|
||||
bytes: bytes.len() as u64,
|
||||
sha256: hash,
|
||||
},
|
||||
};
|
||||
lock.npm[0].build = Some(receipt.identity().unwrap());
|
||||
lock.builds.push(receipt);
|
||||
doctor(&lock).unwrap();
|
||||
(lock, bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_expanded_outputs_are_pruned_only_when_no_raw_consumer_remains() {
|
||||
use dever_cli::libs::{LockedWorker, prune_expanded_resources};
|
||||
use dever_core::native::EmbeddedResource;
|
||||
|
||||
let (mut lock, bytes) = locked_installation_fixture();
|
||||
let roots = lock.npm[0].roots.clone();
|
||||
let other = vec!["npm:other@1.0.0".parse().unwrap()];
|
||||
lock.npm = npm::retain(&lock, &[roots.clone(), other.clone()]).unwrap();
|
||||
let output = &lock.builds[0].output;
|
||||
let managed = LockedWorker {
|
||||
port: "sample.worker".into(),
|
||||
adapter: "default".into(),
|
||||
ecosystem: "npm".into(),
|
||||
runtime: Some(registry_runtime(Ecosystem::Npm).pack),
|
||||
entry: "worker/main.cjs".into(),
|
||||
schema: "checked".into(),
|
||||
capabilities: vec![],
|
||||
operations: vec!["probe".into()],
|
||||
libs: roots.clone(),
|
||||
};
|
||||
let resource = EmbeddedResource {
|
||||
path: output.path.clone(),
|
||||
bytes,
|
||||
sha256: output.sha256.clone(),
|
||||
executable: false,
|
||||
};
|
||||
let mut resources = vec![resource.clone()];
|
||||
prune_expanded_resources(&lock, std::slice::from_ref(&managed), &[], &mut resources).unwrap();
|
||||
assert!(
|
||||
resources.is_empty(),
|
||||
"expanded installation archive leaked into program"
|
||||
);
|
||||
|
||||
// A projected environment shares its original build output with the full
|
||||
// Worker. Retention follows current consumers, not the receipt's old roots.
|
||||
for declared in [roots, other] {
|
||||
let mut resources = vec![resource.clone()];
|
||||
prune_expanded_resources(
|
||||
&lock,
|
||||
std::slice::from_ref(&managed),
|
||||
&declared,
|
||||
&mut resources,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resources.len(), 1);
|
||||
}
|
||||
let mut exec = managed.clone();
|
||||
exec.ecosystem = "exec".into();
|
||||
exec.runtime = None;
|
||||
let mut resources = vec![resource];
|
||||
prune_expanded_resources(&lock, &[managed, exec], &[], &mut resources).unwrap();
|
||||
assert_eq!(resources.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_native_admission_accounts_every_addon_and_prunes_unsupported_objects() {
|
||||
let (lock, bytes) = locked_installation_fixture();
|
||||
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
|
||||
assert!(files.iter().all(|(path, _, _)| !path.ends_with(".node")));
|
||||
let mut missing = lock.clone();
|
||||
missing.builds[0].native_rejections.clear();
|
||||
missing.npm[0].build = Some(missing.builds[0].identity().unwrap());
|
||||
assert!(
|
||||
dever_cli::libs::build::npm_installation(&missing, &missing.npm[0], &bytes)
|
||||
.unwrap_err()
|
||||
.contains("exact addon files")
|
||||
);
|
||||
let mut corrupted = bytes.clone();
|
||||
corrupted[10] ^= 1;
|
||||
assert!(dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &corrupted).is_err());
|
||||
for change in ["tools", "graph", "runtime"] {
|
||||
let mut changed = lock.clone();
|
||||
match change {
|
||||
"tools" => changed.builds[0].tools.sha256 = "not-a-hash".into(),
|
||||
"graph" => changed.builds[0].graph.as_mut().unwrap().instances.clear(),
|
||||
_ => changed.builds[0].runtime.sha256 = "b".repeat(64),
|
||||
}
|
||||
changed.npm[0].build = Some(changed.builds[0].identity().unwrap());
|
||||
assert!(
|
||||
doctor(&changed).is_err(),
|
||||
"accepted internally inconsistent {change}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_locked_output_projects_retained_roots_without_rerunning_hooks() {
|
||||
let (lock, bytes) = locked_installation_fixture();
|
||||
let project = TemporaryDirectory::new();
|
||||
fs::create_dir(project.path().join("config")).unwrap();
|
||||
fs::write(
|
||||
project.path().join("config/setting.json"),
|
||||
br#"{"lib":["npm:app@1.0.0","npm:other@1.0.0"]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
lock.write_atomic(project.path()).unwrap();
|
||||
dever_cli::libs::execute("remove", project.path(), &["npm:app@1.0.0".into()]).unwrap();
|
||||
let retained = LockFile::decode(&fs::read(project.path().join("dever.lock")).unwrap()).unwrap();
|
||||
doctor(&retained).unwrap();
|
||||
let files =
|
||||
dever_cli::libs::build::npm_installation(&retained, &retained.npm[0], &bytes).unwrap();
|
||||
assert_eq!(retained.builds, lock.builds);
|
||||
assert!(
|
||||
files
|
||||
.iter()
|
||||
.all(|(path, _, _)| path.starts_with("node_modules/other/"))
|
||||
);
|
||||
}
|
||||
|
||||
impl AuthorInputs {
|
||||
fn load() -> Self {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/npm-build");
|
||||
let settings =
|
||||
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
|
||||
Self { root, settings }
|
||||
}
|
||||
}
|
||||
|
||||
impl Inputs for AuthorInputs {
|
||||
fn runtime(&self, ecosystem: Ecosystem) -> Result<(RegistryRuntime, Vec<u8>), String> {
|
||||
let (runtime, path) = match ecosystem {
|
||||
Ecosystem::Pip => (&self.settings.python, &self.settings.python_file),
|
||||
Ecosystem::Npm => (&self.settings.runtime, &self.settings.runtime_file),
|
||||
_ => return Err("fixture only supplies Node and auxiliary Python".into()),
|
||||
};
|
||||
let bytes = fs::read(self.root.join(path)).map_err(|error| error.to_string())?;
|
||||
if digest(&bytes) != runtime.pack.sha256 {
|
||||
return Err("author runtime drift".into());
|
||||
}
|
||||
Ok((runtime.clone(), bytes))
|
||||
}
|
||||
fn tools(&self, ecosystem: Ecosystem) -> Result<(pack::Descriptor, Vec<u8>), String> {
|
||||
if ecosystem != Ecosystem::Npm {
|
||||
return Err("fixture only supplies npm tools".into());
|
||||
}
|
||||
Ok((
|
||||
self.settings.build.clone(),
|
||||
fs::read(self.root.join(&self.settings.build_file))
|
||||
.map_err(|error| error.to_string())?,
|
||||
))
|
||||
}
|
||||
fn assets(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
Ok(build_tests::sandbox::assets(&workspace)
|
||||
.into_iter()
|
||||
.map(|asset| {
|
||||
(
|
||||
asset.path.trim_start_matches("sandbox/").into(),
|
||||
asset.bytes,
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
fn diagnostic(&self, output: &[u8]) {
|
||||
eprintln!("{}", String::from_utf8_lossy(output));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit author operation: assembles hashed npm/compiler/header inputs; no network or host execution"]
|
||||
fn prepare_npm_build_tool_fixture() {
|
||||
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let runtime = |name: &str| {
|
||||
let root = workspace.join(format!("target/{name}-managed"));
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
|
||||
let runtime: RegistryRuntime = serde_json::from_value(config["runtime"].clone()).unwrap();
|
||||
assert_eq!(
|
||||
digest(&fs::read(root.join("runtime.pack")).unwrap()),
|
||||
runtime.pack.sha256
|
||||
);
|
||||
runtime
|
||||
};
|
||||
let node = runtime("npm");
|
||||
let python = runtime("python");
|
||||
let root = workspace.join("target/npm-build");
|
||||
let build = build_tests::prepare_tool_pack(Ecosystem::Npm, &node, Some(&python), &root);
|
||||
let settings = Settings {
|
||||
runtime: node,
|
||||
runtime_file: "../npm-managed/runtime.pack".into(),
|
||||
python,
|
||||
python_file: "../python-managed/runtime.pack".into(),
|
||||
build,
|
||||
build_file: "build.pack".into(),
|
||||
};
|
||||
fs::write(
|
||||
root.join("config/setting.json"),
|
||||
serde_json::to_vec_pretty(&settings).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn resolver<'a>(
|
||||
inputs: &AuthorInputs,
|
||||
session: &'a Session<'a>,
|
||||
transport: &'a dyn RegistryTransport,
|
||||
store: &'a FixtureArtifactStore,
|
||||
) -> RegistryResolver<'a> {
|
||||
RegistryResolver {
|
||||
transport,
|
||||
store,
|
||||
runtimes: BTreeMap::from([(Ecosystem::Npm, inputs.settings.runtime.clone())]),
|
||||
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
|
||||
build: Some(session),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit official npm native source build; network only in registry resolver, never in lifecycle sandbox"]
|
||||
fn builds_official_npm_addon_with_locked_lifecycle() {
|
||||
let inputs = AuthorInputs::load();
|
||||
let session = Session::new(&inputs);
|
||||
let transport = dever_cli::libs::HttpRegistry::official();
|
||||
let store = FixtureArtifactStore::default();
|
||||
let resolver = resolver(&inputs, &session, &transport, &store);
|
||||
let roots = vec!["npm:bufferutil@4.0.9".parse().unwrap()];
|
||||
let lock = resolver.resolve(&roots).unwrap();
|
||||
doctor(&lock).unwrap();
|
||||
assert_eq!(lock.builds.len(), 1);
|
||||
let receipt = &lock.builds[0];
|
||||
assert!(
|
||||
receipt
|
||||
.native_entries
|
||||
.contains("node_modules/bufferutil/build/Release/bufferutil.node")
|
||||
);
|
||||
let bytes = store
|
||||
.verify_exact(
|
||||
&receipt.output.sha256,
|
||||
receipt.output.bytes,
|
||||
&receipt.output.target,
|
||||
)
|
||||
.unwrap();
|
||||
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
|
||||
assert!(files.iter().any(
|
||||
|(path, bytes, _)| path.ends_with("/build/Release/bufferutil.node")
|
||||
&& bytes.starts_with(b"\x7fELF")
|
||||
));
|
||||
assert_eq!(lock, resolver.resolve(&roots).unwrap());
|
||||
publish_fixture_artifacts(&inputs.root.join("dependency"), &lock, &store);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit managed npm hooks; uses local registry and real signed author runtime/tools"]
|
||||
fn npm_lifecycle_preserves_optional_failures_bins_and_offline_remove() {
|
||||
let inputs = AuthorInputs::load();
|
||||
let session = Session::new(&inputs);
|
||||
let mut transport = LocalRegistry(BTreeMap::new());
|
||||
npm_tests::release(&mut transport, "helper", "1.0.0", serde_json::json!({"bin":{"build-probe":"cli.js"}}), &[("package/cli.js", b"#!/usr/bin/env node\nrequire('fs').writeFileSync('generated.js','module.exports = 42;');\n".to_vec())]);
|
||||
npm_tests::release(
|
||||
&mut transport,
|
||||
"broken",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"cycle":"1.0.0"},"scripts":{"install":"node -e 'process.exit(7)'"}}),
|
||||
&[],
|
||||
);
|
||||
npm_tests::release(
|
||||
&mut transport,
|
||||
"cycle",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"broken":"1.0.0"}}),
|
||||
&[],
|
||||
);
|
||||
npm_tests::release(
|
||||
&mut transport,
|
||||
"app",
|
||||
"1.0.0",
|
||||
serde_json::json!({"dependencies":{"helper":"1.0.0"},"optionalDependencies":{"broken":"1.0.0"},"scripts":{"postinstall":"build-probe"}}),
|
||||
&[],
|
||||
);
|
||||
npm_tests::release(&mut transport, "other", "1.0.0", serde_json::json!({}), &[]);
|
||||
let store = FixtureArtifactStore::default();
|
||||
let resolver = resolver(&inputs, &session, &transport, &store);
|
||||
let lock = resolver
|
||||
.resolve(&[
|
||||
"npm:app@1.0.0".parse().unwrap(),
|
||||
"npm:other@1.0.0".parse().unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
doctor(&lock).unwrap();
|
||||
assert!(
|
||||
!lock
|
||||
.libs
|
||||
.iter()
|
||||
.any(|lib| matches!(lib.spec.name.as_str(), "broken" | "cycle"))
|
||||
);
|
||||
assert!(
|
||||
lock.npm[0]
|
||||
.instances
|
||||
.iter()
|
||||
.flat_map(|node| &node.edges)
|
||||
.any(|edge| edge.omission == Some(npm::Omission::BuildFailure))
|
||||
);
|
||||
let receipt = &lock.builds[0];
|
||||
let bytes = store
|
||||
.verify_exact(
|
||||
&receipt.output.sha256,
|
||||
receipt.output.bytes,
|
||||
&receipt.output.target,
|
||||
)
|
||||
.unwrap();
|
||||
let files = dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
|
||||
assert!(
|
||||
files
|
||||
.iter()
|
||||
.any(|(path, bytes, _)| path == "node_modules/app/generated.js"
|
||||
&& bytes == b"module.exports = 42;")
|
||||
);
|
||||
assert!(
|
||||
resolver
|
||||
.resolve(&["npm:broken@1.0.0".parse().unwrap()])
|
||||
.unwrap_err()
|
||||
.contains("failed")
|
||||
);
|
||||
let project = TemporaryDirectory::new();
|
||||
fs::create_dir(project.path().join("config")).unwrap();
|
||||
fs::write(
|
||||
project.path().join("config/setting.json"),
|
||||
br#"{"lib":["npm:app@1.0.0","npm:other@1.0.0"]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
lock.write_atomic(project.path()).unwrap();
|
||||
dever_cli::libs::execute("remove", project.path(), &["npm:app@1.0.0".into()]).unwrap();
|
||||
let retained = LockFile::decode(&fs::read(project.path().join("dever.lock")).unwrap()).unwrap();
|
||||
doctor(&retained).unwrap();
|
||||
let files =
|
||||
dever_cli::libs::build::npm_installation(&retained, &retained.npm[0], &bytes).unwrap();
|
||||
assert!(
|
||||
files
|
||||
.iter()
|
||||
.all(|(path, _, _)| path.starts_with("node_modules/other/"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit managed npm hook: sparse scratch overflow and child cleanup; no network"]
|
||||
fn npm_build_scratch_budget_kills_owned_hook_and_cleans_stage() {
|
||||
let inputs = AuthorInputs::load();
|
||||
let session = Session::new(&inputs);
|
||||
let mut transport = LocalRegistry(BTreeMap::new());
|
||||
npm_tests::release(&mut transport,"oversized","1.0.0",serde_json::json!({"scripts":{"install":"node fill.js"}}),&[("package/fill.js",b"const fs=require('fs');fs.writeFileSync('large',Buffer.alloc(1));fs.truncateSync('large',600*1024*1024);setInterval(()=>{},1000);".to_vec())]);
|
||||
let store = FixtureArtifactStore::default();
|
||||
let resolver = resolver(&inputs, &session, &transport, &store);
|
||||
let owned_stages = || {
|
||||
fs::read_dir(std::env::temp_dir())
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
|
||||
.filter(|name| name.starts_with(&format!("dever-lib-build-{}-", std::process::id())))
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
};
|
||||
let before = owned_stages();
|
||||
let error = resolver
|
||||
.resolve(&["npm:oversized@1.0.0".parse().unwrap()])
|
||||
.unwrap_err();
|
||||
assert!(error.contains("scratch budget"), "{error}");
|
||||
assert_eq!(
|
||||
owned_stages(),
|
||||
before,
|
||||
"failed hook leaked its owned staging tree"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit managed npm hook: internal output aliases are flattened; aliases outside output are rejected"]
|
||||
fn npm_build_output_hard_links_stay_inside_the_exported_tree() {
|
||||
let inputs = AuthorInputs::load();
|
||||
let session = Session::new(&inputs);
|
||||
for (name, source, accepted) in [
|
||||
(
|
||||
"insidealias",
|
||||
"const fs=require('fs');fs.writeFileSync('original.txt','same-bytes');fs.linkSync('original.txt','alias.txt');",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"outsidealias",
|
||||
"const fs=require('fs');fs.writeFileSync('/data/build/outside.txt','outside');fs.linkSync('/data/build/outside.txt','alias.txt');",
|
||||
false,
|
||||
),
|
||||
] {
|
||||
let mut transport = LocalRegistry(BTreeMap::new());
|
||||
npm_tests::release(
|
||||
&mut transport,
|
||||
name,
|
||||
"1.0.0",
|
||||
serde_json::json!({"scripts":{"install":"node aliases.js"}}),
|
||||
&[("package/aliases.js", source.as_bytes().to_vec())],
|
||||
);
|
||||
let store = FixtureArtifactStore::default();
|
||||
let result =
|
||||
resolver(&inputs, &session, &transport, &store)
|
||||
.resolve(&[format!("npm:{name}@1.0.0").parse().unwrap()]);
|
||||
if accepted {
|
||||
let lock = result.unwrap();
|
||||
doctor(&lock).unwrap();
|
||||
let receipt = &lock.builds[0];
|
||||
let bytes = store
|
||||
.verify_exact(
|
||||
&receipt.output.sha256,
|
||||
receipt.output.bytes,
|
||||
&receipt.output.target,
|
||||
)
|
||||
.unwrap();
|
||||
let files =
|
||||
dever_cli::libs::build::npm_installation(&lock, &lock.npm[0], &bytes).unwrap();
|
||||
for leaf in ["original.txt", "alias.txt"] {
|
||||
assert!(files.iter().any(|(path, bytes, _)| path
|
||||
== &format!("node_modules/{name}/{leaf}")
|
||||
&& bytes == b"same-bytes"));
|
||||
}
|
||||
} else {
|
||||
assert!(result.unwrap_err().contains("hard-link alias outside"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
use super::*;
|
||||
use dever_cli::libs::sumdb::{self, ChecksumDatabase, Evidence, Verifier};
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
|
||||
fn key() -> Ed25519KeyPair {
|
||||
Ed25519KeyPair::from_seed_unchecked(&[47; 32]).unwrap()
|
||||
}
|
||||
|
||||
pub(super) fn verifier() -> Verifier {
|
||||
let mut encoded = vec![1];
|
||||
encoded.extend(key().public_key().as_ref());
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(b"dever-sumdb-test\n");
|
||||
hash.update(&encoded);
|
||||
let id = hash.finalize();
|
||||
Verifier::new(&format!(
|
||||
"dever-sumdb-test+{}+{}",
|
||||
id[..4]
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>(),
|
||||
base64::engine::general_purpose::STANDARD.encode(encoded)
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn signed(size: usize, root: [u8; 32]) -> String {
|
||||
let text = format!(
|
||||
"go.sum database tree\n{size}\n{}\n",
|
||||
base64::engine::general_purpose::STANDARD.encode(root)
|
||||
);
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(b"dever-sumdb-test\n");
|
||||
hash.update([1]);
|
||||
hash.update(key().public_key().as_ref());
|
||||
let mut signature = hash.finalize()[..4].to_vec();
|
||||
signature.extend(key().sign(text.as_bytes()).as_ref());
|
||||
format!(
|
||||
"{text}\n— dever-sumdb-test {}\n",
|
||||
base64::engine::general_purpose::STANDARD.encode(signature)
|
||||
)
|
||||
}
|
||||
|
||||
fn leaf(record: &[u8]) -> [u8; 32] {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update([0]);
|
||||
hash.update(record);
|
||||
hash.finalize().into()
|
||||
}
|
||||
|
||||
fn root(hashes: &[[u8; 32]]) -> [u8; 32] {
|
||||
if hashes.is_empty() {
|
||||
return Sha256::digest([]).into();
|
||||
}
|
||||
if hashes.len() == 1 {
|
||||
return hashes[0];
|
||||
}
|
||||
let split = hashes.len().next_power_of_two() / 2;
|
||||
let mut hash = Sha256::new();
|
||||
hash.update([1]);
|
||||
hash.update(root(&hashes[..split]));
|
||||
hash.update(root(&hashes[split..]));
|
||||
hash.finalize().into()
|
||||
}
|
||||
|
||||
fn unescape(path: &str) -> String {
|
||||
let mut chars = path.chars();
|
||||
let mut output = String::new();
|
||||
while let Some(ch) = chars.next() {
|
||||
output.push(if ch == '!' {
|
||||
chars.next().unwrap().to_ascii_uppercase()
|
||||
} else {
|
||||
ch
|
||||
});
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub(super) fn seal(registry: &mut LocalRegistry) {
|
||||
registry
|
||||
.0
|
||||
.retain(|(_, path), _| !path.starts_with("https://sum.golang.org/"));
|
||||
let records = registry
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|((ecosystem, path), bytes)| {
|
||||
if *ecosystem != Ecosystem::Go || !path.ends_with(".mod") {
|
||||
return None;
|
||||
}
|
||||
let (module, version) = path.strip_prefix('/')?.split_once("/@v/v")?;
|
||||
let version = version.strip_suffix(".mod")?;
|
||||
let module = unescape(module);
|
||||
let archive = registry.0.get(&(
|
||||
Ecosystem::Go,
|
||||
format!("{}zip", path.strip_suffix("mod").unwrap()),
|
||||
))?;
|
||||
let record = format!(
|
||||
"{module} v{version} {}\n{module} v{version}/go.mod {}\n",
|
||||
sumdb::zip_hash(&module, version, archive).unwrap(),
|
||||
sumdb::mod_hash(bytes)
|
||||
);
|
||||
Some((
|
||||
format!(
|
||||
"/lookup/{}@v{version}",
|
||||
path.strip_prefix('/')
|
||||
.unwrap()
|
||||
.split("/@v/")
|
||||
.next()
|
||||
.unwrap()
|
||||
),
|
||||
record,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let hashes = records
|
||||
.iter()
|
||||
.map(|(_, record)| leaf(record.as_bytes()))
|
||||
.collect::<Vec<_>>();
|
||||
let checkpoint = signed(records.len(), root(&hashes));
|
||||
registry.0.insert(
|
||||
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
|
||||
checkpoint.as_bytes().to_vec(),
|
||||
);
|
||||
for (id, (path, record)) in records.iter().enumerate() {
|
||||
registry.0.insert(
|
||||
(Ecosystem::Go, format!("https://sum.golang.org{path}")),
|
||||
format!("{id}\n{record}\n{checkpoint}").into_bytes(),
|
||||
);
|
||||
}
|
||||
// Fixtures deliberately remain below one tile. Larger trees use official
|
||||
// captured vectors, not a second production tile-address implementation.
|
||||
assert!(hashes.len() < 256);
|
||||
if !hashes.is_empty() {
|
||||
registry.0.insert(
|
||||
(
|
||||
Ecosystem::Go,
|
||||
format!("https://sum.golang.org/tile/8/0/000.p/{}", hashes.len()),
|
||||
),
|
||||
hashes.iter().flatten().copied().collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Transport<'a> {
|
||||
pub registry: &'a dyn RegistryTransport,
|
||||
pub checksums: &'a LocalRegistry,
|
||||
}
|
||||
impl RegistryTransport for Transport<'_> {
|
||||
fn get(&self, ecosystem: Ecosystem, path: &str, limit: usize) -> Result<Vec<u8>, String> {
|
||||
if path.starts_with("https://sum.golang.org/") {
|
||||
self.checksums.get(ecosystem, path, limit)
|
||||
} else {
|
||||
self.registry.get(ecosystem, path, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolved(
|
||||
registry: &LocalRegistry,
|
||||
previous: Option<String>,
|
||||
roots: &[&str],
|
||||
) -> Result<LockFile, String> {
|
||||
let cache = FixtureArtifactStore::default();
|
||||
let mut resolver = local_resolver(registry, &cache);
|
||||
resolver.go_sumdb = ChecksumDatabase::new(verifier(), previous)?;
|
||||
dever_cli::libs::LibResolver::resolve(
|
||||
&resolver,
|
||||
&roots
|
||||
.iter()
|
||||
.map(|root| root.parse().unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticated_modules_replay_offline_and_reject_content_or_proof_drift() {
|
||||
let registry = local_registry();
|
||||
let lock = resolved(®istry, None, &["go:example.com/a@1.0.0"]).unwrap();
|
||||
let evidence = &lock.go_sumdb[0];
|
||||
let checked = evidence.verify(&verifier()).unwrap();
|
||||
let archive = ®istry.0[&(Ecosystem::Go, "/example.com/a/@v/v1.0.0.zip".into())];
|
||||
checked
|
||||
.verify_zip("example.com/a", "1.0.0", archive)
|
||||
.unwrap();
|
||||
assert!(
|
||||
evidence
|
||||
.verify(&Verifier::official())
|
||||
.unwrap_err()
|
||||
.contains("trusted signature")
|
||||
);
|
||||
let mut changed = evidence.clone();
|
||||
changed.records[0].go_mod.push_str("// changed\n");
|
||||
assert!(changed.verify(&verifier()).unwrap_err().contains("go.mod"));
|
||||
let mut changed = evidence.clone();
|
||||
changed.records[0].module = "wrong.example/a".into();
|
||||
assert!(
|
||||
changed
|
||||
.verify(&verifier())
|
||||
.unwrap_err()
|
||||
.contains("different module/version")
|
||||
);
|
||||
let mut changed = evidence.clone();
|
||||
changed.records[0].inclusion[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
|
||||
assert!(
|
||||
changed
|
||||
.verify(&verifier())
|
||||
.unwrap_err()
|
||||
.contains("inclusion proof")
|
||||
);
|
||||
let mut changed = evidence.clone();
|
||||
changed.checkpoint = changed
|
||||
.checkpoint
|
||||
.replace("go.sum database tree", "go.sum database free");
|
||||
assert!(
|
||||
changed
|
||||
.verify(&verifier())
|
||||
.unwrap_err()
|
||||
.contains("signature verification")
|
||||
);
|
||||
assert!(
|
||||
checked
|
||||
.verify_zip(
|
||||
"example.com/a",
|
||||
"1.0.0",
|
||||
&zip_file("example.com/a@v1.0.0/changed.go")
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("sumdb h1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_updates_require_consistency_and_never_move_backwards() {
|
||||
let mut first = local_registry();
|
||||
first
|
||||
.0
|
||||
.remove(&(Ecosystem::Go, "/example.com/b/@v/v1.2.0.mod".into()));
|
||||
first.0.insert(
|
||||
(Ecosystem::Go, "/example.com/a/@v/v1.0.0.mod".into()),
|
||||
b"module example.com/a\n".to_vec(),
|
||||
);
|
||||
seal(&mut first);
|
||||
let old = resolved(&first, None, &["go:example.com/a@1.0.0"]).unwrap();
|
||||
let checkpoint = old.go_sumdb[0].verify(&verifier()).unwrap().checkpoint;
|
||||
let mut next = first;
|
||||
next.0.insert(
|
||||
(Ecosystem::Go, "/example.com/z/@v/v1.0.0.mod".into()),
|
||||
b"module example.com/z\n".to_vec(),
|
||||
);
|
||||
next.0.insert(
|
||||
(Ecosystem::Go, "/example.com/z/@v/v1.0.0.zip".into()),
|
||||
zip_file("example.com/z@v1.0.0/z.go"),
|
||||
);
|
||||
seal(&mut next);
|
||||
let new = resolved(&next, Some(checkpoint.clone()), &["go:example.com/z@1.0.0"]).unwrap();
|
||||
let current = new.go_sumdb[0].verify(&verifier()).unwrap().checkpoint;
|
||||
assert!(resolved(&next, Some(current.clone()), &["go:example.com/z@1.0.0"]).is_ok());
|
||||
next.0.insert(
|
||||
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
|
||||
checkpoint.into_bytes(),
|
||||
);
|
||||
assert!(
|
||||
resolved(&next, Some(current), &["go:example.com/a@1.0.0"])
|
||||
.unwrap_err()
|
||||
.contains("rollback")
|
||||
);
|
||||
let mut fork = new.go_sumdb[0].clone();
|
||||
fork.consistency[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
|
||||
assert!(
|
||||
fork.verify(&verifier())
|
||||
.unwrap_err()
|
||||
.contains("conflicting history")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sumdb_verifier_key_and_upstream_signed_note_vectors() {
|
||||
let note = "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n";
|
||||
let evidence = Evidence {
|
||||
previous: None,
|
||||
checkpoint: note.into(),
|
||||
consistency: vec![],
|
||||
records: vec![],
|
||||
};
|
||||
evidence.verify(&Verifier::official()).unwrap();
|
||||
assert!(evidence.verify(&verifier()).is_err());
|
||||
assert!(Verifier::new(&sumdb::OFFICIAL_VERIFIER.replace("033de0ae", "033de0af")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_large_tree_inclusion_and_consistency_vectors_verify_offline() {
|
||||
// Captured from sum.golang.org/lookup/github.com/google/uuid@v1.6.0
|
||||
// and its immutable height-8 tiles on 2026-10-02. Hashes were assembled
|
||||
// independently of this Rust implementation. Both notes retain Google's
|
||||
// original Ed25519 signatures; no fixture signer can mint these vectors.
|
||||
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
|
||||
let trusted = Verifier::official();
|
||||
let verified = evidence.verify(&trusted).unwrap();
|
||||
assert!(verified.contains("github.com/google/uuid", "1.6.0"));
|
||||
assert_eq!(
|
||||
sumdb::mod_hash(b"module github.com/google/uuid\n"),
|
||||
"h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo="
|
||||
);
|
||||
for index in 0..evidence.records[0].inclusion.len() {
|
||||
let mut changed = evidence.clone();
|
||||
changed.records[0].inclusion[index] =
|
||||
base64::engine::general_purpose::STANDARD.encode([0; 32]);
|
||||
assert!(
|
||||
changed.verify(&trusted).is_err(),
|
||||
"accepted corrupt inclusion hash {index}"
|
||||
);
|
||||
}
|
||||
for index in 0..evidence.consistency.len() {
|
||||
let mut changed = evidence.clone();
|
||||
changed.consistency[index] = base64::engine::general_purpose::STANDARD.encode([0; 32]);
|
||||
assert!(
|
||||
changed.verify(&trusted).is_err(),
|
||||
"accepted corrupt consistency hash {index}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_h1_ignores_zip_order_and_compression_and_preserves_module_case() {
|
||||
let archive = |reverse, compression| {
|
||||
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let mut files = vec![
|
||||
("a.go", b"package a\n".as_slice()),
|
||||
("LICENSE", b"fixture license\n".as_slice()),
|
||||
];
|
||||
if reverse {
|
||||
files.reverse();
|
||||
}
|
||||
for (name, content) in files {
|
||||
writer
|
||||
.start_file(
|
||||
format!("example.com/Mixed@v1.0.0/{name}"),
|
||||
zip::write::SimpleFileOptions::default().compression_method(compression),
|
||||
)
|
||||
.unwrap();
|
||||
writer.write_all(content).unwrap();
|
||||
}
|
||||
writer.finish().unwrap().into_inner()
|
||||
};
|
||||
let first = archive(false, zip::CompressionMethod::Stored);
|
||||
let second = archive(true, zip::CompressionMethod::Deflated);
|
||||
assert_ne!(digest(&first), digest(&second));
|
||||
assert_eq!(
|
||||
sumdb::zip_hash("example.com/Mixed", "1.0.0", &first).unwrap(),
|
||||
sumdb::zip_hash("example.com/Mixed", "1.0.0", &second).unwrap()
|
||||
);
|
||||
let mut registry = LocalRegistry(BTreeMap::from([
|
||||
(
|
||||
(Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.mod".into()),
|
||||
b"module example.com/Mixed\n".to_vec(),
|
||||
),
|
||||
(
|
||||
(Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.zip".into()),
|
||||
first,
|
||||
),
|
||||
]));
|
||||
seal(&mut registry);
|
||||
let lock = resolved(®istry, None, &["go:example.com/Mixed@1.0.0"]).unwrap();
|
||||
assert_eq!(lock.libs[0].spec.name, "example.com/Mixed");
|
||||
assert!(
|
||||
lock.go_sumdb[0]
|
||||
.verify(&verifier())
|
||||
.unwrap()
|
||||
.contains("example.com/Mixed", "1.0.0")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_go_mod_consulted_by_mvs_is_authenticated_even_when_not_selected() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
for (name, version, source) in [
|
||||
(
|
||||
"a",
|
||||
"1.0.0",
|
||||
"module example.com/a\nrequire (\nexample.com/b v1.0.0\nexample.com/c v1.0.0\n)\n",
|
||||
),
|
||||
("b", "1.0.0", "module example.com/b\n"),
|
||||
("b", "2.0.0", "module example.com/b\n"),
|
||||
(
|
||||
"c",
|
||||
"1.0.0",
|
||||
"module example.com/c\nrequire example.com/b v2.0.0\n",
|
||||
),
|
||||
] {
|
||||
registry.0.insert(
|
||||
(
|
||||
Ecosystem::Go,
|
||||
format!("/example.com/{name}/@v/v{version}.mod"),
|
||||
),
|
||||
source.as_bytes().to_vec(),
|
||||
);
|
||||
registry.0.insert(
|
||||
(
|
||||
Ecosystem::Go,
|
||||
format!("/example.com/{name}/@v/v{version}.zip"),
|
||||
),
|
||||
zip_file(&format!("example.com/{name}@v{version}/{name}.go")),
|
||||
);
|
||||
}
|
||||
seal(&mut registry);
|
||||
let lock = resolved(®istry, None, &["go:example.com/a@1.0.0"]).unwrap();
|
||||
assert!(
|
||||
!lock
|
||||
.libs
|
||||
.iter()
|
||||
.any(|lib| lib.spec.key() == "go:example.com/b@1.0.0")
|
||||
);
|
||||
assert!(
|
||||
lock.go_sumdb[0]
|
||||
.records
|
||||
.iter()
|
||||
.any(|record| record.module == "example.com/b" && record.version == "1.0.0")
|
||||
);
|
||||
registry
|
||||
.0
|
||||
.get_mut(&(Ecosystem::Go, "/example.com/b/@v/v1.0.0.mod".into()))
|
||||
.unwrap()
|
||||
.extend(b"// modified discarded metadata\n");
|
||||
let cache = FixtureArtifactStore::default();
|
||||
let resolver = local_resolver(®istry, &cache);
|
||||
assert!(
|
||||
dever_cli::libs::LibResolver::resolve(
|
||||
&resolver,
|
||||
&["go:example.com/a@1.0.0".parse().unwrap()]
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("go.mod does not match")
|
||||
);
|
||||
for ((_, path), bytes) in ®istry.0 {
|
||||
if path.ends_with(".zip") {
|
||||
assert!(!cache.has(&digest(bytes), "linux-x86_64").unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_hashzip_includes_empty_directory_entries() {
|
||||
let archive = |directory| {
|
||||
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
writer
|
||||
.start_file(
|
||||
"example.com/a@v1.0.0/a.go",
|
||||
zip::write::SimpleFileOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
writer.write_all(b"package a\n").unwrap();
|
||||
if directory {
|
||||
writer
|
||||
.add_directory(
|
||||
"example.com/a@v1.0.0/empty/",
|
||||
zip::write::SimpleFileOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writer.finish().unwrap().into_inner()
|
||||
};
|
||||
// Independent Hash1 vectors: sorted "sha256(content) filename\n" records,
|
||||
// including the directory's trailing slash and SHA-256 of empty content.
|
||||
assert_eq!(
|
||||
sumdb::zip_hash("example.com/a", "1.0.0", &archive(false)).unwrap(),
|
||||
"h1:VEUcYKYLbKBYq0eA/VEJ3mMQBzi4jjWpusjuduGJcBQ="
|
||||
);
|
||||
assert_eq!(
|
||||
sumdb::zip_hash("example.com/a", "1.0.0", &archive(true)).unwrap(),
|
||||
"h1:y9ZWKWCmVsKnLxdFkIVoxr4MyXyHgBUvp1hgBzZ+IfE="
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_updates_and_removing_last_lib_preserve_official_checkpoint_anchor() {
|
||||
let root = TemporaryDirectory::new();
|
||||
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
|
||||
let mut lock = LockFile::new(Vec::new()).unwrap();
|
||||
lock.go_sumdb.push(evidence.clone());
|
||||
lock.write_atomic(root.path()).unwrap();
|
||||
let spec = vec!["pip:fixture@1.0.0".into()];
|
||||
for command in ["add", "update", "remove"] {
|
||||
dever_cli::libs::execute(command, root.path(), &spec).unwrap();
|
||||
let lock = LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap();
|
||||
assert_eq!(lock.go_sumdb, vec![evidence.clone()]);
|
||||
let current = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap();
|
||||
let previous = evidence.previous.clone().unwrap();
|
||||
// A subsequent online resolution cannot accept the older official tree.
|
||||
let mut registry = local_registry();
|
||||
registry.0.insert(
|
||||
(Ecosystem::Go, "https://sum.golang.org/latest".into()),
|
||||
previous.into_bytes(),
|
||||
);
|
||||
let cache = FixtureArtifactStore::default();
|
||||
let mut resolver = local_resolver(®istry, &cache);
|
||||
resolver.go_sumdb = ChecksumDatabase::official(Some(current.checkpoint)).unwrap();
|
||||
assert!(
|
||||
dever_cli::libs::LibResolver::resolve(
|
||||
&resolver,
|
||||
&["go:example.com/a@1.0.0".parse().unwrap()]
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("rollback")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_publication_rejects_offline_byte_and_element_overflow_before_replacement() {
|
||||
let root = TemporaryDirectory::new();
|
||||
let original = LockFile::new(Vec::new()).unwrap();
|
||||
original.write_atomic(root.path()).unwrap();
|
||||
let path = root.path().join("dever.lock");
|
||||
let before = fs::read(&path).unwrap();
|
||||
let receipt = Evidence {
|
||||
previous: None,
|
||||
checkpoint: "x".into(),
|
||||
consistency: Vec::new(),
|
||||
records: Vec::new(),
|
||||
};
|
||||
for overflow in [true, false] {
|
||||
let mut lock = original.clone();
|
||||
let mut receipt = receipt.clone();
|
||||
if overflow {
|
||||
receipt.checkpoint = "x".repeat(dever_runtime::wire::MAX_BYTES);
|
||||
} else {
|
||||
receipt.consistency = vec!["x".into(); dever_runtime::wire::MAX_ELEMENTS];
|
||||
}
|
||||
lock.go_sumdb.push(receipt);
|
||||
assert!(lock.encode().unwrap_err().contains("invalid dever.lock"));
|
||||
assert!(lock.write_atomic(root.path()).is_err());
|
||||
assert_eq!(fs::read(&path).unwrap(), before);
|
||||
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_surplus_and_out_of_bounds_sumdb_proofs_are_rejected() {
|
||||
let original: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
|
||||
let trusted = Verifier::official();
|
||||
let mut changed = original.clone();
|
||||
changed.records[0].inclusion.pop();
|
||||
assert!(changed.verify(&trusted).is_err());
|
||||
let mut changed = original.clone();
|
||||
changed.records[0]
|
||||
.inclusion
|
||||
.push(base64::engine::general_purpose::STANDARD.encode([0; 32]));
|
||||
assert!(changed.verify(&trusted).is_err());
|
||||
for id in ["66321505", "09223372036854775807", "9223372036854775808"] {
|
||||
let mut changed = original.clone();
|
||||
let (_, suffix) = changed.records[0].lookup.split_once('\n').unwrap();
|
||||
changed.records[0].lookup = format!("{id}\n{suffix}");
|
||||
assert!(changed.verify(&trusted).is_err());
|
||||
}
|
||||
let mut changed = original.clone();
|
||||
changed.checkpoint.pop();
|
||||
assert!(changed.verify(&trusted).is_err());
|
||||
let mut disconnected = original.clone();
|
||||
disconnected.previous = None;
|
||||
disconnected.consistency.clear();
|
||||
assert!(
|
||||
sumdb::verify_chain(&[original, disconnected], &trusted)
|
||||
.unwrap_err()
|
||||
.contains("not connected")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn lib_and_package_mutations_share_a_stable_nonblocking_project_lock() {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
let root = TemporaryDirectory::new();
|
||||
let requested = vec!["pip:fixture@1.0.0".into()];
|
||||
dever_cli::libs::execute("add", root.path(), &requested).unwrap();
|
||||
let lock_path = root.path().join("dever.lock");
|
||||
let config_path = root.path().join("config/setting.json");
|
||||
let before_lock = fs::read(&lock_path).unwrap();
|
||||
let before_config = fs::read(&config_path).unwrap();
|
||||
let guard = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.mode(0o600)
|
||||
.open(root.path().join(".dever-project.lock"))
|
||||
.unwrap();
|
||||
fs2::FileExt::try_lock_exclusive(&guard).unwrap();
|
||||
for command in ["add", "update", "remove"] {
|
||||
assert!(
|
||||
dever_cli::libs::execute(command, root.path(), &requested)
|
||||
.unwrap_err()
|
||||
.contains("mutation is busy")
|
||||
);
|
||||
assert!(
|
||||
dever_cli::packages::execute(command, root.path(), &[])
|
||||
.unwrap_err()
|
||||
.contains("mutation is busy")
|
||||
);
|
||||
struct UnusedTransport;
|
||||
impl dever_cli::packages::PackageTransport for UnusedTransport {
|
||||
fn get(&self, _: &str, _: usize) -> Result<Vec<u8>, String> {
|
||||
panic!("busy mutation must not contact a registry")
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
dever_cli::packages::execute_with(
|
||||
command,
|
||||
root.path(),
|
||||
&[],
|
||||
&UnusedTransport,
|
||||
&FixtureArtifactStore::default()
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("mutation is busy")
|
||||
);
|
||||
}
|
||||
assert_eq!(fs::read(&lock_path).unwrap(), before_lock);
|
||||
assert_eq!(fs::read(&config_path).unwrap(), before_config);
|
||||
drop(guard);
|
||||
dever_cli::libs::execute("update", root.path(), &requested).unwrap();
|
||||
assert_eq!(fs::read(&lock_path).unwrap(), before_lock);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn project_mutation_rejects_linked_or_unprotected_lock_paths() {
|
||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||
let root = TemporaryDirectory::new();
|
||||
let outside = TemporaryDirectory::new();
|
||||
let target = outside.path().join("untouched");
|
||||
fs::write(&target, "keep").unwrap();
|
||||
let path = root.path().join(".dever-project.lock");
|
||||
symlink(&target, &path).unwrap();
|
||||
let requested = vec!["pip:fixture@1.0.0".into()];
|
||||
assert!(
|
||||
dever_cli::libs::execute("add", root.path(), &requested)
|
||||
.unwrap_err()
|
||||
.contains("without symlinks")
|
||||
);
|
||||
assert_eq!(fs::read(&target).unwrap(), b"keep");
|
||||
fs::remove_file(&path).unwrap();
|
||||
fs::hard_link(&target, &path).unwrap();
|
||||
assert!(
|
||||
dever_cli::libs::execute("add", root.path(), &requested)
|
||||
.unwrap_err()
|
||||
.contains("private and unlinked")
|
||||
);
|
||||
fs::remove_file(&path).unwrap();
|
||||
fs::write(&path, "").unwrap();
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o666)).unwrap();
|
||||
assert!(
|
||||
dever_cli::libs::execute("add", root.path(), &requested)
|
||||
.unwrap_err()
|
||||
.contains("private and unlinked")
|
||||
);
|
||||
fs::set_permissions(root.path(), fs::Permissions::from_mode(0o777)).unwrap();
|
||||
assert!(
|
||||
dever_cli::libs::execute("add", root.path(), &requested)
|
||||
.unwrap_err()
|
||||
.contains("not group/other writable")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_go_workers_merge_connected_offline_evidence() {
|
||||
use dever_cli::libs::{LockedWorker, resolve_workers};
|
||||
let registry = local_registry();
|
||||
let cache = FixtureArtifactStore::default();
|
||||
let resolver = local_resolver(®istry, &cache);
|
||||
let worker = |adapter: &str, spec: &str| LockedWorker {
|
||||
port: "sample.worker".into(),
|
||||
adapter: adapter.into(),
|
||||
ecosystem: "go".into(),
|
||||
runtime: Some(registry_runtime(Ecosystem::Go).pack),
|
||||
entry: format!("worker/{adapter}.go"),
|
||||
schema: "checked-port".into(),
|
||||
capabilities: vec![],
|
||||
operations: vec!["read".into()],
|
||||
libs: vec![spec.parse().unwrap()],
|
||||
};
|
||||
let lock = resolve_workers(
|
||||
&resolver,
|
||||
&[],
|
||||
vec![
|
||||
worker("first", "go:example.com/a@1.0.0"),
|
||||
worker("second", "go:example.com/b@1.2.0"),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(lock.go_sumdb.len(), 2);
|
||||
assert_eq!(
|
||||
lock.go_sumdb[1].previous.as_ref(),
|
||||
Some(&lock.go_sumdb[0].verify(&verifier()).unwrap().checkpoint)
|
||||
);
|
||||
let decoded = LockFile::decode(&lock.encode().unwrap()).unwrap();
|
||||
let verified = sumdb::verify_chain(&decoded.go_sumdb, &verifier()).unwrap();
|
||||
assert!(verified.contains("example.com/a", "1.0.0"));
|
||||
assert!(verified.contains("example.com/b", "1.2.0"));
|
||||
verify_locked_artifacts(&decoded, &cache, "linux-x86_64").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "author operation: explicitly fetches official Go proxy/sumdb; requires target/go-managed/config/setting.json and runtime.pack"]
|
||||
fn prepare_official_go_dependency_fixture() {
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Settings {
|
||||
runtime: RegistryRuntime,
|
||||
}
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/go-managed");
|
||||
let settings: Settings = serde_json::from_slice(
|
||||
&fs::read(root.join("config/setting.json"))
|
||||
.expect("prepare the explicit fixture settings first"),
|
||||
)
|
||||
.unwrap();
|
||||
let pack =
|
||||
fs::read(root.join("runtime.pack")).expect("prepare the explicit Go runtime pack first");
|
||||
assert_eq!(
|
||||
settings.runtime.pack.sha256,
|
||||
digest(&pack),
|
||||
"configured runtime does not match runtime.pack"
|
||||
);
|
||||
assert_eq!(
|
||||
settings.runtime.target,
|
||||
dever_cli::toolchain::platform_identity()
|
||||
);
|
||||
let output = root.join("dependency");
|
||||
let lock_path = output.join("dever.lock");
|
||||
let previous = if lock_path.exists() {
|
||||
let lock = LockFile::decode(&fs::read(&lock_path).unwrap()).unwrap();
|
||||
dever_cli::libs::doctor(&lock).unwrap();
|
||||
let verified = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap();
|
||||
(!verified.checkpoint.is_empty()).then_some(verified.checkpoint)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let target = settings.runtime.target.clone();
|
||||
let store = FixtureArtifactStore::default();
|
||||
let transport = dever_cli::libs::HttpRegistry::official();
|
||||
let resolver = RegistryResolver {
|
||||
build: None,
|
||||
transport: &transport,
|
||||
store: &store,
|
||||
runtimes: BTreeMap::from([(Ecosystem::Go, settings.runtime)]),
|
||||
go_sumdb: ChecksumDatabase::official(previous).unwrap(),
|
||||
};
|
||||
let lock = dever_cli::libs::LibResolver::resolve(
|
||||
&resolver,
|
||||
&["go:github.com/google/uuid@1.6.0".parse().unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
// The same official verifier and offline artifact checks protect normal builds.
|
||||
dever_cli::libs::doctor(&lock).unwrap();
|
||||
dever_cli::libs::verify_locked_artifacts(&lock, &store, &target).unwrap();
|
||||
publish_fixture_artifacts(&output, &lock, &store);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"previous": "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n",
|
||||
"checkpoint": "go.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
|
||||
"consistency": [
|
||||
"3OyjS9XS6r41pxaFWGnW/WSmX7b1ik3nupquDNipdZ8=",
|
||||
"zX5IsB02+IutT35ka/MwY7gFiPSAlihfnt2cu6smUjw=",
|
||||
"Qc0CqO89A+/onGz1u4ZsDoMOfRIgPZpayRGTETOv8kk=",
|
||||
"JhdrRO/y8Iu+MDeUupFG0uM4+odAADMjVH0mOgLOA/c=",
|
||||
"lOgywJvTdrBWol0uphM6ZulvAebwJGNlekm8MIwiwKM=",
|
||||
"FRf0N51Fw+8eSLHEa/e/AsnDway0EkKo4jI0E1Ne7dk=",
|
||||
"Ak0gyC+RS6/ahVBCBuSBDA1wFN9mgq3acIbYm9uJHQ4=",
|
||||
"FFJ8WrKTTQgCjTuAl5CukBdjDaQ7opSdHBpNSolRYWw=",
|
||||
"rdNcd+MUjUxzlX/nlVGeQP5BkkZyTT89sn/yfg0AXQ0=",
|
||||
"gJzOh5y5rP1BILUrFJw+aGjod840PErDnjy4T0BoUE4=",
|
||||
"7I8BPI+ADxeJmu19mQxO+xt6bNhg3Ueq1j2dIfOppwA=",
|
||||
"urhcUF9QOyTRAYRjJjsRHjHdwIc+BxfiQ6a+NuYeC6E=",
|
||||
"p9eGEUHXeKLFnrIPB7VXHHMuwtaBnvHG4CYwgC0wq+w=",
|
||||
"Gcry0ScaNfFmQujPMT95RQ3rbzKDSynQsL546C8CkfU=",
|
||||
"dIpym+JRT/+z9/tlhpuIRyv+uoWGqgND/MHw0f1MtqA=",
|
||||
"cQrKLJIBbPoWDnRnbGfAEMfpsau0h+Aw3aqDOJ5LAVU=",
|
||||
"C46761Ui7jL+lf4vRXBlveoxuFS6tykAd6mMEfNttPE=",
|
||||
"45Kz4HcZ9iZ84ySMHCy21d4vd2MvibvfNMzYm0hKYRs=",
|
||||
"4JZTJ+4VUtLMTEs11jn4GmCxoz56Lm5HDwkYmkmAWlw=",
|
||||
"2hJV4h1jZ2YujjyXUw+7YYCwLXGYZy+ondhRUgxKmXI=",
|
||||
"lrn/kRfgYdYSDxgPdSQ8/GnZw6u/joKu9++AR+Saa5M=",
|
||||
"hnm9+kaHQdhHWrAcyrvY5fbv8QB+GwVjRMFeobtTg8A=",
|
||||
"rPFmKL/vD6Edsk/0DmpvDa/nMOqWgjfpPGRjBUL9yKs=",
|
||||
"2IzbyezmwwGZZPJ276ejAQZiAnar+RcjAUU3MEMtbIA=",
|
||||
"k3kfr9AECmkV+UA3/D6EMqaBDr67aubL5hXGsvJ+Nng=",
|
||||
"/WnQtddEqG03mbcU1XID1iws1nlKHVkJcaREWknCOiE=",
|
||||
"xJ20xXM4QItt5vxFOtolZ5l18p7ej1+RimNkgWmoRFM="
|
||||
],
|
||||
"records": [{
|
||||
"module": "github.com/google/uuid",
|
||||
"version": "1.6.0",
|
||||
"go_mod": "module github.com/google/uuid\n",
|
||||
"lookup": "22152757\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\n\ngo.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
|
||||
"inclusion": [
|
||||
"vYU6vRfhcMyd5/U04n+AYg52mm3BQrjwyN+27saiSnU=",
|
||||
"syWiyNbj6gybIdLKoAFd7xF3qNltS7OhQjtp4y8rCFg=",
|
||||
"Zqpa9zJsOg/sI/mv0UkXO9d7gvPHLLjMAKnZCPzeiNM=",
|
||||
"tGbcXa1AQlYBIm5gM2KDy8JvdAFDizj4lUop18Eehxs=",
|
||||
"/w9FG1Sae/omKxZdOvj719sT1rCow3tlP8VNo0fDQtE=",
|
||||
"dR6Bk+gh/hyJx7bu65HfhQdfuvT/OdVv/G7RUsOc26Y=",
|
||||
"fRC86/2mWWtFWNPz/Vou6f7hdyvRLHFPo9KaOlVOVmc=",
|
||||
"jJS2b3XVP2sst4CS5srpnF34agHpiluU63h6UQyFPDc=",
|
||||
"cOL2+zAXTEOaAiyvmCrL3Ned6soAg08CJJJ2VW3+a18=",
|
||||
"In5NzPUzIAZfV6bnUL11Bkm4Aqoit/ZG7gJ8nxOdB2w=",
|
||||
"r+8IH0m7Ig3BS+uwCVVG1CZT0I/Rro3bbSUVYcSrN2Q=",
|
||||
"wOgTamt00bdokE1Nbv3aHYu2um06V1xdLFEmOBjt0GI=",
|
||||
"21woobbbCvogV/ZXACdAozfVAFjRv5xMID/iLcryHzw=",
|
||||
"5+o2gn9/zVod+MYr4UuoeMzuz68vt/hA5+9H4YZaVME=",
|
||||
"040B+RrLQeFCJhPU69kzoKr4BTqhSwdrnYEVhknZ+I0=",
|
||||
"ioI6UByPBEr2lEZB0dI6pCXgcNss6UkKxm+TYAADujg=",
|
||||
"HOJ0dLK4hnl0tFeu1tmT7TRrNKRIbi/T8KI/2s4DXDs=",
|
||||
"BBcHtB4mS19jHff/LQTNI/+WCVBZ1Up52gNqyoln71U=",
|
||||
"Oz/OlF1Lj7wkqqE15p324ZSX1XHcnLzDSMKNtUx6rwY=",
|
||||
"ofpmTAcvFvNzlwjZnsrMqeG4iIBCP2lt5wIusvZ2gG4=",
|
||||
"IRpVS0aap6n0qb02jyOy6ikkOPxdFIQtWwckaGF2MAQ=",
|
||||
"liRusDUG3TanNZLGkfjb58ppX/6d40M7X2s3wptjxMw=",
|
||||
"ROjUzjXHDN0Yo/I6EeqHhpszFgIs7FCABVeQniliPTk=",
|
||||
"NTjIv8CJqIIGrOHhNQWSP9FV6XEPL6uGA4lmMfNKGJg=",
|
||||
"bwqUQG2wJXGxo1QLA2CFb3chH17xvlqXe6eQy43p0HY=",
|
||||
"bqa+4xGvzGcI/PYne2fXfUvdULgy8o3zQmD13q/gu1Y="
|
||||
],
|
||||
"consistency": []
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
use super::*;
|
||||
use dever_cli::workers::python_wheel::Wheel;
|
||||
|
||||
fn validate_native(
|
||||
target: &str,
|
||||
suffixes: &[String],
|
||||
interpreter: &str,
|
||||
files: &BTreeMap<String, &[u8]>,
|
||||
) -> Result<(), String> {
|
||||
let wheel_files = files
|
||||
.keys()
|
||||
.filter(|path| !path.starts_with("runtime/") && !path.starts_with("sandbox/"))
|
||||
.cloned()
|
||||
.collect();
|
||||
dever_cli::workers::python_wheel::validate_native(
|
||||
target,
|
||||
suffixes,
|
||||
interpreter,
|
||||
files,
|
||||
&wheel_files,
|
||||
)
|
||||
}
|
||||
|
||||
fn spec() -> LibSpec {
|
||||
"pip:sample@1.0.0".parse().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_console_scripts_use_the_same_private_prefix_and_record_generated_files() {
|
||||
let bytes=wheel_fixture::pack("sample","1.0.0","Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("sample","1.0.0",&[],&[]),&[("sample-1.0.0.dist-info/entry_points.txt",b"[console_scripts]\nsample-tool = sample.cli:main\n[gui_scripts]\nsample-gui = sample.gui:App.run [desktop]\n".to_vec())]);
|
||||
let installed = Wheel::parse(&spec(), None, &tags(), &bytes)
|
||||
.unwrap()
|
||||
.install("runtime/bin/python3", "3.12")
|
||||
.unwrap();
|
||||
let script = installed
|
||||
.iter()
|
||||
.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!(String::from_utf8_lossy(&script.bytes).contains("sample.cli"));
|
||||
let record = installed
|
||||
.iter()
|
||||
.find(|file| file.path.ends_with(".dist-info/RECORD"))
|
||||
.unwrap();
|
||||
assert!(String::from_utf8_lossy(&record.bytes).contains("../../../bin/sample-tool,sha256="));
|
||||
let collision = wheel_fixture::pack(
|
||||
"sample",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
|
||||
&[
|
||||
(
|
||||
"sample-1.0.0.dist-info/entry_points.txt",
|
||||
b"[console_scripts]\ntool = sample:main\n".to_vec(),
|
||||
),
|
||||
("sample-1.0.0.data/scripts/tool", b"#!python\n".to_vec()),
|
||||
],
|
||||
);
|
||||
assert!(
|
||||
Wheel::parse(&spec(), None, &tags(), &collision)
|
||||
.unwrap()
|
||||
.install("runtime/bin/python3", "3.12")
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
fn tags() -> Vec<String> {
|
||||
vec!["py3-none-any".into()]
|
||||
}
|
||||
fn valid() -> Vec<u8> {
|
||||
pure_wheel("sample", "1.0.0", &[], &[])
|
||||
}
|
||||
fn unpack(bytes: &[u8]) -> Vec<(String, Vec<u8>)> {
|
||||
use std::io::Read;
|
||||
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
||||
(0..archive.len())
|
||||
.map(|index| {
|
||||
let mut file = archive.by_index(index).unwrap();
|
||||
let name = file.name().to_owned();
|
||||
let mut bytes = vec![];
|
||||
file.read_to_end(&mut bytes).unwrap();
|
||||
(name, bytes)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_identity_and_all_compressed_filename_tags_are_bound_to_metadata() {
|
||||
Wheel::parse(
|
||||
&spec(),
|
||||
Some("sample-1.0.0-py3-none-any.whl"),
|
||||
&tags(),
|
||||
&valid(),
|
||||
)
|
||||
.unwrap();
|
||||
for filename in [
|
||||
"other-1.0.0-py3-none-any.whl",
|
||||
"sample-2.0.0-py3-none-any.whl",
|
||||
"sample-1.0.0-py2.py3-none-any.whl",
|
||||
"sample-1.0.0-1-py3-none-any.whl",
|
||||
] {
|
||||
assert!(
|
||||
Wheel::parse(&spec(), Some(filename), &tags(), &valid()).is_err(),
|
||||
"{filename}"
|
||||
);
|
||||
}
|
||||
let compressed = wheel_fixture::pack(
|
||||
"sample",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py2-none-any\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
|
||||
&[],
|
||||
);
|
||||
Wheel::parse(
|
||||
&spec(),
|
||||
Some("sample-1.0.0-py2.py3-none-any.whl"),
|
||||
&tags(),
|
||||
&compressed,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
Wheel::parse(
|
||||
&spec(),
|
||||
None,
|
||||
&["cp312-cp312-linux_x86_64".into()],
|
||||
&valid()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let forged_metadata = wheel_fixture::pack(
|
||||
"sample",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("other", "1.0.0", &[], &[]),
|
||||
&[],
|
||||
);
|
||||
assert!(Wheel::parse(&spec(), None, &tags(), &forged_metadata).is_err());
|
||||
let hidden_binary = wheel_fixture::pack(
|
||||
"sample",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
|
||||
&[("sample/native.so", elf(62, None, None))],
|
||||
);
|
||||
assert!(Wheel::parse(&spec(), None, &tags(), &hidden_binary).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "author operation: explicitly fetches PyPI simplejson; requires target/python-managed/config/setting.json and runtime.pack"]
|
||||
fn prepare_official_native_python_fixture() {
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Settings {
|
||||
runtime: RegistryRuntime,
|
||||
}
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/python-managed");
|
||||
let settings: Settings =
|
||||
serde_json::from_slice(&fs::read(root.join("config/setting.json")).unwrap()).unwrap();
|
||||
let pack = fs::read(root.join("runtime.pack")).unwrap();
|
||||
assert_eq!(digest(&pack), settings.runtime.pack.sha256);
|
||||
assert_eq!(
|
||||
settings.runtime.target,
|
||||
dever_cli::toolchain::platform_identity()
|
||||
);
|
||||
let target = settings.runtime.target.clone();
|
||||
let store = FixtureArtifactStore::default();
|
||||
let transport = dever_cli::libs::HttpRegistry::official();
|
||||
let resolver = RegistryResolver {
|
||||
build: None,
|
||||
transport: &transport,
|
||||
store: &store,
|
||||
runtimes: BTreeMap::from([(Ecosystem::Pip, settings.runtime)]),
|
||||
go_sumdb: dever_cli::libs::sumdb::ChecksumDatabase::official(None).unwrap(),
|
||||
};
|
||||
let lock = dever_cli::libs::LibResolver::resolve(
|
||||
&resolver,
|
||||
&["pip:simplejson@3.20.1".parse().unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
dever_cli::libs::doctor(&lock).unwrap();
|
||||
dever_cli::libs::verify_locked_artifacts(&lock, &store, &target).unwrap();
|
||||
let artifact = &lock.libs[0].artifacts[0];
|
||||
let wheel = store
|
||||
.verify_exact(&artifact.sha256, artifact.bytes, &target)
|
||||
.unwrap();
|
||||
assert!(
|
||||
unpack(&wheel)
|
||||
.iter()
|
||||
.any(|(path, bytes)| path.contains("_speedups.")
|
||||
&& path.ends_with(".so")
|
||||
&& bytes.starts_with(b"\x7fELF")),
|
||||
"fixture must use native speedups, not the optional pure-Python fallback"
|
||||
);
|
||||
publish_fixture_artifacts(&root.join("dependency"), &lock, &store);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_record_requires_exact_files_hashes_sizes_and_unique_csv_paths() {
|
||||
for mutation in 0..6 {
|
||||
let mut files = unpack(&valid());
|
||||
match mutation {
|
||||
0 => files
|
||||
.iter_mut()
|
||||
.find(|(path, _)| path.ends_with("__init__.py"))
|
||||
.unwrap()
|
||||
.1
|
||||
.push(b'!'),
|
||||
1 => files.push(("sample/unlisted.py".into(), vec![b'x'])),
|
||||
2 => {
|
||||
files.retain(|(path, _)| !path.ends_with("__init__.py"));
|
||||
}
|
||||
_ => {
|
||||
let record = &mut files
|
||||
.iter_mut()
|
||||
.find(|(path, _)| path.ends_with("/RECORD"))
|
||||
.unwrap()
|
||||
.1;
|
||||
let mut content = String::from_utf8(record.clone()).unwrap();
|
||||
match mutation {
|
||||
3 => content = content.replace("sha256=", "sha1="),
|
||||
4 => content.push_str("sample-1.0.0.dist-info/RECORD,,\n"),
|
||||
5 => content.push_str("../outside.py,sha256=abc,1\n"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
*record = content.into_bytes();
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
Wheel::parse(&spec(), None, &tags(), &wheel_fixture::zip(&files)).is_err(),
|
||||
"mutation {mutation}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_installs_all_standard_schemes_and_rewrites_installed_record() {
|
||||
let bytes = wheel_fixture::pack(
|
||||
"sample",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
|
||||
&[
|
||||
("sample-1.0.0.data/purelib/plain.py", b"plain".to_vec()),
|
||||
(
|
||||
"sample-1.0.0.data/platlib/platform.py",
|
||||
b"platform".to_vec(),
|
||||
),
|
||||
(
|
||||
"sample-1.0.0.data/scripts/tool",
|
||||
b"#!python\nprint('ok')\n".to_vec(),
|
||||
),
|
||||
("sample-1.0.0.data/headers/public.h", b"header".to_vec()),
|
||||
(
|
||||
"sample-1.0.0.data/data/share/sample/schema.json",
|
||||
b"{}".to_vec(),
|
||||
),
|
||||
("sample/name,with\"quotes.py", b"quoted".to_vec()),
|
||||
],
|
||||
);
|
||||
let files = Wheel::parse(&spec(), None, &tags(), &bytes)
|
||||
.unwrap()
|
||||
.install("runtime/bin/python3", "3.12")
|
||||
.unwrap();
|
||||
for path in [
|
||||
"runtime/lib/python3.12/site-packages/plain.py",
|
||||
"runtime/lib/python3.12/site-packages/platform.py",
|
||||
"runtime/include/python3.12/sample/public.h",
|
||||
"runtime/share/sample/schema.json",
|
||||
"runtime/lib/python3.12/site-packages/sample/name,with\"quotes.py",
|
||||
] {
|
||||
assert!(files.iter().any(|file| file.path == path), "{path}");
|
||||
}
|
||||
let script = files
|
||||
.iter()
|
||||
.find(|file| file.path == "runtime/bin/tool")
|
||||
.unwrap();
|
||||
assert!(script.executable);
|
||||
assert!(
|
||||
script
|
||||
.bytes
|
||||
.starts_with(b"#!/worker/runtime/bin/python3 -I\n")
|
||||
);
|
||||
let record = files
|
||||
.iter()
|
||||
.find(|file| file.path.ends_with("/RECORD"))
|
||||
.unwrap();
|
||||
let rows = csv::ReaderBuilder::new()
|
||||
.has_headers(false)
|
||||
.from_reader(record.bytes.as_slice())
|
||||
.records()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
let installed_script = rows
|
||||
.iter()
|
||||
.find(|row| &row[0] == "../../../bin/tool")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
installed_script[1].to_owned(),
|
||||
format!(
|
||||
"sha256={}",
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(&script.bytes))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_rejects_scheme_collisions_and_uses_artifact_dependencies() {
|
||||
let bytes = wheel_fixture::pack(
|
||||
"sample",
|
||||
"1.0.0",
|
||||
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
||||
&wheel_fixture::metadata("sample", "1.0.0", &[], &[]),
|
||||
&[
|
||||
("same.py", vec![b'a']),
|
||||
("sample-1.0.0.data/platlib/same.py", vec![b'b']),
|
||||
],
|
||||
);
|
||||
assert!(
|
||||
Wheel::parse(&spec(), None, &tags(), &bytes)
|
||||
.unwrap()
|
||||
.install("runtime/bin/python3", "3.12")
|
||||
.is_err()
|
||||
);
|
||||
let mut registry = local_registry();
|
||||
let release = registry
|
||||
.0
|
||||
.get_mut(&(Ecosystem::Pip, "/pypi/a/1.0.0/json".into()))
|
||||
.unwrap();
|
||||
let mut document: serde_json::Value = serde_json::from_slice(release).unwrap();
|
||||
document["info"]["requires_dist"] = serde_json::json!([]);
|
||||
*release = serde_json::to_vec(&document).unwrap();
|
||||
let lock = python_lock(®istry, &["pip:a@1.0.0"]).unwrap();
|
||||
assert_eq!(dependency_keys(&lock, "pip:a@1.0.0"), ["pip:b@1.1.0"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solver_backtracks_on_selected_wheel_metadata_and_its_actual_extras() {
|
||||
let mut registry = LocalRegistry(BTreeMap::new());
|
||||
python_release(
|
||||
&mut registry,
|
||||
"root",
|
||||
"1.0.0",
|
||||
&[],
|
||||
&["a[x] >=1", "b ==1.0.0"],
|
||||
);
|
||||
python_release(
|
||||
&mut registry,
|
||||
"a",
|
||||
"2.0.0",
|
||||
&["x"],
|
||||
&["b ==2.0.0; extra == 'x'"],
|
||||
);
|
||||
python_release(
|
||||
&mut registry,
|
||||
"a",
|
||||
"1.0.0",
|
||||
&["x"],
|
||||
&["b ==1.0.0; extra == 'x'"],
|
||||
);
|
||||
python_release(&mut registry, "b", "2.0.0", &[], &[]);
|
||||
python_release(&mut registry, "b", "1.0.0", &[], &[]);
|
||||
for version in ["1.0.0", "2.0.0"] {
|
||||
let json = registry
|
||||
.0
|
||||
.get_mut(&(Ecosystem::Pip, format!("/pypi/a/{version}/json")))
|
||||
.unwrap();
|
||||
let mut release: serde_json::Value = serde_json::from_slice(json).unwrap();
|
||||
release["info"] = serde_json::json!({"requires_dist":null,"provides_extra":null});
|
||||
*json = serde_json::to_vec(&release).unwrap();
|
||||
}
|
||||
let lock = python_lock(®istry, &["pip:root@1.0.0"]).unwrap();
|
||||
assert!(
|
||||
lock.libs
|
||||
.iter()
|
||||
.any(|lib| lib.spec.key() == "pip:a[x]@1.0.0")
|
||||
);
|
||||
assert!(!lock.libs.iter().any(|lib| lib.spec.version == "2.0.0"));
|
||||
assert_eq!(dependency_keys(&lock, "pip:a[x]@1.0.0"), ["pip:b@1.0.0"]);
|
||||
}
|
||||
|
||||
/// ELF64 protocol fixture with PT_LOAD and optional PT_DYNAMIC/DT_STRTAB.
|
||||
/// It is parsed only; no executable bytes are launched by this test.
|
||||
fn elf(machine: u16, needed: Option<&str>, rpath: Option<&str>) -> Vec<u8> {
|
||||
let mut bytes = vec![0u8; 1024];
|
||||
bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
|
||||
bytes[16..18].copy_from_slice(&3u16.to_le_bytes());
|
||||
bytes[18..20].copy_from_slice(&machine.to_le_bytes());
|
||||
bytes[20..24].copy_from_slice(&1u32.to_le_bytes());
|
||||
bytes[32..40].copy_from_slice(&64u64.to_le_bytes());
|
||||
bytes[52..54].copy_from_slice(&64u16.to_le_bytes());
|
||||
bytes[54..56].copy_from_slice(&56u16.to_le_bytes());
|
||||
bytes[56..58].copy_from_slice(&2u16.to_le_bytes());
|
||||
bytes[64..68].copy_from_slice(&1u32.to_le_bytes());
|
||||
bytes[96..104].copy_from_slice(&1024u64.to_le_bytes());
|
||||
bytes[104..112].copy_from_slice(&1024u64.to_le_bytes());
|
||||
bytes[120..124].copy_from_slice(&2u32.to_le_bytes());
|
||||
bytes[128..136].copy_from_slice(&176u64.to_le_bytes());
|
||||
bytes[136..144].copy_from_slice(&176u64.to_le_bytes());
|
||||
let mut strings = vec![0];
|
||||
let mut dynamic = vec![];
|
||||
for (tag, value) in [(1u64, needed), (15u64, rpath)] {
|
||||
if let Some(value) = value {
|
||||
dynamic.push((tag, strings.len() as u64));
|
||||
strings.extend(value.bytes());
|
||||
strings.push(0);
|
||||
}
|
||||
}
|
||||
dynamic.extend([(5, 512), (10, strings.len() as u64), (0, 0)]);
|
||||
bytes[152..160].copy_from_slice(&((dynamic.len() * 16) as u64).to_le_bytes());
|
||||
bytes[160..168].copy_from_slice(&((dynamic.len() * 16) as u64).to_le_bytes());
|
||||
for (index, (tag, value)) in dynamic.into_iter().enumerate() {
|
||||
bytes[176 + index * 16..184 + index * 16].copy_from_slice(&tag.to_le_bytes());
|
||||
bytes[184 + index * 16..192 + index * 16].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
bytes[512..512 + strings.len()].copy_from_slice(&strings);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_wheel_checks_target_suffix_and_closed_library_search() {
|
||||
let runtime = elf(62, None, None);
|
||||
let library = elf(62, None, None);
|
||||
let native = elf(62, Some("libsample.so.1"), Some("$ORIGIN/../sample.libs"));
|
||||
let suffixes = vec![
|
||||
".cpython-312-x86_64-linux-gnu.so".into(),
|
||||
".abi3.so".into(),
|
||||
".so".into(),
|
||||
];
|
||||
let mut files = BTreeMap::from([
|
||||
("runtime/bin/python3".into(), runtime.as_slice()),
|
||||
(
|
||||
"site-packages/sample/_native.cpython-312-x86_64-linux-gnu.so".into(),
|
||||
native.as_slice(),
|
||||
),
|
||||
(
|
||||
"site-packages/sample.libs/libsample.so.1".into(),
|
||||
library.as_slice(),
|
||||
),
|
||||
]);
|
||||
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
|
||||
files.remove("site-packages/sample.libs/libsample.so.1");
|
||||
assert!(
|
||||
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
|
||||
.unwrap_err()
|
||||
.contains("packaged dependency closure")
|
||||
);
|
||||
files.insert("sandbox/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")
|
||||
.unwrap();
|
||||
files.insert(
|
||||
"site-packages/sample/_native.cpython-311-x86_64-linux-gnu.so".into(),
|
||||
old,
|
||||
);
|
||||
assert!(
|
||||
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
|
||||
.unwrap_err()
|
||||
.contains("suffix")
|
||||
);
|
||||
for (machine, rpath, message) in [
|
||||
(183, "$ORIGIN", "target"),
|
||||
(62, "/usr/lib", "$ORIGIN"),
|
||||
(62, "$ORIGIN/../../../..", "escapes"),
|
||||
] {
|
||||
let rejected = elf(machine, None, Some(rpath));
|
||||
let files = BTreeMap::from([
|
||||
("runtime/bin/python3".into(), runtime.as_slice()),
|
||||
("site-packages/sample/native.so".into(), rejected.as_slice()),
|
||||
]);
|
||||
assert!(
|
||||
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
|
||||
.unwrap_err()
|
||||
.contains(message)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_runpath_outranks_ancestor_rpath_and_helpers_keep_import_context() {
|
||||
let runtime = elf(62, None, Some("$ORIGIN/../lib"));
|
||||
let shared = elf(62, None, None);
|
||||
let wrong_target = elf(183, None, None);
|
||||
let mut extension = elf(
|
||||
62,
|
||||
Some("libsample.so.1"),
|
||||
Some("$ORIGIN/../../runtime/alternate"),
|
||||
);
|
||||
// Replace DT_RPATH with DT_RUNPATH in the independently encoded ELF vector.
|
||||
for offset in (176..256).step_by(16) {
|
||||
if u64::from_le_bytes(extension[offset..offset + 8].try_into().unwrap()) == 15 {
|
||||
extension[offset..offset + 8].copy_from_slice(&29u64.to_le_bytes());
|
||||
}
|
||||
}
|
||||
let suffixes = vec![".cpython-312-x86_64-linux-gnu.so".into()];
|
||||
let files = BTreeMap::from([
|
||||
("runtime/bin/python3".into(), runtime.as_slice()),
|
||||
("runtime/lib/libsample.so.1".into(), shared.as_slice()),
|
||||
(
|
||||
"runtime/alternate/libsample.so.1".into(),
|
||||
wrong_target.as_slice(),
|
||||
),
|
||||
(
|
||||
"site-packages/pkg/mod.cpython-312-x86_64-linux-gnu.so".into(),
|
||||
extension.as_slice(),
|
||||
),
|
||||
]);
|
||||
assert!(
|
||||
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files)
|
||||
.unwrap_err()
|
||||
.contains("target")
|
||||
);
|
||||
|
||||
let extension = elf(62, Some("liba.so.1"), Some("$ORIGIN/../vendor"));
|
||||
let helper = elf(62, Some("libb.so.1"), None);
|
||||
let files = BTreeMap::from([
|
||||
("runtime/bin/python3".into(), runtime.as_slice()),
|
||||
(
|
||||
"site-packages/pkg/mod.cpython-312-x86_64-linux-gnu.so".into(),
|
||||
extension.as_slice(),
|
||||
),
|
||||
("site-packages/vendor/liba.so.1".into(), helper.as_slice()),
|
||||
("site-packages/vendor/libb.so.1".into(), shared.as_slice()),
|
||||
]);
|
||||
validate_native("linux-x86_64", &suffixes, "runtime/bin/python3", &files).unwrap();
|
||||
}
|
||||
Reference in new issue
Block a user