Per-feature device requirements + the validation that runs before
mix mob.deploy builds anything.
The instinct is to ship the latest-arch path because that's where the
upstream toolchains are easiest to integrate. The cost of that
instinct is silent failure: a user with an older / cheaper / 32-bit
device buys hardware, runs mix mob.deploy, sees a vague gradle
error, and walks away assuming Mob is broken. They never find out
the device was below our floor.
This module makes the floors explicit, declarable, and enforced:
feature_requirements/1— the data. What ABIs / SDK levels does a feature need? Where does the constraint come from upstream (Chaquopy, BeeWare, Apple)?enabled_features/1— what features does this project use? Inferred from the project's mix.exs / generated files, not from a flag the user has to remember to set.check_device/2— given a device's discovered properties + a list of enabled features, return:okor{:error, [%{feature, reason, device}]}.
mix mob.deploy calls these before invoking MobDev.NativeBuild so
the message a user sees on an unsupported device is:
✗ Moto e (armeabi-v7a, Android 10) cannot run this project:
- pythonx requires Android arm64-v8a or x86_64. Chaquopy
(the upstream Python distribution we bundle) dropped
32-bit Android support several releases ago.
To target this device, either disable Pythonx or use an arm64
device. See guides/support_matrix.md for the full floor.…rather than a build that silently produces an APK the device can't load.
Adding a new feature
When you add a feature that has device requirements distinct from
the base Mob floor, add a clause to feature_requirements/1 and a
detection clause to enabled_features/1. Don't bury the constraint
in build-time code — that's how silent failures happen.
Summary
Types
Full requirement spec for a feature.
An incompatibility found by check_device/2.
Requirement spec for a single feature on a single platform.
Functions
Returns the base device requirements every Mob app inherits.
Checks that device can run the given list of enabled features
(plus the base Mob floor).
Returns the list of features enabled in project_dir that have
non-base device requirements.
Returns the device requirements for a specific feature.
Renders an {:error, [incompatibility]} result as the human-readable
block printed by mix mob.deploy before exiting.
Types
@type feature_req() :: %{ android: platform_req() | :unsupported, ios: platform_req() | :unsupported }
Full requirement spec for a feature.
@type incompatibility() :: %{ device: MobDev.Device.t(), feature: atom(), reason: String.t() }
An incompatibility found by check_device/2.
@type platform_req() :: %{ :abis => [String.t()], :min_sdk => non_neg_integer(), optional(:reason) => String.t() }
Requirement spec for a single feature on a single platform.
Functions
@spec base_requirements() :: feature_req()
Returns the base device requirements every Mob app inherits.
@spec check_device(MobDev.Device.t(), [atom()]) :: :ok | {:error, [incompatibility()]}
Checks that device can run the given list of enabled features
(plus the base Mob floor).
Returns :ok if every requirement is satisfied, or
{:error, [incompatibility]} listing every reason the device is
unsupported. We collect every reason rather than short-circuiting so
the user sees the full picture, not a one-at-a-time game of
whack-a-mole.
Returns the list of features enabled in project_dir that have
non-base device requirements.
Inferred from project artifacts (deps in mix.exs, generated source
files), not from a flag — so a user can't accidentally bypass the
validation by forgetting an option.
@spec feature_requirements(atom()) :: feature_req() | nil
Returns the device requirements for a specific feature.
Returns nil for unknown features (caller should treat as base-only).
@spec format_error([incompatibility()]) :: String.t()
Renders an {:error, [incompatibility]} result as the human-readable
block printed by mix mob.deploy before exiting.
Groups by device so multi-feature failures on one device collapse into a single block, then re-displays the device summary line.