Files
core/build/development.py
2026-11-03 03:32:07 +00:00

281 lines
13 KiB
Python

#!/usr/bin/env python3
"""在 Debian 13 开发容器内组装 Linux x86_64 完整离线开发包。"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tomllib
SOURCE = Path(__file__).resolve().parents[1]
TRIPLE = "x86_64-unknown-linux-gnu"
PACKAGES = (
"gcc-14-x86-64-linux-gnu", "cpp-14-x86-64-linux-gnu", "libgcc-14-dev",
"libc6-dev", "linux-libc-dev", "binutils-x86-64-linux-gnu", "libc6", "libgcc-s1",
)
def native_input(path):
"""只保留原生 C 构建所需工具;Rust LTO 使用包内 LLVM,不需要 GCC LTO。"""
name = str(path)
if name.startswith("/usr/include/"):
return True
if path.parent == Path("/usr/bin"):
return path.name in {
"x86_64-linux-gnu-gcc-14", "x86_64-linux-gnu-as",
"x86_64-linux-gnu-ld", "x86_64-linux-gnu-ar", "x86_64-linux-gnu-ranlib",
}
if name.startswith("/usr/libexec/gcc/"):
return path.name in {"cc1", "collect2", "liblto_plugin.so", "lto-wrapper"}
if name.startswith("/usr/lib/gcc/"):
return "/include/" in name or path.name in {
"libgcc.a", "libgcc_eh.a", "libgcc_s.so", "crtbegin.o", "crtbeginS.o",
"crtbeginT.o", "crtend.o", "crtendS.o",
"liblto_plugin.so",
}
return path.parent in {Path("/usr/lib/x86_64-linux-gnu"), Path("/usr/lib64")}
def output(*arguments, **options):
return subprocess.check_output(arguments, text=True, **options).strip()
def digest(path):
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def copy(source, destination):
"""只解引用作者明确选定的工具文件,不复制 Cargo home 或用户配置。"""
if source.is_dir():
shutil.copytree(source, destination, dirs_exist_ok=True)
else:
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, destination)
destination.chmod(0o755 if os.access(source, os.X_OK) else 0o644)
def script(path, body):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('#!/bin/sh\nset -eu\ntools=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\n' + body)
path.chmod(0o755)
def prepare(compiler, work):
"""两种交付方式共用现有本地打包入口,不另做编译器发行格式。"""
work.mkdir()
version = tomllib.loads((SOURCE / "rust/Cargo.toml").read_text())["workspace"]["package"]["version"]
rust_version = tomllib.loads((SOURCE / "rust/rust-toolchain.toml").read_text())["toolchain"]["channel"]
author = work / "author"
for path in (
"rust/Cargo.toml", "rust/Cargo.lock", "rust/rust-toolchain.toml", "rust/src",
"lib", "skills", "LANGUAGE.md", "DOCUMENTATION.md",
):
copy(SOURCE / path, author / "source" / path)
copy(compiler, author / "bin/dever")
(author / "config").mkdir()
(author / "config/setting.json").write_text(json.dumps({
"format": "dever-local-bootstrap-input-v1", "version": version,
"target": "linux-x86_64", "source": "source",
"core": {"source": "bin/dever", "sha256": digest(compiler)},
}))
bundle = work / "dever"
subprocess.run([str(compiler), "release", str(author), "--output", str(bundle)], check=True)
return bundle, version, rust_version
def record(bundle, paths):
"""把交付附带文件纳入原有清单校验。"""
manifest = json.loads((bundle / "bootstrap.json").read_text())
for path in sorted(paths):
if path.is_symlink():
raise RuntimeError(f"staged artifact must be a regular file: {path}")
if path.is_file():
manifest["artifacts"].append({
"path": path.relative_to(bundle).as_posix(), "size": path.stat().st_size,
"sha256": digest(path), "executable": os.access(path, os.X_OK),
})
manifest["artifacts"].sort(key=lambda artifact: artifact["path"])
(bundle / "bootstrap.json").write_text(json.dumps(manifest, indent=2) + "\n")
return manifest
def native_tools(tools):
"""保留 Debian 工具的相对安装布局;glibc 只进链接 sysroot,不覆盖宿主加载器。"""
base = {str(Path(path).resolve()) for path in output("dpkg-query", "-L", "libc6").splitlines()}
packages = set(PACKAGES)
executables = []
for package in PACKAGES:
for name in output("dpkg-query", "-L", package).splitlines():
path = Path(name)
if not path.is_file() or not native_input(path):
continue
copy(path, tools / "native" / path.relative_to("/"))
# Debian 的链接脚本使用 /lib;发行包展开 usrmerge 别名,不改写官方库文件。
if package == "libc6":
copy(path, tools / "native" / path.relative_to("/usr"))
if os.access(path, os.X_OK):
with path.open("rb") as stream:
if stream.read(4) == b"\x7fELF":
executables.append(path)
for name in ("as", "ld", "ar"):
copy(Path(f"/usr/bin/x86_64-linux-gnu-{name}"), tools / "native/usr/bin" / name)
libraries = set()
for executable in executables:
listing = subprocess.run(["ldd", str(executable)], text=True, capture_output=True)
if listing.returncode and "not a dynamic executable" not in listing.stderr + listing.stdout:
raise RuntimeError(f"ldd {executable}: {listing.stderr}{listing.stdout}")
for line in listing.stdout.splitlines():
if "=> not found" in line:
raise RuntimeError(line)
fields = line.split()
if "=>" in fields and fields[2].startswith("/"):
library = Path(fields[2])
if str(library.resolve()) not in base:
libraries.add(library)
for library in sorted(libraries):
copy(library, tools / "lib" / library.name)
owner = output("dpkg-query", "-S", str(library.resolve())).splitlines()[0].split(": ", 1)[0]
packages.add(owner)
copy(Path("/usr/lib/x86_64-linux-gnu/libgcc_s.so.1"), tools / "lib/libgcc_s.so.1")
packages.add("libgcc-s1")
for package in sorted(packages):
name = package.split(":", 1)[0]
copy(Path("/usr/share/doc") / name / "copyright", tools / "licenses" / name / "copyright")
copy(Path("/usr/share/common-licenses"), tools / "licenses/common")
(tools / "native-packages.txt").write_text(output("dpkg-query", "-W", *sorted(packages)) + "\n")
script(tools / "bin/cc", '''export LD_LIBRARY_PATH="$tools/lib"
exec "$tools/native/usr/bin/x86_64-linux-gnu-gcc-14" \\
--sysroot="$tools/native" \\
-B"$tools/native/usr/lib/gcc/x86_64-linux-gnu/14/" \\
-B"$tools/native/usr/libexec/gcc/x86_64-linux-gnu/14/" \\
-B"$tools/native/usr/bin/" \\
-B"$tools/native/usr/lib/x86_64-linux-gnu/" "$@"
''')
def trim_vendor(vendor, version):
"""开发包编译业务程序;按 Cargo 完整依赖图保留运行库的全部特性和平台依赖。"""
metadata = json.loads(output("cargo", f"+{version}", "metadata", "--locked", "--offline",
"--all-features", "--format-version", "1", cwd=SOURCE / "rust"))
dependencies = {node["id"]: node["dependencies"] for node in metadata["resolve"]["nodes"]}
pending = [package["id"] for package in metadata["packages"] if package["name"] == "dever-runtime"]
required = set()
while pending:
package = pending.pop()
if package not in required:
required.add(package)
pending.extend(dependencies[package])
for package in metadata["packages"]:
if package["source"] and package["id"] not in required:
directory = vendor / f'{package["name"]}-{package["version"]}'
# 编译器二进制仍使用这些依赖;源码可移除,第三方许可必须随包保留。
licenses = {path for path in directory.rglob("*") if path.is_file()
and path.name.upper().startswith(("LICENSE", "COPYING", "COPYRIGHT", "NOTICE", "AUTHORS"))}
if package["license_file"]:
licenses.add(directory / package["license_file"])
for path in licenses:
copy(path, vendor.parent / "licenses/cargo" / path.relative_to(vendor))
shutil.rmtree(directory)
def rust_tools(tools, version, vendor):
rust = Path(output("rustc", f"+{version}", "--print", "sysroot"))
if f"host: {TRIPLE}" not in output("rustc", f"+{version}", "-vV"):
raise RuntimeError(f"this package requires the official {TRIPLE} toolchain")
for name in ("cargo", "rustc"):
copy(rust / "bin" / name, tools / "rust/bin" / name)
for library in (rust / "lib").glob("*.so*"):
if not library.is_symlink():
copy(library, tools / "rust/lib" / library.name)
for name in ("lib", "bin"):
copy(rust / "lib/rustlib" / TRIPLE / name, tools / "rust/lib/rustlib" / TRIPLE / name)
# 只裁剪已链接的工具;标准库 rlib/rmeta 中的符号、类型元数据与位码仍用于编译。
binaries = [*(tools / "rust/bin").iterdir(), *(tools / "rust/lib").glob("*.so*")]
binaries.extend(path for path in (tools / "rust/lib/rustlib" / TRIPLE / "bin").rglob("*")
if path.is_file())
for binary in binaries:
# 官方 .so 目录也含链接脚本,按原样保留,只处理 ELF 文件。
with binary.open("rb") as stream:
if stream.read(4) == b"\x7fELF":
subprocess.run(["strip", "--strip-unneeded", str(binary)], check=True)
copy(rust / "share/doc/rust", tools / "licenses/rust")
if vendor:
copy(vendor, tools / "vendor")
else:
subprocess.run([
"cargo", f"+{version}", "vendor", "--locked", "--offline", "--versioned-dirs",
str(tools / "vendor"),
], cwd=SOURCE / "rust", check=True)
trim_vendor(tools / "vendor", version)
config = tools / ".cargo/config.toml"
config.parent.mkdir()
config.write_text('''[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "vendor"
''')
script(tools / "bin/cargo", '''export CARGO_HOME="$tools/.cargo"
export RUSTC="$tools/rust/bin/rustc"
export CC="$tools/bin/cc"
export AR="$tools/native/usr/bin/x86_64-linux-gnu-ar"
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="$CC"
export LD_LIBRARY_PATH="$tools/lib"
export PATH="$tools/native/usr/bin:/usr/bin:/bin"
exec "$tools/rust/bin/cargo" --config "$tools/.cargo/config.toml" "$@"
''')
script(tools / "bin/rustc", '''export LD_LIBRARY_PATH="$tools/lib"
exec "$tools/rust/bin/rustc" "$@"
''')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--compiler", required=True, type=Path)
parser.add_argument("--work", required=True, type=Path, help="new staging directory")
parser.add_argument("--output", required=True, type=Path, help="new .tar.xz or .tar.gz archive")
parser.add_argument("--vendor", type=Path, help="optional cargo vendor --locked --versioned-dirs result")
args = parser.parse_args()
system = Path("/etc/os-release").read_text()
if 'ID=debian\n' not in system or 'VERSION_ID="13"' not in system:
parser.error("prepare this package in the Debian 13 development container")
args.compiler = args.compiler.resolve(strict=True)
args.work = args.work.absolute()
args.output = args.output.absolute()
if not args.output.name.endswith((".tar.xz", ".tar.gz")):
parser.error("output must end in .tar.xz or .tar.gz")
if args.output.exists() or Path(str(args.output) + ".sha256").exists():
parser.error("output archive and checksum must not exist")
bundle, _, rust_version = prepare(args.compiler, args.work)
author = args.work / "author"
tools = bundle / "toolchain"
tools.mkdir()
native_tools(tools)
rust_tools(tools, rust_version, args.vendor)
# 先实际链接,避免把缺少 sysroot 文件的包花时间压缩后才发现。
smoke = author / "link.rs"
smoke.write_text('fn main() { println!("private Rust/C linker ready"); }\n')
subprocess.run([str(tools / "bin/rustc"), str(smoke), "-C",
"linker=" + str(tools / "bin/cc"), "-o", str(author / "link")], check=True)
subprocess.run([str(author / "link")], check=True)
copy(SOURCE / "build/README.md", bundle / "DEVELOPMENT.md")
manifest = record(bundle, [bundle / "DEVELOPMENT.md", *tools.rglob("*")])
args.output.parent.mkdir(parents=True, exist_ok=True)
# 归档必须先于第一次构建,不能把 Cargo 编译缓存放进发行包。
with args.output.open("xb") as stream:
compression = "xz -T4 -6" if args.output.name.endswith(".xz") else "gzip -9"
subprocess.run(["tar", "-C", str(args.work), "-I", compression, "-cf", "-", "dever"],
stdout=stream, check=True)
Path(str(args.output) + ".sha256").write_text(f"{digest(args.output)} {args.output.name}\n")
print(json.dumps({"archive": str(args.output), "bytes": args.output.stat().st_size,
"files": len(manifest["artifacts"]), "compiler_sha256": digest(args.compiler)}))
if __name__ == "__main__":
main()