281 lines
13 KiB
Python
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()
|