Replaces scripts/release/tarball_*.sh × 4 — the staging + tar czf
step that produces the otp-<target>-<hash>.tar.gz archive
MobDev.OtpDownloader later fetches.
iex> MobDev.Release.Tarball.build(:android_arm64,
...> exqlite_build: "/path/to/_build/dev/lib/exqlite")
{:ok, %{tarball: "/tmp/otp-android-abc12345.tar.gz", target: :android_arm64}}Per-target variation (the richest of any iter so far)
| Field | android_arm64 | android_arm32 | ios_sim | ios_device |
|---|---|---|---|---|
| Tarball basename | otp-android | otp-android-arm32 | otp-ios-sim | otp-ios-device |
| Borrow crypto/pk/ssl apps from | (built in-tree) | (built in-tree) | Android install | Android install |
| Bundle exqlite BEAMs | yes | yes | no | no |
| Bundle EPMD source | no | no | no | yes (for static-link) |
| Verify crypto.so present | yes | no | no | no |
| tar --exclude paths | (none) | (none) | test-app dirs | (none) |
Note the asymmetry in tarball_basename: the Android arm64 tarball is
otp-android-<hash>.tar.gz (NOT otp-android-arm64-...), preserved
for backward compat with MobDev.OtpDownloader.@otp_hash —
changing this would break every existing cache entry.
Why iOS targets "borrow" crypto/public_key/ssl apps from Android
iOS OTP is cross-compiled --without-ssl (the iOS xcomp confs use
--enable-static-nifs and OTP's build system doesn't propagate
--with-ssl to beam.emu's link in that mode). The crypto,
public_key, and ssl Erlang apps therefore aren't produced for
iOS. BEAM bytecode is platform-neutral, so we copy these apps'
ebins from the Android arm64 install tree — they're the same
bytecode. The platform-specific NIF artifacts they ship in priv/
are obsolete on iOS anyway because we replace crypto.so with the
static crypto.a + libcrypto.a we just baked.
What "build" does end-to-end
- Stage — mktemp +
cp -r <otp_release>/.(the cross-compiled OTP install tree fromMobDev.Release.OTP) - Borrow apps — iOS targets only. cp from
<android_otp_release>/lib/{crypto,public_key,ssl}-*. - Static libs — cp the four arch-specific
.afiles (libzstd.a,libepcre.a,libryu.a,asn1rt_nif.a) into<stage>/erts-<vsn>/lib/. - Crypto archives — cp
crypto.a(fromMobDev.Release.OpenSSL.CryptoNif) +libcrypto.a(fromMobDev.Release.OpenSSL) into the same place. - ERTS headers — cp the 5 headers (
erl_nif.h,erl_nif_api_funcs.h,erl_drv_nif.h,erl_fixed_size_int_types.h, arch-specificerl_int_sizes_config.h) into<stage>/erts-<vsn>/include/. - Elixir stdlib —
MobDev.Release.Helpers.bundle_elixir_stdlib/2(elixir, logger, eex ebins). - exqlite BEAMs — Android targets only. Detect version from
mix.lock; cp ebins into
<stage>/lib/exqlite-<vsn>/. - EPMD source — iOS device only. cp the 3
.cfiles + headers- the arch-specific config dir.
- tar czf the stage into
<out_dir>/<basename>-<hash>.tar.gz, honouring per-target--excludeflags. Verify —
tar tzf | grep <pattern>for each per-target required entry. Missing entries fail loudly rather than ship a broken tarball.
Why mix.lock parsing lives here
Android tarballs ship exqlite BEAMs that the user app loads as a NIF
on device. The version of those BEAMs has to match the user app's
_build/dev/lib/exqlite/ ebins so dialyzer behaviours and protocol
consolidation line up. Detecting the exqlite version means parsing
mix.lock — a small regex on a known shape. Pure function, tested.
Summary
Functions
Stage + tar the per-target OTP runtime tarball. Returns {:ok, info}
naming the produced tarball + final size, or a tagged error.
Build all four tarballs in sequence. Each target picks up its own
defaults. Returns [{target_id, result}, ...] in canonical order.
Doesn't short-circuit.
Scan a tar tzf listing and confirm every expected pattern matches
at least one entry. Patterns are treated as regex (matching the
shell's grep -q semantics). Public for tests.
Detect the exqlite version by reading mix.lock (preferred) or
<exqlite_build>/ebin/exqlite.app (fallback). Returns {:ok, vsn}.
Parse the vsn field out of an exqlite.app file's content.
Fallback for when mix.lock isn't available (rare, but the shell
version handled it). Returns {:ok, version} or a tagged error.
Parse the exqlite package version out of a mix.lock string.
Returns {:ok, version} or a tagged error.
Per-target required entries. Public for testing — pinning the list is the surface lock that prevents silent drops.
Final tarball path: <out_dir>/<basename>-<hash>.tar.gz. Public for
testing — basename drift breaks the downloader's cache.
Per-target spec. Public for testing — surface lock-down for tarball naming (changing the basename breaks the downloader's cache), the iOS borrow-from-Android decision, and the per-target verify list.
All tarball targets, in canonical order.
Verify the produced tarball contains every required entry. Returns
:ok or a precondition_failed naming the missing entry.
Functions
@spec build( atom(), keyword() ) :: {:ok, map()} | MobDev.Release.Errors.t()
Stage + tar the per-target OTP runtime tarball. Returns {:ok, info}
naming the produced tarball + final size, or a tagged error.
Options:
:otp_src— OTP source checkout (default:$OTP_SRCenv or~/code/otp):otp_release— install tree fromMobDev.Release.OTP.build/2(default: target'sdefault_otp_release):openssl_prefix— OpenSSL install dir (default per-target):exqlite_build—_build/dev/lib/exqlitein any project (required for Android targets; ignored for iOS):android_otp_release— used by iOS targets to borrow crypto apps (default:/tmp/otp-android):asn1rt_nif_arm32— pre-built arm32 asn1rt_nif.a (Android arm32 only; default:/tmp/asn1rt_nif_arm32.a):out_dir— tarball output (default:/tmp):hash— release hash (default: detected from OTP source git)
@spec build_all(keyword()) :: [{atom(), {:ok, map()} | MobDev.Release.Errors.t()}]
Build all four tarballs in sequence. Each target picks up its own
defaults. Returns [{target_id, result}, ...] in canonical order.
Doesn't short-circuit.
@spec check_entries(binary(), [String.t()], Path.t()) :: :ok | MobDev.Release.Errors.t()
Scan a tar tzf listing and confirm every expected pattern matches
at least one entry. Patterns are treated as regex (matching the
shell's grep -q semantics). Public for tests.
@spec detect_exqlite_version(Path.t()) :: {:ok, String.t()} | MobDev.Release.Errors.t()
Detect the exqlite version by reading mix.lock (preferred) or
<exqlite_build>/ebin/exqlite.app (fallback). Returns {:ok, vsn}.
exqlite_build is the path to _build/dev/lib/exqlite/ in any Mob
project that has run mix deps.get && mix compile.
Project root lookup: mix.lock lives 4 levels up from
_build/dev/lib/exqlite/ — that's the project root convention every
Hex umbrella uses.
@spec parse_exqlite_version_from_app_file(binary()) :: {:ok, String.t()} | MobDev.Release.Errors.t()
Parse the vsn field out of an exqlite.app file's content.
Fallback for when mix.lock isn't available (rare, but the shell
version handled it). Returns {:ok, version} or a tagged error.
@spec parse_exqlite_version_from_lock(binary()) :: {:ok, String.t()} | MobDev.Release.Errors.t()
Parse the exqlite package version out of a mix.lock string.
Returns {:ok, version} or a tagged error.
Public for testing — silent parse failures here would silently bundle
no exqlite, then user apps die at runtime with :undef.
@spec required_entries(MobDev.Release.Tarball.Target.t(), map()) :: [String.t()]
Per-target required entries. Public for testing — pinning the list is the surface lock that prevents silent drops.
ERTS version interpolation happens against env.erts_vsn.
@spec tarball_path(MobDev.Release.Tarball.Target.t(), Path.t(), String.t()) :: Path.t()
Final tarball path: <out_dir>/<basename>-<hash>.tar.gz. Public for
testing — basename drift breaks the downloader's cache.
@spec target_spec(atom()) :: MobDev.Release.Tarball.Target.t()
Per-target spec. Public for testing — surface lock-down for tarball naming (changing the basename breaks the downloader's cache), the iOS borrow-from-Android decision, and the per-target verify list.
@spec targets() :: [atom()]
All tarball targets, in canonical order.
@spec verify_tarball( MobDev.Release.Tarball.Target.t(), MobDev.Release.Shell.t() | module(), map(), Path.t() ) :: :ok | MobDev.Release.Errors.t()
Verify the produced tarball contains every required entry. Returns
:ok or a precondition_failed naming the missing entry.
Universal entries (every target):
erts-<vsn>/directory entrylib/elixir/ebin/elixir.apperts-<vsn>/lib/crypto.aerts-<vsn>/lib/libcrypto.a
Per-target additions come from target.additional_verifies.