Skip to content

refactor(toolchain): one producer for host-compile flags; build.mcpp gains MSVC modules (2026.8.2.2) - #334

Merged
Sunrisepeak merged 8 commits into
mainfrom
feat/host-compile-single-producer
Aug 2, 2026
Merged

refactor(toolchain): one producer for host-compile flags; build.mcpp gains MSVC modules (2026.8.2.2)#334
Sunrisepeak merged 8 commits into
mainfrom
feat/host-compile-single-producer

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Design: .agents/docs/2026-08-02-host-compile-single-producer-design.md
Plan: .agents/docs/2026-08-02-host-compile-implementation-plan.md

Why

Three places assembled the same host-compile flags independently — flags.cppm
for the main build, stdmod.cppm for the std module, build_program.cppm for
build.mcpp. The resolvers were already shared; the assembly was not,
because the seam only produced strings and the one consumer that needs argv
(build.mcpp, which execs directly with no shell) could not use it.

Every bug in PR #332's batch was that same sentence: flags.cppm knew about
quoting / the macOS deployment target / the MSVC dialect, and the other two did
not. A 0.0.9x fix had already corrected the same file once for the same reason,
and stdmod.cppm carried a comment telling the reader to keep it in sync with
flags.cppm by hand — the failure mode this repo wrote check_version_pins.sh
to stop.

What changed

Tokens are the source of truth; strings are rendered from them.
mcpp.toolchain.hostflags composes the resolvers once; the three consumers
render with ninja escaping, shell quoting, or not at all.

The HostFlagOptions knobs are documented divergences, not switches preserving
accidents — the host helper keeps trusting clang's cfg on macOS/Windows (the
macOS link needs the libc++abi/unwind handling the main build's
needs_explicit_libcxx path owns), the std module states -stdlib=libc++
explicitly (flags.cppm cannot — the same string feeds C compiles), and
binutils -B / runtime dirs belong only to the one-shot host link.

build.mcpp gains named modules under MSVC as a consequence, not as a
feature.
The gate said cl.exe's .ifc pipeline was unimplemented; it was
not — e2e 99 has been producing .ifc artifacts on every Windows CI run.
build_mcpp_module now dispatches on BmiTraits + CommandDialect, so the
row that already existed is all it took.

Testing

The refactor is verified as a no-op, which is the only way to review a
change to compute_flags (it serves every build): the global
cxxflags/cflags/ldflags lines in build.ninja and stdmod's
std_build_commands are byte-identical before and after, under both
gcc@16.1.0 and llvm@22.1.8, each from a clean target/ and dep cache.
Byte-identity is not cosmetic — std_build_commands feeds the std cache
directory name, so a reordered flag would invalidate every user's std BMIs for
no gain.

  • unit 50/50 (new test_hostflags.cpp, including a capability-parity guard
    that fails when a CompilerId is missing a dialect / module / flags row)
  • all 14 build.mcpp e2e under gcc, and 89/92/112/179/181 under clang —
    covering glibc, musl (168) and mingw-cross (112)
  • e2e 180's third section flipped from "assert refusal" to "assert modules
    work", and asserts a real .ifc came out
  • check_version_pins.sh clean

hostflags is its own module rather than a helper inside
build_program.cppm's anonymous namespace: PR #332 established that adding a
function there can miscompile a neighbouring one under clang 22.1.8 + C++20
modules + -O2.

Three places assembled the same host-compile flags independently:
flags.cppm for the main build, stdmod.cppm for the std module, and
build_program.cppm for build.mcpp. The resolvers (linkmodel, clang driver
model) were already shared; the ASSEMBLY was not, because the seam only
produced strings and the one consumer that needs argv — build.mcpp, which
execs directly with no shell — could not use it and hand-wrote the whole
thing a second time.

That split is where PR#332's batch came from. Every bug in it was the same
sentence: flags.cppm knew about quoting / the macOS deployment target / the
MSVC dialect, and the other two did not. A 0.0.9x fix had already corrected
the same file once for the same reason (musl->static re-derived), and
stdmod.cppm carried a comment instructing the reader to keep it in sync with
flags.cppm by hand — the failure mode this repo wrote check_version_pins.sh
to stop.

Tokens are now the source of truth and strings are rendered from them, not
the reverse. mcpp.toolchain.hostflags composes the resolvers once; the three
consumers render with ninja escaping, shell quoting, or not at all.

