Packages
mob_dev
0.5.1
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/ndk_version.ex
defmodule MobDev.NdkVersion do
@moduledoc """
Single source of truth for the Android NDK version Mob's bundled OTP
runtime was cross-compiled against.
The on-device `libbeam.a` (in the `otp-android-*` tarballs) embeds C++
stdlib symbols using libc++'s versioned inline namespace. NDK 27.2
uses `std::__ne180000::`; NDK 25 uses `std::__ne140000::`. These
don't link cross-version: an app's `libpigeon.so` built with the
wrong NDK fails with `undefined symbol: __cxa_allocate_exception`
(or similar libc++ ABI symbols).
This module is consulted by:
* `mix mob.doctor` — checks the recommended NDK is installed and
that the project's gradle pin (or override) doesn't drift.
* `mix mob.install` — same check during onboarding.
* `mix mob.new`'s gradle template (via `MobNew.NdkVersion`) — sets
the `ndkVersion` literal so AGP picks deterministically.
* `scripts/release/openssl/*.sh` — sources `NDK_VERSION` from
`_lib.sh` so the host-side OpenSSL cross-compile uses the same
NDK as the bundled tarballs.
## Recommended vs effective
`recommended/0` is the version Mob's tarballs were built against. It
changes only when we cross-compile new tarballs.
`effective/0` returns the recommended version *unless* the user has
overridden it. Two override mechanisms:
1. **Environment variable** (`MOB_ANDROID_NDK_VERSION=...`) —
machine-local. Use when one developer needs a specific NDK on
their box and the team's project config should stay clean.
2. **Per-project config** in `mob.exs`:
config :mob_dev,
android_ndk_version: "25.1.8937393"
Travels with the project. Use when the whole team needs to
build against a non-recommended NDK (legacy library
dependency, hardware-specific toolchain, etc).
Precedence: env var > mob.exs > recommended.
## Override caveat
When an override is active the user opts out of the libc++ ABI
guarantee against the bundled tarballs. They're navigating that
alone — `mob.doctor` warns but does not fail. Cryptic link errors
against `libbeam.a` are then their problem to debug. See
`~/code/mob/common_fixes.md` "NDK 27 / clang 18 split libc++"
for the symptom and the diagnostic.
"""
@recommended "27.2.12479018"
@doc "The NDK version the bundled OTP tarballs were cross-compiled with."
@spec recommended() :: String.t()
def recommended, do: @recommended
@doc """
The NDK version the user's build should target.
Returns `recommended/0` unless overridden via `MOB_ANDROID_NDK_VERSION`
env var or `:android_ndk_version` in `mob.exs`'s `:mob_dev` config.
"""
@spec effective() :: String.t()
def effective do
case override() do
{_source, version} -> version
:none -> @recommended
end
end
@doc """
Returns `{:env, version}`, `{:mob_exs, version}`, or `:none`
describing which override mechanism is active (if any).
Used by `mob.doctor` to explain *why* the effective version differs
from the recommendation.
"""
@spec override() :: {:env | :mob_exs, String.t()} | :none
def override do
cond do
env = System.get_env("MOB_ANDROID_NDK_VERSION") -> {:env, env}
cfg = Application.get_env(:mob_dev, :android_ndk_version) -> {:mob_exs, cfg}
true -> :none
end
end
@doc """
True if the given NDK version is installed under the local Android SDK.
Looks for `<sdk>/ndk/<version>/source.properties` since the directory
alone can be a half-extracted partial install.
"""
@spec installed?(String.t()) :: boolean()
def installed?(version) do
case sdk_root() do
nil -> false
sdk -> File.regular?(Path.join([sdk, "ndk", version, "source.properties"]))
end
end
@doc """
Returns all NDK versions present under the local SDK, newest-first by
string sort.
"""
@spec installed_versions() :: [String.t()]
def installed_versions do
case sdk_root() do
nil ->
[]
sdk ->
ndk_dir = Path.join(sdk, "ndk")
case File.ls(ndk_dir) do
{:ok, entries} ->
entries
|> Enum.filter(&File.regular?(Path.join([ndk_dir, &1, "source.properties"])))
|> Enum.sort(:desc)
_ ->
[]
end
end
end
@doc """
Returns the absolute path to the recommended NDK install if present,
or `nil`.
"""
@spec recommended_install_path() :: String.t() | nil
def recommended_install_path do
case sdk_root() do
nil ->
nil
sdk ->
path = Path.join([sdk, "ndk", @recommended])
if File.regular?(Path.join(path, "source.properties")), do: path, else: nil
end
end
@doc """
Reads the project's `android/app/build.gradle` (or `.kts`) for the
`ndkVersion` literal. Returns the string or `nil` if not pinned.
Accepts an optional project root; defaults to the current working
directory.
"""
@spec project_pinned(String.t()) :: String.t() | nil
def project_pinned(project_root \\ File.cwd!()) do
candidates = [
Path.join(project_root, "android/app/build.gradle"),
Path.join(project_root, "android/app/build.gradle.kts")
]
# `ndkVersion '27.2.x'` (Groovy DSL) and `ndkVersion = "27.2.x"` (Kotlin DSL)
# are both accepted — the regex tolerates the optional `=`.
# Compiled at runtime to avoid OTP 28.0 `:re.import/1` undefined-function
# crash on sigil-precompiled regexes loaded from beam files.
pattern = Regex.compile!("ndkVersion\\s*=?\\s*[\"']([^\"']+)[\"']")
Enum.find_value(candidates, fn path ->
case File.read(path) do
{:ok, contents} ->
case Regex.run(pattern, contents, capture: :all_but_first) do
[version] -> version
_ -> nil
end
_ ->
nil
end
end)
end
# ── Internals ────────────────────────────────────────────────────────────────
@doc false
@spec sdk_root() :: String.t() | nil
def sdk_root do
System.get_env("ANDROID_HOME") ||
System.get_env("ANDROID_SDK_ROOT") ||
default_sdk_root_for_os()
end
defp default_sdk_root_for_os do
case :os.type() do
{:unix, :darwin} -> Path.expand("~/Library/Android/sdk")
{:unix, _} -> Path.expand("~/Android/Sdk")
_ -> nil
end
|> then(fn path ->
if path && File.dir?(path), do: path, else: nil
end)
end
@doc """
Build the suggested install command for the recommended NDK. Used by
`mob.doctor` and `mix mob.install` to give the user a one-liner.
"""
@spec install_command() :: String.t()
def install_command do
"sdkmanager --install \"ndk;#{@recommended}\""
end
end