Part 3: Zig
Once again, our Zig code available at GitHub is functionally the same as the C code.
Setting up the Build System
Zig provides a native build system that uses a build.zig
file.
In that file, you basically assemble a directed graph with nodes being build steps and edges being dependencies.
This build system currently does not provide an official package manager.
But Zicross is built upon Nix, which we can use as package manager.
To facilitate this, Zicross provides a function buildZig
.
It uses Nix, instead of Zig, to describe the build steps, and generates a build.zig
file from them.
Here’s how we use it:
{
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.debian
zicross.overlays.windows
];
};
zig-sdl = pkgs.fetchFromGitHub {
owner = "MasterQ32";
repo = "SDL.zig";
rev = "bf72bbef8c1c113b2862ff2fab33b1fedbf159f6";
sha256 = "9M1cBs4hY4cFp6woqYofyzgCVogAotVKp6n+Hla3w48=";
};
zigPackages = let
build_options = {
name = "build_options";
src = ./.;
main = "zig-sdl-build-options.zig";
dependencies = [];
};
sdl-native = {
name = "sdl-native";
src = zig-sdl;
main = "src/binding/sdl.zig";
dependencies = [ build_options ];
};
sdl2 = {
name = "sdl2";
src = zig-sdl;
main = "src/wrapper/sdl.zig";
dependencies = [ sdl-native ];
};
in [ sdl2 ];
in rec {
packages = rec {
demo = pkgs.buildZig {
buildInputs = [ pkgs.SDL2 pkgs.libiconv ];
pname = "zicross_demo_zig";
version = "0.1.0";
src = ./.;
zigExecutables = [
{
name = "zicross_demo_zig";
file = "main.zig";
dependencies = zigPackages;
install = true;
}
];
zigTests = [
{
name = "loadTest";
description = "tests loading the logo";
file = "main.zig";
src = ./.;
dependencies = zigPackages;
}
];
postConfigure = ''
cat <<EOF >resources.zig
pub const data = "$targetSharePath/logo.txt";
EOF
'';
# use upstream logo file for testing
preCheck = ''
cat <<EOF >resources.zig
pub const data = "${zicross.lib.logo_data}";
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";
};
};
};
};
}
First, we fetch zig-sdl
from GitHub.
This shows how Nix can depend on a library even if it is not explicitly set up to be consumed by Nix – other unofficial Zig package managers require some information in the repository.
This SDL2 wrapper does have an SDK which is designed to be consumed by build.zig
.
This currently does not play nice with buildZig
so we supply a file zig-sdl-build-options.zig
that would be generated by the SDK.
Then, we set up the wrapper package named sdl2
, depending on the native API in sdl-native
and build_options
which links to our file.
Our buildInputs
will create linkSystemLibrary
calls in the build.zig
we generate.
linkSystemLibrary
calls, you guessed it, call pkg-config
.
In zigExecutables
, we give our main file and set it up to be installed.
zigTests
sets up the tests we want to run.
In preCheck
, we change resources.zig
to point to the upstream path of the logo, instead of the copy we put into the share
directory when installing.
This allows us to run the tests without building the output directory tree.
Let’s build and test our application:
$ nix build .#demo
$ result/bin/zicross_demo_zig
If we want to have a build.zig
file so that we can call zig
on it directly, we can do
$ nix develop
$ eval "$configurePhase"
This gives us a build.zig
file that can be used with the zig
command.
Cross-Compiling for Debian on Raspberry Pi
There is nothing happening here that has not been discussed before:
rpiDeb = pkgs.packageForDebian demo {
targetSystem = "armv7l-hf-multiplatform";
pkgConfigPrefix = "/usr/lib/arm-linux-gnueabihf/pkgconfig";
name = "zicross-demo-zig";
version = "0.1.0";
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";
};
};
Test it:
$ nix build .#rpiDeb
$ dpkg -I result
new Debian package, version 2.0.
size 198128 bytes: control archive=276 bytes.
228 bytes, 9 lines control
Package: zicross-demo-zig
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
Cross-Compiling for x86_64 Windows
We’re removing -lSDL2main
from sdl2.pc
because it does not work well with Zig.
Otherwise, it’s just what we have seen before:
win64Zip = pkgs.packageForWindows demo {
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//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";
};
};
};
Build and test:
$ nix build .#win64Zip
$ unzip -Z1 result
zicross_demo_zig-0.1.0-win64/
zicross_demo_zig-0.1.0-win64/bin/
zicross_demo_zig-0.1.0-win64/bin/libcharset-1.dll
zicross_demo_zig-0.1.0-win64/bin/libc++.dll
zicross_demo_zig-0.1.0-win64/bin/libiconv-2.dll
zicross_demo_zig-0.1.0-win64/bin/libvulkan-1.dll
zicross_demo_zig-0.1.0-win64/bin/SDL2.dll
zicross_demo_zig-0.1.0-win64/bin/zicross_demo_zig.pdb
zicross_demo_zig-0.1.0-win64/bin/libunwind.dll
zicross_demo_zig-0.1.0-win64/bin/zicross_demo_zig.exe
zicross_demo_zig-0.1.0-win64/share/
zicross_demo_zig-0.1.0-win64/share/logo.txt
Conclusion
With Zig at the heart of Zicross, it is not surprising that cross-compiling boils down to „specify the target system and the dependencies“.
There is no requirement to use Zicross’es buildZig
function; you can instead have a build.zig
and setup PKG_CONFIG_PATH
just like we did for C.
Whether you prefer to write your building steps in Zig or Nix is probably personal preference. Having the build script written in the implementation language does have its appeal. On the other hand, Zicross allows you to pin a certain compiler version for your project, which may be helpful since Zig is not stable yet and projects tend to track its master branch. Also you can depend on any publicly available Zig libraries without worrying about whether they support the package manager you use.