Skip to content

Commit 3306ac6

Browse files
committed
fix(build): the fast path compares every declared build-program input
`rerun_if_changed("data/table.csv")` states that the program must run again when that file's content changes. The project-level fast path skips prepare_build when no source is newer than build.ninja, and prepare_build is where the program's cache is read; the check the fast path did run asked only about glob path sets. A data file is neither under src/ nor named with a C++ extension, so the mtime sweep cannot see it either. Editing it therefore left the previous run's generated header in place: `Finished dev in 0.00s`, and the program compiled the previous bytes. The fast path now compares the three kinds of input the cache records: a glob's path set, a declared file's content hash, and a declared environment variable's value. `glob_inputs_stale` is renamed `program_inputs_stale` because the name was the reason the other two were never asked about. e2e 612 uses the program's output as its criterion -- a stale header and a fresh one make the binary print different strings -- and runs the other half of the control: with nothing touched, the next build still takes the fast path, which "always rebuild" would also pass the first assertion with. Verified failing on 2026.9.5.3 and passing here. Found by mcpp.tools.embed, the first non-rule member of mcpp:plugins: it writes a data file into a header while the build program runs, so it submits no action and falls entirely on this path. Documentation for both languages, CHANGELOG, and version 2026.9.5.4.
1 parent 72b7c5d commit 3306ac6

9 files changed

Lines changed: 172 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@
55
66
## [Unreleased]
77

