Packages
mob_dev
0.2.12
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.install.ex
defmodule Mix.Tasks.Mob.Install do
use Mix.Task
@shortdoc "First-run setup for a new Mob project"
@moduledoc """
Runs first-time setup for a Mob project generated by `mix mob.new`.
Must be run from inside the project directory (the one containing `mix.exs`).
mix mob.install [--no-icon] [--icon PATH]
## What it does
1. Prompts for machine-specific paths (`mob_dir`, `elixir_lib`) and writes them
to `mob.exs` (gitignored) and `android/local.properties`
2. Downloads and caches the pre-built OTP runtime tarballs for Android and iOS
3. Generates app icons (random robot avatar, or a provided source image)
## Options
* `--no-icon` — skip icon generation
* `--icon PATH` — use an existing image instead of generating a random robot
## Icon output
- `android/app/src/main/res/mipmap-*/ic_launcher.png`
- `ios/Assets.xcassets/AppIcon.appiconset/icon_*.png` + `Contents.json`
- `icon_source.png` (1024×1024 master, when generating)
Run at any time to regenerate icons:
mix mob.install --no-icon # skip icons, re-run other setup steps
mix mob.icon # icon only, any time after install
## Under the hood
`mix mob.install` does three things that would otherwise require manual steps:
**1. Path configuration** — reads `mob.exs`, detects sensible defaults, prompts
for missing values, and writes them back. Equivalent to:
# mob.exs (gitignored, machine-specific)
config :mob_dev,
mob_dir: "/Users/me/code/mob"
**2. OTP download** — fetches pre-built ERTS tarballs from GitHub Releases:
# Roughly equivalent to:
curl -L https://github.com/genericjam/mob/releases/download/<tag>/otp-android-arm64.tar.gz \
-o ~/.mob_dev/otp-android-arm64.tar.gz
**3. Icon generation** — calls `mix mob.icon` internally (see that task for details).
"""
@switches [no_icon: :boolean, icon: :string]
@impl Mix.Task
def run(argv) do
{opts, _args, _} = OptionParser.parse(argv, strict: @switches)
project_dir = File.cwd!()
unless File.exists?(Path.join(project_dir, "mix.exs")) do
Mix.raise("No mix.exs found. Run mix mob.install from your project root.")
end
configure_paths(project_dir)
download_otp()
unless opts[:no_icon] do
generate_icons(project_dir, opts[:icon])
end
Mix.shell().info("""
Mob project ready!
mix mob.deploy --native # first deploy: build APK + iOS app, install + push BEAMs
mix mob.deploy # fast push + restart (day-to-day)
mix mob.watch # auto-push changes while developing
mix mob.connect # connect IEx to running device nodes
""")
end
# ── OTP download ─────────────────────────────────────────────────────────────
defp download_otp do
Mix.shell().info("Ensuring OTP releases are cached...")
case MobDev.OtpDownloader.ensure_android() do
{:ok, path} -> Mix.shell().info([:green, "* Android OTP: #{path}", :reset])
{:error, reason} -> Mix.shell().error("Warning: Android OTP download failed: #{reason}")
end
if match?({:unix, :darwin}, :os.type()) do
case MobDev.OtpDownloader.ensure_ios_sim() do
{:ok, path} -> Mix.shell().info([:green, "* iOS OTP: #{path}", :reset])
{:error, reason} -> Mix.shell().error("Warning: iOS OTP download failed: #{reason}")
end
end
end
# ── Path configuration ───────────────────────────────────────────────────────
# elixir_lib is no longer prompted — it's always auto-detected from the running BEAM.
@required_keys [:mob_dir]
defp configure_paths(project_dir) do
mob_exs = Path.join(project_dir, "mob.exs")
cfg =
if File.exists?(mob_exs) do
Config.Reader.read!(mob_exs) |> Keyword.get(:mob_dev, [])
else
[]
end
missing = Enum.filter(@required_keys, fn key ->
val = cfg[key]
is_nil(val) or (is_binary(val) and String.contains?(val, "/path/to/"))
end)
if missing != [] do
defaults = detect_defaults(project_dir)
Mix.shell().info("""
Configure your local build paths in mob.exs.
These are machine-specific and gitignored.
Press Enter to accept a detected value [ ], or type a new path.
""")
updates =
Enum.map(missing, fn key ->
{key, prompt_path(key, defaults[key])}
end)
new_cfg = Keyword.merge(cfg, updates)
write_mob_exs(mob_exs, new_cfg)
write_local_properties(project_dir, new_cfg)
Mix.shell().info([:green, "* mob.exs configured", :reset])
else
# mob.exs already has all required paths (e.g. generated with --local).
# Still sync local.properties if it has placeholder values.
write_local_properties(project_dir, cfg)
end
end
defp detect_defaults(project_dir) do
%{
elixir_lib: detect_elixir_lib(),
mob_dir: detect_mob_dir(project_dir)
}
end
# We're running inside Elixir — :code.lib_dir(:elixir) is always available.
defp detect_elixir_lib do
:code.lib_dir(:elixir) |> to_string() |> Path.dirname()
end
# Read the {:mob, path: "..."} dep from mix.exs and resolve it.
# Falls back to deps/mob for Hex installs.
defp detect_mob_dir(project_dir) do
mix_exs = Path.join(project_dir, "mix.exs")
with {:ok, content} <- File.read(mix_exs),
[_, rel] <- Regex.run(~r/\{:mob,\s+path:\s+"([^"]+)"/, content) do
Path.expand(rel, project_dir)
else
_ ->
deps_mob = Path.join(project_dir, "deps/mob")
if File.dir?(deps_mob), do: deps_mob, else: nil
end
end
defp prompt_path(key, detected) do
label = prompt_label(key)
if detected do
input = Mix.shell().prompt(" #{label} [#{detected}]:") |> String.trim()
if input == "", do: detected, else: input
else
Mix.shell().prompt(" #{label}:") |> String.trim()
end
end
defp prompt_label(:mob_dir), do: "mob library path"
defp prompt_label(:elixir_lib), do: "Elixir lib path"
defp write_mob_exs(path, cfg) do
content = """
import Config
config :mob_dev,
mob_dir: #{inspect(cfg[:mob_dir])}
"""
File.write!(path, content)
end
defp write_local_properties(project_dir, cfg) do
props = Path.join(project_dir, "android/local.properties")
if File.exists?(props) do
content = File.read!(props)
if String.contains?(content, "/path/to/") do
otp_dir = MobDev.OtpDownloader.android_otp_dir()
new_content =
content
|> replace_prop("mob.otp_release", otp_dir)
|> replace_prop("mob.mob_dir", cfg[:mob_dir])
File.write!(props, new_content)
Mix.shell().info([:green, "* android/local.properties configured", :reset])
end
end
end
defp replace_prop(content, key, value) when not is_nil(value) do
String.replace(content, ~r/^#{Regex.escape(key)}=.*$/m, "#{key}=#{value}")
end
defp replace_prop(content, _key, nil), do: content
# ── Icon generation ───────────────────────────────────────────────────────────
defp generate_icons(project_dir, nil) do
Mix.shell().info("Generating app icon (random robot)...")
MobDev.IconGenerator.generate_random(project_dir)
Mix.shell().info([:green, "* icons written", :reset])
end
defp generate_icons(project_dir, source) do
unless File.exists?(source) do
Mix.raise("Source file not found: #{source}")
end
Mix.shell().info("Generating app icon from #{source}...")
MobDev.IconGenerator.generate_from_source(source, project_dir)
Mix.shell().info([:green, "* icons written", :reset])
end
end