Skip to content

The compile side closes the host's C library the link side already dropped (#662) - #664

Merged
Sunrisepeak merged 5 commits into
mainfrom
fix/662-graph-target-header-isolation
Sep 17, 2026
Merged

Sunrisepeak merged 5 commits into
mainfrom
fix/662-graph-target-header-isolation

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 17, 2026

Copy link
Copy Markdown
Member

Summary

Closes #662.

The link side has read plan.targetSide.cAbi.prebuilt() since #511 and drops
-nostdlib whenever a package supplies the target's C library. The compile
side never got the same treatment: clang's own driver kept searching the
host's system directories even when a graph package supplied the C library,
so a build over openkal could silently mix the host's headers with the
graph's C library, or fail with a bare "file not found" on a host that lacked
the matching headers. There is a same-shaped C++ twin: the existing
-nostdinc++ condition was false exactly when BOTH the C library and the C++
runtime came from the graph — the arrangement openkal actually ships — so the
driver kept finding the host's libstdc++ beside itself. Neither half is
reached by the issue's own minimal repro, which only import stds.

  • M1mcpp.toolchain.hostflags appends -nostdlibinc when the C
    library comes from the graph, and the -nostdinc++ condition now reads
    only whether the C++ layer comes from the graph. Both live in the shared
    cflags/cxxflags/asmflags producer, so C, C++, asm, clang-scan-deps
    and the std module precompile all see them.
  • D4 (GCC) — GCC has no one-token equivalent (the shape would be
    -nostdinc plus -isystem <gcc -print-file-name=include> and
    <…/include-fixed>), so hostflags.cppm makes no change to GCC's command
    line at all — it never emitted an isolation token before this PR and still
    does not. What GCC gets is a degradation, not silence and not a refusal:
    mcpp::diag::degraded reports once, in prepare.cppm, when the resolved
    C library comes from the graph and the resolved compiler is not
    Clang-family — naming the library and the compiler, pointing at a
    Clang-family toolchain as the one that isolates. --strict promotes it to
    an error through the existing diag::flush policy. This shape is the third
    one tried: a hard refusal at resolution was tried first and reverted after
    CI caught it firing before the target-side report prints, which broke
    three pre-existing e2e fixtures (268, 282, 303) that use a synthetic
    provides = ["mcpp:c-abi=..."] package on this host's native, GCC-default
    target for something unrelated to isolation — see the review history on
    this PR for the measurement. 268/282/303 are re-verified green with the
    degradation in place (e2e 740).
  • M2mcpp::build::graph_c_library_isolation_advice, the compile-side
    sibling of link_failure_advice: on a failed build whose raw ninja output
    carries both the isolating token and "file not found", it appends a note
    naming the C library and two remedies, without rewriting the compiler's
    own line.
  • M5 — verified (not assumed) that a [feature-deps.<feature>] entry can
    already stay private to its declaring package (visibility = "private").
    The capability already existed and was undocumented; docs/06 and docs/24
    (English + zh) now state the pattern and the layer boundary.
  • Docs/CHANGELOG — docs/05, docs/06, docs/22, docs/24 (and zh) updated;
    CHANGELOG entry describing upgrade impact, including the corrected D4
    account.
  • Version bumped to 2026.9.17.3 (mcpp.toml, modules/versioning).

