Packages
mob_dev
0.6.10
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.enable.ex
defmodule Mix.Tasks.Mob.Enable do
use Igniter.Mix.Task
@shortdoc "Enable optional Mob features in this project"
@moduledoc """
Enables one or more optional Mob features by patching `mix.exs`, manifest
files, and generating any required source files.
## Usage
mix mob.enable FEATURE [FEATURE ...]
Multiple features can be enabled in a single command:
mix mob.enable camera photo_library
mix mob.enable camera photo_library file_sharing liveview
## Features
### `liveview`
Enables LiveView mode — the Mob app runs a local Phoenix endpoint and displays
it in a native WebView. Web developers can ship a mobile app with zero native
UI code.
What it does:
- Generates `lib/<app>/mob_screen.ex` — a `Mob.Screen` that opens a WebView
at `http://127.0.0.1:PORT/`
- Injects the `MobHook` LiveView hook into `assets/js/app.js`
- Injects a hidden `<div id="mob-bridge" phx-hook="MobHook">` into
`root.html.heex` — **this is required for the hook to mount**
- Updates `mob.exs` with `liveview_port` so `Mob.LiveView.local_url/1` works
### Why the hidden div is required
Phoenix LiveView hooks only execute when a DOM element carrying
`phx-hook="MobHook"` exists in the rendered page. Registering `MobHook` in
`app.js` is necessary but not sufficient — without a matching DOM element the
hook never mounts and `window.mob` is never replaced with the LiveView-backed
version. Messages would silently route through the native NIF bridge instead
of the LiveView WebSocket, so `handle_event/3` would never fire.
See `MobDev.Enable` module doc and `guides/liveview.md` for the full
two-bridge architecture explanation.
After running:
1. Add `MyApp.MobScreen` to your supervision tree (or call
`Mob.Screen.start_root(MyApp.MobScreen)` from your `Mob.App.on_start/0`)
2. Ensure Phoenix is running on the port set in `mob.exs` (default: 4000)
### `camera`
Adds camera permission declarations to platform manifests.
- iOS: adds `NSCameraUsageDescription` to `ios/*/Info.plist`
- Android: adds `<uses-permission android:name="android.permission.CAMERA"/>`
to `android/app/src/main/AndroidManifest.xml`
### `photo_library`
- iOS: adds `NSPhotoLibraryAddUsageDescription` to Info.plist
- Android: no manifest change needed (API 29+)
### `file_sharing`
- iOS: adds `UIFileSharingEnabled` and `LSSupportsOpeningDocumentsInPlace`
to Info.plist
- Android: adds `<provider android:name="FileProvider">` with paths config
### `location`
- iOS: adds `NSLocationWhenInUseUsageDescription` to Info.plist
- Android: adds `ACCESS_FINE_LOCATION` permission
### `notifications`
- iOS: creates `ios/<app>.entitlements` with `aps-environment: development`.
After running, execute `mix mob.provision` so Xcode downloads a push-capable
provisioning profile. Then call `Mob.Permissions.request(socket, :notifications)`
and `Mob.Notify.register_push(socket)` at runtime to obtain a device token.
- Android: runtime only — `POST_NOTIFICATIONS` is requested at runtime, no
manifest key needed.
### `pythonx`
Enables embedded CPython via [Pythonx](https://hex.pm/packages/pythonx)
on iOS **and** Android.
- **iOS:** BeeWare's [`Python-Apple-support`](https://github.com/beeware/Python-Apple-support)
`Python.xcframework` is bundled by `mix mob.deploy --native`.
- **Android:** [Chaquopy](https://chaquo.com/chaquopy/)'s prebuilt CPython
is unpacked at first launch. `libpythonx.so` (the Pythonx NIF) is
cross-compiled with the Android NDK against a stub `libpython3.13.so`
so the BEAM dynamic loader is satisfied; the real lib resolves at
runtime via SONAME match.
- **Bare CPython only.** Bundles ship the interpreter, stdlib, and
standard C extensions (`_ssl`, `_ctypes`, `_hashlib`, …). Third-party
wheels (`cryptography`, `numpy`, `RNS`, …) are out of scope —
produce your own (BeeWare's [`mobile-forge`](https://github.com/beeware/mobile-forge)
on iOS, Chaquopy's wheel pipeline on Android) and drop them into
your project.
What it does:
- Adds `{:pythonx, "~> 0.4"}` to `mix.exs` deps.
- Generates `lib/<app>/python_paths.ex` — pure detection module that
locates the bundled framework at runtime (`:desktop` /
`{:ios, paths}` / `{:android, paths}` / `{:partial, missing}`).
- **No `:uv_init` config patch.** Pythonx ships an Application
that auto-runs uv at boot if `:uv_init` is in compile-time config,
and uv doesn't exist on device. Instead the on_start template
inlines `pyproject_toml` and calls `Pythonx.Uv.fetch/2 +
Pythonx.Uv.init/2` only on the `:desktop` branch. Same code path
`iex -S mix` would use, just opt-in.
Bundle size impact: ~70 MB on iOS, ~30 MB on Android (interpreter +
stdlib + arch-specific C extensions). Apply this only when you actually
want to call Python from BEAM — non-Python apps stay vanilla.
After running, your `Mob.App.on_start/0` should:
- call `Application.ensure_all_started(:pythonx)` (starts the
`Pythonx.Janitor`, required for `Pythonx.eval/3`);
- case-match `<App>.PythonPaths.detect/1` and call `Pythonx.Uv.fetch
+ init` on `:desktop` (provisioning a uv-managed CPython on first
run) or `Pythonx.init/4` on `{:ios, _}` / `{:android, _}`.
See `guides/python_embedding.md` for the full template.
### `mlx`
Enables Apple's [MLX](https://github.com/ml-explore/mlx) library + the
[EMLX](https://hex.pm/packages/emlx) Nx backend on iOS. Gives the app
fast on-device tensor math (matmul, FFT, linalg, etc.) backed by
Apple's Accelerate framework (vectorized BLAS/LAPACK).
- **iOS device + simulator:** `libmlx.a` + `libemlx.a` are
cross-compiled and statically linked into the app binary. The
pre-built bundle (~5 MB compressed, ~30 MB on disk per arch) is
downloaded once and cached at `~/.mob/cache/libmlx-<ver>-ios-<slice>/`
by `MobDev.MLXDownloader`. `MOB_STATIC_EMLX_NIF` flips on
automatically — the EMLX NIF is registered in the static-NIF table
so `load_nif/2` resolves it without dlopen.
- **Android:** not supported in v1. No Metal on Android — a CPU-only
NDK build via OpenBLAS is the path forward but isn't shipped yet.
- **CPU-only for v1.** Metal-on-iOS needs the iOS-Metal CMakeLists
patch and Xcode 16's optional Metal Toolchain — deferred to a v2
tarball variant.
What it does:
- Adds `{:nx, "~> 0.10"}` and `{:emlx, "~> 0.2"}` to `mix.exs` deps.
- Generates `lib/<app>/ml_init.ex` — a one-call helper that sets
`EMLX.Backend` as Nx's global default, with a clean
`Nx.BinaryBackend` fallback if the NIF can't load.
After running, your `Mob.App.on_start/0` should call
`<App>.MLInit.configure()` once `Mob.Screen.start_root/1` and
`Mob.Dist.ensure_started/1` have run.
"""
@valid_features ~w(liveview camera photo_library file_sharing location notifications pythonx mlx nxeigen tflite)
@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :mob,
schema: [],
# mob.enable takes a variable list of feature names; we parse those
# from `igniter.args.argv` ourselves rather than declaring a single
# positional binding.
positional: []
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
features = parse_features(igniter.args.argv)
unknown = features -- @valid_features
cond do
features == [] ->
Igniter.add_issue(
igniter,
"Usage: mix mob.enable FEATURE [FEATURE ...]\nValid features: #{Enum.join(@valid_features, ", ")}"
)
unknown != [] ->
Igniter.add_issue(
igniter,
"Unknown feature(s): #{Enum.join(unknown, ", ")}. Valid: #{Enum.join(@valid_features, ", ")}"
)
not File.exists?("mix.exs") ->
Igniter.add_issue(igniter, "No mix.exs found. Run mix mob.enable from your project root.")
true ->
# Read app name from Igniter's view of mix.exs (which respects
# `test_project(files: %{"mix.exs" => ...})` virtualization).
# Falls back to the on-disk read when the igniter doesn't have a
# mix.exs source yet (real invocation outside a test harness).
app_name =
case Igniter.Project.Application.app_name(igniter) do
nil -> read_app_name(File.cwd!())
atom -> Atom.to_string(atom)
end
Enum.reduce(features, igniter, fn feature, acc ->
dispatch(acc, feature, app_name)
end)
end
end
# ── Feature dispatch (Phase 4 iter 1: all features routed through Igniter) ──
alias MobDev.Enable.Igniter, as: EI
defp dispatch(igniter, "camera", app_name), do: EI.enable_camera(igniter, app_name)
defp dispatch(igniter, "photo_library", app_name),
do: EI.enable_photo_library(igniter, app_name)
defp dispatch(igniter, "location", app_name), do: EI.enable_location(igniter, app_name)
defp dispatch(igniter, "file_sharing", app_name), do: EI.enable_file_sharing(igniter, app_name)
defp dispatch(igniter, "notifications", app_name),
do: EI.enable_notifications(igniter, app_name)
defp dispatch(igniter, "liveview", app_name), do: EI.enable_liveview(igniter, app_name)
defp dispatch(igniter, "pythonx", app_name) do
igniter
|> EI.enable_python(app_name)
|> Igniter.add_notice(pythonx_next_steps(app_name))
end
defp dispatch(igniter, "mlx", app_name) do
igniter
|> EI.enable_mlx(app_name)
|> Igniter.add_notice(mlx_next_steps(app_name))
end
defp dispatch(igniter, "nxeigen", app_name) do
igniter
|> EI.enable_nxeigen(app_name)
|> Igniter.add_notice(nxeigen_next_steps(app_name))
end
defp dispatch(igniter, "tflite", app_name) do
igniter
|> EI.enable_tflite(app_name)
|> Igniter.add_notice(tflite_next_steps(app_name))
end
defp parse_features(argv) do
{_opts, features, _} = OptionParser.parse(argv, strict: [yes: :boolean])
features
end
defp mlx_next_steps(app_name) do
module = Macro.camelize(app_name)
"""
Next steps for mlx:
1. Run `mix deps.get` to fetch :nx + :emlx.
2. In your `Mob.App.on_start/0`, call:
#{module}.MLInit.configure()
after `Mob.Screen.start_root/1` and `Mob.Dist.ensure_started/1`.
It picks EMLX as Nx's global backend (CPU + Apple Accelerate on
iOS) and falls back to Nx.BinaryBackend if EMLX can't load.
3. `mix mob.deploy --native --device <udid>` to cross-compile and
install the app. The first build downloads ~5 MB of pre-built
MLX (libmlx.a + libemlx.a) into ~/.mob/cache/.
Bundle size impact: ~5 MB compressed (~30 MB on disk per arch).
Apply this only when you actually want fast on-device tensor math.
iOS-only for v1. Android MLX support is a separate cross-compile
(no Metal — CPU + NDK BLAS); not shipped yet.
"""
end
defp nxeigen_next_steps(app_name) do
module = Macro.camelize(app_name)
"""
Next steps for nxeigen:
1. Run `mix deps.get` to fetch :nx + :nx_eigen.
2. Run `mix deps.compile nx_eigen` once on host — this triggers
the auto-download of the Eigen 3.4.0 header tarball into
deps/nx_eigen/eigen-3.4.0/, which mob_dev's cross-compile
then references.
3. In your `Mob.App.on_start/0`, call:
#{module}.NxEigenInit.configure()
after `Mob.Screen.start_root/1` and `Mob.Dist.ensure_started/1`.
It picks NxEigen as Nx's global backend (Eigen CPU, header-only,
NEON-vectorised on ARM) and falls back to Nx.BinaryBackend if
the NIF can't load.
4. `mix mob.deploy --native --device <udid>` to cross-compile and
install the app. The first build cross-compiles libnx_eigen.a
(two .cpp files: NxEigen's main NIF + our Eigen-FFT bridge)
per target arch into `_build/<env>/nxeigen/`.
Works on BOTH iOS (device + sim) and Android (arm64 + arm32) —
Eigen is header-only C++. FFT support uses Eigen's built-in
kissfft (header-only); swap to a FFTW variant later if needed.
"""
end
defp tflite_next_steps(app_name) do
module = Macro.camelize(app_name)
"""
Next steps for tflite:
1. Run `mix deps.get` to fetch :nx + :nx_tflite_mob.
2. Drop a `.tflite` model into `priv/` (e.g. exported from Ultralytics:
yolo export model=yolov8n.pt format=tflite int8=True
produces `yolov8n_full_integer_quant.tflite`).
3. Use in app code:
tflite = File.read!(Path.join(:code.priv_dir(:#{app_name}), "yolov8n_full_integer_quant.tflite"))
{:ok, m} = NxTfliteMob.load_module(tflite, #{module}.TfliteInit.default_opts())
{:ok, [out]} = NxTfliteMob.call(m, [input_bytes])
4. `mix mob.deploy --native --device <udid>` to cross-compile and install.
First build downloads:
- Android: tensorflow-lite-2.16.1.aar (~6 MB) into ~/.mob/cache/
- iOS: TensorFlowLiteC-2.17.0.tar.gz (~77 MB) into ~/.mob/cache/
Bundle size impact: ~3-4 MB compressed (Android `libtensorflowlite_jni.so`);
~20-30 MB on iOS (TensorFlowLiteC + CoreML/Metal frameworks). Apply this
only when you actually want to run TFLite models.
Cross-platform: same `.tflite` model + same `NxTfliteMob.call/2` Elixir
code on iOS and Android. The per-platform delegate is picked by
`#{module}.TfliteInit.default_opts/0` automatically.
"""
end
defp pythonx_next_steps(app_name) do
module = Macro.camelize(app_name)
"""
Next steps for pythonx:
1. Run `mix deps.get` to fetch :pythonx
2. In your `Mob.App.on_start/0`:
{:ok, _} = Application.ensure_all_started(:pythonx)
case #{module}.PythonPaths.detect(to_string(:code.root_dir())) do
:desktop -> Pythonx.Uv.fetch(toml, false); Pythonx.Uv.init(toml, false)
{:ios, %{dl_path: dl, home_path: home}} -> Pythonx.init(dl, home, dl, sys_paths: [])
{:android, %{dl_path: dl, home_path: home}} -> Pythonx.init(dl, home, dl, sys_paths: [Path.join([home, "lib", "python3.13"])])
{:partial, missing} -> # log + bail out
end
pyproject_toml stays inline so Pythonx.Application doesn't auto-run uv
on device (where uv doesn't exist).
3. `mix mob.deploy --native --device <udid>` to bundle CPython +
install on device. iOS downloads the BeeWare framework on first
run; Android pulls Chaquopy's prebuilt distribution.
"""
end
# ── Helpers ───────────────────────────────────────────────────────────────
defp read_app_name(project_dir) do
MobDev.Enable.read_app_name_from(Path.join(project_dir, "mix.exs"))
rescue
e -> Mix.raise(Exception.message(e))
end
end