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
38 changes: 31 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions android/jni/mob_nif.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
};
Expand All @@ -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;
};
Expand Down Expand Up @@ -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});
}

Expand Down
2 changes: 1 addition & 1 deletion android/jni/mob_stored_queue.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions build_system_migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading