For agentic workers: 逐 task 执行,每个 task 自带 red→green→commit 循环。
Goal: 让 mcpp 把 xlings 当作可查询的运行时底座:补上 libc 轴的分发契约(c_runtime)、消费 subos 的运行时身份与环境声明、修掉 self-contained 打包模式自产的 /proc/self/exe 陷阱。
Architecture: 三条缝。S2 在既有 mcpp.build.distribution 的三层模型(Role→Contract→Mechanism)上复用词汇、新增 libc 机制表,由 linkmodel 单点渲染;S3 新增 mcpp.xlings.subos_info 读取 xlings 的 subos_info 块,run/test 应用其 env;S1 让 runtime 身份成为显式轴。P2(视图寻址)不做。
Tech Stack: C++23 modules,gtest(tests/unit/),shell e2e(tests/e2e/),nlohmann::json(mcpp.libs.json)。
设计文档:.agents/docs/2026-08-07-xlings-as-runtime-substrate-design.md
- 词汇不新造:契约三值
self-contained|toolchain-coupled|host-coupled,与[build] cxx_runtime逐字相同。runtime binding 拼写<name>@<version>,与 xlingssubos_info.runtime逐字相同。 - 默认值不变:
c_runtime缺省 =toolchain-coupled= 今天的行为。引入这条轴本身不改变任何现有产物。 - Mechanism 必须是 total function:每格要么给 flags,要么
degraded=true+ 非空diagnostic。静默无操作是禁止的结果。 - 不引入新的宿主探测:mcpp 里不得出现探测 GPU / 图形能力的代码。
- schema 缺失/未知版本必须出声:一行 stderr,构建继续。
- P2 不做:
toolchain-coupled保持载荷寻址,不切视图寻址。 - 平台特殊处理下沉:平台差异放
src/platform/对应模块,不在build/里写#ifdef。
Files:
- Modify:
src/pack/pack.cppm:431-446(write_bundle_all_wrappers) - Modify:
docs/02-pack-and-release.md(§ Modeself-contained之后) - Test:
tests/e2e/30_pack_modes.sh
Interfaces:
- Consumes: 无
- Produces: 分发包契约新增环境变量
MCPP_BUNDLE_DIR(bundle 根目录绝对路径),由run.sh/ 顶层同名脚本导出。
背景:ELF 规范禁止 PT_INTERP 用 $ORIGIN,所以 self-contained 只能经 ld.so --library-path 启动;代价是内核把 /proc/self/exe 指向 loader、/proc/self/cmdline 混入 --library-path。本 task 不消除这个约束(那需要安装期改写 PT_INTERP,见设计文档 Q6 选项 i),而是把它变成一个有声明、可编程的契约,并写进文档。
- Step 1: 写失败的 e2e 断言
在 tests/e2e/30_pack_modes.sh 的 Mode B(bundle-all)段落后追加:
# self-contained: the wrapper must export MCPP_BUNDLE_DIR so an application
# can resolve resources despite /proc/self/exe pointing at the loader.
grep -q 'MCPP_BUNDLE_DIR' "$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/run.sh" || {
echo "Mode B: run.sh does not export MCPP_BUNDLE_DIR"; exit 1; }
out=$("$TMP/b/myapp-0.1.0-x86_64-linux-gnu-bundle-all/run.sh" 2>&1) || true
echo "$out" | grep -q 'Hello' || { echo "Mode B: wrapper broke execution: $out"; exit 1; }- Step 2: 跑,确认失败
bash tests/e2e/30_pack_modes.shExpected: FAIL,run.sh does not export MCPP_BUNDLE_DIR
- Step 3: 改 wrapper
src/pack/pack.cppm 的 write_bundle_all_wrappers body 改为:
auto body = std::format(
"#!/bin/sh\n"
"# Auto-generated by `mcpp pack --mode self-contained`. Launches the\n"
"# bundled binary through the bundled dynamic linker so the package\n"
"# is portable across glibc versions.\n"
"#\n"
"# TRAP, and why this variable exists: launching through the loader\n"
"# makes the kernel set /proc/self/exe to the LOADER, not to the\n"
"# program, and /proc/self/cmdline carries --library-path. Any\n"
"# \"find my resources next to the executable\" logic silently\n"
"# resolves against the loader's directory instead. MCPP_BUNDLE_DIR\n"
"# is the answer that survives: resolve against it first and fall\n"
"# back to /proc/self/exe only when it is unset.\n"
"here=$(cd \"$(dirname \"$0\")\" && pwd)\n"
"MCPP_BUNDLE_DIR=\"$here\"; export MCPP_BUNDLE_DIR\n"
"exec \"$here/lib/{}\" --library-path \"$here/lib\" \"$here/bin/{}\" \"$@\"\n",
loaderName, binaryName);- Step 4: 跑,确认通过
bash tests/e2e/30_pack_modes.shExpected: PASS
- Step 5: 文档
在 docs/02-pack-and-release.md 的 self-contained loader 说明之后追加一节:
#### Trap: `/proc/self/exe` under the bundled loader
Launching through the loader means the kernel sets `/proc/self/exe` to the
**loader**, not to your program, and `/proc/self/cmdline` carries the
`--library-path` argument. Every "find my resources next to the executable"
path silently resolves against the wrong directory — GUI toolkits looking for
fonts or `assets/`, and helper binaries shipped alongside the program.
The wrapper exports `MCPP_BUNDLE_DIR` (the bundle root) for this. Resolve
against it first:
```c
const char *base = getenv("MCPP_BUNDLE_DIR"); /* set by run.sh */
/* fall back to /proc/self/exe only when unset */If your application cannot be changed, use --mode vendored instead: it
repoints PT_INTERP at the host loader, so /proc/self/exe is correct — at
the cost of requiring the host's glibc to be at least as new as the one the
artifact was built against.
同一节的中文版加到 `docs/zh/` 下对应文件(若存在)。
- [ ] **Step 6: Commit**
```bash
git add src/pack/pack.cppm docs/02-pack-and-release.md tests/e2e/30_pack_modes.sh
git commit -m "fix(pack): the self-contained wrapper broke /proc/self/exe and never said so"
做过,全绿,然后整条 reset。 设计文档 §3-S2 与 §5.2 记录了理由;摘要:
- 它是照着 #375 提报者提出的解法做的,不是解他的问题(问题是「产物没法分发」)
- 它唯一的新能力是
host-coupled= 把产物链到宿主 libc,而 hermetic 策略禁止穿越的第一条就是/lib*下的 libc。mcpp 能不用 host 就不用 host - 去掉那个值之后这条轴不剩任何能力(
self-contained已由--target ...-musl表达) - 它会造出「可不可以用宿主 libc」的第二个回答者(既有的是
[build] allow_host_libs)
代替它的是 Task 4'(下):#375 的真问题有三条 hermetic 答案,全都已存在;缺口在「其中一条自己是坏的」(Task 1 已修)加「三条都不可发现」。
Files:
- Modify:
docs/02-pack-and-release.md+docs/zh/02-pack-and-release.md - Modify:
docs/00-getting-started.md或10-publishing-a-library.md(择一放「怎么分发」的入口) - Test: 文档改动,无自动化断言;判据是 §10 的 V1(三条路径各自在干净容器里跑通)
要写清的三条,并列、等价、都不用宿主 libc:
| 路径 | 命令 | libc 从哪来 | 什么时候选它 |
|---|---|---|---|
| A. 生态闭环 | mcpp emit xpkg → xlings install |
目标机自己的 xlings 载荷,elfpatch 装机期重指 PT_INTERP/RUNPATH | 目标机在生态内 |
| B. 静态单文件 | --target x86_64-linux-musl |
自带,静态 | 任何 Linux,零运行期依赖 |
| C. 自带运行时 | mcpp pack --mode self-contained |
自带这套工具链的 glibc + loader | 任何 Linux,含比构建机更老的 |
A 要特别写明一件容易误读的事:产物里烙的 PT_INTERP 指向构建机路径不构成分发障碍 —— 走 A 时目标机的 xlings 会重写它。#375 观察到的「路径不存在所以起不来」,前提是绕开生态直接拷贝二进制。
不写:任何「让产物链到系统 libc」的做法。已有的那个决定只有一个入口([build] allow_host_libs),文档不给它第二个说法。
- Step 1 在
docs/02-pack-and-release.md顶部「Two axes」之前加一节 "Three ways to ship, none of which use the host's libc",内容如上表 - Step 2 中文版同步
- Step 3 Commit
Files:
- Create:
src/xlings/subos_info.cppm(模块mcpp.xlings.subos_info) - Test:
tests/unit/test_subos_info.cpp
Interfaces:
-
Consumes:
mcpp.libs.json -
Produces:
namespace mcpp::xlings::subos { inline constexpr int kSupportedSchema = 1; struct EnvDecl { std::string var, op, value; }; struct Provider { std::string binding; std::vector<EnvDecl> decls; }; struct Info { int schema = 0; std::string runtime; // "glibc@2.39" std::vector<Provider> providers; // sorted by binding bool present = false; // the block existed at all std::string note; // non-empty ⇒ caller MUST print it }; Info read(const std::filesystem::path& subosDir); std::string family_of(std::string_view runtime, std::string_view arch = "x86_64"); std::vector<std::pair<std::string,std::string>> resolve_env(const Info&, const std::filesystem::path& subosDir); }
-
Step 1: 写失败的单测
创建 tests/unit/test_subos_info.cpp:
#include <gtest/gtest.h>
import std;
import mcpp.xlings.subos_info;
namespace {
namespace su = mcpp::xlings::subos;
struct Tmp {
std::filesystem::path dir;
Tmp() {
dir = std::filesystem::temp_directory_path()
/ ("mcpp_subos_" + std::to_string(std::random_device{}()));
std::filesystem::create_directories(dir);
}
~Tmp() { std::error_code ec; std::filesystem::remove_all(dir, ec); }
void write(std::string_view body) {
std::ofstream(dir / ".xlings.json") << body;
}
};
TEST(SubosInfo, ReadsRuntimeAndEnvs) {
Tmp t;
t.write(R"({
"workspace": {},
"subos_info": {
"schema_version": 1,
"runtime": "glibc@2.39",
"envs": [
{ "binding": "mesa@25.0.7.1", "decls": [
{ "var": "LIBGL_DRIVERS_PATH", "op": "prepend",
"value": "${subosdir}/usr/lib/dri" }
]}
]
}
})");
auto info = su::read(t.dir);
EXPECT_TRUE(info.present);
EXPECT_EQ(info.schema, 1);
EXPECT_EQ(info.runtime, "glibc@2.39");
ASSERT_EQ(info.providers.size(), 1u);
ASSERT_EQ(info.providers[0].decls.size(), 1u);
EXPECT_EQ(info.providers[0].decls[0].var, "LIBGL_DRIVERS_PATH");
EXPECT_TRUE(info.note.empty());
}
// ${subosdir} is expanded against the subos this block was read from.
TEST(SubosInfo, ResolvesSubosdirPlaceholder) {
Tmp t;
t.write(R"({"subos_info":{"schema_version":1,"runtime":"glibc@2.39",
"envs":[{"binding":"mesa@1","decls":[
{"var":"LIBGL_DRIVERS_PATH","op":"prepend","value":"${subosdir}/usr/lib/dri"}]}]}})");
auto env = su::resolve_env(su::read(t.dir), t.dir);
ASSERT_EQ(env.size(), 1u);
EXPECT_EQ(env[0].first, "LIBGL_DRIVERS_PATH");
EXPECT_EQ(env[0].second, (t.dir / "usr/lib/dri").string());
}
// A subos made before xlings grew the block: degrade, and SAY SO. Silence
// here is what made mcpp#352 hard to find in the first place.
TEST(SubosInfo, MissingBlockDegradesWithANote) {
Tmp t;
t.write(R"({"workspace":{}})");
auto info = su::read(t.dir);
EXPECT_FALSE(info.present);
EXPECT_TRUE(info.runtime.empty());
EXPECT_FALSE(info.note.empty());
}
// A schema newer than we understand: use what we can, and say we are behind.
TEST(SubosInfo, NewerSchemaDegradesWithANote) {
Tmp t;
t.write(R"({"subos_info":{"schema_version":99,"runtime":"glibc@2.44","envs":[]}})");
auto info = su::read(t.dir);
EXPECT_TRUE(info.present);
EXPECT_EQ(info.runtime, "glibc@2.44");
EXPECT_FALSE(info.note.empty());
}
TEST(SubosInfo, NoFileAtAllIsNotACrash) {
Tmp t; // nothing written
auto info = su::read(t.dir);
EXPECT_FALSE(info.present);
EXPECT_FALSE(info.note.empty());
}
// The family mapping must agree with xlings's own, verbatim.
TEST(SubosInfo, FamilyOfMirrorsXlings) {
EXPECT_EQ(su::family_of("glibc@2.39"), "linux-x86_64-glibc");
EXPECT_EQ(su::family_of("musl@1.2.5"), "linux-x86_64-musl");
EXPECT_EQ(su::family_of("glibc@2.39", "aarch64"), "linux-aarch64-glibc");
EXPECT_EQ(su::family_of("wasi-libc@1"), "wasm32-wasi");
EXPECT_EQ(su::family_of("nonsense@1"), "unknown");
}
// Malformed JSON must not take the build down.
TEST(SubosInfo, MalformedJsonDegrades) {
Tmp t;
t.write("{ this is not json");
auto info = su::read(t.dir);
EXPECT_FALSE(info.present);
EXPECT_FALSE(info.note.empty());
}
} // namespace- Step 2: 跑,确认失败
mcpp test --filter SubosInfoExpected: FAIL,模块不存在
- Step 3: 实现
创建 src/xlings/subos_info.cppm:
// mcpp.xlings.subos_info — read the `subos_info` block xlings writes into a
// subos's own `.xlings.json`.
//
// WHAT THIS IS FOR
//
// A program needs three things: bootstrap (PT_INTERP + CRT + libc), discovery
// (PATH + RPATH), and configuration (env vars). xlings had the first two —
// glibc + elfpatch, xvm + shims — and until it grew this block, nothing for
// the third. Its own module comment names the consequence: mcpp#352, a GLFW
// binary that links fine and exits 255 because nothing told it where the GL
// drivers are.
//
// mcpp is the consumer of the third one. This module ONLY reads and resolves;
// it never writes the block and never manages subos lifecycle — that is
// xlings's layer, and mcpp reaching into it is the layering inversion the
// ecosystem design calls out by name.
//
// A note on silence: every degradation here fills `note`, and callers are
// required to print it. "It did not happen" and "it succeeded" producing the
// same output is the property that made #352 expensive to find.
//
// Design: .agents/docs/2026-08-07-xlings-as-runtime-substrate-design.md §3-S3
export module mcpp.xlings.subos_info;
import std;
import mcpp.libs.json;
export namespace mcpp::xlings::subos {
// The schema this build understands. A higher one on disk is usable — we read
// the fields we know — but the caller is told it is reading a newer format.
inline constexpr int kSupportedSchema = 1;
inline constexpr std::string_view kBlock = "subos_info";
struct EnvDecl {
std::string var;
std::string op; // "set" | "prepend"
std::string value; // may contain ${subosdir}
};
struct Provider {
std::string binding; // "<name>@<version>"
std::vector<EnvDecl> decls;
};
struct Info {
int schema = 0;
std::string runtime; // "glibc@2.39"
std::vector<Provider> providers; // sorted by binding
bool present = false;
// Non-empty ⇒ the caller MUST surface it. Never a hard error: a missing
// or newer block degrades the experience, it does not invalidate a build.
std::string note;
};
// The runtime string is self-describing: "glibc@2.39" says Linux/glibc
// without a second field that could disagree with it. Mirrors xlings's
// `subos::manifest::family_of` — the mapping is small, stable and part of the
// cross-repo contract, so it is asserted in tests rather than left implicit.
std::string family_of(std::string_view runtime,
std::string_view arch = "x86_64") {
const auto at = runtime.find('@');
const auto name = runtime.substr(0, at == std::string_view::npos
? runtime.size() : at);
if (name == "glibc") return std::format("linux-{}-glibc", arch);
if (name == "musl") return std::format("linux-{}-musl", arch);
if (name == "wasi-libc") return "wasm32-wasi";
if (name == "macos_sdk") return std::format("darwin-{}", arch);
if (name == "ucrt") return std::format("windows-{}-ucrt", arch);
return "unknown";
}
Info read(const std::filesystem::path& subosDir) {
Info info;
auto path = subosDir / ".xlings.json";
std::error_code ec;
if (!std::filesystem::exists(path, ec)) {
info.note = std::format(
"subos '{}' has no .xlings.json, so it cannot say which runtime it "
"is or what environment its programs need", subosDir.string());
return info;
}
std::ifstream is(path);
auto doc = nlohmann::json::parse(is, nullptr, /*allow_exceptions=*/false);
if (doc.is_discarded() || !doc.is_object()) {
info.note = std::format("subos manifest {} is not readable JSON",
path.string());
return info;
}
auto it = doc.find(std::string(kBlock));
if (it == doc.end() || !it->is_object()) {
info.note = std::format(
"subos '{}' does not describe itself (no `{}` block): its runtime "
"identity and environment declarations are unavailable. A newer "
"xlings writes this block; `xlings self update` adds it",
subosDir.string(), kBlock);
return info;
}
info.present = true;
if (auto v = it->find("schema_version");
v != it->end() && v->is_number_integer())
info.schema = v->get<int>();
if (auto v = it->find("runtime"); v != it->end() && v->is_string())
info.runtime = v->get<std::string>();
if (auto envs = it->find("envs"); envs != it->end() && envs->is_array()) {
for (auto& p : *envs) {
if (!p.is_object()) continue;
Provider prov;
if (auto b = p.find("binding"); b != p.end() && b->is_string())
prov.binding = b->get<std::string>();
if (auto ds = p.find("decls"); ds != p.end() && ds->is_array()) {
for (auto& d : *ds) {
if (!d.is_object()) continue;
EnvDecl e;
if (auto x = d.find("var"); x != d.end() && x->is_string())
e.var = x->get<std::string>();
if (auto x = d.find("op"); x != d.end() && x->is_string())
e.op = x->get<std::string>();
if (auto x = d.find("value"); x != d.end() && x->is_string())
e.value = x->get<std::string>();
if (!e.var.empty()) prov.decls.push_back(std::move(e));
}
}
info.providers.push_back(std::move(prov));
}
}
// Sorted by binding, matching xlings's own ordering, so two reads of the
// same subos produce the same environment in the same order.
std::ranges::sort(info.providers,
[](auto const& a, auto const& b) { return a.binding < b.binding; });
if (info.schema > kSupportedSchema)
info.note = std::format(
"subos '{}' declares schema {}, newer than the {} this mcpp "
"understands; reading the fields it knows and ignoring the rest",
subosDir.string(), info.schema, kSupportedSchema);
return info;
}
// Resolve the declarations into concrete (var, value) pairs, `${subosdir}`
// expanded. `prepend` entries for the same variable are joined with ':' in
// provider order; a `set` replaces whatever came before it, which is xlings's
// own precedence.
std::vector<std::pair<std::string, std::string>>
resolve_env(const Info& info, const std::filesystem::path& subosDir) {
std::vector<std::pair<std::string, std::string>> out;
auto expand = [&](std::string v) {
constexpr std::string_view kPh = "${subosdir}";
for (auto pos = v.find(kPh); pos != std::string::npos;
pos = v.find(kPh, pos))
v.replace(pos, kPh.size(), subosDir.string());
return v;
};
for (auto const& p : info.providers) {
for (auto const& d : p.decls) {
auto value = expand(d.value);
auto hit = std::ranges::find(out, d.var, &std::pair<std::string,std::string>::first);
if (hit == out.end()) { out.emplace_back(d.var, value); continue; }
if (d.op == "set") { hit->second = value; continue; }
// prepend, de-duplicated: a doubled entry must not accumulate
// across nested invocations.
if (hit->second != value
&& !hit->second.starts_with(value + ":")
&& hit->second.find(":" + value) == std::string::npos)
hit->second = value + ":" + hit->second;
}
}
return out;
}
} // namespace mcpp::xlings::subos- Step 4: 跑,确认通过
mcpp test --filter SubosInfoExpected: PASS(7 组全绿)
- Step 5: Commit
git add src/xlings/subos_info.cppm tests/unit/test_subos_info.cpp
git commit -m "feat(xlings): read the subos's own description instead of inferring it"Files:
- Modify:
src/build/plan.cppm(把解析好的 env 放进 plan) - Modify:
src/build/execute.cppm:279-300, 1210-1230(应用到子进程) - Test:
tests/e2e/196_subos_env_reaches_program.sh(新建)
Interfaces:
- Consumes:
mcpp::xlings::subos::{read, resolve_env} - Produces:
BuildPlan新增std::vector<std::pair<std::string,std::string>> subosEnv;
为什么不是自己拼图形变量:LIBGL_DRIVERS_PATH / __EGL_VENDOR_LIBRARY_DIRS / XDG_DATA_DIRS 的值由 xlings 的图形包声明,mcpp 只是把它们传下去。mcpp 里出现任何图形相关的字面量都是这条设计的违反。
- Step 1: 写失败的 e2e
创建 tests/e2e/196_subos_env_reaches_program.sh:
#!/usr/bin/env bash
# requires: linux
# A variable a package declared into the subos must reach the program `mcpp
# run` launches. Until this landed, subos env declarations were applied only
# by `xlings subos use`, so a program started by mcpp saw none of them — the
# reason a GL binary could link fine and exit 255 (mcpp#352).
set -euo pipefail
. "$(dirname "$0")/_common.sh"
proj=$(mktemp -d)
trap 'rm -rf "$proj"' EXIT
# A subos that declares one variable, in xlings's own schema.
subos="$proj/subos"
mkdir -p "$subos/usr/lib/dri"
cat > "$subos/.xlings.json" <<'EOF'
{ "workspace": {},
"subos_info": { "schema_version": 1, "runtime": "glibc@2.39",
"envs": [ { "binding": "probe@1", "decls": [
{ "var": "MCPP_E2E_PROBE", "op": "prepend",
"value": "${subosdir}/usr/lib/dri" } ] } ] } }
EOF
cd "$proj"
"$MCPP" new hello >/dev/null
cd hello
cat > src/main.cpp <<'EOF'
#include <cstdlib>
#include <cstdio>
int main() {
const char* v = std::getenv("MCPP_E2E_PROBE");
std::printf("PROBE=%s\n", v ? v : "(unset)");
}
EOF
out=$(MCPP_SUBOS_DIR="$subos" "$MCPP" run 2>&1)
echo "$out" | grep -q "PROBE=$subos/usr/lib/dri" || {
echo "subos env did not reach the program:"; echo "$out"; exit 1; }
echo "PASS: subos env reaches the program"chmod +x tests/e2e/196_subos_env_reaches_program.sh
MCPP_SUBOS_DIR是本 task 引入的测试与覆盖用入口:默认解析仍是活动 subos。它让这条 e2e 不需要改动用户真实环境——记住 memory 里那条教训:破坏性脚本不在真机上试。
- Step 2: 跑,确认失败
bash tests/e2e/196_subos_env_reaches_program.shExpected: FAIL,PROBE=(unset)
- Step 3: 实现
src/build/plan.cppm,在 runtimeLibraryDirs 那段之后:
// The subos's own environment declarations. mcpp reads them and passes
// them on; it does not author them and does not know what they mean. That
// is the whole point: when xlings adds a Vulkan loader or a new driver
// bridge, this code does not change.
{
auto subosDir = mcpp::xlings::subos_dir_for_build(cfg);
auto info = mcpp::xlings::subos::read(subosDir);
if (!info.note.empty()) mcpp::ui::warning(info.note);
plan.subosEnv = mcpp::xlings::subos::resolve_env(info, subosDir);
}src/xlings.cppm 加一个解析器(单点,不让每个消费者各猜一次):
// The subos whose environment a program built here should run under.
//
// MCPP_SUBOS_DIR overrides it outright — that exists so tests can exercise
// this path without touching the developer's real environment, and so a
// user can point a run at another subos without switching the active one.
std::filesystem::path subos_dir_for_build(const config::GlobalConfig& cfg);src/build/execute.cppm:在构造子进程环境的两处(run 的 279-300 与 ninja 侧 1210-1230)把 plan.subosEnv 合并进去,prepend 语义与既有 prepend_path_list 一致。
- Step 4: 跑,确认通过
bash tests/e2e/196_subos_env_reaches_program.shExpected: PASS
- Step 5: Commit
git add src/build src/xlings.cppm tests/e2e/196_subos_env_reaches_program.sh
git commit -m "feat(run): a program mcpp launches gets its subos's environment"Files:
- Modify:
src/toolchain/abi.cppm(libc 维度带版本) - Modify:
src/toolchain/model.cppm(Toolchain加runtimeBinding) - Modify:
src/build/prepare.cppm(解析优先级) - Test:
tests/unit/test_abi.cpp
Interfaces:
- Consumes:
subos::read(...).runtime、subos::family_of - Produces:
AbiProfile新增std::string libcVersion;(空 = 未知,不参与匹配),Toolchain::runtimeBinding
收窄的实现范围与理由:abi.cppm 的 libc 维度参与依赖解析,给它加一个参与匹配的版本轴会立刻改变全索引的解析结果(memory 里 index-floor-must-degrade 是同一族事故)。所以本 task 只做两件事:①把版本记录下来并在 mcpp doctor / --verbose 里可见;②runtimeBinding 的解析优先级落地。匹配语义不变,留给 platform manifest 那一轮。
- Step 1: 写失败的单测
在 tests/unit/test_abi.cpp 追加:
// The libc dimension gains a version, and it is RECORDED, not matched. A
// dependency that says `abi:glibc` must keep matching a glibc@2.39 toolchain
// exactly as it did before — changing that would re-resolve the whole index.
TEST(Abi, LibcVersionIsRecordedButNotMatched) {
mcpp::toolchain::Toolchain tc;
tc.targetTriple = "x86_64-linux-gnu";
tc.runtimeBinding = "glibc@2.39";
auto p = mcpp::toolchain::abi_profile(tc);
EXPECT_EQ(p.libc, "glibc");
EXPECT_EQ(p.libcVersion, "2.39");
auto c = mcpp::toolchain::parse_abi_capability("abi:glibc");
ASSERT_TRUE(c.has_value());
EXPECT_TRUE(mcpp::toolchain::satisfies(p, *c));
}
// No binding resolved: the dimension still answers, without a version.
TEST(Abi, LibcVersionEmptyWithoutABinding) {
mcpp::toolchain::Toolchain tc;
tc.targetTriple = "x86_64-linux-gnu";
auto p = mcpp::toolchain::abi_profile(tc);
EXPECT_EQ(p.libc, "glibc");
EXPECT_TRUE(p.libcVersion.empty());
}- Step 2: 跑,确认失败
mcpp test --filter AbiExpected: FAIL,runtimeBinding / libcVersion 不存在
- Step 3: 实现
model.cppm 的 Toolchain 加:
// The runtime this toolchain's output is built against, in xlings's own
// spelling ("glibc@2.39"). Resolved in prepare, in this order, every step
// explicit — a "default is the convention" step is what grows a second
// answerer (see the design doc §S1):
// 1. --runtime
// 2. [target.<triple>].runtime / [build].runtime
// 3. the active subos's `subos_info.runtime`
// 4. the payload actually resolved, with a note
std::string runtimeBinding;abi.cppm 的 AbiProfile 加 std::string libcVersion;,并在 abi_profile() 的两条路径里从 tc.runtimeBinding 的 @ 之后取值。satisfies() 不动。
prepare.cppm 按上述四级解析并写进 tc.runtimeBinding;第 4 级发一条 ui::info。
- Step 4: 跑,确认通过
mcpp test --filter AbiExpected: PASS
- Step 5: Commit
git add src/toolchain src/build/prepare.cppm tests/unit/test_abi.cpp
git commit -m "feat(toolchain): the runtime a build targets is read, not inferred"Files:
-
Modify:
docs/05-mcpp-toml.md(c_runtime) -
Modify:
docs/02-pack-and-release.md(contract × mode 的关系) -
Modify:
docs/03-toolchains.md(runtime binding) -
Modify:
docs/zh/下的对应文件 -
Modify:
.agents/docs/2026-08-07-xlings-as-runtime-substrate-design.md(标注已实施) -
Step 1:
05-mcpp-toml.md加c_runtime
紧邻既有 cxx_runtime 一节,写明三值、默认值、以及与 mcpp pack --mode 的函数关系表(设计文档 §3-S2 那张)。必须写清 host-coupled 的语义是「宿主 glibc ≥ 构建时的那份」,不是「任何 Linux」。
- Step 2:
02-pack-and-release.md加一节 "Contract vs mode"
说明 mode 管「带多少东西」、contract 管「承诺什么」,并给出不可分发契约在 pack 时会被拒绝的行为。
- Step 3:
03-toolchains.md加 runtime binding
说明四级解析顺序与 --runtime。
- Step 4: 校验中文版同步
ls docs/zh/对每个改过的英文文档,同步中文版。
- Step 5: Commit
git add docs .agents/docs
git commit -m "docs: the two runtime contracts, and what each promises"Files:
-
Modify:
src/version.cppm -
Modify:
src/xlings.cppm(kXlingsVersion) -
Step 1: 查最新 xlings 版本
gh api repos/openxlings/xlings/releases/latest --jq .tag_name- Step 2: bump 两个常量
MCPP_VERSION → 2026.8.8.1;kXlingsVersion → 上一步的值。
只动这两个。 bootstrap pin(
.xlings.json那组)是自举起点,不随发布走 —— 一起 bump 会让全部 CI 去装一个还不存在的版本。
- Step 3: 机器校验 pin 一致
.github/tools/check_version_pins.shExpected: 通过
- Step 4: 全量本地验证
mcpp build && mcpp test
for t in 30_pack_modes 195_c_runtime_host_coupled 196_subos_env_reaches_program; do
bash "tests/e2e/$t.sh" || echo "FAIL: $t"
done- Step 5: 开 PR
git checkout -b feat/xlings-runtime-substrate
git push -u origin feat/xlings-runtime-substrate
gh pr create --title "feat: xlings 作为运行时底座 —— libc 分发契约 + subos 环境 (#375, #352)" --body-file .agents/docs/pr-body.mdFiles(/home/speak/workspace/github/mcpplibs/mcpp-index):
-
Modify:
pkgs/c/compat.glfw.lua,pkgs/c/compat.glx-headers.lua,pkgs/c/compat.vulkan-runtime.lua -
Deprecate:
pkgs/c/compat.glx-runtime.lua -
Step 1: 确认下游只有三个
cd /home/speak/workspace/github/mcpplibs/mcpp-index && grep -rln "glx-runtime" pkgs/Expected: 恰好 4 个文件(含它自己)
- Step 2: 三个消费者改为依赖
xim:graphics
compat.glx-runtime 保留为空壳 provider(仍声明 provides = {"opengl.glx.driver","x11.display"},但不再 symlink 宿主库),一个版本之后再删——下游包的 manifest 已发布,不能立刻失效(与 index-floor-must-degrade 同一条教训)。
- Step 3: 验
bash tests/smoke.sh 2>/dev/null || ls tests/按该仓既有验证入口跑。判据是渲染器身份,不是「窗口出现了」。
- Step 4: PR
Spec coverage — 设计文档各节 → task 映射:
| 设计文档 | Task |
|---|---|
§1.6 / Q6 self-contained wrapper |
T1 |
§3-S2 c_runtime 契约 |
T2 + T3 + T4 |
| §3-S3 subos 环境 | T5 + T6 |
| §3-S1 运行时身份 | T7 |
| §1.5 图形栈迁移 | T10 |
| §6 P0 文档 | T1 Step5 + T8 |
| §7 跨仓契约(xlings 落盘 exports) | 不在本 PR —— 需 xlings 侧改动,设计文档已排 P1 尾;本 PR 的 §5.1「A 降级路径」足以工作 |
| §5.3 / P2 视图寻址 | 不做(设计文档明确) |
未覆盖且是有意的:§2.1 六个回答者里,本 PR 收敛的是 #2/#3/#5(loader 选择统一到 link_tokens + distro_loader_path)。#1/#4/#6 依赖 xlings 侧落盘 exports,留 P1。V5 的静态计数守的是「不再增加」,不是「已经归零」。
Type consistency — 跨 task 一致性已核:dist::Contract / dist::Role / dist::Format 三者在 T2/T4 同名;libcdist::Addressing 与 tc::LibcAddressing 是有意的镜像对(层次不倒置),T3 的注释与 T2 的测试都点明了这一点,并由 T4 的 static_cast 单点转换。subos::Info / resolve_env 在 T5 定义、T6 消费,签名一致。