Skip to content

Commit 5de0959

Browse files
committed
Bump the build-cache epoch: a corrected key cannot tell a poisoned entry
from a trustworthy one The previous commit made fill_package_config read the realised environment, but a corrected derivation does not make an entry written under the old, wrong one safe to keep. An entry is poisoned exactly when its recorded key and its actual compiled inputs already disagreed at write time, and the package most likely to still produce an unchanged key after the fix is the one this same PR newly exempts from the realisation: a kernel-abi provider inferred into c-environment = "platform" now has an empty privateBuild.cflags, so its new key is computed from nothing -- matching its old key, which was also computed from nothing, while the object on disk was compiled WITH the substitution. No cheaper mechanism distinguishes a pre-fix entry from a post-fix one, so kCacheEpoch moves (2 -> 3): every existing ~/.mcpp/build-cache/v1 entry is orphaned unconditionally, one cold rebuild for everyone, rather than trust a key equality that is wrong for exactly the entries that matter most. Also adds CacheKey.EveryPrivateBuildBroadcastFieldReachesTheKey, a durable guard for the class of defect rather than only this one instance of it: PackageRoot::privateBuild (UsageRequirements) is the engine's own broadcast channel, and every one of its members -- today includeDirs, includeDirsAfter, cflags, cxxflags, asmflags, ldflags, modules -- has to move the cache key, because the key's only job is to describe what reaches the compiler. This predates the c-abi wave (targetSideUsage's own broadcast, and __openkal__, had the identical exposure before [c-abi] existed); the two fields nothing broadcasts into yet (ldflags, modules) are asserted uncovered on purpose, with an explicit instruction to flip that assertion in the same change that adds their broadcast.
1 parent 0e6c8e6 commit 5de0959

5 files changed

Lines changed: 126 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,18 @@
4747
环境构建的镜像,一个镜像混两种 C 环境且没有任何诊断,这正是本设计要防止的那个不变量本身。
4848
现在 `fill_package_config` 把 `privateBuild.cflags`/`cxxflags`/新增的 `asmflags`(广播后的
4949
值)与包自身声明的标志一起折进键里,和它原本处理 include 目录的方式一致。
50+
**升级到这个版本后,第一次构建会是一次冷构建**:`kCacheEpoch` 从 2 提到了 3,已有的
51+
`~/.mcpp/build-cache/v1` 条目会被整体作废,而不是逐条判断哪些还能信。这不是求稳的富余
52+
动作——键改对了以后,恰恰是那些*不再*触发替换的包最危险:比如同一个 PR 里被推导进
53+
`c-environment = "platform"` 的 kernel-abi 包,它 broadcast 前后 `privateBuild.cflags`
54+
都是空的,新键和旧键因此照样相同,而旧键当初对应的目标文件,正是带着替换令牌编译出来
55+
的那一份——键从一开始就没描述对它,普通的输入变化检测看不出来,只有作废整个缓存才够。
5056
(`src/build/cache_key.cppm`,单测 `test_cache_key.cpp` 的
51-
`TwoDifferentRealisedCEnvironmentsDoNotShareASlot`)
57+
`TwoDifferentRealisedCEnvironmentsDoNotShareASlot` 与
58+
`EveryPrivateBuildBroadcastFieldReachesTheKey`——后者是给这一类缺陷立的长期防线:
59+
`privateBuild`(`UsageRequirements`)每加一个新的广播字段,都要在这个测试和
60+
`fill_package_config` 里同时补上一行,否则历史会重演,`-D__openkal__` 和
61+
`targetSideUsage` 自己的广播在这次修订之前就已经有过同样的缺口)
5262
- **`__CYGWIN__`/`__CYGWIN32__` 保持定义,经 openkal-musl 尖峰实验修订。** 最初的实现
5363
取消定义它们(理由是图里没有真正的 Cygwin 用户态)。第三方可移植代码里需要知道**目标文件
5464
格式**——不是 C 环境,也不是平台 API——的那部分,没有别的名字能指代「PE 格式 + 呈现

docs/22-target-side.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,21 @@ declared flags, exactly as it already did for include directories.
479479
`--cache=off`, or clearing the cache directory, was never a sign the key
480480
was RIGHT; both routes bypass it entirely.
481481

