On Zig as a C++ Build System

Zig sparks absolutely no interest in me as a language, but it does seem like it could be a compelling build tool for existing C and C++ projects due to its integrations with clang and musl that makes it easy to cross compile and even set specific versions of libraries like glibc. I have little experience with the language, but I wanted to see whether I could build a C++ project with dependencies successfully, meaning all the necessary libraries would be linked and it would conveniently output header files in a reasonable location for any editor to find (I use Neovim btw.).


The Project


The project I wanted to build is a small C++ game which depends on raylib, raylib-cpp, and googletest. Raylib is basically an abstraction layer over different platform graphics systems, with some convenient APIs for 2D and 3D game development. It’s written in C. The raylib-cpp dependency is a header-only C++ library which provides an object-oriented wrapper around the core raylib APIs. GoogleTest is a C++ xUnit testing framework, and building it will be an interesting challenge since it’s not a header-only library.


The structure of the project is pretty straightforward.

(root)
├── build.zig
├── build.zig.zon
├── include
│   └── game.h
├── main.cc
├── src
│   └── game.cc
└── test
└── main_test.cc

Add the following code to their respective files. It just sets up a game class with a run method, as well as a single test.

main.cc
1
#include <raylib-cpp.hpp>
2
3
#include "game.h"
4
5
int main(int argc, char** argv) {
6
using namespace game;
7
8
Game game(800, 450, "game", 60);
9
game.Run();
10
11
return 0;
12
}
include/game.h
1
#pragma once
2
3
#include <filesystem>
4
#include <raylib-cpp.hpp>
5
6
namespace game {
7
8
namespace fs = std::filesystem;
9
10
class Game {
11
public:
12
Game(unsigned int screen_width, unsigned int screen_height,
13
const std::string& title, unsigned int target_fps) noexcept;
14
~Game() {}
15
16
void Run();
17
18
private:
19
static inline fs::path resource_path_ = fs::current_path().parent_path().append("resources");
20
unsigned int level_ = 1;
21
unsigned int target_fps_ = 60;
22
unsigned int screen_width_;
23
unsigned int screen_height_;
24
std::string title_ = "Game";
25
raylib::Window window_;
26
};
27
28
} // namespace game
src/game.cc
1
#include "game.h"
2
3
namespace game {
4
5
Game::Game(unsigned int screen_width, unsigned int screen_height, const std::string& title, unsigned int target_fps) noexcept : window_{raylib::Window(screen_width, screen_height, title)} {
6
window_.SetTargetFPS(target_fps);
7
}
8
9
void Game::Run() {
10
while (!window_.ShouldClose()) {
11
window_.BeginDrawing();
12
13
window_.ClearBackground(raylib::Color::RayWhite());
14
15
raylib::DrawText("Congrats! You created your first window!", 190, 200, 20, raylib::Color::Black());
16
17
window_.EndDrawing();
18
}
19
}
20
21
} // namespace game
test/main_test.cc
1
#include <gmock/gmock.h>
2
#include <gtest/gtest.h>
3
4
// Demonstrate some basic assertions.
5
TEST(MainTest, BasicAssertions) {
6
// Expect two strings not to be equal.
7
EXPECT_STRNE("hello", "world");
8
// Expect equality.
9
EXPECT_EQ(7 * 6, 42);
10
}

Now for the fun stuff.


Specifying Our Dependencies


Zig is not stable. APIs are subject to breakage with each release, and the tooling, while impressive for a pre-1.0 language, has many rough edges. An example of this is the lack of an ergonomic package manager. Coming from the Rust and Go worlds, build.zig.zon is far cry from cargo add or go get, but it is all we have to work with. The build.zig.zon file specifies dependencies in Zig Object Notation, which looks pretty similar to JSON (with more punctuation). The possible top level keys are specified in the documentation (there aren’t very many). We can start off our build.zig.zon with the following:

build.zig.zon
1
.{
2
.name = "game",
3
.version = "0.0.1",
4
.dependencies = .{},
5
6
.paths = .{"build.zig"},
7
}

