Packages
mob_dev
0.3.14
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/mob.deploy.ex
defmodule Mix.Tasks.Mob.Deploy do
use Mix.Task
@shortdoc "Build and deploy to all connected mob devices"
@moduledoc """
Compiles the project then pushes BEAM files to all connected
Android devices and iOS simulators.
## Modes
**Fast deploy** (default) — push BEAMs + restart. Use this for day-to-day
Elixir code changes. Requires the native app already installed on device.
mix mob.deploy
**Full deploy** — build native binary + install APK/app + push BEAMs.
Use this the first time, or after changes to native C/Java/Swift code.
mix mob.deploy --native
## Options
* `--native` — build native binaries before pushing BEAMs
* `--no-restart` — push BEAMs but don't restart the app
* `--device <id>` — target a specific device; use `mix mob.devices` to find IDs
* `--schedulers <N>` — set BEAM scheduler count (saved to mob.exs)
* `--beam-flags "<flags>"` — arbitrary BEAM flags string (saved to mob.exs)
## 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 deps.get # only with --native
mix compile
# Android
adb push _build/prod/lib/*/ebin/*.beam /data/data/<pkg>/files/lib/*/ebin/
adb shell am force-stop <package> # restart
# iOS simulator
xcrun simctl spawn <udid> cp <beam_files> <app_bundle>/
When Erlang distribution is already reachable (app running, node connected),
`mix mob.deploy` skips `adb push` and hot-pushes via RPC instead — equivalent
to calling `nl(Module)` in IEx for every changed module:
:rpc.call(node, :code, :load_binary, [Module, path, beam_binary])
With `--native`, it also runs the platform build before pushing BEAMs:
# Android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
# iOS simulator
xcodebuild -scheme <app> -destination 'platform=iOS Simulator,...' build
xcrun simctl install booted <app>.app
"""
@switches [
native: :boolean,
restart: :boolean,
android: :boolean,
ios: :boolean,
device: :string,
schedulers: :integer,
beam_flags: :string
]
@impl Mix.Task
def run(args) do
{opts, _, _} = OptionParser.parse(args, switches: @switches)
restart = Keyword.get(opts, :restart, true)
native = Keyword.get(opts, :native, false)
device_id = opts[:device]
platforms = resolve_platforms(opts)
beam_flags = resolve_beam_flags(opts)
# When no --device is given and we're doing a native iOS build, auto-detect
# a connected physical device now so both the native build and the BEAM push
# target the same device (not all simulators + the phone).
effective_device_id =
device_id ||
if native and :ios in platforms,
do: MobDev.NativeBuild.detect_physical_ios()
IO.puts("")
if native do
IO.puts("Fetching dependencies...")
mix = System.find_executable("mix")
System.cmd(mix, ["deps.get"], into: IO.stream())
end
Mix.Task.run("compile")
IO.puts("\n#{IO.ANSI.cyan()}Deploying to devices...#{IO.ANSI.reset()}\n")
native_ok =
if native do
MobDev.NativeBuild.build_all(platforms: platforms, device: effective_device_id)
end
# Skip BEAM push if native build failed — the APK/app bundle isn't installed
# so run-as / simctl push would fail with misleading errors.
if native and native_ok == false do
IO.puts("\n#{IO.ANSI.red()}Native build had failures — see errors above.#{IO.ANSI.reset()}")
IO.puts(
"#{IO.ANSI.yellow()}Run `mix mob.doctor` to check your environment, or `mix mob.deploy` (without --native) once the issue is fixed.#{IO.ANSI.reset()}"
)
Mix.raise("Native build failed")
end
{deployed, failed} =
MobDev.Deployer.deploy_all(
restart: restart,
platforms: platforms,
force_fs: native,
device: device_id,
ios_device: effective_device_id,
beam_flags: beam_flags
)
if deployed == [] and failed == [] do
IO.puts("#{IO.ANSI.yellow()}No devices found.#{IO.ANSI.reset()}")
IO.puts("Try: mix mob.devices to diagnose connection issues")
else
if deployed != [] do
IO.puts("\n#{IO.ANSI.green()}Deployed to #{length(deployed)} device(s)#{IO.ANSI.reset()}")
if restart do
IO.puts(
"Apps restarted. Run #{IO.ANSI.cyan()}mix mob.connect#{IO.ANSI.reset()} to open IEx."
)
else
IO.puts(
"BEAMs pushed. In IEx: #{IO.ANSI.cyan()}nl(MyModule)#{IO.ANSI.reset()} to hot-load."
)
end
end
if failed != [] do
IO.puts("\n#{IO.ANSI.red()}Failed on #{length(failed)} device(s)#{IO.ANSI.reset()}")
Enum.each(failed, fn d ->
IO.puts(" ✗ #{d.name || d.serial}: #{d.error}")
end)
end
end
end
defp resolve_platforms(opts) do
android = opts[:android]
ios = opts[:ios]
cond do
android && ios ->
[:android, :ios]
android ->
[:android]
ios ->
if macos?() do
[:ios]
else
IO.puts(
"#{IO.ANSI.yellow()}Warning: --ios is only supported on macOS. Skipping iOS.#{IO.ANSI.reset()}"
)
[]
end
macos?() ->
[:android, :ios]
true ->
[:android]
end
end
defp macos?, do: match?({:unix, :darwin}, :os.type())
# Resolve --schedulers / --beam-flags into a combined flags string, save to
# mob.exs, and return it (or the previously saved value if no flags given).
defp resolve_beam_flags(opts) do
new_flags = combine_beam_flags(opts[:schedulers], opts[:beam_flags])
if new_flags do
save_beam_flags(new_flags)
IO.puts("#{IO.ANSI.cyan()}* beam flags: #{new_flags} (saved to mob.exs)#{IO.ANSI.reset()}")
new_flags
else
MobDev.Config.load_mob_config()[:beam_flags]
end
end
@doc false
@spec combine_beam_flags(pos_integer() | nil, String.t() | nil) :: String.t() | nil
def combine_beam_flags(schedulers, flags_string) do
case {schedulers, flags_string} do
{nil, nil} -> nil
{n, nil} -> "-S #{n}:#{n}"
{nil, flags} -> String.trim(flags)
{n, flags} -> "-S #{n}:#{n} #{String.trim(flags)}"
end
end
# Write or update the beam_flags key in mob.exs.
defp save_beam_flags(flags) do
path = Path.join(File.cwd!(), "mob.exs")
unless File.exists?(path), do: Mix.raise("mob.exs not found in current directory")
content = File.read!(path)
updated = update_beam_flags_in_config(content, flags)
File.write!(path, updated)
end
@doc false
@spec update_beam_flags_in_config(String.t(), String.t() | nil) :: String.t()
def update_beam_flags_in_config(content, flags) do
value = inspect(flags)
if content =~ ~r/^\s+beam_flags:/m do
Regex.replace(~r/^(\s+beam_flags:).*$/m, content, " beam_flags: #{value}")
else
String.trim_trailing(content) <> "\nconfig :mob_dev, beam_flags: #{value}\n"
end
end
end