Cross-Compiling and packaging C, Go and Zig projects with Nix

Part 2: Go

The Go source we want to compile is functionally equivalent to the C source.

Setting up the Build System

For Go, we’ll want a go.mod file that defines our project’s Go dependencies:

module github.com/flyx/Zicross/examples/go

go 1.18

require github.com/veandco/go-sdl2 v0.4.25

We’ll also need a corresponding go.sum, which can be created via go mod tidy. Now let’s look how the go-sdl2 wrapper links to SDL2:

//#cgo windows LDFLAGS: -lSDL2
//#cgo linux freebsd darwin openbsd pkg-config: sdl2
import "C"

pkg-config, how convenient – we’ve already set that up. Except for Windows, we’ll handle that later.

To compile natively, we only need to ensure that SDL2 is available via pkg-config.

Let’s write a flake.nix that compiles our code:

{
  inputs = {
    zicross.url = github:flyx/Zicross;
    nixpkgs.url = github:NixOS/nixpkgs/nixos-22.05;
    utils.url   = github:numtide/flake-utils;
  };
  outputs = {self, zicross, nixpkgs, utils}:
      with utils.lib; eachSystem allSystems (system: let
    pkgs = import nixpkgs {
      inherit system;
      overlays = [
        zicross.overlays.zig
        zicross.overlays.go
        zicross.overlays.debian
        zicross.overlays.windows
      ];
    };
    
    pname = "zicross_demo_go";
    version = "0.1.0";
    
    mySDL2 = if pkgs.stdenv.isDarwin then (pkgs.SDL2.override {
      x11Support = false;
    }) else pkgs.SDL2;
    postUnpack = ''
      mv "$sourceRoot" source
      sourceRoot=source
    '';
  in rec {
    packages = rec {
      demo = pkgs.buildGoModule {
        inherit pname version;
        src = ./.;
        subPackages = [ "zicross_demo_go" ];
        vendorSha256 = "5cfp25rEhmnLI/pQXE1+e6kjiYnb7T3nEuoLw2AfEoM=";
        nativeBuildInputs = [ pkgs.pkg-config ];
        buildInputs = with pkgs; [ mySDL2 ];
        targetSharePath="${placeholder "out"}/share";
        
        # workaround for buildGoModule not being able to take sources in a `go`
        # directory as input
        overrideModAttrs = (_: {
          inherit postUnpack;
        });
        inherit postUnpack;
        
        postConfigure = ''
          cat <<EOF >zicross_demo_go/generated.go
          package main
          
          const LogoPath = "$targetSharePath/logo.txt";
          EOF
        '';
        preInstall = ''
          mkdir -p $out/share
          cp ${zicross.lib.logo_data} $out/share/logo.txt
        '';
        meta = {
          maintainers = [ "Felix Krause <contact@flyx.org>" ];
          description = "Zicross Demo App (in Go)";
        };
      };
    };
  };
}

This is pretty similar to what we did for C. The additional overlay zicross.overlays.go overrides the standard pkgs.buildGoModule with a version that uses Zig as C compiler.

The mySDL2 package is a workaround for macOS. Since go-sdl2 links to much more parts of SDL2 than our C source did, we can run into an error since X11 is by default enabled for macOS in Nixpkgs. We create a modified SDL2 package that has X11 disabled to avoid this error.

The vendorSha256 is something Nix needs to ensure that the Go dependencies, specified in go.mod, are what it expects them to be. Initially, you can give pkgs.lib.fakeSha256 to get an error message that tells you what the actual checksum is, and then substitute that.

Don’t mind the postUnpack workaround, this mitigates a problem arising from the sources being located in the directory examples/go. buildGoModule dislikes the base directory being named go which is what the workaround fixes.

Now with this in place, we can compile our Go application natively and test it:

$ nix build .#demo
$ result/bin/zicross_demo_go

Cross-Compiling for Debian on Raspberry Pi

Like before, let’s add an additional package that builds a .deb file:

      rpiDeb = pkgs.packageForDebian (demo.overrideAttrs (origAttrs: {
        GOOS = "linux";
        GOARCH = "arm";
      })) {
        targetSystem = "armv7l-hf-multiplatform";
        pkgConfigPrefix = "/usr/lib/arm-linux-gnueabihf/pkgconfig";
        includeDirs = [ "/usr/include" "/usr/include/arm-linux-gnueabihf" ];
        name = "zicross-demo-go";
        inherit version;
        deps = {
          sdl2 = {
            path = "debian/pool/main/libs/libsdl2/libsdl2-2.0-0_2.0.14+dfsg2-3_armhf.deb";
            sha256 = "1z3bcjx225gp6lcbcd7h15cvhjik089y5pgivl2v3kfp61zm9wv4";
            dev = {
              path = "debian/pool/main/libs/libsdl2/libsdl2-dev_2.0.14+dfsg2-3_armhf.deb";
              sha256 = "17d8qms1p7961kl0g7hgmkn0qx9avjnxwlmsvx677z5xb8vchl3y";
            };
            packageName = "libsdl2-2.0-0";
            minVersion = "2.0.0";
          };
          libcrypt = {
            path = "debian/pool/main/libx/libxcrypt/libcrypt1_4.4.18-4_armhf.deb";
            sha256 = "0mcr0s5dwcj8rlr70sf6n3271pg7h73xk6zb8r7xvhp2fm51fyri";
            packageName = "libcrypt1";
            minVersion = "1:4.4.18";
          };
          libx11-dev = {
            path = "debian/pool/main/libx/libx11/libx11-dev_1.7.2-1_armhf.deb";
            sha256 = "0n0r21z7lp582pk51fp8dwaymz3jz54nb26xmfwls7q4xbj5f7wz";
          };
          x11proto-dev = {
            path = "debian/pool/main/x/xorgproto/x11proto-dev_2020.1-1_all.deb";
            sha256 = "1xb5ll2fg3as128m5vi6w5kwbcyc732hljy16i66dllsgmc8smnm";
          };
        };
      };

We’re using packageForDebian just like we did for C. As we know, this configures our zig cc compiler. However we also need to configure the Go compiler for cross-compiling, and we do that via overrideAttrs – we need to set GOOS and GOARCH to the correct values for the target system.

Since go-sdl2 includes significantly more SDL2 headers than our C code did, we need to add additional dependencies that provide header files that are included by some of the SDL2 headers – these are transitive dependencies of libsdl2-dev. They don’t have a packageName since we don’t want to add them as dependencies of the created package.

Everything else looks similar to what we did for C. Now let’s test it:

$ nix build .#rpiDeb
$ dpkg -I
 new Debian package, version 2.0.
 size 470990 bytes: control archive=281 bytes.
     235 bytes,     9 lines      control              
 Package: zicross-demo-go
 Version: 0.1.0
 Section: base
 Priority: optional
 Architecture: armhf
 Depends: libcrypt1 (>= 1:4.4.18), libsdl2-2.0-0 (>= 2.0.0)
 Maintainer: Felix Krause <contact@flyx.org>
 Description: Zicross Demo App (in Go)
 

Looks good.

Cross-Compiling for x86_64 Windows

Remember how go-sdl2 doesn’t use pkg-config when compiling for Windows? There are two ways we can remedy this:

We will go for the latter option since it is less intrusive. Here’s our package:

      win64Zip = pkgs.packageForWindows (demo.overrideAttrs (origAttrs: {
        GOOS = "windows";
        GOARCH = "amd64";
        postConfigure = origAttrs.postConfigure + ''
          export CGO_LDFLAGS="$CGO_LDFLAGS $(pkg-config --libs sdl2)"
        '';
      })) {
        targetSystem = "x86_64-windows";
        deps = {
          sdl2 = {
            tail = "SDL2-2.0.22-1-any.pkg.tar.zst";
            sha256 = "13v4wavbxzdnmg6b7qrv7031dmdbd1rn6wnsk9yn4kgs110gkk90";
            postPatch = ''
              ${pkgs.gnused}/bin/sed -i "s:-lSDL2main:$out/clang64/lib/libSDL2main.a:g" upstream/clang64/lib/pkgconfig/sdl2.pc
            '';
          };
          iconv = {
            tail = "libiconv-1.16-2-any.pkg.tar.zst";
            sha256 = "0kwc5f60irrd5ayjr0f103f7qzll9wghcs9kw1v17rj5pax70bxf";
          };
          vulkan = {
            tail = "vulkan-loader-1.3.211-1-any.pkg.tar.zst";
            sha256 = "0n9wnrcclvxj7ay14ia679s2gcj5jyjgpg53j51yfdn48wlqi40l";
          };
          libcpp = {
            tail = "libc++-14.0.3-1-any.pkg.tar.zst";
            sha256 = "1r73zs9naislzzjn7mr3m8s6pikgg3y4mv550hg09gcsjc719kzz";
          };
          unwind = {
            tail = "libunwind-14.0.3-1-any.pkg.tar.zst";
            sha256 = "1lxb0qgnl9fbdmkmj53zjg8i9q5hv0pa83bkmraf2raflpm2yrs5";
          };
        };
      };
    };

As before, we set GOOS and GOARCH appropriately. As discussed, we add the pkg-config libs in postConfigure. The libraries we link are the same as for C.

Let’s build and check the result:

$ nix build .#win64Zip
$ unzip -Z1 result
zicross_demo_go-0.1.0-win64/
zicross_demo_go-0.1.0-win64/bin/
zicross_demo_go-0.1.0-win64/bin/libcharset-1.dll
zicross_demo_go-0.1.0-win64/bin/libc++.dll
zicross_demo_go-0.1.0-win64/bin/libiconv-2.dll
zicross_demo_go-0.1.0-win64/bin/libvulkan-1.dll
zicross_demo_go-0.1.0-win64/bin/SDL2.dll
zicross_demo_go-0.1.0-win64/bin/libunwind.dll
zicross_demo_go-0.1.0-win64/bin/zicross_demo_go.exe
zicross_demo_go-0.1.0-win64/share/
zicross_demo_go-0.1.0-win64/share/logo.txt

Conclusion

The pkg-config setup for C works quite well for Go. The only additional thing needed is setting up the Go compiler for cross-compiling. There are some quirks, but generally it works quite well.