Each dependency is a struct which may include the following keys:

  • url
  • hash
  • path

You must either provide a url and a hash, or a path. We will be using some C and C++ libraries from GitHub, and won’t have any actual zig code in our project at all (although there are zig bindings for raylib)1. The url of a GitHub dependency in build.zig.zon is not the GitHub URL for the project, but rather a link to the zipped (.tar.gz, zip) project. To retrieve this for a branch, go where you normally would to clone the project and copy the “Download ZIP” link. You can also do this for tags by copying the tar.gz or zip URL for the tag. This sounds confusing, but really isn’t difficult in practice. Let’s add raylib as a dependency.

build.zig.zon
1
.{
2
.name = "game",
3
.version = "0.0.1",
4
.dependencies = .{
5
.raylib = .{
6
.url = "https://github.com/raysan5/raylib/archive/refs/heads/master.zip",
7
},
8
},
9
10
.paths = .{"build.zig"},
11
}

We also need to specify a hash. To get this hash, you can run zig fetch <url>. Update your build.zig.zon with the hash printed to the console like so:

build.zig.zon
1
.{
2
.name = "game",
3
.version = "0.0.1",
4
.dependencies = .{
5
.raylib = .{
6
.url = "https://github.com/raysan5/raylib/archive/refs/heads/master.zip",
7
.hash = "1220761aef32a253042bf79d5acc4c2013853f4db5a9a9f68cd341ee4ddb9099803a", // master
8
},
9
},
10
11
.paths = .{"build.zig"},
12
}

It’s important to note that since we’re tracking raylib’s master branch, we will need to update the hash when new commits are made. Raylib’s build.zig in its last release (5.0.0) doesn’t work with the most recent version of zig so we have to track master. If this is an issue, you can fork raylib and point your .url at the forked version of master. It also means you shouldn’t just copy this hash if you’re following along with me–generate your own. It’ll probably be different.


The second dependency we need for our project is the C++ bindings for raylib. We can add another dependency to our project like so:

build.zig.zon
1
.{
2
.name = "game",
3
.version = "0.0.1",
4
.dependencies = .{
5
.raylib = .{
6
.url = "https://github.com/raysan5/raylib/archive/refs/heads/master.zip",
7
.hash = "1220761aef32a253042bf79d5acc4c2013853f4db5a9a9f68cd341ee4ddb9099803a", // master
8
},
9
.@"raylib-cpp" = .{
10
.url = "https://github.com/RobLoach/raylib-cpp/archive/refs/tags/v5.0.2.tar.gz",
11
.hash = "12200de71a671fc3b2155475bc6691c51ac1ce81f4910fb0db9cad579a1047877108",
12
},
13
},
14
15
.paths = .{"build.zig"},
16
}

This dependency is pinned to the 5.0.2 (most recent as of September 13, 2024) tag on the repository. If you need a newer version, just zig fetch <url> for the version you want.


The third dependency we need is GoogleTest.

build.zig.zon
1
.{
2
.name = "game",
3
.version = "0.0.1",
4
.dependencies = .{
5
.raylib = .{
6
.url = "https://github.com/raysan5/raylib/archive/refs/heads/master.zip",
7
.hash = "1220761aef32a253042bf79d5acc4c2013853f4db5a9a9f68cd341ee4ddb9099803a", // master
8
},
9
.@"raylib-cpp" = .{
10
.url = "https://github.com/RobLoach/raylib-cpp/archive/refs/tags/v5.0.2.tar.gz",
11
.hash = "12200de71a671fc3b2155475bc6691c51ac1ce81f4910fb0db9cad579a1047877108",
12
},
13
.googletest = .{
14
.url = "https://github.com/google/googletest/archive/refs/heads/main.zip",
15
.hash = "122098d850ec2ed23b11c196d9a3d8f16d7617cf5a0ee47379399334fbcfb8d9d913",
16
},
17
},
18
19
.paths = .{"build.zig"},
20
}

