Skip to content

batch: #261 windows cmdline, #257 clang depfile, #258 conditional per-glob flags, #254 host/target axis — 0.0.102#264

Merged
Sunrisepeak merged 13 commits into
mainfrom
fix/batch-254-261
Jul 21, 2026
Merged

batch: #261 windows cmdline, #257 clang depfile, #258 conditional per-glob flags, #254 host/target axis — 0.0.102#264
Sunrisepeak merged 13 commits into
mainfrom
fix/batch-254-261

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Jul 21, 2026

Copy link
Copy Markdown
Member

Four issues in one batch, plus the #256 known-issue canary. Design:
.agents/docs/2026-07-22-v0.0.102-batch-254-261-design.md; the triage that
decided which of the six reported issues were real:
.agents/docs/2026-07-22-issue-triage-254-261.md.

Two architectural threads run through all four:

  • One decision, one derivation. #254 and #258 are both the same
    shape: a decision derived in two places that had drifted apart.
  • Failures must be audible (new invariant). Three of the four issues
    were the engine quietly doing less when a precondition was not met.

#261 — windows scan-deps hits the 8191-char limit

The clang scan rule produced its P1689 JSON via shell redirection. Ninja
spawns through CreateProcess on Windows, which does not interpret >, so
the rule was wrapped in cmd /c — and cmd.exe caps a command line at 8191,
a quarter of CreateProcess's 32767. Real measurement from the reporter:
8370–8570 chars, all six scan invocations failing, for a package that is
green in its own CI and only fails when consumed as a dependency at a deeper
path.

clang-scan-deps has -o since LLVM 17 (verified on both bundled
toolchains, 20.1.7 and 22.1.8), so the redirect is unnecessary. That also
makes the clang branch match what the GCC (-fdeps-file=) and MSVC
(/scanDependencies) branches already do.

But -o only raises the ceiling; it does not address the axis.
local_include_flags() emits one -I per dependency include dir with no
upper bound, and five rules inline that payload. So the #247 link-rule
response-file mitigation is generalised to every rule carrying an unbounded
payload. Along the way escape_flag_path switches to generic_string()
the #247 trap one level down, since response-file content is tokenized
GNU-style where backslash is an escape character.

POSIX output is byte-identical, guarded by a test.

#257 — clang reused stale BMIs after a purview #include edit

Editing a file pulled into a module interface via a purview #include did
not retrigger the rebuild under Clang. It failed silently: the wrong
answer arrived looking like a correct incremental build.

Root cause was a conflated predicate from 0.0.97. posixDepfile decided both
whether to emit a depfile and whether to run the awk filter that strips
GCC's reversed module rules. Clang's depfile shape was unverified then, so
the filter was scoped to GCC — and the depfile went with it. Measured now:

clang 20.1.7/22.1.8:  x.o: x.cppm ops.inc          <- one plain rule
gcc 16.1.0:           x.o gcm.cache/x.gcm: x.cppm ops.inc
                      x.c++-module: gcm.cache/x.gcm
                      .PHONY: x.c++-module
                      gcm.cache/x.gcm:| x.o

Clang emits nothing the filter would remove. The gate was protecting against
a shape that does not exist, at the cost of the correctness contract bundled
with it. Split into posixDepfile and needsGnuModuleFilter.

Also fixes the other half of the asymmetry: c_object and asm_object had
no depfile on any toolchain. (Review caught that .s is not preprocessed
while .S is, so they need separate rules — see below.)

e2e 118 drops # requires: gcc and gains a Clang leg.

#258 — per-glob flags in conditional sections

sources was OS-conditional but flags was not, so one manifest covering
three OSes had to present each OS with the other two OSes' flag entries, all
matching zero sources by construction. The vendored-opencv port paid for that
with 703 committed stub files whose only purpose was to give windows TUs
OS-unique paths a global flag table could key on, 32 globs pointing at
them, and ~23 guaranteed dead-glob warnings per build. That is missing engine
expressiveness leaking out as filesystem structure.

