Current section

Files

Jump to
mob_dev lib mix tasks mob.verify_strip.ex
Raw

lib/mix/tasks/mob.verify_strip.ex

defmodule Mix.Tasks.Mob.VerifyStrip do
@shortdoc "Eager-load every shipped .beam on a connected device, report failures"
@moduledoc """
Boot-safety verification for stripped Mob app builds.
After deploying a slim build (where `mix mob.release` has dropped OTP
libs to shrink the IPA), this task connects to the running app's BEAM
and asks it to force-load every `.beam` shipped in the bundle. Any
module that fails to load — typically because a stripped lib was
needed by a transitive dep — shows up here.
## Usage
# First connect to the device (sets up tunnels + verifies node):
mix mob.connect --no-iex
# Then run the verifier:
mix mob.verify_strip
Or pass `--node` explicitly to skip auto-discovery.
## What it checks
1. **Eager load sweep** — `Code.ensure_loaded/1` on every `.beam`
under `<files_dir>/otp/`. Catches "module X depends on stripped
module Y" at load time.
2. **Harness exercise** — runs `MobDev.OtpTrace.Harness.all/0` on
the device and reports any exception. Catches issues with the
common Elixir surface even if individual modules loaded fine.
## What it does NOT check
App-specific code paths. If your app calls `:public_key.something`
when the user opens a particular screen, this verifier won't find it
unless the screen is opened. Run your own integration tests for that.
## Output
Plain mode prints a summary + per-failure detail. Exit code is 0 if
everything loaded + harness passed, 1 otherwise.
"""
use Mix.Task
@impl Mix.Task
def run(args) do
{opts, _, _} = OptionParser.parse(args, strict: [node: :string])
Mix.Task.run("loadpaths")
Mix.Task.run("compile")
node = resolve_node(opts[:node])
Mix.shell().info("Verifying strip safety on #{node}\n")
case :rpc.call(node, Mob.Diag, :verify_loaded_modules, []) do
{:badrpc, reason} ->
Mix.shell().error("RPC failed: #{inspect(reason)}")
Mix.shell().error("""
Is `Mob.Diag` deployed to the device? It ships with the `mob`
runtime library — push the latest BEAMs first:
mix mob.push
And confirm the device's mob version is recent enough.
""")
exit({:shutdown, 1})
report ->
print_load_report(report)
if report.failed != [], do: exit({:shutdown, 1})
end
end
defp resolve_node(nil) do
case Node.list() do
[] ->
Mix.raise("""
No connected nodes found. Run `mix mob.connect --no-iex` first
in a separate terminal so the device's BEAM is reachable.
Or pass --node <name@host> explicitly.
""")
[single] ->
single
multiple ->
Mix.raise("""
Multiple nodes connected — specify with --node:
#{Enum.map_join(multiple, "\n", &" #{&1}")}
""")
end
end
defp resolve_node(node_str), do: String.to_atom(node_str)
defp print_load_report(report) do
h1 = IO.ANSI.bright()
green = IO.ANSI.green()
red = IO.ANSI.red()
reset = IO.ANSI.reset()
Mix.shell().info("#{h1}=== Eager-load sweep ==={reset}")
Mix.shell().info(" OTP root: #{report.otp_root || "(not detected)"}")
Mix.shell().info(" Total .beam files: #{report.total}")
Mix.shell().info(" #{green}Loaded: #{report.loaded}#{reset}")
case report.failed do
[] ->
Mix.shell().info(
" #{green}✓ all modules loaded — strip is safe at the load-time level#{reset}"
)
failures ->
Mix.shell().error(" #{red}#{length(failures)} module(s) failed to load:#{reset}")
for %{module: mod, reason: reason} <- Enum.take(failures, 20) do
Mix.shell().error(" #{inspect(mod)}#{inspect(reason)}")
end
if length(failures) > 20 do
Mix.shell().error(" … and #{length(failures) - 20} more")
end
end
Mix.shell().info(" Elapsed: #{MobDev.Duration.format_us(report.elapsed_us)}")
end
end