You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(pack): read the import table instead of running the binary (§4)
`mcpp pack` refused Windows with `#if defined(_WIN32)`, and the reason given
was that the tools were POSIX-only. That was the symptom. The cause is one
layer down: the dependency closure comes from
LD_TRACE_LOADED_OBJECTS=1 '<binary>'
which RUNS the artifact — so it can cross neither an OS (a Linux box cannot
execute a PE) nor an ARCHITECTURE (an x86_64 box cannot execute an aarch64
ELF, same OS or not). Porting `tar` would not have helped, and every tool the
2026-05-19 design proposed — dumpbin, ImageNtHeader, Compress-Archive — would
have reintroduced the obstacle one layer down, because each exists only on the
platform where the problem had already gone away.
mcpp.pack.binfmt reads it out of the file instead: ELF DT_NEEDED through the
segment table, PE imports AND delay-imports (a missing delay-load does not
fail at startup — it fails at the first call through it, which is strictly
worse to debug). Cross-OS packaging is then not a feature that had to be
added; it is what remains once nothing has to be executed.
mcpp.pack.zip writes the archive, for the same reason: no zip tool exists on
every host (GNU tar cannot write zip, `zip(1)` is often absent,
Compress-Archive is Windows-only). Entries are STORED, which is a real size
cost and the honest trade — a DEFLATE encoder is the one part that could
produce an archive that unpacks WRONG rather than failing loudly, and mcpp has
no zlib to borrow one from. Deterministic by construction: no timestamps are
read, so a published checksum means something.
THE CONTRACT NOW REACHES PACKAGING (§4.3). `cxx_runtime` used to stop at the
compile and link flags, so the step that decides which files actually travel
could not see what had been promised — on ELF the `ldd` closure agreed with it
by luck, on PE nothing did. It is now an input:
toolchain-coupled the toolchain's runtime directory joins the search set
host-coupled it stays OUT, so a vcruntime140.dll in the toolset is not
silently swept into a package that promised the host
would provide it
--mode system/static + toolchain-coupled → refused, naming the way out
PE layout is flat, and that is the relocation mechanism rather than a style:
the Win32 loader resolves a DLL from the directory of the executable, and
there is no rpath to point elsewhere. Windows' own DLLs are never bundled —
two of something that must be unique is a broken program, not a heavier one —
but `force_bundle` overrides that, as it always did on ELF.
Verified on a Linux host against a real cross-built PE, not only synthesised
fixtures: e2e 240 builds a mingw target, drops a stand-in for a DLL the EXE
imports, packs, and has PYTHON verify the archive. The msvcrt.dll assertion is
the positive half (a parser that read nothing could not have produced it) and
the kernel32.dll assertion the negative half; together they are decisive. It
runs in the mingw-cross job because running it on Windows would prove nothing.
Docs: the Windows layout, the cross-host story and both size/determinism
consequences in 02-pack-and-release (en+zh); the MSVC half of `cxx_runtime` in
05-mcpp-toml (en+zh), replacing a claim about /MT that stopped being true;
SDK-by-origin and the `@system` rule in 03-toolchains (en; the zh MSVC section
was rewritten — it still described msvc as a system-only toolchain and
`msvc@19.44` as a pin-verify). 2026-05-19-pack-windows-design.md is marked
superseded with what it got wrong and why, since the mistake is instructive.
Copy file name to clipboardExpand all lines: docs/03-toolchains.md
+35-5Lines changed: 35 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -228,6 +228,17 @@ questions. `msvc@system` asks *"use what this developer already has"*;
228
228
`msvc@14.44.35207` asks *"build this project with exactly this compiler"*.
229
229
Pinned toolsets coexist with each other and with a system Visual Studio.
230
230
231
+
> **`@system` is an MSVC-only spelling.** There is no `gcc@system` or
232
+
> `llvm@system`, and that is deliberate rather than an omission: mcpp is built
233
+
> on xlings, a user-space OS, and the design drives host dependencies to a
234
+
> minimum — a toolchain comes from a payload the manifest names, so every
235
+
> machine builds with the same compiler. Windows is the one place where
236
+
> refusing to use what is already installed would cost more than it buys:
237
+
> Visual Studio is very often present and cannot always be redistributed.
238
+
> `<family>@system` for any other family is an error that names both things
239
+
> you might have meant. (The family-less `[toolchain] … = "system"` — the PATH
240
+
> compiler — is a separate and deliberate escape hatch, and is unaffected.)
241
+
231
242
### `msvc@system` — the machine's own Visual Studio
232
243
233
244
mcpp locates and identifies an installed Visual Studio / Build Tools; it never
@@ -304,18 +315,37 @@ environment from the VC tools + Windows SDK (no `vcvarsall` involved), stages
304
315
`/interface /TP /ifcOutput`, scans with `/scanDependencies`, and links with
305
316
`link.exe`/`lib.exe` through response files.
306
317
307
-
The Windows SDK is located in this order: **`WindowsSdkDir`** (+
308
-
`WindowsSdkVersion`) if declared, then the `xim:windows-sdk` payload beside a
309
-
pinned toolset in mcpp's store, then `C:\Program Files (x86)\Windows Kits\10`.
310
-
A missing SDK fails the build with guidance (`mcpp self doctor` reports SDK
311
-
status).
318
+
**The Windows SDK follows the origin**, because the two origins answer
319
+
different questions and so must the SDK:
320
+
321
+
| origin | how the SDK is chosen |
322
+
|---|---|
323
+
|`msvc@<toolset>`| the `xim:windows-sdk` payload installed **with that toolset**, in mcpp's own store. `WindowsSdkDir` / `WindowsSdkVersion` in the environment are **ignored**, and mcpp prints a `note:` saying so. |
324
+
|`msvc@system`|**`WindowsSdkDir`** (+ `WindowsSdkVersion`) if declared, then `C:\Program Files (x86)\Windows Kits\10`. |
325
+
326
+
The asymmetry is the point. A pinned toolset is a promise that two machines
327
+
compile the same source against the same headers; an environment variable that
328
+
can quietly redirect it turns the pin into a preference. A machine's own SDK,
329
+
on the other hand, can only be found by looking, and there a declared answer
330
+
outranks a scan — the same precedence `VSINSTALLDIR` has over `vswhere`.
331
+
332
+
If a pinned toolset has no SDK payload beside it (an older install, say), mcpp
333
+
falls back to the machine's SDK rather than failing — and says so, because that
334
+
build is no longer reproducible and nothing else would record it.
312
335
313
336
A root only counts as an SDK when it has **both** halves — `Include\<v>\ucrt\
314
337
corecrt.h` *and* `Lib\<v>\um\<arch>\kernel32.lib`. A root with headers and no
315
338
import libraries is skipped rather than selected, so a partially unpacked
316
339
payload cannot outrank the machine's complete SDK and turn into
317
340
`LNK1104: cannot open file 'kernel32.lib'` at the very end of a build.
318
341
342
+
The resolved SDK version is part of the build's **runtime identity**
343
+
(`ucrt@10.0.26100.0`) and therefore of the fingerprint that keys the build
344
+
cache: changing SDK changes the cache key, exactly as changing compiler does.
345
+
It is a **compatibility floor declaration**, not a payload binding like
346
+
`glibc@2.39` on Linux — `ucrtbase.dll` is a Windows component and mcpp neither
347
+
ships nor substitutes it.
348
+
319
349
**CRT model.**`/MD` (host-coupled) by default; `/MT` when either
Copy file name to clipboardExpand all lines: docs/05-mcpp-toml.md
+33-4Lines changed: 33 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -395,10 +395,39 @@ default applies only when nobody said anything.
395
395
`self-contained`, `false` means `host-coupled`. An explicit `cxx_runtime` wins.
396
396
397
397
**A contract that cannot be honored is reported, never silently downgraded.** If a
398
-
toolchain ships no `libc++.a`, or a contract has no mechanism on that platform
399
-
(`self-contained` under the MSVC runtime would need `/MT`, which mcpp does not emit
400
-
yet), the build prints what it fell back to instead of quietly producing a
401
-
different artifact than the manifest asked for.
398
+
toolchain ships no `libc++.a`, or a contract has no mechanism on that platform,
399
+
the build prints what it fell back to instead of quietly producing a different
400
+
artifact than the manifest asked for.
401
+
402
+
#### On the MSVC runtime
403
+
404
+
The CRT model is the mechanism here, and it is a **whole-project** switch: cl
405
+
bakes `_MSVC_MT`/`_MSVC_MD` into the one `std` module a project builds, so a
406
+
per-role contract that disagrees with the project's cannot be honoured and is
407
+
reported rather than ignored.
408
+
409
+
| value | what it is on MSVC |
410
+
|---|---|
411
+
|`self-contained`|`/MT` — the static CRT. `linkage = "static"` selects the same thing from the libc axis. |
412
+
|`host-coupled` (default under `/MD`) | the target provides `vcruntime140.dll` / `msvcp140.dll` — i.e. Visual Studio or the redistributable is installed there. |
413
+
|`toolchain-coupled`| the toolset's **own** copy of those DLLs travels with the artifact. |
414
+
415
+
`toolchain-coupled` is worth spelling out, because the obvious reading is
416
+
wrong. `ucrtbase.dll`*is* a Windows component (since Windows 10) and mcpp
417
+
never ships it. `vcruntime140.dll` and `msvcp140.dll` are **not**: every MSVC
418
+
toolset carries them under `VC\Redist\MSVC\<version>\<arch>\`, exactly the
419
+
way a gcc payload carries `libstdc++.so`. Under this contract mcpp stages them
420
+
beside the artifact — which is what makes a default `/MD` build runnable on a
421
+
machine that has only the pinned toolset and no Visual Studio at all.
422
+
423
+
The debug CRT (`vcruntime140d.dll` and friends, under `debug_nonredist\`) is
424
+
never staged: it may not be redistributed.
425
+
426
+
Combining it with `/MT` is a contradiction rather than a missing feature — a
427
+
static CRT leaves no DLL to couple to — so it is reported and resolved to
428
+
`self-contained`. `mcpp pack` enforces the other half: a mode that bundles
0 commit comments