Repository navigation
RFC: native Windows bottles — cross-compile pilot + architecture sketch #346
Description
Goal
Native Windows .exe / .dll bottles, produced from Linux CI runners via cross-compile, distributable through the existing .tar.xz bottle format. Tracked at pkgxdev/pkgx#607 since 2023; the runtime piece was started at pkgxdev/libpkgx#48 (merged) but stalled on the chicken-and-egg of "no Windows packages exist yet to test against".
This RFC proposes a concrete first deliverable and the brewkit-side architecture, leveraging the patterns we converged on in #343 / #344 / #345.
Current state in this repo
Brewkit already has three host().platform == 'windows' branches (build/build.ts:248, lib/porcelain/build-script.ts:47, lib/hooks/usePantry.getScript.ts:196) — lld-link symlink, TMP/TEMP env, path-separator normalization. ~30% of the scaffolding is there. Zero pantry recipes target windows/* today.
Why now, why this shape
The architecture we converged on last week composes very well with Windows:
| Pattern (linux) | Windows mapping |
|---|---|
linux-sysroot: gnu.org/glibc@2.17 (#343) |
windows-sdk: … (or implicit from a toolchain dep) |
gnu.org/glibc bottle (relocatable libc) |
N/A — UCRT ships with Win10+; the "redistributable" bit is vcruntime140.dll + msvcp140.dll, and that's separate from a bottle |
bklibcvenv stage/seal (#344) |
bkwinvenv stage/seal — gather transitive DLLs, move bin/*.exe → libexec/, generate bin/*.cmd wrappers |
build.skip: fix-patchelf (#345) |
build.skip: fix-pe per-recipe escape hatch |
fix-elf rewriting RPATH |
No RPATH on Windows — DLLs found via app-dir → System32 → %PATH%. Hermeticity by layout, not by metadata. |
| Default: PT_INTERP = system, hermeticity opt-in (your call on #12968) | Default: standard DLL search order. Hermeticity opt-in = bkwinvenv seal + .cmd wrappers. Same separation, gratis. |
The big architectural win: Windows has no PT_INTERP and no RPATH. So "default relocatable, hermeticity opt-in via wrappers" is already the platform's native behaviour — we don't have to fight it like we did with glibc.
Proposed toolchain: llvm-mingw
Three candidates, recommendation = llvm-mingw:
| Cross-compile from Linux? | ABI | Maturity | |
|---|---|---|---|
MSVC cl.exe |
❌ (native-only) | MSVC | Mature, EULA-encumbered |
| MinGW-w64 (gcc) | ✅ | gcc | Mature, mismatches with MSVC .lib |
| llvm-mingw (clang) | ✅ | clang-cl can do both ABIs | Younger but production-grade (used by Qt, …) |
Cross-compile from Linux reuses pkgx CI infra; GitHub Windows runners are ~10× slower / cost ~10× more.
Brewkit changes (sketched)
Layer 1 — toolchain wiring
build/build.ts already symlinks lld-link on Windows. Extend:
clang-cl,clang++-cl,link.exe → lld-link,rc.exe → llvm-rc- Add
llvm.org/mingw-w64recipe to pantry
Layer 2 — fix-pe (the analog of fix-elf, but minimal)
PE doesn't have RPATH — no relocation surgery needed. What fix-pe would actually do:
- Strip
+brewingfrom any embedded strings (same problem as the libc.so linker scripts in #12968) - Strip absolute paths from
.pdbdebug records - Audit-only check: every NEEDED DLL is either Windows-shipped (kernel32, ntdll, ucrtbase, …) or in the bottle
Tool: LIEF or llvm-objcopy (already in the clang-mingw bottle).
Layer 3 — bkwinvenv stage/seal
Direct port of #344's pattern, simpler in implementation:
# bkwinvenv seal <prefix>
# - for each bin/*.exe:
# - walk PE imports recursively
# - move dependent DLLs into libexec/
# - move exe → libexec/
# - write bin/foo.cmd:
# @echo off
# "%~dp0\..\libexec\foo.exe" %*No explicit loader invocation needed (no $LDSO --library-path); the .cmd wrapper does an implicit SetDllDirectory via colocation.
Layer 4 — audit step
PE-aware: llvm-readobj --coff-imports foo.exe to list NEEDED DLLs, verify they're either shipped or system.
Phased plan
Phase 0 — Validation pilot (this RFC's deliverable)
- Bottle
llvm.org/mingw-w64in pantry - One pilot recipe:
jqcross-compiled towindows/x86-64 - Produce
.tar.xzcontainingbin/jq.exe - Manual test: extract on Win11 VM,
jq.exe --versionreturns sanely
Phase 1 — Brewkit Windows-aware (~1 week of focus)
- Complete the
platform == 'windows'branches (audit, bottle, env) - Implement
fix-pe(minimal) - Add
windows/x86-64,windows/aarch64to platform schema
Phase 2 — Pantry mass-port (~2-4 weeks)
- 10 "leaf" packages: jq, ripgrep, fd, bat, hyperfine, hexyl, …
- Cascade to C/C++ libs: zlib, openssl, sqlite, …
Phase 3 — bkwinvenv + hermetic mode
- PE-import walker
.cmdwrapper generator
Phase 4 — pkgx CLI integration (follow-through on libpkgx#48)
- PowerShell profile / cmd.exe registry for
pkgx integrate - Path translation
Questions for maintainers
- Cross-compile from Linux vs. native Windows runners: I'm leaning hard on cross-compile (CI cost + speed). Any blocker you can think of?
- MinGW vs MSVC ABI: target both (max distrib compat) or pick one (start with clang-cl/MSVC ABI)?
- UCRT: rely on Win10+ shipping it (no bundle), or bundle
vcruntime140.dllin each bottle for older targets? pkgx integrateon Windows: PowerShell profile primary, cmd.exe secondary? Or other dispatch?- Pantry layout: keep
projects/*/package.ymlflat (withplatforms: windows/*opt-in), or isolate underprojects/windows/*?
What I'd like to do next (with your sign-off)
If the framing here lands well, I'd:
- Open a follow-up PR to brewkit adding Layer 1 + Layer 4 scaffolding (toolchain symlinks + PE audit hook)
- Add an
llvm.org/mingw-w64recipe to pantry (cross-compile toolchain bottle) - Open a pantry PR with a
jqWindows pilot - Produce + post a downloadable
.tar.xzfor inspection
If you want me to hold off until #12968 lands or the bklibcvenv shape stabilizes, that's fine too — just want to put the architecture on paper so the existing Windows scaffolding doesn't keep bit-rotting.
cc @mxcl (your message on pkgxdev/pkgx#607 mentioned "we'll need it for Windows native anyway" re. bootstrap procedure — this is that)
Activity
tannevaled commentedon May 22, 2026
Drafted the
llvm.org/mingw-w64recipe locally to make the Phase-0 deliverable concrete and reviewable:→ https://github.com/tannevaled/pantry/blob/new/llvm.org/mingw-w64/projects/llvm.org/mingw-w64/package.yml
(branch
new/llvm.org/mingw-w64on my fork; can promote to a real pantry PR once you've had a look at the architecture sketch above)Shape — same as the existing
ziglang.orgrecipe (warnings: vendored, source archive declared viadistributable.url, real payload curl'd frombuild.script). Re-distributes upstream prebuilt tarballs (mstorsjo's GitHub releases; reproducibly-built + signed).Host platforms:
…-ucrt-ubuntu-22.04-x86_64.tar.xz(78 MB)…-ucrt-ubuntu-22.04-aarch64.tar.xz(73 MB)…-ucrt-macos-universal.tar.xz(116 MB, FAT)…-ucrt-macos-universal.tar.xz(116 MB, FAT)(All UCRT variant — Win10+ default; MSVCRT variant available if we ever need to target older Windows.)
Target architectures provided: x86-64, aarch64, i686, armv7 Windows (via the per-target driver wrappers
x86_64-w64-mingw32-clangetc.).Self-test: cross-compiles a trivial
hello.cto both x86-64 and aarch64 Windows, verifies the output starts with theMZDOS header (PE/COFF magic). Doesn't try to run the .exe — it's a Windows binary and the test sandbox is Linux.Once this lands, the pilot recipe (cross-compile
jqtowindows/x86-64) becomes a ~20-line follow-up. I can draft + push that too if you'd like to see the full pattern end-to-end before greenlighting the broader plan.(Side note: I tried to do a local empirical validation — download llvm-mingw, cross-compile hello.c, dump PE format — but my local environment's safety guard blocked the toolchain download. Recipe still validates via the test step when brewkit CI runs it.)
tannevaled commentedon May 22, 2026
Pushing the conversation forward with two concrete artefacts:
llvm.org/mingw-w64recipe (draft)fix-pe.ts+bkwinvenvskeletons (draft)Neither is wired into the build pipeline yet — both are intentional "design as code" so the architecture sketched above is reviewable without me speculating further about shape.
Open questions in the brewkit PR I'd love your read on:
+brewingbyte-stripping be consolidated into a single cross-platform pass (Linux currently handles it viafix-elfRPATH rewriting; the new byte-sweep is more general)?SYSTEM_DLLSallowlist vs. denylist — which way around?bkwinvenvneed astagephase, or justseal?If you'd rather we wait on the RFC discussion to settle before going code, totally fine — these drafts can sit. Just figured the architecture is easier to push back on as TypeScript + bash than as bullet-points.
test(llvm-mingw): run cross-compiled .exe via wine when available
tannevaled commentedon May 22, 2026
Update — wine-based test validation
User feedback raised: "can we test the produced .exe via wine?". Yes — and it slots in naturally:
Updated pantry#12984 test step to run
hello.exethrough wine when available (soft-dep onwinehq.org/wine; falls back to PE-magic check if wine isn't present). So the recipe is forward-compatible — automatically upgrades when wine joins pantry.Opened pantry#12985 to track the wine recipe itself. Wine isn't in pantry today (zero past PRs/issues mentioning it), so this is net-new work but valuable beyond Windows porting (anyone wanting to run Windows binaries on a Linux pkgx setup gets it).
Implication for the architecture: brewkit's test sandbox stays Linux, but we get real Windows-binary execution by adding wine to the dependency closure. No GitHub Windows runners needed.
This is the kind of "wait, why didn't I think of that earlier" moment — it makes the cross-compile-from-Linux story significantly stronger because we close the loop on runtime testing without leaving the existing CI.
So updated plan map:
Adds
winehq.org/wineas a Phase-0.5 dependency (between "toolchain exists" and "pilot recipe ships"). Whether that's worth doing inline or in parallel is your call.tannevaled commentedon May 22, 2026
Wine recipe drafted: pkgxdev/pantry#12986 (closes pkgxdev/pantry#12985).
Once #12986 lands, the existing soft-dep in pkgxdev/pantry#12984's test step activates automatically — no further changes needed on the toolchain recipe to get end-to-end runtime validation.
Stack now looks like:
4 drafts + 1 RFC + 1 closed-by-PR issue, all cross-referenced. Stops the bike-shedding from becoming bottomless and gives reviewable code at every layer.
tannevaled commentedon May 22, 2026
Phase-0 closed: pkgxdev/pantry#12987 —
jqcross-compile pilot forwindows/x86-64.This is the concrete end-to-end loop:
End-to-end: cross-compile from Linux → run through wine on Linux → assert. No GitHub Windows runners, no native Windows environment, no PE-rewriting beyond what the existing autoconf/mingw stack does.
Question for you @jhheider: the
provides: bin/jqblock needs to acceptbin/jq.exeonwindows/*targets. I think that's a brewkit audit hook — extension-aware suffix matching. Looking for the right place in brewkit to add that; pointer welcome, otherwise I'll spelunk throughlib/porcelain/.The other pilot limitation that needs your guidance is conditional
dependencies:— currentlyllvm.org/mingw-w64is an unconditional build dep that's only useful when targeting Windows. Either:dependencies: { foo: '*', if: <predicate> }syntax (mirroring what already works onscript:), orI lean toward (1) for symmetry but (2) is fine for the pilot. WDYT?
tannevaled commentedon May 22, 2026
Loop-closure finding from round-2 CI on pkgxdev/pantry#12984.
The Windows-port stack discovered a direct architectural dependency on pkgxdev/pantry#12968 (glibc host-independence — already approved by @jhheider, awaiting merge):
Upstream llvm-mingw binaries are built against glibc 2.35 (ubuntu-22.04). Brewkit's test sandbox is
debian:buster-slim(glibc 2.28). The toolchain literally can't load.Solution: depend on
gnu.org/glibc: '>=2.34'. But that bottle doesn't exist in pantry main yet — it's gated on #12968 landing.So the Phase-0 dependency tree is actually:
This isn't a problem for the RFC — it's actually a strength of the architecture. The glibc work isn't "just nice-to-have older-target support" — it's the foundation that lets us cross-compile to ANY new platform target from CI runners with predictable host runtime. We could not have shipped Windows bottles at all without it.
Practical implication: getting #12968 merged should be the immediate next step before chasing further CI on #12984 / #12987 / etc. Without #12968 we can keep iterating but the bottom layer of the stack won't be there.
Also fixed wine in 2b0d3dfc —
--without-mingwto avoid wine's configure requiring a mingw cross-compiler on PATH (which would have created a build-dep cycle with #12984 anyway). Wine's builtin Windows-side DLLs are sufficient for our CLI tool testing.scope(llvm-mingw): linux hosts only for pilot
jhheider commentedon May 22, 2026
so, the problem with version predicates on dependencies is that it means the tree has to fork, and may not be resolvable. there's a reason a lot of solvers have run limits (i think haskell's is 10,000) for this reason. so far, we've either gone with harmless bloat, or forking the package.
audit is audit/audit.ts on brewkit, and line 46 is probably where you need to decorate for windows.
feat(audit): accept bin/foo.exe as a match for `provides: - bin/foo`
tannevaled commentedon May 22, 2026
Thanks @jhheider — both pointers actioned:
1. audit hook (audit/audit.ts:46)
Implemented in #347 (commit ad332f9): the audit's
provides:check now acceptsbin/<name>.exeas a fallback forbin/<name>. Cost is one extra stat per provided binary on non-Windows installs (negligible), zero false-positives because.exefiles are unconventional on POSIX.This unblocks the jq Windows pilot in pkgxdev/pantry#12987 — without it, the
provides: - bin/jqaudit would fail onwindows/x86-64(binary isbin/jq.exe).2. Conditional deps → "harmless bloat OR fork"
Good to know. Going with harmless bloat for the jq pilot since
llvm.org/mingw-w64is inbuild.dependencies(build-time only, not bundled in the resulting jq bottle), so the cost is ~10s of CI download per non-Windows build, nothing visible to end users. That seems clearly better than maintaining two recipes for the same logical package.Will reconsider if the pattern scales badly — once we have many Windows-supporting recipes all pulling llvm-mingw, the per-recipe overhead adds up. But for the pilot stage, bloat-over-fork.
Stack state:
The toolchain layer is mergeable on its own — the cascade unblocks once you have a chance to look at #12984.
jhheider commentedon May 22, 2026
well, it shouldn't bloat if it's a windows-only dep, right?
build.dependencies.windows: llvm.org/mingw-w64: "*"build-script: opt-in `linux-sysroot:` directive in package.yml
test(llvm-mingw): run cross-compiled .exe via wine when available
scope(llvm-mingw): linux hosts only for pilot
feat(jq): add windows/x86-64 cross-compile pilot
new(winehq.org): headless wine for Windows-binary CI testing