Skip to content

RFC: native Windows bottles — cross-compile pilot + architecture sketch #346

Open

Description

@tannevaled

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-w64 recipe 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 +brewing from any embedded strings (same problem as the libc.so linker scripts in #12968)
  • Strip absolute paths from .pdb debug 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-w64 in pantry
  • One pilot recipe: jq cross-compiled to windows/x86-64
  • Produce .tar.xz containing bin/jq.exe
  • Manual test: extract on Win11 VM, jq.exe --version returns 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/aarch64 to 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
  • .cmd wrapper generator

Phase 4 — pkgx CLI integration (follow-through on libpkgx#48)

  • PowerShell profile / cmd.exe registry for pkgx integrate
  • Path translation

Questions for maintainers

  1. Cross-compile from Linux vs. native Windows runners: I'm leaning hard on cross-compile (CI cost + speed). Any blocker you can think of?
  2. MinGW vs MSVC ABI: target both (max distrib compat) or pick one (start with clang-cl/MSVC ABI)?
  3. UCRT: rely on Win10+ shipping it (no bundle), or bundle vcruntime140.dll in each bottle for older targets?
  4. pkgx integrate on Windows: PowerShell profile primary, cmd.exe secondary? Or other dispatch?
  5. Pantry layout: keep projects/*/package.yml flat (with platforms: windows/* opt-in), or isolate under projects/windows/*?

What I'd like to do next (with your sign-off)

If the framing here lands well, I'd:

  1. Open a follow-up PR to brewkit adding Layer 1 + Layer 4 scaffolding (toolchain symlinks + PE audit hook)
  2. Add an llvm.org/mingw-w64 recipe to pantry (cross-compile toolchain bottle)
  3. Open a pantry PR with a jq Windows pilot
  4. Produce + post a downloadable .tar.xz for 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

  1. tannevaled commented on May 22, 2026

    @tannevaled
    ContributorAuthor

    Drafted the llvm.org/mingw-w64 recipe 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-w64 on 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.org recipe (warnings: vendored, source archive declared via distributable.url, real payload curl'd from build.script). Re-distributes upstream prebuilt tarballs (mstorsjo's GitHub releases; reproducibly-built + signed).

    Host platforms:

    host upstream tarball
    linux/x86-64 …-ucrt-ubuntu-22.04-x86_64.tar.xz (78 MB)
    linux/aarch64 …-ucrt-ubuntu-22.04-aarch64.tar.xz (73 MB)
    darwin/x86-64 …-ucrt-macos-universal.tar.xz (116 MB, FAT)
    darwin/aarch64 …-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-clang etc.).

    Self-test: cross-compiles a trivial hello.c to both x86-64 and aarch64 Windows, verifies the output starts with the MZ DOS 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 jq to windows/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.)

  2. tannevaled commented on May 22, 2026

    @tannevaled
    ContributorAuthor

    Pushing the conversation forward with two concrete artefacts:

    URL What
    pantry PR pkgxdev/pantry#12984 llvm.org/mingw-w64 recipe (draft)
    brewkit PR #347 fix-pe.ts + bkwinvenv skeletons (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:

    1. Should +brewing byte-stripping be consolidated into a single cross-platform pass (Linux currently handles it via fix-elf RPATH rewriting; the new byte-sweep is more general)?
    2. SYSTEM_DLLS allowlist vs. denylist — which way around?
    3. Does bkwinvenv need a stage phase, or just seal?

    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.

  3. tannevaled commented on May 22, 2026

    @tannevaled
    ContributorAuthor

    Update — wine-based test validation

    User feedback raised: "can we test the produced .exe via wine?". Yes — and it slots in naturally:

    1. Updated pantry#12984 test step to run hello.exe through wine when available (soft-dep on winehq.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.

    2. 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:

                                      cross-compile (linux)
      llvm-mingw recipe ────────────────────────────────────► hello.exe
                                                                  │
                                                                  │  (run on linux test sandbox)
                                                                  ▼
                                                                wine64
                                                                  │
                                                                  ▼
                                                           stdout assertion
                                                                  │
                                                                  ▼
                                                                PASS
    

    Adds winehq.org/wine as a Phase-0.5 dependency (between "toolchain exists" and "pilot recipe ships"). Whether that's worth doing inline or in parallel is your call.

  4. tannevaled commented on May 22, 2026

    @tannevaled
    ContributorAuthor

    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:

                                  pkgxdev/pantry#12986  ←  pkgxdev/pantry#12985
                                     winehq.org                    issue
                                           │
                                           │ (soft-dep)
                                           ▼
                                  pkgxdev/pantry#12984
                                  llvm.org/mingw-w64
                                           │
                                           │ (build-time toolchain)
                                           ▼
                                      pilot recipes
                                      (jq, etc.)
                                           │
                                           │ (consumes brewkit Windows-platform hooks)
                                           ▼
                                  pkgxdev/brewkit#347
                                  fix-pe.ts + bkwinvenv
                                           │
                                           │ (anchored on architecture in)
                                           ▼
                                  pkgxdev/brewkit#346  ←  this RFC
    

    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.

  5. tannevaled commented on May 22, 2026

    @tannevaled
    ContributorAuthor

    Phase-0 closed: pkgxdev/pantry#12987 — jq cross-compile pilot for windows/x86-64.

    This is the concrete end-to-end loop:

                                      pantry#12986
                                      winehq.org
                                           │
                                  ┌────────┴────────┐
                                  │                 │
                           (build dep)        (test dep)
                                  │                 │
                                  ▼                 ▼
                           pantry#12984        pantry#12987   ←  this PR
                           llvm-mingw  ──────► jq (cross to windows/*)
                                                    │
                                                    ▼
                                              bin/jq.exe
                                                    │
                                              (run via wine)
                                                    │
                                                    ▼
                                           stdout assert == "jhheider"
    

    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/jq block needs to accept bin/jq.exe on windows/* 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 through lib/porcelain/.

    The other pilot limitation that needs your guidance is conditional dependencies: — currently llvm.org/mingw-w64 is an unconditional build dep that's only useful when targeting Windows. Either:

    1. brewkit gains a dependencies: { foo: '*', if: <predicate> } syntax (mirroring what already works on script:), or
    2. We keep deps unconditional and accept the "harmless bloat" cost

    I lean toward (1) for symmetry but (2) is fine for the pilot. WDYT?

  6. tannevaled commented on May 22, 2026

    @tannevaled
    ContributorAuthor

    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):

    clang: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found
       (required by .../libLLVM.so.22.1)
    

    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:

    pkgxdev/pantry#12968 (glibc host-independence)     ◄── approved, awaits merge
            │
            │ (provides gnu.org/glibc bottle pantry-wide)
            ▼
    pkgxdev/pantry#12984 (llvm-mingw toolchain)        ◄── can declare glibc >=2.34 as test dep
            │
            │ (build dep)
            ▼
    pkgxdev/pantry#12987 (jq Windows pilot)            ◄── consumes mingw
            │
            │ (test dep for runtime validation)
            ▼
    pkgxdev/pantry#12986 (winehq.org)                  ◄── independent, --without-mingw avoids cycle
    

    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-mingw to 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.

  7. jhheider commented on May 22, 2026

    @jhheider
    Contributor

    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.

  8. tannevaled commented on May 22, 2026

    @tannevaled
    ContributorAuthor

    Thanks @jhheider — both pointers actioned:

    1. audit hook (audit/audit.ts:46)

    Implemented in #347 (commit ad332f9): the audit's provides: check now accepts bin/<name>.exe as a fallback for bin/<name>. Cost is one extra stat per provided binary on non-Windows installs (negligible), zero false-positives because .exe files are unconventional on POSIX.

    const candidates = [
      path.join('bin', name),
      path.join('sbin', name),
      path.join('bin', name + '.exe'),
      path.join('sbin', name + '.exe'),
    ]
    if (!candidates.some(p => p.isExecutableFile())) {
      missing.push([pkg.project, name])
    }

    This unblocks the jq Windows pilot in pkgxdev/pantry#12987 — without it, the provides: - bin/jq audit would fail on windows/x86-64 (binary is bin/jq.exe).

    2. Conditional deps → "harmless bloat OR fork"

    Good to know. Going with harmless bloat for the jq pilot since llvm.org/mingw-w64 is in build.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:

    URL CI
    llvm-mingw pkgxdev/pantry#12984 ✅ green (linux x86-64 + aarch64; darwin scoped out for pilot)
    wine pkgxdev/pantry#12986 ❌ blocked on #12984 merge (configure requires mingw)
    jq pilot pkgxdev/pantry#12987 ❌ blocked on #12984 merge (dep resolution)
    brewkit skeletons #347 now has the audit hook + the original sketch

    The toolchain layer is mergeable on its own — the cascade unblocks once you have a chance to look at #12984.

  9. jhheider commented on May 22, 2026

    @jhheider
    Contributor

    well, it shouldn't bloat if it's a windows-only dep, right?

    build.dependencies.windows: llvm.org/mingw-w64: "*"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions