Skip to content

docs(b3): target-aware artifact naming — the real symptom is a relink every build - #341

Closed
Sunrisepeak wants to merge 3 commits into
mainfrom
docs/b3-artifact-naming
Closed

docs(b3): target-aware artifact naming — the real symptom is a relink every build#341
Sunrisepeak wants to merge 3 commits into
mainfrom
docs/b3-artifact-naming

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 3, 2026

Copy link
Copy Markdown
Member

Docs only. Writes up the B3 follow-up filed in #339, and corrects what that PR's
§6.5 claimed
— the original description of the symptom was wrong.

What §6.5 got wrong

It said B3 was "symmetrically wrong":

direction claimed actual
Windows → linux-musl mcpp.exe for an ELF ✅ correct
Linux → windows-gnu mcpp for a PE false

Measured on a Linux host:

$ mcpp build --target x86_64-windows-gnu
$ find target -type f -path "*/bin/*"
target/x86_64-windows-gnu/751cb693195ee82c/bin/b3probe.exe
$ file …/b3probe.exe
PE32+ executable (console) x86-64, for MS Windows

The name is right, because mingw's GCC driver appends .exe itself when the
-o name has no extension. mcpp never participates in that decision.

What is actually wrong, and why it matters more

ninja is told one thing and GCC does another:

build bin/b3probe : cxx_link obj/main.o     # declared
$ test -f …/bin/b3probe && echo EXISTS || echo MISSING
MISSING                                      # GCC wrote b3probe.exe

The declared output never exists, so ninja reruns the link edge on every
build
. Verified by mtime across two consecutive builds:

mtime before:        1785736156
mtime after rebuild: 1785736180
RESULT: RELINKED

Incremental builds are effectively off for PE targets — the path CI exercises
daily. So the asymmetry is real but inverted from what §6.5 said: Linux→Windows
is a functional defect, Windows→Linux is only cosmetic.

Nothing caught it because 102_mingw_cross_wine.sh looks for the real
artifact (find -name '*.exe'), not for what ninja declared. Both its
assertions hold while the inconsistency sits underneath them — same shape as the
explicit-ninja-goals regressions.

Two further findings

Naming is an (os, env) function, not an os one. windows-gnu uses the GNU
convention (libfoo.a); only windows-msvc is foo.lib. The current _WIN32
branch hardcodes the latter — so building a static library with mingw on a
Windows host is already misnamed today, a pre-existing defect unrelated to cross
compilation.

The blast radius is much smaller than §6.5 feared. e2e and CI need
essentially no changes, precisely because they match the real artifact rather
than the declaration. §6.5 expected to have to touch the mingw e2e and the
release packaging paths; neither turns out to be true.

Scope of the eventual fix

Confined to src/build/plan.cppm. The other 15 exe_suffix references locate
ninja / xlings / clang++ on the build machine — correct host semantics,
must not be touched. Verified one by one.

