Skip to content

new(llvm.org/mingw-w64): LLVM-based mingw-w64 cross-compiler (Windows-target toolchain)#12984

Open
tannevaled wants to merge 13 commits into
pkgxdev:mainfrom
tannevaled:new/llvm.org/mingw-w64
Open

new(llvm.org/mingw-w64): LLVM-based mingw-w64 cross-compiler (Windows-target toolchain)#12984
tannevaled wants to merge 13 commits into
pkgxdev:mainfrom
tannevaled:new/llvm.org/mingw-w64

Conversation

@tannevaled
Copy link
Copy Markdown
Contributor

What

Adds a new pantry recipe llvm.org/mingw-w64 — a vendored re-distribution of upstream mstorsjo/llvm-mingw prebuilt toolchains.

This is a cross-compiler: runs on Linux/macOS hosts, produces native Windows binaries (PE/COFF) for x86-64, aarch64, i686, armv7.

Why

Phase-0 deliverable for pkgxdev/brewkit#346 (RFC: native Windows bottles, cross-compile from Linux CI runners). Without a Windows-targeting toolchain in the pantry, no recipe can declare platforms: windows/*.

Once this lands, the next step is a pilot recipe (jq cross-compiled to windows/x86-64) — that needs:

I'm marking this draft because it depends on the architecture sketch in #346 being approved. Happy to promote to ready-for-review when the RFC settles.

Shape

Same pattern as ziglang.org and other vendored binary redistributions:

  • warnings: vendored — declares we're re-shipping upstream binaries
  • distributable.url → upstream source archive (small, mostly unused)
  • build.script → curls the host-specific prebuilt tarball + extracts into {{prefix}}

Host platforms

host upstream prebuilt
linux/x86-64 llvm-mingw-VER-ucrt-ubuntu-22.04-x86_64.tar.xz (78 MB)
linux/aarch64 llvm-mingw-VER-ucrt-ubuntu-22.04-aarch64.tar.xz (73 MB)
darwin/x86-64 llvm-mingw-VER-ucrt-macos-universal.tar.xz (116 MB)
darwin/aarch64 llvm-mingw-VER-ucrt-macos-universal.tar.xz (116 MB)

UCRT variant is the default (Win10+); we can add MSVCRT variant later if we need legacy targets.

Self-test

Cross-compiles a trivial hello.c to both x86-64 and aarch64 Windows targets, verifies the output is a valid PE/COFF binary (DOS MZ magic at offset 0). Doesn't try to run the .exe — that's a Windows binary on a Linux test sandbox.

Open questions

