- Shell 100%
| .claude | ||
| .forgejo/workflows | ||
| scripts | ||
| .gitattributes | ||
| .gitignore | ||
| build.sh | ||
| README.md | ||
| versions.env | ||
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:
- Build the mingw-w64 CRT with
--with-default-msvcrt=msvcrt(UCRT is not present on a stock XP install). - Bake
--with-default-win32-winnt=0x0501into 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::chronotime-zone DB (current_zone()…)⚠️ same — Vista+ at runtime plain C, iostreams, containers, strings, exceptions ✅ yes These Vista-only bits live in separate
.aobjects, so a binary that doesn't use them stays XP-safe. The CI smoke test proves binaries importmsvcrt.dll(not UCRT) with an XP PE subsystem, but it cannot run them on XP.Need
std::condition_variableto 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
- Create a repo and push these files.
- 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 toubuntu:26.04and installsnodein its first step — required because the forgejo-runner runs JS actions withnodeinside the container — so the label only needs to mean "Docker"). - Trigger it from the Actions tab → Run workflow (you can override the GCC
/ binutils / mingw-w64 versions there), or push a tag like
v1to also cut a release with the zip attached. - Download the
i686-w64-mingw32-xp-gccX.Y.Zartifact.
Notes on the workflow
actions/checkoutandactions/cacheare resolved through your instance's action proxy. If your Forgejo isn't configured to proxygithub.com, point it athttps://code.forgejo.org/actions/...(set[actions].DEFAULT_ACTIONS_URLor prefix theuses:values).- Artifact upload uses
https://code.forgejo.org/forgejo/upload-artifact@v4, notactions/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-64bootstrap 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