Packages
mob_dev
0.3.13
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 [--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. Writes the Mob logo as a placeholder app icon (if no icon exists yet)
## Options
* `--icon PATH` — use a custom image instead of the Mob logo placeholder
## Icon output
- `android/app/src/main/res/mipmap-*/ic_launcher.png`
- `ios/Assets.xcassets/AppIcon.appiconset/icon_*.png` + `Contents.json`
The Mob logo is written as a placeholder. Replace it any time:
mix mob.icon # generate a custom icon
mix mob.icon --source my_logo.png
## 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. Placeholder icon** — writes the Mob logo to all platform icon sizes (pre-built PNGs,
no system tools required). Run `mix mob.icon` afterwards to replace it with a custom icon.
"""
@switches [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()
setup_icon(project_dir, opts[:icon])
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
Run `mix mob.icon` to replace the placeholder icon with a custom one.
""")
end
# ── OTP download ─────────────────────────────────────────────────────────────
defp download_otp do
Mix.shell().info("Ensuring OTP releases are cached...")
case MobDev.OtpDownloader.ensure_android("arm64-v8a") do
{:ok, path} ->
Mix.shell().info([:green, "* Android arm64 OTP: #{path}", :reset])
{:error, reason} ->
Mix.shell().error("Warning: Android arm64 OTP download failed: #{reason}")
end
case MobDev.OtpDownloader.ensure_android("armeabi-v7a") do
{:ok, path} ->
Mix.shell().info([:green, "* Android arm32 OTP: #{path}", :reset])
{:error, reason} ->
Mix.shell().error("Warning: Android arm32 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
@doc false
@spec write_local_properties(String.t(), keyword()) :: :ok | nil
def 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("arm64-v8a")
otp_dir_arm32 = MobDev.OtpDownloader.android_otp_dir("armeabi-v7a")
new_content =
content
|> replace_prop("mob.otp_release", otp_dir)
|> replace_prop("mob.otp_release_arm32", otp_dir_arm32)
|> 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
@doc false
@spec replace_prop(String.t(), String.t(), String.t() | nil) :: String.t()
def replace_prop(content, key, value) when not is_nil(value) do
String.replace(content, ~r/^#{Regex.escape(key)}=.*$/m, "#{key}=#{value}")
end
def replace_prop(content, _key, nil), do: content
# ── Icon setup ────────────────────────────────────────────────────────────────
defp setup_icon(project_dir, nil) do
placeholder =
Path.join([
project_dir,
"android",
"app",
"src",
"main",
"res",
"mipmap-mdpi",
"ic_launcher.png"
])
if File.exists?(placeholder) do
Mix.shell().info([
:cyan,
"* icons already present — skipping (run `mix mob.icon` to replace)",
:reset
])
else
Mix.shell().info("Writing Mob logo as placeholder icon...")
MobDev.IconGenerator.use_mob_logo(project_dir)
Mix.shell().info([
:green,
"* placeholder icons written (run `mix mob.icon` to customise)",
:reset
])
end
end
defp setup_icon(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