No description
Find a file
2026-06-06 11:54:16 -04:00
.claude Try and see if this will compile 2026-06-06 09:24:12 -04:00
.forgejo/workflows Fix upload 2026-06-06 11:54:16 -04:00
scripts Try and see if this will compile 2026-06-06 09:24:12 -04:00
.gitattributes Initial commit for the mingw-w64 xp toolchain 2026-06-06 07:39:13 -04:00
.gitignore Initial commit for the mingw-w64 xp toolchain 2026-06-06 07:39:13 -04:00
build.sh Initial commit for the mingw-w64 xp toolchain 2026-06-06 07:39:13 -04:00
README.md Fix upload 2026-06-06 11:54:16 -04:00
versions.env Try and see if this will compile 2026-06-06 09:24:12 -04:00

i686-w64-mingw32 toolchain for Windows XP

A Windows-hosted cross/native toolchain that builds 32-bit executables which run on Windows XP, using the latest GCC and binutils (for the fixed LTO) paired with an XP-capable mingw-w64 runtime.

Component Default Role
GCC 16.1.0 compiler — latest
binutils 2.46.0 assembler/linker — latest (the LTO fixes you want)
mingw-w64 v12.0.0 target runtime — this is what makes XP support possible
C runtime msvcrt the only CRT that ships on Windows XP (UCRT does not)
_WIN32_WINNT 0x0501 default Windows version baked into the headers (= XP)
threads win32 native Win32 threads, no winpthreads dependency

Triplets (this is a Canadian cross)

build  = x86_64-pc-linux-gnu     the Linux CI runner doing the compiling
host   = x86_64-w64-mingw32      where the toolchain RUNS (your 64-bit Windows dev box)
target = i686-w64-mingw32        what the toolchain GENERATES code for (XP)

The compiler binaries are statically linked, so the unzipped toolchain has no external libstdc++-6.dll / libwinpthread-1.dll dependency.


The key insight: compiler version ≠ XP support

XP support lives entirely in the target runtime (the mingw-w64 CRT and headers), and that is decoupled from the compiler. So you can freely use GCC 16 + binutils 2.46 while still targeting XP — you just have to:

  1. Build the mingw-w64 CRT with --with-default-msvcrt=msvcrt (UCRT is not present on a stock XP install).
  2. Bake --with-default-win32-winnt=0x0501 into the headers.

Modern mingw-w64 (v13/v14) has shifted its defaults toward UCRT and Win7+, which is why v12.0.0 is the default here. If you hit any XP runtime issue, drop to the most conservative known-good release:

MINGW_VERSION=v11.0.1 ./build.sh

Two independent WINVER knobs

Knob Default Meaning
DEFAULT_WIN32_WINNT 0x0501 what your code targets by default (Windows XP)
LIB_WIN32_WINNT 0x0600 what the prebuilt runtime libs (libgcc/libstdc++) compile against

libstdc++ cannot compile at 0x0501: its C++20 <chrono> time-zone database calls GetDynamicTimeZoneInformation (Vista+), and the win32-threads std::condition_variable support is gated on _WIN32_WINNT >= 0x0600 in gthr-win32.h. So the runtime libs are built against Vista headers while your code's default stays XP.

⚠️ What this means for win32 threads on XP (and why you should verify on a real XP VM):

Feature Runs on XP?
std::thread, std::mutex yes (_beginthreadex + CRITICAL_SECTION)
std::condition_variable, timed mutexes ⚠️ compiles, but imports Vista APIs → needs Vista+ at runtime
std::chrono time-zone DB (current_zone() …) ⚠️ same — Vista+ at runtime
plain C, iostreams, containers, strings, exceptions yes

These Vista-only bits live in separate .a objects, so a binary that doesn't use them stays XP-safe. The CI smoke test proves binaries import msvcrt.dll (not UCRT) with an XP PE subsystem, but it cannot run them on XP.

Need std::condition_variable to actually run on XP? That requires posix threads + an XP-capable winpthreads:

THREAD_MODEL=posix MINGW_VERSION=v11.0.1 ./build.sh

