Skip to content

Commit 9839860

Browse files
author
jinzhongjia
committed
feat: add full Zig 0.16 compatibility and tuple module selection
- Support Zig 0.14, 0.15, and 0.16 seamlessly via a central `compat.zig` and conditional build logic for API and stdlib changes. - Add separate tuple helper modules and select at build time to handle breaking changes in Zig 0.16 parser. - Update CI, doc deployment, .gitignore, and examples to use new compat layer and tuple imports for simplified version-agnostic code. - Improve example maintainability by isolating version differences.
1 parent a605649 commit 9839860

12 files changed

Lines changed: 249 additions & 86 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
matrix:
2020
os: [ubuntu-latest, macos-latest, windows-latest]
21-
version: [0.14.0, 0.15.1]
21+
version: [0.14.0, 0.15.1, 0.16.0]
2222
fail-fast: false
2323
runs-on: ${{ matrix.os }}
2424
steps:
@@ -27,7 +27,7 @@ jobs:
2727
with:
2828
xcode-version: latest-stable
2929
- name: Setup Zig
30-
uses: goto-bus-stop/setup-zig@v2
30+
uses: mlugg/setup-zig@v2
3131
with:
3232
version: ${{ matrix.version }}
3333
- uses: actions/checkout@v4
@@ -40,6 +40,6 @@ jobs:
4040
runs-on: ubuntu-latest
4141
steps:
4242
- name: Setup Zig
43-
uses: goto-bus-stop/setup-zig@v2
43+
uses: mlugg/setup-zig@v2
4444
- name: Verify formatting
4545
run: zig fmt .

.github/workflows/deploy_docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ jobs:
3434
uses: actions/checkout@v3
3535
with:
3636
fetch-depth: 0 # Not needed if lastUpdated is not enabled
37-
- uses: goto-bus-stop/setup-zig@v2
37+
- uses: mlugg/setup-zig@v2
3838
with:
39-
version: 0.15.1
39+
version: 0.16.0
4040
- name: remove ./src/examples
4141
run: rm -rf ./src/examples
4242
- name: Generate Docs

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
zig-cache
22
zig-out
3+
zig-pkg
34
.direnv
45
.zig-cache
56
examples/comprehensive/examples

build.zig

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ pub fn build(b: *Build) !void {
4343

4444
const flags_module = flags_options.createModule();
4545

46+
// Pick the tuple-synthesis helper that matches the running Zig version.
47+
// 0.16 removed `@Type` and rejects it at parse time, so the legacy
48+
// implementation must live in a sibling file that is never compiled
49+
// on 0.16. See src/compat_tuple_{old,new}.zig.
50+
const compat_tuple_path = if (current_zig.minor >= 16)
51+
b.pathJoin(&.{ "src", "compat_tuple_new.zig" })
52+
else
53+
b.pathJoin(&.{ "src", "compat_tuple_old.zig" });
54+
const compat_tuple_module = b.addModule("compat_tuple", .{
55+
.root_source_file = b.path(compat_tuple_path),
56+
});
57+
4658
const webui = b.dependency("webui", .{
4759
.target = target,
4860
.optimize = optimize,
@@ -53,10 +65,10 @@ pub fn build(b: *Build) !void {
5365
});
5466
const webui_module = b.addModule("webui", .{
5567
.root_source_file = b.path(b.pathJoin(&.{ "src", "webui.zig" })),
56-
.imports = &.{.{
57-
.name = "flags",
58-
.module = flags_module,
59-
}},
68+
.imports = &.{
69+
.{ .name = "flags", .module = flags_module },
70+
.{ .name = "compat_tuple", .module = compat_tuple_module },
71+
},
6072
});
6173
webui_module.linkLibrary(webui.artifact("webui"));
6274

@@ -78,6 +90,7 @@ pub fn build(b: *Build) !void {
7890
.optimize = optimize,
7991
.target = target,
8092
.flags_module = flags_module,
93+
.compat_tuple_module = compat_tuple_module,
8194
});
8295
}
8396

@@ -94,6 +107,7 @@ const GenerateDocsOptions = struct {
94107
optimize: OptimizeMode,
95108
target: Build.ResolvedTarget,
96109
flags_module: *Module,
110+
compat_tuple_module: *Module,
97111
};
98112

