Skip to content

Commit c57c12f

Browse files
committed
A passing check moves a stamp it did not write past the inputs that changed
1 parent 551d747 commit c57c12f

4 files changed

Lines changed: 120 additions & 7 deletions

File tree

‎docs/30-build-mcpp.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,11 @@ The verdict is the exit code; the stamp is bookkeeping the graph needs, and
549549
mcpp creates it when the command succeeds. Before this, every check needed a
550550
wrapper script to touch the file — and a command is an argv with no shell
551551
assumed, so that wrapper could not be written portably at all. A command that
552-
already writes its own stamp is unaffected: an existing file is left alone.
552+
writes its own stamp is unaffected: a stamp the command created or rewrote is
553+
left as it is. A stamp the command did not write is created on the first pass
554+
and has its modification time moved to the present on every later pass
555+
(2026.9.27.1+), so after an input changes and the check passes again the stamp
556+
is newer than that input and the check does not run on the next build.
553557

554558
> A missing stamp does **not** fail the build. ninja leaves the declared output
555559
> absent and re-runs that edge on every build afterwards, which looks like a

‎docs/zh/30-build-mcpp.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,9 @@ mcpp 为那条边写出 `depfile =` 与 `deps = gcc`,ninja 读取该文件并
468468
**check 的命令不必自己写 stamp**(mcpp 2026.8.29.1+)。判定是退出码,stamp 是**构建图**
469469
需要的记账;命令成功时由 mcpp 创建它。在此之前每个 check 都需要一个包装脚本去 touch
470470
那个文件 —— 而 command 是 argv、不假设有 shell,所以那个包装器**根本没法可移植地写出来**。
471-
已经自己写 stamp 的命令不受影响:已存在的文件不会被动。
471+
自己写 stamp 的命令不受影响:命令创建或改写过的 stamp 保持原样。命令没有写的 stamp,
472+
第一次通过时创建,之后每次通过时把修改时间更新为当前时间(2026.9.27.1+);因此某个输入
473+
变化、check 再次通过之后,stamp 比该输入新,下一次构建不再运行这个 check。
472474

473475
> stamp 缺失**不会**让构建失败。ninja 只是留着那个声明的输出不存在,并在之后**每次构建
474476
> 都重跑**那条边 —— 看起来像一个通过了的检查,实际上它从未被满足。