Writing a Build File


Start with the following in your build.zig. This just sets up an executable with the source files we created (main.cc and src/game.cc), with default target and optimization options. It also links libc and libcpp so the standard libraries for C and C++ are accessible.

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "game",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
exe.addCSourceFiles(.{
14
.root = b.path("."),
15
.files = &.{
16
"main.cc",
17
"src/game.cc",
18
},
19
});
20
exe.linkLibC();
21
exe.linkLibCpp();
22
23
b.installArtifact(exe);
24
}

Now we can add our dependencies.


Since raylib has a build.zig file, we have a relatively easy time installing it. We can reference the dependency by its name in build.zig.zon, create an artifact, link the artifact to the executable, and install the headers alongside the executable. The headers will show up under /zig-out/include.

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
18 collapsed lines
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "game",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
exe.addCSourceFiles(.{
14
.root = b.path("."),
15
.files = &.{
16
"main.cc",
17
"src/game.cc",
18
},
19
});
20
exe.linkLibC();
21
exe.linkLibCpp();
22
23
const raylib = b.dependency("raylib", .{
24
.target = target,
25
.optimize = optimize,
26
});
27
const raylibArtifact = raylib.artifact("raylib");
28
29
exe.linkLibrary(raylibArtifact);
30
31
b.installArtifact(raylibArtifact);
32
33
b.installArtifact(exe);
34
}

The C++ bindings to raylib are a little more complicated to add as a dependency, but it’s still not too difficult–you just can’t make it an artifact the way you can with raylib2.

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
24 collapsed lines
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "game",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
exe.addCSourceFiles(.{
14
.root = b.path("."),
15
.files = &.{
16
"main.cc",
17
"src/game.cc",
18
},
19
});
20
exe.linkLibC();
21
exe.linkLibCpp();
22
23
const raylib = b.dependency("raylib", .{
24
.target = target,
25
.optimize = optimize,
26
});
27
const raylibArtifact = raylib.artifact("raylib");
28
29
const raylibCpp = b.dependency("raylib-cpp", .{
30
.target = target,
31
.optimize = optimize,
32
});
33
34
exe.linkLibrary(raylibArtifact);
35
exe.addIncludePath(raylibCpp.path("include"));
36
37
b.installArtifact(raylibArtifact);
38
b.installDirectory(
39
.{
40
.source_dir = raylibCpp.path("include"),
41
.install_dir = .header,
42
.install_subdir = "",
43
},
44
);
45
46
b.installArtifact(exe);
47
}

These additions add raylib-cpp as a dependency, adds the include path of raylib-cpp, and installs the include directory into the header directory of /zig-out, which is called include.


The last thing we can do is add a run command (kind of like a cmake target). This is easy. Just add the following lines at the bottom.

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
44 collapsed lines
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "game",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
exe.addCSourceFiles(.{
14
.root = b.path("."),
15
.files = &.{
16
"main.cc",
17
"src/game.cc",
18
},
19
});
20
exe.linkLibC();
21
exe.linkLibCpp();
22
23
const raylib = b.dependency("raylib", .{
24
.target = target,
25
.optimize = optimize,
26
});
27
const raylibArtifact = raylib.artifact("raylib");
28
29
const raylibCpp = b.dependency("raylib-cpp", .{
30
.target = target,
31
.optimize = optimize,
32
});
33
34
exe.linkLibrary(raylibArtifact);
35
exe.addIncludePath(raylibCpp.path("include"));
36
37
b.installArtifact(raylibArtifact);
38
b.installDirectory(
39
.{
40
.source_dir = raylibCpp.path("include"),
41
.install_dir = .header,
42
.install_subdir = "",
43
},
44
);
45
46
b.installArtifact(exe);
47
48
const run = b.addRunArtifact(exe);
49
const run_step = b.step("run", "Run the application");
50
run_step.dependOn(&run.step);
51
}

Now if you run zig build, your project will be built and linked properly, and if you run zig build run, your application will also run after its built.


