Skip to content

Commit 74b87a8

Browse files
committed
fix(resources): .rc 编辑要能穿过工程级 fast path,并让 e2e 断言真的能失败
Windows e2e 抓到的:一次只改 `res/app.rc` 的构建报 `Finished dev in 0.15s` —— 工程级 fast path 短路了整个 prepare。 `sources_newer_than` 只扫 `src/**/*` 的 C++ 扩展名,`.rc` 既不在 src/ 下也不是 那些扩展名,完全看不见。这不只是「警告没打」:`.rc` 的 implicit input 集合来自 扫描它,而扫描发生在 prepare —— 于是用户往脚本里新加一行 `#include "ids.h"`, 那个头文件永远不会被跟踪。与 `build.mcpp`、glob 输入是同一类输入:**改了它, 图本身应该长得不一样**,而 mtime 扫描看不见。 只扫 `files`。`icon` 与 `extra-inputs` 已经是 ninja 的 implicit input,改它们 不会改变图的形状,为一次改图标强制走完整 prepare 买不到任何东西。 顺带修掉两处**我自己写的假绿**: - 图标断言原来搜 4 字节(`00ff00ff`),在 MB 级二进制里撞上是常事 —— 换成 4 像素 icon 的 16 字节高熵标记,并加断言「旧标记必须消失」。 (Linux 上之所以过,很可能就是撞上了。) - b3 之所以在 Windows 上没暴露 fast path 问题,正是因为那条弱断言。 验证:同一工程连构两次(第二次 0.00s、无 prepare),只改 .rc 后第三次 prepare 重跑且诊断触发。
1 parent 2ea1b7c commit 74b87a8

2 files changed

Lines changed: 49 additions & 12 deletions

File tree

