Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
31fa043
2026.8.27.1 —— 目标侧被解析出来了,只发给了一个编译单元
speak-agent Aug 27, 2026
9adc5ad
fix(test): 新的 hostflags fixture 用了 MSVC 没有的 getpid/<unistd.h>
speak-agent Aug 27, 2026
dd2b278
fix: 请求的 glibc 版本与装好的 glibc 版本,是两种词汇
speak-agent Aug 27, 2026
1b6f527
chore(xlings): pin 2026.8.17.2 → 2026.8.27.1,索引已经在等这个客户端
speak-agent Aug 27, 2026
af126a3
fix(e2e295): 归一化用了 `^` 锚定,而 Windows 上那个 token 是带引号的
speak-agent Aug 27, 2026
4fd1fa2
fix: 「请求的版本 → 装好的目录」有两个调用方,而我先只修了一个
speak-agent Aug 27, 2026
2533735
fix(prepare): build.mcpp 的宿主工具链不该继承目标侧的 C 库出处
speak-agent Aug 27, 2026
b3735fc
Revert "chore(xlings): pin 2026.8.17.2 -> 2026.8.27.1" —— 它把绑定移到了已装载荷的前面
speak-agent Aug 27, 2026
6e310aa
ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤)
speak-agent Aug 27, 2026
d335ff3
fix: 头集轴把这台机器的 home 写进了键(自我 review 实测)
speak-agent Aug 27, 2026
4ed7511
fix: 空版本请求会把包目录本身当成载荷返回
speak-agent Aug 27, 2026
5087d3a
docs: 新键在旧引擎上的行为(实测),并补上被我改得不准的那条消息
speak-agent Aug 27, 2026
ff24a88
docs(changelog): 补回漏掉的那条修复,并订正一句已经过时的说法
speak-agent Aug 27, 2026
32fc574
test: 两个键会在同一个包里相遇,而没有任何判据覆盖这件事
speak-agent Aug 27, 2026
7325184
ci: 挑「刚构建出来的那个 mcpp」,而它挑的是走到的第一个
speak-agent Aug 27, 2026
464a16a
Revert "ci: 把引导阶段绑定指名的 glibc 载荷装给它(过渡步骤)"
speak-agent Aug 27, 2026
d4cbd39
chore(xlings): pin 2026.8.17.2 -> 2026.8.27.2
speak-agent Aug 27, 2026
5603bc5
Revert "chore(xlings): pin 2026.8.17.2 -> 2026.8.27.2"
speak-agent Aug 27, 2026
557b6e9
docs(plan): 回填实施结果 —— 写实现改掉了两处接口设计
speak-agent Aug 27, 2026
7977744
docs(plan): 回填 ⑤ 的实施结果 —— 排除表从十八条缩到五条
speak-agent Aug 27, 2026
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
1,958 changes: 1,958 additions & 0 deletions .agents/docs/2026-08-27-openkal-ecosystem-design-plan.md

Large diffs are not rendered by default.

1,426 changes: 1,426 additions & 0 deletions .agents/docs/2026-08-27-openkal-native-path-three-issues.md

Large diffs are not rendered by default.