‎src/cli.cppm‎

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -989,9 +989,19 @@ int run(int argc, char** argv) {
989989
// platform mcpp runs on, so this needs no shell, no `touch`, and no
990990
// per-platform spelling.
991991
//
992-
// A command that already creates its stamp is unaffected: existing files
993-
// are left alone, so the pre-2026.8.29.1 wrapper scripts keep working
994-
// byte-for-byte.
992+
// A command that writes its own stamp is unaffected: a stamp the command
993+
// created or rewrote is left as the command left it, so the
994+
// pre-2026.8.29.1 wrapper scripts keep working byte-for-byte.
995+
//
996+
// A STAMP THE COMMAND DID NOT WRITE IS TOUCHED, NOT ONLY CREATED. The
997+
// stamp is what ninja compares with the inputs, so after an input changes
998+
// and the command passes again, its time has to move past that input.
999+
// Until 2026.9.27.1 an existing stamp was left alone whatever the command
1000+
// did, and every check whose command writes nothing -- clang-tidy, an
1001+
// installer run through `mcpp-deps` -- re-ran on every build after its
1002+
// first input change, because its output stayed older than the input
1003+
// forever. The comparison is by the stamp's own time before and after the
1004+
// command, so a command that did write it is still left alone.
9951005
if (std::string_view(argv[1]) == "__action-stamp") {
9961006
std::vector<std::string> stamps;
9971007
int i = 2;
@@ -1011,19 +1021,39 @@ int run(int argc, char** argv) {
10111021
// `run_exec`: no shell, stdio inherited. The analyser's own output has
10121022
// to reach the terminal unchanged — a check that fails is read by a
10131023
// human, and capturing would either swallow it or reprint it wrapped.
1024+
// Each stamp's time before the command runs; empty when it is absent.
1025+
std::vector<std::optional<std::filesystem::file_time_type>> before;
1026+
for (auto const& s : stamps) {
1027+
std::error_code ec;
1028+
const auto p = mcpp::platform::fs::extended_length(std::filesystem::path{s});
1029+
const auto t = std::filesystem::last_write_time(p, ec);
1030+
before.push_back(ec ? std::nullopt : std::optional{t});
1031+
}
10141032
const int r = mcpp::platform::process::run_exec(cmd);
10151033
// The stamps are written ONLY on success. Writing them anyway would
10161034
// make ninja consider the edge satisfied, so the next build would skip
10171035
// a check that had never passed.
10181036
if (r != 0) return r;
1019-
for (auto const& s : stamps) {
1037+
for (std::size_t k = 0; k < stamps.size(); ++k) {
1038+
auto const& s = stamps[k];
10201039
std::error_code ec;
10211040
// Relative to the build directory, which can be deep enough to
10221041
// take the stamp past the Windows path limit (mcpp#641, item 3).
10231042
const auto p = mcpp::platform::fs::extended_length(std::filesystem::path{s});
10241043
if (!p.parent_path().empty())
10251044
std::filesystem::create_directories(p.parent_path(), ec);
1026-
if (std::filesystem::exists(p, ec)) continue;
1045+
if (std::filesystem::exists(p, ec)) {
1046+
const auto now = std::filesystem::last_write_time(p, ec);
1047+
// Written by the command during this run: left as it is.
1048+
if (!ec && (!before[k] || now != *before[k])) continue;
1049+
std::filesystem::last_write_time(
1050+
p, std::filesystem::file_time_type::clock::now(), ec);
1051+
if (ec) {
1052+
std::println(stderr, "error: cannot update check stamp '{}': {}", s, ec.message());
1053+
return 1;
1054+
}
1055+
continue;
1056+
}
10271057
std::ofstream out(p, std::ios::trunc);
10281058
if (!out) {
10291059
std::println(stderr, "error: cannot write check stamp '{}'", s);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
# 780_a_passing_check_moves_its_stamp_past_a_changed_input.sh — a `role =
3+
# "check"` whose command writes no stamp, after one of its inputs changed.
4+
#
5+
# The engine writes such a check's stamp when the command succeeds (313). Until
6+
# 2026.9.27.1 it only CREATED the stamp: an existing one was left alone. So once
7+
# an input changed, the check ran and passed, and its stamp stayed older than
8+
# that input -- and ninja ran it again on every build after, forever. Measured
9+
# with mcpp-plugins' `deps-cmake`, whose check is a CMake build: every `mcpp
10+
# build` after one edit re-ran configure, build and install.
11+
#
12+
# The criterion is the build AFTER the one that re-ran the check: it must not
13+
# run the check again. Portable like 313: the command is the engine itself.
14+
set -e
15+
16+
source "$(dirname "$0")/_host_path.sh"
17+
18+
TMP=$(mktemp -d)
19+
trap "rm -rf $TMP" EXIT
20+
cd "$TMP"
21+
22+
mkdir -p src
23+
cat > mcpp.toml <<'EOF'
24+
[package]
25+
name = "stampmove"
26+
version = "0.1.0"
27+
EOF
28+
cat > src/main.cpp <<'EOF'
29+
#include <cstdio>
30+
int main() { std::printf("STAMP_MOVE_OK\n"); }
31+
EOF
32+
echo one > input.txt
33+
34+
MCPP_HOST="$(host_path "$MCPP")"
35+
INPUT_HOST="$(host_path "$TMP/input.txt")"
36+
cat > build.mcpp <<EOF
37+
import std;
38+
import mcpp;
39+
int main() {
40+
mcpp::action a;
41+
a.id = "probe";
42+
a.role = "check";
43+
a.description = "a check whose command writes no stamp";
44+
a.arg("$MCPP_HOST").arg("--version")
45+
.input("$INPUT_HOST")
46+
.output((std::string(mcpp::out_dir()) + "/probe.stamp").c_str())
47+
.submit();
48+
}
49+
EOF
50+
51+
# `-v` prints each edge ninja runs; the check's is the one that goes through
52+
# the engine's stamp wrapper.
53+
ran() { grep -q '__action-stamp' "$1"; }
54+
55+
"$MCPP" build -v > b1.log 2>&1 || { cat b1.log; echo "FAIL: build failed"; exit 1; }
56+
ran b1.log || { cat b1.log; echo "FAIL: the first build did not run the check"; exit 1; }
57+
58+
"$MCPP" build -v > b2.log 2>&1 || { cat b2.log; echo "FAIL: no-op rebuild failed"; exit 1; }
59+
if ran b2.log; then cat b2.log; echo "FAIL: the check re-ran with nothing changed"; exit 1; fi
60+
61+
# A changed input: the check runs once...
62+
sleep 1
63+
echo two > input.txt
64+
"$MCPP" build -v > b3.log 2>&1 || { cat b3.log; echo "FAIL: rebuild after the edit failed"; exit 1; }
65+
ran b3.log || { cat b3.log; echo "FAIL: a changed input did not re-run the check"; exit 1; }
66+
67+
# ...and not again: its stamp is now newer than the input.
68+
"$MCPP" build -v > b4.log 2>&1 || { cat b4.log; echo "FAIL: rebuild after the check failed"; exit 1; }
69+
if ran b4.log; then
70+
cat b4.log
71+
echo "FAIL: the check re-ran on the build after it passed -- its stamp stayed older than the input"
72+
exit 1
73+
fi
74+
75+
out="$("$MCPP" run 2>&1 | tail -1)"
76+
[[ "$out" == *STAMP_MOVE_OK* ]] || { echo "FAIL: the program did not run: '$out'"; exit 1; }
77+
echo "OK"

0 commit comments

Comments
 (0)