src/build/execute.cppm

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,8 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache,
447447
// caller's broader glob; and it's the same choke-point fix as expand_glob
448448
// itself, see scanner.cppm).
449449
bool sources_newer_than(const std::filesystem::path& projectRoot,
450-
std::filesystem::file_time_type ninjaTime) {
450+
std::filesystem::file_time_type ninjaTime,
451+
const std::vector<std::filesystem::path>& resourceScripts = {}) {
451452
std::error_code ec;
452453
// The root build.mcpp is a build input too — its directives shape
453454
// build.ninja (flags, generated/selected sources). A changed program must
@@ -465,6 +466,25 @@ bool sources_newer_than(const std::filesystem::path& projectRoot,
465466
// "Finished dev in 0.00s" while the new file is never generated. Same
466467
// question as the build.mcpp check above, different kind of input.
467468
if (mcpp::build::glob_inputs_stale(projectRoot)) return true;
469+
// mcpp#365: an author-written `.rc` is a third input of the same kind. It
470+
// is not under src/ and has no C++ extension, so the sweep below cannot see
471+
// it — and unlike the icon or a header the script includes, editing it can
472+
// change WHAT THE GRAPH SHOULD BE: the implicit-input set comes from
473+
// scanning the script, and the "your VERSIONINFO is named by string"
474+
// diagnostic is produced while scanning. Both happen in prepare_build, so a
475+
// fresh build.ninja made the edit invisible — the resource itself rebuilt
476+
// (ninja tracks it), but a newly added `#include "ids.h"` went untracked and
477+
// the diagnostic never fired again after the first build.
478+
//
479+
// Only `files` is swept. `icon` and `extra-inputs` are already ninja
480+
// implicit inputs and changing them cannot change the shape of the graph,
481+
// so forcing a full prepare on every icon tweak would buy nothing.
482+
for (auto const& f : resourceScripts) {
483+
auto p = f.is_absolute() ? f : (projectRoot / f);
484+
auto ft = std::filesystem::last_write_time(p, ec);
485+
if (ec) { ec.clear(); continue; } // missing → prepare_build reports it
486+
if (ft > ninjaTime) return true;
487+
}
468488
for (auto& f : mcpp::modgraph::expand_glob(projectRoot, "src/**/*")) {
469489
auto ext = f.extension().string();
470490
if (ext != ".cppm" && ext != ".cpp" && ext != ".cc" &&
@@ -556,6 +576,11 @@ std::optional<int> run_ninja_fast(const std::string& ninjaProgram,
556576
struct FastPathIdentity {
557577
std::string profile;
558578
std::string cacheMode;
579+
// mcpp#365: author-written resource scripts, for the freshness sweep. They
580+
// ride along here because this is the one place on the fast path that
581+
// already parses the manifest — re-reading it to answer a second question
582+
// would be a second derivation of the same fact.
583+
std::vector<std::filesystem::path> resourceScripts;
559584
};
560585

561586
std::optional<FastPathIdentity>
@@ -567,6 +592,7 @@ fast_path_identity(const std::filesystem::path& projectRoot,
567592
mcpp::build::resolve_profile_name(*m, profileOverride),
568593
std::string(mcpp::build::cache_mode_name(
569594
mcpp::build::resolve_cache_mode(*m, ""))),
595+
m->resources.files,
570596
};
571597
}
572598

@@ -631,7 +657,7 @@ export std::optional<int> try_fast_build(const std::filesystem::path& projectRoo
631657

632658
// mcpp#225: bounded + vcs/build-dir-excluded walk (see sources_newer_than)
633659
// instead of a hand-rolled recursive_directory_iterator over src/.
634-
if (sources_newer_than(projectRoot, ninjaTime)) return std::nullopt;
660+
if (sources_newer_than(projectRoot, ninjaTime, want->resourceScripts)) return std::nullopt;
635661

636662
// All inputs are older than build.ninja → fast-path: just run ninja.
637663
std::chrono::milliseconds elapsed{};
@@ -706,7 +732,7 @@ std::optional<int> try_fast_run(const std::filesystem::path& projectRoot,
706732
auto tomlTime = std::filesystem::last_write_time(tomlPath, ec);
707733
if (ec || tomlTime > ninjaTime) return std::nullopt;
708734

709-
if (sources_newer_than(projectRoot, ninjaTime)) return std::nullopt;
735+
if (sources_newer_than(projectRoot, ninjaTime, want->resourceScripts)) return std::nullopt;
710736

711737
// Fresh → run ninja (picks up any incremental object/link work) then
712738
// exec the cached exe path directly.

tests/e2e/_windows_resources_body.sh

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,21 @@ utf16hex() {
2828
mkdir -p "$TMP/proj/src" "$TMP/proj/assets"
2929
cd "$TMP/proj"
3030

31-
# A minimal but structurally valid 1x1 32bpp icon: ICONDIR + ICONDIRENTRY +
32-
# BITMAPINFOHEADER + one BGRA pixel + AND mask. The pixel is ff0000ff so the
33-
# payload can be found again inside the linked image.
34-
write_icon() { # $1 = BGRA pixel bytes as printf escapes
35-
printf '\x00\x00\x01\x00\x01\x00\x01\x01\x00\x00\x01\x00\x20\x00\x30\x00\x00\x00\x16\x00\x00\x00\x28\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\x00\x20\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'"$1"'\x00\x00\x00\x00' > assets/app.ico
31+
# A minimal but structurally valid 4x1 32bpp icon: ICONDIR + ICONDIRENTRY +
32+
# BITMAPINFOHEADER + four BGRA pixels + AND mask.
33+
#
34+
# Four pixels, not one, purely so the payload is findable AGAIN inside the
35+
# linked image without false positives: an .ico's bitmap data is embedded
36+
# verbatim, and searching a megabyte-scale binary for a 4-byte pattern hits by
37+
# chance often enough to make the assertion meaningless. 16 bytes does not.
38+
write_icon() { # $1 = 4 BGRA pixels (16 bytes) as printf escapes
39+
printf '\x00\x00\x01\x00\x01\x00\x04\x01\x00\x00\x01\x00\x20\x00\x3c\x00\x00\x00\x16\x00\x00\x00\x28\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x01\x00\x20\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'"$1"'\x00\x00\x00\x00' > assets/app.ico
3640
}
37-
write_icon '\xff\x00\x00\xff'
41+
ICON_A='\xd3\x1c\x7a\x45\x92\xe6\x0b\xa8\x41\xf7\x2d\x63\xbe\x50\x84\x19'
42+
ICON_B='\x6c\xa2\x38\xd7\xe1\x4b\x95\x0f\x77\xc4\x1a\x8e\x2b\xf3\x60\xd5'
43+
ICON_A_HEX='d31c7a4592e60ba841f72d63be508419'
44+
ICON_B_HEX='6ca238d7e14b950f77c41a8e2bf360d5'
45+
write_icon "$ICON_A"
3846

3947
printf 'int main() { return 0; }\n' > src/main.cpp
4048

@@ -111,7 +119,7 @@ EXE="$BUILD_DIR/bin/resapp$EXE_SUFFIX"
111119
EXE_HEX=$(hexof "$EXE")
112120
echo "$EXE_HEX" | grep -q "$(utf16hex 'Acme Corp')" \
113121
|| fail "the version metadata did not reach the executable" b1.log
114-
echo "$EXE_HEX" | grep -q 'ff0000ff' \
122+
echo "$EXE_HEX" | grep -q "$ICON_A_HEX" \
115123
|| fail "the icon payload did not reach the executable" b1.log
116124

117125
# ── A2. Resources are tracked build inputs ────────────────────────────────
@@ -123,10 +131,13 @@ echo "$EXE_HEX" | grep -q 'ff0000ff' \
123131
grep -qE 'no work to do|Finished' b2.log || fail "unexpected rebuild output" b2.log
124132

125133
sleep 1
126-
write_icon '\x00\xff\x00\xff' # a different pixel: same size, new bytes
134+
write_icon "$ICON_B" # same size, entirely different bytes
127135
"$MCPP" build $BUILD_ARGS > b3.log 2>&1 || fail "rebuild after icon change failed" b3.log
128-
hexof "$BUILD_DIR/bin/resapp$EXE_SUFFIX" | grep -q '00ff00ff' \
136+
NEW_HEX=$(hexof "$BUILD_DIR/bin/resapp$EXE_SUFFIX")
137+
echo "$NEW_HEX" | grep -q "$ICON_B_HEX" \
129138
|| fail "editing the icon did not reach the executable (the #365 symptom)" b3.log
139+
echo "$NEW_HEX" | grep -q "$ICON_A_HEX" \
140+
&& fail "the old icon is still embedded — the resource was not rebuilt" b3.log
130141

131142
# Metadata is an input too: the .rc is regenerated and everything downstream
132143
# re-runs.

0 commit comments

Comments
 (0)