diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b58565..d3adc7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,19 @@ jobs: elixir-version: ${{ matrix.elixir }} otp-version: ${{ matrix.otp }} + # `:zig` tests (mob_nif.zig built for Android, MOB-226) need the pinned + # Zig; read it from .tool-versions so CI can't drift from it. + - name: Zig version from .tool-versions + id: zig + run: echo "version=$(awk '$1 == "zig" {print $2}' .tool-versions)" >> "$GITHUB_OUTPUT" + + - uses: mlugg/setup-zig@v2 + with: + version: ${{ steps.zig.outputs.version }} + # A cached .zig-cache holds configure results that name the zig + # binary by its absolute path, which changes every run. + use-cache: false + - name: Cache deps + _build uses: actions/cache@v4 with: @@ -91,17 +104,28 @@ jobs: test/native/erts_dir_test.c \ android/jni/mob_beam.h - - name: Android init-args tokenizer + - name: Zig version from .tool-versions + id: zig + run: echo "version=$(awk '$1 == "zig" {print $2}' .tool-versions)" >> "$GITHUB_OUTPUT" + + - name: Set up Zig uses: mlugg/setup-zig@v2 with: - version: 0.17.0-dev.269+ebff43698 + version: ${{ steps.zig.outputs.version }} + # A cached .zig-cache holds configure results that name the zig + # binary by its absolute path, which changes every run. + use-cache: false - - name: Android init-args and ERTS-dir tests + - name: Android Zig unit tests (every file with `test` blocks) run: | - zig test android/jni/mob_init_args.zig - zig test android/jni/mob_erts_dir.zig - - - name: Native harness (frame registry MOB-102/MOB-103, dist port MOB-139, dist cookie MOB-348, stored queue MOB-379, init args MOB-406, node host MOB-428, code versions MOB-408) + files=$(grep -lE '^\s*test( "|\s*\{)' android/jni/*.zig) + [ -n "$files" ] || { echo "no Zig test files found"; exit 1; } + for f in $files; do + echo "== zig test $f" + zig test "$f" + done + + - name: Native harness (frame registry MOB-102/MOB-103, dist port MOB-139, dist cookie MOB-348, stored queue MOB-379, init args MOB-406, node host MOB-428, code versions MOB-408, ERTS dir MOB-493) # The frame-registry check is the only automated guard on the # element-frame registry's ownership, generation-gating and purge rules. # It reproduces the algorithm rather than linking ios/mob_nif.m (which diff --git a/.tool-versions b/.tool-versions index b3d9d31..e6da9a5 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ elixir 1.20.0-otp-29 erlang 29.1.2 -zig 0.17.0-dev.269+ebff43698 +zig 0.17.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 310e2ee..607301c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,20 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). ## [Unreleased] +### Changed +- **Zig pin is 0.17.0.** Replaces `0.17.0-dev.269+ebff43698`. That nightly + existed because 0.15.x could not link on the macOS 26 SDK; 0.17.0 is the + stable that has the fix (`build_system_migration.md`). `mix mob.doctor` + still requires an exact `zig version` match. Companion to mob_dev and + mob_new, which move with it. + ### Fixed +- **Android Zig sources compile on Zig 0.17.0.** `std.fmt.bufPrintZ` was + removed, so the post-mortem marker paths in `mob_nif.zig` use + `std.mem.printSentinel`, and `Allocator.dupeZ` was removed, so + `mob_stored_queue.zig`'s tests use `dupeSentinel`. CI now sets up the + pinned Zig from `.tool-versions`, compiles `mob_nif.zig` for Android and + runs every Android Zig test file. - **A task killed from outside right after `start_async/3` reports the real reason (MOB-500).** `Mob.Screen.Server` could pass `{:exit, :noproc}` to `handle_async/3` when the kill landed before the screen's monitor reached @@ -19,6 +32,19 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). for `Process.exit(pid, :kill)`). Seen once in a few thousand runs of `async_test.exs`, next to a test-side race on the same signal ordering. +### Upgrading +- **Zig 0.17.0, with mob_dev and mob_new's matching releases.** In each + app, set `zig 0.17.0` in `.tool-versions` (`mise use zig@0.17.0` installs + it and edits the file; `mise install zig@0.17.0` alone leaves the app on + the nightly). iOS apps also need `ios/build.zig` and + `ios/build_device.zig` off `b.build_root`, which 0.17.0 removed: in the + Swift glob, replace `b.build_root.handle.openDir(` with + `std.Io.Dir.openDirAbsolute(`, and add `b.dependOnDirectoryContents(.{ + .cwd_relative = b.fmt("{s}/ios", .{mob_dir}) });` before it (0.17.0 + caches the configure phase, so without it a new mob Swift file is missed). + Or copy the block from a freshly generated app. `mix mob.doctor` flags + both. + ## [0.9.25] - 2026-10-22 ### Security diff --git a/android/jni/mob_nif.zig b/android/jni/mob_nif.zig index 0b0f256..1d3ef66 100644 --- a/android/jni/mob_nif.zig +++ b/android/jni/mob_nif.zig @@ -4898,7 +4898,7 @@ const MARKER_FILENAME = "mob_post_mortem_appexit_marker.txt"; // Registry then dedups within the current BEAM session. fn readMarker(dir: []const u8) i64 { var path_buf: [640]u8 = @splat(0); - const path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }) catch return 0; + const path = std.mem.printSentinel(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }, 0) catch return 0; const f = jni.fopen(path.ptr, "rb") orelse return 0; defer _ = jni.fclose(f); @@ -4928,7 +4928,7 @@ fn readMarker(dir: []const u8) i64 { // console on a permanently-failing path. fn writeMarker(dir: []const u8, ts_ms: i64) void { var path_buf: [640]u8 = @splat(0); - const path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }) catch { + const path = std.mem.printSentinel(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }, 0) catch { logMarkerFailureOnce(.path_too_long); return; }; @@ -4941,7 +4941,7 @@ fn writeMarker(dir: []const u8, ts_ms: i64) void { defer _ = jni.close(fd); var out_buf: [32]u8 = @splat(0); - const out = std.fmt.bufPrintZ(&out_buf, "{d}\n", .{ts_ms}) catch { + const out = std.mem.printSentinel(&out_buf, "{d}\n", .{ts_ms}, 0) catch { logMarkerFailureOnce(.format_failed); return; }; @@ -4977,7 +4977,7 @@ fn logMarkerFailureOnce(kind: MarkerFailure) void { }; var buf: [128]u8 = @splat(0); - const msg = std.fmt.bufPrintZ(&buf, "mob_post_mortem: marker write failed ({s}); cross-boot dedup will re-emit until this clears", .{reason}) catch return; + const msg = std.mem.printSentinel(&buf, "mob_post_mortem: marker write failed ({s}); cross-boot dedup will re-emit until this clears", .{reason}, 0) catch return; jni.logWrite(jni.ANDROID_LOG_ERROR, "MobNIF", "{s}", .{msg}); } diff --git a/android/jni/mob_stored_queue.zig b/android/jni/mob_stored_queue.zig index f02dcb9..e69b9f1 100644 --- a/android/jni/mob_stored_queue.zig +++ b/android/jni/mob_stored_queue.zig @@ -105,7 +105,7 @@ pub const StoredQueue = struct { const testing = std.testing; fn dup(s: []const u8) ![*:0]u8 { - return (try testing.allocator.dupeZ(u8, s)).ptr; + return (try testing.allocator.dupeSentinel(u8, s, 0)).ptr; } fn release(s: [*:0]u8) void { diff --git a/build_system_migration.md b/build_system_migration.md index 3a8ed6c..b212e21 100644 --- a/build_system_migration.md +++ b/build_system_migration.md @@ -532,6 +532,11 @@ Apple framework module maps under -fmodules — Phase 1 finding). nightly has the fix; planning to switch to a zig stable that ships the macOS 26 fix when one is released (likely 0.17.0). + - 2026-10-22: Zig 0.17.0 is that stable. Pin across mob / mob_dev / + mob_new is `0.17.0`. `std.fmt.bufPrintZ` is gone (`std.mem.printSentinel`); + iOS templates open `{mob_dir}/ios` with `std.Io.Dir.openDirAbsolute` + because `Build.build_root` was removed. + - iter 1: build.zig owns driver_tab_ios.c compile. - iter 2: + enif_keepalive.c compile. - iter 3: + 5 ObjC compiles via xcrun cc system commands diff --git a/issues.md b/issues.md index 04ae59e..1f0603b 100644 --- a/issues.md +++ b/issues.md @@ -1030,8 +1030,8 @@ landed should drop in cleanly: **Why** — `:zigler ~> 0.15` resolves to `zigler 0.15.2`, which targets a Zig stdlib snapshot from before the `@Type` builtin signature change and before `std.fs.File.stdout()` was removed. The installed Zig -(currently `0.17.0-dev.269+ebff43698`, the version mob_dev builds with) -is past both changes. +(then `0.17.0-dev.269+ebff43698`, the version mob_dev built with; 0.17.0 +since 2026-10-22) is past both changes. The Elixir-side scaffolding itself is correct — it emits a clean `use Zig, otp_app: :app` module with an example `pub fn` in a `~Z` @@ -1043,8 +1043,8 @@ sigil. The bug is only the version pin. tracks Zig 0.17-dev. If a newer zigler release is compatible, bump the version in `MobDev.AddNif.maybe_add_zigler_dep/2`. -2. **Pin Zig instead.** Mob already pins a specific Zig version via - `~/zig/zig-aarch64-macos-0.17.0-dev.269+ebff43698/`. If zigler 0.15 +2. **Pin Zig instead.** Mob already pins a specific Zig version (then the + `0.17.0-dev.269+ebff43698` nightly, now 0.17.0). If zigler 0.15 needs an earlier Zig, document the supported range, or vendor a second Zig install for the zigler path. diff --git a/test/test_helper.exs b/test/test_helper.exs index 18aee82..da5f2b1 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -24,7 +24,7 @@ {:ok, _} = Mob.ComponentRegistry.start_link() # `:zig` tests shell out to a real zig toolchain (MOB-226); skip where none is -# installed (CI) rather than fail. +# installed rather than fail. CI installs the pinned Zig, so they run there. zig_exclude = if System.find_executable("zig"), do: [], else: [:zig] ExUnit.start(exclude: [:onboarding, :on_device] ++ zig_exclude)