MobDev.Release.Tarball (mob_dev v0.5.8)

Copy Markdown View Source

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)

Fieldandroid_arm64android_arm32ios_simios_device
Tarball basenameotp-androidotp-android-arm32otp-ios-simotp-ios-device
Borrow crypto/pk/ssl apps from(built in-tree)(built in-tree)Android installAndroid install
Bundle exqlite BEAMsyesyesnono
Bundle EPMD sourcenononoyes (for static-link)
Verify crypto.so presentyesnonono
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

  1. Stage — mktemp + cp -r <otp_release>/. (the cross-compiled OTP install tree from MobDev.Release.OTP)
  2. Borrow apps — iOS targets only. cp from <android_otp_release>/lib/{crypto,public_key,ssl}-*.
  3. Static libs — cp the four arch-specific .a files (libzstd.a, libepcre.a, libryu.a, asn1rt_nif.a) into <stage>/erts-<vsn>/lib/.
  4. Crypto archives — cp crypto.a (from MobDev.Release.OpenSSL.CryptoNif) + libcrypto.a (from MobDev.Release.OpenSSL) into the same place.
  5. 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-specific erl_int_sizes_config.h) into <stage>/erts-<vsn>/include/.
  6. Elixir stdlib — MobDev.Release.Helpers.bundle_elixir_stdlib/2 (elixir, logger, eex ebins).
  7. exqlite BEAMs — Android targets only. Detect version from mix.lock; cp ebins into <stage>/lib/exqlite-<vsn>/.
  8. EPMD source — iOS device only. cp the 3 .c files + headers
    • the arch-specific config dir.
  9. tar czf the stage into <out_dir>/<basename>-<hash>.tar.gz, honouring per-target --exclude flags.
  10. 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

build(target_id, opts \\ [])

@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_SRC env or ~/code/otp)
  • :otp_release — install tree from MobDev.Release.OTP.build/2 (default: target's default_otp_release)
  • :openssl_prefix — OpenSSL install dir (default per-target)
  • :exqlite_build_build/dev/lib/exqlite in 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)

build_all(opts \\ [])

@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.

check_entries(listing, expected, tarball_path)

@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.

detect_exqlite_version(exqlite_build)

@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.

parse_exqlite_version_from_app_file(content)

@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.

parse_exqlite_version_from_lock(content)

@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.

required_entries(target, map)

@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.

tarball_path(target, out_dir, hash)

@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.

target_spec(atom)

@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.

targets()

@spec targets() :: [atom()]

All tarball targets, in canonical order.

verify_tarball(target, shell, env, tarball)

@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 entry
  • lib/elixir/ebin/elixir.app
  • erts-<vsn>/lib/crypto.a
  • erts-<vsn>/lib/libcrypto.a

Per-target additions come from target.additional_verifies.