Mob.Certs + jniLibs-via-mob_beam for runtime escript/rebar3 on Android#38
Merged
Conversation
…_load/0 can't) `:public_key.cacerts_load/0` probes a handful of distro paths for a system CA bundle — none of which exist on Android. The system trust store lives behind a Java API that BEAM's `:public_key` doesn't reach, so the next `:public_key.cacerts_get/0` call raises `no_cacerts_found`. In some OTP versions `pubkey_os_cacerts.conv_error_reason/1` doesn't have a clause for that error, so the surface crash is the worse `FunctionClauseError` on `conv_error_reason/1`. Hex itself bakes its own DER bundle into `Hex.HTTP.SSL`, so it isn't affected — but every other Elixir HTTP library (Req → Mint → :ssl, Finch, anything using OTP-26+ default `:ssl` opts) breaks on the first TLS connect. Same shape as the DNS issue in #36: the OS exposes something Erlang can't reach, and the workaround is to point Erlang at an app-provided alternative. Adds: - `Mob.Certs.load_cacerts/1` — thin, predictable wrapper around `:public_key.cacerts_load/1` (returns `{:error, reason}` rather than the `FunctionClauseError` you sometimes see from OTP). - `Mob.Certs.load_cacerts!/1` — raising variant for boot use. - `Mob.Certs.loaded?/0` — diagnostic helper that wraps the raising `cacerts_get/0` and returns a boolean. `extra_applications: [:logger, :public_key]` so Elixir 1.19+'s unused-app culling doesn't strip `:public_key.beam` from the code path. Documented at length in the moduledoc + `common_fixes.md`. Usage: def on_start do Mob.Certs.load_cacerts!(Application.app_dir(:my_app, "priv/cacerts.pem")) # …rest of startup… end The bundle is the app's choice (security: who do you trust). `castore` ships a current Mozilla trust store and is the conventional source — copy its `cacerts.pem` into your `priv/` at build time. iOS isn't affected — Darwin exposes the trust store at the paths Erlang knows about. macOS keychain auto-loads from `:public_key`'s `cacerts_get/0` too. Cross-platform apps can call `load_cacerts!/1` unconditionally — a no-op on platforms that already have OS certs. End-to-end verified on a Moto G Power 5G 2024 (Android 14): - `Mob.Certs.load_cacerts!("…/priv/cacerts.pem")` succeeds - `Mob.Certs.loaded?()` returns true - `:public_key.cacerts_get()` returns 121 (castore's bundle size) - `Mix.install([{:jason, …}, {:kino, …}])` resolves and compiles - `:httpc.request(:get, "https://geocoding-api.open-meteo.com/v1/search?…", [ssl: [verify: :verify_peer, cacerts: :public_key.cacerts_get()]], …)` → status 200 Tests: 811 + 27 doctests pass (7 new Certs tests), mix credo --strict clean, mix format clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ime rebar3
Opens the door for apps that need runtime Mix.install of rebar3-built
deps (telemetry, jose, jiffy, brod, …) on Android. The walls there
were three-fold:
1. `rebar3` itself is an escript. `escript` is in ERTS but lives at
app_data_file context — execve is blocked from the app uid.
2. escript spawns a fresh BEAM via `erl`/`erlexec`. Same execve wall.
3. `erlexec` exec's `beam.smp`. Same execve wall.
Same shape as the existing solution for `inet_gethost`/`epmd`/`erl_child_setup`:
ship the binary as a `lib<name>.so` in `jniLibs/<abi>/`. Android extracts
it under apk_data_file context, and execve is allowed there. Differences
from the existing required list:
- These extras aren't on the BEAM-boot critical path. Apps that don't
use them shouldn't see scary error logs at startup. Hence an
optional list with silent-skip when the corresponding lib isn't in
nativeLibDir.
- `erl` and `erlexec` map to the same library — erlexec doesn't switch
on argv[0].
Additionally exposes `MOB_NATIVE_LIB_DIR` as an env var. Apps that
bundle the extra binaries need its path to set MIX_REBAR3 / locate the
rebar3 escript at runtime — the path includes the APK install hash and
isn't predictable at compile time.
Documented end-to-end (the wrapper-script trick, the rebar3-symlink-
for-module-name-derivation, the `bin/`-boot-symlinks materialization)
in common_fixes.md. The mob side of the patch is minimal — the app
ships the binaries. Verified end-to-end on a Moto G Power 5G 2024:
Mix.install([{:req, "~> 0.5"}])
# … resolves Req's full tree; telemetry compiles via on-device rebar3 …
Req.get!("https://geocoding-api.open-meteo.com/v1/search?name=Vancouver&count=1")
#=> %{status: 200, body: %{"results" => [%{"name" => "Vancouver", "country" => "Canada", …}]}}
Trade-off: ~33 MB APK size for apps that bundle beam.smp. Apps that
don't need runtime rebar3 deps pay nothing — the new lib names just
fail the existing lstat check and skip silently.
Tests still green: 811 + 27 doctests, 0 failures.
mix credo --strict clean, mix format clean, zig fmt clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two lines exceeded the formatter's line length; this is the autoformat that `mix format --check-formatted` produces. No logic change.
GenericJam
added a commit
that referenced
this pull request
May 28, 2026
Bundles PR #38 (Android CA certificates via Mob.Certs.load_cacerts!/1 and the optional jniLibs symlinks for runtime rebar3 / escript / beam.smp) plus the mix.exs before_closing_body_tag/1 dedupe so the language-elixir highlighter and the mermaid renderer are no longer mutually shadowed. CHANGELOG.md has the full per-section breakdown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two related changes to unblock real Elixir HTTP libraries on physical
Android. Same shape as #36 (Mob.DNS): the OS exposes something Erlang
can't reach, and the workaround is to point Erlang at an app-provided
alternative.
Commits
1.
Mob.Certs— load CA certificates on Android:public_key.cacerts_load/0probes a handful of distro paths for asystem CA bundle. None exist on Android — the system trust store lives
behind a Java API that BEAM's
:public_keydoesn't reach. The next:public_key.cacerts_get/0then raisesno_cacerts_found; in some OTPversions
pubkey_os_cacerts.conv_error_reason/1has no clause for thatand the surface error is a
FunctionClauseErrorinstead.Hex itself bakes its own DER bundle, so
mix installmay succeed wherethe first HTTP call from a user dep fails — Req → Mint →
:sslisthe typical place it shows up. iOS and the Android emulator don't hit
this (their
:ssldefaults find the OS trust store fine), which iswhy it wasn't caught earlier.
Adds
Mob.Certs.load_cacerts/1+load_cacerts!/1+loaded?/0—thin, predictable wrappers around
:public_key.cacerts_load/1. Appbrings its own PEM (conventional source:
castore'scacerts.pemcopied into priv at build time) and loads it once at startup:
```elixir
def on_start do
Mob.Certs.load_cacerts!(Application.app_dir(:my_app, "priv/cacerts.pem"))
...rest of startup...
end
```
`extra_applications: [:logger, :public_key]` so Elixir 1.19+'s
unused-app culling doesn't strip
:public_key.beamfrom the code path.Moduledoc covers the rationale + cross-platform notes (calling
unconditionally is harmless on iOS / emulator). Tests: 7 new, all
passing; a small ISRG Root X1 PEM is embedded in the test module so
the suite needs no fixture file or network fetch.
2.
mob_beam.zig: optional escript/erl/erlexec/beam.smp symlinks for runtime rebar3Opens the door for apps that need runtime
Mix.installof rebar3-builtdeps (telemetry, jose, jiffy, brod, …) on Android. Three walls:
rebar3itself is an escript.escriptis in ERTS but lives atapp_data_filecontext — execve is blocked from the app uid.erl/erlexec. Same execve wall.erlexecexec'sbeam.smp. Same execve wall.Same fix shape as the existing solution for
inet_gethost/epmd/erl_child_setup: ship the binary as alib<name>.soinjniLibs/<abi>/. Android extracts it underapk_data_filecontextwhere execve is allowed.
Differences from the existing required list:
don't use them shouldn't see scary error logs at startup. Hence an
optional list with silent-skip when the lib isn't in nativeLibDir.
erlanderlexecmap to the same library — erlexec doesn't switchon
argv[0].Additionally exposes
MOB_NATIVE_LIB_DIRas an env var. Apps thatbundle the extra binaries need its path at runtime to set
MIX_REBAR3and locate the rebar3 escript — the path includes the APK install hash
and isn't predictable at compile time.
The mob side of this commit is intentionally minimal — apps decide
whether to opt in, and ship the binaries. `common_fixes.md` documents
the full pattern (wrapper-script trick, rebar3-symlink-for-module-
name-derivation,
\$ROOTDIR/bin/*.bootmaterialization).Verified end-to-end
Moto G Power 5G 2024 (Android 14), via mob.connect RPC into the
livebook_mob app on master with both changes pinned in via path dep:
```elixir
Mob.Certs.load_cacerts!(".../priv/cacerts.pem") #=> :ok
Mob.Certs.loaded?() #=> true
length(:public_key.cacerts_get()) #=> 121
Mix.install([{:req, "~> 0.5"}])
→ resolves Req's full tree
→ telemetry compiles via on-device rebar3 (using bundled escript +
erlexec + beam.smp + the wrapper script)
→ finch + jason + mime + nimble_options + nimble_pool + hpax + req
all compile via Mix
#=> :ok
Req.get!("https://geocoding-api.open-meteo.com/v1/search?name=Vancouver&count=1\")
#=> %{status: 200,
body: %{"results" => [%{"name" => "Vancouver",
"admin1" => "British Columbia",
"country" => "Canada",
...}]}}
```
Real Req. Real HTTPS via cacerts. Real network. Real rebar3-compiled
telemetry. All on the physical phone.
Quality
mix test: 811 + 27 doctests pass, 0 failures (was 804 before thecacerts tests).
mix credo --strict: clean.mix format --check-formatted: clean.zig fmt --checkon the touched zig file: clean.Trade-off (rebar3 commit)
Apps that opt in pay ~33 MB APK size for the bundled
beam.smp. Appsthat don't ship the extra
lib*.sofiles in jniLibs see no change —the optional symlink loop just
lstats a missing path and skips itsilently. Pure-Mix-deps
Mix.installworks fine without bundlinganything (only Mob.Certs is needed for HTTPS).
🤖 Generated with Claude Code