Embeddable program-update library for Zig host apps.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker 88780d7903 docs: repoint links to git.urverk.org
The referenced repos have migrated; their codeberg copies are deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 11:41:42 +02:00
examples/demo demo: package v0.0.2 + point manifest at it 2026-05-03 18:46:53 +02:00
src snapshot: refresh download byte counters from live Progress atomics 2026-05-03 20:18:28 +02:00
test fix manifests after botched regex (aarch64-macos block was eaten) 2026-05-03 20:22:30 +02:00
.gitignore zupdate M2: HTTP wire-up + integration fixtures 2026-05-03 10:08:03 +02:00
build.zig zupdate M2: HTTP wire-up + integration fixtures 2026-05-03 10:08:03 +02:00
build.zig.zon Scaffold zupdate 2026-05-03 09:37:53 +02:00
LICENSE Scaffold zupdate 2026-05-03 09:37:53 +02:00
README.md docs: repoint links to git.urverk.org 2026-07-25 11:41:42 +02:00

zupdate

Embeddable program-update library for Zig host apps. Pulls a JSON manifest, downloads + SHA-256-verifies the right artifact for the current target, then hands off to a small standalone helper binary that swaps the install in place after the host process exits and relaunches it.

Designed to be non-blocking — the host calls Updater.checkAsync or Updater.downloadAsync, then polls Updater.snapshot() each frame to render progress. Internal worker thread handles the I/O.

Status

Pre-0.1. macOS self-update works end-to-end — see examples/demo/ for a working CLI that updates itself against the fixtures in test/fixtures/. Linux + Windows apply paths are the next milestones — see milestones below.

Public API at a glance

const zupdate = @import("zupdate");

var updater = try zupdate.Updater.init(gpa, .{
    .gpa = gpa,
    .io = io,
    .manifest_url = "https://git.urverk.org/urverk/quote/raw/branch/main/MANIFEST.json",
    .current_version = "0.1.0",
    .target_triple = "aarch64-macos",
    .staging_path = "/tmp/quote-update.tar.gz",
    .helper_path = "/Applications/Quote.app/Contents/MacOS/zupdate-helper",
    .install_dir = "/Applications/Quote.app",
    .relaunch_argv = &.{"/Applications/Quote.app/Contents/MacOS/quote"},
});
defer updater.deinit();

try updater.checkAsync();

// Each frame:
switch (updater.snapshot()) {
    .idle, .checking => render_busy_indicator(),
    .no_update => render_up_to_date(),
    .update_available => |u| if (user_clicked_install) try updater.downloadAsync(),
    .downloading => |d| render_progress(d.bytes_done, d.bytes_total),
    .ready_to_apply => |r| if (user_clicked_restart) try updater.apply(),  // does not return
    .failed => |msg| render_error(msg),
}

Manifest format

A single JSON file the host hosts at a stable URL. For Codeberg projects, committing a MANIFEST.json to main and fetching via https://codeberg.org/<user>/<repo>/raw/branch/main/MANIFEST.json works without API tokens or rate limits.

{
  "version": "0.2.0",
  "released": "2026-05-03",
  "notes_url": "https://git.urverk.org/urverk/quote/releases/tag/v0.2.0",
  "artifacts": {
    "aarch64-macos":  { "url": "...", "sha256": "..." },
    "x86_64-macos":   { "url": "...", "sha256": "..." },
    "aarch64-linux":  { "url": "...", "sha256": "..." },
    "x86_64-linux":   { "url": "...", "sha256": "..." },
    "x86_64-windows": { "url": "...", "sha256": "..." }
  }
}

version is SemVer-like (MAJOR.MINOR.PATCH with optional -prerelease). The library only offers an update when the manifest's version compares strictly greater than current_version.

sha256 is hex (lower-case, 64 chars). Compute with sha256sum / shasum -a 256 against the artifact bytes.

Architecture

zupdate/
  src/
    zupdate.zig         public API + Updater state machine
    manifest.zig        Manifest JSON parsing, SemVer compare
    download.zig        HTTP fetch + streaming SHA-256 (+ Progress)
    archive.zig         tar/unzip extraction (next milestone)
    apply.zig           platform-dispatched swap logic (next milestone)
    helper/main.zig     standalone updater binary

The helper binary (zupdate-helper) is what makes replace-while-running work cleanly across macOS/Linux/Windows. The host calls Updater.apply(), which spawns the helper with arguments describing what to do, then exits. The helper waits on the parent PID to be gone, swaps the install, and relaunches the host. Same pattern Sparkle and Squirrel.Windows use.

Build

zig build                     # builds the library module + zupdate-helper
zig build test                # unit tests (manifest parse, SemVer, sha256)
zig build test -Dnet=true     # also runs integration tests against the
                              # codeberg-hosted fixtures in test/fixtures

Requires Zig 0.16.0 or newer.

Io gotcha (Zig 0.16)

std.http.Client on Zig 0.16 fails to set up TLS when handed std.Io.Threaded.global_single_threaded — every fetch returns error.TlsInitializationFailed, regardless of host. The host app must pass a full Io.Threaded.init(allocator, .{}) to zupdate's Updater.Config.io. Single-threaded GUI apps can stash the Threaded instance once at startup and reuse .io() from it.

We discovered this by sampling — init_single_threaded is documented as not supporting concurrency or cancellation; turns out it also omits something the TLS path needs. If a future Zig fixes this we'll note it here.

Milestones

  • M1 Scaffold — module layout, manifest parsing, version compare, SHA-256 helper, public API + State machine, helper-binary stub, unit tests
  • M2 HTTPstd.http.Client wired through; integration tests fetch fixtures over HTTPS; live byte counters via Progress atomics
  • M3 macOS apply + demo — helper does PID-wait → tar -xzf → atomic install-dir swap (with .OLD-<ts> backup) → spawn relaunch. examples/demo/ self-updates end-to-end against the fixtures
  • M4 Linux apply — same shape as macOS; mostly already works since the helper is POSIX-shaped, but needs validation + fixtures for aarch64-linux / x86_64-linux
  • M5 Windows apply — replace kill(pid, 0) with OpenProcess/WaitForSingleObject, swap via MoveFileEx, fixtures for x86_64-windows
  • M6 Quote integration:update command-palette entry, dvui dialog UI, ship zupdate-helper in Quote.app

Optional/future:

  • EdDSA-signed manifests (the SHA-256-from-HTTPS trust root is fine if you trust your HTTPS cert chain; signing is a one-day add)
  • Delta updates (per-file bsdiff patches like Sparkle)
  • Background scheduled checks (currently strictly user-triggered)
  • Rollback UX (the helper renames the old install with a .OLD-vN suffix, so manual rollback is mv Quote.app.OLD-0.1.0 Quote.app)

License

MIT — see LICENSE.