git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitf00d1ccbde8ca7be2d2c7263a1f0100851e43d30
parent4384719b86
authorIsaac Freund <[email protected]>
date2026-04-16 17:14
build: update to Zig 0.16

 .builds/alpine.yml    |  8 ++---
 .builds/archlinux.yml |  8 ++---
 .builds/freebsd.yml   |  8 ++---
 .gitignore            |  1 +
 README.md             |  2 +-
 build.zig             | 10 +++---
 build.zig.zon         |  4 +--
 common/flags.zig      | 66 ++++++++++++-----------------------
 river/InputPopup.zig  |  2 +-
 river/Output.zig      |  2 +-
 river/Server.zig      | 20 ++---------
 river/Window.zig      |  2 +-
 river/XkbConfig.zig   | 17 +++++----
 river/c.zig           |  6 ----
 river/main.zig        | 95 ++++++++++++++++++++++++++++++++++++---------------
 river/process.zig     |  4 +--
 river/util.zig        | 11 +++++-
 17 files changed, 135 insertions(+), 131 deletions(-)

diff --git a/.builds/alpine.yml b/.builds/alpine.yml
index 16f1257..f7886d0 100644
--- a/.builds/alpine.yml
+++ b/.builds/alpine.yml
@@ -45,10 +45,10 @@ tasks:
       cd ..
 
       # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz
-      tar xf zig-x86_64-linux-0.15.1.tar.xz
-      sudo mv zig-x86_64-linux-0.15.1/zig /usr/bin/
-      sudo mv zig-x86_64-linux-0.15.1/lib /usr/lib/zig
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
+      tar xf zig-x86_64-linux-0.16.0.tar.xz
+      sudo mv zig-x86_64-linux-0.16.0/zig /usr/bin/
+      sudo mv zig-x86_64-linux-0.16.0/lib /usr/lib/zig
   - build: |
       cd river
       zig build --summary all
diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml
index 521daca..86bdc89 100644
--- a/.builds/archlinux.yml
+++ b/.builds/archlinux.yml
@@ -42,10 +42,10 @@ tasks:
       cd ..
 
       # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz
-      tar xf zig-x86_64-linux-0.15.1.tar.xz
-      sudo mv zig-x86_64-linux-0.15.1/zig /usr/bin/
-      sudo mv zig-x86_64-linux-0.15.1/lib /usr/lib/zig
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
+      tar xf zig-x86_64-linux-0.16.0.tar.xz
+      sudo mv zig-x86_64-linux-0.16.0/zig /usr/bin/
+      sudo mv zig-x86_64-linux-0.16.0/lib /usr/lib/zig
   - build: |
       cd river
       zig build --summary all
diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml
index ac7d064..7d9087f 100644
--- a/.builds/freebsd.yml
+++ b/.builds/freebsd.yml
@@ -48,10 +48,10 @@ tasks:
       cd ..
 
       # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.15.1/zig-x86_64-freebsd-0.15.1.tar.xz
-      tar xf zig-x86_64-freebsd-0.15.1.tar.xz
-      sudo mv zig-x86_64-freebsd-0.15.1/zig /usr/bin/
-      sudo mv zig-x86_64-freebsd-0.15.1/lib /usr/lib/zig
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-freebsd-0.16.0.tar.xz
+      tar xf zig-x86_64-freebsd-0.16.0.tar.xz
+      sudo mv zig-x86_64-freebsd-0.16.0/zig /usr/bin/
+      sudo mv zig-x86_64-freebsd-0.16.0/lib /usr/lib/zig
   - build: |
       cd river
       zig build --summary all
diff --git a/.gitignore b/.gitignore
index ca6f198..f6ce2cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
 
 .zig-cache/
 zig-out/
+zig-pkg/
diff --git a/README.md b/README.md
index bf898af..5e94c53 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ To compile river first ensure that you have the following dependencies
 installed. The "development" versions are required if applicable to your
 distribution.
 
