@@ -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);
0 commit comments