Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,123 @@
|
||||
//! 真实 CLI 参数分派及 Package/Lib 双向锁合并,不安装全局运行时。
|
||||
|
||||
use super::super::commands::Services;
|
||||
use super::python_fixture::Python;
|
||||
use dever::compiler::project::dependency::{self, Lock};
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn package_changes_preserve_lib_closure_and_lib_changes_preserve_packages() {
|
||||
let mut python = Python::new();
|
||||
python.transport.release("alpha", "1.0.0", "");
|
||||
python.transport.release("beta", "1.0.0", "");
|
||||
python.command("add", &["pip:alpha@1.0.0"]).unwrap();
|
||||
let before = python.lock();
|
||||
let mut packages = Services::default();
|
||||
packages.release("numbers", "1.0.0", &[]);
|
||||
packages.release("numbers", "2.0.0", &[]);
|
||||
let root = &python.fixture.directory.0;
|
||||
for (action, specs, version) in [
|
||||
("add", vec!["numbers@^1".into()], "1.0.0"),
|
||||
("update", vec!["numbers@^2".into()], "2.0.0"),
|
||||
] {
|
||||
dependency::execute_with(action, root, &specs, &packages, &python.store).unwrap();
|
||||
let lock = python.lock();
|
||||
assert!(lock.libs == before.libs);
|
||||
assert_eq!(lock.packages[0].version, version);
|
||||
}
|
||||
let packages_before = serde_json::to_value(python.lock().packages).unwrap();
|
||||
python.command("add", &["pip:beta@1.0.0"]).unwrap();
|
||||
python.command("remove", &["pip:alpha@1.0.0"]).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(python.lock().packages).unwrap(),
|
||||
packages_before
|
||||
);
|
||||
dependency::execute_with(
|
||||
"remove",
|
||||
root,
|
||||
&["numbers".into()],
|
||||
&packages,
|
||||
&python.store,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(python.lock().packages.is_empty());
|
||||
assert_eq!(python.lock().libs[0].spec.name, "beta");
|
||||
python.command("doctor", &[]).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_cli_routes_lib_install_doctor_list_and_rejects_bad_target_arguments() {
|
||||
let python = Python::new();
|
||||
let root = &python.fixture.directory.0;
|
||||
std::fs::write(root.join("dever.lock"), Lock::new(vec![]).encode().unwrap()).unwrap();
|
||||
let lock = std::fs::read(root.join("dever.lock")).unwrap();
|
||||
let setting = std::fs::read(root.join("config/setting.json")).unwrap();
|
||||
for action in ["doctor", "list", "install"] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_dever"))
|
||||
.env_clear()
|
||||
.arg("lib")
|
||||
.arg(action)
|
||||
.arg(root)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{action}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
if action != "list" {
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("validated 0 Lib(s)"));
|
||||
}
|
||||
if action != "install" {
|
||||
assert!(!root.join(".dever-project.lock").exists());
|
||||
}
|
||||
}
|
||||
for arguments in [
|
||||
vec!["list", "--target", "linux-x86_64"],
|
||||
vec!["doctor", "--target"],
|
||||
vec!["doctor", "--target", "invalid-target"],
|
||||
vec![
|
||||
"doctor",
|
||||
"--target",
|
||||
"linux-x86_64",
|
||||
"--target",
|
||||
"linux-x86_64",
|
||||
],
|
||||
vec!["install", "pip:unexpected@1.0.0"],
|
||||
] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_dever"))
|
||||
.env_clear()
|
||||
.arg("lib")
|
||||
.arg(arguments[0])
|
||||
.arg(root)
|
||||
.args(&arguments[1..])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success(), "accepted {arguments:?}");
|
||||
}
|
||||
assert_eq!(std::fs::read(root.join("dever.lock")).unwrap(), lock);
|
||||
assert_eq!(
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
setting
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_registry_rejects_ambiguous_urls_before_transport() {
|
||||
use dependency::library::registry::official;
|
||||
assert!(official("https://proxy.golang.org/example.com/mod/@v/v1.0.0.zip").is_ok());
|
||||
for value in [
|
||||
"https://pypi.org:443/pypi/a/json",
|
||||
"https://@pypi.org/pypi/a/json",
|
||||
"https://pypi.org/x/%2e%2e/pypi/a/json",
|
||||
"https://pypi.org/x%2fpypi/a/json",
|
||||
"https://pypi.org/../pypi/a/json",
|
||||
"https://pypi.org/pypi/a/json?secret=value",
|
||||
"https://pypi.org/pypi/a/json#fragment",
|
||||
"https://pypi.org.evil.test/pypi/a/json",
|
||||
" http://pypi.org/pypi/a/json",
|
||||
"https://pypi.org\\evil.test/pypi/a/json",
|
||||
] {
|
||||
assert!(official(value).is_err(), "accepted ambiguous registry URL");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
//! 自有签名、归档和 Lib 输入验证;不连接生产 deverd 或修改机器缓存。
|
||||
|
||||
mod central;
|
||||
mod pep517;
|
||||
mod pep517_fixture;
|
||||
mod python;
|
||||
mod python_fixture;
|
||||
|
||||
use super::{Fixture, fixture::digest, write};
|
||||
use dever::compiler::project::dependency::{
|
||||
library::{Spec, archive},
|
||||
worker::signed::SignedResources,
|
||||
};
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn lib_identities_normalize_extras_and_reject_ranges_and_path_injection() {
|
||||
assert_eq!(
|
||||
"pip:Some_Name[TEST,docs]@1.2.3"
|
||||
.parse::<Spec>()
|
||||
.unwrap()
|
||||
.key(),
|
||||
"pip:some-name[docs,test]@1.2.3"
|
||||
);
|
||||
assert_eq!(
|
||||
"go:example.org/widget@v1.2.3"
|
||||
.parse::<Spec>()
|
||||
.unwrap()
|
||||
.version,
|
||||
"1.2.3"
|
||||
);
|
||||
assert_eq!(
|
||||
"npm:@scope/name@1.2.3".parse::<Spec>().unwrap().name,
|
||||
"@scope/name"
|
||||
);
|
||||
for value in [
|
||||
"npm:thing@^1",
|
||||
"npm:../thing@1.2.3",
|
||||
"pip:thing[]@1",
|
||||
"pip:thing@>=1",
|
||||
"go:../thing@1.2.3",
|
||||
"npm:@scope/../thing@1.2.3",
|
||||
] {
|
||||
assert!(value.parse::<Spec>().is_err(), "accepted {value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_resources_require_independent_signature_and_exact_catalog_bytes() {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = fixture.directory.0.join("installation");
|
||||
let key = Ed25519KeyPair::from_seed_unchecked(&[91; 32]).unwrap();
|
||||
let hex = |bytes: &[u8]| {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>()
|
||||
};
|
||||
write(
|
||||
&root,
|
||||
"state/trusted-release-key",
|
||||
hex(key.public_key().as_ref()),
|
||||
);
|
||||
let bytes = b"signed fixture contents";
|
||||
let path = "runtime/npm/linux-x86_64/runtime.pack";
|
||||
let target = dever::compiler::toolchain::BuildTarget::host().unwrap();
|
||||
let catalog = serde_json::to_vec(&serde_json::json!({"format":"dever-release-v2","version":"0.0.1","platform":target.name(),"artifacts":[{"path":"base.txt","bytes":bytes.len(),"sha256":digest(bytes)}],"extensions":[{"kind":{"type":"runtime","ecosystem":"npm"},"target":"linux-x86_64","artifacts":[{"path":path,"bytes":bytes.len(),"sha256":digest(bytes)}]}]})).unwrap();
|
||||
write(&root, "versions/0.0.1/base.txt", bytes);
|
||||
write(&root, "versions/0.0.1/manifest.json", &catalog);
|
||||
write(
|
||||
&root,
|
||||
"versions/0.0.1/manifest.sig",
|
||||
hex(key.sign(&catalog).as_ref()),
|
||||
);
|
||||
let signed = SignedResources::load(&root, "0.0.1").unwrap();
|
||||
assert_eq!(signed.read("base.txt").unwrap(), bytes);
|
||||
let actual = signed.read_with(path, |version, catalog_hash, extension, artifact| {
|
||||
assert_eq!(version, "0.0.1");
|
||||
assert_eq!(catalog_hash, digest(&catalog));
|
||||
assert_eq!(extension, &serde_json::json!({"kind":{"type":"runtime","ecosystem":"npm"},"target":"linux-x86_64"}));
|
||||
assert_eq!(artifact.path, path);
|
||||
Ok(bytes.to_vec())
|
||||
}).unwrap();
|
||||
assert_eq!(actual, bytes);
|
||||
assert!(
|
||||
signed
|
||||
.read_with(path, |_, _, _, _| Ok(b"changed".to_vec()))
|
||||
.is_err()
|
||||
);
|
||||
write(&root, "versions/0.0.1/base.txt", b"different signed file");
|
||||
assert!(signed.read("base.txt").is_err());
|
||||
write(&root, "versions/0.0.1/manifest.json", b"{}");
|
||||
assert!(SignedResources::load(&root, "0.0.1").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_consumption_rejects_links_and_file_directory_collisions() {
|
||||
let make = |entries: &[(&str, &[u8], tar::EntryType)]| {
|
||||
let gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
let mut archive = tar::Builder::new(gzip);
|
||||
for (path, bytes, kind) in entries {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_entry_type(*kind);
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o755);
|
||||
header.set_cksum();
|
||||
archive.append_data(&mut header, path, *bytes).unwrap();
|
||||
}
|
||||
archive.into_inner().unwrap().finish().unwrap()
|
||||
};
|
||||
let bytes = make(&[(
|
||||
"package/main.js",
|
||||
b"export const value = 42;",
|
||||
tar::EntryType::Regular,
|
||||
)]);
|
||||
assert_eq!(archive::files(&bytes, false).unwrap().len(), 1);
|
||||
assert!(archive::entries(&bytes, false).unwrap()["package/main.js"].1);
|
||||
let bytes = make(&[("package/main.js", b"", tar::EntryType::Symlink)]);
|
||||
assert!(archive::files(&bytes, false).unwrap_err().contains("links"));
|
||||
let bytes = make(&[
|
||||
("package", b"not a directory", tar::EntryType::Regular),
|
||||
("package/main.js", b"", tar::EntryType::Regular),
|
||||
]);
|
||||
assert!(
|
||||
archive::files(&bytes, false)
|
||||
.unwrap_err()
|
||||
.contains("collision")
|
||||
);
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
zip.start_file("../escape", zip::write::SimpleFileOptions::default())
|
||||
.unwrap();
|
||||
zip.write_all(b"bad").unwrap();
|
||||
assert!(archive::files(&zip.finish().unwrap().into_inner(), true).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lib_zip_rejects_duplicate_central_paths() {
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for name in ["package/first", "package/other"] {
|
||||
zip.start_file(
|
||||
name,
|
||||
zip::write::SimpleFileOptions::default().unix_permissions(0o755),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(name.as_bytes()).unwrap();
|
||||
}
|
||||
let mut bytes = zip.finish().unwrap().into_inner();
|
||||
let files = archive::entries(&bytes, true).unwrap();
|
||||
assert_eq!(files.len(), 2);
|
||||
assert!(files["package/first"].1);
|
||||
let mut cursor = 0;
|
||||
while cursor + 5 <= bytes.len() {
|
||||
if &bytes[cursor..cursor + 5] == b"other" {
|
||||
bytes[cursor..cursor + 5].copy_from_slice(b"first");
|
||||
}
|
||||
cursor += 1;
|
||||
}
|
||||
assert!(
|
||||
archive::entries(&bytes, true)
|
||||
.unwrap_err()
|
||||
.contains("duplicate or inconsistent")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_wheel_identity_abi_and_dependency_edges_are_checked_before_installation() {
|
||||
let bytes = python_fixture::wheel("tiny", "1.0.0", "");
|
||||
let lib = serde_json::from_value(serde_json::json!({"spec":{"ecosystem":"pip","name":"tiny","version":"1.0.0"},"dependencies":[],"runtime":{"name":"python","version":"3.12.1","sha256":"0".repeat(64)},"artifacts":[],"schema":"pypi-wheel-v1"})).unwrap();
|
||||
let markers = serde_json::json!({"implementation_name":"cpython","implementation_version":"3.12.1","os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":"3.12.1","python_version":"3.12","sys_platform":"linux"});
|
||||
let mut runtime = serde_json::from_value::<dever::compiler::project::dependency::library::runtime::Manifest>(serde_json::json!({"format":"dever-worker-runtime-v1","ecosystem":"pip","target":"linux-x86_64","executable":"bin/python3","arguments":["-I","-S","-B"],"python_wheel_tags":["py3-none-any"],"python_extension_suffixes":[".so"],"python_markers":markers})).unwrap();
|
||||
let files =
|
||||
dever::compiler::project::dependency::library::python::install(&lib, &bytes, &runtime, &[])
|
||||
.unwrap();
|
||||
assert_eq!(files["tiny/__init__.py"], b"VALUE = 42\n");
|
||||
runtime.python_wheel_tags.clear();
|
||||
assert!(
|
||||
dever::compiler::project::dependency::library::python::install(&lib, &bytes, &runtime, &[])
|
||||
.unwrap_err()
|
||||
.contains("tags")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! 源码 hook 真正执行签名 Python/C 工具;安装回放与失败均保持原始项目文件。
|
||||
|
||||
use super::{pep517_fixture, python_fixture::Python};
|
||||
use dever::compiler::project::dependency::library::lock::receipt::Receipt;
|
||||
|
||||
#[test]
|
||||
fn pep517_native_build_and_exact_install_replay_use_locked_static_dynamic_inputs() {
|
||||
let mut fixture = Python::new();
|
||||
pep517_fixture::prepare(&mut fixture, "stable");
|
||||
fixture.command("add", &["pip:built-native@1.0.0"]).unwrap();
|
||||
let lock = fixture.lock();
|
||||
assert_eq!(
|
||||
lock.libs
|
||||
.iter()
|
||||
.map(|lib| lib.spec.key())
|
||||
.collect::<Vec<_>>(),
|
||||
["pip:built-native@1.0.0", "pip:runtime-helper@1.0.0"]
|
||||
);
|
||||
let receipt = Receipt::parse(&lock.builds[0]).unwrap();
|
||||
assert_eq!(
|
||||
receipt
|
||||
.inputs
|
||||
.libs
|
||||
.iter()
|
||||
.map(|lib| lib.spec.key())
|
||||
.collect::<Vec<_>>(),
|
||||
["pip:build-helper@1.0.0", "pip:dynamic-helper@1.0.0"]
|
||||
);
|
||||
assert_eq!(receipt.dynamic_requires, ["dynamic-helper==1.0.0"]);
|
||||
let before = snapshot(&fixture);
|
||||
let original = fixture
|
||||
.store
|
||||
.0
|
||||
.borrow_mut()
|
||||
.remove(&receipt.output.sha256)
|
||||
.unwrap();
|
||||
fixture.transport.requests.borrow_mut().clear();
|
||||
fixture.command("install", &[]).unwrap();
|
||||
assert_eq!(fixture.store.0.borrow()[&receipt.output.sha256], original);
|
||||
assert!(fixture.transport.requests.borrow().is_empty());
|
||||
assert_eq!(snapshot(&fixture), before);
|
||||
fixture.command("doctor", &[]).unwrap();
|
||||
assert_eq!(snapshot(&fixture), before);
|
||||
// 完整冷缓存恢复只下载固定 source/wheel;不请求任何版本 metadata。
|
||||
fixture.store.0.borrow_mut().clear();
|
||||
fixture.command("install", &[]).unwrap();
|
||||
assert!(
|
||||
fixture
|
||||
.transport
|
||||
.requests
|
||||
.borrow()
|
||||
.iter()
|
||||
.all(|url| url.starts_with("https://files.pythonhosted.org/"))
|
||||
);
|
||||
assert_eq!(fixture.store.0.borrow()[&receipt.output.sha256], original);
|
||||
assert_eq!(snapshot(&fixture), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep517_changed_metadata_and_exit10_preserve_previous_project() {
|
||||
for mode in ["mismatch", "exit10"] {
|
||||
let mut fixture = Python::new();
|
||||
pep517_fixture::prepare(&mut fixture, mode);
|
||||
fixture
|
||||
.command("add", &["pip:runtime-helper@1.0.0"])
|
||||
.unwrap();
|
||||
let before = snapshot(&fixture);
|
||||
let error = fixture
|
||||
.command("add", &["pip:built-native@1.0.0"])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error.contains(if mode == "mismatch" {
|
||||
"prepared metadata"
|
||||
} else {
|
||||
"expected PEP517 exit10"
|
||||
}),
|
||||
"{error}"
|
||||
);
|
||||
assert_eq!(snapshot(&fixture), before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep517_nonreproducible_replay_preserves_lock_and_does_not_publish_replacement() {
|
||||
let mut fixture = Python::new();
|
||||
pep517_fixture::prepare(&mut fixture, "unstable");
|
||||
fixture.command("add", &["pip:built-native@1.0.0"]).unwrap();
|
||||
let lock = fixture.lock();
|
||||
let receipt = Receipt::parse(&lock.builds[0]).unwrap();
|
||||
fixture.store.0.borrow_mut().remove(&receipt.output.sha256);
|
||||
let before = snapshot(&fixture);
|
||||
let cache = fixture.store.0.borrow().clone();
|
||||
assert!(
|
||||
fixture
|
||||
.command("install", &[])
|
||||
.unwrap_err()
|
||||
.contains("not byte reproducible")
|
||||
);
|
||||
assert_eq!(snapshot(&fixture), before);
|
||||
assert_eq!(*fixture.store.0.borrow(), cache);
|
||||
}
|
||||
|
||||
fn snapshot(fixture: &Python) -> [Vec<u8>; 2] {
|
||||
let root = &fixture.fixture.directory.0;
|
||||
[
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
std::fs::read(root.join("dever.lock")).unwrap(),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Owned test backend: separate hooks, exact build dependencies, real C compilation."""
|
||||
|
||||
import base64
|
||||
import ctypes
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
NAME = "built_native"
|
||||
VERSION = "1.0.0"
|
||||
INFO = NAME + "-" + VERSION + ".dist-info"
|
||||
META = "Metadata-Version: 2.1\nName: built-native\nVersion: 1.0.0\nRequires-Dist: runtime-helper>=1\n\n"
|
||||
|
||||
|
||||
def get_requires_for_build_wheel(settings):
|
||||
import build_helper
|
||||
assert build_helper.VALUE == 42
|
||||
assert subprocess.check_output(["build-probe"]).strip() == b"signed-console-42"
|
||||
return ["dynamic-helper==1.0.0"]
|
||||
|
||||
|
||||
def prepare_metadata_for_build_wheel(directory, settings):
|
||||
import dynamic_helper
|
||||
assert dynamic_helper.VALUE == 42
|
||||
assert not Path("hook-was-here").exists()
|
||||
Path("hook-was-here").write_text("metadata hook must run separately")
|
||||
target = Path(directory) / INFO
|
||||
target.mkdir()
|
||||
(target / "METADATA").write_text(META)
|
||||
return INFO
|
||||
|
||||
|
||||
def build_wheel(directory, settings, metadata_directory=None):
|
||||
import dynamic_helper
|
||||
assert dynamic_helper.VALUE == 42
|
||||
assert not Path("hook-was-here").exists()
|
||||
assert metadata_directory and (Path(metadata_directory) / "METADATA").read_text() == META
|
||||
mode = Path("mode.txt").read_text().strip()
|
||||
source = Path("native.c")
|
||||
source.write_text("int answer(void) { return 42; }\n")
|
||||
native = Path("native.so").resolve()
|
||||
compiler = shlex.split(os.environ["CC"])
|
||||
tools = Path(compiler[0]).parent
|
||||
subprocess.run(compiler + ["-B" + str(tools) + "/", "-ffreestanding", "-fno-use-linker-plugin", "-fno-lto", "-shared", "-fPIC", "-nostdlib", str(source), "-o", str(native)], check=True)
|
||||
assert ctypes.CDLL(str(native)).answer() == 42
|
||||
assert subprocess.check_output(["signed-pack-probe", "42"]) == b"signed-tool-42\n"
|
||||
files = {
|
||||
NAME + "/native.so": native.read_bytes(),
|
||||
NAME + "/__init__.py": b"VALUE = 42\n",
|
||||
INFO + "/METADATA": (META + ("Changed: yes\n" if mode == "mismatch" else "")).encode(),
|
||||
INFO + "/WHEEL": b"Wheel-Version: 1.0\nRoot-Is-Purelib: false\nTag: py3-none-linux_x86_64\n",
|
||||
}
|
||||
if mode == "unstable":
|
||||
files[NAME + "/nondeterministic.txt"] = str(time.time_ns()).encode()
|
||||
record = "".join(path + ",sha256=" + base64.urlsafe_b64encode(hashlib.sha256(value).digest()).decode().rstrip("=") + "," + str(len(value)) + "\n" for path, value in sorted(files.items()))
|
||||
files[INFO + "/RECORD"] = (record + INFO + "/RECORD,,\n").encode()
|
||||
filename = NAME + "-" + VERSION + "-py3-none-linux_x86_64.whl"
|
||||
with zipfile.ZipFile(Path(directory) / filename, "w") as archive:
|
||||
for path, value in sorted(files.items()):
|
||||
archive.writestr(zipfile.ZipInfo(path, date_time=(1980, 1, 1, 0, 0, 0)), value)
|
||||
if mode == "exit10":
|
||||
print("expected PEP517 exit10", flush=True)
|
||||
os._exit(10)
|
||||
return filename
|
||||
@@ -0,0 +1,187 @@
|
||||
//! 仅测试读取 dev Pod 工具构造签名 pack;生产路径没有这些工具定位逻辑。
|
||||
|
||||
use super::super::{
|
||||
Fixture,
|
||||
fixture::{digest, write},
|
||||
};
|
||||
use super::python_fixture::Python;
|
||||
use dever::compiler::project::dependency::worker::signed::SignedResources;
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use serde_json::json;
|
||||
use std::{collections::BTreeMap, fs, path::Path, process::Command, sync::OnceLock};
|
||||
|
||||
pub(super) fn prepare(fixture: &mut Python, mode: &str) {
|
||||
static ASSETS: OnceLock<BTreeMap<String, Vec<u8>>> = OnceLock::new();
|
||||
fixture.signed = signed(&fixture.fixture, ASSETS.get_or_init(assets));
|
||||
let helper = super::python_fixture::wheel_with_files(
|
||||
"build-helper",
|
||||
"1.0.0",
|
||||
"",
|
||||
BTreeMap::from([
|
||||
(
|
||||
"build_helper/probe.py".into(),
|
||||
b"def main():\n print('signed-console-42')\n".to_vec(),
|
||||
),
|
||||
(
|
||||
"build_helper-1.0.0.dist-info/entry_points.txt".into(),
|
||||
b"[console_scripts]\nbuild-probe = build_helper.probe:main\n".to_vec(),
|
||||
),
|
||||
]),
|
||||
);
|
||||
fixture
|
||||
.transport
|
||||
.release_wheel("build-helper", "1.0.0", helper);
|
||||
fixture.transport.release("dynamic-helper", "1.0.0", "");
|
||||
fixture.transport.release("runtime-helper", "1.0.0", "");
|
||||
let source = super::python_fixture::zip(BTreeMap::from([
|
||||
("built-native-1.0.0/pyproject.toml".into(), b"[build-system]\nrequires = ['build-helper>=1']\nbuild-backend = 'backend'\nbackend-path = ['.']\n".to_vec()),
|
||||
("built-native-1.0.0/backend.py".into(), include_bytes!("pep517_backend.py").to_vec()),
|
||||
("built-native-1.0.0/mode.txt".into(), mode.as_bytes().to_vec()),
|
||||
]));
|
||||
let url = "https://files.pythonhosted.org/packages/built-native-1.0.0.zip";
|
||||
fixture.transport.files.insert(url.into(), source.clone());
|
||||
fixture.transport.files.insert(
|
||||
"https://pypi.org/pypi/built-native/json".into(),
|
||||
serde_json::to_vec(&json!({"releases":{"1.0.0":[]}})).unwrap(),
|
||||
);
|
||||
fixture.transport.files.insert("https://pypi.org/pypi/built-native/1.0.0/json".into(), serde_json::to_vec(&json!({"urls":[{"packagetype":"sdist","filename":"built-native-1.0.0.zip","url":url,"digests":{"sha256":digest(&source)},"size":source.len()}]})).unwrap());
|
||||
}
|
||||
|
||||
fn assets() -> BTreeMap<String, Vec<u8>> {
|
||||
let details = Command::new("/usr/bin/python3").args(["-I", "-S", "-B", "-c", "import sys,sysconfig,json;print(json.dumps([sys.version.split()[0],sysconfig.get_path('stdlib')]))"]).output().unwrap();
|
||||
assert!(details.status.success());
|
||||
let values: Vec<String> = serde_json::from_slice(&details.stdout).unwrap();
|
||||
let version = &values[0];
|
||||
let short = version.rsplit_once('.').unwrap().0;
|
||||
let markers = json!({"implementation_name":"cpython","implementation_version":version,"os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":version,"python_version":short,"sys_platform":"linux"});
|
||||
let tags = ["py3-none-any", "py3-none-linux_x86_64"];
|
||||
let mut runtime = BTreeMap::new();
|
||||
runtime.insert("bin/python3".into(), fs::read("/usr/bin/python3").unwrap());
|
||||
files(
|
||||
Path::new(&values[1]),
|
||||
&format!("lib/python{short}/"),
|
||||
&mut runtime,
|
||||
);
|
||||
runtime.insert("dever-runtime.json".into(), serde_json::to_vec(&json!({"format":"dever-worker-runtime-v1","ecosystem":"pip","target":"linux-x86_64","executable":"bin/python3","arguments":["-I","-S","-B"],"python_wheel_tags":tags,"python_extension_suffixes":[".so"],"python_markers":markers})).unwrap());
|
||||
let mut libraries = BTreeMap::new();
|
||||
elf_libraries(Path::new("/usr/bin/python3"), &mut libraries);
|
||||
for entry in fs::read_dir(Path::new(&values[1]).join("lib-dynload")).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
if entry.path().extension().is_some_and(|value| value == "so") {
|
||||
elf_libraries(&entry.path(), &mut libraries);
|
||||
}
|
||||
}
|
||||
let runtime = archive(&runtime);
|
||||
let identity = json!({"name":"cpython","version":version,"sha256":digest(&runtime)});
|
||||
let mut tools = BTreeMap::new();
|
||||
let mut executables = Vec::new();
|
||||
for (name, path) in [
|
||||
("sh", "/bin/sh"),
|
||||
("cc", "/usr/bin/cc"),
|
||||
("c++", "/usr/bin/c++"),
|
||||
("as", "/usr/bin/as"),
|
||||
("ld", "/usr/bin/ld"),
|
||||
("ar", "/usr/bin/ar"),
|
||||
("ranlib", "/usr/bin/ranlib"),
|
||||
("make", "/usr/bin/make"),
|
||||
("cc1", "/usr/libexec/gcc/x86_64-linux-gnu/14/cc1"),
|
||||
("collect2", "/usr/libexec/gcc/x86_64-linux-gnu/14/collect2"),
|
||||
] {
|
||||
let relative = format!("rootfs/usr/bin/{name}");
|
||||
tools.insert(relative.clone(), fs::read(path).unwrap());
|
||||
executables.push(relative);
|
||||
elf_libraries(Path::new(path), &mut libraries);
|
||||
}
|
||||
let script = "rootfs/usr/bin/signed-pack-probe";
|
||||
tools.insert(
|
||||
script.into(),
|
||||
b"#!/bin/sh\nprintf 'signed-tool-%s\\n' \"$1\"\n".to_vec(),
|
||||
);
|
||||
executables.push(script.into());
|
||||
tools.insert("dever-build.json".into(), serde_json::to_vec(&json!({"format":"dever-lib-build-v1","ecosystem":"pip","target":"linux-x86_64","runtime_sha256":identity["sha256"],"python_runtime_sha256":null,"executables":executables})).unwrap());
|
||||
let tools = archive(&tools);
|
||||
let mut assets = BTreeMap::from([
|
||||
("runtime/pip/linux-x86_64/manifest.json".into(), serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":identity,"target":"linux-x86_64","python_markers":markers,"python_wheel_tags":tags}})).unwrap()),
|
||||
("runtime/pip/linux-x86_64/runtime.pack".into(), runtime),
|
||||
("build/pip/linux-x86_64/manifest.json".into(), serde_json::to_vec(&json!({"format":"dever-registry-build-v1","ecosystem":"pip","target":"linux-x86_64","runtime_sha256":identity["sha256"],"python_runtime_sha256":null,"sha256":digest(&tools)})).unwrap()),
|
||||
("build/pip/linux-x86_64/build.pack".into(), tools),
|
||||
]);
|
||||
for (name, bytes) in libraries {
|
||||
assets.insert(format!("process-runtime/linux-x86_64/lib/{name}"), bytes);
|
||||
}
|
||||
assets
|
||||
}
|
||||
|
||||
fn files(root: &Path, prefix: &str, output: &mut BTreeMap<String, Vec<u8>>) {
|
||||
for entry in fs::read_dir(root).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let name = entry.file_name().into_string().unwrap();
|
||||
if name == "__pycache__" || name == "site-packages" || name == "test" {
|
||||
continue;
|
||||
}
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
files(&entry.path(), &format!("{prefix}{name}/"), output);
|
||||
} else if entry.file_type().unwrap().is_file() {
|
||||
output.insert(format!("{prefix}{name}"), fs::read(entry.path()).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn elf_libraries(program: &Path, output: &mut BTreeMap<String, Vec<u8>>) {
|
||||
let result = Command::new("/usr/bin/ldd").arg(program).output().unwrap();
|
||||
assert!(result.status.success(), "fixture ldd {}", program.display());
|
||||
for name in String::from_utf8(result.stdout)
|
||||
.unwrap()
|
||||
.split_whitespace()
|
||||
.filter(|value| value.starts_with('/'))
|
||||
{
|
||||
let path = Path::new(name);
|
||||
let name = path.file_name().unwrap().to_str().unwrap().to_owned();
|
||||
let bytes = fs::read(path).unwrap();
|
||||
if let Some(previous) = output.insert(name, bytes.clone()) {
|
||||
assert_eq!(previous, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn archive(files: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
|
||||
let gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
let mut archive = tar::Builder::new(gzip);
|
||||
for (path, bytes) in files {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_mode(0o755);
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_cksum();
|
||||
archive
|
||||
.append_data(&mut header, path, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
archive.into_inner().unwrap().finish().unwrap()
|
||||
}
|
||||
|
||||
fn signed(fixture: &Fixture, assets: &BTreeMap<String, Vec<u8>>) -> SignedResources {
|
||||
let root = fixture.directory.0.join("pep517-signed");
|
||||
let key = Ed25519KeyPair::from_seed_unchecked(&[82; 32]).unwrap();
|
||||
let hex = |bytes: &[u8]| {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>()
|
||||
};
|
||||
let catalog = serde_json::to_vec(&json!({"format":"dever-release-v2","version":"0.0.1","platform":"linux-x86_64","artifacts":assets.iter().map(|(path, bytes)| json!({"path":path,"bytes":bytes.len(),"sha256":digest(bytes)})).collect::<Vec<_>>(),"extensions":[]})).unwrap();
|
||||
write(
|
||||
&root,
|
||||
"state/trusted-release-key",
|
||||
hex(key.public_key().as_ref()),
|
||||
);
|
||||
for (path, bytes) in assets {
|
||||
write(&root, &format!("versions/0.0.1/{path}"), bytes);
|
||||
}
|
||||
write(&root, "versions/0.0.1/manifest.json", &catalog);
|
||||
write(
|
||||
&root,
|
||||
"versions/0.0.1/manifest.sig",
|
||||
hex(key.sign(&catalog).as_ref()),
|
||||
);
|
||||
SignedResources::load(&root, "0.0.1").unwrap()
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
//! 真实 wheel、回环元数据及命令发布回归,不以字符串匹配代替完整闭包与文件校验。
|
||||
|
||||
use super::python_fixture::{Python, wheel, zip};
|
||||
use dever::compiler::project::dependency::{
|
||||
Lock,
|
||||
library::{self, python},
|
||||
};
|
||||
use dever::compiler::toolchain::BuildTarget;
|
||||
|
||||
#[test]
|
||||
fn python_resolves_wheel_metadata_extras_markers_and_backtracks() {
|
||||
let mut fixture = Python::new();
|
||||
fixture.transport.release("alpha", "1.0.0", "Requires-Dist: common>=1,<3\nRequires-Dist: helper; extra == 'test'\nProvides-Extra: test\nRequires-Dist: windows-only; sys_platform == 'win32'\n");
|
||||
fixture
|
||||
.transport
|
||||
.release("common", "2.0.0", "Requires-Dist: helper>=2\n");
|
||||
fixture
|
||||
.transport
|
||||
.release("common", "1.0.0", "Requires-Dist: helper<2\n");
|
||||
fixture.transport.release("helper", "1.0.0", "");
|
||||
fixture.transport.release("helper", "2.0.0", "");
|
||||
fixture
|
||||
.command("add", &["pip:alpha[test]@1.0.0", "pip:helper@1.0.0"])
|
||||
.unwrap();
|
||||
let lock = fixture.lock();
|
||||
assert_eq!(
|
||||
lock.libs
|
||||
.iter()
|
||||
.map(|lib| lib.spec.key())
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
"pip:alpha[test]@1.0.0",
|
||||
"pip:common@1.0.0",
|
||||
"pip:helper@1.0.0"
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
fixture
|
||||
.transport
|
||||
.requests
|
||||
.borrow()
|
||||
.iter()
|
||||
.all(|url| !url.contains("wrong-registry-summary") && !url.contains("windows-only"))
|
||||
);
|
||||
let metadata = fixture.runtime();
|
||||
let runtime = library::runtime::load(
|
||||
&fixture.signed,
|
||||
"pip",
|
||||
BuildTarget::host().unwrap(),
|
||||
&metadata.pack,
|
||||
)
|
||||
.unwrap();
|
||||
for lib in &lock.libs {
|
||||
let files = python::install(
|
||||
lib,
|
||||
&fixture.store.0.borrow()[&lib.artifacts[0].sha256],
|
||||
&runtime.manifest,
|
||||
&lock.libs,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(files.keys().any(|path| path.ends_with("__init__.py")));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_update_preserves_other_roots_and_exact_install_never_resolves() {
|
||||
let mut fixture = Python::new();
|
||||
for (name, version) in [("alpha", "1.0.0"), ("alpha", "2.0.0"), ("beta", "1.0.0")] {
|
||||
fixture.transport.release(name, version, "");
|
||||
}
|
||||
fixture
|
||||
.command("add", &["pip:alpha@1.0.0", "pip:beta@1.0.0"])
|
||||
.unwrap();
|
||||
fixture.command("update", &["pip:alpha@2.0.0"]).unwrap();
|
||||
let lock = fixture.lock();
|
||||
assert_eq!(
|
||||
lock.libs
|
||||
.iter()
|
||||
.map(|lib| lib.spec.key())
|
||||
.collect::<Vec<_>>(),
|
||||
["pip:alpha@2.0.0", "pip:beta@1.0.0"]
|
||||
);
|
||||
let root = &fixture.fixture.directory.0;
|
||||
let before = [
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
std::fs::read(root.join("dever.lock")).unwrap(),
|
||||
];
|
||||
fixture.store.0.borrow_mut().clear();
|
||||
fixture.transport.requests.borrow_mut().clear();
|
||||
fixture.command("install", &[]).unwrap();
|
||||
assert_eq!(fixture.transport.requests.borrow().len(), 2);
|
||||
assert!(
|
||||
fixture
|
||||
.transport
|
||||
.requests
|
||||
.borrow()
|
||||
.iter()
|
||||
.all(|url| url.starts_with("https://files.pythonhosted.org/"))
|
||||
);
|
||||
assert_eq!(fixture.store.0.borrow().len(), 2);
|
||||
fixture.transport.requests.borrow_mut().clear();
|
||||
let cache = fixture.store.0.borrow().clone();
|
||||
for action in ["install", "doctor"] {
|
||||
fixture.store.1.borrow_mut().clear();
|
||||
fixture.command(action, &[]).unwrap();
|
||||
let reads = fixture.store.1.borrow();
|
||||
let read_bytes = reads.iter().map(|(_, bytes)| bytes).sum::<usize>();
|
||||
eprintln!(
|
||||
"cache {action}: reads={} bytes={read_bytes} fixtures={:?}",
|
||||
reads.len(),
|
||||
cache.keys().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(reads.len(), cache.len());
|
||||
assert_eq!(read_bytes, cache.values().map(Vec::len).sum::<usize>());
|
||||
for hash in cache.keys() {
|
||||
assert_eq!(reads.iter().filter(|(key, _)| key == hash).count(), 1);
|
||||
}
|
||||
}
|
||||
assert_eq!(*fixture.store.0.borrow(), cache);
|
||||
assert!(fixture.transport.requests.borrow().is_empty());
|
||||
assert_eq!(
|
||||
before,
|
||||
[
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
std::fs::read(root.join("dever.lock")).unwrap()
|
||||
]
|
||||
);
|
||||
let hash = cache.keys().next().unwrap();
|
||||
for damaged in [vec![0; cache[hash].len()], vec![]] {
|
||||
fixture.store.0.borrow_mut().insert(hash.clone(), damaged);
|
||||
for action in ["install", "doctor"] {
|
||||
assert!(
|
||||
fixture
|
||||
.command(action, &[])
|
||||
.unwrap_err()
|
||||
.contains("digest or length")
|
||||
);
|
||||
assert!(fixture.transport.requests.borrow().is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_resolution_and_tampered_restore_leave_project_and_cache_unchanged() {
|
||||
let mut fixture = Python::new();
|
||||
fixture.transport.release("alpha", "1.0.0", "");
|
||||
fixture
|
||||
.transport
|
||||
.release("broken", "1.0.0", "Requires-Dist: alpha<1\n");
|
||||
fixture.command("add", &["pip:alpha@1.0.0"]).unwrap();
|
||||
let root = &fixture.fixture.directory.0;
|
||||
let before = [
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
std::fs::read(root.join("dever.lock")).unwrap(),
|
||||
];
|
||||
assert!(
|
||||
fixture
|
||||
.command("add", &["pip:broken@1.0.0"])
|
||||
.unwrap_err()
|
||||
.contains("conflict")
|
||||
);
|
||||
let source = fixture.lock().libs[0].artifacts[0]
|
||||
.source
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.locator
|
||||
.clone();
|
||||
fixture.transport.files.insert(source, b"tampered".to_vec());
|
||||
fixture.store.0.borrow_mut().clear();
|
||||
assert!(
|
||||
fixture
|
||||
.command("install", &[])
|
||||
.unwrap_err()
|
||||
.contains("digest")
|
||||
);
|
||||
assert!(fixture.store.0.borrow().is_empty());
|
||||
assert_eq!(
|
||||
before,
|
||||
[
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
std::fs::read(root.join("dever.lock")).unwrap()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_record_rejects_mutation_omission_duplicate_and_weak_hash() {
|
||||
let fixture = Python::new();
|
||||
let bytes = wheel("alpha", "1.0.0", "");
|
||||
let lib = serde_json::from_value(serde_json::json!({"spec":{"ecosystem":"pip","name":"alpha","version":"1.0.0"},"dependencies":[],"runtime":fixture.runtime().pack,"artifacts":[],"schema":"pypi-wheel-v1"})).unwrap();
|
||||
let runtime = library::runtime::load(
|
||||
&fixture.signed,
|
||||
"pip",
|
||||
BuildTarget::host().unwrap(),
|
||||
&fixture.runtime().pack,
|
||||
)
|
||||
.unwrap();
|
||||
let original = library::archive::files(&bytes, true).unwrap();
|
||||
python::install(&lib, &bytes, &runtime.manifest, &[]).unwrap();
|
||||
let record = "alpha-1.0.0.dist-info/RECORD";
|
||||
for mode in 0..4 {
|
||||
let mut files = original.clone();
|
||||
match mode {
|
||||
0 => {
|
||||
files.insert("alpha/__init__.py".into(), b"VALUE = 43\n".to_vec());
|
||||
}
|
||||
1 => {
|
||||
files.insert("alpha/extra.py".into(), b"unlisted".to_vec());
|
||||
}
|
||||
2 => {
|
||||
let duplicate = files[record].clone();
|
||||
files.get_mut(record).unwrap().extend_from_slice(&duplicate);
|
||||
}
|
||||
_ => {
|
||||
let text = String::from_utf8(files[record].clone())
|
||||
.unwrap()
|
||||
.replace("sha256=", "md5=");
|
||||
files.insert(record.into(), text.into_bytes());
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
python::install(&lib, &zip(files), &runtime.manifest, &[]).is_err(),
|
||||
"accepted mode {mode}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_resolver_failure_does_not_mutate_input_lock() {
|
||||
let mut fixture = Python::new();
|
||||
fixture
|
||||
.transport
|
||||
.release("alpha", "1.0.0", "Requires-Dist: missing>=1\n");
|
||||
let mut lock = Lock::new(vec![]);
|
||||
let before = lock.encode().unwrap();
|
||||
let result = python::resolve(
|
||||
&["pip:alpha@1.0.0".parse().unwrap()],
|
||||
BuildTarget::host().unwrap(),
|
||||
&fixture.runtime(),
|
||||
&fixture.transport,
|
||||
&fixture.store,
|
||||
&mut lock,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(lock.encode().unwrap(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_requirements_are_checked_without_a_spurious_locked_edge() {
|
||||
let mut fixture = Python::new();
|
||||
fixture
|
||||
.transport
|
||||
.release("alpha", "1.0.0", "Requires-Dist: alpha>=1\n");
|
||||
fixture.command("add", &["pip:alpha@1.0.0"]).unwrap();
|
||||
assert!(fixture.lock().libs[0].dependencies.is_empty());
|
||||
fixture.command("doctor", &[]).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
//! 回环 PyPI 和仅测试签名发行;测试显式使用 dev Pod 的 Python,不修改全局安装。
|
||||
|
||||
use super::super::{
|
||||
Fixture,
|
||||
fixture::{digest, write},
|
||||
};
|
||||
use dever::compiler::project::dependency::{
|
||||
Store,
|
||||
library::{registry::Transport, runtime},
|
||||
worker::signed::SignedResources,
|
||||
};
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::BTreeMap,
|
||||
io::{Read, Write},
|
||||
net::TcpListener,
|
||||
};
|
||||
|
||||
pub(super) struct Python {
|
||||
pub fixture: Fixture,
|
||||
pub signed: SignedResources,
|
||||
pub transport: Registry,
|
||||
pub store: Memory,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Memory(
|
||||
pub RefCell<BTreeMap<String, Vec<u8>>>,
|
||||
pub RefCell<Vec<(String, usize)>>,
|
||||
);
|
||||
impl Store for Memory {
|
||||
fn get(&self, hash: &str, _: u64) -> Result<Option<Vec<u8>>, String> {
|
||||
let bytes = self.0.borrow().get(hash).cloned();
|
||||
self.1
|
||||
.borrow_mut()
|
||||
.push((hash.into(), bytes.as_ref().map_or(0, Vec::len)));
|
||||
Ok(bytes)
|
||||
}
|
||||
fn put(&self, bytes: &[u8]) -> Result<(), String> {
|
||||
self.0.borrow_mut().insert(digest(bytes), bytes.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Registry {
|
||||
pub files: BTreeMap<String, Vec<u8>>,
|
||||
versions: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
|
||||
pub requests: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Transport for Registry {
|
||||
fn get(&self, url: &str, limit: usize) -> Result<Vec<u8>, String> {
|
||||
self.requests.borrow_mut().push(url.into());
|
||||
let bytes = self
|
||||
.files
|
||||
.get(url)
|
||||
.ok_or_else(|| format!("test registry misses {url}"))?
|
||||
.clone();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let thread = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(3)))
|
||||
.unwrap();
|
||||
let mut request = Vec::new();
|
||||
while !request.ends_with(b"\r\n\r\n") {
|
||||
let mut byte = [0];
|
||||
stream.read_exact(&mut byte).unwrap();
|
||||
request.push(byte[0]);
|
||||
assert!(request.len() < 4096);
|
||||
}
|
||||
write!(
|
||||
stream,
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
bytes.len()
|
||||
)
|
||||
.unwrap();
|
||||
stream.write_all(&bytes).unwrap();
|
||||
});
|
||||
let client: ureq::Agent = ureq::Agent::config_builder()
|
||||
.proxy(None)
|
||||
.max_redirects(0)
|
||||
.timeout_global(Some(std::time::Duration::from_secs(3)))
|
||||
.build()
|
||||
.into();
|
||||
let mut response = client
|
||||
.get(format!("http://{address}/metadata"))
|
||||
.call()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut bytes = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.as_reader()
|
||||
.take(limit as u64 + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.unwrap();
|
||||
thread.join().unwrap();
|
||||
if bytes.len() > limit {
|
||||
return Err("fixture exceeded transfer budget".into());
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn release(&mut self, name: &str, version: &str, headers: &str) -> Vec<u8> {
|
||||
self.release_wheel(name, version, wheel(name, version, headers))
|
||||
}
|
||||
|
||||
pub fn release_wheel(&mut self, name: &str, version: &str, bytes: Vec<u8>) -> Vec<u8> {
|
||||
let filename = format!("{}-{version}-py3-none-any.whl", name.replace('-', "_"));
|
||||
let url = format!("https://files.pythonhosted.org/packages/{filename}");
|
||||
self.files.insert(url.clone(), bytes.clone());
|
||||
let artifact = serde_json::json!({"packagetype":"bdist_wheel","filename":filename,"url":url,"digests":{"sha256":digest(&bytes)},"size":bytes.len(),"requires_python":">=3.11"});
|
||||
self.files.insert(format!("https://pypi.org/pypi/{name}/{version}/json"), serde_json::to_vec(&serde_json::json!({"info":{"requires_dist":["wrong-registry-summary>=999"]},"urls":[artifact]})).unwrap());
|
||||
self.versions
|
||||
.entry(name.into())
|
||||
.or_default()
|
||||
.insert(version.into(), serde_json::json!([]));
|
||||
self.files.insert(
|
||||
format!("https://pypi.org/pypi/{name}/json"),
|
||||
serde_json::to_vec(&serde_json::json!({"releases":self.versions[name]})).unwrap(),
|
||||
);
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn wheel(name: &str, version: &str, headers: &str) -> Vec<u8> {
|
||||
wheel_with_files(name, version, headers, BTreeMap::new())
|
||||
}
|
||||
|
||||
pub(super) fn wheel_with_files(
|
||||
name: &str,
|
||||
version: &str,
|
||||
headers: &str,
|
||||
extra: BTreeMap<String, Vec<u8>>,
|
||||
) -> Vec<u8> {
|
||||
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use sha2::{Digest, Sha256};
|
||||
let stem = format!("{}-{version}.dist-info", name.replace('-', "_"));
|
||||
let mut files = BTreeMap::from([
|
||||
(
|
||||
format!("{}/__init__.py", name.replace('-', "_")),
|
||||
b"VALUE = 42\n".to_vec(),
|
||||
),
|
||||
(
|
||||
format!("{stem}/METADATA"),
|
||||
format!("Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n{headers}\n")
|
||||
.into_bytes(),
|
||||
),
|
||||
(
|
||||
format!("{stem}/WHEEL"),
|
||||
b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n".to_vec(),
|
||||
),
|
||||
]);
|
||||
files.extend(extra);
|
||||
let mut record = files
|
||||
.iter()
|
||||
.map(|(path, bytes)| {
|
||||
format!(
|
||||
"{path},sha256={},{}\n",
|
||||
URL_SAFE_NO_PAD.encode(Sha256::digest(bytes)),
|
||||
bytes.len()
|
||||
)
|
||||
})
|
||||
.collect::<String>();
|
||||
record.push_str(&format!("{stem}/RECORD,,\n"));
|
||||
files.insert(format!("{stem}/RECORD"), record.into_bytes());
|
||||
zip(files)
|
||||
}
|
||||
|
||||
pub(super) fn zip(files: BTreeMap<String, Vec<u8>>) -> Vec<u8> {
|
||||
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for (path, bytes) in files {
|
||||
writer
|
||||
.start_file(path, zip::write::SimpleFileOptions::default())
|
||||
.unwrap();
|
||||
writer.write_all(&bytes).unwrap();
|
||||
}
|
||||
writer.finish().unwrap().into_inner()
|
||||
}
|
||||
|
||||
impl Python {
|
||||
pub fn new() -> Self {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = fixture.directory.0.join("test-signed");
|
||||
let executable = std::fs::read("/usr/bin/python3")
|
||||
.expect("Python fixture requires the existing dev Pod interpreter");
|
||||
let result = std::process::Command::new("/usr/bin/python3")
|
||||
.args([
|
||||
"-I",
|
||||
"-S",
|
||||
"-B",
|
||||
"-c",
|
||||
"import sys; print(sys.version.split()[0])",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(result.status.success());
|
||||
let version = String::from_utf8(result.stdout).unwrap().trim().to_owned();
|
||||
let short = version.rsplit_once('.').unwrap().0;
|
||||
let markers = serde_json::json!({"implementation_name":"cpython","implementation_version":version,"os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":version,"python_version":short,"sys_platform":"linux"});
|
||||
let manifest = serde_json::to_vec(&serde_json::json!({"format":"dever-worker-runtime-v1","ecosystem":"pip","target":"linux-x86_64","executable":"bin/python3","arguments":["-I","-S","-B"],"python_wheel_tags":["py3-none-any"],"python_extension_suffixes":[".so"],"python_markers":markers})).unwrap();
|
||||
let gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
let mut archive = tar::Builder::new(gzip);
|
||||
for (path, bytes) in [
|
||||
("bin/python3", executable),
|
||||
("dever-runtime.json", manifest),
|
||||
] {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_mode(0o755);
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_cksum();
|
||||
archive
|
||||
.append_data(&mut header, path, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
let pack = archive.into_inner().unwrap().finish().unwrap();
|
||||
let metadata = serde_json::to_vec(&serde_json::json!({"format":"dever-registry-runtime-v1","runtime":{"pack":{"name":"cpython","version":version,"sha256":digest(&pack)},"target":"linux-x86_64","python_markers":markers,"python_wheel_tags":["py3-none-any"]}})).unwrap();
|
||||
let assets = [
|
||||
("runtime/pip/linux-x86_64/manifest.json", metadata),
|
||||
("runtime/pip/linux-x86_64/runtime.pack", pack),
|
||||
];
|
||||
let key = Ed25519KeyPair::from_seed_unchecked(&[81; 32]).unwrap();
|
||||
let hex = |bytes: &[u8]| {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>()
|
||||
};
|
||||
write(
|
||||
&root,
|
||||
"state/trusted-release-key",
|
||||
hex(key.public_key().as_ref()),
|
||||
);
|
||||
let catalog = serde_json::to_vec(&serde_json::json!({"format":"dever-release-v2","version":"0.0.1","platform":"linux-x86_64","artifacts":assets.iter().map(|(path, bytes)| serde_json::json!({"path":path,"bytes":bytes.len(),"sha256":digest(bytes)})).collect::<Vec<_>>(),"extensions":[]})).unwrap();
|
||||
for (path, bytes) in assets {
|
||||
write(&root, &format!("versions/0.0.1/{path}"), bytes);
|
||||
}
|
||||
write(&root, "versions/0.0.1/manifest.json", &catalog);
|
||||
write(
|
||||
&root,
|
||||
"versions/0.0.1/manifest.sig",
|
||||
hex(key.sign(&catalog).as_ref()),
|
||||
);
|
||||
let signed = SignedResources::load(&root, "0.0.1").unwrap();
|
||||
Self {
|
||||
fixture,
|
||||
signed,
|
||||
transport: Registry::default(),
|
||||
store: Memory::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runtime(&self) -> runtime::Metadata {
|
||||
runtime::metadata(
|
||||
&self.signed,
|
||||
"pip",
|
||||
dever::compiler::toolchain::BuildTarget::host().unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn command(&self, action: &str, specs: &[&str]) -> Result<String, String> {
|
||||
dever::compiler::project::dependency::library::command::execute_with(
|
||||
action,
|
||||
&self.fixture.directory.0,
|
||||
&specs
|
||||
.iter()
|
||||
.map(|value| value.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
dever::compiler::toolchain::BuildTarget::host().unwrap(),
|
||||
&self.transport,
|
||||
&self.store,
|
||||
Some(&self.signed),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn lock(&self) -> dever::compiler::project::dependency::Lock {
|
||||
dever::compiler::project::dependency::Lock::decode(
|
||||
&std::fs::read_to_string(self.fixture.directory.0.join("dever.lock")).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user