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
Generated
+6
-6
@@ -414,7 +414,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-backend-bridge"
|
||||
version = "0.1.6"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dever-core",
|
||||
@@ -427,7 +427,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-cli"
|
||||
version = "0.1.6"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"csv",
|
||||
@@ -455,7 +455,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-core"
|
||||
version = "0.1.6"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"dever-runtime",
|
||||
"pulldown-cmark",
|
||||
@@ -465,7 +465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-process"
|
||||
version = "0.1.6"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"goblin",
|
||||
"rustix",
|
||||
@@ -473,7 +473,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-runtime"
|
||||
version = "0.1.6"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64 0.22.1",
|
||||
@@ -506,7 +506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dever-tests"
|
||||
version = "0.1.6"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"dever-core",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ members = ["crates/dever-core", "crates/dever-runtime", "crates/dever-cli", "cra
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.6"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
publish = false
|
||||
|
||||
@@ -120,6 +120,7 @@ impl Environment {
|
||||
for (path, (bytes, executable)) in &self.files {
|
||||
stage.write(&format!("input/{path}"), bytes, *executable)?;
|
||||
}
|
||||
stage.link_tool_libraries(&self.files)?;
|
||||
for (path, bytes) in &self.assets {
|
||||
stage.write(
|
||||
&format!("process-runtime/{path}"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! One lifecycle for every managed compiler and package build hook.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
@@ -13,6 +14,10 @@ const MAX_OUTPUT: u64 = 16 * 1024 * 1024;
|
||||
static NEXT_STAGE: AtomicU64 = AtomicU64::new(0);
|
||||
mod budget;
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
#[path = "../../../../../test/dever-cli-tests/build_tool_libraries.rs"]
|
||||
mod tests;
|
||||
|
||||
pub(crate) struct Stage(pub(crate) PathBuf);
|
||||
|
||||
impl Stage {
|
||||
@@ -68,6 +73,55 @@ impl Stage {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub(crate) fn link_tool_libraries(
|
||||
&self,
|
||||
files: &BTreeMap<String, (Vec<u8>, bool)>,
|
||||
) -> Result<(), String> {
|
||||
for (path, (bytes, _)) in files {
|
||||
if !path.starts_with("rootfs/usr/lib/") || !bytes.starts_with(b"\x7fELF") {
|
||||
continue;
|
||||
}
|
||||
let library = goblin::elf::Elf::parse(bytes)
|
||||
.map_err(|error| format!("invalid build library: {error}"))?;
|
||||
let Some(name) = library.soname else {
|
||||
continue;
|
||||
};
|
||||
crate::workers::valid_path(name)?;
|
||||
if name.contains('/') {
|
||||
return Err("build library SONAME must be a filename".into());
|
||||
}
|
||||
if matches!(
|
||||
name,
|
||||
"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"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let directory = self.0.join("input/lib");
|
||||
fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
|
||||
let destination = directory.join(name);
|
||||
match fs::hard_link(self.0.join("input").join(path), &destination) {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
if fs::read(&destination).map_err(|error| error.to_string())? != *bytes {
|
||||
return Err("build libraries contain conflicting SONAMEs".into());
|
||||
}
|
||||
}
|
||||
Err(error) => return Err(format!("cannot link signed build library: {error}")),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn run(
|
||||
&self,
|
||||
command: Command,
|
||||
|
||||
@@ -400,6 +400,7 @@ impl Environment {
|
||||
for (path, (bytes, executable)) in &self.files {
|
||||
stage.write(&format!("input/{path}"), bytes, *executable)?;
|
||||
}
|
||||
stage.link_tool_libraries(&self.files)?;
|
||||
for (path, bytes) in &self.assets {
|
||||
stage.write(
|
||||
&format!("process-runtime/{path}"),
|
||||
|
||||
@@ -90,7 +90,7 @@ pub fn install(installer_root: &Path) -> Result<Version, String> {
|
||||
// The private copy is checked again by the version owner before any health
|
||||
// command executes. A writable download cannot race signed bytes into it.
|
||||
let manifest = MachineManager::verify_package(&package, None, &key)?;
|
||||
validate_payload(&package, &manifest.artifacts, true, false)?;
|
||||
validate_payload(&package, &manifest.artifacts, true)?;
|
||||
pin_key(&layout, &key)?;
|
||||
let manager = MachineManager::new(layout.clone());
|
||||
if manager
|
||||
@@ -134,7 +134,6 @@ pub(crate) fn validate_payload(
|
||||
root: &Path,
|
||||
artifacts: &[Artifact],
|
||||
current_template: bool,
|
||||
previous_layout: bool,
|
||||
) -> Result<(), String> {
|
||||
let scoped = artifacts
|
||||
.iter()
|
||||
@@ -149,20 +148,10 @@ pub(crate) fn validate_payload(
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
validate_files(
|
||||
&root.join("bootstrap"),
|
||||
&scoped,
|
||||
current_template,
|
||||
previous_layout,
|
||||
)
|
||||
validate_files(&root.join("bootstrap"), &scoped, current_template)
|
||||
}
|
||||
|
||||
fn validate_files(
|
||||
root: &Path,
|
||||
artifacts: &[Artifact],
|
||||
new_payload: bool,
|
||||
previous_layout: bool,
|
||||
) -> Result<(), String> {
|
||||
fn validate_files(root: &Path, artifacts: &[Artifact], new_payload: bool) -> Result<(), String> {
|
||||
let bytes = artifacts
|
||||
.iter()
|
||||
.try_fold(0u64, |total, artifact| total.checked_add(artifact.bytes))
|
||||
@@ -174,8 +163,7 @@ fn validate_files(
|
||||
let supported = matches!(
|
||||
artifact.path.as_str(),
|
||||
"dever" | "deverd" | "deverd.service"
|
||||
) || artifact.path.starts_with("lib/")
|
||||
|| previous_layout && !new_payload && artifact.path == "dever-sandbox";
|
||||
) || artifact.path.starts_with("lib/");
|
||||
if !supported {
|
||||
return Err("bootstrap has an unsupported artifact".into());
|
||||
}
|
||||
@@ -240,7 +228,7 @@ fn validate_bin(root: &Path, key: &[u8]) -> Result<Vec<u8>, String> {
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
validate_files(root, &artifacts, false, manifest.uses_sandbox_layout())?;
|
||||
validate_files(root, &artifacts, false)?;
|
||||
let expected = artifacts
|
||||
.iter()
|
||||
.map(|artifact| artifact.path.as_str())
|
||||
|
||||
@@ -55,7 +55,7 @@ impl ExtensionId {
|
||||
format!("ext-{}.tar.zst", self.name())
|
||||
}
|
||||
|
||||
fn permits(&self, path: &str, process_root: &str) -> bool {
|
||||
fn permits(&self, path: &str) -> bool {
|
||||
let target = self.target.platform();
|
||||
match &self.kind {
|
||||
ExtensionKind::Runtime(ecosystem) => {
|
||||
@@ -66,7 +66,7 @@ impl ExtensionId {
|
||||
}
|
||||
ExtensionKind::Target => {
|
||||
path.starts_with(&format!("runtime/{target}/"))
|
||||
|| path.starts_with(&format!("{process_root}/{target}/"))
|
||||
|| path.starts_with(&format!("process-runtime/{target}/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,11 +113,6 @@ pub fn validate_catalog(manifest: &ReleaseManifest) -> Result<(), String> {
|
||||
}
|
||||
let mut paths = BTreeSet::new();
|
||||
validate_artifacts(&manifest.artifacts, &mut paths)?;
|
||||
let process_root = if manifest.uses_sandbox_layout() {
|
||||
"sandbox"
|
||||
} else {
|
||||
"process-runtime"
|
||||
};
|
||||
let mut ids = BTreeSet::new();
|
||||
for extension in &manifest.extensions {
|
||||
let id = extension.id();
|
||||
@@ -131,7 +126,7 @@ pub fn validate_catalog(manifest: &ReleaseManifest) -> Result<(), String> {
|
||||
if extension
|
||||
.artifacts
|
||||
.iter()
|
||||
.any(|artifact| !id.permits(&artifact.path, process_root))
|
||||
.any(|artifact| !id.permits(&artifact.path))
|
||||
{
|
||||
return Err(format!(
|
||||
"extension '{}' contains a file outside its resource namespace",
|
||||
|
||||
@@ -49,7 +49,7 @@ pub(super) fn assemble(
|
||||
bytes: bytes.len() as u64,
|
||||
sha256: format!("{:x}", Sha256::digest(bytes)),
|
||||
});
|
||||
crate::toolchain::bootstrap::validate_payload(writer.staging, &artifacts, true, false)?;
|
||||
crate::toolchain::bootstrap::validate_payload(writer.staging, &artifacts, true)?;
|
||||
}
|
||||
Ok(artifacts)
|
||||
}
|
||||
@@ -78,10 +78,6 @@ pub struct ReleaseManifest {
|
||||
}
|
||||
|
||||
impl ReleaseManifest {
|
||||
pub(super) fn uses_sandbox_layout(&self) -> bool {
|
||||
version_components(self.version.as_str()) < [0, 1, 5]
|
||||
}
|
||||
|
||||
pub fn new(version: Version, artifacts: Vec<Artifact>) -> Self {
|
||||
Self {
|
||||
format: "dever-release-v2".into(),
|
||||
@@ -707,12 +703,7 @@ impl MachineManager {
|
||||
{
|
||||
// Older installed versions retain their own signed service template.
|
||||
// Only the explicit bootstrap installer applies the current template.
|
||||
super::bootstrap::validate_payload(
|
||||
root,
|
||||
&manifest.artifacts,
|
||||
false,
|
||||
manifest.uses_sandbox_layout(),
|
||||
)?;
|
||||
super::bootstrap::validate_payload(root, &manifest.artifacts, false)?;
|
||||
}
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dever-language",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dever-language",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"devDependencies": {
|
||||
"@vscode/vsce": "4.0.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "dever-language",
|
||||
"displayName": "Dever Language",
|
||||
"description": "Dever syntax highlighting and editing support, including Markdown code blocks.",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"publisher": "shemic",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
|
||||
@@ -63,6 +63,26 @@ Worker 和依赖构建都直接在当前容器中运行,由内核启动真实
|
||||
|
||||
GPU 使用父容器已经分配的设备和驱动,不再由 Dever 重新挂载。除已打包的私有库路径外,仅透传 `CUDA_VISIBLE_DEVICES`、`NVIDIA_VISIBLE_DEVICES`、`NVIDIA_DRIVER_CAPABILITIES`、`CUDA_DEVICE_ORDER` 与 `LD_LIBRARY_PATH`;其他宿主环境不自动传入。CUDA/PyTorch 等依赖仍需匹配目标平台。是否有 GPU、是否可加载驱动以及显存是否足够由真实部署决定,不会绕过 Kubernetes 调度或容器权限。设备验收须单独取得 GPU 准入;本次源码改造不等于已部署或模型推理通过。
|
||||
|
||||
## Python 发行资源
|
||||
|
||||
应用容器的系统库基线与编译器开发机不同。不要直接复制开发容器的 Python:解释器和每个动态扩展都必须满足目标 glibc 基线;Python 版本相同不代表系统 ABI 相同。保留真实解释器入口,不改用私有 GNU loader,也不回退到应用容器的系统 Python。
|
||||
|
||||
`sdk/python-runtime.py` 从显式提供并锁定 SHA-256 的 Python Build Standalone `install_only_stripped` 归档生成输入,不联网下载、不安装服务。参数为 `--archive`、`--sha256`、`--version`、`--target`、`--readelf`、`--output`,默认 `--glibc-max 2.17`。输出目录必须不存在;工具检查路径、目标 ELF、所有 GLIBC 版本依赖、基础模块及迁移后的解释器身份,保留运行文件和 Python 许可证,不带 pip、第三方 site-packages 或测试缓存。
|
||||
|
||||
输出的 `python-runtime.json` 对应既有作者配置 `runtimes.pip`,其 `source` 相对输出根;合入同一作者根后仍由 `native-release` 校验、打包和签名,Python 继续是独立扩展,不塞进基础包。wheel 标签包含同一 CPython 的原生 ABI、兼容的 abi3 和纯 Python 标签;manylinux 不超过目标 glibc 基线,通用 `linux_<架构>` 用于本机源码构建产物,不承诺跨系统 ABI 兼容。源码归档的来源、SHA-256 和实际目标环境验收结果归本次发行记录;本地候选目录不能充当正式源或修改现行安装。
|
||||
|
||||
## 完整公开发行
|
||||
|
||||
正式附件使用 `sdk/release-assets.py --editor-vsix <VSIX>` 导出,默认执行完整性检查。必须包含独立基础包、x86_64/ARM64 的 Python/Node/Go 运行扩展、宿主 Python/npm 构建扩展、ARM64 原生目标扩展、同版本编辑器扩展,以及签名清单和引导文件。缺任一项就失败;部分资源只允许通过显式的 `--partial-test-fixture` 用于定向测试,不能当作完整公开版本。Go 扩展由 `test/go-managed/make_pack.go` 导出完整 `std`,不能只导出样例依赖。
|
||||
|
||||
`.tar.zst` 是 tar 文件集合经过 Zstandard 压缩,减少附件体积;它不是另一个运行环境。安装器使用已验签的引导程序解压,不要求使用者额外安装 zstd。Python、Node、Go 及构建工具继续按需单独下载,不并入基础包。
|
||||
|
||||
当前发行序列经项目所有者确认从 `0.0.1` 重新开始,旧 Release 与附件在替代包验收后清理,不删除源代码提交历史。资源格式固定为现行 `process-runtime`,不再根据版本号推断旧沙箱布局。旧版 `0.1.x` 安装不能通过普通升级降到 `0.0.1`;须在新的安装根验收后明确切换,保留项目和凭据。不要关闭签名、同版本身份检查或防降级检查来绕过此规则。后续版本变更由项目所有者确认,不因每次修改自动递增版本号。
|
||||
|
||||
定向测试:`python3 -B -m unittest discover -s test/onboarding -p test_python_runtime.py -v`。这些测试验证打包约束,不代替在目标容器中执行签名应用、真实模型和取消清理验收;x86_64 验收也不证明 ARM64 已验收。
|
||||
|
||||
跨架构作者验证可显式传入 `--runner /absolute/qemu-aarch64 --sysroot /absolute/arm64-sysroot`,仍执行相同解释器身份和基础模块探测,不跳过 ELF 检查;须记录为 QEMU 验收,而不是原生 ARM64 硬件验收。这两个参数不进入应用运行配置。
|
||||
|
||||
## 依赖与跨平台构建
|
||||
|
||||
`dever lib install <project>` 按现有锁恢复依赖,不重新选版本、不修改配置或锁。`dever-lock-v6` 记录 Python extras/wheel METADATA、npm 嵌套依赖及 peers/optional/bundled 依赖、Go 模块校验证据。原始包保留来源与文件名,构建记录保留完整输入,包括 npm 剪除可选构建依赖前的依赖图。
|
||||
@@ -77,6 +97,8 @@ Go pack 的 `build.host` 描述编译器、链接器与分析器的运行平台
|
||||
|
||||
只有显式准备 Lib 时才构建 Python sdist 或运行 npm 安装钩子。构建使用签名工具包,在独立临时工作目录内直接运行受管子进程;不再提供内层文件/网络隔离,因此只运行可信依赖及构建钩子。缺少工具时不回退宿主 PATH。
|
||||
|
||||
直接执行 GCC/binutils 时,工具依赖的非 glibc 动态库从已验签的 SDK 在本次临时目录内建立硬链接,供既有受管库路径加载;不依赖宿主另装 libbfd 等作者工具库,不复制额外库数据,也不替换宿主 glibc/loader。Python 原生扩展和 npm 原生 addon 必须分别通过真实源码构建验收,纯 Python/JS 回退不算通过。
|
||||
|
||||
Python PEP 517 的构建依赖单独锁定,每个 hook 使用新进程,最终 wheel 按保留的元数据核对。npm 的配套运行器处理 `preinstall/install/postinstall`、依赖 bin 和 node-gyp。
|
||||
|
||||
构建记录绑定源码、目标运行环境、工具、构建前端、依赖图和输出摘要。Worker 准备阶段离线恢复结果,不重跑安装钩子。npm `.node` 产物须通过目标检查、动态加载检查和独立 Node 加载探测,才能进入 Worker 资源目录。不匹配平台或 ABI 的其他变体保留在锁定原始包中,不加入运行目录。运行时非系统依赖来自 Worker 目录,系统 ABI 由目标容器提供,不能借用编译器 sysroot。
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
|
||||
MAX_BYTES = 256 * 1024 * 1024
|
||||
TARGETS = {
|
||||
"linux-x86_64": ("x86_64", "Advanced Micro Devices X86-64"),
|
||||
"linux-aarch64": ("aarch64", "AArch64"),
|
||||
}
|
||||
OMIT = {"__pycache__", "site-packages", "test", "tests", "tkinter", "turtledemo", "idlelib", "ensurepip"}
|
||||
PROBE = """import ctypes, hashlib, importlib.machinery, json, platform, sqlite3, ssl, sys, zlib
|
||||
print(json.dumps({"version": platform.python_version(), "machine": platform.machine(),
|
||||
"executable": sys.executable, "prefix": sys.prefix,
|
||||
"suffixes": importlib.machinery.EXTENSION_SUFFIXES}))
|
||||
"""
|
||||
|
||||
|
||||
def checksum(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def selected(name, minor):
|
||||
path = PurePosixPath(name)
|
||||
if path == PurePosixPath(f"python/bin/python{minor}"):
|
||||
return True
|
||||
if len(path.parts) == 3 and path.parts[:2] == ("python", "lib"):
|
||||
return bool(re.fullmatch(r"libpython[\d.]+\.so(?:\.\d+)*", path.name))
|
||||
prefix = ("python", "lib", f"python{minor}")
|
||||
return (path.parts[:3] == prefix and not OMIT.intersection(path.parts[3:])
|
||||
and not any(part.startswith("config-") for part in path.parts[3:])
|
||||
and not path.name.startswith("_test") and path.suffix not in (".pyc", ".pyo", ".a"))
|
||||
|
||||
|
||||
def extract(archive, output, minor):
|
||||
total = 0
|
||||
seen = set()
|
||||
with tarfile.open(archive, "r:gz") as source:
|
||||
for member in source:
|
||||
name = member.name.rstrip("/")
|
||||
parts = name.split("/")
|
||||
if (parts[0] != "python" or any(part in ("", ".", "..") for part in parts)
|
||||
or "\\" in name or name in seen or len(seen) >= 65536):
|
||||
raise ValueError("invalid, repeated or excessive archive paths")
|
||||
seen.add(name)
|
||||
total += member.size
|
||||
if total > MAX_BYTES:
|
||||
raise ValueError("Python source exceeds the runtime byte budget")
|
||||
if not member.isfile() or not selected(name, minor):
|
||||
continue
|
||||
destination = output / name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with source.extractfile(member) as incoming, destination.open("xb") as outgoing:
|
||||
shutil.copyfileobj(incoming, outgoing)
|
||||
destination.chmod(0o755 if name == f"python/bin/python{minor}" else 0o644)
|
||||
|
||||
|
||||
def check_elf(path, readelf, target, baseline):
|
||||
with path.open("rb") as stream:
|
||||
if stream.read(4) != b"\x7fELF":
|
||||
return
|
||||
result = subprocess.run([str(readelf), "--wide", "--file-header", "--version-info", str(path)],
|
||||
check=True, capture_output=True, text=True, timeout=15)
|
||||
if len(result.stdout) > 256 * 1024 or TARGETS[target][1] not in result.stdout:
|
||||
raise ValueError("runtime ELF has the wrong target or excessive metadata")
|
||||
if "Name: GLIBC_PRIVATE" in result.stdout:
|
||||
raise ValueError("runtime ELF depends on private glibc interfaces")
|
||||
versions = [tuple(map(int, value.split(".")))
|
||||
for value in re.findall(r"Name: GLIBC_(\d+(?:\.\d+)+)", result.stdout)]
|
||||
if versions and max(versions) > baseline:
|
||||
raise ValueError(f"{path.name} requires GLIBC_{'.'.join(map(str, max(versions)))}")
|
||||
|
||||
|
||||
def wheel_tags(minor, target, baseline):
|
||||
architecture = TARGETS[target][0]
|
||||
oldest = 5 if architecture == "x86_64" else 17
|
||||
if baseline[0] != 2 or baseline[1] < oldest:
|
||||
raise ValueError("unsupported target glibc baseline")
|
||||
aliases = {17: "manylinux2014", 12: "manylinux2010", 5: "manylinux1"}
|
||||
platforms = []
|
||||
for glibc_minor in range(baseline[1], oldest - 1, -1):
|
||||
platforms.append(f"manylinux_2_{glibc_minor}_{architecture}")
|
||||
if glibc_minor in aliases:
|
||||
platforms.append(f"{aliases[glibc_minor]}_{architecture}")
|
||||
platforms.append(f"linux_{architecture}")
|
||||
python_minor = int(minor.split(".")[1])
|
||||
abi = "cp" + minor.replace(".", "")
|
||||
tags = [f"{abi}-{abi}-{platform}" for platform in platforms]
|
||||
tags += [f"{abi}-abi3-{platform}" for platform in platforms]
|
||||
tags += [f"{abi}-none-{platform}" for platform in platforms]
|
||||
tags += [f"cp3{version}-abi3-{platform}"
|
||||
for version in range(python_minor - 1, 1, -1) for platform in platforms]
|
||||
interpreters = [f"py3{python_minor}", "py3"]
|
||||
interpreters += [f"py3{version}" for version in range(python_minor - 1, -1, -1)]
|
||||
tags += [f"{interpreter}-none-{platform}" for interpreter in interpreters for platform in platforms]
|
||||
tags += [f"{abi}-none-any"]
|
||||
tags += [f"{interpreter}-none-any" for interpreter in interpreters]
|
||||
return tags
|
||||
|
||||
|
||||
def prepare(archive, expected, version, target, baseline, readelf, output, runner=None, sysroot=None):
|
||||
if (not re.fullmatch(r"[0-9a-f]{64}", expected) or archive.stat().st_size > MAX_BYTES
|
||||
or checksum(archive) != expected):
|
||||
raise ValueError("Python archive SHA-256 or size mismatch")
|
||||
if not re.fullmatch(r"3\.\d+\.\d+", version) or target not in TARGETS:
|
||||
raise ValueError("invalid Python version or target")
|
||||
if not readelf.is_absolute() or not readelf.is_file():
|
||||
raise ValueError("readelf must be an explicit absolute executable")
|
||||
if ((runner is None) != (sysroot is None)
|
||||
or runner is not None and (not runner.is_absolute() or not runner.is_file()
|
||||
or not sysroot.is_absolute() or not sysroot.is_dir())):
|
||||
raise ValueError("cross probing requires an explicit QEMU runner and target sysroot")
|
||||
minor = ".".join(version.split(".")[:2])
|
||||
output.mkdir(mode=0o700)
|
||||
try:
|
||||
extract(archive, output, minor)
|
||||
root = output / "python"
|
||||
binary = root / "bin" / f"python{minor}"
|
||||
if not binary.is_file() or not (root / "lib" / f"python{minor}" / "LICENSE.txt").is_file():
|
||||
raise ValueError("Python archive lacks its interpreter or license")
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file():
|
||||
check_elf(path, readelf, target, baseline)
|
||||
environment = {"HOME": str(root), "TMPDIR": str(output), "PATH": "/usr/bin:/bin"}
|
||||
prefix = [str(runner), "-L", str(sysroot)] if runner else []
|
||||
result = subprocess.run(prefix + [str(binary), "-I", "-S", "-B", "-c", PROBE],
|
||||
check=True, capture_output=True, text=True, env=environment, timeout=30)
|
||||
probe = json.loads(result.stdout)
|
||||
if (probe["version"] != version or probe["machine"] != TARGETS[target][0]
|
||||
or Path(probe["prefix"]) != root or Path(probe["executable"]) != binary):
|
||||
raise ValueError("Python runtime is not relocatable or has the wrong identity")
|
||||
tags = wheel_tags(minor, target, baseline)
|
||||
markers = {"implementation_name": "cpython", "implementation_version": version,
|
||||
"os_name": "posix", "platform_machine": TARGETS[target][0],
|
||||
"platform_python_implementation": "CPython", "platform_release": "",
|
||||
"platform_system": "Linux", "platform_version": "", "python_full_version": version,
|
||||
"python_version": minor, "sys_platform": "linux"}
|
||||
descriptor = {"format": "dever-worker-runtime-v1", "ecosystem": "pip", "target": target,
|
||||
"executable": f"bin/python{minor}", "arguments": ["-I", "-S", "-B"],
|
||||
"python_markers": markers, "python_wheel_tags": tags,
|
||||
"python_extension_suffixes": probe["suffixes"]}
|
||||
(root / "dever-runtime.json").write_text(json.dumps(descriptor, indent=2) + "\n")
|
||||
files = [{"source": str(path.relative_to(output)), "path": str(path.relative_to(root)),
|
||||
"sha256": checksum(path)} for path in sorted(root.rglob("*")) if path.is_file()]
|
||||
author = {"name": "cpython", "version": version, "python_markers": markers,
|
||||
"python_wheel_tags": tags, "files": files}
|
||||
(output / "python-runtime.json").write_text(json.dumps(author, indent=2) + "\n")
|
||||
return {"version": version, "target": target, "glibc_max": ".".join(map(str, baseline)),
|
||||
"source_sha256": expected, "files": len(files)}
|
||||
except BaseException:
|
||||
shutil.rmtree(output)
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Prepare verified portable Python inputs for native-release")
|
||||
parser.add_argument("--archive", type=Path, required=True)
|
||||
parser.add_argument("--sha256", required=True)
|
||||
parser.add_argument("--version", required=True)
|
||||
parser.add_argument("--target", choices=TARGETS, required=True)
|
||||
parser.add_argument("--glibc-max", default="2.17")
|
||||
parser.add_argument("--readelf", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--runner", type=Path, help="explicit QEMU user-mode executable for cross probing")
|
||||
parser.add_argument("--sysroot", type=Path, help="target system libraries for the explicit QEMU runner")
|
||||
arguments = parser.parse_args()
|
||||
if not re.fullmatch(r"2\.\d+", arguments.glibc_max):
|
||||
parser.error("glibc-max must be a 2.x baseline")
|
||||
print(json.dumps(prepare(arguments.archive.resolve(), arguments.sha256, arguments.version,
|
||||
arguments.target, tuple(map(int, arguments.glibc_max.split("."))),
|
||||
arguments.readelf, arguments.output.absolute(), arguments.runner,
|
||||
arguments.sysroot), indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+35
-3
@@ -12,11 +12,35 @@ import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
|
||||
MAX_PACKAGE = 2 * 1024**3
|
||||
|
||||
|
||||
def verify_complete(manifest, editor):
|
||||
groups = packages(manifest)
|
||||
expected = {f"ext-runtime-{ecosystem}-{target}"
|
||||
for ecosystem in ("pip", "npm", "go")
|
||||
for target in ("linux-x86_64", "linux-aarch64")}
|
||||
expected.update(f"ext-build-{ecosystem}-{manifest['platform']}" for ecosystem in ("pip", "npm"))
|
||||
expected.update(f"ext-target-{target}" for target in ("linux-x86_64", "linux-aarch64")
|
||||
if target != manifest["platform"])
|
||||
missing = expected - groups.keys()
|
||||
if missing:
|
||||
raise ValueError("complete release is missing: " + ", ".join(sorted(missing)))
|
||||
if editor is None or editor.is_symlink() or editor.stat().st_size > 16 * 1024**2:
|
||||
raise ValueError("complete release requires a bounded editor VSIX")
|
||||
with zipfile.ZipFile(editor) as archive:
|
||||
info = archive.getinfo("extension/package.json")
|
||||
if info.file_size > 64 * 1024:
|
||||
raise ValueError("editor metadata exceeds limit")
|
||||
package = json.loads(archive.read(info))
|
||||
if (package.get("name") != "dever-language" or package.get("publisher") != "shemic"
|
||||
or package.get("version") != manifest["version"]):
|
||||
raise ValueError("editor identity/version does not match the release")
|
||||
|
||||
|
||||
def extension_suffix(extension):
|
||||
kind = extension["kind"]
|
||||
target = extension["target"]
|
||||
@@ -126,7 +150,7 @@ def compress(release, artifacts, destination, zstd):
|
||||
process.stderr.close()
|
||||
|
||||
|
||||
def create(release, output, zstd):
|
||||
def create(release, output, zstd, complete=True, editor=None):
|
||||
release = release.absolute()
|
||||
output = output.absolute()
|
||||
if release.is_symlink() or not release.is_dir():
|
||||
@@ -142,6 +166,8 @@ def create(release, output, zstd):
|
||||
if len(manifest_bytes) > 2 * 1024**2:
|
||||
raise ValueError("release metadata exceeds limit")
|
||||
manifest = json.loads(manifest_bytes)
|
||||
if complete:
|
||||
verify_complete(manifest, editor)
|
||||
groups = packages(manifest)
|
||||
verify_artifacts(release, groups)
|
||||
with tempfile.TemporaryDirectory(prefix=".dever-release-assets-", dir=output.parent) as scratch:
|
||||
@@ -158,6 +184,8 @@ def create(release, output, zstd):
|
||||
blob = prepared / f"{stem}.blob-{artifact['sha256']}"
|
||||
if not blob.exists():
|
||||
shutil.copyfile(release / artifact["path"], blob)
|
||||
if complete:
|
||||
shutil.copyfile(editor, prepared / f"dever-language-{manifest['version']}.vsix")
|
||||
publish(prepared, output)
|
||||
return sorted(path.name for path in output.iterdir())
|
||||
|
||||
@@ -179,11 +207,15 @@ def main():
|
||||
parser.add_argument("release", type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--zstd", required=True, type=Path, help="explicit author-side zstd executable")
|
||||
parser.add_argument("--partial-test-fixture", action="store_true", help="allow incomplete private test fixtures, not public releases")
|
||||
parser.add_argument("--editor-vsix", type=Path, help="matching editor package required for public releases")
|
||||
args = parser.parse_args()
|
||||
if args.partial_test_fixture and args.editor_vsix:
|
||||
parser.error("partial test fixtures must not include a public editor package")
|
||||
try:
|
||||
for name in create(args.release, args.output, args.zstd):
|
||||
for name in create(args.release, args.output, args.zstd, not args.partial_test_fixture, args.editor_vsix):
|
||||
print(name)
|
||||
except (OSError, ValueError, KeyError, TypeError, tarfile.TarError) as error:
|
||||
except (OSError, ValueError, KeyError, TypeError, tarfile.TarError, zipfile.BadZipFile) as error:
|
||||
parser.exit(1, f"cannot prepare release assets: {error}\n")
|
||||
|
||||
|
||||
|
||||
+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