|
22 | 22 | build. If the input hash matches, skip compilation entirely. The ninja rule uses |
23 | 23 | `restat = 1`, so unchanged output triggers no downstream rebuilds. The depfile |
24 | 24 | is saved as `.d.cache` since ninja deletes the original after reading it. |
| 25 | +The skip is only taken when no input is newer than the PCH: clang breaks |
| 26 | +`#pragma once` for inputs whose mtime moved (see `inputs_newer_than`). |
25 | 27 |
|
26 | 28 | Usage: |
27 | 29 | python compile_pch.py <compiler> <args...> |
@@ -230,6 +232,28 @@ def hash_compile_flags(args: list[str]) -> str: |
230 | 232 | return h.hexdigest() |
231 | 233 |
|
232 | 234 |
|
| 235 | +def inputs_newer_than(files: list[Path], pch_file: Path) -> bool: |
| 236 | + """Return True if any input file has an mtime newer than ``pch_file``. |
| 237 | +
|
| 238 | + A content-hash match is NOT enough to reuse a PCH. Clang records each |
| 239 | + input's mtime in the PCH, and with ``-fpch-validate-input-files-content`` |
| 240 | + it accepts a PCH whose inputs are byte-identical but have a newer mtime -- |
| 241 | + yet it then fails to associate those headers with the PCH copies, so |
| 242 | + ``#pragma once`` no longer dedupes them. Every consumer TU that includes |
| 243 | + such a header again dies with "redefinition of ..." (reproduced on clang |
| 244 | + 21.1.8). Touching a header without changing it is enough to trigger this, |
| 245 | + so a touched input must force a real PCH rebuild. |
| 246 | + """ |
| 247 | + pch_mtime = pch_file.stat().st_mtime_ns |
| 248 | + for f in files: |
| 249 | + try: |
| 250 | + if f.stat().st_mtime_ns > pch_mtime: |
| 251 | + return True |
| 252 | + except OSError: |
| 253 | + return True # Missing input: cannot trust the PCH |
| 254 | + return False |
| 255 | + |
| 256 | + |
233 | 257 | def invalidate_stale_pch(pch_file: Path) -> None: |
234 | 258 | """Delete a PCH file if its input files have changed since it was built. |
235 | 259 |
|
@@ -269,7 +293,7 @@ def invalidate_stale_pch(pch_file: Path) -> None: |
269 | 293 | return |
270 | 294 | current_hash = hash_input_files(input_files) |
271 | 295 | stored_hash = hash_file.read_text(encoding="utf-8").strip() |
272 | | - if current_hash != stored_hash: |
| 296 | + if current_hash != stored_hash or inputs_newer_than(input_files, pch_file): |
273 | 297 | print( |
274 | 298 | f"PCH inputs changed — removing stale {pch_file.name} to force rebuild", |
275 | 299 | file=sys.stderr, |
@@ -337,6 +361,13 @@ def main() -> int: |
337 | 361 | "PCH compiler flags changed - rebuilding", |
338 | 362 | file=sys.stderr, |
339 | 363 | ) |
| 364 | + elif current_hash == stored_hash and inputs_newer_than( |
| 365 | + input_files, pch_output |
| 366 | + ): |
| 367 | + print( |
| 368 | + "PCH inputs touched (mtime newer, content same) - rebuilding", |
| 369 | + file=sys.stderr, |
| 370 | + ) |
340 | 371 | elif current_hash == stored_hash: |
341 | 372 | print( |
342 | 373 | "PCH inputs unchanged (hash match) - skipping compilation", |
|
0 commit comments