Skip to content

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

@tannevaled

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions