+16
-13
languages/ziglang/README.md
+16
-13
languages/ziglang/README.md
···
1
1
# zig
2
2
3
-
notes on [zig](https://ziglang.org/) patterns from building atproto/bluesky infrastructure.
3
+
notes on [zig](https://ziglang.org/) patterns.
4
4
5
-
## versions
5
+
## topics
6
6
7
-
- [0.15](./0.15/) - major i/o overhaul, new build system patterns ([release notes](https://ziglang.org/download/0.15.1/release-notes.html))
7
+
- [0.15](./0.15/) - version-specific patterns (i/o overhaul, arraylist, concurrency)
8
+
- [build](./build/) - build system patterns from large projects
8
9
9
-
## projects
10
+
## sources
10
11
11
-
these notes are derived from:
12
+
patterns derived from building and studying:
12
13
13
-
| project | description |
14
-
|---------|-------------|
15
-
| [music-atmosphere-feed](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed) | bluesky feed generator - http server, jetstream consumer, jwt verification |
16
-
| [find-bufo](https://tangled.sh/@zzstoatzz.io/find-bufo) | bluesky bot - websocket client, bluesky api, image posting |
17
-
| [leaflet-search](https://tangled.sh/@zzstoatzz.io/leaflet-search) | fts search backend - sqlite fts5, http server, dashboard |
18
-
| [zql](https://tangled.sh/@zzstoatzz.io/zql) | comptime sql parsing - parameter extraction, struct mapping |
14
+
| project | what it is |
15
+
|---------|------------|
16
+
| [music-atmosphere-feed](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed) | bluesky feed generator |
17
+
| [find-bufo](https://tangled.sh/@zzstoatzz.io/find-bufo) | bluesky bot |
18
+
| [leaflet-search](https://tangled.sh/@zzstoatzz.io/leaflet-search) | fts search backend |
19
+
| [zql](https://tangled.sh/@zzstoatzz.io/zql) | comptime sql parsing |
20
+
| [ghostty](https://github.com/ghostty-org/ghostty) | terminal emulator (build system) |
21
+
| [bun](https://github.com/oven-sh/bun) | javascript runtime (build system) |
19
22
20
-
## dependencies
23
+
## libraries
21
24
22
-
- [websocket.zig](https://github.com/karlseguin/websocket.zig) - websocket client with tls support
25
+
- [websocket.zig](https://github.com/karlseguin/websocket.zig) - websocket client with tls
23
26
- [zqlite.zig](https://github.com/karlseguin/zqlite.zig) - sqlite wrapper
+10
languages/ziglang/build/README.md
+10
languages/ziglang/build/README.md
···
1
+
# build system
2
+
3
+
notes on zig's build system, drawn from studying [ghostty](https://github.com/ghostty-org/ghostty) (terminal emulator) and [bun](https://github.com/oven-sh/bun) (javascript runtime). both are large, production zig projects with sophisticated builds.
4
+
5
+
## notes
6
+
7
+
- [organization](./organization.md) - structuring build.zig for large projects
8
+
- [dependencies](./dependencies.md) - vendored vs system libs, lazy deps, pinning
9
+
- [codegen](./codegen.md) - generating zig source at build time
10
+
- [cross-compilation](./cross-compilation.md) - targeting multiple platforms
+97
languages/ziglang/build/codegen.md
+97
languages/ziglang/build/codegen.md
···
1
+
# code generation
2
+
3
+
generating zig source at build time - for help text, unicode tables, bindings, anything derived from data.
4
+
5
+
## the pattern
6
+
7
+
build an executable, run it, capture output, import as zig source:
8
+
9
+
```zig
10
+
// 1. build the generator (always for host, not target)
11
+
const gen_exe = b.addExecutable(.{
12
+
.name = "helpgen",
13
+
.root_module = b.createModule(.{
14
+
.root_source_file = b.path("src/helpgen.zig"),
15
+
.target = b.graph.host, // runs on build machine
16
+
}),
17
+
});
18
+
19
+
// 2. run it and capture stdout
20
+
const gen_run = b.addRunArtifact(gen_exe);
21
+
const gen_output = gen_run.captureStdOut();
22
+
23
+
// 3. make it available as an import
24
+
step.root_module.addAnonymousImport("help_strings", .{
25
+
.root_source_file = gen_output,
26
+
});
27
+
```
28
+
29
+
now your code can `@import("help_strings")` and get the generated content.
30
+
31
+
## why `.target = b.graph.host`
32
+
33
+
the generator runs during the build, on your machine. even if you're cross-compiling to arm64-linux, the generator needs to run on your x86-macos (or whatever you're building from).
34
+
35
+
`b.graph.host` gives you the host target - the machine running the build.
36
+
37
+
## writing to files instead
38
+
39
+
if you need the output as a file (not just an import):
40
+
41
+
```zig
42
+
const wf = b.addWriteFiles();
43
+
const output_path = wf.addCopyFile(
44
+
gen_run.captureStdOut(),
45
+
"generated.zig",
46
+
);
47
+
// output_path is a LazyPath you can use elsewhere
48
+
```
49
+
50
+
## custom build steps for external tools
51
+
52
+
wrap non-zig tools (metal shader compiler, lipo, etc.) as build steps:
53
+
54
+
```zig
55
+
pub const MetallibStep = struct {
56
+
step: std.Build.Step,
57
+
output: std.Build.LazyPath,
58
+
59
+
pub fn create(b: *std.Build, shader_source: []const u8) *MetallibStep {
60
+
const run = b.addSystemCommand(&.{
61
+
"/usr/bin/xcrun", "-sdk", "macosx", "metal",
62
+
"-c", "-o",
63
+
});
64
+
const ir_output = run.addOutputFileArg("shader.ir");
65
+
run.addFileArg(b.path(shader_source));
66
+
67
+
// chain another command for metallib...
68
+
const self = b.allocator.create(MetallibStep) catch @panic("OOM");
69
+
self.* = .{
70
+
.step = std.Build.Step.init(.{ ... }),
71
+
.output = ir_output,
72
+
};
73
+
return self;
74
+
}
75
+
};
76
+
```
77
+
78
+
key points:
79
+
- `addOutputFileArg()` creates a LazyPath for the output
80
+
- `addFileArg()` adds a dependency on an input file
81
+
- proper dependency tracking means the step reruns when inputs change
82
+
83
+
## conditional embedding
84
+
85
+
bun embeds javascript runtime code in release builds but loads from disk in debug:
86
+
87
+
```zig
88
+
pub fn shouldEmbedCode(opts: *const BuildOptions) bool {
89
+
return opts.optimize != .Debug or opts.force_embed;
90
+
}
91
+
```
92
+
93
+
debug builds iterate faster (no recompile to change js). release builds are self-contained.
94
+
95
+
sources:
96
+
- [ghostty/src/build/HelpStrings.zig](https://github.com/ghostty-org/ghostty/blob/main/src/build/HelpStrings.zig)
97
+
- [ghostty/src/build/MetallibStep.zig](https://github.com/ghostty-org/ghostty/blob/main/src/build/MetallibStep.zig)
+136
languages/ziglang/build/cross-compilation.md
+136
languages/ziglang/build/cross-compilation.md
···
1
+
# cross-compilation
2
+
3
+
building for platforms other than the one you're on. zig makes this unusually easy, but there are still patterns to know.
4
+
5
+
## basic cross-compilation
6
+
7
+
zig can target any platform from any platform:
8
+
9
+
```zig
10
+
const target = b.standardTargetOptions(.{}); // from -Dtarget=...
11
+
```
12
+
13
+
user runs: `zig build -Dtarget=aarch64-linux-gnu`
14
+
15
+
that's it for pure zig. c dependencies complicate things.
16
+
17
+
## cpu targeting
18
+
19
+
bun explicitly sets cpu models per platform:
20
+
21
+
```zig
22
+
pub fn getCpuModel(os: OperatingSystem, arch: Arch) ?Target.Query.CpuModel {
23
+
if (os == .linux and arch == .aarch64) {
24
+
return .{ .explicit = &Target.aarch64.cpu.cortex_a35 };
25
+
}
26
+
if (os == .mac and arch == .aarch64) {
27
+
return .{ .explicit = &Target.aarch64.cpu.apple_m1 };
28
+
}
29
+
// x86_64 defaults to haswell for avx2
30
+
return null;
31
+
}
32
+
```
33
+
34
+
and offers a "baseline" mode for maximum compatibility:
35
+
36
+
```zig
37
+
if (opts.baseline) {
38
+
// target nehalem (~2008) instead of haswell (~2013)
39
+
return .{ .explicit = &Target.x86_64.cpu.nehalem };
40
+
}
41
+
```
42
+
43
+
baseline builds run on older hardware but miss avx2 optimizations.
44
+
45
+
## glibc version
46
+
47
+
linux binaries link against glibc. if you build against glibc 2.34, it won't run on systems with glibc 2.17.
48
+
49
+
```zig
50
+
pub fn getOSGlibCVersion(os: OperatingSystem) ?Version {
51
+
return switch (os) {
52
+
.linux => .{ .major = 2, .minor = 26, .patch = 0 },
53
+
else => null,
54
+
};
55
+
}
56
+
```
57
+
58
+
bun targets glibc 2.26 (from ~2017) for broad compatibility.
59
+
60
+
## macos universal binaries
61
+
62
+
ghostty builds for both x86_64 and aarch64, then combines with lipo:
63
+
64
+
```zig
65
+
// build for both architectures
66
+
const x86_lib = try buildLib(b, deps.retarget(b, x86_64_macos));
67
+
const arm_lib = try buildLib(b, deps.retarget(b, aarch64_macos));
68
+
69
+
// combine into universal binary
70
+
const lipo_step = LipoStep.create(b, .{
71
+
.input_a = x86_lib.getEmittedBin(),
72
+
.input_b = arm_lib.getEmittedBin(),
73
+
.out_name = "libghostty.a",
74
+
});
75
+
```
76
+
77
+
the `deps.retarget()` pattern creates a copy of SharedDeps pointing at a different target.
78
+
79
+
## xcframework for apple platforms
80
+
81
+
for ios apps, you need an xcframework containing:
82
+
- macos universal (x86_64 + arm64)
83
+
- ios arm64
84
+
- ios simulator (arm64 + x86_64)
85
+
86
+
```zig
87
+
const macos = try buildMacOSUniversal(b, deps);
88
+
const ios = try buildLib(b, deps.retarget(b, .{ .os_tag = .ios, .cpu_arch = .aarch64 }));
89
+
const ios_sim = try buildLib(b, deps.retarget(b, .{ .os_tag = .ios, .abi = .simulator }));
90
+
91
+
// xcodebuild -create-xcframework ...
92
+
```
93
+
94
+
## minimum os versions
95
+
96
+
centralize version requirements:
97
+
98
+
```zig
99
+
pub fn osVersionMin(os: std.Target.Os.Tag) std.Target.Os.SemVer {
100
+
return switch (os) {
101
+
.macos => .{ .major = 13, .minor = 0, .patch = 0 },
102
+
.ios => .{ .major = 17, .minor = 0, .patch = 0 },
103
+
else => .{ .major = 0, .minor = 0, .patch = 0 },
104
+
};
105
+
}
106
+
```
107
+
108
+
apply when creating targets:
109
+
110
+
```zig
111
+
const target = b.resolveTargetQuery(.{
112
+
.os_tag = .macos,
113
+
.os_version_min = Config.osVersionMin(.macos),
114
+
});
115
+
```
116
+
117
+
## environment detection
118
+
119
+
helpful warnings for common mistakes:
120
+
121
+
```zig
122
+
fn checkNixShell(exe: *std.Build.Step.Compile, cfg: *const Config) !void {
123
+
std.fs.accessAbsolute("/etc/NIXOS", .{}) catch return; // not nixos
124
+
if (cfg.env.get("IN_NIX_SHELL") != null) return; // in nix shell, good
125
+
126
+
try exe.step.addError(
127
+
"Building on NixOS outside nix shell. " ++
128
+
"Use: nix develop -c zig build",
129
+
.{},
130
+
);
131
+
}
132
+
```
133
+
134
+
sources:
135
+
- [ghostty/src/build/GhosttyLib.zig](https://github.com/ghostty-org/ghostty/blob/main/src/build/GhosttyLib.zig)
136
+
- [bun/build.zig](https://github.com/oven-sh/bun/blob/main/build.zig)
+109
languages/ziglang/build/dependencies.md
+109
languages/ziglang/build/dependencies.md
···
1
+
# dependencies
2
+
3
+
managing external code - when to vendor, when to use system libs, how to keep things reproducible.
4
+
5
+
## lazy dependencies
6
+
7
+
dependencies marked `.lazy = true` in build.zig.zon are only fetched when actually used:
8
+
9
+
```zig
10
+
// build.zig.zon
11
+
.dependencies = .{
12
+
.harfbuzz = .{ .path = "./pkg/harfbuzz", .lazy = true },
13
+
.macos = .{ .path = "./pkg/macos", .lazy = true },
14
+
},
15
+
```
16
+
17
+
```zig
18
+
// build.zig
19
+
if (b.lazyDependency("harfbuzz", .{ .target = target })) |dep| {
20
+
step.linkLibrary(dep.artifact("harfbuzz"));
21
+
}
22
+
```
23
+
24
+
why: platform-specific deps (like `macos`) don't slow down linux builds. optional features don't fetch deps when disabled.
25
+
26
+
## vendored vs system libraries
27
+
28
+
ghostty supports both - vendored for reproducibility, system for distro integration:
29
+
30
+
```zig
31
+
if (b.systemIntegrationOption("freetype", .{})) {
32
+
step.linkSystemLibrary2("freetype2", .{});
33
+
} else {
34
+
step.linkLibrary(freetype_dep.artifact("freetype"));
35
+
}
36
+
```
37
+
38
+
with smart defaults:
39
+
40
+
```zig
41
+
_ = b.systemIntegrationOption("freetype", .{
42
+
.default = if (target.result.os.tag.isDarwin()) false else null,
43
+
});
44
+
```
45
+
46
+
macos defaults to vendored (for universal binaries). linux defaults to "ask pkg-config".
47
+
48
+
users override with `-Dsystem-freetype=true` or `-Dsystem-freetype=false`.
49
+
50
+
## c library wrappers
51
+
52
+
each c dependency gets its own package with a build.zig:
53
+
54
+
```
55
+
pkg/
56
+
├── freetype/
57
+
│ ├── build.zig # builds the C library
58
+
│ ├── build.zig.zon # declares the package
59
+
│ └── main.zig # zig bindings
60
+
├── harfbuzz/
61
+
└── libpng/
62
+
```
63
+
64
+
the wrapper handles:
65
+
- platform-specific source file selection
66
+
- compiler flags for c code
67
+
- exposing a zig module with bindings
68
+
69
+
## pinning for reproducibility
70
+
71
+
bun pins every dependency to exact git commits:
72
+
73
+
```cmake
74
+
register_repository(
75
+
NAME boringssl
76
+
REPOSITORY oven-sh/boringssl
77
+
COMMIT f1ffd9e83d4f5c28a9c70d73f9a4e6fcf310062f
78
+
)
79
+
```
80
+
81
+
and forks dependencies to their own org (`oven-sh/boringssl`). no surprise breakage from upstream.
82
+
83
+
in zig terms, this means using `.hash` in build.zig.zon and avoiding `.url` pointing to `master` branches.
84
+
85
+
## detecting what's installed
86
+
87
+
before defaulting to system libs, check what's available:
88
+
89
+
```zig
90
+
pub fn gtkTargets(b: *std.Build) struct { x11: bool, wayland: bool } {
91
+
var code: u8 = undefined;
92
+
const output = b.runAllowFail(
93
+
&.{ "pkg-config", "--variable=targets", "gtk4" },
94
+
&code,
95
+
.Ignore,
96
+
) catch return .{ .x11 = false, .wayland = false };
97
+
98
+
return .{
99
+
.x11 = std.mem.indexOf(u8, output, "x11") != null,
100
+
.wayland = std.mem.indexOf(u8, output, "wayland") != null,
101
+
};
102
+
}
103
+
```
104
+
105
+
fails gracefully when pkg-config isn't available.
106
+
107
+
sources:
108
+
- [ghostty/build.zig.zon](https://github.com/ghostty-org/ghostty/blob/main/build.zig.zon)
109
+
- [ghostty/pkg/](https://github.com/ghostty-org/ghostty/tree/main/pkg)
+101
languages/ziglang/build/organization.md
+101
languages/ziglang/build/organization.md
···
1
+
# build organization
2
+
3
+
as projects grow, a single `build.zig` becomes unwieldy. ghostty's solution: treat build logic as a package.
4
+
5
+
## the pattern
6
+
7
+
instead of one giant build.zig, create `src/build/` as a zig package:
8
+
9
+
```
10
+
src/build/
11
+
├── main.zig # exports everything
12
+
├── Config.zig # all -D options in one struct
13
+
├── SharedDeps.zig # dependency wiring
14
+
├── GhosttyExe.zig # executable-specific logic
15
+
├── GhosttyLib.zig # library-specific logic
16
+
└── steps/ # custom build steps
17
+
```
18
+
19
+
the root `build.zig` becomes a thin shell:
20
+
21
+
```zig
22
+
const buildpkg = @import("src/build/main.zig");
23
+
24
+
pub fn build(b: *std.Build) !void {
25
+
const config = buildpkg.Config.fromOptions(b);
26
+
const deps = try buildpkg.SharedDeps.init(b, &config);
27
+
28
+
if (config.emit_exe) {
29
+
_ = try buildpkg.GhosttyExe.init(b, &deps);
30
+
}
31
+
if (config.emit_lib) {
32
+
_ = try buildpkg.GhosttyLib.init(b, &deps);
33
+
}
34
+
}
35
+
```
36
+
37
+
## centralized configuration
38
+
39
+
all `-D` options live in one struct. this makes them discoverable and passable:
40
+
41
+
```zig
42
+
// Config.zig
43
+
pub const Config = @This();
44
+
45
+
// features
46
+
x11: bool = false,
47
+
wayland: bool = false,
48
+
sentry: bool = true,
49
+
50
+
// artifacts to emit
51
+
emit_exe: bool = false,
52
+
emit_lib: bool = false,
53
+
emit_docs: bool = false,
54
+
55
+
pub fn fromOptions(b: *std.Build) Config {
56
+
return .{
57
+
.x11 = b.option(bool, "x11", "Enable X11") orelse false,
58
+
.wayland = b.option(bool, "wayland", "Enable Wayland") orelse false,
59
+
// ...
60
+
};
61
+
}
62
+
63
+
// export to comptime for runtime introspection
64
+
pub fn addOptions(self: *const Config, step: *std.Build.Step.Compile) void {
65
+
const opts = step.root_module.addOptions();
66
+
opts.addOption(bool, "x11", self.x11);
67
+
opts.addOption(bool, "wayland", self.wayland);
68
+
// now @import("build_options").x11 works in source
69
+
}
70
+
```
71
+
72
+
## shared dependencies
73
+
74
+
avoid duplicating dependency wiring across artifacts:
75
+
76
+
```zig
77
+
// SharedDeps.zig
78
+
pub const SharedDeps = @This();
79
+
80
+
config: *const Config,
81
+
freetype: *std.Build.Dependency,
82
+
harfbuzz: *std.Build.Dependency,
83
+
84
+
pub fn add(self: *const SharedDeps, step: *std.Build.Step.Compile) void {
85
+
step.linkLibrary(self.freetype.artifact("freetype"));
86
+
step.linkLibrary(self.harfbuzz.artifact("harfbuzz"));
87
+
// add all the deps once, use everywhere
88
+
}
89
+
```
90
+
91
+
now both `GhosttyExe` and `GhosttyLib` just call `deps.add(step)`.
92
+
93
+
## when to split
94
+
95
+
small projects don't need this. consider splitting when:
96
+
- build.zig exceeds ~500 lines
97
+
- you have multiple artifacts (exe, lib, tests) sharing deps
98
+
- platform-specific logic is getting tangled
99
+
- you want to test build logic itself
100
+
101
+
source: [ghostty/src/build/](https://github.com/ghostty-org/ghostty/tree/main/src/build)