47 changes: 45 additions & 2 deletions .github/workflows/openkal-cross.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,52 @@ jobs:
# decisions about compile and link flags; an optimisation level
# changes none of them and a release self-build is most of the budget
# of a job on a two-core runner.
# ⚠️⚠️ REMOVE ANY mcpp ALREADY UNDER `target/` FIRST, so that after the
# build there is exactly ONE and it is the one this step produced.
#
# `target/` is a RESTORED CACHE whose key hashes `src/**`. A source
# change misses the key, `restore-keys` hands back the nearest older
# tree anyway — that is the point of the layered restore — and the
# build then writes its output into a NEW `<triple>/<fingerprint>/`
# directory beside the ones already there. `find … | head -1` returns
# whichever directory the walk reaches first, which is not the newest
# and is not this build's.
#
# ⚠️ MEASURED, LOCALLY AND ON CI. On this developer's own tree the
# command picks `007bc0f2b78f7fa5` while the build just wrote
# `148448dec6c4a7a7`. On CI it picked a stale binary whose ELF
# interpreter names a glibc payload the runner no longer has:
#
# …/bin/mcpp: cannot execute: required file not found (exit 127)
#
# — which reads as a broken build of THIS commit and is nothing of the
# kind. Two runs, same stale fingerprint, after seven green ones: the
# trigger is simply a source change large enough to leave two
# directories in the restored tree.
#
# ⭐ mtime CANNOT BE THE CRITERION HERE. A cache restore writes every
# file at extraction time, so "newest" is meaningless on exactly the
# tree where this goes wrong — `ls -t | head -1` has picked a stale
# fingerprint in this repository before. Absence can't be faked:
# delete them, and whatever exists afterwards was linked just now.
#
# The objects and BMIs stay cached — this costs one link.
find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) -delete 2>/dev/null || true
"$MCPP" build --dev
BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) | head -1)
[ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; }
# ⚠️ EXACTLY ONE, not "at least one". Two would mean the delete above
# missed a tree and the ambiguity this step exists to remove is back.
#
# ⚠️ NO `mapfile`. This job also runs on macos, whose `/bin/bash` is
# 3.2 and has no such builtin — a bashism here would fail on one row
# of the matrix for a reason unrelated to what the row tests.
find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) > /tmp/mcpp-built.txt
COUNT=$(wc -l < /tmp/mcpp-built.txt | tr -d ' ')
if [ "$COUNT" -ne 1 ]; then
echo "::error::expected exactly one built mcpp, found $COUNT"
sed 's/^/ /' /tmp/mcpp-built.txt
exit 1
fi
BUILT=$(cat /tmp/mcpp-built.txt)
BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")
echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV"
"$BUILT" --version
Expand Down
94 changes: 94 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,100 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [2026.8.27.1] — 2026-08-27

目标侧被解析出来了,只发给了一个编译单元。完整分析见
[`.agents/docs/2026-08-27-openkal-native-path-three-issues.md`](.agents/docs/2026-08-27-openkal-native-path-three-issues.md)。

⭐ **与 `2026.8.25.x`/`2026.8.26.1` 是同一族的下一层。** 那两批修的是「谓词问错了」
与「答案没接到决定上」;这一批里,答案**接上了一个消费者,而它有五个**。

### 修复

- **⭐⭐ 编译侧的谓词,是 `2026.8.26.1` 在链接侧修掉的那条的孪生兄弟。**

`hostflags.cppm` 问的是 `!crossTargetFlag.empty()` ——「命令行上有没有
`--target=`」——而它的注释写的是「目标侧来自图」。同一台机器、同一个编译器、
同一个目标,只差写不写 `--target`,编译线少了**六个 token**:

```
--no-default-config -nostdinc++
-isystem <payload>/include/c++/v1
-isystem <payload>/include/<triple>/c++/v1
-isystem <glibc>/include
-isystem <linux-headers>/include
```

⇒ 头文件来自一个库,目标文件链自另一个库。两侧现在读同一个
`plan.targetSide.cAbi.prebuilt()`。⚠️ `--no-default-config` 从这个条件里
**拆了出来无条件发** —— 它不是载荷头文件集合的一部分,而 cfg 文件按
`post_install.cppm` 自己的说法是「per-machine, per-install-path artifact」。

⚠️ `e2e 295` 写的就是这条恒等式,而它只比对 `^ldflags`,所以恒等式在**下一行**
不成立而测试看不见。现在两条都比。

- **⭐⭐ 载荷目录名是 LLVM 词汇,而查找用的是 mcpp 词汇 —— 而且失配是静默的。**

`include/<triple>/c++/v1` 与 `lib/<triple>` 由 LLVM 的构建写下,带的是
`x86_64-unknown-linux-gnu`;`tc.targetTriple` 是 mcpp 的
`x86_64-linux-gnu`。两者只在三元组是**探测来的**时候恰好相同。查找是
`if (exists) push_back`,所以找不到就什么也不发生 —— 而那个目录里只有一个文件,
`__config_site`,它的缺席产生的报错读起来像载荷坏了。两种拼法现在都试。