482+
**This release also bumps the cache's epoch, orphaning every existing
483+
entry — the first build after upgrading is cold.** A corrected key does not
484+
by itself make an entry written under the old, wrong derivation safe to
485+
keep: an entry is poisoned exactly when its recorded key and its actual
486+
compiled inputs already disagreed, and the package MOST likely to still
487+
show an unchanged key after the fix is the one this same revision newly
488+
exempts from the realisation (a `kernel-abi` provider inferred into
489+
`c-environment = "platform"`, above) — its `privateBuild.cflags` is now
490+
empty, so the new key is computed from nothing, matching the OLD key, which
491+
was also computed from nothing, while the object on disk was compiled WITH
492+
the substitution. No cheaper check tells a pre-fix entry from a post-fix
493+
one, so `mcpp.build.cache_key::kCacheEpoch` moves (2 → 3), which orphans
494+
the whole cache unconditionally rather than trust a key equality that
495+
cannot be trusted for exactly the entries that matter most.
496+
482497
**Store key — not yet closed.** A package whose *install hook* compiles a
483498
static library from source into the shared store is keyed by package and
484499
version, not by which environment it was built against — the same gap

docs/zh/22-target-side.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,16 @@ C 环境,而且没有任何诊断。`fill_package_config` 现在把 `PackageRoot
384384
include 目录的方式完全一致。`--cache=off`,或者干脆清空缓存目录,从来都不是键本身正确
385385
的信号——这两条路径都是绕开了这个键,而不是证明了它。
386386

387+
**这次发布还把缓存的 epoch 提了一版,让已有条目全部作废——升级后第一次构建会是冷构建。**
388+
键改对了,不代表用旧的、错误推导方式写下的条目就可以留着继续信:一个条目被污染,恰恰是
389+
因为它记录的键和它实际编译时的输入从一开始就对不上——而修好之后最可能仍然拿到不变的键
390+
的那个包,正好是这次修订里新推导进 `c-environment = "platform"` 的那一类(上文的
391+
kernel-abi 提供者):它的 `privateBuild.cflags` 现在是空的,新键因此是从空内容算出来的,
392+
跟旧键(同样是从空内容算出来的)一样;而磁盘上那份目标文件,却是带着替换令牌编译出来的。
393+
没有更便宜的办法能把修复前写下的条目和修复后写下的条目分开,所以
394+
`mcpp.build.cache_key::kCacheEpoch` 往上提了一版(2 → 3),让整个缓存无条件作废,而不是
395+
去相信一个恰恰在最要紧的那些条目上靠不住的键相等判断。
396+
387397
**存储键——尚未补上。** 安装钩子把从源码编译的静态库装进共享 store 时,键只含包名与版本,
388398
不含针对哪种环境编译——这与 [requires](#requires) 已经记录的 C++ 运行时选择缺口同形。用同样
389399
的方式补上(一条 `requires` 形状的环境声明,在解析时核对、不符即拒绝)已经设计好,但尚未

src/build/cache_key.cppm

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,26 @@ export namespace mcpp::build::cache_key {
7373
// not ask for. They would all miss anyway (probe_cached compares the REQUESTED
7474
// artifacts), but sharing a directory between two layouts makes `cache gc`'s
7575
// size accounting and `cache verify`'s output meaningless.
76-
inline constexpr int kCacheEpoch = 2;
76+
// 3 (design 2026-09-18, coordinator review): `fill_package_config` used to
77+
// read only a package's OWN declared cflags/cxxflags, never the engine
78+
// broadcast channel (`privateBuild`) the realised [c-abi] environment and
79+
// `-D__openkal__` actually travel through — so an entry written before this
80+
// fix can be WRONG in a way probe_cached's normal "did the inputs change"
81+
// check cannot see: the compiled object and the recorded key silently
82+
// disagreed about what was compiled from the moment they were written, not
83+
// from a change since. This is the one case in this axis's whole design
84+
// (see the header comment above `struct BuildAxes`) where a narrower fix
85+
// is not enough — the poisoned entries are not the ones whose inputs
86+
// changed, they are ENTRIES WHOSE KEY NEVER DESCRIBED THEM, and the package
87+
// most likely to still show the identical (wrong) key after the fix is
88+
// exactly the one whose `privateBuild.cflags` just emptied out from under
89+
// it (a `kernel-abi` provider inferred into `c-environment = "platform"` in
90+
// this same PR): its new key is computed from nothing, matching its old
91+
// key, which was ALSO computed from nothing, while the STORED OBJECT was
92+
// compiled WITH the substitution. Bumping this orphans the entire cache —
93+
// one cold rebuild — rather than trust a key equality that cannot tell a
94+
// pre-fix entry from a post-fix one.
95+
inline constexpr int kCacheEpoch = 3;
7796

7897
// Axes A/B/C — identical for every package in one build, computed once.
7998
struct BuildAxes {

tests/unit/test_cache_key.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,76 @@ TEST(CacheKey, TwoDifferentRealisedCEnvironmentsDoNotShareASlot) {
428428
EXPECT_NE(lp64Key, llp64Key);
429429
}
430430

431+
// A DURABLE GUARD FOR THE WHOLE CLASS THE PREVIOUS TEST FOUND ONE INSTANCE
432+
// OF — this predates the c-abi wave (`targetSideUsage`'s own broadcast, and
433+
// `-D__openkal__`, had the identical exposure before [c-abi] existed) and
434+
// will recur: `PackageRoot::privateBuild` (`mcpp.modgraph.scanner
435+
// ::UsageRequirements`) is the engine's OWN channel for "this reaches a
436+
// package's compile command line even though the package wrote nothing" —
437+
// today's members are `includeDirs`, `includeDirsAfter`, `cflags`,
438+
// `cxxflags`, `asmflags`, `ldflags`, `modules` — and every one of them has
439+
// to move `fill_package_config`'s output, because the cache key's only job
440+
// is to describe what actually reaches the compiler.
441+
//
442+
// C++ has no reflection this side of the standard that could enumerate
443+
// `UsageRequirements`'s members and fail this test automatically the day a
444+
// new one is added without a matching line below — so this is the nearest
445+
// substitute the language allows: one assertion per CURRENT member, so the
446+
// list itself is the checklist. Adding a member to `UsageRequirements`
447+
// without adding its case here and its read in `fill_package_config` is
448+
// exactly the shape of the defect `TwoDifferentRealisedCEnvironmentsDoNotSh
449+
// areASlot` above caught — if you are adding one, add it in both places in
450+
// the SAME change.
451+
//
452+
// `ldflags` and `modules` are asserted UNCOVERED, on purpose: nothing
453+
// broadcasts into `privateBuild.ldflags` or `.modules` today (checked by
454+
// grep across `src/` and `modules/` when this test was written), so there
455+
// is nothing yet for `fill_package_config` to be missing on those two — the
456+
// day something does broadcast into either, this pair of assertions must
457+
// flip (start reading it) at the same time the broadcast is added, not
458+
// after.
459+
TEST(CacheKey, EveryPrivateBuildBroadcastFieldReachesTheKey) {
460+
std::filesystem::path store = "/home/u/.mcpp/registry/data/xpkgs";
461+
auto keyFor = [&](auto mutate) {
462+
auto pkgRoot = rootAt(store / "p" / "1");
463+
pkgRoot.usageResolved = true;
464+
mutate(pkgRoot);
465+
ck::PackageAxes p;
466+
p.indexName = "p"; p.packageName = "p"; p.version = "1";
467+
ck::fill_package_config(p, pkgRoot, store);
468+
return ck::key_hex(axes(), p);
469+
};
470+
const auto baseline = keyFor([](auto&) {});
471+
472+
EXPECT_NE(baseline, keyFor([](auto& r) {
473+
r.privateBuild.includeDirs = {"/somewhere/broadcast-only-include"};
474+
})) << "privateBuild.includeDirs";
475+
EXPECT_NE(baseline, keyFor([](auto& r) {
476+
r.privateBuild.includeDirsAfter = {"/somewhere/broadcast-only-after"};
477+
})) << "privateBuild.includeDirsAfter";
478+
EXPECT_NE(baseline, keyFor([](auto& r) {
479+
r.privateBuild.cflags = {"-Dbroadcast_only_c"};
480+
})) << "privateBuild.cflags";
481+
EXPECT_NE(baseline, keyFor([](auto& r) {
482+
r.privateBuild.cxxflags = {"-Dbroadcast_only_cxx"};
483+
})) << "privateBuild.cxxflags";
484+
EXPECT_NE(baseline, keyFor([](auto& r) {
485+
r.privateBuild.asmflags = {"-Dbroadcast_only_asm"};
486+
})) << "privateBuild.asmflags";
487+
488+
// Documented gap, not an oversight — see the comment above.
489+
EXPECT_EQ(baseline, keyFor([](auto& r) {
490+
r.privateBuild.ldflags = {"-Wl,--broadcast-only"};
491+
})) << "privateBuild.ldflags is not yet a live broadcast channel; if "
492+
"this starts failing, something now writes it, and "
493+
"fill_package_config must be taught to read it in the SAME change "
494+
"that flips this expectation";
495+
EXPECT_EQ(baseline, keyFor([](auto& r) {
496+
r.privateBuild.modules = {"broadcast.only.module"};
497+
})) << "privateBuild.modules is not yet a live broadcast channel; see "
498+
"the ldflags case above for what to do when it becomes one";
499+
}
500+
431501
// THE HEADER SET THE DRIVER IS POINTED AT IS PART OF THE IDENTITY.
432502
//
433503
// Everything else on axis A describes the COMPILER. Nothing described the

0 commit comments

Comments
 (0)