Packages
mob_dev
0.3.16
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/mob_dev/hot_push.ex
defmodule MobDev.HotPush do
@moduledoc """
Connects to already-running device nodes and hot-pushes BEAM modules via RPC.
Unlike `MobDev.Deployer`, this does NOT restart apps — modules are loaded
into the running BEAM in place, just like `nl/1` in IEx.
Requires apps to already be running (start with `mix mob.connect` or
`mix mob.deploy` first).
"""
alias MobDev.{Tunnel}
alias MobDev.Discovery.{Android, IOS}
@cookie :mob_secret
@doc """
Sets up adb tunnels (idempotent) and connects to all running device nodes.
Returns list of connected node atoms.
"""
@spec connect(keyword()) :: [node()]
def connect(opts \\ []) do
cookie = Keyword.get(opts, :cookie, @cookie)
nodes =
(Android.list_devices() ++ IOS.list_simulators())
|> Enum.with_index()
|> Enum.flat_map(fn {device, idx} ->
case Tunnel.setup(device, idx) do
{:ok, d} -> [d]
_ -> []
end
end)
|> Enum.flat_map(fn device ->
ensure_local_dist(cookie)
Node.set_cookie(device.node, cookie)
case Node.connect(device.node) do
true -> [device.node]
_ -> []
end
end)
nodes
end
@doc """
Pushes all compiled BEAM files from `_build/dev/lib/*/ebin/` to `nodes`.
Only pushes BEAMs for runtime dependencies — deps marked `only: :dev` or
`runtime: false` in `mix.exs` (and their transitive deps) are excluded.
This prevents dev tooling (mob_dev, Bandit, Phoenix, etc.) from being pushed
to the device when using `path:` deps during local framework development.
Returns `{pushed_count, failed_list}`.
"""
@spec push_all([node()]) :: {non_neg_integer(), list()}
def push_all(nodes) do
beams = runtime_beam_paths()
push_beams(nodes, beams)
end
@doc """
Takes a snapshot of current BEAM mtimes for runtime deps only.
Pass the result to `push_changed/2` before and after compiling to get only
the modules that actually changed.
"""
@spec snapshot_beams() :: %{String.t() => non_neg_integer()}
def snapshot_beams do
runtime_beam_paths()
|> Map.new(fn path ->
mtime =
case File.stat(path, time: :posix) do
{:ok, %{mtime: t}} -> t
_ -> 0
end
{path, mtime}
end)
end
@doc """
Pushes BEAM files that changed since `snapshot` (from `snapshot_beams/0`).
Returns `{pushed_count, failed_list}` — pushed_count is 0 if nothing changed.
"""
@spec push_changed([node()], %{String.t() => non_neg_integer()}) :: {non_neg_integer(), list()}
def push_changed(nodes, snapshot) do
beams =
runtime_beam_paths()
|> Enum.filter(fn path ->
current_mtime =
case File.stat(path, time: :posix) do
{:ok, %{mtime: t}} -> t
_ -> 0
end
current_mtime != Map.get(snapshot, path, 0)
end)
push_beams(nodes, beams)
end
# ── Runtime dep filtering ────────────────────────────────────────────────────
# Returns only BEAM paths that belong to the app's runtime dependency tree.
# Excludes deps marked only: :dev or runtime: false in mix.exs, and all of
# their transitive deps (resolved via OTP .app files).
defp runtime_beam_paths do
runtime = runtime_lib_names()
Path.wildcard("_build/dev/lib/*/ebin/*.beam")
|> Enum.filter(fn path ->
lib = path |> Path.split() |> Enum.at(-3)
MapSet.member?(runtime, lib)
end)
end
@doc """
Returns ebin directories for runtime deps only (no dev-only tooling).
Used by `Deployer` so the filesystem push matches the dist push scope.
"""
@spec runtime_beam_dirs() :: [String.t()]
def runtime_beam_dirs do
runtime = runtime_lib_names()
case File.ls("_build/dev/lib") do
{:ok, libs} ->
libs
|> Enum.filter(&MapSet.member?(runtime, &1))
|> Enum.map(&"_build/dev/lib/#{&1}/ebin")
|> Enum.filter(&File.dir?/1)
{:error, _} ->
[]
end
end
defp runtime_lib_names do
project_app = to_string(Mix.Project.config()[:app])
# Direct runtime deps: no only: :dev and not runtime: false
direct =
Mix.Project.config()
|> Keyword.get(:deps, [])
|> Enum.flat_map(&dep_runtime_name/1)
|> MapSet.new()
|> MapSet.put(project_app)
expand_runtime_libs(direct)
end
# Expand a set of lib names to include their transitive OTP deps,
# by reading each lib's .app file in _build/dev.
defp expand_runtime_libs(libs) do
new_libs =
Enum.flat_map(libs, fn lib ->
case Path.wildcard("_build/dev/lib/#{lib}/ebin/*.app") do
[app_file | _] ->
case :file.consult(String.to_charlist(app_file)) do
{:ok, [{:application, _app, props}]} ->
(props[:applications] || []) |> Enum.map(&to_string/1)
_ ->
[]
end
[] ->
[]
end
end)
|> MapSet.new()
|> MapSet.difference(libs)
if MapSet.size(new_libs) == 0 do
libs
else
expand_runtime_libs(MapSet.union(libs, new_libs))
end
end
# Returns the app name as a string if this dep is a runtime dep, else [].
defp dep_runtime_name(dep) do
{app, opts} =
case dep do
{app, _version, opts} when is_list(opts) -> {app, opts}
{app, opts} when is_list(opts) -> {app, opts}
{app, _version} -> {app, []}
app when is_atom(app) -> {app, []}
end
only = Keyword.get(opts, :only)
runtime = Keyword.get(opts, :runtime, true)
dev_only = only == :dev or only == [:dev] or (is_list(only) and only == [:dev])
if dev_only or not runtime, do: [], else: [to_string(app)]
end
# ── Private ─────────────────────────────────────────────────────────────────
defp push_beams(_nodes, []), do: {0, []}
defp push_beams(nodes, beam_files) do
results =
Enum.map(beam_files, fn path ->
module = beam_path_to_module(path)
case File.read(path) do
{:ok, binary} -> load_on_nodes(nodes, module, path, binary)
{:error, reason} -> {:error, {module, reason}}
end
end)
pushed = Enum.count(results, &match?(:ok, &1))
failed = for {:error, pair} <- results, do: pair
{pushed, failed}
end
defp load_on_nodes(nodes, module, path, binary) do
fname = String.to_charlist(path)
errors =
Enum.flat_map(nodes, fn node ->
case :rpc.call(node, :code, :load_binary, [module, fname, binary]) do
{:module, ^module} -> []
# NIF modules already loaded — safe to ignore
{:error, :on_load_failure} -> []
{:badrpc, reason} -> [{node, reason}]
{:error, reason} -> [{node, reason}]
end
end)
if errors == [], do: :ok, else: {:error, {module, errors}}
end
defp beam_path_to_module(path) do
path |> Path.basename(".beam") |> String.to_atom()
end
defp ensure_local_dist(cookie) do
unless Node.alive?() do
Node.start(:"mob_dev@127.0.0.1", :longnames)
Node.set_cookie(cookie)
end
end
end