How to easily bundle your CGO application for Windows

Maxim Gradan
4 min readDec 4, 2021

--

To be honest, I don’t consider Windows a suitable platform for applications written in the Go programming language. It’s the language that was naturally born to be used under a POSIX-compatible OS and it’s really hard to build an application that depends on C libraries on Windows.

First steps

First it’s required to perform all the actions stated in this tutorial. That’s an initial setup sufficient to build an application that will only rely on the DLLs located at C:\Windows\System32. And yeah, games that use nothing but Pixel by faiface usually don’t need anything besides standard pre-installed dependencies. But what if there’s a need to use some third-party C libraries?

Let’s try to compile the example player from Reisen. It utilizes libav binaries to work. On Linux or Mac OS, we only need to install the required dependencies and then build and run the application. But it’s not that easy on Windows under MSYS2 MinGW 64-bit. First you’ll need to install the ffmpeg package:

pacman -S mingw-w64-x86_64-ffmpeg

After that we need to clone the repository and cd to it. The command to build it for Windows is:

go build -ldflags "-s -w -H=windowsgui"

The -ldflags option specifies flags for the linker (or ld). -s and -w options are required to strip the unneeded debug symbols from the executable and -H is required to suppress the launch of the command line when we start the application.

The example player binary can be launched from the MSYS2 command line using the next command:

./player.exe

And it will work just fine. But it’s not a good way for the user to launch an application executable file. Now let’s open the containing folder of the file in the Explorer and try to open the application by double-clicking. The following error emerges suddenly:

Well, applications that use Reisen rely on libav binaries such as libavformat, libavutil and so on. On Windows, they are presented as DLL files. According to the article, when you open an executable file that requires some dynamically linked libraries not listed in the system registry, the Windows operating system will search for them in the following locations:

  1. The directory of the executable file being called.
  2. The current working directory from which the executable was called.
  3. The %SystemRoot%\SYSTEM32 directory.
  4. The %SystemRoot% directory.
  5. The directories listed in the Path environment variable.

MSYS2 creates its own virtual environment with the substistuted Path to execute the applications built with MinGW so none of the libav binaries exist in the aforementioned locations. Instead, all of them are located in C:\msys64\mingw64\bin (if you didn’t change the original installation path of MSYS2). Now let’s just try to copy all the libav DLLs to the executable file directory. If we launch the application with the direct dependencies placed in the same folder, the following error will appear:

Now it asks for a different library not mentioned previously. Well, the thing is DLLs may depend on other DLLs, and this chain goes further forming a whole dependency tree. And there’s no way to figure out the transitive dependencies before the direct dependencies are satisfied.

ldd

ldd is a comand line utility that comes along with MSYS2 MinGW 64-bit. It can list all the DLLs required by the executable file whose name is provided as an argument. The tool will print both DLL names and locations:

Now let’s create a bundle.py script that will take the output of the ldd command and place all the required DLLs in the final distribution of the application. The only libraries we need to move are the ones located in C:\msys64\mingw64\bin.

import os, fileinput, shutil

bundleDirPath = os.path.abspath("bundle")
os.makedirs(bundleDirPath, exist_ok=True)

for dllInfo in fileinput.input():
dllInfo = dllInfo.strip()
dllInfoParts = dllInfo.split(sep=" ")
dllName = dllInfoParts[0]
dllPath = dllInfoParts[2]
dllBundlePath = os.path.join(bundleDirPath, dllName)

if dllPath.startswith("/mingw64/bin"):
dllPath = os.path.join("C:/msys64", dllPath[1:])
shutil.copyfile(dllPath, dllBundlePath)

shutil.copyfile("analyze.exe", "bundle/analyze.exe")

The command to create the final application distribution is:

ldd player.exe | python bundle.py

The bundle directory will contain all the binaries necessary to run the application.

Further considerations

The bundle directory now contains quite a lot of components, it’s pretty huge. Perhaps it would be better to somehow pre-install all the dependencies to some system folder where they can be easily found by Windows. But in my mind, not many users will find it appropriate to place some third-party DLLs in %SystemRoot%\SYSTEM32 and other critical places.

--

--

Maxim Gradan

I’m a Golang programmer. I like learning databases and have videogames and gamedev as hobbies.