Validates plugin manifests, in two stages (see MOB_PLUGINS.md).
Single-plugin (validate_plugin/3, behind mix mob.validate_plugin): a
plugin author's pre-publish check — required fields, referenced files exist,
mob_version satisfied by the installed mob, plus advisory warnings.
Cross-plugin (cross_validate/1, run by mob_dev when activating): the
collision checks that only make sense across the set of activated plugins —
no two may claim the same component atom, screen route, or migration namespace.
Every result is %{errors: [...], warnings: [...]}. Errors fail loud;
warnings are advisory. Both stages are pure given their inputs (the only I/O
is File.exists?/1 for path checks, isolated in validate_plugin/3).
Summary
Functions
Activation-time capability check across every activated plugin.
The usage descriptions Merge.plist_keys/1 combines from more than one
plugin, as {key, plugin names} sorted by key: keys at least two plugins
declare with different strings. Used to tell the host that its permission
prompt text was put together from several plugins and how to word it itself.
The cross-plugin conflict surface: every MobDev.Plugin.Merge gatherer
(each combines N plugins' manifest contributions into one space) classified by
what happens when two plugins clash. The map is keyed by the Merge gatherer
name, and conflict_surface_test asserts it covers every public gatherer —
so a new shared-resource field can't be added without classifying its conflict
behavior here (the systematic guarantee that multiples compose safely).
Cross-plugin collision validation across the activated set.
The top-level keys an Info.plist sets ([] when the file is absent or
unparseable, which keeps every collision an error). The XML is parsed
structurally (MobDev.IosLayoutPlist.top_level_keys/1); a binary plist is
converted with plutil first when it is available.
Runs activated_capability_errors/1 and raises a Mix.raise/1 (with a
user-readable bullet list) when any plugin's source references a
capability not in its manifest. No-op when every plugin is clean.
Runs cross_validate/2 over the activated {plugin_dir, manifest} pairs and
Mix.raise/1s on any collision, so a native build stops before codegen
instead of letting one plugin's registration silently overwrite another's.
Info.plist keys the project's own ios/Info.plist already sets are exempt
(the project's value wins; see cross_validate/2).
Collects the file paths a manifest references, relative to the plugin root.
Verifies every <uses-permission android:name="X"/> declared in
AndroidManifest.xml fragments under the plugin's tree appears in
manifest.android.permissions.
Single-plugin validation, run from the plugin's own project directory.
Verifies every import X in the plugin's Swift sources resolves to either
a base iOS framework or one declared in manifest.ios.frameworks.
Types
Functions
Activation-time capability check across every activated plugin.
plugins is the MobDev.Plugin.activated/0 shape — a list of
{plugin_dir, manifest} pairs. For each plugin, runs
validate_swift_imports/2 and validate_android_permissions/2 and
returns the flattened error list, each entry prefixed with the
plugin's :name so the user can tell which plugin tripped.
Empty list means every activated plugin's source matches its manifest's
declared capability surface. The build hooks this into iOS + Android
builds via raise_on_capability_drift!/1, which raises a Mix.raise/1
with the full list when any drift is found.
@spec combined_usage_descriptions([{atom() | Path.t(), map() | nil}]) :: [ {String.t(), [String.t()]} ]
The usage descriptions Merge.plist_keys/1 combines from more than one
plugin, as {key, plugin names} sorted by key: keys at least two plugins
declare with different strings. Used to tell the host that its permission
prompt text was put together from several plugins and how to word it itself.
The cross-plugin conflict surface: every MobDev.Plugin.Merge gatherer
(each combines N plugins' manifest contributions into one space) classified by
what happens when two plugins clash. The map is keyed by the Merge gatherer
name, and conflict_surface_test asserts it covers every public gatherer —
so a new shared-resource field can't be added without classifying its conflict
behavior here (the systematic guarantee that multiples compose safely).
Kinds:
{:collision, [{label, extractor}]}— two plugins contributing the same value is a build error.extractorreturns the values one manifest claims.{:namespaced, reason}— per-plugin namespaced; no cross-plugin collision.{:union, reason}— set-union semantics; duplicates are harmless.{:build_time, reason}— collision is caught later in the native build (e.g. font/migration planners inMobDev.Plugin.Assets), not here.{:derived, reason}— returns values derived from another classified field; introduces no new namespace of its own.
Cross-plugin collision validation across the activated set.
plugins is a list of {name, manifest} for the activated plugins (tier-0
no-manifest plugins, i.e. manifest == nil, contribute nothing and are
ignored). The first element identifies the plugin in error messages: a
plugin directory (as MobDev.Plugin.activated/0 returns) is reported as its
priv/mob_plugin.exs path, anything else via inspect/1.
Options:
:host_plist_keys— the keys the host's ownios/Info.plistsets. For a scalar value the host's wins over every plugin's (plugin keys only fill gaps), so two plugins declaring one of these keys is not a conflict: neither value lands. A key any plugin declares as an array still conflicts: arrays merge into the host's array instead of yielding to it.
A privacy usage description (Manifest.usage_description_key?/1) that every
declaring plugin gives as a string is never a conflict either:
Merge.plist_keys/1 combines the sentences, so no plugin's reason is lost
(combined_usage_descriptions/1).
The top-level keys an Info.plist sets ([] when the file is absent or
unparseable, which keeps every collision an error). The XML is parsed
structurally (MobDev.IosLayoutPlist.top_level_keys/1); a binary plist is
converted with plutil first when it is available.
Runs activated_capability_errors/1 and raises a Mix.raise/1 (with a
user-readable bullet list) when any plugin's source references a
capability not in its manifest. No-op when every plugin is clean.
Lives in the validator (not NativeBuild) so the iOS-sim and iOS-device build paths can both call it as a one-liner, and so it stays unit-testable.
Also runs the Phase 2 signature gate
(MobDev.Plugin.SignatureGate.raise_on_signature_drift!/1) at the top
— fails fast on a tampered or untrusted plugin before any capability
analysis runs. The unsigned-plugin banner is printed afterwards so it
surfaces on every successful invocation.
Runs cross_validate/2 over the activated {plugin_dir, manifest} pairs and
Mix.raise/1s on any collision, so a native build stops before codegen
instead of letting one plugin's registration silently overwrite another's.
Info.plist keys the project's own ios/Info.plist already sets are exempt
(the project's value wins; see cross_validate/2).
Collects the file paths a manifest references, relative to the plugin root.
Pure. Covers the concrete file declarations (nifs.native_dir,
android.bridge_kt, android.jni_source, ios.swift_files). Component
view_module/composable are type/function names, not paths, so they are
not included here.
Verifies every <uses-permission android:name="X"/> declared in
AndroidManifest.xml fragments under the plugin's tree appears in
manifest.android.permissions.
Scope (deliberate): scans priv/native/android/**/*.xml for
<uses-permission/> entries — declarations the plugin author explicitly
wrote. Does not attempt to infer permissions from Kotlin/Java API usage
(the static-analysis rabbit hole MOB_PLUGIN_SECURITY.md warns against).
Returns [] when the plugin ships no AndroidManifest fragment.
Single-plugin validation, run from the plugin's own project directory.
installed_mob_version is the version of :mob resolved in the plugin's
deps (a string), or nil to skip the compatibility check.
Verifies every import X in the plugin's Swift sources resolves to either
a base iOS framework or one declared in manifest.ios.frameworks.
See MOB_PLUGIN_SECURITY.md (Layer 2 — capability enforcement at compile
time): the manifest is the contract; the plugin's source cannot reach for a
framework that isn't manifest-declared. Catches drift at validate time
rather than at link time, where the error points at the linker invocation
and not at the manifest that produced it.
Returns a list of error strings (empty when the manifest passes). Skips
plugins with no ios.swift_files. Files referenced by the manifest but
missing on disk are flagged by add_path_errors/3, not here — this check
only opens files that exist.