Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .agents/docs/2026-08-23-llvm-musl-libcxx-package-design.md
Original file line number Diff line number Diff line change
@@ -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
122 changes: 122 additions & 0 deletions pkgs/l/llvm-musl-libcxx.lua
Original file line number Diff line number Diff line change
@@ -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
85 changes: 85 additions & 0 deletions tests/l/test_llvm_musl_libcxx.py
Original file line number Diff line number Diff line change
@@ -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)