MobDev.Plugin.IosLinkCheck (mob_dev v0.7.26)

Copy Markdown View Source

Fails an iOS build early when a plugin's cpp_archive references a symbol nothing in the app defines.

The iOS links (mix mob.deploy's ios/build.zig / build_device.zig and mix mob.release --ios) all pass -dead_strip, and ld64 reports an undefined symbol only from code that survives dead-stripping. A reference in code the app never reaches therefore links everywhere, until a link without -dead_strip, or a change that makes that code reachable, fails at the worst moment (MOB-470: mobwhisper 0.1.1's ggml `dl*helpers passed every dev build and failedmix mob.release --ios`).

check/5 links the plugin's archive into a throwaway dylib without -dead_strip: every member of its own lib<module>.a (-force_load), plus the members of its prebuilt archives (MOB-427) that those pull in, as the app's link would. The symbols left unresolved must each be defined by something the app's link supplies, and the allow-list is derived from those inputs, not written down:

  • the BEAM: every symbol the target's ERTS static libraries define (<otp>/erts-*/lib/**/*.a; the enif_* API and the rest of ERTS),
  • mob core: the non-static C functions, globals and ObjC classes mob's iOS sources (<mob_dir>/ios/*.{m,mm,c}) define, e.g. mob_register_permission_handler,
  • the iOS SDK: every symbol a library or framework of the target's SDK exports (its .tbd stubs), since the app may link any of them, explicitly or through clang module autolinking.

Anything else is a dangling reference and fails the build, naming the plugin, the symbols and the archive members that reference them. See decisions/2026-10-13-ios-release-dead-strips-and-plugin-link-check.md.

Summary

Functions

Check one built plugin archive for target_id (:ios_sim or :ios_device).

The Mach-O names of what a C / ObjC source defines with external linkage: top-level non-static functions and globals, and @implementation classes. A text scan, for mob core's sources (which import a Swift header the build generates, so they can't be compiled on their own here). Pure.

The build error for dangling (symbols nothing in the app defines), with the archive members referencing each from referrers. Pure.

The symbols nm -m -u lists as left for dyld to look up (the ones the probe link could not resolve). Pure.

Which of names some .tbd under sdk_path exports, matched as whole words. Every library and framework the SDK ships has one.

How a Mach-O symbol is spelled in an SDK .tbd stub: ObjC class, metaclass, exception-type and ivar symbols by their bare class (or Class.ivar) name, everything else as is. Pure.

Functions

check(spec, target_id, archive, prebuilt, opts)

@spec check(map(), :ios_sim | :ios_device, Path.t(), [Path.t()], keyword()) ::
  :ok | {:error, String.t()}

Check one built plugin archive for target_id (:ios_sim or :ios_device).

spec is the MobDev.Plugin.Merge.static_archives/2 entry (:plugin, :module); archive its lib<module>.a; prebuilt the archives its prebuilt bundle contributes for the target. Options: :otp_dir (the target's OTP runtime, required) and :mob_dir (mob's checkout; mob core's symbols are allowed only when given).

Returns :ok, or {:error, message}.

defined_symbols(source)

@spec defined_symbols(String.t()) :: [String.t()]

The Mach-O names of what a C / ObjC source defines with external linkage: top-level non-static functions and globals, and @implementation classes. A text scan, for mob core's sources (which import a Swift header the build generates, so they can't be compiled on their own here). Pure.

error_message(spec, target_id, dangling, referrers)

@spec error_message(map(), atom(), [String.t()], %{
  required(String.t()) => [String.t()]
}) :: String.t()

The build error for dangling (symbols nothing in the app defines), with the archive members referencing each from referrers. Pure.

parse_dynamic_lookups(nm_output)

@spec parse_dynamic_lookups(String.t()) :: [String.t()]

The symbols nm -m -u lists as left for dyld to look up (the ones the probe link could not resolve). Pure.

sdk_exports(sdk_path, names)

@spec sdk_exports(Path.t(), [String.t()]) :: MapSet.t(String.t())

Which of names some .tbd under sdk_path exports, matched as whole words. Every library and framework the SDK ships has one.

tbd_name(symbol)

@spec tbd_name(String.t()) :: String.t()

How a Mach-O symbol is spelled in an SDK .tbd stub: ObjC class, metaclass, exception-type and ivar symbols by their bare class (or Class.ivar) name, everything else as is. Pure.