Building a Test Binary


Building GoogleTest is a lot more involved than adding Raylib since it doesn’t have a zig build system configured. We’ll have to do that ourselves. Luckily, that’s not too difficult. First, let’s create a new executable and load the dependency.

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "game",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
const test_exe = b.addExecutable(.{
14
.name = "game_test",
15
.target = target,
16
.optimize = optimize,
17
});
18
19
exe.addCSourceFiles(.{
20
.root = b.path("."),
21
.files = &.{
22
"main.cc",
23
"src/game.cc",
24
},
25
});
26
exe.linkLibC();
27
exe.linkLibCpp();
28
29
30
test_exe.addCSourceFiles(.{
31
.root = b.path("test"),
32
.files = &.{
33
"main_test.cc",
34
},
35
});
35 collapsed lines
36
test_exe.linkLibCpp();
37
38
const googletest = b.dependency("googletest", .{
39
.target = target,
40
.optimize = optimize,
41
});
42
43
const raylib = b.dependency("raylib", .{
44
.target = target,
45
.optimize = optimize,
46
});
47
const raylibArtifact = raylib.artifact("raylib");
48
49
const raylibCpp = b.dependency("raylib-cpp", .{
50
.target = target,
51
.optimize = optimize,
52
});
53
54
exe.linkLibrary(raylibArtifact);
55
exe.addIncludePath(raylibCpp.path("include"));
56
57
b.installArtifact(raylibArtifact);
58
b.installDirectory(
59
.{
60
.source_dir = raylibCpp.path("include"),
61
.install_dir = .header,
62
.install_subdir = "",
63
},
64
);
65
66
b.installArtifact(exe);
67
68
const run = b.addRunArtifact(exe);
69
const run_step = b.step("run", "Run the application");
70
run_step.dependOn(&run.step);
71
}

Now we need to specify how to build and link GoogleTest. This process involves inspecting the structure of the GoogleTest library on GitHub and telling zig to create a static library with that information.

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
39 collapsed lines
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "game",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
const test_exe = b.addExecutable(.{
14
.name = "game_test",
15
.target = target,
16
.optimize = optimize,
17
});
18
19
exe.addCSourceFiles(.{
20
.root = b.path("."),
21
.files = &.{
22
"main.cc",
23
"src/game.cc",
24
},
25
});
26
exe.linkLibC();
27
exe.linkLibCpp();
28
29
30
test_exe.addCSourceFiles(.{
31
.root = b.path("test"),
32
.files = &.{
33
"main_test.cc",
34
},
35
});
36
test_exe.linkLibCpp();
37
38
const googletest = b.dependency("googletest", .{
39
.target = target,
40
.optimize = optimize,
41
});
42
43
const gtest = b.addStaticLibrary(.{
44
.name = "gtest",
45
.target = target,
46
.optimize = optimize,
47
});
48
49
gtest.linkLibCpp();
50
gtest.addCSourceFiles(
51
.{
52
.root = googletest.path("googletest/src"),
53
.files = &.{
54
"gtest-all.cc",
55
"gtest-assertion-result.cc",
56
"gtest-death-test.cc",
57
"gtest-filepath.cc",
58
// "gtest-internal-inl.h",
59
"gtest-matchers.cc",
60
"gtest-port.cc",
61
"gtest-printers.cc",
62
"gtest-test-part.cc",
63
"gtest-typed-test.cc",
64
"gtest.cc",
65
"gtest_main.cc",
66
},
67
.flags = &.{
68
"-std=c++17",
69
},
70
},
71
);
72
73
gtest.addIncludePath(googletest.path("googletest"));
74
gtest.addIncludePath(googletest.path("googletest/src"));
75
gtest.addIncludePath(googletest.path("googletest/include"));
76
gtest.addIncludePath(googletest.path("googletest/include/internal"));
77
gtest.addIncludePath(googletest.path("googletest/include/internal/custom"));
78
79
const raylib = b.dependency("raylib", .{
80
.target = target,
81
.optimize = optimize,
82
});
83
const raylibArtifact = raylib.artifact("raylib");
84
85
const raylibCpp = b.dependency("raylib-cpp", .{
86
.target = target,
87
.optimize = optimize,
88
});
89
90
exe.linkLibrary(raylibArtifact);
91
exe.addIncludePath(raylibCpp.path("include"));
92
93
b.installArtifact(raylibArtifact);
94
b.installDirectory(
95
.{
96
.source_dir = raylibCpp.path("include"),
97
.install_dir = .header,
98
.install_subdir = "",
99
},
100
);
101
102
b.installArtifact(exe);
103
104
const run = b.addRunArtifact(exe);
105
const run_step = b.step("run", "Run the application");
106
run_step.dependOn(&run.step);
107
}

