janw.name Personal Blog and Portfolio of Jan Wolff

Collecting the DLLs Required by an MSYS2 Binary (21. April 2022)

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