The fix is not the feature but the type behind it. mcpp has two conditional
axes — cfg and features — and each hand-picked which build fields it could
carry, picking different subsets. Extracting BuildInputs makes
membership answerable by the type instead of by a list someone must remember
to update. BuildConfig inherits it (~150 read sites unchanged);
ConditionalConfig nests it, which is where the guarantee has to bite.

With that in place #258 is mostly plumbing. Conditional entries append
after the base table, so GNU last-wins makes a per-OS removal
expressible — the windows leg needs -UHAVE_UNISTD_H against a base -D,
which no unconditional overlay can express.

The dead-glob noise disappears structurally: an off-OS entry never enters
globFlags, so there is no hit counter to warn about. The optional = true
escape hatch floated in the issue is deliberately not added — it would be a
second suppression mechanism alongside #253's.

Two exclusions are by category, not convenience: generatedFiles is a
side-effecting materialization action, not an input; featureDefines are
interface contributions that propagate along Public edges. Per-glob
defines are private and per-TU, so those do belong — which is exactly what
the opencv use case needs.

#254 — xpkg per-OS sections keyed on the host, not the target

Every per-OS splice and every xpm version/asset table keyed on a
compile-time host constant, while [target.'cfg(...)'] was evaluated
against the resolved target. Cross builds therefore spliced the host's
section into a dependency built for the target. Native builds cannot observe
this, which is why three-platform CI never caught it.

Both axes were std::string_view, so mixing them cost nothing. They are now
distinct types with no conversion between them and no constructor from a bare
string, and the platform parameter loses its host default — a silent default
is precisely how this survived. Every call site now names its axis; the
classification is in the commit message.

#256 — clang module operator-template canary

No mcpp defect: --precompile succeeds and the crash is inside the Clang
frontend on the import side. But mcpp bundles LLVM, and the hazard lands
squarely on the module-package pattern this project recommends. Adds a
documented rule (every template parameter of an exported operator template
should be pinned by the first argument) with the reporter's workaround,
and a state canary: LLVM 20–22 recorded as crashing, 18–19 as fine, and
the test fails when reality stops matching — a bump that fixes this is as
much a signal as one that re-breaks it.

Reproduced locally across the full matrix: clang 20.1.7 crash, 22.1.8 crash,
system clang 18.1.3 ok, gcc 16.1.0 ok; control without the unpinned overload
ok on both.


Architecture review pass

The whole-branch review found three things, two of them defects this batch
introduced:

  1. diag::flush() was never called in production. The --strict
    promotion mechanism was dead code outside its own unit test, so the one
    degradation this branch adds would print and then be ignored. The channel
    built to stop silent degradation was itself silently degrading. Now
    settled in exactly one place, after the build (degradations are discovered
    during backend emission).
  2. -MMD was applied to .s as well as .S. The C driver preprocesses
    .S but not .s; measured, clang emits two argument unused warnings
    per file and writes no depfile, and ninja's deps = gcc treats an absent
    depfile as an error — so they cannot share a rule. Split into
    asm_object and asm_object_raw.
  3. The feature axis was still folding its flags field-by-field, so the "one
    funnel for both axes" claim held for one axis. Now routed through
    append(), with the two genuinely-different feature tables documented as
    to why they stay out.

Self-review separately caught an ordering bug in the #254 change: the
target platform was computed before overrides.target_triple is filled from
[build] target and canonicalized, so a project setting its target in the
manifest would have fallen back to the host — the same bug, one level up.