Notice how gtest-internals-inl.h is commented out. Do not make the same mistake I did and include that file. Inline headers are not part of the C++ source files, and you will get a scary error if you include it.


For GoogleMock, the process is similar:

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
74 collapsed lines
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "game",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
const test_exe = b.addExecutable(.{
14
.name = "game_test",
15
.target = target,
16
.optimize = optimize,
17
});
18
19
exe.addCSourceFiles(.{
20
.root = b.path("."),
21
.files = &.{
22
"main.cc",
23
"src/game.cc",
24
},
25
});
26
exe.linkLibC();
27
exe.linkLibCpp();
28
29
30
test_exe.addCSourceFiles(.{
31
.root = b.path("test"),
32
.files = &.{
33
"main_test.cc",
34
},
35
});
36
test_exe.linkLibCpp();
37
38
const googletest = b.dependency("googletest", .{
39
.target = target,
40
.optimize = optimize,
41
});
42
43
const gtest = b.addStaticLibrary(.{
44
.name = "gtest",
45
.target = target,
46
.optimize = optimize,
47
});
48
49
gtest.linkLibCpp();
50
gtest.addCSourceFiles(
51
.{
52
.root = googletest.path("googletest/src"),
53
.files = &.{
54
"gtest-all.cc",
55
"gtest-assertion-result.cc",
56
"gtest-death-test.cc",
57
"gtest-filepath.cc",
58
// "gtest-internal-inl.h",
59
"gtest-matchers.cc",
60
"gtest-port.cc",
61
"gtest-printers.cc",
62
"gtest-test-part.cc",
63
"gtest-typed-test.cc",
64
"gtest.cc",
65
"gtest_main.cc",
66
},
67
.flags = &.{
68
"-std=c++17",
69
},
70
},
71
);
72
73
gtest.addIncludePath(googletest.path("googletest"));
74
gtest.addIncludePath(googletest.path("googletest/src"));
75
gtest.addIncludePath(googletest.path("googletest/include"));
76
gtest.addIncludePath(googletest.path("googletest/include/internal"));
77
gtest.addIncludePath(googletest.path("googletest/include/internal/custom"));
78
79
const gmock = b.addStaticLibrary(.{
80
.name = "gmock",
81
.target = target,
82
.optimize = optimize,
83
});
84
85
gmock.linkLibCpp();
86
87
gmock.addCSourceFiles(
88
.{
89
.root = googletest.path("googlemock/src"),
90
.files = &.{
91
"gmock-all.cc",
92
"gmock-cardinalities.cc",
93
"gmock-internal-utils.cc",
94
"gmock-matchers.cc",
95
"gmock-spec-builders.cc",
96
"gmock.cc",
97
"gmock_main.cc",
98
},
99
.flags = &.{
100
"-std=c++17",
101
},
102
},
103
);
104
105
gmock.addIncludePath(googletest.path("googlemock"));
106
gmock.addIncludePath(googletest.path("googlemock/include"));
107
gmock.addIncludePath(googletest.path("googlemock/include/gmock"));
108
gmock.addIncludePath(googletest.path("googlemock/include/gmock/internal"));
109
gmock.addIncludePath(googletest.path("googlemock/include/gmock/internal/custom"));
110
gmock.addIncludePath(googletest.path("googletest"));
111
gmock.addIncludePath(googletest.path("googletest/src"));
112
gmock.addIncludePath(googletest.path("googletest/include"));
113
gmock.addIncludePath(googletest.path("googletest/include/internal"));
114
gmock.addIncludePath(googletest.path("googletest/include/internal/custom"));
115
29 collapsed lines
116
117
const raylib = b.dependency("raylib", .{
118
.target = target,
119
.optimize = optimize,
120
});
121
const raylibArtifact = raylib.artifact("raylib");
122
123
const raylibCpp = b.dependency("raylib-cpp", .{
124
.target = target,
125
.optimize = optimize,
126
});
127
128
exe.linkLibrary(raylibArtifact);
129
exe.addIncludePath(raylibCpp.path("include"));
130
131
b.installArtifact(raylibArtifact);
132
b.installDirectory(
133
.{
134
.source_dir = raylibCpp.path("include"),
135
.install_dir = .header,
136
.install_subdir = "",
137
},
138
);
139
140
b.installArtifact(exe);
141
142
const run = b.addRunArtifact(exe);
143
const run_step = b.step("run", "Run the application");
144
run_step.dependOn(&run.step);
145
}

