Skip to content

Commit b96b61c

Browse files
feat(run): --features 与 --profile,以及那个不看 feature 的构建缓存条目 (#552)
* feat(run): --features and --profile, and the cache entry that ignored them ## The one scenario the design could not run `mcpp build` and `mcpp test` have always taken `--features` and `--profile`. `mcpp run` did not, so it could only execute whatever a previous `build` happened to leave behind: there was no spelling of `mcpp run` that ran a release artefact, or one built with a feature on. That is the axis the whole device surface is built around. A board-support package expresses its two environments — an emulator, a debug probe — AS features, so mcpp run --features hardware is the command a developer types when the board arrives on their desk. It did not exist, which made the design's own scenario a manifest edit rather than a command. `--release` and `--dev` come along as the shorthands the other verbs accept, and `--list-runners` takes the same axes: which runners a project HAS depends on the features, so reporting them without it answers a question nobody asked. ## ⚠️⚠️ And the pre-existing defect it uncovered The build cache entry is keyed on (target, profile, cache mode). The OUTPUT DIRECTORY is keyed on a fingerprint that includes the features. So an entry written by `mcpp build --features loud` pointed at the loud directory, and the next plain `mcpp build` matched it and reported success in 0.00s — serving a featured artefact to a request that had none. Measured: three consecutive builds of one project printed `quiet`, `LOUD`, `LOUD`. The third had no feature on. The entry now records its feature set, normalised so `a,b` and `b a` are one request, and both fast paths compare it. Absent on older caches, which reads as "no features" — correct for a request that has none, a miss otherwise, which is the safe direction. ⭐ The run fast path additionally declines whenever either new axis is set, for the reason `--cache` and `--profile` already bypass `cmd_build`'s: the cached graph was generated without them, and accepting a flag to ignore it is worse than refusing it. `tests/e2e/337` covers both halves and fails without this change. 97/97 unit; e2e 87, 88, 130-131, 327, 332-337 green. * test(cortex-m): 332 reads the exit status, and takes it from the emulator ⚠️⚠️ EVERY ASSERTION IN 332 WAS ABOUT WHAT THE IMAGE PRINTED, AND THAT IS NOT ENOUGH ON THIS ARCHITECTURE. `SYS_EXIT` (0x18) takes its reason code in r1 DIRECTLY; the `{reason, code}` block is `SYS_EXIT_EXTENDED` (0x20), which exists because a 32-bit r1 cannot carry both a reason and a status. A board that confuses them prints every expected line and then reports the WRONG status. Measured in mcpplibs/openarch while this branch was open: an example printed `both tasks observed preemption` and `mcpp run` exited 1. Every check on its output passed. ⚠️ And the status has to come from the emulator rather than from the tail of a pipeline: `$?` after `qemu | head` is head's, which is always 0 — the shape that would have made this assertion vacuous in exactly the case it is for. docs/13 records it as an AArch32 fact rather than an A-profile one, and names both measurements. e2e 332 green: four rows boot AND exit 0. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent ece01a2 commit b96b61c

11 files changed

Lines changed: 299 additions & 26 deletions

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ M-profile:MPU 按基址与上限描述区域,没有页表项。A-profile 有真
106106
而不是它要陈述的性质。新行加进来时规则适用而测试**静默跳过**了它们,每条断言依然通过。
107107
谓词已改为「32 位 ARM」。
108108

109+
### `mcpp run` 接受 `--features``--profile`
110+
111+
`build``test` 一直有这两条轴,`run` 没有 —— 于是 `run` **只能执行上一次 `build`
112+
恰好留下的东西**:没有任何一种写法能跑一个 release 产物,或一个开了 feature 的产物。
113+
114+
⚠️ 而这正是整个设备面赖以成立的形状:板级包把「模拟器」与「真板」表达成 feature,
115+
所以 `mcpp run --features hardware` 才是板子到手那天开发者敲的命令。**方案里唯一一个
116+
自己跑不起来的场景就是它。**
117+
118+
### ⚠️⚠️ 顺带修掉一个既有缺陷:构建缓存不看 feature
119+
120+
缓存条目按 (target, profile, cache mode) 索引,而**输出目录按含 feature 的指纹索引**
121+
于是 `mcpp build --features loud` 写下的条目指向 loud 的目录,下一次**不带 feature 的**
122+
`mcpp build` 命中它、0.00s 报成功,**把带 feature 的产物交给一个没要 feature 的请求**
123+
124+
实测(修复前):同一个工程连续三次构建打印 `quiet``LOUD``LOUD`
125+
126+
条目现在记录它的 feature 集合(归一化,`a,b``b a` 同一);两条快路径都比对它。
127+
109128
### 发现性
110129

111130
`mcpp why runners` 列出本工程提供的 runner,与其余解析结果并列;

docs/13-baremetal.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,18 @@ asked what a *32-bit* machine's entry looks like — short descriptors are 32 bi
4343
wide, long (LPAE) ones 64. That question cannot be put to a machine with no
4444
entries, which is why `openarch`'s Cortex-M backend declines the capability.
4545

46-
**The semihosting exit call is not spelled the way M-profile spells it.**
47-
`SYS_EXIT` (`0x18`) on AArch32 takes the reason code in `r1` *directly*; the
48-
`{reason, code}` block a Cortex-M board passes is `SYS_EXIT_EXTENDED` (`0x20`),
49-
which exists because a 32-bit `r1` cannot carry both. Measured: passing the
50-
block to `0x18` prints correctly and then reports the wrong exit status, so a
51-
board that only checks its output cannot see the difference. This is a *board*
52-
fact rather than a target fact; it is recorded here because it is where the next
53-
person writing such a board will look.
46+
**The semihosting exit call has two spellings and only one of them carries a
47+
status.** `SYS_EXIT` (`0x18`) takes its reason code in `r1` *directly*; the
48+
`{reason, code}` block is `SYS_EXIT_EXTENDED` (`0x20`), which exists because a
49+
32-bit `r1` cannot carry both a reason and a status. Passing the block to `0x18`
50+
prints everything correctly and then reports the **wrong** exit status.
51+
52+
⚠️ This is an *AArch32* fact and applies to M-profile as much as to A-profile.
53+
Measured twice: an ARMv7-A image exiting 0 reported 1, and an `openarch`
54+
Cortex-M example printed `both tasks observed preemption` and exited 1 — every
55+
assertion on its output passed. A board that only checks what it printed cannot
56+
see the difference, which is why `tests/e2e/332` and `336` both read `$?`, and
57+
both take it from the emulator rather than from the tail of a pipeline.
5458

5559
### M-profile is seven rows rather than one
5660

docs/18-devices.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ mcpp run # the default runner
2121
mcpp run --runner flash # a named one
2222
mcpp run --list-runners # what this project supplies
2323
mcpp why runners # the same list, beside everything else resolved
24+
mcpp run --features hardware # the same board, reached the other way
2425
```
2526

27+
`mcpp run` takes `--features` and `--profile`, the same axes `mcpp build` and
28+
`mcpp test` take. That is what makes the line above a command rather than a
29+
manifest edit: a board package expresses its two environments as features, so
30+
selecting one is the same act as selecting any other feature.
31+
2632
`mcpp run` is the whole of the common case, including on real hardware. On a
2733
device, running a program means writing it, resetting, attaching to its output
2834
and reading its exit status — which is one command (`probe-rs run`, `qemu-system-*

docs/zh/13-baremetal.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,16 @@ A-profile 有真正的 MMU 与页表走查器,所以它是第一个能被问「*
3939
什么样」的目标 —— 短描述符 32 位宽,长描述符(LPAE)64 位。这个问题无法向一台没有
4040
页表项的机器提出,这正是 `openarch` 的 Cortex-M 后端不声明该能力的原因。
4141

42-
**半主机的退出调用与 M-profile 的拼法不同。** AArch32 的 `SYS_EXIT`(`0x18`)把
43-
原因码**直接**放在 `r1` 里;Cortex-M 板级代码传的那个 `{reason, code}` 块是
44-
`SYS_EXIT_EXTENDED`(`0x20`),它存在的理由正是 32 位的 `r1` 装不下两者。实测:把
45-
块传给 `0x18`,打印正确而**退出状态是错的** —— 只看输出的板子看不出这个差别。这是
46-
****的事实而不是目标的事实,记在这里是因为下一个写这种板的人会来这里找。
42+
**半主机的退出调用有两种拼法,只有一种带得走状态。** `SYS_EXIT`(`0x18`)把原因码
43+
**直接**放在 `r1`;`{reason, code}` 块是 `SYS_EXIT_EXTENDED`(`0x20`),它存在的理由
44+
正是 32 位的 `r1` 装不下「原因」与「状态」两者。把块传给 `0x18`,一切打印都正确,而
45+
**退出状态是错的**
46+
47+
⚠️ 这是 **AArch32** 的事实,对 M-profile 与 A-profile 同样成立。实测两次:一个
48+
ARMv7-A 程序退 0 而报回 1;`openarch` 的一个 Cortex-M 示例打印了
49+
`both tasks observed preemption` 然后退 1 —— 对**输出**的每一条断言都通过了。只看
50+
自己打印了什么的板子看不出这个差别,所以 `tests/e2e/332``336` 都读 `$?`,而且都
51+
从模拟器本身取,不从管道末端取。
4752

4853
### M-profile 是七行而不是一行
4954

docs/zh/18-devices.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ mcpp run # 默认 runner
1616
mcpp run --runner flash # 具名的
1717
mcpp run --list-runners # 这个工程提供了哪些
1818
mcpp why runners # 同一份清单,与其余解析结果并列
19+
mcpp run --features hardware # 同一块板,换一条路到达
1920
```
2021

22+
`mcpp run` 接受 `--features``--profile`,与 `mcpp build``mcpp test` 同轴。
23+
上面那一行因此是一条**命令**而不是一次清单改动:板级包把两种环境表达成 feature,
24+
选其中之一与选任何别的 feature 是同一个动作。
25+
2126
**`mcpp run` 覆盖了常见情形的全部,真实硬件也一样。** 在设备上,「运行一个程序」
2227
意味着写进去、复位、接上它的输出、读回退出状态 —— 这是**一条**命令
2328
(`probe-rs run``qemu-system-* -kernel`),不是几条。因此板级包把它作为**默认**

src/build/execute.cppm

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,22 @@ struct BuildCacheEntry {
139139
// every entry such a cache could hold, because no manifest could express
140140
// the tier.
141141
bool runTierPending = false;
142+
// ⚠️⚠️ THE FEATURE SET THIS ENTRY'S ARTEFACTS WERE BUILT WITH.
143+
//
144+
// The entry is keyed on (target, profile, cache mode) and was matched on
145+
// those three alone, while the OUTPUT DIRECTORY is keyed on a fingerprint
146+
// that includes the features. So `mcpp build --features loud` wrote an
147+
// entry pointing at the loud output directory, and the next plain
148+
// `mcpp build` matched it and reported success in 0.00s — serving the
149+
// featured artefact to a request that asked for none.
150+
//
151+
// Measured before this field existed: three builds of one project printed
152+
// `quiet`, `LOUD`, `LOUD`. The third had no feature on.
153+
//
154+
// Absent on caches written before the field: empty, which reads as "no
155+
// features" — correct for every entry such a cache could hold whose
156+
// request also has none, and a miss otherwise, which is the safe direction.
157+
std::string features;
142158
};
143159

144160
std::vector<BuildCacheEntry> read_build_cache(const std::filesystem::path& projectRoot) {
@@ -254,6 +270,11 @@ std::vector<BuildCacheEntry> read_build_cache(const std::filesystem::path& proje
254270
e.runTierPending = (line.substr(8) == "1");
255271
haveNextLine = static_cast<bool>(std::getline(f, line));
256272
}
273+
// Optional `features=<list>`. Absent ⇒ empty; see the field.
274+
if (haveNextLine && line.starts_with("features=")) {
275+
e.features = line.substr(9);
276+
haveNextLine = static_cast<bool>(std::getline(f, line));
277+
}
257278
entries.push_back(std::move(e));
258279
if (!haveNextLine || line.empty()) break;
259280
}
@@ -265,6 +286,25 @@ std::vector<BuildCacheEntry> read_build_cache(const std::filesystem::path& proje
265286
void write_build_cache_entries(const std::filesystem::path& path,
266287
const std::vector<BuildCacheEntry>& entries);
267288

289+
// `a, b` and `b a` are one request. Normalised on both sides of the comparison
290+
// — the entry stores this form and the fast path computes it — so a cache hit
291+
// depends on the SET rather than on how it was typed.
292+
std::string normalize_features(std::string_view raw) {
293+
std::vector<std::string> toks;
294+
for (std::size_t i = 0; i < raw.size();) {
295+
auto c = raw.find_first_of(", ", i);
296+
auto t = raw.substr(i, c == std::string_view::npos ? c : c - i);
297+
if (!t.empty()) toks.emplace_back(t);
298+
if (c == std::string_view::npos) break;
299+
i = c + 1;
300+
}
301+
std::ranges::sort(toks);
302+
toks.erase(std::unique(toks.begin(), toks.end()), toks.end());
303+
std::string out;
304+
for (auto const& t : toks) { if (!out.empty()) out += ','; out += t; }
305+
return out;
306+
}
307+
268308
void write_build_cache(const std::filesystem::path& projectRoot,
269309
const std::filesystem::path& outputDir,
270310
const std::string& ninjaProgram,
@@ -280,7 +320,8 @@ void write_build_cache(const std::filesystem::path& projectRoot,
280320
const mcpp::platform::runtime::RuntimeBinding& runtimeBinding = {},
281321
std::vector<std::string> depSourceRoots = {},
282322
bool runnerDeclared = false,
283-
bool runTierPending = false) {
323+
bool runTierPending = false,
324+
const std::string& features = {}) {
284325
auto path = projectRoot / kBuildCacheFile;
285326
auto entries = read_build_cache(projectRoot);
286327

@@ -303,6 +344,7 @@ void write_build_cache(const std::filesystem::path& projectRoot,
303344
newEntry.depSourceRootsRecorded = true;
304345
newEntry.runnerDeclared = runnerDeclared;
305346
newEntry.runTierPending = runTierPending;
347+
newEntry.features = features;
306348
entries.insert(entries.begin(), std::move(newEntry));
307349

308350
// Trim to LRU capacity.
@@ -345,6 +387,7 @@ void write_build_cache_entries(const std::filesystem::path& path,
345387
for (auto& r : e.depSourceRoots) f << r << '\n';
346388
f << "runner=" << (e.runnerDeclared ? 1 : 0) << '\n';
347389
f << "runtier=" << (e.runTierPending ? 1 : 0) << '\n';
390+
f << "features=" << e.features << '\n';
348391
}
349392
}
350393

@@ -764,7 +807,12 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache,
764807
!choose_runner(ctx).tmpl.empty(),
765808
// …and one written by a build that left a run-tier
766809
// tool uninstalled, for the same reason.
767-
ctx.runTierPending);
810+
ctx.runTierPending,
811+
// The feature set these artefacts were built with:
812+
// the entry is matched on it, because the output
813+
// directory is keyed on a fingerprint that includes
814+
// it and the entry was not.
815+
normalize_features(ctx.activeFeatureRequest));
768816
}
769817

770818
// The one place the --strict policy is settled. Degradations reported by
@@ -1013,11 +1061,15 @@ struct FastPathIdentity {
10131061
// the same single manifest read, and the only one that can VETO the fast
10141062
// path rather than describe it — see try_fast_build.
10151063
bool hooksActive = false;
1064+
// What `--features` asked for, normalised so that spelling and order
1065+
// cannot make two identical requests compare unequal.
1066+
std::string features;
10161067
};
10171068

10181069
std::optional<FastPathIdentity>
10191070
fast_path_identity(const std::filesystem::path& projectRoot,
1020-
std::string_view profileOverride = "") {
1071+
std::string_view profileOverride = "",
1072+
std::string_view featuresRequested = "") {
10211073
auto m = mcpp::manifest::load(projectRoot / "mcpp.toml");
10221074
if (!m) return std::nullopt;
10231075
return FastPathIdentity{
@@ -1028,6 +1080,7 @@ fast_path_identity(const std::filesystem::path& projectRoot,
10281080
mcpp::extension_table_for(m->buildConfig.moduleExtensions),
10291081
m->buildConfig.target,
10301082
m->hooks.active(),
1083+
normalize_features(featuresRequested),
10311084
};
10321085
}
10331086

@@ -1040,11 +1093,19 @@ fast_path_identity(const std::filesystem::path& projectRoot,
10401093
// what THIS graph supplies, which is knowable only after resolution.
10411094
export int list_runners(const std::string& package_filter,
10421095
const std::string& cache_mode, bool no_cache,
1043-
const std::string& target_triple) {
1096+
const std::string& target_triple,
1097+
// Which runners exist DEPENDS on the features: a board
1098+
// package supplies a different set for an emulator and
1099+
// for a probe. Reporting them without the axis that
1100+
// selects them would answer a question nobody asked.
1101+
const std::string& features = {},
1102+
const std::string& profile = {}) {
10441103
mcpp::build::BuildOverrides ov;
10451104
ov.package_filter = package_filter;
10461105
ov.cache_mode = no_cache ? std::string("off") : cache_mode;
10471106
ov.target_triple = target_triple;
1107+
ov.features = features;
1108+
ov.profile = profile;
10481109
// Reporting what `mcpp run` would do means resolving what `mcpp run`
10491110
// resolves, tool tiers included — otherwise this command would list a
10501111
// runner whose program it had declined to install.
@@ -1113,7 +1174,7 @@ export std::optional<int> try_fast_build(const std::filesystem::path& projectRoo
11131174
const BuildCacheEntry* match = nullptr;
11141175
for (auto& e : entries) {
11151176
if (e.targetTriple == currentTarget && e.profile == want->profile
1116-
&& e.cacheMode == want->cacheMode) {
1177+
&& e.cacheMode == want->cacheMode && e.features == want->features) {
11171178
match = &e;
11181179
break;
11191180
}
@@ -1245,7 +1306,7 @@ std::optional<int> try_fast_run(const std::filesystem::path& projectRoot,
12451306
const BuildCacheEntry* match = nullptr;
12461307
for (auto& e : entries) {
12471308
if (e.targetTriple.empty() && e.profile == want->profile
1248-
&& e.cacheMode == want->cacheMode) {
1309+
&& e.cacheMode == want->cacheMode && e.features == want->features) {
12491310
match = &e;
12501311
break;
12511312
}
@@ -1407,7 +1468,24 @@ export int build_run_target(const std::optional<std::string>& targetName,
14071468
// Empty is the default runner — `mcpp run`. Any
14081469
// other value came from `--runner <name>` and the
14091470
// engine has never seen it before.
1410-
std::string_view runner_name = {}) {
1471+
std::string_view runner_name = {},
1472+
// ⭐⭐ THE TWO AXES `build` AND `test` HAVE ALWAYS
1473+
// TAKEN, AND `run` DID NOT.
1474+
//
1475+
// Both change WHAT IS BUILT, so a `run` that could
1476+
// not express them could only ever execute whatever
1477+
// a previous `build` happened to leave behind — and
1478+
// there is no spelling of `mcpp run` that runs a
1479+
// release artefact, or one built with a feature on.
1480+
//
1481+
// It is the shape the whole device surface is built
1482+
// around: a board package expresses "emulator" and
1483+
// "hardware" as features, so `mcpp run --features
1484+
// hardware` is the command a developer types when
1485+
// the board arrives. Without this it was the one
1486+
// scenario the design's own example could not run.
1487+
const std::string& features = {},
1488+
const std::string& profile = {}) {
14111489
// mcpp#225 (E2): reuse the resolved build cache when it's still fresh,
14121490
// skipping prepare_build's toolchain resolution + modgraph scan
14131491
// entirely — mirrors cmd_build's try_fast_build fast path. The cached
@@ -1424,6 +1502,11 @@ export int build_run_target(const std::optional<std::string>& targetName,
14241502
// manifest to print the note against.
14251503
if (package_filter.empty() && cache_mode.empty() && !no_cache
14261504
&& target_triple.empty() && !no_runner
1505+
// ⚠️ AND NEITHER NEW AXIS IS SET. The cached entry was written for
1506+
// whichever feature set and profile the last build used; taking it
1507+
// here would silently ignore the flag, which is the same reason
1508+
// `--cache` and `--profile` bypass it in `cmd_build`.
1509+
&& features.empty() && profile.empty()
14271510
// ⚠️⚠️ THE FAST PATH IS `run`'s, AND ONLY `run`'s.
14281511
//
14291512
// It exec's the cached artefact directly — that IS its definition — so
@@ -1448,6 +1531,8 @@ export int build_run_target(const std::optional<std::string>& targetName,
14481531
ov.package_filter = package_filter;
14491532
ov.cache_mode = cache_mode;
14501533
ov.target_triple = target_triple;
1534+
ov.features = features;
1535+
ov.profile = profile;
14511536
// This verb executes what it builds, so the `when = "run"` tool tier is
14521537
// part of what has to exist. `mcpp build` does not set it, which is the
14531538
// whole of the difference the tier buys.

src/build/prepare.cppm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,11 @@ export struct BuildContext {
677677
// build cache records it so `mcpp run`'s fast path declines an entry a
678678
// plain `mcpp build` wrote — see BuildCacheEntry::runTierPending.
679679
bool runTierPending = false;
680+
// What `--features` asked for, verbatim. Carried so the build cache entry
681+
// can record the set its artefacts were built with — the output directory
682+
// is keyed on a fingerprint that includes the features and the entry was
683+
// not, which let a plain build serve a featured artefact.
684+
std::string activeFeatureRequest;
680685
std::filesystem::path outputDir;
681686
std::filesystem::path stdBmi;
682687
std::filesystem::path stdObject;
@@ -8828,6 +8833,7 @@ prepare_build(bool print_fingerprint,
88288833
ctx.runtimeSelection = runtimeSelection;
88298834
ctx.runtimeBinding = runtimeBindingSnapshot;
88308835
ctx.profile = effectiveProfile;
8836+
ctx.activeFeatureRequest = overrides.features;
88318837
ctx.compilerChoice = { std::string(tc_origin_name(tcOrigin)),
88328838
graphCompilerRequiredBy,
88338839
graphCompilerReplaced.empty() ? pinReplacedDefault

0 commit comments

Comments
 (0)