-- [zig](https://ziglang.org/download/) 0.15
+- [zig](https://ziglang.org/download/) 0.16
 - wayland
 - wayland-protocols
 - [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots) 0.20
diff --git a/build.zig b/build.zig
index 46b515b..b9e4931 100644
--- a/build.zig
+++ b/build.zig
@@ -31,7 +31,6 @@ pub fn build(b: *Build) !void {
     ) orelse scdoc_found: {
         _ = b.findProgram(&.{"scdoc"}, &.{}) catch |err| switch (err) {
             error.FileNotFound => break :scdoc_found false,
-            else => return err,
         };
         break :scdoc_found true;
     };
@@ -51,7 +50,7 @@ pub fn build(b: *Build) !void {
             const git_describe_long = b.runAllowFail(
                 &.{ "git", "-C", b.build_root.path orelse ".", "describe", "--long" },
                 &ret,
-                .Ignore,
+                .ignore,
             ) catch break :blk version;
 
             var it = mem.splitSequence(u8, mem.trim(u8, git_describe_long, &std.ascii.whitespace), "-");
@@ -158,12 +157,13 @@ pub fn build(b: *Build) !void {
                 .root_source_file = b.path("river/main.zig"),
                 .target = target,
                 .optimize = optimize,
+
                 .strip = strip,
+                .link_libc = true,
             }),
         });
         river.root_module.addOptions("build_options", options);
 
-        river.root_module.link_libc = true;
         river.root_module.linkSystemLibrary("libevdev", .{});
         river.root_module.linkSystemLibrary("libinput", .{});
         river.root_module.linkSystemLibrary("wayland-server", .{});