The knobs on HostFlagOptions are documented divergences, not switches to
preserve accidents: the host helper keeps trusting clang's cfg on
macOS/Windows (the macOS link needs libc++abi/unwind handling the main
build's needs_explicit_libcxx path owns), the std module states
-stdlib=libc++ explicitly (flags.cppm cannot — the same string feeds C
compiles), and binutils -B / runtime dirs belong only to the one-shot host
link.

Pure refactor, verified as such: the global cxxflags/cflags/ldflags lines in
build.ninja AND stdmod's std_build_commands are byte-identical before and
after, under both gcc@16.1.0 and llvm@22.1.8, each from a clean target and
dep cache. Byte-identity is not cosmetic here — std_build_commands feeds the
std cache directory name, so a reordered flag would invalidate every user's
std BMIs for no gain.

hostflags lives in its own module rather than inside build_program.cppm's
anonymous namespace: PR#332 established that adding a function there can
miscompile a neighbouring one under clang 22.1.8 + C++20 modules + -O2.
`import mcpp;` / `import std;` in a build.mcpp used to fail under native MSVC
with "not yet supported", and the stated reason was that mcpp had not wired up
cl.exe's .ifc pipeline. That reason was wrong: the main build has compiled
named modules with cl.exe for a while — e2e 99 produces real .ifc artifacts on
every Windows CI run, and ninja_backend has emitted /interface /TP /ifcOutput
and /scanDependencies all along. What was missing was only that build.mcpp
hand-rolled its own compile path and never read those tables.

build_mcpp_module now dispatches on BmiTraits + CommandDialect — the same rows
the main build uses — so cl.exe needed no new pipeline, just the row that was
already there. The gate is gone.

bmi_reference_tokens covers the shape difference the traits do not have to
care about: `-fmodule-file=std=<p>` is one argv word, `/reference std=<p>` is
two, and only an argv consumer notices. The traits keep storing the string
form for the ninja channel.

The capability-parity guard in test_hostflags.cpp is the part meant to
outlast this change: it walks every CompilerId and fails if a family is
missing a dialect row, a module row, or host flags. Stating "build.mcpp has no
capability list of its own" in prose is exactly how the MSVC gate outlived its
own justification.
…ut it

Placing it after the libc++ -isystem flags instead of right after
-nostdinc++ changed the std module's command string, and that string is part
of the std cache identity (std_build_commands feeds the cache directory
name) — every user's std BMIs would have been invalidated for a reordering
that buys nothing. Caught only because the first byte-equality check scanned
the whole shared cache directory and compared stale entries alongside fresh
ones; the check now wipes the std cache first so it compares like with like.

Also moves the bundled mcpp module's compile out of build_program.cppm's
anonymous namespace into mcpp.build.hostprogram. Growing that namespace
reproduced PR#332's clang miscompilation exactly — Segmentation fault: 11 on
every macOS build.mcpp e2e, in contract_env, which this change never touched.
Byte-equality with what earlier releases emitted is a compatibility surface
here, not an implementation detail: stdmod folds its compile command into
std_build_commands and the std cache directory name is derived from the
metadata containing it, so a reordered flag invalidates every user's std BMIs.

Verifying that by inspecting the cache after a build turned out to be
unreliable — the cache is shared and accumulates entries from every toolchain
used since, so the first check compared stale entries alongside fresh ones and
passed while the string had in fact moved. These tests assert the spelling
directly from a synthetic model, deterministically, and keep asserting it.
…'s cfg; drop -x for cl

Two failures the local runs could not see, both the same shape — a branch
that was dead while the MSVC gate existed, or a platform this machine is not.

host_compile_tokens returned early on the trust-cfg path, which on macOS IS
the build.mcpp path: no clang flags and no deployment target, so the std BMI
(built for 14.0) was rejected by a TU compiled without it. Trusting the cfg
means contributing no include paths or stdlib selection — not contributing
nothing. The deployment target is emitted regardless now, which is what the
hand-written host_base_flags did deliberately ("FIRST and unconditionally")
before this refactor absorbed it.

The `-x none` before the module objects was unconditional and harmless only
while cl.exe could not reach it. Removing the module gate made the dead
branch live, and cl answered `D9002: ignoring unknown option '-x'` — after
compiling the .ifc and reaching the link, which is further than build.mcpp
has ever gone under MSVC.
cl.exe rejected the std and mcpp module objects with
"C2018: character 'U+10' is not permitted here" — it was compiling them as
C++ source. `/TP` is not the counterpart of `-x c++`: GNU's is positional and
lasts until `-x none`, cl's applies to EVERY input on the line, so the
objects that follow are fed to the C++ frontend.

The dialect now carries the per-FILE form (`/Tp<file>`) alongside the
positional one, and the build.mcpp compile uses whichever the driver has.
That is the same structural difference the row already records for libFlag
(prefix vs suffix): the two drivers are not spelling the same concept
differently, they have different concepts.
cl answered "C2230: could not find module 'std'" because the std reference
was still built by string concatenation, producing one argv element with a
space inside it. bmi_reference_tokens existed for exactly this and was only
applied to the bundled mcpp module.

Same shape for the third time — a table entry written for the ninja STRING
channel concatenated into an argv element — so it now has a test that covers
every family and both spellings, plus one for the language-force tokens. The
compiler's own diagnostics name neither the flag nor the reason, which is why
this kept costing a CI round each time.
@Sunrisepeak
Sunrisepeak merged commit 655e58d into main Aug 2, 2026
17 checks passed
@Sunrisepeak
Sunrisepeak deleted the feat/host-compile-single-producer branch August 2, 2026 04:09
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