Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,48 @@ jobs:
- name: Run Deno lint
run: deno lint

# Independent of the rust job and intentionally absent from build/playwright
# needs: so Zig work cannot extend the critical path.
zig:
name: Zig Lint, Test, and Build
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 10

steps:
- uses: actions/checkout@v6

- name: Read pinned Zig toolchain from .tool-versions
id: zig-version
shell: bash
run: |
version=$(grep '^zig ' .tool-versions | awk '{print $2}')
echo "toolchain=${version}" >> "$GITHUB_OUTPUT"

- name: Restore Zig build cache
uses: actions/cache@v4
with:
path: |
zig-core/.zig-cache
zig-core/zig-out
key: zig-build-${{ runner.os }}-${{ steps.zig-version.outputs.toolchain }}-${{ hashFiles('zig-core/build.zig', 'zig-core/build.zig.zon', 'zig-core/src/**/*.zig', '.tool-versions') }}

- name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: ${{ steps.zig-version.outputs.toolchain }}

- name: Install Task
uses: go-task/setup-task@v1

- name: Zig format check
run: task zig:lint

- name: Zig tests
run: task zig:test

- name: Zig build
run: task zig:build

build:
name: Build (${{ matrix.platform }})
# Gate on lint/format/test jobs so builds don't burn CI minutes
Expand Down
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ prek 0.3.2
ruff 0.12.11
uv 0.9.24
hadolint 2.14.0
zig 0.15.2
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
id: TASK-355.1
title: 'Zig toolchain, Taskfile integration, and CI job'
status: To Do
status: In Progress
assignee: []
created_date: '2026-09-11 00:38'
labels: []
Expand All @@ -19,10 +19,29 @@ The Zig toolchain must be reproducibly available to both local developers and CI

## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 zig 0.15.2 is pinned in .tool-versions
- [ ] #2 taskfiles/zig.yml provides format, lint, test, build, and cross-compile tasks following the structure of taskfiles/cargo.yml
- [ ] #3 A missing or version-mismatched zig installation fails with an actionable error message rather than silently using the wrong binary
- [ ] #4 task lint, task format, and task test invoke the Zig equivalents alongside the existing Rust and Deno ones
- [ ] #5 A new zig CI job runs on a Blacksmith Linux runner in parallel with the existing rust job and does not extend the critical path
- [x] #1 zig 0.15.2 is pinned in .tool-versions
- [x] #2 taskfiles/zig.yml provides format, lint, test, build, and cross-compile tasks following the structure of taskfiles/cargo.yml
- [x] #3 A missing or version-mismatched zig installation fails with an actionable error message rather than silently using the wrong binary
- [x] #4 task lint, task format, and task test invoke the Zig equivalents alongside the existing Rust and Deno ones
- [x] #5 A new zig CI job runs on a Blacksmith Linux runner in parallel with the existing rust job and does not extend the critical path
- [ ] #6 Both a cold-cache and a warm-cache run of the zig job are measured and the timings recorded on this task
<!-- AC:END -->

## Plan
<!-- SECTION:PLAN:BEGIN -->
See PROMPT.md decisions on the branch: `zig-core/` placeholder executable (no SQLite/HTTP — that is TASK-355.2), zig resolved through mise against the pinned version rather than PATH, and a standalone `zig` CI job that no other job depends on.
<!-- SECTION:PLAN:END -->

## Notes
<!-- SECTION:NOTES:BEGIN -->
### Local verification (2026-09-11)

`mise install zig` -> 0.15.2, then all green against the placeholder `zig-core/`:
`task zig:format`, `task zig:lint`, `task zig:test` (1/1 tests passed), `task zig:build`, plus `task zig:cross-compile TARGET=aarch64-macos|x86_64-macos|x86_64-linux|x86_64-windows`. `task --list` shows the six `zig:*` tasks, and root `task format` / `task lint` / `task test` now run the zig step last (the Vitest step in root `task test` fails on 17 pre-existing frontend failures in `context-menu-favorites`, `go-to-album`, `go-to-artist` and `library.store` tests — untouched by this task).

AC#3 was exercised by hand: a pinned version that is not installed, a resolved binary reporting a different version, and `mise` absent from PATH each produce their own error naming the expected version, what was found, and the `mise install zig` fix.

### AC#6 still open — needs a push

Cold/warm cache timings cannot be measured from this unpushed worktree. The job caches `zig-core/.zig-cache` and `zig-core/zig-out` under a key of runner OS + pinned version + `hashFiles(build.zig, build.zig.zon, src/**/*.zig, .tool-versions)`, so the measurement is ready to take: push this branch, run the `zig` job twice (first run cold, second warm with unchanged inputs), and record both durations from the Actions UI here.
<!-- SECTION:NOTES:END -->
7 changes: 6 additions & 1 deletion taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ includes:
taskfile: ./taskfiles/deno.yml
cargo:
taskfile: ./taskfiles/cargo.yml
zig:
taskfile: ./taskfiles/zig.yml
tauri:
taskfile: ./taskfiles/tauri.yml
ci:
Expand All @@ -51,18 +53,21 @@ tasks:
cmds:
- task: cargo:lint
- task: deno:lint
- task: zig:lint

format:
desc: "Run formatters"
cmds:
- task: cargo:format
- task: deno:format
- task: zig:format

test:
desc: "Run tests (Rust + Frontend)"
desc: "Run tests (Rust + Frontend + Zig)"
cmds:
- task: cargo:test
- task: deno:test
- task: zig:test