Now we just need to link the static libraries we’ve built to test_exe and tell zig to output the C++ headers in a convenient location. We can also add another run target at the bottom so we can conveniently run all the tests.

build.zig
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
141 collapsed lines
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
7
const exe = b.addExecutable(.{
8
.name = "roguelike",
9
.target = target,
10
.optimize = optimize,
11
});
12
13
const test_exe = b.addExecutable(.{
14
.name = "roguelike_test",
15
.target = target,
16
.optimize = optimize,
17
});
18
19
exe.addCSourceFiles(.{
20
.root = b.path("."),
21
.files = &.{
22
"main.cc",
23
"src/game.cc",
24
},
25
});
26
exe.linkLibC();
27
exe.linkLibCpp();
28
29
test_exe.addCSourceFiles(.{
30
.root = b.path("test"),
31
.files = &.{
32
"main_test.cc",
33
},
34
});
35
test_exe.linkLibCpp();
36
37
const googletest = b.dependency("googletest", .{
38
.target = target,
39
.optimize = optimize,
40
});
41
42
const gtest = b.addStaticLibrary(.{
43
.name = "gtest",
44
.target = target,
45
.optimize = optimize,
46
});
47
48
gtest.linkLibCpp();
49
gtest.addCSourceFiles(
50
.{
51
.root = googletest.path("googletest/src"),
52
.files = &.{
53
"gtest-all.cc",
54
"gtest-assertion-result.cc",
55
"gtest-death-test.cc",
56
"gtest-filepath.cc",
57
// LOL @ this bug
58
// "gtest-internal-inl.h",
59
"gtest-matchers.cc",
60
"gtest-port.cc",
61
"gtest-printers.cc",
62
"gtest-test-part.cc",
63
"gtest-typed-test.cc",
64
"gtest.cc",
65
"gtest_main.cc",
66
},
67
.flags = &.{
68
"-std=c++17",
69
},
70
},
71
);
72
73
gtest.addIncludePath(googletest.path("googletest"));
74
gtest.addIncludePath(googletest.path("googletest/src"));
75
gtest.addIncludePath(googletest.path("googletest/include"));
76
gtest.addIncludePath(googletest.path("googletest/include/internal"));
77
gtest.addIncludePath(googletest.path("googletest/include/internal/custom"));
78
79
const gmock = b.addStaticLibrary(.{
80
.name = "gmock",
81
.target = target,
82
.optimize = optimize,
83
});
84
85
gmock.linkLibCpp();
86
87
gmock.addCSourceFiles(
88
.{
89
.root = googletest.path("googlemock/src"),
90
.files = &.{
91
"gmock-all.cc",
92
"gmock-cardinalities.cc",
93
"gmock-internal-utils.cc",
94
"gmock-matchers.cc",
95
"gmock-spec-builders.cc",
96
"gmock.cc",
97
"gmock_main.cc",
98
},
99
.flags = &.{
100
"-std=c++17",
101
},
102
},
103
);
104
105
gmock.addIncludePath(googletest.path("googlemock"));
106
gmock.addIncludePath(googletest.path("googlemock/include"));
107
gmock.addIncludePath(googletest.path("googlemock/include/gmock"));
108
gmock.addIncludePath(googletest.path("googlemock/include/gmock/internal"));
109
gmock.addIncludePath(googletest.path("googlemock/include/gmock/internal/custom"));
110
gmock.addIncludePath(googletest.path("googletest"));
111
gmock.addIncludePath(googletest.path("googletest/src"));
112
gmock.addIncludePath(googletest.path("googletest/include"));
113
gmock.addIncludePath(googletest.path("googletest/include/internal"));
114
gmock.addIncludePath(googletest.path("googletest/include/internal/custom"));
115
116
const raylib = b.dependency("raylib", .{
117
.target = target,
118
.optimize = optimize,
119
});
120
121
const raylibArtifact = raylib.artifact("raylib");
122
123
const raylibCpp = b.dependency("raylib-cpp", .{
124
.target = target,
125
.optimize = optimize,
126
});
127
128
exe.linkLibrary(raylibArtifact);
129
exe.addIncludePath(raylibCpp.path("include"));
130
exe.addIncludePath(b.path("include"));
131
132
test_exe.linkLibrary(gtest);
133
test_exe.linkLibrary(gmock);
134
test_exe.addIncludePath(googletest.path("googletest/include"));
135
test_exe.addIncludePath(googletest.path("googlemock/include"));
136
137
b.installArtifact(raylibArtifact);
138
b.installDirectory(
139
.{
140
.source_dir = raylibCpp.path("include"),
141
.install_dir = .header,
142
.install_subdir = "",
143
},
144
);
145
146
b.installDirectory(
147
.{
148
.source_dir = googletest.path("googletest/include"),
149
.install_dir = .header,
150
.install_subdir = "",
151
},
152
);
153
154
b.installDirectory(
155
.{
156
.source_dir = googletest.path("googlemock/include"),
157
.install_dir = .header,
158
.install_subdir = "",
159
},
160
);
161
162
b.installArtifact(exe);
163
164
b.installArtifact(test_exe);
165
166
const run = b.addRunArtifact(exe);
167
const run_step = b.step("run", "Run the application");
168
run_step.dependOn(&run.step);
169
170
const run_test = b.addRunArtifact(test_exe);
171
const run_test_step = b.step("test", "Run the tests");
172
run_test_step.dependOn(&run_test.step);
173
}

Running zig build should build GoogleTest and add the headers to /zig-out/include/. Running zig build test will run the tests.


Editor Support


The header files are located under /zig-out/include. You’ll need to add this to your IDE search path in order to make the header not found errors go away. I use neovim with clangd as my C++ LSP, so I just add the following to a .clangd file and restart my LSP to get IDE functionality.

.clangd
1
CompileFlags:
2
Add:
3
- "-I<path-to-root>/zig-out/include"

For VSCode, I’m pretty sure you just have to modify a .vscode/settings.json file to override/append to your include path.


Anyway, hope this was informative. It was interesting working with zig, and while I don’t really see its value proposition as a language, it seems like a very promising build tool for C and C++ projects.


Addendums

  1. There are zig bindings to raylib, but I really wouldn’t recommend using zig for actual production code outside of small, hobby projects. It’s fine as a build system because of its cross compile capabilities and abstractions over clang, but, at the end of the day, its unstable. Use it for larger projects at your own risk. In my opinion, its LSP/editor support is lacking, its documentation is anemic, and you are expected to regularly read the source code of the standard library to keep up with breaking API changes which leads to a poor developer experience. I just wanted to include a link to the zig bindings for completeness.

raylib
c++
zig