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
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,12 +454,15 @@ Real fix: `sim_runtime_dir/0` should detect `ios/build.zig` and use
schemes set, the launcher must be `singleTask`/`singleInstance` (enforced,
never rewritten; mob_new's template is `singleTop`). See
`decisions/2026-10-03-url-schemes.md`.
- **Android deploys relabel `otp/` once, last.** Anything written as root
(`adb root` push, `ln -s`) keeps root's SELinux categories until
- **Android deploys relabel `otp/` last (and also first).** Anything written
as root (`adb root` push, `ln -s`) keeps root's SELinux categories until
`relabel_otp_android/1` in `deploy_android/3` runs. Add new device writes
before that call, never after: the dist (hot) path doesn't restart, so there
is no later relabel, and the app fails on its next launch (P11: exqlite's
NIF symlink, `dlopen ... not found`).
before the final call, never after: the dist (hot) path doesn't restart, so
there is no later relabel, and the app fails on its next launch (P11:
exqlite's NIF symlink, `dlopen ... not found`). The first call, before the
`run-as` ERTS probe, exists because after an APK reinstall the root-pushed
tree can carry the previous install's categories, which `run-as` can't read
(MOB-478).
- **Physical iOS BEAM overrides must be exact and self-verifying.** The app
prefers `Documents/otp/<app>` over its complete signed bundle. Replace that
directory rather than incrementally merging it, require `<app>.beam` before
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
collision and fails the build. See
`decisions/2026-10-13-style-packages-bundle-fonts.md`.

### Fixed

- **`mix mob.deploy --native --android` works again after the app is
reinstalled on a rooted emulator (MOB-478).** The reinstalled app runs
under new SELinux MCS categories, but the OTP tree the deploy pushed as
root came out with the previous install's, so the `run-as` check for ERTS
couldn't read it and the deploy failed with "OTP runtime missing on
device". The deploy now relabels `otp/` before that check too, not only
after the last write. Without root the relabel is a no-op, as before.

## [0.7.24] - 2026-10-13

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions lib/mob_dev/deployer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ defmodule MobDev.Deployer do
{:skipped,
"#{pkg} not installed on #{device.name || serial} (ABI mismatch or app not built for this platform)"}
else
# Also before the run-as probe below. After an APK reinstall the app
# runs under new MCS categories, but the otp/ tree `--native` pushed as
# root can carry the previous install's; run-as can't read it, the probe
# reports ERTS missing, and the deploy stopped before the relabel below.
relabel_otp_android(serial)

case ensure_erts_on_device(serial, pkg) do
:ok ->
case push_beams_android(serial, beam_dirs) do
Expand Down
92 changes: 92 additions & 0 deletions test/mob_dev/deployer_android_relabel_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule MobDev.DeployerAndroidRelabelTest do
@moduledoc """
An Android deploy relabels `otp/` before the `run-as` ERTS probe, not only
after the last write (MOB-478). After an APK reinstall on a rooted emulator,
the OTP tree `--native` pushed as root carried the previous install's MCS
categories; `run-as` couldn't read it, the probe reported ERTS missing, and
the deploy aborted before its only relabel.

Runs `Deployer.deploy_all/1` against a fake `adb` on PATH that behaves like
that device: the probe sees nothing until a `chcon` has run.

async: false — PATH is process-global.
"""
use ExUnit.Case, async: false

alias MobDev.{Deployer, Device}

@moduletag :tmp_dir

# The probe's failure output is what the emulator printed on 2026-10-13
# (Scene3d_arm, android-35 google_apis userdebug): the unreadable erts-*
# dir doesn't match the glob, so ls is handed the glob itself.
@fake_adb """
#!/bin/sh
set -f
printf '%s\\n' "$*" >> "$FAKE_ADB_DIR/calls"
case "$*" in
*"pm list packages "*) for a; do last=$a; done; echo "package:$last" ;;
*" root") echo "adbd is already running as root" ;;
*"id -u") echo 0 ;;
*chcon*) : > "$FAKE_ADB_DIR/relabeled" ;;
*"sh -c 'ls "*)
[ -e "$FAKE_ADB_DIR/relabeled" ] && exit 0
cmd=$*
globs=${cmd#*\\'ls }
for g in ${globs%\\'}; do echo "ls: $g: No such file or directory"; done
exit 1 ;;
esac
exit 0
"""

setup %{tmp_dir: tmp} do
bin = Path.join(tmp, "bin")
File.mkdir_p!(bin)
adb = Path.join(bin, "adb")
File.write!(adb, @fake_adb)
File.chmod!(adb, 0o755)

path = System.get_env("PATH")
System.put_env("PATH", bin <> ":" <> path)
System.put_env("FAKE_ADB_DIR", tmp)

on_exit(fn ->
System.put_env("PATH", path)
System.delete_env("FAKE_ADB_DIR")
end)

:ok
end

test "a reinstalled app's stale otp/ labels are fixed before the ERTS probe, and again last",
%{tmp_dir: tmp} do
emulator = %Device{
name: "emu",
serial: "emulator-5620",
platform: :android,
type: :emulator
}

ExUnit.CaptureIO.capture_io(fn ->
assert {[_], [], []} =
Deployer.deploy_all(
devices: [emulator],
platforms: [:android],
restart: false,
force_fs: true
)
end)

calls = tmp |> Path.join("calls") |> File.read!() |> String.split("\n", trim: true)
probe = Enum.find_index(calls, &(&1 =~ "sh -c 'ls "))
assert is_integer(probe), "the run-as ERTS probe never ran"

assert [first_relabel, last_relabel] =
for({call, i} <- Enum.with_index(calls), call =~ "chcon", do: i)

assert first_relabel < probe
# Nothing written after the final relabel: a root write after it would
# keep root's categories (the exqlite NIF symlink, P11).
assert last_relabel == length(calls) - 1
end
end
Loading