MobDev.OtpAudit.Slim (mob_dev v0.5.2)

Copy Markdown View Source

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:

  1. apple_binaries.so / .a and priv/bin/* (Apple-policy parity: no standalone executables in the bundle)
  2. prefix_libs — every lib/<name>-* whose <name> is in the computed strip set (see compute_strip_set/1)
  3. foreign_appslib/{toy_,test_,mob_test,scratch_}* (other projects' code that snuck into a shared OTP cache)
  4. dedup_versions — when the same lib appears at multiple versions, keep only the highest
  5. src_and_headers — every src/ and include/ directory
  6. beam_chunks:beam_lib.strip_release/1 drops 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

slim_result()

@type slim_result() :: %{
  steps: [step_info()],
  final_kb: non_neg_integer(),
  strip_set: [String.t()]
}

step_info()

@type step_info() :: %{
  label: String.t(),
  before_kb: non_neg_integer(),
  after_kb: non_neg_integer()
}

Functions

always_keep_libs()

@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_strip_set(opts \\ [])

@spec compute_strip_set(keyword()) :: [String.t()]

Compute the final strip set. Returns a sorted, deduplicated list.

Composition (low to high precedence)

  1. hardcoded_prefixes/0 — baseline.
  2. :audit_input expansion (when given), minus always_keep_libs/0 (the safety guardrail).
  3. :drop_libs — user-explicit force-strip.
  4. :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_libs a lib that's in always_keep_libs/0 if they really mean it.
  • :audit_inputMobDev.OtpAudit.report/0 to expand the strip set with audit-derived libs:
    • report.foreign_app_names always unions in (allow-list- validated by OtpAudit's :project_deps).
    • When the audit was run with :trace_input:
      • strippable_libs ∩ trace_strippable_libs unions in (both signals agree → high confidence).
      • trace_strippable_libs \ strippable_libs unions in (trace-only — the megaco/snmp/diameter unblocking signal).
    • strippable_libs alone is NOT unioned without trace (NIF dispatch like exqlite is statically invisible).
    • The expansion is then filtered through always_keep_libs/0 so trace lies (e.g. trace window missed sasl boot or crypto's lazy TLS calls) can't break production.

hardcoded_prefixes()

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

slim_bundle(otp_bundle, opts \\ [])

@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 — see compute_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 callback fn(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).