diff --git a/.agents/docs/2026-08-23-llvm-musl-libcxx-package-design.md b/.agents/docs/2026-08-23-llvm-musl-libcxx-package-design.md new file mode 100644 index 00000000..41d25768 --- /dev/null +++ b/.agents/docs/2026-08-23-llvm-musl-libcxx-package-design.md @@ -0,0 +1,56 @@ +# xim:llvm-musl-libcxx 包设计方案 + +> 状态: **已落地待 review**(配合 mcpp#492 引擎侧 PR) +> 日期: 2026-08-23 + +## 动机 + +llvm 家族(clang 前端)不携带任何目标 libc;其 payload 的 `clang.cfg` +还把宿主 glibc 世界钉死在驱动上。`*-linux-musl` 从 llvm 家族服务需要三样 +clang 不自带的东西: + +1. 为 musl 目标配置的 libc++(宿主 libc++ 的 `__config_site` 描述的是另 + 一个 ABI,不可复用); +2. 基于该 libc++ 的 std/std.compat 模块源(在宿主 libc++ 上预编译的 + std BMI 在 PCM 层就是错的); +3. crt/libgcc/libc —— musl-gcc 已提供,本包**不**重复:mcpp 引擎经 + `--gcc-toolchain` 指向 musl-gcc payload,这里只出 C++ 运行时。 + +## 形态 + +- `type = "package"`,`spec = "2"`,纯数据 payload(头文件 + 静态库 + + 模块源),无 programs,xvm 伞节点(同 `libcxx-headers` 先例:第二份 + C++ 标准库绝不能进宿主 sysroot,会遮蔽宿主自己的 libc++)。 +- `archs = {"x86_64", "aarch64"}`:按**目标**架构双资产,每个一份权威 + sha256。资产为 `.tar.xz`,各约 1.5MB。 +- 构建来源:llvm-project release/22.x runtimes + (`LLVM_ENABLE_RUNTIMES=libcxx;libcxxabi;libunwind`),clang 22.1.8 + payload 交叉驱动进各目标 musl sysroot。仅静态(`.a`):musl 目标是 + 全静态 ELF 世界,动态 libc++ 无消费者。 +- payload 布局(mcpp 的 llvm-musl 分支按此消费): + `include/c++/v1/`、`lib/libc++.a|libc++abi.a|libunwind.a`、 + `share/libc++/v1/std.cppm|std.compat.cppm` 及 `std/`、`std.compat/` + 导出表。 + +## 资源托管 + +暂存于贡献者 fork 的 release(`cloud-teahouse/mcpp` tag +`llvm-musl-libcxx-22.1.8`),sha256 已钉死字节;由维护者迁移至 +xlings-res(GLOBAL + CN 双镜像)后改指 source 模板。故**未声明 `ci` +块**——`mirror` 会在资产未迁移前指向空仓库,`update` 会 bump 到尚不存在 +的版本(资产需从新 llvm payload 逐目标重建,是带构建配方的人工步骤), +与 `libcxx-headers` 不声明 ci 的理由一致。 + +## 验证 + +- `pytest tests/ -m 'static or isolation'`:1887 passed(含本包 12 项)。 +- 隔离 XLINGS_HOME 下 `config --add-xpkg` + `install` + `remove` 全链路 + 实测通过:下载、sha256 校验、install 三断言(algorithm 头 / libc++.a + / std.cppm)、卸载清理。 +- 端到端(配合 mcpp#492):等效直驱 clang 管线已产出全静态 + x86_64(本机运行)与 aarch64(qemu)二进制验证。 + +## 关联 + +- 引擎侧:mcpp-community/mcpp#492(`llvmSysroot` 列消费本包) +- 议题:mcpp-community/mcpp#491 diff --git a/pkgs/l/llvm-musl-libcxx.lua b/pkgs/l/llvm-musl-libcxx.lua new file mode 100644 index 00000000..12a6c940 --- /dev/null +++ b/pkgs/l/llvm-musl-libcxx.lua @@ -0,0 +1,122 @@ +package = { + spec = "2", + homepage = "https://libcxx.llvm.org/", + + name = "llvm-musl-libcxx", + description = "musl-targeted libc++ static runtime and std module sources - closes the LLVM-family gap on *-linux-musl", + + maintainers = {"https://github.com/llvm/llvm-project/graphs/contributors"}, + licenses = {"Apache-2.0 WITH LLVM-exception"}, + repo = "https://github.com/llvm/llvm-project", + docs = "https://libcxx.llvm.org/", + + type = "package", + -- HOST arches: the payload is consumed by a HOST clang cross-compiling to + -- the musl target of the matching TARGET arch (x86_64 host -> x86_64 + -- target, aarch64 host -> aarch64 target; a cross from either host arch to + -- either target arch also resolves the matching asset). + archs = {"x86_64", "aarch64"}, + status = "stable", + categories = {"library", "toolchain", "c++", "llvm", "musl"}, + keywords = {"libc++", "libcxx", "musl", "llvm", "static", "cross"}, + + -- No programs: a data payload of headers, archives and module sources. + xvm_enable = true, + + -- ⭐ WHY THIS PACKAGE EXISTS. + -- + -- The llvm payload's clang frontend carries no target libc at all, and its + -- bundled clang.cfg pins the HOST's glibc world. Serving *-linux-musl from + -- the LLVM family therefore needs three things clang does not ship: + -- 1. libc++ configured FOR the musl target (a host libc++'s __config_site + -- describes a different ABI and cannot be reused), + -- 2. the std/std.compat module sources precompiled against that libc++ + -- (a std BMI built over the host libc++ is wrong at the PCM level), + -- 3. crt/libgcc/libc -- which musl-gcc already provides, so this package + -- does NOT duplicate them; the mcpp engine points clang at the + -- musl-gcc payload via --gcc-toolchain and only the C++ runtime + -- (libc++/libc++abi/libunwind static archives) comes from here. + -- + -- Built from llvm-project release/22.x runtimes (LLVM_ENABLE_RUNTIMES = + -- libcxx;libcxxabi;libunwind) with the clang 22.1.8 payload cross-driving + -- into each target's musl sysroot. Static-only: the musl story is fully + -- static ELF, and dynamic libc++ on musl buys nothing this target wants. + -- + -- Payload layout (consumed by mcpp's llvm-musl branch, PR mcpp#492): + -- include/c++/v1/ libc++ headers configured for the musl target + -- lib/libc++.a lib/libc++abi.a lib/libunwind.a + -- share/libc++/v1/std.cppm share/libc++/v1/std.compat.cppm + -- share/libc++/v1/std/ share/libc++/v1/std.compat/ export tables + -- + -- ⚠️ No `ci` block, deliberately: `mirror` would point the mirror at + -- xlings-res before the asset is migrated there (the URLs below stage on + -- the contributor fork's releases and maintainers move them to xlings-res, + -- same handover `libcxx-headers` documents), and `update` would bump + -- `latest` to a new LLVM release whose artifact does not exist -- this + -- payload must be REBUILT from a new llvm payload per target arch, which + -- is a human step with a build recipe, not a re-download. + xpm = { + source = { + GLOBAL = "https://github.com/xlings-res/llvm-musl-libcxx/releases/download/${version}/llvm-musl-libcxx-${version}-linux-${arch2}.tar.xz", + CN = "https://gitcode.com/xlings-res/llvm-musl-libcxx/releases/download/${version}/llvm-musl-libcxx-${version}-linux-${arch2}.tar.xz", + }, + linux = { + ["latest"] = { ref = "22.1.8" }, + ["22.1.8"] = { + url = { + -- Dedicated payload repo (source + per-release archives) + -- until the asset migrates to xlings-res; sha256 pins the + -- bytes either way. See the ci-block note above. + GLOBAL = "https://github.com/cloud-teahouse/llvm-musl-libcxx/releases/download/22.1.8/llvm-musl-libcxx-22.1.8-linux-x86_64.tar.xz", + }, + sha256 = { + x86_64 = "bdd30f05fc9136f9e582caec727f2c39c195acd29a21e79d1a19e3c170cc381c", + aarch64 = "e5bfac3136c69a50dff4511a8220e7022caac1facbe4e2db15c03e363f368548", + }, + }, + }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.xvm") + +function install() + local dir = pkginfo.install_dir() + os.tryrm(dir) + + local extracted = pkginfo.install_file():replace(".tar.xz", "") + if not os.isdir(extracted) then + raise("llvm-musl-libcxx payload not found at " .. extracted) + end + os.mv(extracted, dir) + + -- Assert the three halves a consumer needs; the easiest mistake when + -- rebuilding this payload from a new llvm release is dropping one. + if not os.isfile(path.join(dir, "include", "c++", "v1", "algorithm")) then + raise("llvm-musl-libcxx payload is missing include/c++/v1/algorithm") + end + if not os.isfile(path.join(dir, "lib", "libc++.a")) then + raise("llvm-musl-libcxx payload is missing lib/libc++.a") + end + if not os.isfile(path.join(dir, "share", "libc++", "v1", "std.cppm")) then + raise("llvm-musl-libcxx payload is missing share/libc++/v1/std.cppm") + end + + return true +end + +function config() + -- Umbrella node only, for the same reason `libcxx-headers` is one: this is + -- a SECOND copy of the C++ standard library, targeted at musl. Publishing + -- it into the host sysroot would shadow the host's own libc++ for every + -- ordinary build. The mcpp engine's llvm-musl branch is the consumer: it + -- resolves this payload for the target and points -isystem/-L at it. + xvm.add(package.name) + return true +end + +function uninstall() + xvm.remove(package.name) + return true +end diff --git a/tests/l/test_llvm_musl_libcxx.py b/tests/l/test_llvm_musl_libcxx.py new file mode 100644 index 00000000..9053d4da --- /dev/null +++ b/tests/l/test_llvm_musl_libcxx.py @@ -0,0 +1,85 @@ +"""测试 llvm-musl-libcxx 包""" +import pytest +from tests.lib.xpkg_parser import parse_xpkg +from tests.lib.assertions import ( + assert_required_fields, assert_valid_spec, assert_valid_type, + assert_no_typos, assert_no_exec_xvm, assert_no_bashrc_modification, + assert_no_direct_path_modification, assert_uses_new_api, + assert_xim_add_succeeds, assert_install_succeeds, +) +from tests.lib.platform_utils import skip_if_not + +PKG = "llvm-musl-libcxx" +PKG_FILE = "pkgs/l/llvm-musl-libcxx.lua" + + +@pytest.fixture(scope='module') +def meta(): + return parse_xpkg(PKG_FILE) + + +class TestStatic: + @pytest.mark.static + def test_required_fields(self, meta): + assert_required_fields(meta) + + @pytest.mark.static + def test_valid_spec(self, meta): + assert_valid_spec(meta) + + @pytest.mark.static + def test_valid_type(self, meta): + assert_valid_type(meta) + + @pytest.mark.static + def test_no_typos(self): + assert_no_typos(PKG_FILE) + + @pytest.mark.static + def test_both_target_arches_have_sha256(self, meta): + # The payload is per-TARGET-arch: one musl libc++ per arch, each + # pinned by its own sha256. A missing arch leaves that target's users + # downloading an unverifiable (or 404) asset. + rc = meta.raw_content + assert 'x86_64 = "' in rc, "missing x86_64 sha256" + assert 'aarch64 = "' in rc, "missing aarch64 sha256" + + @pytest.mark.static + def test_static_only_archives(self, meta): + # The musl story is fully static ELF; a dynamic libc++ on musl is + # nothing this target wants, and shipping one would double the asset + # for no consumer. + rc = meta.raw_content + assert "libc++.a" in rc, "payload must ship the static archive" + assert ".so" not in rc, "payload must not ship dynamic libc++" + + +class TestIndex: + @pytest.mark.index + def test_xim_add(self): + assert_xim_add_succeeds(PKG_FILE) + + +class TestIsolation: + @pytest.mark.isolation + def test_no_exec_xvm(self): + assert_no_exec_xvm(PKG_FILE) + + @pytest.mark.isolation + def test_no_bashrc(self): + assert_no_bashrc_modification(PKG_FILE) + + @pytest.mark.isolation + def test_no_path_modification(self): + assert_no_direct_path_modification(PKG_FILE) + + @pytest.mark.isolation + def test_new_api(self): + assert_uses_new_api(PKG_FILE) + + +class TestLifecycle: + @pytest.mark.lifecycle + @skip_if_not('linux') + def test_install(self): + assert_install_succeeds(PKG)