99113
// ========== Helper Functions ==========
@@ -164,6 +178,7 @@ fn generateDocs(b: *Build, options: GenerateDocsOptions) void {
164178
);
165179

166180
webui_lib.root_module.addImport("flags", options.flags_module);
181+
webui_lib.root_module.addImport("compat_tuple", options.compat_tuple_module);
167182

168183
const docs_step = b.step("docs", "Generate docs");
169184
const docs_install = b.addInstallDirectory(.{
@@ -182,21 +197,37 @@ fn buildExamples(b: *Build, options: BuildExamplesOptions) !void {
182197
const build_all_step = b.step("examples", "build all examples");
183198
const examples_path = lazy_path.getPath(b);
184199

185-
var examples_dir = b.build_root.handle.openDir(examples_path, .{ .iterate = true }) catch |err| {
186-
switch (err) {
187-
error.FileNotFound => return,
188-
else => return err,
200+
if (comptime builtin.zig_version.minor >= 16) {
201+
// Zig 0.16+: build_root.handle is std.Io.Dir and requires an `io`.
202+
const io = b.graph.io;
203+
var examples_dir = b.build_root.handle.openDir(io, examples_path, .{ .iterate = true }) catch |err| {
204+
switch (err) {
205+
error.FileNotFound => return,
206+
else => return err,
207+
}
208+
};
209+
defer examples_dir.close(io);
210+
211+
var iter = examples_dir.iterate();
212+
while (try iter.next(io)) |entry| {
213+
if (entry.kind != .directory) continue;
214+
try buildExample(b, entry.name, options, build_all_step);
189215
}
190-
};
191-
defer examples_dir.close();
192-
193-
var iter = examples_dir.iterate();
194-
while (try iter.next()) |entry| {
195-
if (entry.kind != .directory) {
196-
continue;
216+
} else {
217+
// Zig 0.14/0.15: build_root.handle is std.fs.Dir.
218+
var examples_dir = b.build_root.handle.openDir(examples_path, .{ .iterate = true }) catch |err| {
219+
switch (err) {
220+
error.FileNotFound => return,
221+
else => return err,
222+
}
223+
};
224+
defer examples_dir.close();
225+
226+
var iter = examples_dir.iterate();
227+
while (try iter.next()) |entry| {
228+
if (entry.kind != .directory) continue;
229+
try buildExample(b, entry.name, options, build_all_step);
197230
}
198-
199-
try buildExample(b, entry.name, options, build_all_step);
200231
}
201232
}
202233

examples/compat.zig

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
//! Compatibility layer for Zig 0.15 and 0.16
1+
//! Compatibility layer for Zig 0.14 / 0.15 / 0.16.
2+
//!
3+
//! Several stdlib APIs the examples rely on were renamed or removed in
4+
//! Zig 0.16 (Writergate, `std.fs` -> `std.Io.Dir`, `GeneralPurposeAllocator`
5+
//! -> `DebugAllocator`, etc). This module hides the version differences
6+
//! so each example can stay short and readable.
27
const std = @import("std");
38
const builtin = @import("builtin");
49

@@ -44,8 +49,7 @@ pub fn fixedBufferStream(buffer: []u8) FixedBufferStream {
4449
}
4550

4651
/// Type alias for FixedBufferStream that works in both versions
47-
pub const FixedBufferStream = if (is_zig_0_16_or_later)
48-
blk: {
52+
pub const FixedBufferStream = if (is_zig_0_16_or_later) blk: {
4953
// Zig 0.16: Define our own FixedBufferStream with custom Writer
5054
break :blk struct {
5155
buffer: []u8,
@@ -91,3 +95,115 @@ blk: {
9195
// Zig 0.15: Use standard library type
9296
break :blk @TypeOf(std.io.fixedBufferStream(@as([]u8, undefined)));
9397
};
98+
99+
// ===== Allocator compat ======================================================
100+
101+
/// `std.heap.GeneralPurposeAllocator` was renamed to `std.heap.DebugAllocator`
102+
/// in Zig 0.16. Use this alias to write code that works on all supported
103+
/// versions.
104+
pub const GeneralPurposeAllocator = if (is_zig_0_16_or_later)
105+
std.heap.DebugAllocator
106+
else
107+
std.heap.GeneralPurposeAllocator;
108+
109+
// ===== Filesystem compat =====================================================
110+
//
111+
// `std.fs.cwd()` and the synchronous `std.fs.File` API were removed in 0.16.
112+
// The new API lives under `std.Io.Dir` / `std.Io.File` and threads an `io`
113+
// instance through every call. The helpers below give the examples a small,
114+
// uniform surface that hides this difference.
115+
116+
/// Create directories as needed up to (and including) `path`. Equivalent to
117+
/// `mkdir -p` on POSIX.
118+
pub fn makePath(path: []const u8) !void {
119+
if (comptime is_zig_0_16_or_later) {
120+
// Zig 0.16 renamed `makePath` to `createDirPath`.
121+
const io = ioInstance();
122+
try std.Io.Dir.cwd().createDirPath(io, path);
123+
} else {
124+
try std.fs.cwd().makePath(path);
125+
}
126+
}
127+
128+
/// Create a single directory. Returns an error if `path` already exists.
129+
pub fn makeDir(path: []const u8) !void {
130+
if (comptime is_zig_0_16_or_later) {
131+
// Zig 0.16 renamed `makeDir` to `createDir`; pass the platform default
132+
// permissions so callers don't need to know the new shape.
133+
const io = ioInstance();
134+
try std.Io.Dir.cwd().createDir(io, path, .default_dir);
135+
} else {
136+
try std.fs.cwd().makeDir(path);
137+
}
138+
}
139+
140+
/// Delete a regular file relative to the current working directory.
141+
pub fn deleteFile(path: []const u8) !void {
142+
if (comptime is_zig_0_16_or_later) {
143+
const io = ioInstance();
144+
try std.Io.Dir.cwd().deleteFile(io, path);
145+
} else {
146+
try std.fs.cwd().deleteFile(path);
147+
}
148+
}
149+
150+
/// One-shot "create file and write everything to it" helper. Hides the
151+
/// reader/writer plumbing differences between 0.15 and 0.16.
152+
pub fn writeFile(path: []const u8, content: []const u8) !void {
153+
if (comptime is_zig_0_16_or_later) {
154+
const io = ioInstance();
155+
var file = try std.Io.Dir.cwd().createFile(io, path, .{});
156+
defer file.close(io);
157+
try file.writeStreamingAll(io, content);
158+
} else {
159+
const file = try std.fs.cwd().createFile(path, .{});
160+
defer file.close();
161+
try file.writeAll(content);
162+
}
163+
}
164+
165+
/// Get a usable `std.Io` instance on 0.16. Cheap to call: returns the
166+
/// process-global single-threaded implementation.
167+
fn ioInstance() std.Io {
168+
if (comptime !is_zig_0_16_or_later) @compileError("ioInstance is 0.16+ only");
169+
return std.Io.Threaded.global_single_threaded.io();
170+
}
171+
172+
// ===== Child process compat ==================================================
173+
//
174+
// Zig 0.16 removed `std.process.Child.init(argv, allocator)` and reworked
175+
// child-process spawning around `std.process.spawn(io, options)`. The wrapper
176+
// below is intentionally narrow — it exposes only what the examples need:
177+
// spawn-with-argv, get the OS pid, and kill.
178+
179+
pub const ChildProcess = struct {
180+
/// OS-level pid. Optional because 0.16 stores it as `?i32` (the value is
181+
/// `null` after `kill`/`wait`); on 0.14/0.15 it is always populated.
182+
pid: ?std.process.Child.Id,
183+
child: std.process.Child,
184+
185+
/// Spawn a process with the given argv. `allocator` is used for argv
186+
/// translation on 0.14/0.15; ignored on 0.16 (which routes through Io).
187+
pub fn spawn(argv: []const []const u8, allocator: std.mem.Allocator) !ChildProcess {
188+
if (comptime is_zig_0_16_or_later) {
189+
const io = ioInstance();
190+
const child = try std.process.spawn(io, .{ .argv = argv });
191+
return .{ .pid = child.id, .child = child };
192+
} else {
193+
// The 0.14/0.15 Child.init signature requires an allocator; suppress
194+
// the "unused parameter" warning by referencing it explicitly here.
195+
var child = std.process.Child.init(argv, allocator);
196+
try child.spawn();
197+
return .{ .pid = child.id, .child = child };
198+
}
199+
}
200+
201+
pub fn kill(self: *ChildProcess) !void {
202+
if (comptime is_zig_0_16_or_later) {
203+
const io = ioInstance();
204+
self.child.kill(io);
205+
} else {
206+
_ = try self.child.kill();
207+
}
208+
}
209+
};

examples/comprehensive/main.zig

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub fn main() !void {
8585
main_window.setRuntime(.NodeJS);
8686

8787
// Create public directory
88-
std.fs.cwd().makeDir("examples/comprehensive/public") catch {};
88+
compat.makeDir("examples/comprehensive/public") catch {};
8989

9090
// Show window
9191
try main_window.show(html);
@@ -388,7 +388,7 @@ fn uploadFile(e: *webui.Event, filename: [:0]const u8, content: [:0]const u8) vo
388388
}
389389

390390
// Ensure public directory exists
391-
std.fs.cwd().makePath("examples/comprehensive/public") catch |err| {
391+
compat.makePath("examples/comprehensive/public") catch |err| {
392392
std.debug.print("Warning: Failed to create public directory: {}\n", .{err});
393393
// Continue anyway, maybe directory already exists
394394
};
@@ -458,19 +458,10 @@ fn uploadFile(e: *webui.Event, filename: [:0]const u8, content: [:0]const u8) vo
458458

459459
std.debug.print("Creating file at: {s}\n", .{file_path});
460460

461-
// Create file
462-
const file = std.fs.cwd().createFile(file_path, .{}) catch |err| {
463-
std.debug.print("Failed to create file {s}: {}\n", .{ file_path, err });
464-
result = std.fmt.bufPrintZ(response[0..], "Error: Failed to create file '{s}' ({s})", .{ filename, @errorName(err) }) catch "Error";
465-
e.returnString(result);
466-
return;
467-
};
468-
defer file.close();
469-
470461
// Write content to file
471-
file.writeAll(content) catch |err| {
472-
std.debug.print("Failed to write to file {s}: {}\n", .{ file_path, err });
473-
result = std.fmt.bufPrintZ(response[0..], "Error: Failed to write to file '{s}' ({s})", .{ filename, @errorName(err) }) catch "Error";
462+
compat.writeFile(file_path, content) catch |err| {
463+
std.debug.print("Failed to write file {s}: {}\n", .{ file_path, err });
464+
result = std.fmt.bufPrintZ(response[0..], "Error: Failed to write file '{s}' ({s})", .{ filename, @errorName(err) }) catch "Error";
474465
e.returnString(result);
475466
return;
476467
};

examples/custom_spa_server_on_free_port/main.zig

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// Note: if you want to run this example, you nedd a python, zig will wrap a child process to launch python server
33
const std = @import("std");
44
const webui = @import("webui");
5+
const compat = @import("compat");
56

6-
var python_server_proc: std.process.Child = undefined;
7+
var python_server_proc: compat.ChildProcess = undefined;
78
var python_running: bool = false;
89

910
var home_url: [:0]u8 = undefined;
@@ -38,10 +39,9 @@ pub fn main() !void {
3839
const port_argument1: []u8 = try std.fmt.bufPrintZ(&buf1, "{d}", .{backend_port});
3940
const port_argument2: []u8 = try std.fmt.bufPrintZ(&buf2, "{d}", .{webui_port});
4041
const argv = [_][]const u8{ "python", "./free_port_web_server.py", port_argument1, port_argument2 };
41-
python_server_proc = std.process.Child.init(&argv, std.heap.page_allocator);
4242

4343
// start the SPA web server:
44-
startPythonWebServer();
44+
startPythonWebServer(&argv);
4545

4646
// Show a new window served by our custom web server (spawned above):
4747
var buf: [64]u8 = undefined;
@@ -58,11 +58,12 @@ pub fn main() !void {
5858
killPythonWebServer();
5959
}
6060

61-
fn startPythonWebServer() void {
61+
fn startPythonWebServer(argv: []const []const u8) void {
6262
if (python_running == false) { // a better check would be a test for the process itself
63-
if (python_server_proc.spawn()) |_| {
63+
if (compat.ChildProcess.spawn(argv, std.heap.page_allocator)) |child| {
64+
python_server_proc = child;
6465
python_running = true;
65-
std.debug.print("Spawned python server process PID={}\n", .{python_server_proc.id});
66+
std.debug.print("Spawned python server process PID={?}\n", .{python_server_proc.pid});
6667
} else |err| {
6768
std.debug.print("NOT Starting python server: {}\n", .{err});
6869
}

0 commit comments

Comments
 (0)