Skip to content

Add x86 multilib toolchain support - #412

Draft
Peter0x44 wants to merge 7 commits into
skeeto:masterfrom
Peter0x44:multilib
Draft

Add x86 multilib toolchain support#412
Peter0x44 wants to merge 7 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.

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 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 and others added 4 commits August 29, 2026 12:57
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 <[email protected]>
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 <[email protected]>
@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.

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.

3 participants