Test plan

  • Unit: mcpp test test_hostflags — the option matrix
    (cAbiPrebuilt x cxxFromGraph x {clang, gcc}), the openkal-shape
    double-graph regression test, and the byte-identical guard for
    payload-served builds (GCC's command line is unaffected by this PR,
    asserted directly). All pass.
  • Unit: mcpp test test_ninja_backendgraph_c_library_isolation_advice
    matching rules. All pass.
  • Unit: full suite (mcpp test, no filter) — 121/121 files pass, 0 failed
    (re-run after every substantive change on this branch).
  • e2e 738 (new): openkal x86_64-windows-gnu, each unit's real command
    re-run with -v -fsyntax-only, header search list asserted store-only.
    Confirmed RED on main (host mingw and host libstdc++ both leak
    in) and GREEN on this branch, reproduced multiple times each way.
  • e2e 739 (new): a private [feature-deps] entry does not reach the
    consumer, both directions on the same two packages.
  • e2e 740 (new): GCC over a graph-supplied C library gets the
    degradation, names the library and the compiler, the target-side
    report still prints, and a Clang-family toolchain over the same graph
    gets no such note (the harness's own validity check).
  • e2e regression sweep, including the ones the reverted hard-refusal
    broke: 268, 281, 282, 292, 295-298, 301-304, 328, 619 (target-side /
    toolchain-origin invariants), 286 (full openkal stack — static link,
    no INTERP, runs), 130/133 (freestanding), 736 — all green.
  • bash .github/tools/check_version_pins.sh,
    check_docs_style.sh, check_docs_structure.sh — OK.
  • CI: all 41 checks green (1 legitimate skip:
    iOS - what this runner actually provides).

Review history

An earlier revision of this PR (commits up to d82137b6) implemented D4 as
a hard refusal in prepare_build. CI's invariants (linux-x86_64) job
caught what local testing had not: it broke 268, 282 and 303 by firing
before the target-side report prints, which is exactly what 268's own
comment says must never happen. bcbb3e04 reverted the refusal; 356c77ec
replaces it with the degradation described above, which keeps the same three
files green while still surfacing the gap rather than leaving it silent.

Not done / deferred (by design, see plan §3.2 and §5)

  • mcpp-index's compat.zlib POSIX branch under cfg(all(windows, c-abi = "musl")) (issue's actual compat:libarchive failure) is a separate PR
    in mcpplibs/mcpp-index
    , pinned to this release, per the plan's I1–I3.
    It cannot land before this PR is released.
  • The iOS row (Apple SDK C library + graph libc++) and bare-metal targets are
    covered by existing e2e (130/133/730s) staying green plus the unit-test
    guard against a duplicated -nostdinc++; no macOS runner was available
    locally to re-verify the iOS leg directly (CI's iOS job is green).

…opped (#662)

Target-side header isolation had a half. The link side has read
`plan.targetSide.cAbi.prebuilt()` since #511 and dropped `-nostdlib` whenever
a package supplies the target's C library; the compile side never got the
same treatment, so clang's own driver kept searching the host's system
directories. On a machine that happens to have the matching host headers
(mingw-w64, in the issue's repro) the two C libraries' declarations collide
and the error names neither the host header nor why it was read; on a machine
without them the failure is a bare "file not found" with no explanation.
There is a same-shaped C++ twin: the existing `-nostdinc++` condition asked
`!graphSuppliesTarget && !cxxFromPayload`, which is false exactly when BOTH
layers come from the graph — the openkal shape — so the driver kept finding
the host's libstdc++ beside itself. The issue's own minimal repro only
`import std`s and never text-includes a header, so it exposed neither half.

- `mcpp.toolchain.hostflags` appends `-nostdlibinc` when the C library comes
  from the graph, and fixes the `-nostdinc++` condition to read only whether
  the C++ layer comes from the graph. Both tokens live in the global
  `cflags`/`cxxflags`/`asmflags` producer, so every consumer (C, C++, asm,
  `clang-scan-deps`, the std module precompile) gets them identically.
- GCC has no one-token equivalent. `can_isolate_graph_c_library` states the
  capability in the toolchain model (Clang only); `prepare.cppm` refuses the
  combination at resolution when it would otherwise reach an unisolated
  build, rather than silently skipping isolation. No target row needs the
  GCC form today.
- `mcpp::build::graph_c_library_isolation_advice`, the compile-side sibling
  of `link_failure_advice`: when a failed build's raw ninja output carries
  both the isolating token and a "file not found", it appends a note naming
  the C library and two remedies (adapt on `cfg(c-abi = "...")`, or bring the
  platform headers into the graph as a private dependency of the package that
  needs them) without rewriting the compiler's own line.
- M5: verified rather than assumed that a `[feature-deps.<feature>]` entry
  can already stay private to its declaring package (`visibility =
  "private"`, folding only into `privateBuild`, never into `publicUsage`) —
  measured both directions with local path packages before writing e2e 739.
  The capability already existed and was undocumented; docs/06 now states
  the pattern and docs/24 gains a "boundary" section (kernel-abi vs c-abi,
  one C/C++ runtime per image, adapt on the differing layer, no source-level
  implementation detection) — both in English and zh.
- Unit tests: the option matrix (`cAbiPrebuilt` x `cxxFromGraph` x
  {clang, gcc}), a byte-identical regression guard for payload-served
  builds, and `graph_c_library_isolation_advice`'s own matching rules.
- e2e 738: openkal's `x86_64-windows-gnu` build, each C/C++ unit's actual
  command from the build database re-run with `-v -fsyntax-only`, asserting
  clang's own header search list names only store paths. Red before this fix
  (confirmed locally, both the C and the C++ twin), green after. Gated on a
  `mingw-host-headers` capability (the criterion has no discriminating power
  without a host mingw to have leaked in); `openkal-cross.yml`'s
  `ecosystem-e2e` job installs `mingw-w64` so it holds in CI, since `llvm` —
  which 738 also requires — is never in `run_all.sh`'s detected CAPS on any
  sharded runner.
- e2e 739: the private-feature-dep guarantee, in both directions on the same
  two packages so the harness cannot pass vacuously.
- Version 2026.9.17.3.
CI's check_docs_structure.sh caught this — python3 .github/tools/gen_agents_index.py.
docs/06's new 'A platform SDK dependency stays private' section cited
[05 — Dependencies] for the visibility field, and that field (public /
private / interface, defaulting to public) was not documented anywhere.
Adds it, in English and zh, at the point in docs/05 where the other
per-edge dependency-spec fields are introduced.
CI's "invariants (linux-x86_64)" job turned up what local testing had not:
268, 282 and 303 all went red. The common shape is a `provides =
["mcpp:c-abi=..."]` fixture package with no real headers, used to exercise
something the target-side report says — none of them about isolation —
under this host's ambient default toolchain, which is gcc. The refusal
fired before the target-side report line ever printed, which is exactly
what 268's own comment says must not happen ("the resolution is reported
during planning ... before a single object is compiled ... complete
whether or not the link afterwards succeeds"): a hard refusal at that
point is not a stricter report, it is the report's replacement.

GCC's behaviour on this combination reverts to precisely what it was before
#662: `hostflags.cppm` never emitted `-nostdlibinc` for GCC in the first
place (`bypassCfg` is already false there — no `<driver>.cfg` beside a GCC
binary), so nothing about GCC's own compile command changes. What is
removed is the extra, invented gate in `prepare_build` and the
`can_isolate_graph_c_library` predicate it read, both added preemptively
and unsupported by anything the plan actually required — measured against
the real test suite rather than against a hypothesis about it.

- `prepare.cppm`: the `LayerRequirement` refusal for "graph-supplied C
  library + a compiler that cannot isolate" is removed.
- `modules/toolchain-model`: `can_isolate_graph_c_library` is removed —
  unused once nothing enforces it, and its comment's claim about
  `prepare.cppm` would otherwise go stale silently.
- `hostflags.cppm`, `test_hostflags.cpp`: comments corrected to state GCC's
  actual, unchanged behaviour instead of a removed enforcement.
- Re-verified: 268, 282, 303 (the three CI caught), plus 292, 304, 328, 619,
  281 (every other e2e using a synthetic c-abi/kernel-abi provider), 286
  (the real openkal stack), 295-302 (the native-target and toolchain-origin
  invariants), 130/133 (freestanding), 736, and 738/739 (this change's own
  new tests) — all still green. Full unit suite: 121/121 files, 0 failed.
…aded

The revert (bcbb3e0) removed the hard refusal for "graph-supplied C
library + a compiler that cannot isolate" because it broke the "report
before refusing" contract. That left GCC silent again on this combination
-- which is what D4 actually ruled out. This adds the third option: a
degradation, not a refusal and not silence.

`mcpp::diag::degraded("target/c-abi-isolation", ...)` fires once resolution
knows the C library came from the graph and the resolved compiler is not
Clang-family. It names the C library (interface and package@version) and
the compiler, points at a Clang-family toolchain as the one that isolates,
and changes nothing about the command line -- `hostflags.cppm` was already
not emitting an isolation token for GCC, and still is not. `--strict`
promotes it to an error through the existing `diag::flush` policy, which
is the one place that decision already lives.

- `prepare.cppm`: the check sits right where `resolvedTargetSide` becomes
  available, beside the existing `target/compiler-runtime` degradation,
  and before `format_report` prints -- so the report is unaffected either
  way, which is what the earlier refusal got wrong.
- e2e 740: names the library and the compiler in the warning, asserts the
  target-side report still prints (the exact thing the reverted refusal
  broke), and controls against a Clang-family toolchain over the same
  graph getting no such note. 268, 282, 303 re-verified green.
- CHANGELOG's D4 bullet rewritten to describe what this branch actually
  does, not the removed refusal.
@Sunrisepeak
Sunrisepeak merged commit 9f15525 into main Sep 17, 2026
45 of 47 checks passed
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.

Cross-compiling a C package to x86_64-windows-gnu reads the host's mingw headers, and fails where they exist

2 participants