Skip to content
Merged
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ M-profile:MPU 按基址与上限描述区域,没有页表项。A-profile 有真
而不是它要陈述的性质。新行加进来时规则适用而测试**静默跳过**了它们,每条断言依然通过。
谓词已改为「32 位 ARM」。

### ⭐ `mcpp run` 接受 `--features` 与 `--profile`

`build` 与 `test` 一直有这两条轴,`run` 没有 —— 于是 `run` **只能执行上一次 `build`
恰好留下的东西**:没有任何一种写法能跑一个 release 产物,或一个开了 feature 的产物。

⚠️ 而这正是整个设备面赖以成立的形状:板级包把「模拟器」与「真板」表达成 feature,
所以 `mcpp run --features hardware` 才是板子到手那天开发者敲的命令。**方案里唯一一个
自己跑不起来的场景就是它。**

### ⚠️⚠️ 顺带修掉一个既有缺陷:构建缓存不看 feature

缓存条目按 (target, profile, cache mode) 索引,而**输出目录按含 feature 的指纹索引**。
于是 `mcpp build --features loud` 写下的条目指向 loud 的目录,下一次**不带 feature 的**
`mcpp build` 命中它、0.00s 报成功,**把带 feature 的产物交给一个没要 feature 的请求**。

实测(修复前):同一个工程连续三次构建打印 `quiet`、`LOUD`、`LOUD`。

条目现在记录它的 feature 集合(归一化,`a,b` 与 `b a` 同一);两条快路径都比对它。

### 发现性

`mcpp why runners` 列出本工程提供的 runner,与其余解析结果并列;
Expand Down
20 changes: 12 additions & 8 deletions docs/13-baremetal.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ asked what a *32-bit* machine's entry looks like — short descriptors are 32 bi
wide, long (LPAE) ones 64. That question cannot be put to a machine with no
entries, which is why `openarch`'s Cortex-M backend declines the capability.

**The semihosting exit call is not spelled the way M-profile spells it.**
`SYS_EXIT` (`0x18`) on AArch32 takes the reason code in `r1` *directly*; the
`{reason, code}` block a Cortex-M board passes is `SYS_EXIT_EXTENDED` (`0x20`),
which exists because a 32-bit `r1` cannot carry both. Measured: passing the
block to `0x18` prints correctly and then reports the wrong exit status, so a
board that only checks its output cannot see the difference. This is a *board*
fact rather than a target fact; it is recorded here because it is where the next
person writing such a board will look.
**The semihosting exit call has two spellings and only one of them carries a
status.** `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. Passing the block to `0x18`
prints everything correctly and then reports the **wrong** exit status.

⚠️ This is an *AArch32* fact and applies to M-profile as much as to A-profile.
Measured twice: an ARMv7-A image exiting 0 reported 1, and an `openarch`
Cortex-M example printed `both tasks observed preemption` and exited 1 — every
assertion on its output passed. A board that only checks what it printed cannot
see the difference, which is why `tests/e2e/332` and `336` both read `$?`, and
both take it from the emulator rather than from the tail of a pipeline.

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

Expand Down
6 changes: 6 additions & 0 deletions docs/18-devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ mcpp run # the default runner
mcpp run --runner flash # a named one
mcpp run --list-runners # what this project supplies
mcpp why runners # the same list, beside everything else resolved
mcpp run --features hardware # the same board, reached the other way
```

`mcpp run` takes `--features` and `--profile`, the same axes `mcpp build` and
`mcpp test` take. That is what makes the line above a command rather than a
manifest edit: a board package expresses its two environments as features, so
selecting one is the same act as selecting any other feature.