- **⭐⭐ 由图供给的目标侧,只到达了一个编译单元(mcpp#514 §A)。**

提供 `mcpp:` 层的包发布的是**整个目标**编译时所依据的头文件集合,而它今天以
`publicUsage` 的形态**沿依赖边**传播。于是根与 provider 自己的单元拿得到,而
**兄弟依赖包**拿不到 —— `nlohmann.json` 不在 `openkal-llvm-runtime` 的下游,
它在它旁边。结果是一次构建里两种口味的 BMI,任何同时导入两者的 TU 在第一个
模板实例化处炸掉(`reference to 'space' is ambiguous`)。

⭐ 目标侧解析之后,`fromGraph()` 的层的 `publicUsage` 并入**每一个**包的
`privateBuild`;`std` 模块的命令行也改读同一个集合,不再自己推一遍。

- **⭐⭐ 缓存键描述了编译器,没有描述它被指向的头文件集合(mcpp#514 §B)。**

A 轴上的每一项都在描述**编译器**,没有一项描述它编译时所依据的**库** ——
而两者是分开安装的。`driverIdentity` 按设计也覆盖不了它:
`normalize_driver_output` **故意**抹掉路径,好让一个条目能被两个 home 共享。
新增 `targetHeaderSet` 轴,取自已经解析好的 `linkmodel`,并**分两档相对化**:
`<store>` 与 `<home>`。⚠️ 只做 `<store>` 一档不够 —— 最常见的那台机器走
`CLibMode::Sysroot`,它唯一的编译 token 是 `--sysroot=<home>/registry/subos/default`,
在 HOME 底下而不在 store 底下,于是每个条目都会带上这台机器的 home。分得开什么:
两个载荷(路径里带版本号)⇒ 两个键;一个 home 下的两个 subos ⇒ 两个键;
两个 home 下同名的 subos ⇒ 仍是一个键(由整工程指纹的第 11 项区分)。
⚠️ 不 bump `kCacheEpoch` —— 旧条目是 miss 而不是不可用。

- **⭐⭐ 请求的版本和载荷目录的版本是两套词汇,而每个查找都按请求那套拼。**

RuntimeBinding 带的是**声明的**版本(`glibc@2.44`),而 xlings 把载荷目录按这个
请求**解析成**的版本命名(`2.44.2`)。索引在同一序列内挪动一次包,所有按声明版本
拼目录名的查找就同时失效 —— 在每一台**新**机器上,在任何已存在的机器上都不出现。

两个查找点各自拼过一遍,所以修好一个还剩另一个,而第二处的失败**根本不提版本**:
glibc 的 include 目录只是没被加上,用户读到的是 libstdc++ 头文件里的
`features.h: No such file`。⇒ 收敛到一个 `payload_dir_for_version`。

⭐ **判据是「精化」,不是「按目录序挑一个」**:`2.44.2` 的版本**分量**以请求的分量
开头。`2.4` 回答不了 `2.44`(逐分量比,不是逐字符)。两个载荷都精化同一个请求时
返回**空** —— 「这个请求的解析结果」得是唯一一个才配叫答案,而按目录序挑正是这里
每个调用者都拒绝做的猜测。反方向(拿更旧的载荷回答更新的请求)不接受。

- **⭐ home 发现有第四份拷贝,而且会伸到别的 home 里去。**

`active_home_xpkgs()` 自己重推了一遍 home(漏掉自包含安装那一档);
`find_sibling_package` 找不到时**无条件回落** `~/.xlings/data/xpkgs`。
后者意味着一次密闭构建可以从**另一棵树**取载荷,而结果直接进每条编译命令的
`-isystem`。前者改为 `mcpp::home::root()`,后者删除 —— 找不到会说话,找错了不会。

### 新增

- **`[build] private_include_dirs`** —— 指出 `include_dirs` 中在本包边界处停住的
条目。`publicUsage` 此前整份接过 `privateBuild` 的目录,于是一个内嵌了带内部头
覆盖层的库(musl 的 `src/include` 定义 `hidden`/`weak`/`weak_alias`)会把那些宏
发给每一个消费者。⚠️ 它是 `include_dirs` 的**子集**而不是第二个列表:两类目录的
相对顺序是承重的,而两个 TOML 数组表达不了一个顺序。

## [2026.8.26.2] — 2026-08-26

已经解析出的答案,没有被用来做决定。完整分析见
Expand Down
39 changes: 39 additions & 0 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ module_extensions = [".ixx"] # Extra extensions used by module INTERFACES (
build_program_timeout = 1800 # Seconds a build.mcpp may run; 0 = no limit (§ below)
include_dirs = ["include", "third_party/include"] # Header search paths
include_dirs_after = ["*"] # Header dirs searched AFTER system dirs (-idirafter)
private_include_dirs = ["vendor/src/include"] # Of `include_dirs`, the ones a consumer must NOT get
c_standard = "c11" # Standard for C source files (default c11)
cflags = ["-DFOO=1"] # Extra C compile flags
cxxflags = ["-DBAR=2"] # Extra C++ compile flags (do not put -std=... here)
Expand All @@ -190,6 +191,44 @@ jobs = "auto" # Concurrent compiles: a positive number, or "
bmi_schedule = "auto" # Module-edge scheduling: auto (= off) | on | off (§ below)
```

`private_include_dirs` names the entries **of `include_dirs`** that stop at this
package's own boundary: this package compiles with them, and a consumer never
receives them.

Almost every package publishes exactly the set it is built from, which is why
`include_dirs` alone was enough for a long time. The shape where the two differ
is a package that vendors a library with an *internal header overlay*. musl
reaches its own declarations through `src/include`, whose headers define
`hidden`, `weak` and `weak_alias` — names that mean something only to musl's own
sources. Publishing that directory hands those macros to every consumer, and a
consumer that uses `hidden` as an ordinary identifier stops compiling for a
reason it has no way to see.

```toml
[build]
# ⚠️ The relative ORDER of the two kinds is load-bearing: the internal overlay
# must precede the public headers for this package's own build. That is why
# this is a SUBSET of `include_dirs` rather than a second list — two arrays
# cannot express one order.
include_dirs = ["port/include", "musl/src/include", "musl/include"]
private_include_dirs = ["musl/src/include"]
```

Entries take the same `*` glob convention as `include_dirs`, and are matched
after expansion — so a glob may name exactly the directories it expands to. An
entry that is not among this package's `include_dirs` withholds nothing and is
reported as such rather than passing in silence.

**On an older engine the key is ignored, never fatal.** Measured on 2026.8.26.2:
in a dependency's manifest it is accepted silently, and in a root manifest it
warns — `[build] has unsupported key 'private_include_dirs' (ignored)` — and the
build continues. So a package may adopt the key without waiting for its
consumers to upgrade; those on an older engine simply keep receiving the
directory as they did before. The one place this does **not** hold is a
published `xim` descriptor's `target_cfg` block, where an unrecognised sub-key
is a hard error that fails the whole manifest — do not put this key there until
the index floor names an engine that knows it.

`include_dirs_after` (#249) lists header directories that are searched **after**
the toolchain's system directories (emitted as `-idirafter` on GCC/Clang, as
trailing `/I` under the MSVC dialect, and as plain `-I` for NASM
Expand Down
30 changes: 30 additions & 0 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ module_extensions = [".ixx"] # 模块**接口**额外使用的扩展名(见
build_program_timeout = 1800 # build.mcpp 的运行上限(秒);0 = 不限(见下节)
include_dirs = ["include", "third_party/include"] # 头文件搜索路径
include_dirs_after = ["*"] # 排在系统目录之后搜索的头文件目录(-idirafter)
private_include_dirs = ["vendor/src/include"] # `include_dirs` 中不发布给消费者的那些
c_standard = "c11" # C 源文件的标准(默认 c11)
cflags = ["-DFOO=1"] # 额外 C 编译参数
cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...)
Expand All @@ -178,6 +179,35 @@ jobs = "auto" # 并发编译数:正整数,或 "auto"(见下
bmi_schedule = "auto" # 模块边调度:auto(= 关)| on | off(见下节)
```

`private_include_dirs` 指出 **`include_dirs` 中**在本包边界处停住的那些条目:
本包用它们编译,消费者永远收不到。

绝大多数包发布的就是它编译时用的那一套,所以长期以来只有 `include_dirs` 就够了。
两者不同的形状只有一种 —— 一个包**内嵌了带内部头覆盖层的库**。musl 通过
`src/include` 到达它自己的声明,而那些头定义了 `hidden`、`weak`、`weak_alias`,
这些名字只对 musl 自己的源码有意义。把那个目录发布出去,等于把这些宏交给每一个
消费者;而一个把 `hidden` 当普通标识符用的消费者会编不过,且看不出原因。

```toml
[build]
# ⚠️ 两类目录的**相对顺序**是承重的:本包自己构建时,内部覆盖层必须排在公共头之前。
# 这正是它被设计成 `include_dirs` 的**子集**而不是第二个列表的原因 ——
# 两个数组表达不了一个顺序。
include_dirs = ["port/include", "musl/src/include", "musl/include"]
private_include_dirs = ["musl/src/include"]
```

条目支持与 `include_dirs` 相同的 `*` glob 约定,并在**展开之后**比对 ——
所以一个 glob 可以恰好指名它展开出的那些目录。若某条目不在本包的 `include_dirs`
里,它什么也没扣下,mcpp 会把这件事说出来而不是让它悄悄通过。

**旧引擎会忽略这个键,而不会因此失败。** 在 2026.8.26.2 上实测:出现在依赖的清单里
时被静默接受;出现在根清单里时给一条警告 —— `[build] has unsupported key
'private_include_dirs' (ignored)` —— 构建照常继续。所以一个包可以先用上这个键,
不必等消费者升级;还在旧引擎上的消费者只是像以前一样继续收到那个目录。**唯一不成立
的地方**是已发布的 `xim` 描述符的 `target_cfg` 块:那里不认识的子键是硬错误,会让
整份清单加载失败 —— 在索引下限指向认识它的引擎之前,不要把这个键写进那里。

`include_dirs_after`(#249)列出**排在工具链系统目录之后**搜索的头文件目录
(GCC/Clang 发射为 `-idirafter`;MSVC 方言退化为排在末尾的 `/I`,NASM 汇编
单元退化为普通 `-I`——两者都没有对应 flag,也都没有需要保护的系统头搜索链)。当目录是解压后的源码 tarball 根目录、且其中的文件名会与标准头冲突时,
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "2026.8.26.2"
version = "2026.8.27.1"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
9 changes: 9 additions & 0 deletions src/build/build_program.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,15 @@ std::vector<std::string> host_base_flags(const mcpp::toolchain::Toolchain& tc,
// undefined __cxa_* / __gxx_personality_v0.
opt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::LinuxOnly;
opt.clangStdlibSelect = true;
// ⭐ `cAbiPrebuilt` is left at its default (true), and that is a statement
// rather than an omission: `tc` here is always HOST-targeting (see this
// function's header), so the helper's C library is the payload's whatever
// the project's target side turns out to be.
//
// ⚠️ It also corrects a latent defect. The predicate this replaced was
// `!tc.crossTargetFlag.empty()`, and a host toolchain resolved for a cross
// build could carry one — in which case the helper lost the payload's own
// headers for a reason that had nothing to do with it.
// binutils -B so the driver finds ld/as (GCC; musl and MinGW ship their own).
opt.binutilsPrefix = !mcpp::toolchain::is_musl_target(tc)
&& !mcpp::toolchain::is_mingw_target(tc);
Expand Down
Loading
Loading