Building Tools

How To Build Go & C Code for Windows using Github Actions

A few weeks ago, a friend of mine needed a small program that accepts data in a CSV format and produces some reports. The code itself was very simple – I’ve chosen Go because I wanted to produce an executable that could run on any machine. No runtime dependencies are required.
The fun part started when he asked to generate the PDF reports. I’ve found a good C library, wkhtmltopdf, and a Go bindings library. I got it working and was able to produce PDF files on my Mac machines pretty fast. The problem: My friend needed a Windows binary! How can I set up a CI/CD pipeline to build the executable with support for the native C library? This is the story I am planning to tell today. If you’re just interested in the TL;DR, you can just check the workflow file on Github.
So, without further ado, let’s begin!

Building Go Native Module

The Go library is just bindings for the native C code. Go has pretty good support for calling C libraries from Go. But, in order to compile my code, I must have wkhtmltopdf library installed on my machine. For Mac, it’s fairly simple – a brew formulae exist, so after I’ve installed it the code compiled without any issues. But what about the CI?
I’ve already had a CI that generates Windows executable. One of the cool things about Go is the ability to generate an executable for any operating system and any architecture, no matter which machine you have. So I just used a regular Linux runner in Github Actions and set the required operating system and architecture:
GOOS=windows GOARCH=amd64 go build
While this worked flawlessly, to support native C modules I had to do something different…

Hello Windows Server

So to build native modules we are going to use a Windows machine to compile and produce Windows executable. Github actions make it really simple to run code on a Windows machine – this is all that is required:
runs-on: windows-latest
And from now, the build will run on a Windows server machine.
Ok, we have the right machine. What now? To build the code, we need to install the native wkhtmltopdf library. If you, like me, use mainly Linux / Mac in your day-to-day life, this is pretty confusing: How do I install stuff? Where is my apt / apk / brew ??
It took me some Googling but I was able to find a solution – Chocolatey, the (yummier) package manager for Windows. Installing wkhtmltopdf is as simple as running (with some flags for cleaner output):
choco install --limit-output --no-progress wkhtmltopdf
Easy, right? Now, all we need is to actually build the code!

It’s build time!

The Go binding library has instructions for installing on Windows. It is not enough to install the library – we need to tell the Go compiler where we installed it by setting the relevant environment variables:
set CGO_LDFLAGS=-L{your-sdk-path}\bin
set CGO_CFLAGS=-I{your-sdk-path}\include
Chocolatey install the library to C:\Program Files\wkhtmltopdf, so I’ve tried to set those environment variables and build my code. And of course, it failed:
Error: C:\Users\runneradmin\go\pkg\mod\github.com\adrg\[email protected]\converter.go:7:10: fatal error: wkhtmltox/pdf.h: No such file or directory
 #include <wkhtmltox/pdf.h>
          ^~~~~~~~~~~~~~~~~
Why Meme
Why????
The error is clear – the compiler did not find the native library. But why? I did everything correctly… It took me a while to figure it out, but at some point, I’ve noticed this line at the beginning of the log:
  shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"
And this is when it hit me: This is not a regular command prompt, this is PowerShell! PowerShell is the new(ish) shiny successor of the command prompt, and it does not always behave the same.

PowerShell??

Setting environment variables PowerShell has a different syntax (see the docs for more details):
Set-Item -Path Env:CGO_LDFLAGS -Value "-LC:\Program Files\wkhtmltopdf\bin"
Set-Item -Path Env:CGO_CFLAGS -Value "C:\Program Files\wkhtmltopdfIC:\wkhtmltopdf\include"
If we try to run it, this time we will find a new and different error (which is always better!):
# runtime/cgo
gcc.exe: error: Files\wkhtmltopdf\include: No such file or directory
# runtime/cgo
gcc.exe: error: Files\wkhtmltopdf\include: No such file or directory
So the compile now has the right environment variables, but it is looking in the wrong place. What happened now?
The Go compiler does not support folders with spaces – which is a known bug, as I’ve found out the hard way. I couldn’t find how to change the installation folder using Chocolatey, so I’ve decided to solve it by creating a symbolic link. This is fairly trivial with PowerShell:
New-Item -ItemType Junction -Path "C:\wkhtmltopdf" -Target "C:\Program Files\wkhtmltopdf"
All I need to do now is update the compiler environment variables to point to the new folder, and this time it will compile!

Wrapping Up

This was a fun journey, at least for me. I had to deal with some non-trivial surprises in the way, and I couldn’t find good documentation showing the way. So I’ve decided to share my journey – in the hope that if you had to do something similar, it will be easier for you.
Happy Building! If you did find my post helpful, do tell – what worked for you? What didn’t?

Leave a Reply

Your email address will not be published.