@@ -179,7 +179,7 @@ pub fn build(b: *Build) !void {
         river.root_module.addImport("slotmap", slotmap);
         river.root_module.addImport("deque", deque);
 
-        river.addCSourceFile(.{
+        river.root_module.addCSourceFile(.{
             .file = b.path("river/wlroots_log_wrapper.c"),
             .flags = &.{ "-std=c99", "-O2" },
         });
@@ -223,7 +223,7 @@ pub fn build(b: *Build) !void {
             // This makes the caching work for the Workaround, and the extra argument is ignored by /bin/sh.
             scdoc.addFileArg(b.path("doc/" ++ page ++ ".1.scd"));
 
-            const stdout = scdoc.captureStdOut();
+            const stdout = scdoc.captureStdOut(.{});
             b.getInstallStep().dependOn(&b.addInstallFile(stdout, "share/man/man1/" ++ page ++ ".1").step);
         }
     }
diff --git a/build.zig.zon b/build.zig.zon
index 32e1b64..5918d5d 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -16,8 +16,8 @@
             .hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX",
         },
         .wayland = .{
-            .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.5.0.tar.gz",
-            .hash = "wayland-0.5.0-lQa1knz8AQCh08NA8BeQrwJB9U3CfqcVAdHZYGRKIGuu",
+            .url = "git+https://codeberg.org/ifreund/zig-wayland#4a150a04f76f7329e80280661355c04328369d1f",
+            .hash = "wayland-0.6.0-dev-lQa1krD8AQBlMqwuhAMJjPQKXvpRByZBxxqMVAZ7yzbG",
         },
         .wlroots = .{
             .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.20.0.tar.gz",
diff --git a/common/flags.zig b/common/flags.zig
index f7f9536..72f197f 100644
--- a/common/flags.zig
+++ b/common/flags.zig
@@ -1,71 +1,51 @@
 // SPDX-FileCopyrightText: © 2023 Isaac Freund
 // SPDX-License-Identifier: 0BSD
 
-//! Zero allocation argument parsing for unix-like systems.
-
 const std = @import("std");
 const mem = std.mem;
 
 pub const Flag = struct {
-    name: [:0]const u8,
+    name: []const u8,
     kind: enum { boolean, arg },
 };
 
-pub fn parser(comptime Arg: type, comptime flags: []const Flag) type {
-    switch (Arg) {
-        // TODO consider allowing []const u8
-        [:0]const u8, [*:0]const u8 => {}, // ok
-        else => @compileError("invalid argument type: " ++ @typeName(Arg)),
-    }
+pub fn parser(comptime flags: []const Flag) type {
     return struct {
         pub const Result = struct {
             /// Remaining args after the recognized flags
-            args: []const Arg,
+            args: []const [:0]const u8,
             /// Data obtained from parsed flags
             flags: Flags,
 
             pub const Flags = flags_type: {
-                var fields: []const std.builtin.Type.StructField = &.{};
-                for (flags) |flag| {
-                    const field: std.builtin.Type.StructField = switch (flag.kind) {
-                        .boolean => .{
-                            .name = flag.name,
-                            .type = bool,
-                            .default_value_ptr = &false,
-                            .is_comptime = false,
-                            .alignment = @alignOf(bool),
+                const Attributes = std.builtin.Type.StructField.Attributes;
+                var names: [flags.len][]const u8 = undefined;
+                var types: [flags.len]type = undefined;
+                var attrs: [flags.len]Attributes = undefined;
+                for (flags, &names, &types, &attrs) |flag, *name, *ty, *attr| {
+                    name.* = flag.name;
+                    switch (flag.kind) {
+                        .boolean => {
+                            ty.* = bool;
+                            attr.* = .{ .default_value_ptr = &false };
                         },
-                        .arg => .{
-                            .name = flag.name,
-                            .type = ?[:0]const u8,
-                            .default_value_ptr = &@as(?[:0]const u8, null),
-                            .is_comptime = false,
-                            .alignment = @alignOf(?[:0]const u8),
+                        .arg => {
+                            ty.* = ?[:0]const u8;
+                            attr.* = .{ .default_value_ptr = &@as(ty.*, null) };
                         },
-                    };
-                    fields = fields ++ [_]std.builtin.Type.StructField{field};
+                    }
                 }
-                break :flags_type @Type(.{ .@"struct" = .{
-                    .layout = .auto,
-                    .fields = fields,
-                    .decls = &.{},
-                    .is_tuple = false,
-                } });
+                break :flags_type @Struct(.auto, null, &names, &types, &attrs);
             };
         };
 
-        pub fn parse(args: []const Arg) !Result {
+        pub fn parse(args: []const [:0]const u8) error{MissingFlagArgument}!Result {
             var result_flags: Result.Flags = .{};
 
             var i: usize = 0;
             outer: while (i < args.len) : (i += 1) {
-                const arg = switch (Arg) {
-                    [*:0]const u8 => mem.sliceTo(args[i], 0),
-                    [:0]const u8 => args[i],
-                    else => unreachable,
-                };
                 inline for (flags) |flag| {
-                    if (mem.eql(u8, "-" ++ flag.name, arg)) {
+                    if (mem.eql(u8, "-" ++ flag.name, args[i])) {
                         switch (flag.kind) {
                             .boolean => @field(result_flags, flag.name) = true,
                             .arg => {
@@ -75,11 +55,7 @@ pub fn parser(comptime Arg: type, comptime flags: []const Flag) type {
                                         "' requires an argument but none was provided!", .{});
                                     return error.MissingFlagArgument;
                                 }
-                                @field(result_flags, flag.name) = switch (Arg) {
-                                    [*:0]const u8 => mem.sliceTo(args[i], 0),
-                                    [:0]const u8 => args[i],
-                                    else => unreachable,
-                                };
+                                @field(result_flags, flag.name) = args[i];
                             },
                         }
                         continue :outer;
diff --git a/river/InputPopup.zig b/river/InputPopup.zig
index 7e5adc1..c7c5dc2 100644
--- a/river/InputPopup.zig
+++ b/river/InputPopup.zig
@@ -97,7 +97,7 @@ pub fn update(input_popup: *InputPopup) void {
         .window => |window| window.popup_tree,
         .shell_surface => |shell_surface| shell_surface.popup_tree,
         // TODO fix positioning for lock surfaces not at 0,0 in layout coords
-        .lock_surface => |_| server.scene.layers.popups,
+        .lock_surface => server.scene.layers.popups,
         .layer_surface => |layer_surface| layer_surface.popup_tree,
         // Xwayland doesn't use the text-input protocol
         .override_redirect => unreachable,
diff --git a/river/Output.zig b/river/Output.zig
index 9d56930..2ed7273 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -449,7 +449,7 @@ fn handleFrame(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) voi
         error.CommitFailed => log.err("output commit failed for {s}", .{wlr_output.name}),
     };
 
-    var now = posix.clock_gettime(posix.CLOCK.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported");
+    var now = util.timestamp();
     output.scene_output.?.sendFrameDone(&now);
 }
 
diff --git a/river/Server.zig b/river/Server.zig
index 592b915..31f9a6d 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -128,8 +128,8 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
 
     server.* = .{
         .wl_server = wl_server,
-        .sigint_source = try loop.addSignal(*wl.Server, posix.SIG.INT, terminate, wl_server),
-        .sigterm_source = try loop.addSignal(*wl.Server, posix.SIG.TERM, terminate, wl_server),
+        .sigint_source = try loop.addSignal(*wl.Server, @intFromEnum(posix.SIG.INT), terminate, wl_server),
+        .sigterm_source = try loop.addSignal(*wl.Server, @intFromEnum(posix.SIG.TERM), terminate, wl_server),
 
         .fixes = try wlr.Fixes.create(wl_server, 1),
 
@@ -281,20 +281,6 @@ pub fn deinit(server: *Server) void {
     server.wl_server.destroy();
 }
 
-/// Create the socket, start the backend, and setup the environment
-pub fn start(server: Server) !void {
-    var buf: [11]u8 = undefined;
-    const socket = try server.wl_server.addSocketAuto(&buf);
-    try server.backend.start();
-    // TODO: don't use libc's setenv
-    if (c.setenv("WAYLAND_DISPLAY", socket.ptr, 1) < 0) return error.SetenvError;
-    if (build_options.xwayland) {
-        if (server.xwayland) |xwayland| {
-            if (c.setenv("DISPLAY", xwayland.display_name, 1) < 0) return error.SetenvError;
-        }
-    }
-}
-
 fn globalFilter(client: *const wl.Client, global: *const wl.Global, server: *Server) bool {
     // Only expose the xwalyand_shell_v1 global to the Xwayland process.
     if (build_options.xwayland) {
@@ -507,7 +493,7 @@ fn handleRequestActivate(
 ) void {
     const node_data = SceneNodeData.fromSurface(event.surface) orelse return;
     switch (node_data.data) {
-        .window => |_| {}, // TODO support xdg-activation with a rwm extension protocol
+        .window => {}, // TODO support xdg-activation with a rwm extension protocol
         else => |tag| {
             log.info("ignoring xdg-activation-v1 activate request of {s} surface", .{@tagName(tag)});
         },
diff --git a/river/Window.zig b/river/Window.zig
index 45037b9..fb91514 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -1083,7 +1083,7 @@ pub fn sendFrameDone(window: Window) void {
     assert(window.state == .mapped);
     assert(window.impl != .destroying);
 
-    var now = posix.clock_gettime(posix.CLOCK.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported");
+    var now = util.timestamp();
     window.rootSurface().?.sendFrameDone(&now);
 }
 
diff --git a/river/XkbConfig.zig b/river/XkbConfig.zig
index cc81cdc..159604e 100644
--- a/river/XkbConfig.zig
+++ b/river/XkbConfig.zig
@@ -125,14 +125,13 @@ fn handleRequest(
 fn createKeymap(object: *river.XkbConfigV1, id: u32, format: xkb.Keymap.Format, fd: i32) !void {
     defer _ = std.c.close(fd);
 
-    var stat: std.c.Stat = std.mem.zeroes(std.c.Stat);
-    switch (std.posix.errno(std.c.fstat(fd, &stat))) {
-        .SUCCESS => {},
-        else => |e| {
-            log.err("failed to fstat() keymap fd: E{s}", .{@tagName(e)});
-            return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "failed to fstat() keymap fd");
-        },
-    }
+    const io = std.Io.Threaded.global_single_threaded.io();
+    var file: std.Io.File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
+    const stat = file.stat(io) catch |err| {
+        log.err("failed to stat keymap fd: {}", .{err});
+        return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "failed to stat keymap fd");
+    };
+
     // Must be zero terminated
     if (stat.size < 1) {
         log.err("keymap too small", .{});
@@ -144,7 +143,7 @@ fn createKeymap(object: *river.XkbConfigV1, id: u32, format: xkb.Keymap.Format,
     }
     const keymap_len: usize = @intCast(stat.size - 1);
 
-    const keymap_ptr = std.c.mmap(null, keymap_len, std.c.PROT.READ, .{ .TYPE = .PRIVATE }, fd, 0);
+    const keymap_ptr = std.c.mmap(null, keymap_len, .{ .READ = true }, .{ .TYPE = .PRIVATE }, fd, 0);
     if (keymap_ptr == std.c.MAP_FAILED) {
         log.err("failed to mmap() keymap fd: {s}", .{@tagName(@as(std.c.E, @enumFromInt(std.c._errno().*)))});
         return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "failed to mmap() keymap fd");
diff --git a/river/c.zig b/river/c.zig
index 590de7a..aead518 100644
--- a/river/c.zig
+++ b/river/c.zig
@@ -2,13 +2,7 @@
 // SPDX-License-Identifier: GPL-3.0-only
 
 pub const c = @cImport({
-    @cDefine("_POSIX_C_SOURCE", "200809L");
-
-    @cInclude("stdlib.h");
-    @cInclude("unistd.h");
-
     @cInclude("linux/input-event-codes.h");
     @cInclude("libevdev/libevdev.h");
-
     @cInclude("libinput.h");
 });
diff --git a/river/main.zig b/river/main.zig
index a0966b7..195fa06 100644
--- a/river/main.zig
+++ b/river/main.zig
@@ -5,9 +5,11 @@ const build_options = @import("build_options");
 const std = @import("std");
 const mem = std.mem;
 const fs = std.fs;
-const io = std.io;
+const Io = std.Io;
 const log = std.log;
 const posix = std.posix;
+const exit = std.process.exit;
+
 const builtin = @import("builtin");
 const wlr = @import("wlroots");
 const flags = @import("flags");
@@ -18,6 +20,8 @@ const process = @import("process.zig");
 
 const Server = @import("Server.zig");
 
+const io = Io.Threaded.global_single_threaded.io();
+
 const usage: []const u8 =
     \\usage: river [options]
     \\
@@ -36,35 +40,41 @@ const full_version = std.fmt.comptimePrint("{s} {c}xwayland", .{
 
 pub var server: Server = undefined;
 
-pub fn main() anyerror!void {
-    const result = flags.parser([*:0]const u8, &.{
+pub fn main(init: std.process.Init.Minimal) anyerror!void {
+    var arena_alloc = std.heap.ArenaAllocator.init(util.gpa);
+    defer arena_alloc.deinit();
+    const arena = arena_alloc.allocator();
+
+    const args = try init.args.toSlice(arena);
+
+    const result = flags.parser(&.{
         .{ .name = "h", .kind = .boolean },
         .{ .name = "version", .kind = .boolean },
         .{ .name = "c", .kind = .arg },
         .{ .name = "log-level", .kind = .arg },
         .{ .name = "log-scopes", .kind = .arg },
         .{ .name = "no-xwayland", .kind = .boolean },
-    }).parse(std.os.argv[1..]) catch {
+    }).parse(args[1..]) catch {
         try stderr.writeAll(usage);
         try stderr.flush();
-        posix.exit(1);
+        exit(1);
     };
     if (result.flags.h) {
         try stdout.writeAll(usage);
         try stdout.flush();
-        posix.exit(0);
+        exit(0);
     }
     if (result.args.len != 0) {
         log.err("unknown option '{s}'", .{result.args[0]});
         try stderr.writeAll(usage);
         try stderr.flush();
-        posix.exit(1);
+        exit(1);
     }
 
     if (result.flags.version) {
         try stdout.writeAll(full_version ++ "\n");
         try stdout.flush();
-        posix.exit(0);
+        exit(0);
     }
     if (result.flags.@"log-level") |level| {
         if (mem.eql(u8, level, "error")) {
@@ -77,7 +87,7 @@ pub fn main() anyerror!void {
             runtime_log_level = .debug;
         } else {
             log.err("invalid log level '{s}'", .{level});
-            posix.exit(1);
+            exit(1);
         }
     }
     if (result.flags.@"log-scopes") |scopes| {
@@ -94,13 +104,13 @@ pub fn main() anyerror!void {
                 // former requires quoting in most shells.
                 const scope = std.meta.stringToEnum(LogScope, raw[1..]) orelse {
                     log.err("invalid log scope '{s}'", .{raw});
-                    posix.exit(1);
+                    exit(1);
                 };
                 log_scopes.remove(scope);
             } else {
                 const scope = std.meta.stringToEnum(LogScope, raw) orelse {
                     log.err("invalid log scope '{s}'", .{raw});
-                    posix.exit(1);
+                    exit(1);
                 };
                 log_scopes.insert(scope);
             }
@@ -111,9 +121,10 @@ pub fn main() anyerror!void {
         if (result.flags.c) |command| {
             break :blk try util.gpa.dupeZ(u8, command);
         } else {
-            break :blk try defaultInitPath();
+            break :blk try defaultInitPath(init.environ);
         }
     };
+    defer if (startup_command) |cmd| util.gpa.free(cmd);
 
     try detectClassic(startup_command);
 
@@ -141,17 +152,47 @@ pub fn main() anyerror!void {
 
     process.setup();
 
-    try server.start();
+    var buf: [11]u8 = undefined;
+    const socket = try server.wl_server.addSocketAuto(&buf);
+    try server.backend.start();
 
     // Run the child in a new process group so that we can send SIGTERM to all
     // descendants on exit.
     const child_pgid = if (startup_command) |cmd| blk: {
         log.info("running init executable '{s}'", .{cmd});
+
+        var env_map = try init.environ.createMap(util.gpa);
+        defer env_map.deinit();
+
+        try env_map.put("WAYLAND_DISPLAY", socket);
+
+        if (build_options.xwayland) {
+            if (server.xwayland) |xwayland| {
+                try env_map.put("DISPLAY", mem.sliceTo(xwayland.display_name, 0));
+            }
+        }
+
+        const env_block = try env_map.createPosixBlock(util.gpa, .{});
+        defer env_block.deinit(util.gpa);
+
         const child_args = [_:null]?[*:0]const u8{ "/bin/sh", "-c", cmd, null };
-        const pid = try posix.fork();
+
+        const pid: posix.pid_t = fork: {
+            const rc = posix.system.fork();
+            switch (posix.errno(rc)) {
+                .SUCCESS => break :fork @intCast(rc),
+                .AGAIN => return error.SystemResources,
+                .NOMEM => return error.SystemResources,
+                .NOSYS => return error.OperationUnsupported,
+                else => |err| return posix.unexpectedErrno(err),
+            }
+        };
+
         if (pid == 0) {
             process.cleanupChild();
-            posix.execveZ("/bin/sh", &child_args, std.c.environ) catch c._exit(1);
+            if (posix.errno(posix.system.execve("/bin/sh", &child_args, env_block.slice.ptr)) != .SUCCESS) {
+                posix.system.exit(1);
+            }
         }
         util.gpa.free(cmd);
         // Since the child has called setsid, the pid is the pgid
@@ -166,22 +207,22 @@ pub fn main() anyerror!void {
     server.wl_server.run();
 }
 
-fn defaultInitPath() !?[:0]const u8 {
+fn defaultInitPath(environ: std.process.Environ) !?[:0]const u8 {
     const path = blk: {
-        if (posix.getenv("XDG_CONFIG_HOME")) |xdg_config_home| {
+        if (environ.getPosix("XDG_CONFIG_HOME")) |xdg_config_home| {
             break :blk try fs.path.joinZ(util.gpa, &[_][]const u8{ xdg_config_home, "river/init" });
-        } else if (posix.getenv("HOME")) |home| {
+        } else if (environ.getPosix("HOME")) |home| {
             break :blk try fs.path.joinZ(util.gpa, &[_][]const u8{ home, ".config/river/init" });
         } else {
             return null;
         }
     };
 
-    posix.accessZ(path, posix.X_OK) catch |err| {
+    Io.Dir.cwd().access(io, path, .{ .execute = true }) catch |err| {
         if (err == error.PermissionDenied) {
-            if (posix.accessZ(path, posix.R_OK)) {
+            if (Io.Dir.cwd().access(io, path, .{})) {
                 log.err("failed to run init executable {s}: the file is not executable", .{path});
-                posix.exit(1);
+                exit(1);
             } else |_| {}
         }
         log.err("failed to run init executable {s}: {s}", .{ path, @errorName(err) });
@@ -215,15 +256,15 @@ fn detectClassic(startup_command: ?[:0]const u8) !void {
             .version = build_options.version,
         });
         try stderr.flush();
-        posix.exit(1);
+        exit(1);
     }
 }
 
 fn grepRiverctl(path: [:0]const u8) !bool {
-    var file = try std.fs.cwd().openFileZ(path, .{});
-    defer file.close();
+    var file = try Io.Dir.cwd().openFile(io, path, .{});
+    defer file.close(io);
     var buffer: [1024]u8 = undefined;
-    var file_reader = file.reader(&buffer);
+    var file_reader = file.reader(io, &buffer);
     const reader = &file_reader.interface;
     while (true) {
         _ = try reader.discardDelimiterExclusive('r');
@@ -239,11 +280,11 @@ fn grepRiverctl(path: [:0]const u8) !bool {
 }
 
 var stderr_buffer: [1024]u8 = undefined;
-var stderr_writer = fs.File.stderr().writer(&stderr_buffer);
+var stderr_writer = Io.File.stderr().writer(io, &stderr_buffer);
 const stderr = &stderr_writer.interface;
 
 var stdout_buffer: [1024]u8 = undefined;
-var stdout_writer = fs.File.stdout().writer(&stdout_buffer);
+var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
 const stdout = &stdout_writer.interface;
 
 // Scopes should be added to this list sparingly.
diff --git a/river/process.zig b/river/process.zig
index 809b89f..d6e8793 100644
--- a/river/process.zig
+++ b/river/process.zig
@@ -4,8 +4,6 @@
 const std = @import("std");
 const posix = std.posix;
 
-const c = @import("c.zig").c;
-
 var original_rlimit: ?posix.rlimit = null;
 
 pub fn setup() void {
@@ -47,7 +45,7 @@ pub fn setup() void {
 }
 
 pub fn cleanupChild() void {
-    if (c.setsid() < 0) unreachable;
+    if (std.c.setsid() < 0) unreachable;
     if (posix.system.sigprocmask(posix.SIG.SETMASK, &posix.sigemptyset(), null) < 0) unreachable;
 
     const sig_dfl = posix.Sigaction{
diff --git a/river/util.zig b/river/util.zig
index a713e46..79679b9 100644
--- a/river/util.zig
+++ b/river/util.zig
@@ -2,12 +2,21 @@
 // SPDX-License-Identifier: GPL-3.0-only
 
 const std = @import("std");
+const posix = std.posix;
 
 /// The global general-purpose allocator used throughout river's code
 pub const gpa = std.heap.c_allocator;
 
+pub fn timestamp() posix.timespec {
+    var timespec: posix.timespec = undefined;
+    switch (posix.errno(posix.system.clock_gettime(posix.CLOCK.MONOTONIC, &timespec))) {
+        .SUCCESS => return timespec,
+        else => @panic("CLOCK_MONOTONIC not supported"),
+    }
+}
+
 pub fn msecTimestamp() u32 {
-    const now = std.posix.clock_gettime(.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported");
+    const now = timestamp();
     // 2^32-1 milliseconds is ~50 days, which is a realistic uptime.
     // This means that we must wrap if the monotonic time is greater than
     // 2^32-1 milliseconds and hope that clients don't get too confused.