Zig cross-compilation: linking to system libraries

  • Date: October 7, 2023

Recently I was trying to cross compile a Zig executable that links to libmariadb (or any system library). I could not find any guides or documentation after much searching, hope this will be useful for the next person.

Goal

The goal is simple:

  • I’m running on a computer with ARM64 (AArch64) CPUs.
  • I have a Zig program linking to the system librarylibmariadb
  • I want to build a binary that can run on AMD64 CPUs
  • I want to build the binary from a host that is native to my CPU (ARM64) to avoid emulation—it be slow!

Steps

Separately, I’m doing all this in Docker, since my actual machine is running MacOS. To do this, I’m using a Debian-based Docker image, since it has multi-architecture support, which allows installing system libraries for a different CPU architecture.

0. Configure Debian for multi-architecture

FROM --platform=$BUILDPLATFORM debian:bullseye-slim AS builder
RUN dpkg --add-architecture amd64

1. Install packages

In this step, we install packages that are needed for development, and also any system library that your Zig code is targeting:

RUN apt-get update && apt-get install -y \
pkg-config \
build-essential \
wget \
libmariadb-dev:amd64 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

Note I’m specifying specifically the CPU architecture in libmariadb-dev:amd64. This is necessary. Otherwise the default host architecture will be used.

2. Install Zig

In this step, we’ll be installing Zig with the architecture of the host (AArch64).

RUN wget -qO- https://ziglang.org/download/0.11.0/zig-linux-aarch64-0.11.0.tar.xz | tar -xJ -C /usr/local/bin
ENV PATH="${PATH}:/usr/local/bin/zig-linux-aarch64-0.11.0"

3. Configure pkg-config

pkg-config is the utility that build systems like Zig can use to find the include paths, library paths, etc. of their dependencies. When you install a package, pkg-config stores a .pc file in /usr/lib/pkgconfig. However, in a multi-architecture environment, .pc files are in /usr/lib/<architecture>/pkgconfig. We have to set the PKG_CONFIG_PATH environment variable to this path.

RUN echo $PKG_CONFIG_PATH
ENV PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig
# This command should print the include path and library path for libmariadb
RUN pkg-config --cflags --libs mariadb

4. Build the Zig binary

In build.zig, add these two lines:

exe.linkLibC();
exe.linkSystemLibrary("mariadb");

Then in the dockerfile:

WORKDIR /build
COPY ./build.zig .
COPY ./src ./src
RUN --mount=type=cache,target=/build/zig-cache \
zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-linux-gnu

That’s it, we have built the binary, and it’s targeting a different architecture!

5. Copy the binary to the destination

Using Docker’s multi-stage build, we can now just copy the binary to a different image. This will allow us to keep the final image small, containing just our binary and the system libraries needed—without all the compilers and such. Notice the --platform flag is set to the destination platform we’re targeting.

FROM --platform=linux/amd64 debian:bullseye-slim
RUN apt-get update && apt-get install -y libmariadb-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /build/zig-out/bin/myapp .
CMD ["/app/myapp"]