8+
## [2026.9.5.4] — 2026-09-06
9+
10+
### 构建程序声明的文件输入,快路径此前不比较
11+
12+
`rerun_if_changed("data/table.csv")` 声明的是「这个文件的内容变了就重跑」。工程级快路径
13+
在没有任何**源文件**`build.ninja` 新时跳过 `prepare_build`,而构建程序缓存正是在
14+
`prepare_build` 里被读取的;快路径自己只问过 glob 输入的**路径集合**(#359),没问过声明
15+
文件的内容。数据文件既不在 `src/` 下也没有 C++ 扩展名,mtime 扫描看不见它,于是改了数据、
16+
`mcpp build` 打印 `Finished dev in 0.00s`、程序里编进去的还是上一次的字节。
17+
18+
快路径现在按缓存里记录的方式比较三类构建程序输入:glob 的路径集合、声明文件的内容哈希、
19+
声明环境变量的值。e2e 612 用程序的**输出**做判据(陈旧的头文件与新的头文件让二进制打印
20+
不同的字符串),并跑对照的另一半:输入没变时第二次构建仍然走快路径,否则「总是重建」也能
21+
让第一条断言通过。发现它的是 `mcpp.tools.embed`——mcpp-plugins 0.1.1 里第一个非规则成员,
22+
它把数据文件写成头文件,没有 action 可提交,所以完全落在这条路径上。
23+
24+
### 注释与文档里不再有装饰性符号
25+
26+
2026.9.5.3 的清理覆盖了 `docs/``README.md`、CHANGELOG、引擎源码、测试、示例与工作流
27+
文件,没有覆盖 `bench/``tools/``scripts/``mcpp.toml``README.zh-CN.md`。程序
28+
输出保留原样:用户在终端上读到的一行既不是文档也不是注释。`README.zh-CN.md` 同时补上英文
29+
版已有的 Cortex-M 行,状态列改用与英文版相同的词。
30+
831
## [2026.9.5.3] — 2026-09-05
932

1033
### 官方构建插件集中为一个包:`mcpp:plugins`

docs/07-build-mcpp.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,17 @@ paths** and nothing else:
348348
The build output tree and `.git` are never part of the set, so a wide pattern
349349
cannot make the program re-run forever against its own outputs.
350350

351+
**Every declared input is compared on the fast path too** (2026.9.5.4+). A
352+
project whose sources are all older than `build.ninja` takes a fast path that
353+
skips the phase where the program's cache is normally consulted, and until
354+
2026.9.5.4 that path asked only about glob path sets. A data file a program
355+
reads is neither under `src/` nor named with a C++ extension, so the mtime
356+
sweep cannot see it either: editing it left the previous run's output in place
357+
and the build reported `Finished dev in 0.00s`. The fast path now compares what
358+
the cache records — a glob's path set, a declared file's content hash, and a
359+
declared environment variable's value — so `rerun_if_changed` means the same
360+
thing under both paths.
361+
351362
### Declaring work instead of doing it: `mcpp::action` (2026.8.5.1+)
352363

353364
Generating a source by writing it *here* is the easy path and the wrong one

docs/zh/07-build-mcpp.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ int main() {
305305
构建输出目录与 `.git` 永远不进入集合,因此再宽的模式也不会让程序对着自己的产物
306306
无限重跑。
307307

308+
**声明过的输入在快路径上同样被比较**(2026.9.5.4+)。当所有源文件都不比
309+
`build.ninja` 新时,工程走快路径,跳过读取构建程序缓存的那个阶段;在 2026.9.5.4
310+
之前,快路径只问 glob 的路径集合。数据文件既不在 `src/` 下也没有 C++ 扩展名,mtime
311+
扫描同样看不见它:改了它,上一次的产出原样留着,构建打印 `Finished dev in 0.00s`
312+
现在快路径按缓存记录的方式比较三类输入 —— glob 的路径集合、声明文件的内容哈希、
313+
声明环境变量的值 —— 因此 `rerun_if_changed` 在两条路径上含义相同。
314+
308315
### 声明产出而非执行动作:`mcpp::action`(2026.8.5.1+)
309316

310317
**这里**直接把源码写出来是省事的路,超过一定规模就是错的:它每次 prepare 跑

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.9.5.3"
3+
version = "2026.9.5.4"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

modules/versioning/src/version.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.9.5.3";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.9.5.4";
3535

3636
} // namespace mcpp

src/build/build_program.cppm

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,9 @@ std::expected<void, std::string> run_build_program(
255255
const mcpp::manifest::CppStandardConfig& cppStandard,
256256
const BuildProgramEnv& env);
257257

258-
// #359: has any recorded glob input's path SET changed since its build.mcpp
259-
// cache was written?
258+
// Has any recorded build-program input changed since its build.mcpp cache was
259+
// written: a glob's path SET (#359), a declared file's CONTENT, or a declared
260+
// environment variable's value?
260261
//
261262
// The project-level fast path skips prepare_build entirely when no source is
262263
// newer than build.ninja, and prepare is where the build.mcpp cache is
@@ -267,10 +268,19 @@ std::expected<void, std::string> run_build_program(
267268
// the build.mcpp source itself; a glob is one more kind of build-program input,
268269
// so it belongs to the same question.
269270
//
271+
// A DECLARED FILE IS THE SAME QUESTION AND WAS NOT ASKED (2026.9.5.4). The
272+
// mtime sweep that guards the fast path walks SOURCES, so a data file a build
273+
// program reads -- `rerun_if_changed("data/table.csv")` -- is invisible to it,
274+
// and this function used to skip a cache record that carried no glob at all.
275+
// The result was that editing such a file left the generated header from the
276+
// previous build in place: `Finished dev in 0.00s`, and the program compiled
277+
// the previous bytes. `mcpp.tools.embed` is the case that found it. Contents,
278+
// not mtime, exactly as the cache records them.
279+
//
270280
// Scans the caches under `<projectRoot>/target/.build-mcpp` (the root's own and
271-
// each dependency's). Each cache records the root its globs were relative to,
272-
// so a dependency's glob is evaluated against the dependency's tree.
273-
bool glob_inputs_stale(const std::filesystem::path& projectRoot);
281+
// each dependency's). Each cache records the root its entries were relative to,
282+
// so a dependency's input is evaluated against the dependency's tree.
283+
bool program_inputs_stale(const std::filesystem::path& projectRoot);
274284

275285
} // namespace mcpp::build
276286

@@ -1199,7 +1209,7 @@ std::expected<void, std::string> run_build_program(
11991209
return {};
12001210
}
12011211

1202-
bool glob_inputs_stale(const fs::path& projectRoot) {
1212+
bool program_inputs_stale(const fs::path& projectRoot) {
12031213
std::error_code ec;
12041214
const fs::path base = projectRoot / "target" / ".build-mcpp";
12051215
if (!fs::exists(base, ec)) return false;
@@ -1216,11 +1226,19 @@ bool glob_inputs_stale(const fs::path& projectRoot) {
12161226
if (it.depth() >= 3) { it.disable_recursion_pending(); continue; }
12171227
if (it->path().filename() != "build.mcpp.cache") continue;
12181228
auto rec = read_cache(it->path().parent_path());
1219-
if (!rec.loaded || rec.globs.empty() || rec.rootPath.empty()) continue;
1229+
if (!rec.loaded || rec.rootPath.empty()) continue;
12201230
fs::path recRoot{rec.rootPath};
12211231
auto outName = output_dir_name(recRoot, it->path().parent_path());
12221232
for (auto const& [h, pattern] : rec.globs)
12231233
if (dirs::glob_fingerprint(recRoot, pattern, outName) != h) return true;
1234+
// The same comparison `cache_fresh` makes when prepare_build runs. It
1235+
// is repeated here rather than shared because the fast path has no
1236+
// manifest, no toolchain and no context hash -- only the recorded
1237+
// entries and the tree they were measured against.
1238+
for (auto const& [h, rel] : rec.inputs)
1239+
if (mcpp::toolchain::hash_file(abs_against_root(recRoot, rel)) != h) return true;
1240+
for (auto const& [h, name] : rec.envs)
1241+
if (mcpp::toolchain::hash_string(env_value(name)) != h) return true;
12241242
}
12251243
return false;
12261244
}

src/build/execute.cppm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,12 @@ bool sources_newer_than(const std::filesystem::path& projectRoot,
918918
// sweep therefore cannot see it, and the fast path would report
919919
// "Finished dev in 0.00s" while the new file is never generated. Same
920920
// question as the build.mcpp check above, different kind of input.
921-
if (mcpp::build::glob_inputs_stale(projectRoot)) return true;
921+
//
922+
// 2026.9.5.4: the same call now also compares a declared FILE's content and
923+
// a declared environment variable's value. The sweep below walks sources,
924+
// so a data file a build program reads is invisible to it for the same
925+
// reason a new .proto is.
926+
if (mcpp::build::program_inputs_stale(projectRoot)) return true;
922927
// mcpp#365: an author-written `.rc` is a third input of the same kind. It
923928
// is not under src/ and has no C++ extension, so the sweep below cannot see
924929
// it — and unlike the icon or a header the script includes, editing it can
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc
3+
# 612_build_program_file_input_fast_path.sh — editing a file a build program
4+
# declared with `rerun_if_changed` rebuilds, even on the fast path
5+
# (mcpp 2026.9.5.4+).
6+
#
7+
# The fast path skips prepare_build when no SOURCE is newer than build.ninja,
8+
# and prepare_build is where the build.mcpp cache is consulted. A data file is
9+
# not a source: it has no C++ extension and does not live under src/, so the
10+
# mtime sweep cannot see it, and the staleness check the fast path did run
11+
# asked only about glob path SETS. Editing the file therefore left the header
12+
# generated by the previous run in place, `mcpp build` reported
13+
# "Finished dev in 0.00s", and the program kept compiling the previous bytes.
14+
#
15+
# The criterion is the program's OUTPUT, not a line of build log: a stale
16+
# header and a fresh one differ in what the binary prints.
17+
set -e
18+
19+
TMP=$(mktemp -d)
20+
trap "rm -rf $TMP" EXIT
21+
cd "$TMP"
22+
23+
"$MCPP" new probe > /dev/null; cd probe
24+
rm -f src/*.cppm
25+
mkdir -p data
26+
printf 'first\n' > data/value.txt
27+
28+
cat > mcpp.toml <<'EOT'
29+
[package]
30+
name = "probe"
31+
version = "0.1.0"
32+
[language]
33+
standard = "c++23"
34+
modules = true
35+
import_std = true
36+
37+
[targets.probe]
38+
kind = "bin"
39+
main = "src/main.cpp"
40+
EOT
41+
42+
# A build program that reads a data file and writes a header from it. No
43+
# action is submitted: the work happens while the program runs, which is the
44+
# shape `mcpp.tools.embed` has and the shape the fast path used to skip.
45+
cat > build.mcpp <<'EOT'
46+
import std;
47+
import mcpp;
48+
int main() {
49+
const std::filesystem::path root(mcpp::manifest_dir());
50+
const std::filesystem::path gen(std::string(mcpp::out_dir()) + "/include");
51+
std::error_code ec;
52+
std::filesystem::create_directories(gen, ec);
53+
std::ifstream in(root / "data" / "value.txt");
54+
std::string value;
55+
std::getline(in, value);
56+
std::ofstream out(gen / "value.h", std::ios::trunc);
57+
out << "#pragma once\n#define PROBE_VALUE \"" << value << "\"\n";
58+
mcpp::rerun_if_changed((root / "data" / "value.txt").string().c_str());
59+
mcpp::include_dir(gen.string().c_str());
60+
return 0;
61+
}
62+
EOT
63+
64+
cat > src/main.cpp <<'EOT'
65+
#include <cstdio>
66+
#include "value.h"
67+
int main() { std::printf("VALUE=%s\n", PROBE_VALUE); }
68+
EOT
69+
70+
value() { "$MCPP" run 2>&1 | grep '^VALUE=' | tail -1; }
71+
72+
"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: the first build failed"; exit 1; }
73+
[[ "$(value)" == "VALUE=first" ]] || { echo "FAIL: the first build did not embed the first value"; exit 1; }
74+
75+
# Content only. The path set is unchanged, so a glob fingerprint cannot see
76+
# this, and no source's mtime moves.
77+
printf 'second\n' > data/value.txt
78+
79+
"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: the second build failed"; exit 1; }
80+
out="$(value)"
81+
[[ "$out" == "VALUE=second" ]] || {
82+
echo "FAIL: a declared file input changed and the build replayed the old header ($out)"
83+
cat b2.log
84+
exit 1
85+
}
86+
echo "PASS: a declared file input is compared on the fast path"
87+
88+
# The mirror half: an unchanged input must NOT force work. Two builds in a row
89+
# with nothing touched, and the second one has to take the fast path -- without
90+
# this the fix could be "always rebuild", which passes the assertion above and
91+
# is not the behaviour asked for.
92+
"$MCPP" build > b3.log 2>&1
93+
grep -qE 'Finished .* in ' b3.log || { cat b3.log; echo "FAIL: the third build did not finish"; exit 1; }
94+
"$MCPP" build > b4.log 2>&1
95+
grep -q 'Compiling probe' b4.log && { cat b4.log; echo "FAIL: an unchanged input recompiled the project"; exit 1; }
96+
echo "PASS: an unchanged input still takes the fast path"

tests/unit/test_source_kind.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ TEST(SourceKind, PredicatesAgreeWithTheKind) {
116116
// Assembly is absent on purpose — it has no import and no scanned include
117117
// graph, so editing one changes its object (ninja tracks that) and nothing
118118
// else. A NEW assembly file is a different question, answered by
119-
// glob_inputs_stale.
119+
// program_inputs_stale.
120120
EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::ModuleInterface));
121121
EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::Cxx));
122122
EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::C));
@@ -305,7 +305,7 @@ TEST(SourceKind, DevicePredicates) {
305305
EXPECT_FALSE(mcpp::is_cxx_like(SourceKind::Device));
306306
EXPECT_FALSE(mcpp::links_unconditionally(SourceKind::Device));
307307
// Absent for the same reason assembly is absent: the content change is
308-
// tracked by ninja, and a NEW file is `glob_inputs_stale`'s question.
308+
// tracked by ninja, and a NEW file is `program_inputs_stale`'s question.
309309
EXPECT_FALSE(mcpp::affects_graph_shape(SourceKind::Device));
310310
EXPECT_EQ(mcpp::to_string(SourceKind::Device), "device");
311311
}

0 commit comments

Comments
 (0)