Verification

  • unit: 36/36
  • e2e: 139 passed, 5 skipped; the 3 failures (22, 54, 62) are this machine's
    pre-existing environmental baseline (54/62 exit 127 on a missing
    ~/.xlings gcc binary), unchanged by this branch
  • new e2e 148 (wide include list), 149 (conditional per-glob flags, four
    quadrants), 150 (#256 canary); 118 extended with a Clang leg
  • #257 verified by hand under llvm 20.1.7 and 22.1.8
  • #261's -o and @rsp pass-through verified against the bundled
    clang-scan-deps

What CI caught that local testing could not

Two of the fixes in this PR exist because the first CI run failed. Both are
worth recording, because they are the class of bug this batch is about.

Windows: the #247 trap one level deeper. The response-file change broke
every import std;:

module file 'D:amcppmcpptargetx86_64-windows-msvcc1ffdc8b90b687f4pcm.cachestd.pcm'
not found

D:\a\mcpp\...\pcm.cache\std.pcm with every separator eaten. Switching
escape_flag_path to generic_string() covered the -I paths this file
builds itself — but $cxxflags carries paths produced in flags.cppm
(-fprebuilt-module-path=, module-file mappings) that are still
native-separated. The response file is scoped to $local_includes only now:
the part whose form this file can guarantee, and the only part that is
actually unbounded. Everything else stays inline, where a backslash is just a
character.

Note only the mcpp test step failed, not the build — the build step runs
the bootstrap mcpp, so a freshly built binary is first exercised when it
compiles the test targets.

macOS: removing a gate exposes a test to a new platform. Dropping e2e
118's # requires: gcc for #257 meant it ran on macOS for the first time,
where BSD sed -i reads its next argument as a backup suffix. Fixed with a
portable in-place edit; the three genuinely new e2e tests pass on macOS
as-is.

Deliberately out of scope

  • #259 — root cause traced to a silent per-dep skip on the xlings side;
    the investigation (which retracts the issue's own attribution and found
    that mcpp's sysroot safety-net has never executed) is posted as a comment
    there.
  • manifest unknown-key policy — five inconsistent policies across
    sections, split out as manifest: unknown-key policy is inconsistent across five sections — [build] and [target.*.build] drop unknown keys silently #263. It has a compatibility surface and #258 does
    not depend on it.
  • The #254 cross-build e2e did not land; the harness lacks a fixture for
    reusing the host registry, and a throwaway MCPP_HOME re-installs the
    whole cross toolchain. Semantics are pinned by unit tests and the gap is
    written down rather than papered over.

… one --strict policy

Every branch that does LESS work because a precondition was not met used to
be silent or debug-logged, which is the same thing from the user's side.
mcpp.diag makes that reporting explicit and mandatory:

  - degraded() requires an 'impact' string, forcing the author to state what
    the user will actually experience — the sentence missing from every
    silent-degradation bug this channel exists to prevent.
  - records dedupe on their whole payload and render as they are reported,
    so warning/status interleaving and early-return behaviour are unchanged.
  - flush(strict) settles the --strict policy in one place.

Migrates prepare.cppm's eight raw println(stderr, "warning: ...") sites
(schema, platforms, feature forwarding/request, scanner, validator) onto the
sink. Rendering still goes through ui::warning, so output is byte-identical
and existing e2e greps keep matching. The 11 ui::warning call sites in other
files are left alone — out of scope for this batch.

Design: .agents/docs/2026-07-22-v0.0.102-batch-254-261-design.md section 2
…per and its 8191 ceiling

The clang scan rule produced its P1689 JSON through shell redirection. Ninja
on Windows spawns via CreateProcess, which does not interpret `>`, so the
rule was wrapped in `cmd /c` — and cmd.exe caps a command line at 8191
chars, a quarter of CreateProcess's 32767.

That ceiling is reached in practice: Sunrisepeak/opencv-m consumed as a
dependency measured 8370-8570 char scan commands (48 -I entries, ~124 chars
of package-root prefix at a workspace-member path), and all six scan
invocations failed with "The command line is too long". The same package
builds green in its own CI, where the path prefix is 23 chars.

clang-scan-deps has -o since LLVM 17 — verified on both bundled toolchains
(20.1.7, 22.1.8) — so the redirect is unnecessary. Writing the output via a
flag also makes the clang branch match what the GCC (-fdeps-file=) and MSVC
(/scanDependencies) branches already do, and collapses the two platform
branches into one rule text.

`cmd /c` was the only shell mcpp put between ninja and a compiler; a
regression test keeps it gone. Note this only raises the ceiling to 32767 —
the include-dir list itself is still unbounded, which the following commit
addresses.
…cal_includes

Dropping the cmd /c wrapper raised the scan rule's ceiling from 8191 to
32767, but did not address the axis: local_include_flags() emits one -I per
dependency include dir with no upper bound, and cxx_module / cxx_object /
c_object / asm_object / cxx_scan all inline that payload. The scan command
is strictly longer than the compile command for the same TU, since it wraps
it, so it just happens to fail first.

Generalises the #247 link-rule mitigation to every rule that carries an
unbounded flag payload: on Windows the payload moves into `rspfile_content`
and the command references `@$out.rsp`. Safe for all consumers — the gcc and
clang drivers and cl.exe expand @file, and clang-scan-deps passes an @file
in its post-`--` command straight through to the driver (verified against
the bundled 20.1.7). nasm_object is deliberately excluded: NASM spells
response files `-@ file`, not `@file`.

Also switches escape_flag_path to generic_string(). This is the #247 trap
one level down: response-file content is tokenized GNU-style, where
backslash is an escape character, so a Windows -I path would lose its
separators once it moved off the command line. Forward slashes are accepted
by every Windows consumer and POSIX output is unchanged.

useCompileRsp keys on `is_windows || msvcDeps` rather than is_windows alone.
The msvc dialect only ever runs on Windows, so this is faithful, and it
makes the response-file shape reachable from a POSIX test host — the same
over-approximation the link rules already use.

POSIX output is byte-identical, guarded by a test asserting the compile
rules stay inline there. New e2e 148 builds a package with 64 include dirs
on every platform: the failure was found from the consumer side, where a
deep dependency path pushed an otherwise-green package over the limit.
… from "filter GCC module rules"

Editing a file pulled into a module interface via a purview #include did not
retrigger the interface rebuild under Clang: the next build reused the stale
BMI, so importers compiled against the old surface. It failed silently, which
is the worst shape a build system can take — the wrong answer arrives looking
like a correct incremental build, and suspicion falls on user code first.
The issue reporter lost a debugging round to it.

Root cause is a conflated predicate from 0.0.97. `posixDepfile` decided BOTH
whether to emit a depfile at all AND whether to run the awk filter that
strips the reversed make-rules GCC's `-fmodules -MMD` bolts on. Clang's
depfile shape was unverified at the time, so the filter was scoped to GCC —
and the depfile went with it. Measured now, on both bundled toolchains:

  clang 20.1.7/22.1.8:  x.o: x.cppm ops.inc
  gcc 16.1.0:           x.o gcm.cache/x.gcm: x.cppm ops.inc
                        x.c++-module: gcm.cache/x.gcm
                        .PHONY: x.c++-module
                        gcm.cache/x.gcm:| x.o

Clang emits a single plain rule — nothing the filter would remove. The gate
was protecting against a shape that does not exist, at the cost of the
correctness contract it was bundled with.

Split into `posixDepfile` (any POSIX non-MSVC toolchain) and
`needsGnuModuleFilter` (GCC only, which alone writes to the scratch
$out.d.raw). Clang writes -MF straight to $out.d.

Also fixes the other half of the same asymmetry: c_object and asm_object had
no depfile on any toolchain, so C and GAS units never tracked their headers.
They take the flag but not the filter — module reversed-rules cannot occur
there.

Windows non-MSVC (mingw gcc / hosted clang) remains without include
tracking, since the GCC filter needs awk. That is now reported through
diag::degraded with its user-visible impact instead of being silent. cl.exe
is unaffected: deps=msvc via /showIncludes is the equivalent mechanism, not
a degradation.

e2e 118 drops `# requires: gcc` and gains a Clang leg exercising both
assertions against the newest installed LLVM (verified locally on 22.1.8).
…l axis may contribute

mcpp has two conditional axes: `[target.'cfg(...)']` (platform) and
`[features.<name>]`. Each hand-picked which build fields it could carry, and
they picked DIFFERENT subsets — cfg took cflags/cxxflags/ldflags/sources,
features took sources/defines/flags. "Which build inputs may be contributed
conditionally" was being decided in two places, differently, which is the
architectural debt the batch ledger names: the same decision derived twice
drifts, and the drift only surfaces when someone tries to use the capability
one axis has and the other silently lacks (#258).

Extracts the additive build inputs into a type. Membership is now answered
BY the type rather than by a list someone has to remember to update, and two
properties qualify a field: appending is its merge semantics, and it is
consumed after the conditional merge point. That excludes the selection axis
(`target` SELECTS the triple its own predicate is evaluated against —
conditioning it is circular) and the resolved policy scalars, which would
need last-wins override semantics: a different operation and a separate
design.

BuildConfig INHERITS BuildInputs rather than nesting it. `buildConfig.cflags`
is read in ~150 places and a BuildConfig genuinely is a set of build inputs
plus selection and policy, so nesting would be 150 mechanical edits buying
nothing. ConditionalConfig, by contrast, nests it — that is where the
guarantee has to bite, and it costs ~20 call sites.

merge_conditional_sources_flags becomes merge_conditional_build_inputs and
folds through a single append(BuildInputs&, const BuildInputs&), so "how does
a contribution combine with the base" has one answer for every axis: append
in declaration order, giving later entries GNU last-wins precedence.

Pure refactor: no behaviour change, and every existing unit assertion passes
unmodified apart from the mechanical cc.<field> -> cc.inputs.<field> path.

Design: .agents/docs/2026-07-22-v0.0.102-batch-254-261-design.md section 5
….)'.build] (xpkg target_cfg parity)

With BuildInputs in place this is mostly plumbing: a conditional section is a
set of build inputs, so it reads through the same entry grammar [build].flags
uses, and the merge already appends whatever BuildInputs carries.

What it unblocks. `sources` was OS-conditional but `flags` was not, so one
manifest covering three OSes had to present every OS with the other two
OSes' flag entries, each matching zero sources by construction. The
vendored-opencv port paid for that with 703 committed stub files whose only
purpose was to give windows TUs OS-unique PATHS a global flag table could
key on, 32 globs pointing at them, and ~23 structurally-guaranteed dead-glob
warnings on every build of every OS. This is the engine's missing
expressiveness leaking out as filesystem structure — the same shape as the
pre-0.0.97 tu-stub machinery that #233 deleted.

Ordering carries the semantics: conditional entries append AFTER the base
table, so under GNU last-wins a conditional rule beats a broader
unconditional one. That is what makes an off-OS REMOVAL expressible — the
windows leg needs `-UHAVE_UNISTD_H` against a base `-DHAVE_UNISTD_H=1`,
which no unconditional overlay can express (a `-U` counter-entry would
itself need to be windows-only, recursing into the same gap).

The dead-glob noise disappears structurally, not by suppression: an off-OS
entry never enters buildConfig.globFlags, so globFlagHits has no counter for
it and scanner.cppm cannot warn. Same shape as #253's fix on the feature
axis. The `optional = true` / `?`-prefix escape hatch floated in the issue
is deliberately NOT added — it would be a second suppression mechanism
alongside #253's "the entry conditionally does not exist", i.e. the same
decision derived twice again.

xpkg's target_cfg gets the same key for grammar parity, and its unknown-key
handling stays a HARD error. Sharing a parser must not be read as licence to
relax a grammar that was already closed; making the unknown-key policy
consistent across all sections is #263, deliberately not bundled here since
it has a compatibility surface and #258 does not depend on it.

e2e 149 asserts the three costs are paid off: matching-OS entries reach the
TU, they override the base table, and off-OS entries produce no warning —
with a control proving a genuinely dead glob in [build].flags still does.
…es; xpkg per-OS sections key on the target

mcpp resolves "which platform?" along two axes that coincide only for a
native build: the host (where mcpp runs — correct for toolchain payloads and
other host tools) and the target (where the produced binaries run — correct
for anything compiled INTO the user's build).

Both were std::string_view, so mixing them cost nothing and read identically,
and they had drifted apart: every xpkg per-OS splice and every xpm
version/asset table keyed on `mcpp::platform::xpkg_platform`, a COMPILE-TIME
HOST constant, while `[target.'cfg(...)']` was evaluated against the resolved
target. The same decision, derived two different ways. Cross-compiling
therefore spliced the host's section into a dependency being built for the
target: its per-OS sources, flags, deps and prebuilt asset all came from the
wrong leg. Native builds cannot observe this, which is why three-platform CI
never caught it.

Introduces mcpp.platform.axis: HostPlatform and TargetPlatform, with no
conversion between them and no constructor from a bare string. APIs take the
common PlatformKey base, so a function states which axis it accepts and a
call site must NAME the axis it supplies. `synthesize_from_xpkg_lua`'s
platform parameter loses its host default and becomes required — a silent
default is precisely how this bug survived. The triple-vs-xpkg vocabulary
translation ("macos" vs "macosx") moves into TargetPlatform::for_os so no
call site has to remember it.

The type change and the behaviour fix land together because they are the
same edit: once the parameter is required, every call site must decide, and
those decisions ARE the fix. Classified per site:

  prepare.cppm dep manifest synthesis (3), pm/resolver version+asset
  selection  -> TARGET; these are code compiled into the user's build.
  toolchain/lifecycle version listing -> HOST; a toolchain payload runs on
  this machine. Its existing comment already said so.
  cmd_xpkg --all-os -> for_lint_of(); neither axis, and named awkwardly on
  purpose so that reads as deliberate at the call site.
  scaffold `mcpp add` and the legacy resolver overloads -> HOST, stated
  explicitly rather than inherited from a default.

Tests: static_asserts that the axes are mutually inconvertible and neither is
constructible from a string; per-OS splice selects each target's own section
and none of the others; and a native-build guard asserting host == target
produces an identical splice, which is the safety net for every existing
platform's behaviour.
…e hazard docs

mcpp has no defect here: --precompile succeeds and the crash is inside the
Clang frontend on the import side. But mcpp bundles LLVM, so the hazard ships
with the toolchain, and it lands squarely on the module-package pattern this
project recommends — wrapping an upstream header whose operators are static
inline templates and mirroring their signatures with a trivially-true
constraint is exactly how you hit it.

Two things it needs, both of which mcpp owns:

Documentation, so the next person does not repeat the bisect: every template
parameter of an exported operator template should be pinned by the FIRST
argument; otherwise deduce whole operand types and constrain them. The
reporter's shipped workaround is included, along with the property that makes
this so hard to attribute — the crash is name-keyed, so one poisoned
operator* makes every `x * y` in every importer crash on unrelated types.

A canary, because the alternative is finding out by accident. It is a STATE
canary rather than a pass/fail contract: the expectation table records
crash for LLVM 20-22 and ok for 18-19, and the test fails when reality stops
matching — a Clang bump that FIXES this is just as much a signal as one that
re-breaks it, and neither should be able to change what packages can express
without anyone noticing. A control build without the unpinned overload guards
the canary itself.

Verified against both bundled toolchains (20.1.7 and 22.1.8 crash as
recorded; system clang 18.1.3 and gcc 16.1.0 compile the same repro fine).
An upstream LLVM report is filed separately; this does not wait on it.
…invariants

Version bump plus the three documentation surfaces this batch owes:

- CHANGELOG entry covering #261 / #257 / #258 / #254 and what was explicitly
  left out (#259, and the unknown-key policy split into #263).
- docs/05-mcpp-toml.md: the new conditional keys, and — more usefully than a
  key list — WHY the set is what it is. A conditional section accepts exactly
  the additive build inputs; linkage/target/profile knobs are excluded
  because they are inputs to target selection or need override semantics.
  Includes the per-OS removal idiom and the "off-target entries do not exist,
  so they cannot warn" rule.
- Batch ledger gains a section 6 for cross-batch invariants: the existing
  "one decision, one derivation" rule with #254/#258 recorded as instances,
  and the new "failures must be audible" rule with its rationale — three of
  this batch's four issues were the same silent-degradation shape, in a code
  base that already practiced the discipline elsewhere but had never made it
  an invariant new code inherits.
…d, .s depfile split, feature axis folds through append

Three findings from the whole-branch review, two of them defects introduced
by this batch.

1. diag::flush() was never called outside its own unit test. The one new
   degradation this branch introduces (#257's windows + non-MSVC "no depfile"
   case) would print its warning and then be ignored, and `mcpp build
   --strict` would still exit 0. The channel built to stop silent degradation
   was itself silently degrading. BuildContext now carries `strict` and
   run_build_plan calls flush() after the build — after, because backend
   emission is where degradations are discovered, so end-of-prepare_build
   would be too early.

2. `-MMD` was applied to `.s` as well as `.S`. The C driver preprocesses `.S`
   but not `.s`, so clang emits two `argument unused during compilation`
   warnings per file and writes no depfile (measured on 20.1.7; gcc writes
   none either, just quietly) — and ninja's `deps = gcc` treats an absent
   depfile as an error, so the cases cannot share a rule. Split into
   asm_object (.S, tracked) and asm_object_raw (.s, pre-#257 shape).

3. The feature axis was still folding its per-glob flags field-by-field
   rather than through append(BuildInputs&), so the "one funnel for both
   axes" claim held for only one axis. Now routed through append. The other
   two feature tables stay out on purpose and the comment says why: feature
   `sources` carry DROP-then-ADD semantics, and feature `defines` are
   interface contributions that propagate along Public edges — neither is a
   plain append, so neither belongs in BuildInputs.

Also fixes an ordering bug found while re-reading the #254 change: the target
platform was computed at the top of prepare_build, but overrides.target_triple
is only filled in from `[build] target` / the config default and canonicalized
some 200 lines later. Any project that sets its target in the manifest rather
than on the command line would have silently fallen back to the host axis —
the exact bug #254 is about, reintroduced one level up. Moved below the
resolution block, with a comment recording why it cannot move back.
The planned mingw-cross e2e consuming an xpkg dep with per-OS sections did
not land: it needs a real cross build inside a throwaway MCPP_HOME, and a
fresh home re-installs the entire cross toolchain (measured >10 min). The
e2e harness has no fixture for reusing the host registry while injecting a
local index and a pre-placed payload.

Rather than ship a test that times out, the semantics stay pinned by three
unit tests and the gap is written down in both the design doc and the
changelog, with what building the fixture would take.
…caught the #247 trap one level deeper

The Windows leg failed with

  module file 'D:amcppmcpptargetx86_64-windows-msvcc1ffdc8b90b687f4pcm.cachestd.pcm'
  not found

i.e. D:\a\mcpp\mcpp\target\...\pcm.cache\std.pcm with every separator eaten.
Response-file content is tokenized GNU-style, where backslash is an ESCAPE
character. Switching escape_flag_path to generic_string() covered the -I
paths this file builds itself, but $cxxflags carries paths produced
elsewhere — flags.cppm's -fprebuilt-module-path= and module-file mappings —
which are still native-separated. Moving those off the command line broke
every `import std;`.

The fix is to scope the response file to what this file can guarantee the
form of: $local_includes only. $cxxflags / $unit_cxxflags / $asmflags stay
inline, where a backslash is just a character. That is also sufficient for
#261: the unbounded axis is one -I per dependency include dir, and the rest
of the payload is bounded. Making every flag producer emit forward slashes
would be the more general fix, but it is a much wider change than this batch
should carry, and it is not needed for the ceiling.

Only the `mcpp test` step failed, not the build: the build step runs the
bootstrap mcpp, so the freshly-built binary is first exercised when it
compiles the test targets.

Also makes e2e 118 use a portable in-place edit. Dropping its
`# requires: gcc` gate in #257 meant it ran on macOS for the first time,
where BSD sed reads `-i`'s next argument as a backup suffix and swallows the
script. The other three new e2e tests pass on macOS as-is.
…a capability it lacks

Windows plus a GNU-dialect toolchain — which is what the CI leg's clang is —
genuinely cannot track textual includes: the depfile GCC emits for a module
TU needs an awk filter to be loadable by ninja, and native Windows has no
awk. #257 does not fix that platform; it makes the engine SAY so through
diag::degraded, and the Windows CI log carries that message 13 times.

Dropping the `# requires: gcc` gate pointed the test at that platform for
the first time, where it asserted a rebuild that cannot happen. Asserting
the capability there would be wrong, and re-gating the test to POSIX would
put the gap back out of sight.

So on Windows the test now asserts the degradation IS reported and stops.
That is the stronger contract: silence is the actual defect, and this pins
it. Everywhere else the rebuild assertions are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant