Skip to content

Add x86 multilib toolchain support - #412

Merged
skeeto merged 13 commits into
skeeto:masterfrom
Peter0x44:multilib
Aug 30, 2026
Merged

Add x86 multilib toolchain support#412
skeeto merged 13 commits into
skeeto:masterfrom
Peter0x44:multilib

Conversation

@Peter0x44

Copy link
Copy Markdown
Collaborator

Keep x86_64 as the default output while enabling the GCC and mingw-w64 32-bit multilib under lib32. Build 32-bit support archives, CRT libraries, and winpthreads for both the bootstrap and final sysroots so each compiler stage has a complete i686 runtime.

Use pentium4 as the 32-bit architecture baseline. Add a GCC specs rule that defaults _WIN32_WINNT to 0x0501 only under -m32 and only when the caller has not supplied a value, preserving the existing x64 default and explicit overrides.

Retain the x86_64-w64-mingw32-prefixed tools and add a separate build step for an i686-w64-mingw32 interface. Compiler drivers inject -m32. Target-sensitive binutils select their 32-bit modes explicitly: as uses --32, ld uses -m i386pe, dlltool uses -m i386 with --as-flags=--32, and windres uses --target=pe-i386. Input-driven inspection and archive tools forward without a target override.

These forwarding executables provide both prefixed interfaces without duplicating a second binutils installation. They deliberately remain selectors for the underlying x86_64-configured multilib compiler rather than spoofing its configured target identity.

Direct, Autoconf, CMake, and Libtool probes using the i686-prefixed interface all produced PE-i386 output.

@Peter0x44

Peter0x44 commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

This should probably be turned into a variant patch, but it was easier to submit and review it this way for now.
Obviously, don't merge it.

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

related #36 #81

@Peter0x44
Peter0x44 marked this pull request as draft August 9, 2026 21:53
@Peter0x44

Copy link
Copy Markdown
Collaborator Author
    "--with-specs=%{m32:%{!D_WIN32_WINNT*:-D_WIN32_WINNT=0x0501}}" \

despite that this spec works, I wonder if it would make sense to submit upstream having a per-arch default win32 winnt.

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

I replaced those specs with a patch in mingw headers instead.

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

@skeeto what do you think of offering this variant? Has been working fine in my testing.
Both -m32 and the i686-w64-mingw32 aliases.
Debugging the 32 bit exes with gdb also seems to work.

skeeto added a commit that referenced this pull request Aug 11, 2026
The actual tools are not included and these aliases point at nothing.
Plucked from #412.
@skeeto

skeeto commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Excellent attention to detail and solid work. You've been quite thorough! Good catch on readelf and elfedit. I didn't realize those aliases where in there, and I cherry-picked that fix separately as d0d13ce.

Your branch is in such good shape that the feasibility questions are all resolved, and it's down to deciding whether or not this is something I want. When I started w64dk I omitted multilib in order to keep the toolchain simple and small, and I saw 32-bit as dead. Then with some prodding from the community I realized it was a stone's throw from supporting x86 retro-development (if only I could toss a copy back to myself ~20 years ago...), and so I added the alternative 32-bit toolchain, still refraining from multilib for simplicity.

I'm not reading this wrong, am I? The release only 6MB larger with mutlilib?!

A couple things:

  • i686-mingw32-w64-gcc -dumpmachine reports x86_64-mingw32-w64, though that obviously didn't interfere with the builds you tested. It would take a specialized alias that parses options like GCC, or a GCC patch, which may be simpler.
  • The 32-bit widl alias should get --win32.

I'll have to spend some time using it to see if I spot anything else.

@Peter0x44

Peter0x44 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

I don't use 32 bits often, but when I do, it's usually for the purpose of replacing a dll in an existing program with an agent assisting "reverse engineering". I saw some of your recent projects fit this use case so thought you might be interested. I was using LLVM-mingw for that, which I always keep on my PATH, but thought, why not make w64devkit do it too.

This patch was written completely by gpt 5.6 in one shot, aside from the minor points on my reviews (the existence of elf edit and replacing the spec).

I was impressed that worked.

@anzz1

anzz1 commented Aug 12, 2026

Copy link
Copy Markdown

I'll just chime in saying that I also use w64devkit for building both 64-bit and 32-bit binaries, and even on 64-bit platforms for example Lua <= 5.2 that doesn't support 64-bit integers anyway uses less memory on 32-bit but more importantly its easier to work with 32-bit DLLs for the 64-bit integer non-support reasons. For example, passing a handle from 64-bit winapi requires to make it a special data type, while using 32-bit winapi can just use a 32-bit integer like normal. So there are reasons to use 32-bit for other reasons too that are not backwards compatibility with 32-bit systems.

That being said, I found it perfectly fine to just the x86 version of the w64devkit to build anything 32-bit. As long as x64 and x86 versions exist of the devkit, I don't see a necessity for multilib, but on the other hand this would make it possible to build both 64-bit and 32-bit binaries with one build script, so that's a plus.

@Peter0x44

Peter0x44 commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

A couple things:

i686-mingw32-w64-gcc -dumpmachine reports x86_64-mingw32-w64, though that obviously didn't interfere with the builds you tested. It would take a specialized alias that parses options like GCC, or a GCC patch, which may be simpler.
The 32-bit widl alias should get --win32.

I fixed both of these. Anything else to think about?

Honestly, I always found the whole "variant" patch being applied to be a little cumbersome. Perhaps having a way to merge all variants into the dockerfile based on some sort of cli arg passed to docker would be good.

@skeeto

skeeto commented Aug 29, 2026

Copy link
Copy Markdown
Owner

I've decided to keep multilib, and so I pushed some changes to your branch.

I noticed dllwrap lacked the 32-bit prefix, but I realized that it's long deprecated and we really don't want it anyway. I also cut i686-w64-mingw32-c++ (in a fixup we'll squash out) because that's not a real alias. There's no $ARCH-linux-gnu-c++ on your Linux systems.

I agree about the variant patch being clunky. I had never found a satisfactory alternative, it gets worse with multilib. Some months back I asked Opus 4.6 or 4.7 to propose alternatives, and it didn't come up with anything better. Trying again with Fable 5, it came up with this build-arg approach that I like. What do you think? Hopefully the corresponding GitHub Action changes work, and we'll find out in a little while.

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

It looks... OK. I don't really like splitting out the args into individual files. I feel that's unneeded. And the x64 variant is now a multilib variant. I didn't intend that.
I think gpt 5.6 was making a better version in my case.

Comment thread Dockerfile Outdated
Comment thread Dockerfile
&& scons -j$(nproc) \
XGCC_W32_PREFIX=$ARCH- \
TARGET_ARCH=amd64 \
TARGET_ARCH=$NSIS_ARCH \

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, does nsis need to be able to make "multilib" 32-or-64 bit installers?
I'm not going to use it, just curious. I've actually made windows installers myself at my job it's a surprisingly tricky problem.

@Peter0x44

Peter0x44 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

What do you think about it now? I feel I made improvements and it's a bit easier to read.

@Peter0x44
Peter0x44 marked this pull request as ready for review August 30, 2026 00:29
@Peter0x44
Peter0x44 requested a balanced review from Copilot August 30, 2026 00:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

It removes existing dllwrap interfaces and does not implement the described GCC specs behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds selectable x64, x86, and multilib toolchains while preserving x64 defaults.

Changes:

  • Introduces Docker build variants and 32-bit runtime support.
  • Adds i686-prefixed compiler and binutils interfaces.
  • Extends local and CI builds to produce all variants.
File summaries
File Description
Dockerfile Builds selectable toolchain variants and i686 aliases.
.github/workflows/build.yml Builds and releases all three variants.
multibuild.sh Adds multilib variant selection.
README.md Documents variant builds.
src/mingw-default-win32-winnt.patch Sets the 32-bit Windows API baseline.
src/crossgcc-i686-dumpmachine.patch Reports i686 for aliased compiler drivers.
src/variant-x86.patch Removes the obsolete patch-based variant mechanism.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Dockerfile
Comment thread src/mingw-default-win32-winnt.patch
@Peter0x44

Peter0x44 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

What a useless review from copilot. Oh well, it must have picked a dumb model this time.

@skeeto

skeeto commented Aug 30, 2026

Copy link
Copy Markdown
Owner

I don't want to complicate downloading (and building) with a new variant. As I had noted, multilib only adds 6MB to the distribution, so the cost is small. What's your concern about rolling it into x64?

I wonder, does nsis need to be able to make "multilib" 32-or-64 bit installers?

Good catch, I didn't think of this. NSIS is a toolchain itself: inputs source code and assets, produces a native program for a particular target. Looks like we could build and install stubs for both, and the Target directive will pick the appropriate stub.

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

What's your concern about rolling it into x64?

I guess it's no big deal. I just figured, there's probably people who don't care at all about 32-bits, and don't want this bloat. But now there's way bigger and more controversial/objectionable bloat so I guess it doesn't matter.

I'll roll it back then.

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

Good catch, I didn't think of this. NSIS is a toolchain itself: inputs source code and assets, produces a native program for a particular target. Looks like we could build and install stubs for both, and the Target directive will pick the appropriate stub.

And actually, it seems it has several "variants". ANSI and Unicode.

 In upstream NSIS, “ANSI” and “Unicode” describe the installer runtime’s string ABI, not its CPU architecture and not
 its manifest.

 ### x86-ansi

 - PE32/i386 installer process.
 - NSIS runtime strings use narrow char/TCHAR.
 - Calls the Win32 ...A APIs.
 - Text is constrained by the active Windows code page.
 - Uses ANSI-ABI plugins from Plugins/x86-ansi.
 - Exists mainly for NSIS 2 compatibility, old third-party plugins, and legacy scripts.

 ### x86-unicode

 - Also PE32/i386; same CPU architecture.
 - Runtime strings use wide wchar_t/TCHAR.
 - Calls the Win32 ...W APIs.
 - String tables and variables can represent Unicode independently of the system code page.
 - Uses Unicode-ABI plugins from Plugins/x86-unicode.

It seems like the ansi variants are bloat and we can remove them in general. Unless this NSIS 2 compatibility really matters (I don't know if it does).

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

I suggest we deal with this nsis stuff at a later time, it shouldn't block this.

@skeeto

skeeto commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Was "Use official 7-Zip source archive" simply pushed in the wrong branch? I didn't realize there was a tarball hosted on the main site. It doesn't matter much to me, and I only prefer whichever is more reliable.

Unless this NSIS 2 compatibility really matters (I don't know if it does).

I added NSIS only recently, motivated by my own ends, for applications that require installers to function properly (ex. https://github.com/skeeto/dumb-alt-tab). I don't intend for our NSIS to support legacy installers, and no ancient compatibility is necessary or wanted. I didn't realize it was including compatibility stubs in the x86 variant.

I've got a couple more tweaks cooking, which I'll share once it's ready.

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

Was "Use official 7-Zip source archive" simply pushed in the wrong branch?
Yes I force pushed the commit out of existence. A GitHub actions run failed to download it from source forge.

@skeeto

skeeto commented Aug 30, 2026

Copy link
Copy Markdown
Owner

I'm currently satisfied with the state of this MR (after git rebase -i). Anything else, @Peter0x44?

@Peter0x44

Copy link
Copy Markdown
Collaborator Author

LGTM. I suppose some of the history could be rewritten but I don't care enough.

Peter0x44 and others added 13 commits August 30, 2026 13:13
Keep x86_64 as the default output while enabling the GCC and mingw-w64
32-bit multilib under lib32. Build 32-bit support archives, CRT
libraries, and winpthreads for both the bootstrap and final sysroots so
each compiler stage has a complete i686 runtime.

Use pentium4 as the 32-bit architecture baseline. Add a GCC specs rule
that defaults _WIN32_WINNT to 0x0501 only under -m32 and only when the
caller has not supplied a value, preserving the existing x64 default and
explicit overrides.

Retain the x86_64-w64-mingw32-prefixed tools and add a separate build
step for an i686-w64-mingw32 interface. Compiler drivers inject -m32.
Target-sensitive binutils select their 32-bit modes explicitly: as uses
--32, ld uses -m i386pe, dlltool uses -m i386 with --as-flags=--32, and
windres uses --target=pe-i386. Input-driven inspection and archive tools
forward without a target override.

These forwarding executables provide both prefixed interfaces without
duplicating a second binutils installation. They deliberately remain
selectors for the underlying x86_64-configured multilib compiler rather
than spoofing its configured target identity.

Direct, Autoconf, CMake, and Libtool probes using the i686-prefixed
interface all produced PE-i386 output.
It's been deprecated for 6 years and unmaintained for 20 years.
Hoist every variant-sensitive value into an ARG at the top of the cross
stage, with defaults producing the x64 kit, and gate the multilib-only
sidecar builds on MULTILIB. The x86 variant becomes a set of NAME=value
overrides in src/variant-x86.args, which multibuild.sh turns into
--build-arg options. A plain "docker build" continues to work untouched,
downloads stay shared between variants, and the patch mechanism remains
available as a fallback for a future variant needing structural changes.

One hazard of this approach: ARGs leak into the environment of every
subsequent RUN in the stage, and configure scripts read arbitrary
environment variables. mingw-w64-crt decides whether to build the Win64
runtime by testing whether a shell variable named LIB64 is merely set,
so an ARG of that name forced lib64 on past an explicit --disable-lib64.
Hence CRT_LIB64. Keep ARG names clear of names configure might consult.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The variant patch no longer exists, so both x86 builds read
src/variant-x86.args into a step output, which build-push-action
accepts verbatim as its newline-delimited build-args input. The args
file remains the single definition of the variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the loosely coordinated collection of x86 build arguments with
a single VARIANT selector. Define x64, x86, and multilib configuration
stages, then select the requested stage as the common cross-compiler
base. This keeps each supported configuration closed and prevents
callers from combining incompatible architecture settings.

Make x64 the default standalone profile. It targets
x86_64-w64-mingw32, disables GCC multilib support, builds only the
64-bit CRT, and omits the secondary 32-bit libraries and aliases.

Keep the standalone x86 behavior in its own profile. It targets
i686-w64-mingw32, retains the Pentium 4 default, enables only the
32-bit CRT, preserves the Windows XP header default and manifest
behavior, selects the 32-bit BusyBox and NSIS configurations, disables
zstd threading, and raises _WIN32_WINNT only while building CMake.
CMake and its bundled curl require at least Windows Vista APIs, so the
C and C++ flags continue to target Windows 7 without changing the
XP-oriented default of the installed toolchain.

Define multilib as an x86_64 compiler with GCC multilib enabled, both
CRT library sets installed, and the existing i686 forwarding aliases.
Use a dedicated EXTRA_32 setting for the secondary lib32 libraries,
winpthreads builds, thread support object, and aliases instead of
inferring that work from the spelling of a GCC configure option.
Expose independent CRT_LIB32 and CRT_LIB64 settings so each profile
selects exactly the runtime libraries it needs.

Pass the profile's CMake C and C++ flags directly rather than building
an optional argument list through shell positional parameters. Keep all
other profile-specific values next to the selected base stage, including
GCC architecture and manifest flags, BusyBox configuration, zstd
options, and NSIS target architecture.

Simplify multibuild.sh to pass only VARIANT to Docker. Add -m for the
multilib flavor, make -a build all three profiles, and remove the old
argument-file and Dockerfile-patch fallback paths. Update unsigned and
signed CI builds to select x64 or x86 directly, remove the obsolete
variant-x86.args bundle, and document the new build interface.

Verify all three profiles with docker build --check and complete cross
toolchain builds. Smoke-test x64 and x86 compilation, plus both the
64-bit compiler and i686 alias in the multilib profile. Also build the
x64 and x86 CMake targets to exercise both the empty and Windows 7
CMake flag configurations.
Treat the multilib profile as a first-class CI and release artifact
alongside the standalone x64 and x86 profiles.

Add a build-multilib job that selects VARIANT=multilib, uses an
independent GitHub Actions cache scope, extracts the generated
self-extracting archive, and uploads it with a multilib-specific name.
Run it after the x64 job so the workflow retains the existing ordering
while allowing the x86 and multilib builds to proceed independently.

Make tagged releases wait for the multilib build as well. Build the
signed target with VARIANT=multilib, pass the same trusted-signing
credentials used by the other profiles, and pack the result as
w64devkit-multilib-<version>.7z.exe. The existing release glob then
publishes the signed multilib archive with the x64, x86, and source
artifacts.
Use the selected VARIANT directly for multilib-only control flow rather
than maintaining a separate EXTRA_32 boolean. Redeclare the global build
argument in the selected cross stage so each conditional can state the
actual profile requirement explicitly.

Represent GCC multilib and mingw-w64 CRT selections as enable or disable
state instead of storing complete configure options. Form the GCC and CRT
arguments at their call sites, which keeps punctuation out of the profile
data and makes the three configurations easier to compare.

Remove the standalone x86 WINNT_FLAGS override. The
mingw-default-win32-winnt patch already substitutes Windows XP for every
__i386__ compilation, so passing the same header default through configure
was redundant. Keep the separate Windows 7 CMake flags because CMake and
its bundled curl cannot build against the XP target.

Rename the remaining values that intentionally contain complete command
arguments to GCC_ARCH_FLAG, GCC_MANIFEST_FLAG, and ZSTD_THREAD_FLAG. The
component scope and representation are now visible in each name.

Verify the resulting x64, x86, and multilib Dockerfiles with build checks
and complete cross-toolchain builds. Confirm that only the multilib profile
executes the secondary 32-bit runtime and alias blocks and that each GCC and
CRT configure command receives the expected enable or disable options.
CMAKE_C_FLAGS and CMAKE_CXX_FLAGS are real CMake variable names, and
profile values sit in the environment of every RUN, including the other
cmake invocations that do not pass these flags explicitly. CMake does
not read them from the environment today, but it honors a growing list
of CMAKE_* environment variables, so real names are a latent hazard.
Rename them and document the naming discipline alongside the profiles,
which also covers why CRT_LIB32/CRT_LIB64 avoid their obvious names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
makensis is retargetable: the architecture of the installers it emits
is decided by which precompiled stub it attaches, stubs and plugins
are stored under target-suffixed names, and a script selects one with
"Target x86-unicode" (or CPU x86). Its default remains amd64, chosen
by the bitness of makensis itself. Add a second stub-and-plugin pass
so the multilib kit can emit 32-bit installers, which remain the
traditional choice even for 64-bit payloads since they run on every
Windows and can fail politely on 32-bit hosts.

The NSIS build derives each tool from XGCC_W32_PREFIX and never adds
-m32 on its own, so present the multilib compiler through an i686
interface: wrapper scripts that forward with -m32, --32, and
--target=pe-i386, mirroring the aliases the kit itself ships. The
x86-ansi extras come along unconditionally, matching the standalone
x86 kit's layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NSIS unconditionally builds ANSI stubs and plugin variants alongside
Unicode whenever the target CPU is x86, with no option to disable
them. ANSI installers are legacy compatibility w64devkit does not
need: makensis defaults to Unicode everywhere, and "Target x86-ansi"
fails cleanly with "adequate stub not found". This keeps the x86 kit
and the multilib kit's secondary x86 set Unicode-only, matching the
shape of the amd64 set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@skeeto
skeeto merged commit 54c7887 into skeeto:master Aug 30, 2026
@skeeto

skeeto commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Thanks, @Peter0x44! This was cheaper than I expected, and it simplifies the way I use w64devkit. I like having 32-bit tools on hand, but now I don't need the x86 variant unpacked somewhere to get it.

@Peter0x44
Peter0x44 deleted the multilib branch August 30, 2026 20:38
@Peter0x44
Peter0x44 restored the multilib branch August 30, 2026 20:47
@Peter0x44
Peter0x44 deleted the multilib branch August 30, 2026 20:47
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.

4 participants