test:e2e:
desc: "Run Playwright E2E tests"
Expand Down
107 changes: 107 additions & 0 deletions taskfiles/zig.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
version: "3.0"

set: ['e', 'u', 'pipefail']
shopt: ['globstar']

vars:
ZIG_CORE_DIR: "{{.ROOT_DIR}}/zig-core"
ZIG_OUT_DIR: "{{.ZIG_CORE_DIR}}/zig-out"
ZIG_CACHE_DIR: "{{.ZIG_CORE_DIR}}/.zig-cache"
PINNED_ZIG:
sh: grep '^zig ' {{.ROOT_DIR}}/.tool-versions 2>/dev/null | awk '{print $2}' || true
# Resolved through mise against the pinned version rather than PATH, which may
# hold an IDE-bundled zig or a mise default for a different version.
ZIG:
sh: |
pin="{{.PINNED_ZIG}}"
if [ -n "$pin" ] && command -v mise &> /dev/null; then
mise which zig --tool "zig@$pin" 2>/dev/null || true
fi
# Explicit target triple for the cross-compile task, e.g. TARGET=aarch64-macos
TARGET: '{{.TARGET | default ""}}'

tasks:
_check-zig-version:
internal: true
desc: "Verify the pinned zig resolves and matches .tool-versions"
cmds:
- |
mise_bin="{{.ZIG}}"
if [ ! -x "$mise_bin" ]; then
if ! command -v mise &> /dev/null; then
echo "error: zig {{.PINNED_ZIG}} not found and mise is not on PATH." >&2
echo " Expected: {{.PINNED_ZIG}} (pinned in .tool-versions)" >&2
echo " Fix: install mise (https://mise.jdx.dev), then run: mise install zig" >&2
exit 1
fi
echo "error: zig {{.PINNED_ZIG}} (pinned in .tool-versions) is not installed." >&2
echo " Fix: mise install zig" >&2
exit 1
fi
found=$("$mise_bin" version 2>/dev/null)
if [ "$found" != "{{.PINNED_ZIG}}" ]; then
echo "error: zig version mismatch." >&2
echo " Expected: {{.PINNED_ZIG}} (pinned in .tool-versions)" >&2
echo " Found: ${found:-unknown} ($mise_bin)" >&2
echo " Fix: mise install zig@{{.PINNED_ZIG}}" >&2
exit 1
fi
echo "zig $found -> $mise_bin"

format:
desc: "Format Zig source files"
deps: [_check-zig-version]
cmds:
- '{{.ZIG}} fmt {{.ZIG_CORE_DIR}}/'

lint:
desc: "Check Zig formatting without making changes"
deps: [_check-zig-version]
cmds:
- '{{.ZIG}} fmt --check {{.ZIG_CORE_DIR}}/'

test:
desc: "Run Zig unit tests"
deps: [_check-zig-version]
cmds:
- cd {{.ZIG_CORE_DIR}} && {{.ZIG}} build test

build:
desc: "Build zig-core sidecar (debug)"
deps: [_check-zig-version]
cmds:
- cd {{.ZIG_CORE_DIR}} && {{.ZIG}} build
sources:
- "{{.ZIG_CORE_DIR}}/src/**/*.zig"
- "{{.ZIG_CORE_DIR}}/build.zig"
- "{{.ZIG_CORE_DIR}}/build.zig.zon"
generates:
- "{{.ZIG_OUT_DIR}}/bin/mt-zig-core"

cross-compile:
desc: "Cross-compile zig-core for a Zig target triple (TARGET required)"
summary: |
Cross-compile the zig-core sidecar for an explicit Zig target triple.

task zig:cross-compile TARGET=aarch64-macos
task zig:cross-compile TARGET=x86_64-macos
task zig:cross-compile TARGET=x86_64-linux
task zig:cross-compile TARGET=x86_64-windows
deps: [_check-zig-version]
preconditions:
- sh: test -n "{{.TARGET}}"
msg: |
TARGET is not set. Pass a Zig target triple, e.g.:
task zig:cross-compile TARGET=aarch64-macos
task zig:cross-compile TARGET=x86_64-macos
task zig:cross-compile TARGET=x86_64-linux
task zig:cross-compile TARGET=x86_64-windows
cmds:
- cd {{.ZIG_CORE_DIR}} && {{.ZIG}} build -Dtarget={{.TARGET}}

clean:
desc: "Clean Zig build artifacts"
cmds:
- rm -rf {{.ZIG_OUT_DIR}}
- rm -rf {{.ZIG_CACHE_DIR}}
silent: true
37 changes: 37 additions & 0 deletions zig-core/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

// Sidecar executable. Metadata parsing stays in crates/mt-core until the
// Zig core is vendored in (TASK-355.2).
const exe = b.addExecutable(.{
.name = "mt-zig-core",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});

b.installArtifact(exe);

const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Run the sidecar");
run_step.dependOn(&run_cmd.step);

const exe_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});

const run_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
}
11 changes: 11 additions & 0 deletions zig-core/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.{
.name = .mt_zig_core,
.version = "0.0.0",
.fingerprint = 0x644b78534ae9e63e, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}
10 changes: 10 additions & 0 deletions zig-core/src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const std = @import("std");

pub fn main() !void {
// Prints to stderr, ignoring potential errors.
std.debug.print("mt-zig-core\n", .{});
}

test "sanity" {
try std.testing.expect(1 + 1 == 2);
}
Loading