After many unsuccessful attempts of writing a third blog post, I just wanted to use this opportunity to share something useful I found after dabbling with porting an SDL2 app to Windows via MSYS2 MinGW x64.
TL;DR: You can collect all the MinGW DLLs your EXE file needs like this:
ldd my-cool-program.exe | grep /mingw64 | awk '{print $3}' | xargs -i cp {} .
You see, in the magical Linux realm you’ll never really worry about shipping programs in binary form. A nice tarball with a well documented build system is often regarded as good enough. If your program is actually used by people, its likely that distributions pick it up into their repositories. Or you use stuff like Flatpak or AppImage if you want to provide binaries yourself. But over in Windows-land providing .exe files is a normal part of distributing your programs.
Now, if you use any special libraries they are often dynamically linked and their code is stored externally to your .exe as a .dll. Some .dll files are simply stored somewhere in System32 and are thus available everywhere, but others have to be shipped manually alongside your program.
Now let me go over a short tangent praising the efforts the developers of MinGW64 have made. I wrote a little game in C using Lua and SDL2. On Linux, building that game was simple. I’d use GNU/Make, pkg-config and my distro’s package manager to collect the dependencies and tie everything together. I’d dreaded the Windows port but eventually gave it a shot. And holy shit. MSYS2 provides EXACTLY THE SAME workflow!
The packages’ names are a bit more arcane sounding (e.g.:
mingw-w64-x86_64-gcc
, mingw-w64-x86_64-SDL2
, mingw-w64-x86_64-lua
),
because MSYS2 provides multiple toolchains and thus the packages have to be
more explicit in their naming. But after installing everything and running
make
I had a working .exe file! Well… It was working after I fixed a
segfault that I didn’t catch under Linux anyways. But that one’s on me.
Running the .exe in MSYS2’s shell worked fine, but starting it outside of the
shell resulted in most DLLs being unavailable. That’s because, all non-system
DLLs are not provided to the program by default. That includes stuff like Lua’s
or SDL2’s DLL files. They are stored in the path /mingw64/bin/
. Now, you
COULD go ahead and manually copy them, but why do that when that task can be
automated?
MinGW64 provides a version of the ldd
program, which spits out all the
dynamically linked libraries required by a program. Its output may look
something like this:
$ ldd my-cool-game.exe
ntdll.dll => /c/Windows/SYSTEM32/ntdll.dll (0x7ffa09e30000)
KERNEL32.DLL => /c/Windows/System32/KERNEL32.DLL (0x7ffa09d30000)
KERNELBASE.dll => /c/Windows/System32/KERNELBASE.dll (0x7ffa075f0000)
msvcrt.dll => /c/Windows/System32/msvcrt.dll (0x7ffa08c00000)
SHELL32.dll => /c/Windows/System32/SHELL32.dll (0x7ffa08cb0000)
msvcp_win.dll => /c/Windows/System32/msvcp_win.dll (0x7ffa07550000)
ucrtbase.dll => /c/Windows/System32/ucrtbase.dll (0x7ffa07d90000)
USER32.dll => /c/Windows/System32/USER32.dll (0x7ffa09690000)
win32u.dll => /c/Windows/System32/win32u.dll (0x7ffa07d60000)
GDI32.dll => /c/Windows/System32/GDI32.dll (0x7ffa08a50000)
gdi32full.dll => /c/Windows/System32/gdi32full.dll (0x7ffa078c0000)
SDL2_mixer.dll => /mingw64/bin/SDL2_mixer.dll (0x7ff9f7fd0000)
lua54.dll => /mingw64/bin/lua54.dll (0x7ff9f0e80000)
libwinpthread-1.dll => /mingw64/bin/libwinpthread-1.dll (0x7ff9f7210000)
SDL2.dll => /mingw64/bin/SDL2.dll (0x7ff9e39f0000)
(...)
Most referenced DLLs reside in Window’s System32 folder. They are provided by
the system and will always be there. So you don’t have to worry about providing
them. The DLLs in /mingw64/bin/...
however need to be placed alongside your
.exe file if people should be able to run your game via double-clicking it.
Cool, so that’s where our automation can begin! First step is super simple:
let’s filter out the non-system DLLs using grep
:
ldd my-cool-program.exe | grep /mingw64
Might not be super robust, but I highly doubt that the string “/mingw64” will ever show up in a Windows system DLL’s path.
Next, we only care about the DLL’s path. So we use awk
to cut out that
portion of the lines. If we regard the spaces as delimiting characters of text
columns, the full path is in the third column (The first is just the filename,
and the second is “=>”). The awk
command awk {print $3}
gives us this third
column, so we can just append it to our shell command:
ldd my-cool-program.exe | grep /mingw64 | awk '{print $3}'
By now, our shell command spits our a list of full paths of all non-system DLLs
dynamically linked to our program. Cool! But we want to automate the whole
thing, so lets add a final call to xargs
, to copy all files in this list into
our current directory. Here, I’ll use xargs -i cp {} .
. The -i cp {} .
parameter means that, for every file in the list, we call cp
, pass the DLL’s
path as the first parameter and the target directory .
as the second
parameter.
Here is the final call:
ldd my-cool-program.exe | grep /mingw64 | awk '{print $3}' | xargs -i cp {} .
Cool! Now go put this in your CI script and automate packaging your Windows releases. Or, if you’re like me, make exactly one release and wonder why you spent so much time on figuring this out when you could have just collected the files manually once and then forget about it ARGH