`mcpp run` is the whole of the common case, including on real hardware. On a
device, running a program means writing it, resetting, attaching to its output
and reading its exit status — which is one command (`probe-rs run`, `qemu-system-*
Expand Down
15 changes: 10 additions & 5 deletions docs/zh/13-baremetal.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,16 @@ A-profile 有真正的 MMU 与页表走查器,所以它是第一个能被问「*
什么样」的目标 —— 短描述符 32 位宽,长描述符(LPAE)64 位。这个问题无法向一台没有
页表项的机器提出,这正是 `openarch` 的 Cortex-M 后端不声明该能力的原因。

**半主机的退出调用与 M-profile 的拼法不同。** AArch32 的 `SYS_EXIT`(`0x18`)把
原因码**直接**放在 `r1` 里;Cortex-M 板级代码传的那个 `{reason, code}` 块是
`SYS_EXIT_EXTENDED`(`0x20`),它存在的理由正是 32 位的 `r1` 装不下两者。实测:把
块传给 `0x18`,打印正确而**退出状态是错的** —— 只看输出的板子看不出这个差别。这是
**板**的事实而不是目标的事实,记在这里是因为下一个写这种板的人会来这里找。
**半主机的退出调用有两种拼法,只有一种带得走状态。** `SYS_EXIT`(`0x18`)把原因码
**直接**放在 `r1`;`{reason, code}` 块是 `SYS_EXIT_EXTENDED`(`0x20`),它存在的理由
正是 32 位的 `r1` 装不下「原因」与「状态」两者。把块传给 `0x18`,一切打印都正确,而
**退出状态是错的**。

⚠️ 这是 **AArch32** 的事实,对 M-profile 与 A-profile 同样成立。实测两次:一个
ARMv7-A 程序退 0 而报回 1;`openarch` 的一个 Cortex-M 示例打印了
`both tasks observed preemption` 然后退 1 —— 对**输出**的每一条断言都通过了。只看
自己打印了什么的板子看不出这个差别,所以 `tests/e2e/332` 与 `336` 都读 `$?`,而且都
从模拟器本身取,不从管道末端取。

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

Expand Down
5 changes: 5 additions & 0 deletions docs/zh/18-devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ mcpp run # 默认 runner
mcpp run --runner flash # 具名的
mcpp run --list-runners # 这个工程提供了哪些
mcpp why runners # 同一份清单,与其余解析结果并列
mcpp run --features hardware # 同一块板,换一条路到达
```

`mcpp run` 接受 `--features` 与 `--profile`,与 `mcpp build`、`mcpp test` 同轴。
上面那一行因此是一条**命令**而不是一次清单改动:板级包把两种环境表达成 feature,
选其中之一与选任何别的 feature 是同一个动作。

**`mcpp run` 覆盖了常见情形的全部,真实硬件也一样。** 在设备上,「运行一个程序」
意味着写进去、复位、接上它的输出、读回退出状态 —— 这是**一条**命令
(`probe-rs run`、`qemu-system-* -kernel`),不是几条。因此板级包把它作为**默认**
Expand Down
99 changes: 92 additions & 7 deletions src/build/execute.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ struct BuildCacheEntry {
// every entry such a cache could hold, because no manifest could express
// the tier.
bool runTierPending = false;
// ⚠️⚠️ THE FEATURE SET THIS ENTRY'S ARTEFACTS WERE BUILT WITH.
//
// The entry is keyed on (target, profile, cache mode) and was matched on
// those three alone, while the OUTPUT DIRECTORY is keyed on a fingerprint
// that includes the features. So `mcpp build --features loud` wrote an
// entry pointing at the loud output directory, and the next plain
// `mcpp build` matched it and reported success in 0.00s — serving the
// featured artefact to a request that asked for none.
//
// Measured before this field existed: three builds of one project printed
// `quiet`, `LOUD`, `LOUD`. The third had no feature on.
//
// Absent on caches written before the field: empty, which reads as "no
// features" — correct for every entry such a cache could hold whose
// request also has none, and a miss otherwise, which is the safe direction.
std::string features;
};

std::vector<BuildCacheEntry> read_build_cache(const std::filesystem::path& projectRoot) {
Expand Down Expand Up @@ -254,6 +270,11 @@ std::vector<BuildCacheEntry> read_build_cache(const std::filesystem::path& proje
e.runTierPending = (line.substr(8) == "1");
haveNextLine = static_cast<bool>(std::getline(f, line));
}
// Optional `features=<list>`. Absent ⇒ empty; see the field.
if (haveNextLine && line.starts_with("features=")) {
e.features = line.substr(9);
haveNextLine = static_cast<bool>(std::getline(f, line));
}
entries.push_back(std::move(e));
if (!haveNextLine || line.empty()) break;
}
Expand All @@ -265,6 +286,25 @@ std::vector<BuildCacheEntry> read_build_cache(const std::filesystem::path& proje
void write_build_cache_entries(const std::filesystem::path& path,
const std::vector<BuildCacheEntry>& entries);

// `a, b` and `b a` are one request. Normalised on both sides of the comparison
// — the entry stores this form and the fast path computes it — so a cache hit
// depends on the SET rather than on how it was typed.
std::string normalize_features(std::string_view raw) {
std::vector<std::string> toks;
for (std::size_t i = 0; i < raw.size();) {
auto c = raw.find_first_of(", ", i);
auto t = raw.substr(i, c == std::string_view::npos ? c : c - i);
if (!t.empty()) toks.emplace_back(t);
if (c == std::string_view::npos) break;
i = c + 1;
}
std::ranges::sort(toks);
toks.erase(std::unique(toks.begin(), toks.end()), toks.end());
std::string out;
for (auto const& t : toks) { if (!out.empty()) out += ','; out += t; }
return out;
}

void write_build_cache(const std::filesystem::path& projectRoot,
const std::filesystem::path& outputDir,
const std::string& ninjaProgram,
Expand All @@ -280,7 +320,8 @@ void write_build_cache(const std::filesystem::path& projectRoot,
const mcpp::platform::runtime::RuntimeBinding& runtimeBinding = {},
std::vector<std::string> depSourceRoots = {},
bool runnerDeclared = false,
bool runTierPending = false) {
bool runTierPending = false,
const std::string& features = {}) {
auto path = projectRoot / kBuildCacheFile;
auto entries = read_build_cache(projectRoot);

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

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

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

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

std::optional<FastPathIdentity>
fast_path_identity(const std::filesystem::path& projectRoot,
std::string_view profileOverride = "") {
std::string_view profileOverride = "",
std::string_view featuresRequested = "") {
auto m = mcpp::manifest::load(projectRoot / "mcpp.toml");
if (!m) return std::nullopt;
return FastPathIdentity{
Expand All @@ -1028,6 +1080,7 @@ fast_path_identity(const std::filesystem::path& projectRoot,
mcpp::extension_table_for(m->buildConfig.moduleExtensions),
m->buildConfig.target,
m->hooks.active(),
normalize_features(featuresRequested),
};
}

Expand All @@ -1040,11 +1093,19 @@ fast_path_identity(const std::filesystem::path& projectRoot,
// what THIS graph supplies, which is knowable only after resolution.
export int list_runners(const std::string& package_filter,
const std::string& cache_mode, bool no_cache,
const std::string& target_triple) {
const std::string& target_triple,
// Which runners exist DEPENDS on the features: a board
// package supplies a different set for an emulator and
// for a probe. Reporting them without the axis that
// selects them would answer a question nobody asked.
const std::string& features = {},
const std::string& profile = {}) {
mcpp::build::BuildOverrides ov;
ov.package_filter = package_filter;
ov.cache_mode = no_cache ? std::string("off") : cache_mode;
ov.target_triple = target_triple;
ov.features = features;
ov.profile = profile;
// Reporting what `mcpp run` would do means resolving what `mcpp run`
// resolves, tool tiers included — otherwise this command would list a
// runner whose program it had declined to install.
Expand Down Expand Up @@ -1113,7 +1174,7 @@ export std::optional<int> try_fast_build(const std::filesystem::path& projectRoo
const BuildCacheEntry* match = nullptr;
for (auto& e : entries) {
if (e.targetTriple == currentTarget && e.profile == want->profile
&& e.cacheMode == want->cacheMode) {
&& e.cacheMode == want->cacheMode && e.features == want->features) {
match = &e;
break;
}
Expand Down Expand Up @@ -1245,7 +1306,7 @@ std::optional<int> try_fast_run(const std::filesystem::path& projectRoot,
const BuildCacheEntry* match = nullptr;
for (auto& e : entries) {
if (e.targetTriple.empty() && e.profile == want->profile
&& e.cacheMode == want->cacheMode) {
&& e.cacheMode == want->cacheMode && e.features == want->features) {
match = &e;
break;
}
Expand Down Expand Up @@ -1407,7 +1468,24 @@ export int build_run_target(const std::optional<std::string>& targetName,
// Empty is the default runner — `mcpp run`. Any
// other value came from `--runner <name>` and the
// engine has never seen it before.
std::string_view runner_name = {}) {
std::string_view runner_name = {},
// ⭐⭐ THE TWO AXES `build` AND `test` HAVE ALWAYS
// TAKEN, AND `run` DID NOT.
//
// Both change WHAT IS BUILT, so a `run` that could
// not express them could only ever execute whatever
// a previous `build` happened to leave behind — and
// there is no spelling of `mcpp run` that runs a
// release artefact, or one built with a feature on.
//
// It is the shape the whole device surface is built
// around: a board package expresses "emulator" and
// "hardware" as features, so `mcpp run --features
// hardware` is the command a developer types when
// the board arrives. Without this it was the one
// scenario the design's own example could not run.
const std::string& features = {},
const std::string& profile = {}) {
// mcpp#225 (E2): reuse the resolved build cache when it's still fresh,
// skipping prepare_build's toolchain resolution + modgraph scan
// entirely — mirrors cmd_build's try_fast_build fast path. The cached
Expand All @@ -1424,6 +1502,11 @@ export int build_run_target(const std::optional<std::string>& targetName,
// manifest to print the note against.
if (package_filter.empty() && cache_mode.empty() && !no_cache
&& target_triple.empty() && !no_runner
// ⚠️ AND NEITHER NEW AXIS IS SET. The cached entry was written for
// whichever feature set and profile the last build used; taking it
// here would silently ignore the flag, which is the same reason
// `--cache` and `--profile` bypass it in `cmd_build`.
&& features.empty() && profile.empty()
// ⚠️⚠️ THE FAST PATH IS `run`'s, AND ONLY `run`'s.
//
// It exec's the cached artefact directly — that IS its definition — so
Expand All @@ -1448,6 +1531,8 @@ export int build_run_target(const std::optional<std::string>& targetName,
ov.package_filter = package_filter;
ov.cache_mode = cache_mode;
ov.target_triple = target_triple;
ov.features = features;
ov.profile = profile;
// This verb executes what it builds, so the `when = "run"` tool tier is
// part of what has to exist. `mcpp build` does not set it, which is the
// whole of the difference the tier buys.
Expand Down
6 changes: 6 additions & 0 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,11 @@ export struct BuildContext {
// build cache records it so `mcpp run`'s fast path declines an entry a
// plain `mcpp build` wrote — see BuildCacheEntry::runTierPending.
bool runTierPending = false;
// What `--features` asked for, verbatim. Carried so the build cache entry
// can record the set its artefacts were built with — the output directory
// is keyed on a fingerprint that includes the features and the entry was
// not, which let a plain build serve a featured artefact.
std::string activeFeatureRequest;
std::filesystem::path outputDir;
std::filesystem::path stdBmi;
std::filesystem::path stdObject;
Expand Down Expand Up @@ -8828,6 +8833,7 @@ prepare_build(bool print_fingerprint,
ctx.runtimeSelection = runtimeSelection;
ctx.runtimeBinding = runtimeBindingSnapshot;
ctx.profile = effectiveProfile;
ctx.activeFeatureRequest = overrides.features;
ctx.compilerChoice = { std::string(tc_origin_name(tcOrigin)),
graphCompilerRequiredBy,
graphCompilerReplaced.empty() ? pinReplacedDefault
Expand Down
Loading
Loading