Commit f519dfb
committed
fix(build): a package's generated inputs come before its compiles; an empty link is refused (2026.8.30.1)
Three defects, one change of mind about evidence. Each of them was a place
where something answered "is this ready?" from a proxy instead of from the
artifact.
── mcpp#534: `role = "source"` did not order a generated header ─────────────
The engine's own comment claimed ordering needed no special handling because
"a Source action's outputs ARE the compile edge's inputs". That is true of a
generated `.cpp` and false of a generated `.h`: a header is reached through
`-I`, never appears as an edge input, and the depfile that would record it does
not exist until a compile has already succeeded.
So an action whose outputs were all headers had a node in build.ninja that
nothing could reach — not `default` (Source outputs are excluded on purpose),
not the goal phony (objects and link outputs only), and no consuming edge. It
never ran. The issue was filed as an intermittent race; it is deterministic,
and five consecutive builds reproduce it identically. What made it look like a
race is that `prepare_actions` wrote a zero-byte placeholder for every declared
Source output, headers included — so the file was on disk whether or not the
generator had run.
* `BuildAction` records the package that declared it, spelled the same way
`CompileUnit::packageName` is (`qualified_package_name`, now exported so
the two cannot drift).
* Each package that declares a gating action gets one phony over its outputs,
and every compile edge of that package takes it as an order-only
prerequisite. Per package, not per build: `include_dir` colours only the
declaring package's own TUs, and a build-wide edge would express a
dependency that does not exist while landing on the critical path.
* `check_action_ordering` scans the emitted manifest and fails the build if
any such edge is missing one — including the denominator, because "every
edge that should carry it does" is vacuously true when none does, which was
exactly the previous state. Seven call sites append the string today; being
careful at seven sites is not a mechanism.
* `blocking` on a `check` now does what it has been documented to do since it
was introduced. It was typed, emitted over the build-program protocol,
parsed, documented in two languages and demonstrated in a shipped example —
and read by nothing.
* Placeholders are no longer written for outputs that are not translation
units. The scan never reads a header, and the empty file only ever turned
"the generator did not run" into "the header is empty".
── mcpp#533: an empty link unit, reported as a shell error ──────────────────
A dependency whose `install()` was skipped over a package-identity collision
left a version directory with no sources. mcpp planned its shared library
anyway and the user was shown `/bin/sh: 1: -shared: not found`.
* A link unit with no inputs is refused at plan time, naming the target. The
static case is why this is an error rather than a better linker message:
`ar rcs` with no members exits 0 and writes an 8-byte archive, so the build
REPORTED SUCCESS and every consumer failed later with undefined symbols.
* `cc` is emitted unconditionally, for the reason `c_ldflags` twenty-six
lines below already carried (mcpp#426). The rule had been written down for
one variable of `c_link`/`c_shared` and not for the other.
* `check_rule_commands_name_a_program` scans the manifest for the class: a
rule's command must begin with a program. Deliberately not the more obvious
"no undefined variables" — ninja's empty expansion is a feature several
rules rely on (`$soname_flag`, `$unit_ldflags`), and that check would have
needed an allowlist of exceptions.
* `.mcpp_ok` is no longer written from the installer's exit code plus the
existence of a directory the installer creates before doing any work. It
now requires one entry that neither mcpp nor xlings wrote. Withheld rather
than fatal, because a package may legitimately install no payload.
* The same predicate runs on the fast path, so a store already poisoned by
this bug heals on the next build instead of requiring the user to know
which directory to delete.
* The lib-root warning asked `has_lib_target` — "does this produce a library"
— when the property it wants is "is this a C++ module library". A
source-built C package warned that `src/<name>.cppm` was missing in every
consumer's build.
── Tests ───────────────────────────────────────────────────────────────────
e2e 314 (a dependency generating its only header), 315 (blocking gates the
compile, non-blocking does not), 316 (empty shared AND static targets refused;
a populated one still builds). All three were run against the pre-fix binary
and all three fail there, so they discriminate rather than describe.
Unit: 14 new cases over the two emitter guards, the ordering denominator, and
the install-marker evidence — including the poisoned-store heal.
Analysis and cross-repo plan: .agents/docs/2026-08-30-*.md
Refs #533, #5341 parent ab1da5d commit f519dfb
19 files changed
Lines changed: 2550 additions & 52 deletions
File tree
- .agents/docs
- docs
- zh
- modules
- buildmcpp/src
- manifest/src
- versioning/src
- src
- build
- fallback
- modgraph
- pm
- tests
- e2e
- unit
Lines changed: 837 additions & 0 deletions
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
315 | | - | |
| 314 | + | |
| 315 | + | |
316 | 316 | | |
317 | 317 | | |
318 | 318 | | |
319 | | - | |
320 | | - | |
321 | | - | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
322 | 335 | | |
323 | 336 | | |
324 | 337 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
277 | 277 | | |
278 | 278 | | |
279 | 279 | | |
280 | | - | |
281 | | - | |
| 280 | + | |
| 281 | + | |
282 | 282 | | |
283 | 283 | | |
284 | 284 | | |
285 | | - | |
286 | | - | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
287 | 296 | | |
288 | 297 | | |
289 | 298 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
387 | 387 | | |
388 | 388 | | |
389 | 389 | | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
390 | 398 | | |
391 | | - | |
| 399 | + | |
| 400 | + | |
392 | 401 | | |
393 | 402 | | |
394 | 403 | | |
| |||
773 | 782 | | |
774 | 783 | | |
775 | 784 | | |
776 | | - | |
| 785 | + | |
| 786 | + | |
777 | 787 | | |
778 | 788 | | |
779 | 789 | | |
| |||
788 | 798 | | |
789 | 799 | | |
790 | 800 | | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
791 | 806 | | |
792 | 807 | | |
793 | 808 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
303 | 315 | | |
304 | 316 | | |
305 | 317 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
0 commit comments