- Zig 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The referenced repos have migrated; their codeberg copies are deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| examples/demo | ||
| src | ||
| test | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| LICENSE | ||
| README.md | ||
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 HTTP —
std.http.Clientwired through; integration tests fetch fixtures over HTTPS; live byte counters viaProgressatomics - 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)withOpenProcess/WaitForSingleObject, swap viaMoveFileEx, fixtures forx86_64-windows - M6 Quote integration —
:updatecommand-palette entry, dvui dialog UI, shipzupdate-helperinQuote.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
bsdiffpatches like Sparkle) - Background scheduled checks (currently strictly user-triggered)
- Rollback UX (the helper renames the old install with a
.OLD-vNsuffix, so manual rollback ismv Quote.app.OLD-0.1.0 Quote.app)
License
MIT — see LICENSE.