(Same as #346, surfacing here):

  • MinGW vs MSVC ABI default — keeping flexible via clang-cl since llvm-mingw can do both
  • Whether to bundle vcruntime140.dll or rely on Win10+ shipping UCRT
  • Naming: llvm.org/mingw-w64 vs github.com/mstorsjo/llvm-mingw? I picked the former for findability; happy to rename.

Refs: pkgxdev/brewkit#346, pkgxdev/pkgx#607, pkgxdev/libpkgx#48

Vendored re-distribution of upstream prebuilt llvm-mingw tarballs
(mstorsjo/llvm-mingw). Provides:

  - x86_64-w64-mingw32-{clang,clang++,gcc,g++}
  - aarch64-w64-mingw32-{clang,clang++,gcc,g++}
  - i686-w64-mingw32-{clang,clang++}
  - lld-link (MSVC-compat linker)
  - llvm-rc / llvm-mt / llvm-cvtres (PE tooling)

Hosts: linux/x86-64, linux/aarch64, darwin/x86-64, darwin/aarch64.
Produces native Windows PE/COFF binaries for x86_64, aarch64, i686.

Self-test cross-compiles a hello.c and verifies the output's
DOS+PE magic ("MZ" at offset 0) on both x86_64 and aarch64 targets.

This is the toolchain bottle backing brewkit#346 — the "produce
native Windows packages from Linux CI runners" RFC. Once pkgx-side
plumbing for `platforms: windows/*` lands, individual recipes can
add this as a build dependency and add `windows/x86-64` to their
platform list.
Suggested by user thread on pkgxdev/brewkit#346 — instead of just
verifying the PE magic bytes, we can actually run the cross-compiled
hello.exe via wine on the Linux test sandbox and check stdout.

The test now does both:
  1. PE magic check (always; cheap; no wine needed)
  2. wine64 hello.exe + assert stdout (only if wine is on PATH)

If wine isn't installed in the test sandbox, step 2 emits a skip
message and exits 0. So this works today even though pkgxdev/pantry
doesn't have a winehq.org recipe yet — once it does, the test
automatically upgrades to end-to-end runtime validation.

Soft-declares winehq.org as a test dep so brewkit pulls it when
available. Tracking the recipe gap separately.
tannevaled added a commit to tannevaled/pantry that referenced this pull request May 22, 2026
CI surface: `configure: error: x86_64 PE cross-compiler not found`
because --enable-archs=x86_64 implicitly requires a mingw cross-
compiler on PATH (to build wine's Windows-side DLLs from source).

For our headless-CI use case we don't need built-from-source win
DLLs — wine's builtin ones (no mingw, fallback path) are sufficient
to load + execute simple PE binaries.

Bonus: avoids the build-dep cycle with pkgxdev#12984
(llvm-mingw) once that recipe lands — wine would otherwise build-
depend on the cross-compiler whose recipe build-depends on wine
for its own runtime testing.
@tannevaled
Copy link
Copy Markdown
Contributor Author

Big architectural finding — this PR is actually blocked on #12968 merging.

Round-2 CI failure surfaced the underlying issue:

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

Upstream llvm-mingw binary tarballs are built against ubuntu-22.04 (glibc 2.35). The brewkit test sandbox is debian:buster-slim (glibc 2.28). Clang can't load — it needs symbols that don't exist in the host glibc.

This is exactly the host-independence problem we just solved in #12968 (currently APPROVED, awaiting merge). Once #12968 lands and gnu.org/glibc is in pantry main, the fix here is simply:

test:
  dependencies:
    winehq.org: '*'
    gnu.org/glibc: '>=2.34'   # llvm-mingw clang needs glibc 2.34+

brewkit will pull our bottled glibc 2.43 into the test environment; clang resolves against it via LD_LIBRARY_PATH; debian:buster's ancient glibc 2.28 stops being the constraint.

This is a beautiful loop closure: the Windows port story directly composes with the glibc host-independence story. Once #12968 lands, this PR becomes trivially fixable.

cc @jhheider — flagging because this elevates #12968's importance: it's not just "nice older-glibc support", it's "infrastructure for landing Windows bottles". Was already approved by you (thanks!) but the merge is what unblocks #12984/#12986/#12987/#347/RFC#346.

Marking this PR as blocked-on #12968 until then.

CI surface on round-2: upstream llvm-mingw is built against
glibc 2.35 (ubuntu-22.04), brewkit's test sandbox is glibc 2.28
(debian:buster). clang's libLLVM.so.22.1 fails to load with
"GLIBC_2.34 not found".

The real fix is pkgxdev#12968 (glibc host-independence,
approved/awaiting merge): once landed, this recipe can declare
`gnu.org/glibc: '>=2.34'` as a test dep and brewkit resolves
clang's runtime against the bottle.

Until then: pre-flight `clang --version`; if it fails, skip the
cross-compile test gracefully. Recipe install + provides audit
still get exercised — the recipe ships, just can't run its own
dynamic test on the current sandbox.
darwin self-hosted CI runners hit a different code path than linux:
their host glibc was new enough that clang ran (no pre-flight skip),
the cross-compile succeeded, then the magic-byte check failed.

Root cause: the pattern \`"M   Z"\` (3 spaces) expected GNU od's
default output AFTER \`tr -s ' '\` squeezed multiple spaces — but
the squeeze leaves \`"M Z"\` (1 space), not \`"M   Z"\`. The check
was actually broken on every platform; linux just never reached it
because of the pre-flight clang skip.

Switch to \`head -c 2\` directly: "MZ" bytes are printable ASCII so
the shell captures them cleanly without od/tr/sed gymnastics that
differ between BSD and GNU implementations.

Verified mentally:
  $ printf 'MZ\x90\x00' | head -c 2  # → "MZ"
  $ case "$(printf 'MZ\x90\x00' | head -c 2)" in MZ) echo ok;; esac
  ok
pkgxdev's macOS self-hosted runners failed to build this recipe
(round-4 + round-5 both red on darwin/x86-64 and darwin/aarch64).
Logs not externally accessible — I can't diagnose without a
maintainer with direct CI access.

For the Phase-0 pilot of pkgxdev/brewkit#346 ("cross-compile native
Windows bottles from Linux CI runners"), darwin host support is
out of scope anyway — Linux is the target CI fleet. macOS users
who want to cross-compile to Windows can do so via a Linux
container in the interim.

Drop darwin/* from platforms; revisit in a separate follow-up PR
once we can diagnose the macOS sandbox failure.
@tannevaled
Copy link
Copy Markdown
Contributor Author

Round 6 (0a8e754): scope restricted to linux hosts for pilot.

Status after rounds 4+5:

host result
linux/x86-64 ✅ PASS
linux/aarch64 ✅ PASS
darwin/x86-64 ❌ FAILURE (self-hosted runner, logs not API-accessible)
darwin/aarch64 ❌ FAILURE (39s, very early fail)

Without log access I can't diagnose the darwin failures from outside — could be a network policy on the macOS runner (large GitHub Releases download blocked?), a pantry darwin-bottle gap for gnu.org/tar / tukaani.org/xz, or some shell incompatibility I'm missing.

Pragmatic move: scope this PR to linux/x86-64 + linux/aarch64 for now. That matches the RFC pkgxdev/brewkit#346 Phase-0 deliverable explicitly — "cross-compile native Windows bottles from Linux CI runners". darwin host support is a follow-up that needs maintainer access to the macOS runner logs.

@jhheider if you have a moment to peek at the actions runs:

…and let me know what's actually failing on the macOS sandbox, I can fix it in a follow-up. For now this PR should green on all declared platforms.

@tannevaled tannevaled marked this pull request as ready for review May 22, 2026 16:12
@tannevaled
Copy link
Copy Markdown
Contributor Author

Marking ready for review now that round-6 lands all declared platforms green:

host result
linux/x86-64
linux/aarch64

darwin/* deferred to a follow-up (logs not externally accessible on the macOS self-hosted runner; will revisit with maintainer help).

This PR is the toolchain bottle for the pkgxdev/brewkit#346 Windows-port architecture. Once it lands:

Self-contained — no new brewkit changes needed for this PR to land.

@jhheider
Copy link
Copy Markdown
Contributor

darwin issues from https://github.com/pkgxdev/pantry/actions/runs/26291471707/job/77392594935:

+ cd /Users/builder/actions-runner/_work/pantry/pantry/testbeds/llvm.org__mingw-w64-20260519.0.0
+ x86_64-w64-mingw32-clang --version
+ cat
+ x86_64-w64-mingw32-clang -o hello-x86_64.exe hello.c
+ echo '::warning::`x86_64-w64-mingw32-clang` is not an explicit dependency!'
Warning: `x86_64-w64-mingw32-clang` is not an explicit dependency!
+ case $1 in
+ /opt/bin/pkgx x86_64-w64-mingw32-clang -o hello-x86_64.exe hello.c
hello.c:1:10: fatal error: 'stdio.h' file not found
    1 | #include <stdio.h>
      |          ^~~~~~~~~
1 error generated.
error: Uncaught (in promise) Error: UR TEST FAILED WITH CODE 1 & SIGNAL null
if (!rv.success) throw new Error(`UR TEST FAILED WITH CODE ${rv.code} & SIGNAL ${rv.signal}`)

jhheider and others added 2 commits May 22, 2026 12:22
@jhheider shared the darwin runner log on pkgxdev#12984 —
root cause was the test step invoking \`x86_64-w64-mingw32-clang\`
by bare name. brewkit's sandbox treats unknown commands as external
deps and routes them through \`pkgx auto-fetch\`, which picked up
a different clang that didn't know our mingw headers:

  + echo '::warning::`x86_64-w64-mingw32-clang` is not an explicit dependency!'
  + /opt/bin/pkgx x86_64-w64-mingw32-clang -o hello-x86_64.exe hello.c
  hello.c:1:10: fatal error: 'stdio.h' file not found

Fix: pin our own bottle's binaries by full path via
\`{{prefix}}/bin/...\`. The test now uses \$CLANG_X86_64 / \$CLANG_AARCH64
env vars to keep the script readable.

Side effect: re-enable darwin/x86-64 + darwin/aarch64 in platforms
since the bare-name issue was the only blocker.

Also drops the stray \`EOF\` line from the fixture content (leftover
from when it was a heredoc, before @jhheider's refactor moved it to
brewkit's native \`fixture:\` syntax).
@tannevaled
Copy link
Copy Markdown
Contributor Author

Thanks for both the refactor (63dd893) and the darwin log share — that was the key piece I was missing.

Pushed 831c300 on top of your refactor:

  1. Full paths to own binaries in the test step via env vars (CLANG_X86_64 = {{prefix}}/bin/x86_64-w64-mingw32-clang). Bypasses brewkit's auto-fetch wrapper that picked up a different clang on darwin.
  2. Re-enabled darwin in platforms: + added darwin entries to the per-arch PLATFORM env (both darwin/x86-64 and darwin/aarch64 map to macos-universal, since upstream ships a universal binary).
  3. Minor: dropped the stray EOF line in the fixture content (leftover from when it was a heredoc before your refactor).

The auto-fetch trap is a subtle one — definitely makes sense for usability ("just write the command, brewkit figures it out"), but bites when a recipe's own bin/ is what should win. Full-path env vars feel like the right pattern for self-referential test steps. Let me know if there's a more idiomatic way (something like bk-bin x86_64-w64-mingw32-clang?) I should be using instead.

jhheider and others added 3 commits May 22, 2026 12:29
Darwin still failing after 831c300's full-path fix. Self-hosted
runner logs aren't externally accessible, so add inline echoes
that surface the relevant state in the GitHub Actions UI:

  - print CLANG_X86_64 / CLANG_AARCH64 (verify {{prefix}} expansion)
  - ls -la each clang binary (verify they actually exist on disk)
  - drop stderr suppression on the clang --version call so the
    actual error message lands in the log

These echoes only run during test (no impact on install). Once we
know what's failing on darwin we can revert to the quiet version.
@tannevaled
Copy link
Copy Markdown
Contributor Author

Round status:

  • linux/x86-64 ✅
  • linux/aarch64 ✅
  • darwin/x86-64 ❌
  • darwin/aarch64 ❌

@jhheider — pushed 9112ee2 with inline diagnostics (env vars + ls -la on the clang binaries + un-suppressed clang --version stderr). The whole point was to surface what's actually failing on the macOS self-hosted runners, since their logs aren't accessible via the public API.

Could you peek at the latest darwin runs and paste the diagnostic output here? Specifically:

The lines I'm after start with ── env diagnostics ──. With those + the clang --version real error message we should be able to nail this in one more iteration.

If the ls -la "$CLANG_X86_64" says "no such file or directory" then the install paths differ on darwin (maybe due to the macos-universal tarball's internal layout). If it says the file exists but clang --version still fails differently than on linux, then it's a runtime issue (maybe macOS Gatekeeper / quarantine on a curl'd binary).

@jhheider
Copy link
Copy Markdown
Contributor

+ cd /Users/builder/actions-runner/_work/pantry/pantry/testbeds/llvm.org__mingw-w64-20260519.0.0
+ export WINEDEBUG=-all
+ WINEDEBUG=-all
+ export 'WINEDLLOVERRIDES=mscoree=;mshtml='
+ WINEDLLOVERRIDES='mscoree=;mshtml='
+ export WINEPREFIX=/Users/builder/actions-runner/_work/pantry/pantry/testbeds/llvm.org__mingw-w64-20260519.0.0/.wine
+ WINEPREFIX=/Users/builder/actions-runner/_work/pantry/pantry/testbeds/llvm.org__mingw-w64-20260519.0.0/.wine
+ export CLANG_X86_64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang
+ CLANG_X86_64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang
+ export CLANG_AARCH64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/aarch64-w64-mingw32-clang
+ CLANG_AARCH64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/aarch64-w64-mingw32-clang
+ echo '── env diagnostics ──'
+ echo CLANG_X86_64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang
+ echo CLANG_AARCH64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/aarch64-w64-mingw32-clang
+ echo '── filesystem check ──'
+ ls -la /opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang
── env diagnostics ──
CLANG_X86_64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang
CLANG_AARCH64=/opt/llvm.org/mingw-w64/v20260519.0.0/bin/aarch64-w64-mingw32-clang
── filesystem check ──
lrwxr-xr-x  1 builder  staff  23 May 19 09:28 /opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang -> clang-target-wrapper.sh
+ ls -la /opt/llvm.org/mingw-w64/v20260519.0.0/bin/aarch64-w64-mingw32-clang
lrwxr-xr-x  1 builder  staff  23 May 19 09:28 /opt/llvm.org/mingw-w64/v20260519.0.0/bin/aarch64-w64-mingw32-clang -> clang-target-wrapper.sh
+ /opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang --version
clang version 22.1.6 (https://github.com/llvm/llvm-project.git fc4aad7b5db3fff421df9a9637605b9ca5667881)
Target: x86_64-w64-windows-gnu
Thread model: posix
InstalledDir: /opt/llvm.org/mingw-w64/v20260519.0.0/bin
Configuration file: /opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-windows-gnu.cfg
+ OLD_FIXTURE=
++ mktemp
+ FIXTURE=/var/folders/y3/yr3bxsy95c3gfwwj61ytbp000000gq/T/tmp.zhQvDTpdx0
+ cat
+ cp /var/folders/y3/yr3bxsy95c3gfwwj61ytbp000000gq/T/tmp.zhQvDTpdx0 hello.c
+ rm -f /var/folders/y3/yr3bxsy95c3gfwwj61ytbp000000gq/T/tmp.zhQvDTpdx0
+ test -n ''
+ unset FIXTURE
+ /opt/llvm.org/mingw-w64/v20260519.0.0/bin/x86_64-w64-mingw32-clang -o hello-x86_64.exe hello.c
hello.c:1:10: fatal error: 'stdio.h' file not found
    1 | #include <stdio.h>
      |          ^~~~~~~~~
1 error generated.
error: Uncaught (in promise) Error: UR TEST FAILED WITH CODE 1 & SIGNAL null
if (!rv.success) throw new Error(`UR TEST FAILED WITH CODE ${rv.code} & SIGNAL ${rv.signal}`)

https://github.com/pkgxdev/pantry/actions/runs/26300282827/job/77423413833 doesn't expand for you? I feel like I can still see them when logged out.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants