# `mix mob.deploy`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mix/tasks/mob.deploy.ex#L1)

Compiles the project then pushes BEAM files to a frozen set of selected
Android and iOS devices.

## Modes

**Fast deploy** (default) — load the new BEAMs into the app. Use this for
day-to-day Elixir code changes. Requires the native app already installed on
device. When the app's node is reachable over Erlang distribution, the
modules are hot-loaded in place with no restart; otherwise the BEAMs are
written to the device and the app is restarted to pick them up. An app built
against a pre-MOB-49 mob is restarted instead of hot-loaded, which moves it to
its private cookie, except on a physical iPhone: writing BEAMs to a phone over
`devicectl` has no undo, so a connected legacy iPhone app is still hot-loaded
and keeps `mob_secret` until you deploy with `--native`.

    mix mob.deploy

**Full deploy** — build native binary + install APK/app + push BEAMs + restart.
Use this the first time, or after changes to native C/Java/Swift/Zig code.

    mix mob.deploy --native

Plugin NIFs only reach the device through a native build. `--native` warns
about deps that ship a NIF but aren't in `config :mob, :plugins`, and records
which NIF plugins it built in; a fast deploy warns when a NIF plugin was
activated since that record (see `MobDev.Plugin.NifActivation`). Both are
warnings — otherwise the first sign is `:nif_not_loaded` at the first call.

Device selection is resolved once before any build or push starts. With no
selection flag, one emulator or simulator is selected automatically and the
task prints which; physical devices always require an explicit `--device` or
`--all-physical`. The `ANDROID_SERIAL` environment variable is treated like
`--device` for Android.

When `agent-device` is on PATH, selection honours its device claims
(`MobDev.DeviceLeases`): auto-selection and `--all-devices` /
`--all-physical` skip a device another agent-device session has claimed,
and say so. A claim is yours when its session matches `AGENT_DEVICE_SESSION`.
A claimed device named with `--device` is still used, after a warning.

## Options

  * `--native`              — build native binaries before pushing BEAMs
  * `--no-restart`          — write the BEAMs without restarting the app (a
                             hot load over dist never restarts anyway)
  * `-d`, `--device <id>`   — target a specific device; use `mix mob.devices` to find IDs
  * `--all-devices`         — target all emulators and simulators
  * `--all-physical`        — target all physical devices; combine with
                             `--all-devices` to target every connected device
  * `--dist-port <N>`       — pin the BEAM dist listen port (default: derived per
                            device from the device serial and app name, in
                            `9100..9899`, moved past ports another device already
                            uses; `mix mob.connect` derives the same port). Use to
                            resolve a collision with a non-BEAM listener on that port.
  * `--node-suffix <S>`     — append `_<S>` to the BEAM node name (default: auto-derived
                            from device serial on Android, SIMULATOR_UDID on iOS sim). Use
                            for scripted scenarios where you need a specific naming scheme.
  * `--schedulers <N>`      — set BEAM scheduler count (saved to mob.exs)
  * `--beam-flags "<flags>"` — arbitrary BEAM flags string (saved to mob.exs)
  * `--json`                — machine-readable result on stdout; progress goes to
                            stderr, so `mix mob.deploy --json | jq` gets one document
  * `--slim`                — strip OTP source/debug for size measurement on
                              a real device. OFF by default for dev iteration
                              (the strip pass adds ~5-10s per build); use this
                              to verify a slim build runs before
                              `mix mob.republish` round-trips through TestFlight.
                              The strip set is controlled by `MobDev.OtpAudit.Slim`;
                              per-app overrides live in `mob.exs`:

                                  config :mob_dev,
                                    slim: [
                                      drop_libs: ["my_unused_dep"],
                                      keep_libs: ["mnesia"],
                                      audit: true,                       # opt in
                                      # Single capture (a starting point):
                                      trace_json: "priv/mob_trace.json",
                                      # OR multiple captures unioned —
                                      # much safer for production
                                      # stripping. A lib is trace-
                                      # strippable only if NONE of the
                                      # captures observed any of its
                                      # modules.
                                      trace_jsons: [
                                        "priv/boot.json",
                                        "priv/ui.json",
                                        "priv/auth.json"
                                      ]
                                    ]

                              With `audit: true`, the slim pass runs
                              `MobDev.OtpAudit` against the bundle and
                              expands the strip set with foreign apps
                              + (when a trace is supplied) the
                              trace-augmented strip set. Trace JSON
                              comes from `mix mob.trace_otp --json`.

## BEAM scheduler tuning

The default native build uses `1:1` (single scheduler) for battery efficiency.
Override for the current deploy and all future deploys until changed:

    # Pin to 2 schedulers
    mix mob.deploy --schedulers 2

    # Let BEAM auto-detect — one scheduler per logical core
    mix mob.deploy --schedulers 0

    # Arbitrary flags (replaces --schedulers)
    mix mob.deploy --beam-flags "-S 4:4 -A 4"

The chosen value is written to `mob.exs` under `beam_flags:` and reused on
subsequent `mix mob.deploy` runs that don't pass either flag. The flags are
written alongside the BEAMs as a `mob_beam_flags` file that the native launcher
reads at startup — no APK/app rebuild required.

## Under the hood

A fast deploy is equivalent to:

    mix compile

    # Each target whose app answers over Erlang distribution: hot load in
    # place, no restart — `nl(Module)` in IEx for every module
    :rpc.call(node, :code, :load_binary, [Module, path, beam_binary])

    # Any other Android target: copy the BEAMs into the app's files, restart
    adb push <beams> /data/data/<package>/files/otp/<app>/   # as root (emulator), or
                                                            # via /data/local/tmp + `run-as tar xf`
    adb shell am force-stop <package>
    adb shell am start -n <package>/.MainActivity --ei mob_dist_port <port> ...

    # Any other iOS simulator: copy into the simulator runtime, relaunch
    rsync -a <compile path>/ ~/.mob/runtime/ios-sim/<app>/
    xcrun simctl launch <udid> <ios bundle id>

    # Any other physical iPhone: replace Documents/otp/<app>, relaunch
    xcrun devicectl device copy to --device <udid> ... Documents/otp/<app>

The BEAMs come from Mix's active build path (`Mix.Project.compile_path/0`
and `Mix.Project.build_path/0`): the app and its runtime dependencies, never
dev-only tooling.

With `--native`, it first runs `mix deps.get` and the platform build, installs
the result, then writes the BEAMs and restarts (never a hot load: the old
BEAM is gone after an install):

    # Android
    ./gradlew assembleDebug
    adb install -r app/build/outputs/apk/debug/app-debug.apk

    # iOS: ios/build.zig (simulator) or ios/build_device.zig (device), then
    xcrun simctl install <udid> <app>.app            # simulator
    xcrun devicectl device install app --device <udid> <app>.app   # device

A named `--device` supplies the platform when `--native` is used, so
`mix mob.deploy --native --device <id>` does not also need `--android` or
`--ios`. Before an Android build, the task creates
`android/local.properties` when the SDK is detectable, just as
`mix mob.install` does during first-run setup.

## Exit status

Every targeted device is attempted and the full summary printed, then the
task exits non-zero if **any** device landed in the `Failed on N device(s)`
bucket — including a partial success where other devices deployed fine.

Devices under `Skipped on N device(s)` (app not installed for that platform)
do not fail an implicit single-emulator run. They do fail when an explicit
device, broad scope, or platform was requested and nothing reached that
target platform:

  * `mix mob.deploy --ios --device X` where X lacked the app — exit 1.
  * `mix mob.deploy --all-devices` where every selected iOS simulator was
    skipped — exit 1 for the requested iOS target set.
  * `mix mob.deploy --ios --all-devices` where one simulator deployed and a
    stale one was skipped — exit 0. The rule remains per platform.
  * `mix mob.deploy --device X` that reached X and deployed nothing — exit 1.
  * `mix mob.deploy --device NOPE` matching no device — exit 1.
  * `mix mob.deploy --android --native` that built the APK with no device
    attached — exit 0. The artifact is what was asked for.

`--native` fails the run when a platform you named — directly or through
`--device` — built nothing at all. If the Android SDK cannot be detected,
a missing `sdk.dir` therefore produces a non-zero exit instead of a warning
followed by success.

# `failure_message`

```elixir
@spec failure_message([MobDev.Device.t()], [MobDev.Device.t()], [MobDev.Device.t()]) ::
  String.t() | nil
```

The `Mix.raise` message for a finished deploy, or `nil` when the run
should exit 0.

A deploy that printed "Failed on N device(s)" used to still exit 0, so
CI and wrapper scripts read a failed deploy as a success.

Only `failed` (a real error during push) is fatal. `skipped` is not:
it means "app not installed for that platform", the expected outcome of
e.g. building `--ios` with an Android phone also plugged in — the same
distinction `format_summary/4` renders.

