fix: restart Dever releases at 0.0.1 with complete signed assets
This commit is contained in:
1 parent
5589b657db
commit
faf8b168a7
22 files changed
+582
-94
No files matched your search
+3
-3
@@ -56,14 +56,14 @@
|
||||
|
||||
```sh
|
||||
python3 -B sdk/release-assets.py /absolute/candidate/ecosystems/release \
|
||||
--output /absolute/candidate/assets --zstd /absolute/author/bin/zstd
|
||||
--output /absolute/candidate/assets --zstd /absolute/author/bin/zstd --partial-test-fixture
|
||||
```
|
||||
|
||||
拆包测试复用 `test/onboarding/test_installer.py`,默认作者工具为 `/usr/bin/zstd`;不同工具位置通过测试专用的 `DEVER_TEST_ZSTD` 显式指定。该测试同时核验三种语言扩展互不混入、基础包内容不变且压缩字节不因新增扩展变化,不安装全局编译器。
|
||||
不带 `--partial-test-fixture` 时默认按完整公开发行检查,要求双架构语言扩展、宿主构建扩展、ARM64 原生目标及 `--editor-vsix` 指定的同版本编辑器包;不完整的测试附件不能用于公开发行。拆包测试复用 `test/onboarding/test_installer.py`,默认作者工具为 `/usr/bin/zstd`;不同工具位置通过测试专用的 `DEVER_TEST_ZSTD` 显式指定。该测试同时核验三种语言扩展互不混入、基础包内容不变且压缩字节不因新增扩展变化,不安装全局编译器。
|
||||
|
||||
完整版本验收使用 `test/onboarding/verify_release_update.py`,显式传入 `--previous-release`、`--release`、`--assets`、`--trusted-key`、`--extension-helper`,以及不存在的绝对 `--work`、`--report` 路径。在独立验证容器中以 root 运行,不挂载业务数据、不启动系统服务;使用容器自有临时空间,不占用正在服务的 dev/deverd 临时卷。它先验证基础包首装、正式项目模板、四档运行库、`long` 和按需语言扩展,再验证真实旧版本升级与回退。旧、新版本必须具有各自实际构建的二进制,不能只改清单版本模拟升级;临时测试密钥的结果也不能算作正式签名验收。
|
||||
|
||||
升级核验按签名版本读取旧版原始资源路径和 bootstrap 元数据,不恢复旧沙箱执行;0.1.5 起的新包不得携带已移除的 bootstrap 沙箱配置。旧文件仍须通过原签名中的大小和摘要校验。
|
||||
当前发行只接受 `process-runtime` 资源布局和现行 bootstrap 文件集合,不再读取旧沙箱布局;资源格式不由版本号大小推断,因此重置为 `0.0.1` 不改变执行方式。已签名的服务模板仍校验原大小与摘要,显式安装器才应用当前模板;签名和防降级检查不关闭。
|
||||
|
||||
`author_outputs` 精确声明 author 会制作的 pack、下载结果和 receipt 路径。author 阶段只将其余原始输入当作不可变输入,另外记录完整输出摘要;后续验收按准备后完整输入摘要绑定。不要把整个作者输入根排除掉。显式 `author_network: true` 才能执行已分类的固定官方下载/源码构建。新 ignored 必须人工更新 `ignored.json`;inventory 核对源码声明及实际 all-feature libtest 名单,执行时一律使用完整 `--exact` 名称。内部 probe 记录 parent-only,只由所属父用例运行。
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use super::Stage;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[path = "support/native_elf.rs"]
|
||||
mod elf;
|
||||
|
||||
fn prepared(files: &BTreeMap<String, (Vec<u8>, bool)>) -> Stage {
|
||||
let stage = Stage::new().unwrap();
|
||||
for (path, (bytes, executable)) in files {
|
||||
stage
|
||||
.write(&format!("input/{path}"), bytes, *executable)
|
||||
.unwrap();
|
||||
}
|
||||
stage
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_tool_libraries_share_bytes_without_replacing_host_glibc() {
|
||||
let names = [
|
||||
"libbfd.so",
|
||||
"libstdc++.so.6",
|
||||
"libgcc_s.so.1",
|
||||
"libc.so.6",
|
||||
"libm.so.6",
|
||||
"libmvec.so.1",
|
||||
"libanl.so.1",
|
||||
"libdl.so.2",
|
||||
"libpthread.so.0",
|
||||
"librt.so.1",
|
||||
"libutil.so.1",
|
||||
"libresolv.so.2",
|
||||
"ld-linux-x86-64.so.2",
|
||||
"ld-linux-aarch64.so.1",
|
||||
];
|
||||
let files = names
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
(
|
||||
format!("rootfs/usr/lib/fixture/{name}"),
|
||||
(elf::library(name, &[]), false),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let stage = prepared(&files);
|
||||
stage.link_tool_libraries(&files).unwrap();
|
||||
for name in names {
|
||||
let destination = stage.0.join("input/lib").join(name);
|
||||
if matches!(name, "libbfd.so" | "libstdc++.so.6" | "libgcc_s.so.1") {
|
||||
let source = stage.0.join("input/rootfs/usr/lib/fixture").join(name);
|
||||
assert_eq!(
|
||||
fs::metadata(&source).unwrap().ino(),
|
||||
fs::metadata(&destination).unwrap().ino()
|
||||
);
|
||||
assert_eq!(fs::read(source).unwrap(), fs::read(destination).unwrap());
|
||||
} else {
|
||||
assert!(!destination.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_sonames_require_identical_signed_bytes() {
|
||||
let original = elf::library("libfixture.so", &[]);
|
||||
let mut files = BTreeMap::from([
|
||||
("rootfs/usr/lib/first.so".into(), (original.clone(), false)),
|
||||
("rootfs/usr/lib/second.so".into(), (original, false)),
|
||||
]);
|
||||
let stage = prepared(&files);
|
||||
stage.link_tool_libraries(&files).unwrap();
|
||||
files.get_mut("rootfs/usr/lib/second.so").unwrap().0 =
|
||||
elf::library("libfixture.so", &["libc.so.6"]);
|
||||
let stage = prepared(&files);
|
||||
assert!(
|
||||
stage
|
||||
.link_tool_libraries(&files)
|
||||
.unwrap_err()
|
||||
.contains("conflicting SONAMEs")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_library_names_cannot_escape_the_owned_search_directory() {
|
||||
for name in ["../escape.so", "/escape.so", "nested/escape.so"] {
|
||||
let files = BTreeMap::from([(
|
||||
"rootfs/usr/lib/fixture.so".into(),
|
||||
(elf::library(name, &[]), false),
|
||||
)]);
|
||||
let stage = prepared(&files);
|
||||
assert!(stage.link_tool_libraries(&files).is_err());
|
||||
assert!(!stage.0.join("input/escape.so").exists());
|
||||
}
|
||||
}
|
||||
@@ -177,9 +177,9 @@ fn signed_installed_versions_keep_their_original_bootstrap_service_template() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_historical_bootstrap_profiles_are_read_only_and_version_bound() {
|
||||
fn historical_bootstrap_profiles_are_rejected_regardless_of_version_number() {
|
||||
use dever_cli::toolchain::{Artifact, Layout, MachineManager, Version};
|
||||
for release_version in ["0.1.4", "0.1.5"] {
|
||||
for release_version in ["0.0.1", "0.1.4", "0.1.6"] {
|
||||
let mut author = Author::new();
|
||||
author.settings["version"] = json!(release_version);
|
||||
add_bootstrap(&mut author);
|
||||
@@ -220,26 +220,12 @@ fn signed_historical_bootstrap_profiles_are_read_only_and_version_bound() {
|
||||
)
|
||||
.unwrap();
|
||||
let manager = MachineManager::new(layout);
|
||||
if release_version == "0.1.4" {
|
||||
assert_eq!(
|
||||
manager.resolve_core(&version).unwrap(),
|
||||
installed.join("dever-core")
|
||||
);
|
||||
fs::write(profile, b"unsigned change").unwrap();
|
||||
assert!(
|
||||
manager
|
||||
.resolve_core(&version)
|
||||
.unwrap_err()
|
||||
.contains("integrity verification")
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
manager
|
||||
.resolve_core(&version)
|
||||
.unwrap_err()
|
||||
.contains("unsupported artifact")
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
manager
|
||||
.resolve_core(&version)
|
||||
.unwrap_err()
|
||||
.contains("unsupported artifact")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ use dever_runtime::config::RuntimeProfile;
|
||||
|
||||
#[test]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn target_resource_namespaces_are_bound_to_the_signed_release_version() {
|
||||
fn target_resource_namespaces_do_not_depend_on_release_version_numbers() {
|
||||
use dever_cli::toolchain::{
|
||||
Artifact, Extension, ExtensionKind, ReleaseManifest, Version, validate_catalog,
|
||||
};
|
||||
let mut manifest = ReleaseManifest::new(
|
||||
Version::parse("0.1.4").unwrap(),
|
||||
Version::parse("0.0.1").unwrap(),
|
||||
vec![Artifact {
|
||||
path: "dever-core".into(),
|
||||
bytes: 1,
|
||||
@@ -25,21 +25,18 @@ fn target_resource_namespaces_are_bound_to_the_signed_release_version() {
|
||||
sha256: "0".repeat(64),
|
||||
}],
|
||||
});
|
||||
validate_catalog(&manifest).unwrap();
|
||||
manifest.version = Version::parse("0.1.5").unwrap();
|
||||
assert!(
|
||||
validate_catalog(&manifest)
|
||||
.unwrap_err()
|
||||
.contains("resource namespace")
|
||||
);
|
||||
manifest.extensions[0].artifacts[0].path = "process-runtime/linux-aarch64/lib/libc.so.6".into();
|
||||
validate_catalog(&manifest).unwrap();
|
||||
manifest.version = Version::parse("0.1.4").unwrap();
|
||||
assert!(
|
||||
validate_catalog(&manifest)
|
||||
.unwrap_err()
|
||||
.contains("resource namespace")
|
||||
);
|
||||
for version in ["0.0.1", "0.1.4", "0.1.6", "1.0.0"] {
|
||||
manifest.version = Version::parse(version).unwrap();
|
||||
manifest.extensions[0].artifacts[0].path = "sandbox/linux-aarch64/lib/libc.so.6".into();
|
||||
assert!(
|
||||
validate_catalog(&manifest)
|
||||
.unwrap_err()
|
||||
.contains("resource namespace")
|
||||
);
|
||||
manifest.extensions[0].artifacts[0].path =
|
||||
"process-runtime/linux-aarch64/lib/libc.so.6".into();
|
||||
validate_catalog(&manifest).unwrap();
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub(super) fn cross_author() -> Author {
|
||||
|
||||
@@ -32,6 +32,7 @@ TOOLS = {
|
||||
)},
|
||||
}
|
||||
LIBRARIES = (
|
||||
"ld-linux-x86-64.so.2",
|
||||
"libc.so.6", "libm.so.6", "libdl.so.2", "libpthread.so.0", "librt.so.1",
|
||||
"libgcc_s.so.1", "libstdc++.so.6", "libisl.so.23", "libmpc.so.3",
|
||||
"libmpfr.so.6", "libgmp.so.10", "libz.so.1", "libzstd.so.1",
|
||||
@@ -46,8 +47,10 @@ LIBRARIES = (
|
||||
|
||||
def signed_library_paths(content):
|
||||
# 仅迁移以 /lib 开头的绝对 token,不能再次改写已有的 /usr/lib。
|
||||
return re.sub(r"(?<!\S)/lib/" + re.escape(MULTIARCH) + "/",
|
||||
f"/usr/lib/{MULTIARCH}/", content)
|
||||
rewritten = re.sub(r"(?<!\S)/lib/" + re.escape(MULTIARCH) + "/",
|
||||
f"/usr/lib/{MULTIARCH}/", content)
|
||||
return re.sub(r"(?<!\S)/lib64/ld-linux-x86-64\.so\.2(?=[\s)])",
|
||||
f"/usr/lib/{MULTIARCH}/ld-linux-x86-64.so.2", rewritten)
|
||||
|
||||
|
||||
def sdk_leaf(source, destination, changes):
|
||||
|
||||
@@ -11,6 +11,8 @@ class LinkerScriptTests(unittest.TestCase):
|
||||
f"/usr/lib/{MULTIARCH}/libc_nonshared.a "
|
||||
"AS_NEEDED ( /lib64/ld-linux-x86-64.so.2 ) )")
|
||||
expected = original.replace(f"( /lib/{MULTIARCH}/", f"( /usr/lib/{MULTIARCH}/")
|
||||
expected = expected.replace("/lib64/ld-linux-x86-64.so.2",
|
||||
f"/usr/lib/{MULTIARCH}/ld-linux-x86-64.so.2")
|
||||
self.assertEqual(signed_library_paths(original), expected)
|
||||
self.assertEqual(signed_library_paths(expected), expected)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from unittest import mock
|
||||
|
||||
from install_support import load_installer
|
||||
@@ -88,7 +89,7 @@ class InstallerTests(unittest.TestCase):
|
||||
self.platform, "1.2.3")
|
||||
|
||||
def create_assets(self, output):
|
||||
return assets.create(self.release, output, ZSTD)
|
||||
return assets.create(self.release, output, ZSTD, complete=False)
|
||||
|
||||
def release_record(self, directory, version="1.2.3"):
|
||||
return {
|
||||
@@ -173,6 +174,39 @@ class InstallerTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "base package"):
|
||||
self.create_assets(self.root / "host-target")
|
||||
|
||||
def test_complete_release_requires_all_extensions_and_matching_editor(self):
|
||||
manifest = json.loads((self.release / "manifest.json").read_text())
|
||||
with self.assertRaisesRegex(ValueError, "complete release is missing"):
|
||||
assets.create(self.release, self.root / "incomplete", ZSTD)
|
||||
self.assertFalse((self.root / "incomplete").exists())
|
||||
with self.assertRaisesRegex(ValueError, "complete release is missing"):
|
||||
assets.verify_complete(manifest, None)
|
||||
for target in ("linux-x86_64", "linux-aarch64"):
|
||||
for ecosystem in ("pip", "npm", "go"):
|
||||
manifest["extensions"].append({"kind": {"type": "runtime", "ecosystem": ecosystem},
|
||||
"target": target, "artifacts": []})
|
||||
for ecosystem in ("pip", "npm"):
|
||||
manifest["extensions"].append({"kind": {"type": "build", "ecosystem": ecosystem},
|
||||
"target": self.platform, "artifacts": []})
|
||||
manifest["extensions"].append({"kind": {"type": "target"}, "target": "linux-aarch64",
|
||||
"artifacts": []})
|
||||
with self.assertRaisesRegex(ValueError, "editor VSIX"):
|
||||
assets.verify_complete(manifest, None)
|
||||
editor = self.root / "editor.vsix"
|
||||
package = {"name": "dever-language", "publisher": "shemic", "version": manifest["version"]}
|
||||
with zipfile.ZipFile(editor, "w") as archive:
|
||||
archive.writestr("extension/package.json", json.dumps(package))
|
||||
assets.verify_complete(manifest, editor)
|
||||
for index in range(len(manifest["extensions"])):
|
||||
incomplete = {**manifest, "extensions": manifest["extensions"][:index] + manifest["extensions"][index + 1:]}
|
||||
with self.assertRaisesRegex(ValueError, "complete release is missing"):
|
||||
assets.verify_complete(incomplete, editor)
|
||||
package["version"] = "9.9.9"
|
||||
with zipfile.ZipFile(editor, "w") as archive:
|
||||
archive.writestr("extension/package.json", json.dumps(package))
|
||||
with self.assertRaisesRegex(ValueError, "identity/version"):
|
||||
assets.verify_complete(manifest, editor)
|
||||
|
||||
def test_signed_release_without_matching_skill_is_rejected(self):
|
||||
path = self.release / "manifest.json"
|
||||
manifest = json.loads(path.read_text())
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[2] / "sdk/python-runtime.py"
|
||||
SPECIFICATION = importlib.util.spec_from_file_location("python_runtime", SOURCE)
|
||||
runtime = importlib.util.module_from_spec(SPECIFICATION)
|
||||
SPECIFICATION.loader.exec_module(runtime)
|
||||
|
||||
|
||||
class PythonRuntimeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = Path(self.temporary.name)
|
||||
|
||||
def archive(self, names):
|
||||
path = self.root / "python.tar.gz"
|
||||
with tarfile.open(path, "w:gz") as archive:
|
||||
for name in names:
|
||||
member = tarfile.TarInfo(name)
|
||||
member.size = 4
|
||||
archive.addfile(member, io.BytesIO(b"test"))
|
||||
return path
|
||||
|
||||
def test_only_runtime_files_are_selected(self):
|
||||
for name in ["python/bin/python3.13", "python/lib/libpython3.13.so.1.0",
|
||||
"python/lib/python3.13/LICENSE.txt", "python/lib/python3.13/asyncio/tasks.py"]:
|
||||
self.assertTrue(runtime.selected(name, "3.13"), name)
|
||||
for name in ["python/bin/python3", "python/bin/pip3", "python/include/python3.13/Python.h",
|
||||
"python/lib/python3.13/site-packages/pip/__init__.py",
|
||||
"python/lib/python3.13/test/test_os.py", "python/lib/python3.13/__pycache__/os.pyc"]:
|
||||
self.assertFalse(runtime.selected(name, "3.13"), name)
|
||||
|
||||
def test_wheel_tags_include_compatible_abis_without_claiming_newer_glibc(self):
|
||||
tags = runtime.wheel_tags("3.13", "linux-x86_64", (2, 17))
|
||||
self.assertEqual(tags[0], "cp313-cp313-manylinux_2_17_x86_64")
|
||||
self.assertEqual(len(tags), len(set(tags)))
|
||||
for tag in ["cp313-cp313-manylinux2014_x86_64", "cp39-abi3-manylinux_2_17_x86_64",
|
||||
"cp38-abi3-manylinux1_x86_64", "cp313-none-manylinux_2_17_x86_64",
|
||||
"cp313-cp313-linux_x86_64", "py313-none-any", "py3-none-any", "py38-none-any"]:
|
||||
self.assertIn(tag, tags)
|
||||
for tag in ["cp314-abi3-manylinux_2_17_x86_64", "cp312-cp312-manylinux_2_17_x86_64",
|
||||
"cp313-cp313-manylinux_2_28_x86_64"]:
|
||||
self.assertNotIn(tag, tags)
|
||||
arm_tags = runtime.wheel_tags("3.13", "linux-aarch64", (2, 17))
|
||||
self.assertIn("cp39-abi3-manylinux2014_aarch64", arm_tags)
|
||||
self.assertIn("cp313-cp313-linux_aarch64", arm_tags)
|
||||
self.assertFalse(any("manylinux1_" in tag or "manylinux2010_" in tag for tag in arm_tags))
|
||||
with self.assertRaises(ValueError):
|
||||
runtime.wheel_tags("3.13", "linux-aarch64", (2, 16))
|
||||
|
||||
def test_extraction_rejects_traversal_and_duplicates(self):
|
||||
for names in [["python/../escape"], ["/python/bin/python3.13"],
|
||||
["python/bin/python3.13", "python/bin/python3.13"]]:
|
||||
with self.subTest(names=names), self.assertRaises(ValueError):
|
||||
runtime.extract(self.archive(names), self.root / "output", "3.13")
|
||||
|
||||
def test_wrong_hash_does_not_create_output(self):
|
||||
archive = self.archive([])
|
||||
output = self.root / "output"
|
||||
with self.assertRaisesRegex(ValueError, "SHA-256"):
|
||||
runtime.prepare(archive, "0" * 64, "3.13.5", "linux-x86_64", (2, 17), SOURCE, output)
|
||||
self.assertFalse(output.exists())
|
||||
|
||||
def test_failure_cleans_owned_output_and_preserves_existing_output(self):
|
||||
archive = self.archive([])
|
||||
output = self.root / "output"
|
||||
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
|
||||
with self.assertRaisesRegex(ValueError, "interpreter or license"):
|
||||
runtime.prepare(archive, digest, "3.13.5", "linux-x86_64", (2, 17), SOURCE, output)
|
||||
self.assertFalse(output.exists())
|
||||
output.mkdir()
|
||||
(output / "keep").write_text("existing")
|
||||
with self.assertRaises(FileExistsError):
|
||||
runtime.prepare(archive, digest, "3.13.5", "linux-x86_64", (2, 17), SOURCE, output)
|
||||
self.assertEqual((output / "keep").read_text(), "existing")
|
||||
|
||||
def test_newer_glibc_is_rejected_before_python_executes(self):
|
||||
binary = self.root / "python"
|
||||
binary.write_bytes(b"\x7fELF")
|
||||
text = "Machine: Advanced Micro Devices X86-64\nName: GLIBC_2.38\n"
|
||||
result = subprocess.CompletedProcess([], 0, text, "")
|
||||
with patch.object(runtime.subprocess, "run", return_value=result):
|
||||
with self.assertRaisesRegex(ValueError, "GLIBC_2.38"):
|
||||
runtime.check_elf(binary, SOURCE, "linux-x86_64", (2, 17))
|
||||
result.stdout = "Machine: AArch64\nName: GLIBC_2.17\n"
|
||||
with patch.object(runtime.subprocess, "run", return_value=result):
|
||||
with self.assertRaisesRegex(ValueError, "wrong target"):
|
||||
runtime.check_elf(binary, SOURCE, "linux-x86_64", (2, 17))
|
||||
result.stdout = "Machine: Advanced Micro Devices X86-64\nName: GLIBC_PRIVATE\n"
|
||||
with patch.object(runtime.subprocess, "run", return_value=result):
|
||||
with self.assertRaisesRegex(ValueError, "private glibc"):
|
||||
runtime.check_elf(binary, SOURCE, "linux-x86_64", (2, 17))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user