From ad36b84762ab8fa1cbc9cf7e0989430775a88fac Mon Sep 17 00:00:00 2001 From: youfun <3973573+youfun@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:53:21 +0530 Subject: [PATCH 1/7] Pin Zig 0.17.0 and replace bufPrintZ. 0.17.0 is the stable with the macOS 26 SDK fix that 0.17.0-dev.269 was taken for. std.fmt.bufPrintZ is gone; post-mortem marker paths use bufPrintSentinel. Companion to mob_dev and mob_new. --- .tool-versions | 2 +- CHANGELOG.md | 9 +++++++++ android/jni/mob_nif.zig | 8 ++++---- build_system_migration.md | 5 +++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.tool-versions b/.tool-versions index b3d9d31b..e6da9a5c 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 310e2eef..b41df149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,16 @@ 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, so install `0.17.0` (mise: + `mise install zig@0.17.0`). Companion to mob_dev and mob_new. + ### Fixed +- **Android post-mortem marker paths compile on Zig 0.17.0.** `std.fmt.bufPrintZ` + was removed; `mob_nif.zig` now uses `std.fmt.bufPrintSentinel`. - **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 diff --git a/android/jni/mob_nif.zig b/android/jni/mob_nif.zig index 0b0f2568..03ae934c 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.fmt.bufPrintSentinel(&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.fmt.bufPrintSentinel(&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.fmt.bufPrintSentinel(&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.fmt.bufPrintSentinel(&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/build_system_migration.md b/build_system_migration.md index 3a8ed6c6..d14749bc 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 (`bufPrintSentinel`); + 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 From 9475be5b556e5d97e22e09b5c16aa7c2c0947085 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Fri, 23 Oct 2026 18:44:13 -0600 Subject: [PATCH 2/7] CI: set up Zig 0.17.0 (was the dev.269 nightly) --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b585653..13f7729d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,10 +91,10 @@ jobs: test/native/erts_dir_test.c \ android/jni/mob_beam.h - - name: Android init-args tokenizer + - name: Set up Zig uses: mlugg/setup-zig@v2 with: - version: 0.17.0-dev.269+ebff43698 + version: 0.17.0 - name: Android init-args and ERTS-dir tests run: | From 128aea66aa3e4972d269168f7a2a53704da6dabd Mon Sep 17 00:00:00 2001 From: GenericJam Date: Fri, 23 Oct 2026 19:10:10 -0600 Subject: [PATCH 3/7] std.mem.printSentinel; CI reads Zig from .tool-versions and runs :zig tests; upgrade steps --- .github/workflows/test.yml | 18 ++++++++++++++++-- CHANGELOG.md | 16 +++++++++++++--- android/jni/mob_nif.zig | 8 ++++---- build_system_migration.md | 2 +- issues.md | 8 ++++---- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13f7729d..77cb6ffa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,16 @@ 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 }} + - name: Cache deps + _build uses: actions/cache@v4 with: @@ -91,17 +101,21 @@ jobs: test/native/erts_dir_test.c \ android/jni/mob_beam.h + - 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 + version: ${{ steps.zig.outputs.version }} - name: Android init-args and ERTS-dir tests 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) + - 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/CHANGELOG.md b/CHANGELOG.md index b41df149..44edfc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,12 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). - **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, so install `0.17.0` (mise: - `mise install zig@0.17.0`). Companion to mob_dev and mob_new. + still requires an exact `zig version` match. Companion to mob_dev and + mob_new, which move with it. ### Fixed - **Android post-mortem marker paths compile on Zig 0.17.0.** `std.fmt.bufPrintZ` - was removed; `mob_nif.zig` now uses `std.fmt.bufPrintSentinel`. + was removed; `mob_nif.zig` now uses `std.mem.printSentinel`. - **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 @@ -28,6 +28,16 @@ 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(`, or copy both files from a freshly + generated app. `mix mob.doctor` flags the old form. + ## [0.9.25] - 2026-10-22 ### Security diff --git a/android/jni/mob_nif.zig b/android/jni/mob_nif.zig index 03ae934c..1d3ef66a 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.bufPrintSentinel(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }, 0) 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.bufPrintSentinel(&path_buf, "{s}/{s}", .{ dir, MARKER_FILENAME }, 0) 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.bufPrintSentinel(&out_buf, "{d}\n", .{ts_ms}, 0) 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.bufPrintSentinel(&buf, "mob_post_mortem: marker write failed ({s}); cross-boot dedup will re-emit until this clears", .{reason}, 0) 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/build_system_migration.md b/build_system_migration.md index d14749bc..b212e21b 100644 --- a/build_system_migration.md +++ b/build_system_migration.md @@ -533,7 +533,7 @@ Apple framework module maps under -fmodules — Phase 1 finding). 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 (`bufPrintSentinel`); + 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. diff --git a/issues.md b/issues.md index 04ae59e3..1f0603be 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. From bb5d631bc0986d71557f45a1a653f9de65b92728 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Fri, 23 Oct 2026 19:22:49 -0600 Subject: [PATCH 4/7] Track the mob/ios Swift glob as a configure dependency; doctor and upgrade notes --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44edfc53..378fe02c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,8 +35,11 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). 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(`, or copy both files from a freshly - generated app. `mix mob.doctor` flags the old form. + `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 From c1bfb32f2bceba0455f59db6e59eef6382a831d0 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Fri, 23 Oct 2026 19:47:20 -0600 Subject: [PATCH 5/7] Review round 3: dupeZ, doctor associates the dependency with mob/ios, CI lanes for the pinned Zig, test path without apostrophe --- .github/workflows/test.yml | 8 +++++--- CHANGELOG.md | 8 ++++++-- android/jni/mob_stored_queue.zig | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77cb6ffa..fcfb7c76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -110,10 +110,12 @@ jobs: with: version: ${{ steps.zig.outputs.version }} - - 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 + for f in $(grep -l '^test "' android/jni/*.zig); 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 378fe02c..607301c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,12 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). mob_new, which move with it. ### Fixed -- **Android post-mortem marker paths compile on Zig 0.17.0.** `std.fmt.bufPrintZ` - was removed; `mob_nif.zig` now uses `std.mem.printSentinel`. +- **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 diff --git a/android/jni/mob_stored_queue.zig b/android/jni/mob_stored_queue.zig index f02dcb95..e69b9f1a 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 { From 4d721ccf6cc3d3808d010c03e9f5086518e62e84 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Fri, 23 Oct 2026 20:00:10 -0600 Subject: [PATCH 6/7] Review nits: CI/Zig comments; zig test loop fails when it finds no files --- .github/workflows/test.yml | 4 +++- test/test_helper.exs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fcfb7c76..81bf1608 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -112,7 +112,9 @@ jobs: - name: Android Zig unit tests (every file with `test` blocks) run: | - for f in $(grep -l '^test "' android/jni/*.zig); do + 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 diff --git a/test/test_helper.exs b/test/test_helper.exs index 18aee827..da5f2b19 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) From 911e4545992983d7876d23e867a96203b50baea6 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Fri, 23 Oct 2026 20:03:45 -0600 Subject: [PATCH 7/7] CI: setup-zig without its .zig-cache cache (cached configure results name the old zig path) --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81bf1608..d3adc7e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,9 @@ jobs: - 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 @@ -109,6 +112,9 @@ jobs: 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: Android Zig unit tests (every file with `test` blocks) run: |