feat(windows): the SDK and the runtime get the axes the compiler already had - #448
Merged
Conversation
The architecture review found where the problems are. This is what to do
about them, written after a round of questions that overturned two shapes in
my own proposal.
Both corrections are recorded in §0.2, because the wrong version looked
equally reasonable and will otherwise be proposed again:
- generalising `@system` to gcc/llvm is backwards. xlings is a user-space
OS and mcpp minimises host dependence; `msvc@system` is a Windows
concession, not a capability three families are missing.
- a `windows_sdk = "..."` manifest key should not exist. The model already
reserves the slot (`runtime_binding.cppm:26` documents `ucrt@...` and
nothing populates it), and a version key would sit next to `_WIN32_WINNT`
looking interchangeable while controlling a different thing — which is
worse than not having it.
The design separates three axes that are currently entangled: where the
compiler came from, which SDK is used, and what the artifact ships. They take
values independently — a managed toolset with the machine's SDK is what
xrgui's CI does today — so any design that fuses them into one switch is
wrong.
It also records the finding that makes `mcpp pack` more than a missing
feature: `ldd_parse` computes the dependency closure by RUNNING the binary
(LD_TRACE_LOADED_OBJECTS), so it cannot cross an OS or an architecture by
construction. Reading imports statically is what makes cross-packaging fall
out rather than be added.
The acceptance criteria are written so that none of them can be met by CI
going green — two of them explicitly require a machine POORER than the CI
runner, because "the verification environment is richer than the target" is
the shape that produced most of this round's eleven defect layers.
…eady had Implements §1 / §2 / §3 of .agents/docs/2026-08-16-windows-toolchain-three-axes-design.md. Three questions were entangled because only one of them had ever been modelled. The compiler got a version axis in the last round; the headers it compiles against and the runtime the artifact loads did not. §2 THE SDK IS BOUND, NOT SEARCHED. `find_windows_sdk()` scanned — WindowsSdkDir, then the sibling store, then the conventional roots — for BOTH origins. So a pinned `msvc@<toolset>` was a pin the environment could overwrite, and two machines could build one manifest against two SDKs with nothing in the log naming either. It is now resolved by origin: a managed toolset takes the SDK payload from its own store and ignores WindowsSdkDir/WindowsSdkVersion *out loud*; `msvc@system` keeps today's chain, because a machine's things can only be found by looking. A managed toolset with no SDK payload beside it still falls back to the machine's — working beats failing — and says so, because that build is no longer reproducible and only that line records it. §2.3 `ucrt@<version>` FILLS A SLOT THAT HAS BEEN RESERVED SINCE THE FIELD EXISTED. `RuntimeBinding::runtimeId`'s comment has documented it from the start and nothing ever wrote one, so the SDK version never reached `runtimeContractHash` and two SDKs shared one build cache. It is NOT isomorphic to `glibc@`, and the comment says so where it will be read: glibc@ binds a payload (headers + .so, patchelf makes the artifact run on that copy), ucrt@ declares a floor (ucrtbase.dll is an OS component from Win10 on; mcpp's windows-sdk payload deliberately carries only half of ucrt and no redistributable). It is therefore not projected into `libc`. The `glibc@`-prefix gates become `runtime_provider()` dispatch, so another provider reads as "no rules here" rather than "no identity". §3.3 `toolchain-coupled` NOW MEANS SOMETHING ON PE. The refusal said the MSVC runtime "ships with the OS/redistributable, not with the toolchain" — true of ucrtbase.dll, false of vcruntime140.dll/msvcp140.dll, which sit in VC\Redist\MSVC\ inside every toolset. That is the relationship gcc has to libstdc++.so, so it takes the same contract; PE has no rpath, so the mechanism is a copy beside the artifact rather than a search path. /MT stays a degradation, and a genuine one: a static CRT leaves no DLL to couple to. The DLL set comes from `vc_redist_dir()` — the single criterion that excludes `debug_nonredist\`, which may not be redistributed. A second, name-shaped rule here could disagree with it, and disagreeing about that is a licensing defect rather than a bug. §1 THE ORIGIN AXIS IS CONTAINED, NOT GENERALISED. - `gcc@system` / `llvm@system` are refused where they are read, naming both things the user might have meant. They used to parse and then fail elsewhere as `xim:gcc@system` → "no such package", sending the reader after a version that was never going to exist. `msvc@system` is a concession to one platform, not a capability the other families lack; the family-less `system` escape hatch is untouched. - `resolve_managed_msvc()` replaces two hand-written copies of "where does a managed toolset live, and why is the fetcher's `root` wrong for it" — the reason existed in only one of them. - `needs_linux_sysroot_payloads()` replaces two spellings of one rule whose comment claimed they mirrored each other. They did not: the PE term was missing from one. Unreachable today, which is how it survived. - The toolchain resolution order was documented twice, as "3 steps" and "4 steps", naming five of the nine inputs and disagreeing about two. One table now, keyed to `TcOrigin` enumerators so it cannot quietly stop matching. `dist::Format` is derived from the target triple before falling back to the host, which only ADDS answers — and makes a Windows contract assertable on the Linux runner that reviews most of this. Tests: 22 new. The SDK-override criterion is the design doc's §6 acceptance test as a unit test (point WindowsSdkDir elsewhere; the payload SDK must still win, and the note must say the variable was ignored); the deploy tests assert reachability twice over, since a copy edge nothing asks for never runs under explicit ninja goals.
`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.
…came The design doc gains a status section per item, including the two places the PLAN was corrected by the implementation and the one half of §4 that was deliberately not done: - `dist::Format` now reads the target triple before falling back to the host. Everything but MinGW used to ask the host, which made a Windows contract unassertable on the Linux runner where most of this gets reviewed. It only ADDS answers, so no existing build changes. - `force_bundle` had to reach the PE system list too. ELF always worked that way; making the PE exclusion unconditional would have turned an explicitly written decision into decoration. - The ELF closure still runs the artifact. `ldd` hands back RESOLVED PATHS while `DT_NEEDED` gives only names, and turning names into paths means reimplementing the loader's search order ($ORIGIN, DT_RPATH before LD_LIBRARY_PATH before DT_RUNPATH before ld.so.cache, hwcaps). Rewriting that under a correct, e2e-covered path is more risk than it buys — and the cost is stated rather than left to be discovered: cross-ARCHITECTURE ELF packing is still unsupported, which is the second limit §4.1 names. `mcpp self doctor` reports the Windows SDK PER ORIGIN. One unlabelled line was the same "one question, two answerers" shape this axis exists to close: a user reading it would believe it applied to their pinned build, and it did not.
Every Windows job and the macOS job went red on the same new code, in two
different ways, while gcc was green everywhere:
Windows clang 20.1.7 (MSVC ABI) segfaulted COMPILING mcpp.pack —
0xC0000005, no diagnostic, five jobs at once
macOS test_pack_binfmt died with SIGSEGV at RUN time, inside the PE
import-table test
The parser is not the problem, and that was measured rather than assumed: the
same code is clean under ASan+UBSan with clang 22.1.8 + libc++, and correct
when compiled AS A CLANG MODULE on x86_64 Linux at both -O0 and -O2. What is
left is the compiler, on the two targets neither of those probes covers.
This codebase has been here before. hostflags.cppm exists because adding an
UNUSED helper to a module's anonymous namespace miscompiled a NEIGHBOURING
function under clang + C++20 modules + -O2, and its verdict was "mechanism
unknown, reproduction solid; the cheap response is to not grow that
namespace". Same response here — remove the shapes, keep the behaviour:
mcpp.pack a scoped enum from another module as a defaulted member of
an EXPORTED struct (`dist::Contract` in `Options`) → a plain
bool, since only one of the three values changes anything
here; a ranges projection over an imported type's member →
std::sort with a comparator
mcpp.pack.zip `std::span<const Entry>` across the boundary → the vector by
const reference (one caller)
mcpp.pack.binfmt `template <typename T> le(...)` in the module purview →
four concrete le8/le16/le32/le64; `constexpr std::array` via
`std::to_array<>` → plain arrays; ranges algorithms over
them → loops
WHICH ONE IT WAS IS NOT ESTABLISHED, and the comments say so rather than
inventing a finding — they were removed together because each CI round costs
minutes and none of the replacements is worse than what it replaced. Every
one is also simpler, so nothing is being paid for the avoidance.
Behaviour is unchanged: the same 10 unit tests and e2e 240 pass.
The combined test died with SIGSEGV on the macOS ARM64 runner and nowhere else — not under ASan+UBSan with clang 22 + libc++, not as a clang module on x86_64 Linux, not under gcc. A single test that builds a fixture, identifies it and parses it cannot say which of the three it was, so each hypothesis costs a CI round. Three tests now: the fixture is well-formed, identify(), and needed_names().
The MinGW section says the key does not exist and is silently ignored; the MSVC section two hundred lines down showed exactly that form as the way to select `/MT`. Found by writing it out and watching mcpp print "unsupported key 'linkage' (ignored)" — which is also what a user following the page would have got, without the page telling them why nothing changed. `linkage` is exact-triple only (`[target.<triple>]`, or `--static`). Both language editions now show that, and say which section each key lives in — the zh page had it right before this round and lost it when the MSVC section was rewritten from the English one.
…dies Splitting the test localised the macOS ARM64 SIGSEGV to ThePeFixtureItselfIsWellFormed — fixture code that calls no module at all. So it is not mcpp.pack.binfmt, and the three probes that came back clean (ASan + UBSan under clang 22 + libc++, the same module compiled by clang on x86_64 Linux, gcc everywhere) were looking in the right place for the wrong thing. The fixture used two `std::span` parameters and two lambdas that mutated a captured string through a captured cursor. It now takes vectors, indexes explicitly, and captures nothing — and traces each phase to stderr, so if it moves again the log names the step instead of costing another CI round. `#include <cstdio>` is not redundant next to `import std;`: `stderr` is a macro, and a module cannot export one.
`--mode static` on its own has always meant "the musl-static ELF", and that is unchanged. But the re-prepare that enforces it ignored `opts.targetTriple`, so `--mode static --target x86_64-windows-gnu` silently produced a LINUX build. That was invisible while PE packaging did not exist — there was no Windows package to notice was missing — and it is a wrong answer now that there is. Measured both ways: with an explicit target the output is `…-x86_64-w64-mingw32-static.zip`; without one it is still `…-x86_64-linux-musl-static.tar.gz`.
It did its job — the plain rewrite is green on macOS ARM64 — and 24 lines of stderr in every CI run forever is a poor trade for a crash that is currently fixed. `MCPP_TEST_TRACE=1` brings it back, which keeps a recurrence one CI round to localise instead of the four this one cost.
This was referenced Aug 16, 2026
Merged
Sunrisepeak
pushed a commit
that referenced
this pull request
Aug 16, 2026
It says what it produces, and since #448 that is a .zip for a Windows target. `--format tar` selects "an archive rather than a plain directory"; WHICH archive follows the artifact, because a .tar.gz full of DLLs is a package most Windows users cannot open without installing something first. Found by smoke-testing the RELEASED binary — the behaviour shipped and its help did not follow.
Sunrisepeak
added a commit
that referenced
this pull request
Aug 16, 2026
…s the self-review found (#449) * ci: bootstrap pin -> 2026.8.17.1, and the round's report The pin moves only now, in the order the release checklist requires: released → mirrored (GitHub + GitCode, both GET-verified byte-for-byte) → in the index (openxlings/xim-pkgindex#643, merged). A pin ahead of what the index actually has fails every CI job's bootstrap with "package not found". The report records what shipped, what was deliberately not done and why, and the three things this round ran into that were not in the plan — including a clang crash on two targets at once, and a page of the docs that contradicted itself about a key that does not exist. * docs: record the ecosystem verification against the released binary Not the local build and not this machine: `xlings install mcpp@2026.8.17.1` from the index, into a SubOS created for it, every command through `--sandbox`. Including the one that matters most — a Windows zip packed from inside that Linux sandbox, verified by Python's own zipfile reader. Also records that the local-`gtc` gitcode fill was not needed this time: the mirror leg passed on its own and was re-checked with a real GET. * docs: one more finding from the ecosystem run, with its provenance `mcpp new` + `mcpp add compat.gtest@1.15.2` does not build: the scaffolded test carries its own main() and the dependency links gtest_main.o into every test target. Found while verifying the ecosystem, and reproduced on the PREVIOUS release (2026.8.16.3) in the same sandbox — so it is recorded as pre-existing, not as something this round did. * test(windows): assert the ucrt identity actually reaches a real build The unit tests pin the hash function — two SDK versions produce two hashes. What they cannot see is whether the value ever gets there on a real Windows build, and a green Windows CI does not distinguish "the identity is filled in" from "the code path ran and produced nothing": an empty string flows through every one of those jobs without a complaint. So this reads resolution.json and asserts the VALUE — `runtime_id` starts with `ucrt@`, the contract hash is non-empty (an identity that participates in nothing is decoration), and it was NOT projected into `libc`, which names a private libc PAYLOAD that has no ucrt equivalent. It pins `msvc@system` explicitly. Windows' default toolchain is clang targeting the MSVC ABI, where clang finds its own SDK and mcpp honestly has nothing to declare — a test that took the default would assert an empty identity and pass for the wrong reason. * docs: the report was missing e2e 241, and the design had no forward pointer The report described the unit tests and e2e 240 but not the assertion added after it was written — the one that says the ucrt identity reaches a real Windows build rather than merely being computable. A report that omits the test which closes an acceptance criterion is a report that overstates the other evidence. The design doc now points forward to it, so a reader who starts at the plan finds what actually happened. * fix(pack): a garbage e_lfanew threw instead of answering "not a PE" `identify()` is documented as never throwing and, for one input, terminated the process. `std::string_view::substr` throws `std::out_of_range` when `pos > size()`, and the position came straight out of the file: if (b.substr(*lfanew, 4) == "PE\0\0") A file that starts with "MZ" and has garbage at 0x3C is ordinary malformed input — a truncated download, a DOS stub, a text file named `.exe` — and `mcpp pack` calls this on every artifact it packages. Measured rather than argued: `std::string_view{256 bytes}.substr(0xFFFFFFFF, 4)` throws `string_view::substr` under libc++. Every other read in the module goes through a bounds-checked accessor; these two comparisons were the exceptions, which is exactly how a module whose stated contract is "total over nonsense" stopped being one. `has_at()` now does the same job with the same check as the rest. Found by re-reading the file after it was already merged and green — the existing truncation test never produced an `e_lfanew` PAST the end, only one exactly AT it, where `substr` is well-defined and returns empty. * docs: the report said three findings; the self-review made it four The fourth is the one the report would most have been improved by hiding: a crash in code written this round, found by re-reading it AFTER it was merged and green. Recorded with what the existing test missed and why. * docs: state the crash's exposure precisely rather than dramatically The examples I reached for — a half-downloaded file, a DOS stub, a text file named .exe — are inputs mcpp is never handed: `identify()` receives a LINK OUTPUT, so the realistic path is a truncated or killed link, not arbitrary user input. The finding stands on what it is (a function documented as never throwing did), not on a frequency it does not have. * docs(cli): `mcpp pack --help` still said tarball It says what it produces, and since #448 that is a .zip for a Windows target. `--format tar` selects "an archive rather than a plain directory"; WHICH archive follows the artifact, because a .tar.gz full of DLLs is a package most Windows users cannot open without installing something first. Found by smoke-testing the RELEASED binary — the behaviour shipped and its help did not follow. * docs: release verification found SHA256SUMS covers one platform of four Checking the four published archives against the file whose name promises to cover them: only `linux-x86_64` is in it — the aarch64, macOS and Windows archives are absent, and have been for at least v2026.8.16.3 and v2026.8.15.1 too, so this is not something this round did. The mechanism is structural rather than flaky: `build-release` (linux x86_64) runs first and writes the file; the other three platforms upload their own `.sha256` sidecars and nothing appends. Verification IS possible — the sidecars are there — but not through the file that says SHA256SUMS. Recorded as outstanding, not fixed: it is the release pipeline, not one of the three axes, and this PR is already the post-release tail. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #446, which carried the design alone. The goal was "one PR if
possible", so the design and its implementation land together.
Windows involves three independent questions, and until now only one of
them had been modelled:
vcruntime140.dllTheir answers vary independently — a managed toolset with the machine's SDK is
what xrgui's CI does today — so any design that binds them to one switch is
wrong. This makes each one a value that is declared and resolved once.
§2 — the SDK is bound, not searched
find_windows_sdk()scannedWindowsSdkDir→ the sibling store → theconventional roots, for both origins. So a pinned
msvc@<toolset>was apin the environment could silently overwrite, and two machines could build one
manifest against two different SDKs with nothing in the log naming either.
That is not hypothetical: the LNK1104 this round chased was a half-unpacked
payload winning the scan on version number.
A managed toolset with no SDK payload beside it still falls back to the
machine's — working beats failing — and says so, because that build is no
longer reproducible and only that line records it.
§2.3
ucrt@<version>fills a slot reserved since the field existedRuntimeBinding::runtimeId's own comment has read"(
glibc@…,macos_sdk@…,ucrt@…)" from the start, and nothing in therepository ever wrote one — so the SDK version never reached
runtimeContractHashand two SDKs shared one build cache.It is not isomorphic to
glibc@, and the code says so where it will beread:
glibc@2.39ucrt@10.0.26100.0.soboth in itucrtbase.dllis an OS component; it cannot be swapped and must not be shippedIt is therefore not projected into
libc, or the private-libc machinerywould go looking for a payload that was never meant to exist. The
starts_with("glibc@")gates becomeruntime_provider()dispatch, so anotherprovider reads as "no rules here" rather than "no identity".
§2.4 (a
windows_sdk = "…"manifest key) is deliberately not done — seethe design doc. It would sit next to
_WIN32_WINNTlooking interchangeablewhile managing a different thing.
§3.3 —
toolchain-couplednow means something on PEThe refusal said the MSVC runtime "ships with the OS/redistributable, not
with the toolchain". True of
ucrtbase.dll. False ofvcruntime140.dll/msvcp140.dll, which sit inVC\Redist\MSVC\…insideevery toolset — exactly the relationship gcc has to
libstdc++.so.So it takes the same contract. PE has no rpath, so the mechanism is a copy
beside the artifact rather than a search path — same contract, different
mechanism, which is what the three-layer model exists to express.
/MTstays a degradation and a genuine one: a static CRT leaves no DLL tocouple to, and the message says which side won.
The DLL set comes from
vc_redist_dir()— the single criterion thatexcludes
debug_nonredist\, which may not be redistributed. A second,name-shaped rule here could disagree with it, and disagreeing about that is
a licensing defect, not a bug.
§4 — read the import table; don't run the binary
mcpp packrefused Windows with#if defined(_WIN32), blaming POSIX-onlytools. That was the symptom. The closure came from
which runs the artifact — so it crosses neither an OS nor an
architecture. Porting
tarwould not have helped, and every tool the 2026-05design proposed (dumpbin,
ImageNtHeader,Compress-Archive) would havereintroduced the obstacle one layer down, each existing only where the problem
had already gone away.
mcpp.pack.binfmt— ELFDT_NEEDEDthrough the segment table; PE importsand delay-imports (a missing delay-load fails at the first call through
it, not at startup — strictly worse to debug).
mcpp.pack.zip— mcpp writes the archive itself, because no zip tool existson every host. 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. Deterministic — no
timestamps are read, so a published checksum means something.
packnow reads the Contract. It used to stop at compile/linkflags, so the step deciding which files travel could not see what had been
promised — on ELF
lddagreed by luck, on PE nothing did.Deliberately not done, with the cost stated: the ELF closure still runs
the artifact.
lddreturns resolved paths;DT_NEEDEDreturns only names,and turning names into paths means reimplementing the loader's search order.
Rewriting that under a correct, e2e-covered path is more risk than it buys —
so cross-architecture ELF packing remains unsupported, which is the second
limit §4.1 names.
§1 — contain the origin axis; do not generalise it
gcc@system/llvm@systemare refused where they are read, namingboth things the user might have meant. They used to parse and then fail
elsewhere as
xim:gcc@system→ "no such package", sending the readerafter a version that was never going to exist.
msvc@systemis aconcession to one platform, not a capability the others lack; the
family-less
systemescape hatch is untouched.site drawing its own conclusions. Once now,
origin_of()dispatches.resolve_managed_msvc()replaces two hand-written copies of "where does amanaged toolset live, and why is the fetcher's
rootwrong for it" — thereason existed in only one of them.
needs_linux_sysroot_payloads()replaces two spellings of one rule whosecomment claimed they mirrored each other. They did not — the PE term was
missing from one. Unreachable today, which is how it survived.
steps", naming five of the nine real inputs and disagreeing about two. One
table now, keyed to
TcOriginenumerators so it cannot quietly drift.Verification
32 new unit tests, and the two that matter most are written so that CI
going green cannot satisfy them:
WindowsSdkDirCannotOverrideAPinnedToolsetsSdk— the design's §6 acceptancecriterion as a unit test. Point
WindowsSdkDirsomewhere else entirely; thepayload's SDK must still win, and the note must say the variable was
ignored. An override that is ignored silently is indistinguishable from one
that was never set.
NothingElseAsksForStagedRuntimeFiles— the deploy flag reaches a copystep, so a stray
trueputs DLLs in an output tree on a platform that hasno such thing. Swept over every (format × stdlib × contract × /MT ×
explicit) cell.
e2e 240 — packing a PE from Linux, run in the mingw-cross job because
running it on Windows would prove nothing. It builds a real cross PE, drops a
stand-in for a DLL the EXE imports, packs, and has Python verify the
archive:
msvcrt.dllpresent — the positive half. A parser that read nothing couldnot have produced it.
kernel32.dllabsent — the negative half. Alone it would also pass for aparser that read nothing; together they are decisive.
--mode systemstages no DLL;toolchain-coupled+--mode systemisrefused, naming the contract and the way out.
Verified locally end to end: on this Linux box,
mcpp pack --target x86_64-windows-gnuproduced a zip thatpython3 -m zipfileandunzip -tboth accept, containing the exe and the resolved DLL and nothing else.
Compatibility
ucrt@inruntimeContractHashWindowsSdkDirgoes from effective to ignored + reported on a pinned toolsetgcc@systemrefusedtoolchain-coupled/ PE packEverything else is internal.