As a follow-up to the previous post about compiling dwm with Zig1, this post explores using the Zig build system to build a C project.
In this case dwm.
Let's see how we can replace Makefile
and config.mk
2 with a build.zig
file.
Step 1. Build the binary with build.zig #
As the first step, let's replace the dwm
Makefile target, which compiles the binary, with a build.zig
file.
Following the Zig documentation and guides, this is pretty straight forward.
We start by simply replacing the following parts of Makefile
:
1# Makefile:
2
3SRC = drw.c dwm.c util.c
4OBJ = ${SRC:.c=.o}
5
6.c.o:
7 ${CC} -c ${CFLAGS} $<
8
9${OBJ}: config.h config.mk
10
11dwm: ${OBJ}
12 ${CC} -o $@ ${OBJ} ${LDFLAGS}
With this build.zig
file:
1const std = @import("std");
2
3const src = .{
4 "drw.c",
5 "dwm.c",
6 "util.c",
7};
8
9pub fn build(b: *std.Build) void {
10 const target = b.standardTargetOptions(.{});
11 const optimize = b.standardOptimizeOption(.{});
12
13 const dwm = b.addExecutable(.{
14 .name = "dwm",
15 .target = target,
16 .optimize = optimize,
17 .link_libc = true,
18 });
19
20 dwm.addCSourceFiles(.{
21 .files = &src,
22 });
23
24 b.installArtifact(dwm);
25}
Above, we added an executable to build.zig
without specifying a root source file.
Then, we added the C source files and finally added the executable as an install artifact.
This makes zig build
install the binary to [prefix]/bin.
Let's run the build:
1$ zig build
2zig build-exe dwm Debug native: error: the following command failed with 2
3compilation errors:
4zig build-exe
5[dwm]/drw.c [dwm]/dwm.c
6[dwm]/util.c -lc --cache-dir
7[dwm]/zig-cache --global-cache-dir [~]/.cache/zig
8--name dwm --listen=-
9Build Summary: 0/3 steps succeeded; 1 failed (disable with --summary none)
10install transitive failure
11└─ install dwm transitive failure
12 └─ zig build-exe dwm Debug native 2 errors
13usr/include/X11/Xft/Xft.h:39:10: error: 'ft2build.h' file not found
14include <ft2build.h>
15 ^~~~~~~~~~~~~
16[dwm]/drw.c:6:10: note: in file included from
17[dwm]/drw.c:6:
18#include <X11/Xft/Xft.h>
19 ^
20/usr/include/X11/Xft/Xft.h:39:10: error: 'ft2build.h' file
21not found
22#include <ft2build.h>
23 ^~~~~~~~~~~~~
24[dwm]/dwm.c:42:10: note: in
25file included from
26[dwm]/dwm.c:42:
27#include <X11/Xft/Xft.h>
28 ^
OK, so were missing some libraries.
These are specified in config.mk
along with C compiler and linker flags:
1# config.mk:
2VERSION = 6.4
3
4X11INC = /usr/X11R6/include
5X11LIB = /usr/X11R6/lib
6
7# Xinerama, comment if you don't want it
8XINERAMALIBS = -lXinerama
9XINERAMAFLAGS = -DXINERAMA
10
11# freetype
12FREETYPELIBS = -lfontconfig -lXft
13FREETYPEINC = /usr/include/freetype2
14
15# includes and libs
16INCS = -I${X11INC} -I${FREETYPEINC}
17LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS}
18
19# flags
20CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS}
21CFLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS}
22LDFLAGS = ${LIBS}
Let's link the system libraries in build.zig
:
1@@ -17,6 +17,16 @@ pub fn build(b: *std.Build) void {
2 .link_libc = true,
3 });
4
5+ dwm.linkSystemLibrary("X11");
6+
7+ // Xinerama, comment if you don't want it
8+ dwm.linkSystemLibrary("Xinerama");
9+ dwm.defineCMacro("XINERAMA", "1");
10+
11+ // freetype
12+ dwm.linkSystemLibrary("fontconfig");
13+ dwm.linkSystemLibrary("Xft");
14+
15 dwm.addCSourceFiles(.{
16 .files = &src,
17 });
And let's add the C defines and flags:
1@@ -1,5 +1,7 @@
2 const std = @import("std");
3
4+const version = "6.4";
5+
6 const src = .{
7 "drw.c",
8 "dwm.c",
9@@ -27,8 +29,21 @@ pub fn build(b: *std.Build) void {
10 dwm.linkSystemLibrary("fontconfig");
11 dwm.linkSystemLibrary("Xft");
12
13+ // C defines
14+ dwm.defineCMacro("_DEFAULT_SOURCE", "1");
15+ dwm.defineCMacro("_BSD_SOURCE", "1");
16+ dwm.defineCMacro("_POSIX_C_SOURCE", "200809L");
17+ dwm.defineCMacro("VERSION", "\"" ++ version ++ "\"");
18+
19 dwm.addCSourceFiles(.{
20 .files = &src,
21+ .flags = &.{
22+ "-std=c99",
23+ "-pedantic",
24+ "-Wall",
25+ "-Wno-deprecated-declarations",
26+ "-Os",
27+ },
28 });
29
30 b.installArtifact(dwm);
Great! Now we are able to build the binary, and it gets installed to [prefix]/bin:
1$ zig build
2$ tree zig-out
3zig-out
4└── bin
5 └── dwm
6
71 directory, 1 file
Step 2. Complete install target with binary and manual page #
Next, let's look at the install target in Makefile
1# config.mk
2PREFIX = /usr/local
3MANPREFIX = ${PREFIX}/share/man
4
5# Makefile
6all: dwm
7
8install: all
9 mkdir -p ${DESTDIR}${PREFIX}/bin
10 cp -f dwm ${DESTDIR}${PREFIX}/bin
11 chmod 755 ${DESTDIR}${PREFIX}/bin/dwm
12 mkdir -p ${DESTDIR}${MANPREFIX}/man1
13 sed "s/VERSION/${VERSION}/g" < dwm.1 > ${DESTDIR}${MANPREFIX}/man1/dwm.1
14 chmod 644 ${DESTDIR}${MANPREFIX}/man1/dwm.1
This step installs the dwm
binary into ${PREFIX}/bin
and the dwm.1
man page into ${PREFIX}/share/man/man1/dwm.1
.
The zig build
command has a --prefix
(default: zig-out
) option that can be used to set the install prefix path, so we don't need to handle the prefix within build.zig
.
But we do need to handle the sed
command to replace VERSION in dwm.1
:
1@@ -1,6 +1,7 @@
2 const std = @import("std");
3
4 const version = "6.4";
5+const man_prefix = "share/man";
6
7 const src = .{
8 "drw.c",
9@@ -47,4 +48,13 @@ pub fn build(b: *std.Build) void {
10 });
11
12 b.installArtifact(dwm);
13+
14+ // Run sed command to replace VERSION in man page
15+ const sed_cmd = b.addSystemCommand(&.{ "sed", "s/VERSION/" ++ version ++ "/g" });
16+ sed_cmd.addFileArg(.{ .path = "dwm.1" });
17+ const sed_output = sed_cmd.captureStdOut();
18+
19+ // Install the man page
20+ const man_page = b.addInstallFileWithDir(sed_output, .prefix, man_prefix ++ "/man1/dwm.1");
21+ b.getInstallStep().dependOn(&man_page.step);
22 }
We introduce the man_prefix
constant, run the sed
command on the dwm.1
source file, and install the output as man1/dwm.1
in the man_prefix
directory.
zig build
installs now both the binary and the man page into the prefix dir:
1$ zig build
2$ tree zig-out
3zig-out
4├── bin
5│ └── dwm
6└── share
7 └── man
8 └── man1
9 └── dwm.1
10
114 directories, 2 files
Step 3. Build distribution tarball #
The dwm Makefile has a dist
target that builds a tarball for source distribution.
As the last step, let's add a dist
step to the build.zig
file that builds this tarball.
First we add the list of files to include in the tarball:
1@@ -9,6 +9,18 @@ const src = .{
2 "util.c",
3 };
4
5+const dist_files: []const []const u8 = &(.{
6+ "LICENSE",
7+ "build.zig",
8+ "README",
9+ "config.def.h",
10+ "dwm.1",
11+ "drw.h",
12+ "util.h",
13+ "dwm.png",
14+ "transient.c",
15+} ++ src);
16+
17 pub fn build(b: *std.Build) void {
18 const target = b.standardTargetOptions(.{});
19 const optimize = b.standardOptimizeOption(.{});
Next, we'll create the build step.
The files in the tarball are listed in a dwm-VERSION/
directory.
We use b.addWriteFiles()
to create a temporary directory and use b.pathJoin()
to copy in all distribution files into the dwm-VERSION
sub-dir of that temp dir.
Then we can run the tar
system command with the temp dir as the working directory and add that sub-dir as argument.
This produces the tarball, that we can add to the dist
step as a dependency.
Please note that we have to explicitly add the dist files as dependencies to the tar command. This instructs the build system to invalidate the build cache if a dist file has been changed.
1@@ -57,4 +69,27 @@ pub fn build(b: *std.Build) void {
2 // Install the man page
3 const man_page = b.addInstallFileWithDir(sed_output, .prefix, man_prefix ++ "/man1/dwm.1");
4 b.getInstallStep().dependOn(&man_page.step);
5+
6+ // Build step for distribution tarball
7+ const dist = b.step("dist", "Create distribution tarball");
8+ const dist_dir = "dwm-" ++ version;
9+ const tar_file = dist_dir ++ ".tar.gz";
10+
11+ const wf = b.addWriteFiles();
12+ for (dist_files[0..]) |file| {
13+ _ = wf.addCopyFile(.{ .cwd_relative = file }, b.pathJoin(&.{ dist_dir, file }));
14+ }
15+
16+ // Step for tar command
17+ const tar_cmd = b.addSystemCommand(&.{ "tar", "czf" });
18+ tar_cmd.setCwd(wf.getDirectory());
19+ const out_file = tar_cmd.addOutputFileArg(tar_file);
20+ tar_cmd.addArg(dist_dir);
21+
22+ // tar_cmd depends on dist files:
23+ tar_cmd.extra_file_dependencies = dist_files[0..];
24+
25+ // dist step installs dist_file
26+ const dist_file = b.addInstallFile(out_file, tar_file);
27+ dist.dependOn(&dist_file.step);
28 }
Let's try the distribution build:
1$ zig build dist
2$ tree zig-out
3zig-out
4└── dwm-6.4.tar.gz
5
60 directories, 1 file
7$ tar -tzf zig-out/dwm-6.4.tar.gz
8dwm-6.4/
9dwm-6.4/drw.h
10dwm-6.4/dwm.c
11dwm-6.4/build.zig
12dwm-6.4/LICENSE
13dwm-6.4/util.c
14dwm-6.4/transient.c
15dwm-6.4/util.h
16dwm-6.4/config.def.h
17dwm-6.4/drw.c
18dwm-6.4/dwm.1
19dwm-6.4/dwm.png
20dwm-6.4/README
Summary #
This was an experiment to replace Makefile
with build.zig
for a pure C project, inspired by Loris Cro3.
It took a while to figure out how to do this, mostly due to lacking documentation. Zig is a relatively young project and the standard library documentation, including the build system documentation, is still a work in progress.
For example, at the time of writing this, there is no documentation for linkSystemLibrary()
so at first I assumed that I needed to call addIncludePath()
for all libraries' headers, since config.mk
added the include paths for the library headers.
At some point I noticed that the build system tutorial4 doesn't call addIncludePath()
, so I tried removing these calls and the build still works, so apparently Zig handles that for you.
Another tricky part was running the tar
system command as a part of the build.
The build system tutorial was of great help to learn about how to write temporary files and how to call tar as a system command.
However, I had trouble with cache invalidation - the build would use the cached tarball even if source files were modified.
After some trial and error I found the extra_file_dependencies
property which solved this problem.
In the end, this was a successful experiment. We replaced dwm's build system with build.zig, and learned how Zig's build system works for C projects.
In this post, I used dwm version 6.4 (commit 50ad171e
) and Zig version 0.12.0-dev.1695+e4977f3e8
.