Doc includes the commit layering (stub-first so the assertions go red),
the verification criteria — the load-bearing one being a no-relink e2e assertion,
since that is the only thing that can falsify "fixed" — and two open questions
for you: whether the foo.liblibfoo.a correction ships in the same PR
(it changes a host build's output name), and that PE import libraries are
entirely unmodelled today.


Both open questions resolved (深度调研 update)

Q1 — foo.liblibfoo.a: do it in the same PR. Blast radius traced to empty.

The worry was that it changes a host build's output name. Tracing every consumer:

consumer mechanism affected?
mcpp package deps object-level splicingplan.cppm:836 puts dependency .o files straight into lu.objects. A static library is never produced or read on an internal dependency edge
external prebuilt libs free-form ldflags; the name lives in the package descriptor
[runtime] library_dirs a directory, not a name; Windows side filters on .dll for runtime deployment only
fingerprint / cache fingerprint.cppm carries no artifact name but does carry MCPP_VERSION — any bump already rotates target/<triple>/<fp>/, so no mixed state and no cache migration
whoever takes the artifact outside mcpp direct path reference ⚠️ the only one

The structural fact: mcpp static libraries are not internal link units. That turns
this from "load-bearing rename" into "one outward-facing filename".

And the name they get today is wrong: mingw's ar emits a GNU archive named
foo.lib, claiming an MSVC convention it does not satisfy — a pre-existing
correctness bug, unrelated to cross-compilation. Shipping the (os, env) rule
half-way would leave a state harder to explain than the bug itself.

Q2 — PE import libs: do not model them yet — draw the boundary instead

A fact that reframes the question. All five shared-library e2e tests declare
# requires: elf:

08_shared_library.sh  55_dependency_shared_artifact.sh  56_transitive_shared_artifact.sh
57_static_dep_shared_artifact.sh  64_shared_soname_runtime_alias.sh

and elf is only added on the Linux branch of run_all.sh:46 — Darwin gets
macos, Windows gets windows.

Shared libraries have never been verified end to end on PE or Mach-O. This is not
a missing feature; it is a path nobody has walked, while the code carries branches that
look like it was. Those branches are speculation — mingw tolerates linking a .dll
directly, MSVC's link.exe cannot — and shared_library_link_flags keys on
is_windows, a host constant, so it points the wrong way under cross-compilation
regardless.

Recommendation: reject SharedLibrary on non-ELF targets with a clear error before
attempting to support it. Silently emitting something unusable is worse than saying no.
An untested branch that also refuses to say no is the hardest kind of debt — it can
neither be trusted nor deleted, because nobody knows who depends on it.

Resulting split — three PRs, not one

  • PR-1 B3 proper (§5), five layered commits, CHANGELOG notes the mingw static-lib rename
  • PR-2 non-ELF SharedLibrary → explicit error + e2e. Independent of PR-1; can land first
  • PR-3 import lib support — its own design doc, gated on shared-library coverage
    existing for PE and Mach-O first. Adding artifact edges with no coverage would just
    create the next batch of unverified branches

Safe to move now: v2026.8.3.2 is published on all four platforms, mirrored to
xlings-res, and xim-pkgindex tracks it as latest (openxlings/xim-pkgindex#480).
Verified end to end — `xlings install mcpp@2026.8.3.2` then `xlings use`
resolves to 2026.8.3.2.

The pin is the self-hosting starting point, so it only ever moves AFTER the
release it names actually exists and is reachable through the index; bumping it
alongside the version bump would have pointed every CI job at a release that had
not been built yet.
… every build

Writes up B3 properly and corrects what the original §6.5 claimed.

The original said it was 'symmetrically wrong' — Windows→Linux produces
mcpp.exe for an ELF, Linux→Windows produces mcpp for a PE. The second half is
false. Measured: a Linux host cross-compiling to x86_64-windows-gnu produces
b3probe.exe (PE32+), because mingw's GCC driver appends .exe itself when the
-o name has no extension. mcpp never participates in that decision.

The actual defect is elsewhere and matters more. ninja is told the output is
bin/foo while GCC writes bin/foo.exe, so the declared file never exists and
ninja reruns the link edge on every build. Verified by mtime across two
consecutive builds — the artifact is relinked every time. Incremental builds
are effectively off for PE targets, which is the path CI exercises daily.

That also explains why nothing caught it: 102_mingw_cross_wine.sh looks for the
real artifact (find -name '*.exe'), not for what ninja declared, so both of its
assertions hold while the inconsistency sits underneath them.

Two further findings the fix has to account for:

  - naming is an (os, env) function, not an os one. windows-gnu uses the GNU
    convention (libfoo.a); only windows-msvc is foo.lib. The current _WIN32
    branch hardcodes the latter, so building a static library with mingw ON a
    Windows host is already misnamed today — a pre-existing defect unrelated to
    cross-compilation.

  - the blast radius is much smaller than §6.5 feared. e2e and CI need
    essentially no changes, precisely because they match the real artifact.

Also confirms the other 15 exe_suffix references are correct host semantics
(locating ninja / xlings / clang++ on the build machine) and must not be
touched; the change is confined to src/build/plan.cppm.
Q1 — windows-gnu static lib foo.lib -> libfoo.a: DO IT IN THE SAME PR.

The worry was that it changes a host build's output name. Tracing every
consumer shows the blast radius is empty:

  - mcpp package deps link at OBJECT level (plan.cppm:836 splices dependency
    .o files into lu.objects). A static library is never produced OR read as
    part of an internal dependency edge.
  - external prebuilt libs come through free-form ldflags; the name is written
    in the package descriptor, mcpp never spells it.
  - [runtime] library_dirs is a directory, not a name; the Windows side only
    filters on the .dll extension for runtime deployment.
  - fingerprint.cppm carries no artifact name but does carry MCPP_VERSION, so
    any bump already rotates target/<triple>/<fp>/. No mixed state, no cache
    migration, no "cache clean" advice needed.

So the only consumer is whoever takes the artifact outside mcpp — and the name
they get today is wrong: mingw's ar emits a GNU archive named foo.lib, claiming
an MSVC convention it does not satisfy. That is a pre-existing correctness bug,
not a nice-to-have, and shipping the (os, env) rule half-way would leave a state
harder to explain than the bug.

Q2 — PE import libs: DO NOT MODEL THEM YET. Draw the boundary instead.

All five shared-library e2e tests declare "# requires: elf", and that capability
is only added on the Linux branch of run_all.sh — Darwin gets "macos", Windows
gets "windows". Shared libraries have therefore never been verified end to end
on PE *or* Mach-O. This reframes the question: it is not a missing feature, it
is a path that was never walked while the code carries branches that look like
it was.

Those branches are speculation: mingw tolerates linking a .dll directly, MSVC's
link.exe cannot — and the branch keys on the host constant, so it points the
wrong way under cross-compilation anyway.

Recommends rejecting SharedLibrary on non-ELF targets with a clear error before
attempting to support it. An untested branch that also refuses to say no is the
hardest kind of debt — it can neither be trusted nor deleted, because nobody
knows who depends on it. Same shape as the offline-first code that a TTL gate
had quietly made unreachable.

Splits the work into three PRs accordingly; import lib support gets its own
design doc, gated on shared-library coverage existing for PE and Mach-O first.
@Sunrisepeak

Copy link
Copy Markdown
Member Author

Superseded by #342, which carries these docs plus the implementation. This PR could not go green on its own: it was blocked by the target/ cache issue fixed in #342's first commit.

@Sunrisepeak Sunrisepeak closed this Aug 3, 2026
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