(You'd also need to re-enable building winpthreads in Phase A — ask if you want that variant wired up.)


Run it on your Forgejo instance

  1. Create a repo and push these files.
  2. Make sure you have a Forgejo runner with the Docker backend registered. Edit runs-on: in .forgejo/workflows/build.yml to match a label your runner advertises (the workflow pins the container to ubuntu:26.04 and installs node in its first step — required because the forgejo-runner runs JS actions with node inside the container — so the label only needs to mean "Docker").
  3. Trigger it from the Actions tab → Run workflow (you can override the GCC / binutils / mingw-w64 versions there), or push a tag like v1 to also cut a release with the zip attached.
  4. Download the i686-w64-mingw32-xp-gccX.Y.Z artifact.

Notes on the workflow

  • actions/checkout and actions/cache are resolved through your instance's action proxy. If your Forgejo isn't configured to proxy github.com, point it at https://code.forgejo.org/actions/... (set [actions].DEFAULT_ACTIONS_URL or prefix the uses: values).
  • Artifact upload uses https://code.forgejo.org/forgejo/upload-artifact@v4, not actions/upload-artifact@v4 — the GitHub v4 action uses an artifact API (v2/twirp) that Forgejo doesn't implement and rejects as "GHES".
  • The release step uses secrets.GITHUB_TOKEN, which Forgejo provides to Actions automatically — no manual secret needed.

Build locally (Linux / WSL / container) instead

You need a Debian/Ubuntu-ish box with the mingw bootstrap compiler:

sudo apt-get install -y build-essential g++-mingw-w64-x86-64 gcc-mingw-w64-x86-64 \
  curl xz-utils bzip2 zip texinfo bison flex m4 autoconf automake libtool gettext python3 file

./build.sh                 # full build -> ./i686-w64-mingw32-xp-*.zip
# or run phases individually:
./build.sh cross           # Phase A: Linux-hosted cross + target sysroot
./build.sh canadian        # Phase B: Windows-hosted toolchain
./build.sh package         # Phase C: smoke test + zip

Override versions via the environment:

GCC_VERSION=16.1.0 BINUTILS_VERSION=2.46.0 MINGW_VERSION=v11.0.1 JOBS=8 ./build.sh

This is not meant to run on native MSYS2/Windows — the Canadian cross needs a Linux build environment and the apt g++-mingw-w64-x86-64 bootstrap compiler. Use the Docker/Linux runner (or WSL) as designed.


How the build works

Because you can't execute a freshly-built Windows .exe on the Linux runner, the target runtime libraries are built once by a Linux-hosted compiler and then reused by the Windows-hosted compiler:

Phase A  scripts/01-cross.sh   build=host=Linux, target=XP
  binutils → mingw headers → gcc stage1 → mingw CRT (msvcrt) → gcc stage2
  ⇒ a Linux-hosted i686-w64-mingw32 compiler + the TARGET sysroot
    (libgcc, libstdc++, CRT — all host-independent machine code)

Phase B  scripts/02-canadian.sh   build=Linux, host=Windows, target=XP
  import Phase A's target sysroot → binutils (host) → gcc `make all-host`
  ⇒ i686-w64-mingw32-gcc.EXE et al., statically linked, in dist/

Phase C  scripts/03-package.sh
  compile hello.c/.cpp with the Phase A compiler, assert msvcrt import +
  XP PE subsystem, write TOOLCHAIN-MANIFEST.txt, zip dist/

make all-host / install-host is what keeps Phase B from trying to rebuild (and run) target libraries on Linux.

Using the toolchain on Windows

Unzip, add i686-w64-mingw32-xp\bin to PATH, then:

i686-w64-mingw32-gcc.exe hello.c -o hello.exe
i686-w64-mingw32-g++.exe -static hello.cpp -o hello.exe   :: -static avoids shipping libgcc/libstdc++ DLLs

The headers already default to XP (_WIN32_WINNT=0x0501); raise it per-TU with -D_WIN32_WINNT=0x0600 if a specific build needs newer APIs.

Layout

versions.env                 pinned, env-overridable versions + XP knobs
build.sh                     orchestrator (cross | canadian | package | all)
scripts/lib.sh               shared helpers (fetch/extract/log)
scripts/01-cross.sh          Phase A
scripts/02-canadian.sh       Phase B
scripts/03-package.sh        Phase C
.forgejo/workflows/build.yml Forgejo Actions CI