repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
fac9795
parent
7cdb333
author
Eric Bower
date
2026-03-23 14:16:32 -0400 EDT
chore: dockerfile and ci file

I'm experimenting with using zmx as a job engine for CI.
5 files changed,  +144, -89
A .dockerignore
+2, -0
1@@ -0,0 +1,2 @@
2+.git
3+zig-out
A Dockerfile
+20, -0
 1@@ -0,0 +1,20 @@
 2+FROM alpine:3.23
 3+
 4+RUN apk add curl git
 5+
 6+ARG ZIG_VERSION=0.15.2
 7+RUN curl -L -o /tmp/zig.tar.xz https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz && \
 8+	cd /tmp && \
 9+	tar -xvf zig.tar.xz && \
10+  mv zig-x86_64-linux-${ZIG_VERSION} /usr/local/zig && \
11+  ln -s /usr/local/zig/zig /usr/local/bin/zig
12+
13+ENV PATH=/usr/local/zig:$PATH
14+
15+WORKDIR /app
16+
17+COPY build.zig build.zig.zon src/ /app/
18+
19+RUN zig build
20+
21+CMD ["zig", "build"]
M build.zig
+95, -87
  1@@ -16,9 +16,6 @@ pub fn build(b: *std.Build) void {
  2     const version = b.option([]const u8, "version", "Version string for release") orelse
  3         @as([]const u8, @import("build.zig.zon").version);
  4 
  5-    const run_step = b.step("run", "Run the app");
  6-    const test_step = b.step("test", "Run unit tests");
  7-
  8     var code: u8 = 0;
  9     const git_sha = std.mem.trim(u8, b.runAllowFail(
 10         &.{ "git", "rev-parse", "--short", "HEAD" },
 11@@ -29,7 +26,8 @@ pub fn build(b: *std.Build) void {
 12     const options = b.addOptions();
 13     options.addOption([]const u8, "version", version);
 14     options.addOption([]const u8, "git_sha", git_sha);
 15-    options.addOption([]const u8, "ghostty_version", @import("build.zig.zon").dependencies.ghostty.hash);
 16+    const ghostty_ver = @import("build.zig.zon").dependencies.ghostty.hash;
 17+    options.addOption([]const u8, "ghostty_version", ghostty_ver);
 18 
 19     const exe_mod = b.createModule(.{
 20         .root_source_file = b.path("src/main.zig"),
 21@@ -38,8 +36,6 @@ pub fn build(b: *std.Build) void {
 22     });
 23     exe_mod.addOptions("build_options", options);
 24 
 25-    // You'll want to use a lazy dependency here so that ghostty is only
 26-    // downloaded if you actually need it.
 27     if (b.lazyDependency("ghostty", .{
 28         .target = target,
 29         .optimize = optimize,
 30@@ -50,97 +46,109 @@ pub fn build(b: *std.Build) void {
 31         );
 32     }
 33 
 34-    // Exe
 35-    const exe = b.addExecutable(.{
 36-        .name = "zmx",
 37-        .root_module = exe_mod,
 38-    });
 39-    exe.linkLibC();
 40-
 41-    b.installArtifact(exe);
 42-
 43     // Run
 44-    const run_cmd = b.addRunArtifact(exe);
 45-    run_cmd.step.dependOn(b.getInstallStep());
 46-    if (b.args) |args| run_cmd.addArgs(args);
 47-    run_step.dependOn(&run_cmd.step);
 48+    {
 49+        const run_step = b.step("run", "Run the app");
 50+        const exe = b.addExecutable(.{
 51+            .name = "zmx",
 52+            .root_module = exe_mod,
 53+        });
 54+        exe.linkLibC();
 55+        b.installArtifact(exe);
 56+        const run_cmd = b.addRunArtifact(exe);
 57+        run_cmd.step.dependOn(b.getInstallStep());
 58+        if (b.args) |args| run_cmd.addArgs(args);
 59+        run_step.dependOn(&run_cmd.step);
 60+    }
 61 
 62     // Test
 63-    const exe_unit_tests = b.addTest(.{
 64-        .root_module = exe_mod,
 65-    });
 66-    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
 67-    test_step.dependOn(&run_exe_unit_tests.step);
 68-
 69-    // This is where the interesting part begins.
 70-    // As you can see we are re-defining the same executable but
 71-    // we're binding it to a dedicated build step.
 72-    const exe_check = b.addExecutable(.{
 73-        .name = "zmx",
 74-        .root_module = exe_mod,
 75-    });
 76-    exe_check.linkLibC();
 77-    // There is no `b.installArtifact(exe_check);` here.
 78-
 79-    // Finally we add the "check" step which will be detected
 80-    // by ZLS and automatically enable Build-On-Save.
 81-    // If you copy this into your `build.zig`, make sure to rename 'foo'
 82-    const check = b.step("check", "Check if foo compiles");
 83-    check.dependOn(&exe_check.step);
 84-
 85-    // Release step - macOS can cross-compile to Linux, but Linux cannot cross-compile to macOS (needs SDK)
 86-    const native_os = @import("builtin").os.tag;
 87-    const release_targets = if (native_os == .macos) linux_targets ++ macos_targets else linux_targets;
 88-    const release_step = b.step("release", "Build release binaries (macOS builds all, Linux builds Linux only)");
 89-    for (release_targets) |release_target| {
 90-        const resolved = b.resolveTargetQuery(release_target);
 91-        const release_mod = b.createModule(.{
 92-            .root_source_file = b.path("src/main.zig"),
 93-            .target = resolved,
 94-            .optimize = .ReleaseSafe,
 95+    {
 96+        const test_step = b.step("test", "Run unit tests");
 97+        const exe_unit_tests = b.addTest(.{
 98+            .root_module = exe_mod,
 99         });
100-        release_mod.addOptions("build_options", options);
101-
102-        if (b.lazyDependency("ghostty", .{
103-            .target = resolved,
104-            .optimize = .ReleaseSafe,
105-        })) |dep| {
106-            release_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
107-        }
108+        const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
109+        test_step.dependOn(&run_exe_unit_tests.step);
110+    }
111 
112-        const release_exe = b.addExecutable(.{
113+    // Check for LSP integration
114+    {
115+        const check = b.step("check", "Check if zmx compiles");
116+        const exe_check = b.addExecutable(.{
117             .name = "zmx",
118-            .root_module = release_mod,
119+            .root_module = exe_mod,
120         });
121-        release_exe.linkLibC();
122-
123-        const os_name = @tagName(release_target.os_tag orelse .linux);
124-        const arch_name = @tagName(release_target.cpu_arch orelse .x86_64);
125-        const tarball_name = b.fmt("zmx-{s}-{s}-{s}.tar.gz", .{ version, os_name, arch_name });
126-
127-        const tar = b.addSystemCommand(&.{ "tar", "--no-xattrs", "-czf" });
128-
129-        const tarball = tar.addOutputFileArg(tarball_name);
130-        tar.addArg("-C");
131-        tar.addDirectoryArg(release_exe.getEmittedBinDirectory());
132-        tar.addArg("zmx");
133+        exe_check.linkLibC();
134 
135-        const shasum = b.addSystemCommand(&.{ "shasum", "-a", "256" });
136-        shasum.addFileArg(tarball);
137-        const shasum_output = shasum.captureStdOut();
138-
139-        const install_tar = b.addInstallFile(tarball, b.fmt("dist/{s}", .{tarball_name}));
140-        const install_sha = b.addInstallFile(shasum_output, b.fmt("dist/{s}.sha256", .{tarball_name}));
141-        release_step.dependOn(&install_tar.step);
142-        release_step.dependOn(&install_sha.step);
143+        // Finally we add the "check" step which will be detected
144+        // by ZLS and automatically enable Build-On-Save.
145+        // If you copy this into your `build.zig`, make sure to rename 'foo'
146+        check.dependOn(&exe_check.step);
147     }
148 
149-    // Upload step - rsync docs and dist to pgs.sh
150-    const upload_step = b.step("upload", "Upload docs and dist to pgs.sh:/zmx");
151+    // Release step - macOS can cross-compile to Linux,
152+    // but Linux cannot cross-compile to macOS (needs SDK)
153+    {
154+        const release_step = b.step(
155+            "release",
156+            "Build release binaries (macOS builds all, Linux builds Linux only)",
157+        );
158+        const native_os = @import("builtin").os.tag;
159+        const release_targets = if (native_os == .macos) linux_targets ++ macos_targets else linux_targets;
160+        for (release_targets) |release_target| {
161+            const resolved = b.resolveTargetQuery(release_target);
162+            const release_mod = b.createModule(.{
163+                .root_source_file = b.path("src/main.zig"),
164+                .target = resolved,
165+                .optimize = .ReleaseSafe,
166+            });
167+            release_mod.addOptions("build_options", options);
168+
169+            if (b.lazyDependency("ghostty", .{
170+                .target = resolved,
171+                .optimize = .ReleaseSafe,
172+            })) |dep| {
173+                release_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
174+            }
175+
176+            const release_exe = b.addExecutable(.{
177+                .name = "zmx",
178+                .root_module = release_mod,
179+            });
180+            release_exe.linkLibC();
181+
182+            const os_name = @tagName(release_target.os_tag orelse .linux);
183+            const arch_name = @tagName(release_target.cpu_arch orelse .x86_64);
184+            const tarball_name = b.fmt("zmx-{s}-{s}-{s}.tar.gz", .{ version, os_name, arch_name });
185+
186+            const tar = b.addSystemCommand(&.{ "tar", "--no-xattrs", "-czf" });
187+
188+            const tarball = tar.addOutputFileArg(tarball_name);
189+            tar.addArg("-C");
190+            tar.addDirectoryArg(release_exe.getEmittedBinDirectory());
191+            tar.addArg("zmx");
192+
193+            const shasum = b.addSystemCommand(&.{ "shasum", "-a", "256" });
194+            shasum.addFileArg(tarball);
195+            const shasum_output = shasum.captureStdOut();
196+
197+            const install_tar = b.addInstallFile(tarball, b.fmt("dist/{s}", .{tarball_name}));
198+            const install_sha = b.addInstallFile(
199+                shasum_output,
200+                b.fmt("dist/{s}.sha256", .{tarball_name}),
201+            );
202+            release_step.dependOn(&install_tar.step);
203+            release_step.dependOn(&install_sha.step);
204+        }
205+    }
206 
207-    const rsync_docs = b.addSystemCommand(&.{ "rsync", "-rv", "docs/", "pgs.sh:/zmx" });
208-    const rsync_dist = b.addSystemCommand(&.{ "rsync", "-rv", "zig-out/dist/", "pgs.sh:/zmx/a" });
209+    // Upload artifacts to pgs
210+    {
211+        const upload_step = b.step("upload", "Upload docs and dist to pgs.sh:/zmx");
212+        const rsync_docs = b.addSystemCommand(&.{ "rsync", "-rv", "docs/", "pgs.sh:/zmx" });
213+        const rsync_dist = b.addSystemCommand(&.{ "rsync", "-rv", "zig-out/dist/", "pgs.sh:/zmx/a" });
214 
215-    upload_step.dependOn(&rsync_docs.step);
216-    upload_step.dependOn(&rsync_dist.step);
217+        upload_step.dependOn(&rsync_docs.step);
218+        upload_step.dependOn(&rsync_dist.step);
219+    }
220 }
A pico.sh
+19, -0
 1@@ -0,0 +1,19 @@
 2+#!/usr/bin/env bash
 3+set -xeo pipefail
 4+
 5+# This is a little experiement seeing how we could use zmx as a job engine for CI
 6+
 7+export ZMX_SESSION_PREFIX="ci-"
 8+
 9+zmx run build podman build -t zig .
10+zmx wait build
11+
12+zmx run fmt podman run --rm -it -v "$(pwd)":/app zig zig fmt --check .
13+zmx run test podman run --rm -it -v "$(pwd)":/app zig zig build test --summary all
14+zmx wait fmt test
15+
16+zmx kill build
17+zmx kill fmt
18+zmx kill test
19+
20+echo "success!"
M src/main.zig
+8, -2
 1@@ -835,10 +835,12 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
 2                 continue;
 3             }
 4             if (session.task_ended_at == 0) {
 5-                try stdout.print("still waiting task={s}\n", .{session.name});
 6+                try stdout.print("waiting task={s}\n", .{session.name});
 7                 try stdout.flush();
 8                 continue;
 9             }
10+            try stdout.print("completed task={s} exit_code={d}\n", .{ session.name, session.task_exit_code.? });
11+            try stdout.flush();
12             if (session.task_exit_code != 0) {
13                 agg_exit_code = session.task_exit_code orelse 0;
14             }
15@@ -865,7 +867,11 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
16         max_seen = total;
17 
18         if (total > 0 and total == done) {
19-            try stdout.print("tasks completed!\n", .{});
20+            if (agg_exit_code == 0) {
21+                try stdout.print("task(s) completed!\n", .{});
22+            } else {
23+                try stdout.print("task(s) failed!\n", .{});
24+            }
25             try stdout.flush();
26             std.process.exit(agg_exit_code);
27             return;