Partial success is still a failure. Every targeted device is still
attempted and reported before this runs, so the operator can see which
ones got the BEAMs; a *script* has no way to notice one device missed
out if the status code says everything is fine.

# `failure_message`

```elixir
@spec failure_message([MobDev.Device.t()], [MobDev.Device.t()], [MobDev.Device.t()], [
  atom()
]) ::
  String.t() | nil
```

As `failure_message/3`, but knowing which platforms were explicitly asked for.

A skipped device is normally not a failure — it means "this device is not a
target for this app", the expected outcome of an Android phone being attached
during a default run. It IS a failure when the run named that platform: a
`mix mob.deploy --android` that skips every Android device asked for
something and got nothing, and must not report success.

Pass `[]` for requested and every skip is incidental, which is the
`failure_message/3` behaviour.

# `failure_message`

```elixir
@spec failure_message(
  [MobDev.Device.t()],
  [MobDev.Device.t()],
  [MobDev.Device.t()],
  [atom()],
  boolean()
) :: String.t() | nil
```

As `failure_message/4`, but knowing whether a native build succeeded.

A `--native` run that built the artifact and found no device to push it to
did its main job. Failing it would break "build the APK now, attach the phone
after", which used to exit 0.

# `format_summary`

```elixir
@spec format_summary(
  [MobDev.Device.t()],
  [MobDev.Device.t()],
  [MobDev.Device.t()],
  keyword()
) :: [
  String.t()
]
```

Build the per-deploy summary lines from the three device buckets.

Returns an iolist of strings (one per line) that the task prints
verbatim. Public so the report shape can be pinned against fixture
device lists — keeps "Failed on N" from regressing back into
counting skipped-because-not-installed devices.

Opts:
  * `:restart` — boolean; the follow-up line for devices deployed by push
    (a device the deployer marked `:hot_loaded` was updated over dist and
    never restarted, whatever this says)

# `invalid_options_message`

```elixir
@spec invalid_options_message([{String.t(), String.t() | nil}]) :: String.t()
```

The error for options the task does not accept.

Names them, because the failure this replaces was silent: the flag was
dropped and the deploy proceeded as if it had never been passed.

# `join_dashed_values`

```elixir
@spec join_dashed_values([String.t()]) :: [String.t()]
```

Rewrite `--flag value` to `--flag=value` when the value starts with a dash.

`OptionParser` will not consume a dash-prefixed argument as a `:string`
value, so `--beam-flags "-S 4:4 -A 4"` — the spelling this repo prints in
seven places, including the README and both battery-bench workflows — parsed
as two unknown options. Under the old lenient parsing the value was silently
dropped and the deploy carried on with whatever `mob.exs` held; under strict
parsing it became a hard failure that named a valid option as unknown.

BEAM flags essentially all start with a dash, so this is not an edge case:
it is the documented invocation.

# `json_result`

```elixir
@spec json_result(
  [MobDev.Device.t()],
  [MobDev.Device.t()],
  [MobDev.Device.t()],
  String.t() | nil
) ::
  map()
```

The machine-readable result of a finished deploy.

Exists because an agent driving `mix mob.deploy` otherwise has to infer the
outcome from coloured prose, and the exit code alone does not say *which*
target missed out. `outcome` mirrors the exit status: `"ok"` when the task
returns 0, `"error"` when it raises.

# `missing_device_message`

```elixir
@spec missing_device_message(
  String.t() | {:device | :android_serial, String.t()} | nil,
  [MobDev.Device.t()],
  [MobDev.Device.t()],
  [MobDev.Device.t()]
) :: String.t() | nil
```

The message for a run that named a device and did not find it, or `nil`.

`mix mob.deploy --device NOPE` printed "No devices found." and exited 0. The
device filter matches nothing, every bucket comes back empty, and a run that
shipped to a device you named by id is indistinguishable from one that
shipped nowhere.

Only fires when a device was named: with no `--device`, an empty run is the
ordinary "nothing is plugged in" case and stays non-fatal.

# `requested_platforms`

```elixir
@spec requested_platforms(keyword()) :: [:android | :ios]
```

The platforms the user explicitly asked for, from the raw flags.

Deliberately NOT `resolve_platforms/1`, which collapses "no flag given" into
every platform with a scaffold. That distinction is the whole point: a device
skipped during a default run is incidental (a phone that happens to be
attached), while one skipped during `--android` is a request that went
unserved. Returns `[]` when no platform flag was given.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
