In-place strip pass for the per-app OTP bundle, called by
MobDev.NativeBuild when MOB_SLIM=1. Was an inline ~100-line
defp in native_build.ex; extracted here so it's testable and
has a place for per-app override hooks to live.
What gets stripped
Six phases run in fixed order against <app_bundle>/otp:
apple_binaries—.so/.aandpriv/bin/*(Apple-policy parity: no standalone executables in the bundle)prefix_libs— everylib/<name>-*whose<name>is in the computed strip set (seecompute_strip_set/1)foreign_apps—lib/{toy_,test_,mob_test,scratch_}*(other projects' code that snuck into a shared OTP cache)dedup_versions— when the same lib appears at multiple versions, keep only the highestsrc_and_headers— everysrc/andinclude/directorybeam_chunks—:beam_lib.strip_release/1drops Debug/Doc chunks from every.beam
Steps are intentionally idempotent so repeat runs are safe.
Strip set composition
The default strip set is the hardcoded baseline (hardcoded_prefixes/0)
— a curated list of OTP libs mobile apps never need (megaco, snmp,
diameter, …). Per-app overrides in mob.exs adjust the set:
config :mob_dev,
slim: [
drop_libs: ["foo_dep"], # force-strip these too
keep_libs: ["mnesia"] # don't strip these even if baseline says so
]drop_libs and keep_libs accept plain <name> strings — the
same shape MobDev.OtpAudit's report uses, so users can copy
basenames directly out of mix mob.audit_otp output.
Audit-driven expansion + the always-keep guardrail
When the caller passes :audit_input (typically via mob.exs
slim: [audit: true, trace_json: "..."]), the strip set grows
by:
audit.foreign_app_names(allow-list-validated cache cruft)strippable_libs ∩ trace_strippable_libs(both signals agree)trace_strippable_libs \ strippable_libs(trace-only — libs the static graph reaches but the trace says are never called)
The last category is the powerful one and the dangerous one. A
60-second trace window can easily miss libs that ARE used but
only at boot (sasl) or only in code paths the user didn't
drive during the window (crypto, public_key, asn1, ssl
when no TLS happens). Auto-stripping those would break apps.
The always_keep_libs/0 set is the safety guardrail: those libs
are NEVER added to the strip set by audit-driven expansion, no
matter what the trace says. The hardcoded baseline still strips
what it strips, and :drop_libs still works as the user-explicit
escape hatch — but the trace can't accidentally take down crypto
or sasl.
Override patterns from mob.exs:
config :mob_dev,
slim: [
# Force-keep — user override beats everything else.
# Use this if audit-driven strip is taking out a lib you
# actually need at runtime.
keep_libs: ["some_lib"],
# Force-strip — user override beats the audit. Use this
# to expand beyond what the trace alone would mark.
drop_libs: ["my_unused_dep"]
]Precedence (in order from least to most authoritative): hardcoded
baseline → audit-derived expansion (minus always_keep_libs) →
:drop_libs → :keep_libs. The user's :keep_libs is always
the last word.
Summary
Functions
Libs that audit-driven expansion is forbidden from auto-stripping.
Source of truth for the safety guardrail — see moduledoc.
Users can still force-strip these via :drop_libs in mob.exs.
Compute the final strip set. Returns a sorted, deduplicated list.
Hardcoded baseline of OTP libs mobile apps never need. Source of
truth for what mix mob.deploy --slim strips by default.
Apply the strip pass to an OTP bundle in place.
Types
@type slim_result() :: %{ steps: [step_info()], final_kb: non_neg_integer(), strip_set: [String.t()] }
@type step_info() :: %{ label: String.t(), before_kb: non_neg_integer(), after_kb: non_neg_integer() }
Functions
@spec always_keep_libs() :: [String.t()]
Libs that audit-driven expansion is forbidden from auto-stripping.
Source of truth for the safety guardrail — see moduledoc.
Users can still force-strip these via :drop_libs in mob.exs.
Compute the final strip set. Returns a sorted, deduplicated list.
Composition (low to high precedence)
hardcoded_prefixes/0— baseline.:audit_inputexpansion (when given), minusalways_keep_libs/0(the safety guardrail).:drop_libs— user-explicit force-strip.:keep_libs— user-explicit force-keep. Wins over everything.
Recognized opts
:keep_libs—[String.t()], force-keep (subtracts from set). Highest precedence — overrides every other source.:drop_libs—[String.t()], force-strip (adds to set). Higher precedence than the guardrail: a user can:drop_libsa lib that's inalways_keep_libs/0if they really mean it.:audit_input—MobDev.OtpAudit.report/0to expand the strip set with audit-derived libs:report.foreign_app_namesalways unions in (allow-list- validated by OtpAudit's:project_deps).- When the audit was run with
:trace_input:strippable_libs ∩ trace_strippable_libsunions in (both signals agree → high confidence).trace_strippable_libs \ strippable_libsunions in (trace-only — the megaco/snmp/diameter unblocking signal).
strippable_libsalone is NOT unioned without trace (NIF dispatch likeexqliteis statically invisible).- The expansion is then filtered through
always_keep_libs/0so trace lies (e.g. trace window missed sasl boot or crypto's lazy TLS calls) can't break production.
@spec hardcoded_prefixes() :: [String.t()]
Hardcoded baseline of OTP libs mobile apps never need. Source of
truth for what mix mob.deploy --slim strips by default.
@spec slim_bundle( Path.t(), keyword() ) :: {:ok, slim_result()}
Apply the strip pass to an OTP bundle in place.
Recognized opts:
:keep_libs,:drop_libs,:audit_input— seecompute_strip_set/1.:strip_set— short-circuit override. When set, every other opt that would feed into the computation is ignored. Primarily for tests.:on_step— optional callbackfn(step_info) -> any()invoked after each phase. Caller uses it for build-log output.
Returns {:ok, slim_result()}. The bundle is mutated in place; if a
phase fails, the bundle is in a partially-stripped state (matches the
pre-extraction behaviour, which had no rollback either).