All notable changes to mob_dev are documented here.
Format: Keep a Changelog. Versioning: SemVer.
Full module documentation: hexdocs.pm/mob_dev.
[0.6.27] - 2026-08-26
Added
mix mob.doctordetects the pre-fix Android component-event JNI mismatch (seemob's 0.7.25 CHANGELOG entry) in an already-generated project'sMobBridge.ktand points at the fix — this repo doesn't auto-patch hand-editable native source (seedecisions/2026-08-25-detect-dont-autopatch-native-source.md), so an existing app stays broken until a human ports the template fix or regenerates; this surfaces it viamix mob.doctorinstead of anUnsatisfiedLinkErroron first real interaction with a tier-2 native component. (MOB-98)
Fixed
mix mob.connectwaited the full ~500ms iOS accessibility settle delay even against physical devices, where it does nothing —IOS.enable_accessibility/1shells out toxcrun simctl, simulator-only tooling. Now gated on simulator type as well as platform, matching the same predicate already used elsewhere in this file for the identical reason. (MOB-99)
[0.6.26] - 2026-08-25
Added
MobDev.Plugin.Manifest.validate/1now warns on unknown top-level keys inpriv/mob_plugin.exs.mix mob.pluginspreviously reported "vetting clean" for a manifest carrying keys that meant nothing — the validator only checked the shape of known keys, never that every key was one of them. Astyles:/default_style:key (which belongs in the separatepriv/mob_style.exsmanifest — seemob'sMOB_PLUGINS.md) validated silently and then went nowhere. The warning is spec-versioned: a manifest declaring aplugin_spec_versionthismob_devdoesn't support already errors separately (check_spec_version), so an unrecognized key there doesn't pile a second, noisier warning on top — forward-compat stays intact. For a supported spec version, an unrecognized key is almost always author error, and now says so instead of vanishing.
Found via a real report from someone building a style plugin against this system.
[0.6.25] - 2026-08-25
Added
- Plugin-declared default fonts, part of the mob custom-fonts feature
(see
mob'sMOB_FONTS.md). A capability plugin'spriv/mob_plugin.exsmanifest can now carry adefault_font: %{family:, file:}field —familyis the iOS PostScript name (can't be derived from the filename, so the plugin author supplies it),filemust be one of the plugin's ownassets.fontsentries.MobDev.Plugin.Manifest's validation pipeline checks the shape and the file reference;MobDev.Plugin.Merge.default_font/1gathers it;MobDev.Plugin.Validator's conflict surface makes two plugins declaring one a build error (only one can win, sinceMob.Theme.fonts[:default]is a single slot);MobDev.Plugin.RuntimeManifest.build/1converts the survivor to the on-device%{ios:, android:}spec read at boot byMob.Plugins.default_font/0. Mob.Font.android_resource_name/1is now called from mob core instead of a local copy. The plugin asset planner (MobDev.Plugin.Assets) used to carry its own filename→Android-resource-name normalization, kept in sync with mob's runtime copy (Mob.Theme.font/2) only by convention. Both now call the one implementation inmob, so a build-time bundled font and its runtime theme token can no longer drift apart.
Fixed
{:mob, path: "../mob"}only resolved on a machine with a siblingmobcheckout — brokemob_dev's own CI, which has no such directory (** (Mix) Cannot compile dependency :mob because it isn't available). Switched to a git dependency during font development, and now to the proper Hex constraint (~> 0.7.25) now that mob has published the version carryingMob.Font.
[0.6.24] - 2026-08-19
Fixed
mix mob.deploy's ERTS preflight silently treated a non-debuggable APK as healthy, so every subsequentrun-as-based BEAM push failed and got swallowed too.ensure_erts_on_device/2only pattern-matched the preflightrun-as ... ls ...output for"No such file"/"not found"(missing-ERTS signatures). Arun-as: package not debuggable: <pkg>response — the normal signal for an APK built as a release/non-debug variant — matched neither, so the check returned:okanddeploy_androidproceeded straight intopush_beams_android/push_priv_android/push_exqlite, each of which shells out torun-as #{pkg} tar xf ... 2>/dev/null; true. That trailing; trueexists to swallow Toybox tar's benign chown-to-macOS-UID exit code, but it also swallowed therun-asfailure itself — somix mob.deployreported✓while nothing reached the device.ensure_erts_on_device/2now detects anyrun-as:-prefixed response and returns a clear, actionable error (rebuild viamix mob.deploy --nativeto get a debuggable install) before any push is attempted.
[0.6.23] - 2026-07-12
Fixed
cpp_archive/ nx_eigen NDK resolution honorsANDROID_HOME.MobDev.Plugin.CppArchiveandMobDev.NxEigenNifhardcoded~/Library/Android/sdk/ndk/<ver>(+ thedarwin-x86_64host), ignoringANDROID_HOME/ANDROID_SDK_ROOT— so alang: :cpp_archiveplugin build failed withNDK toolchain not foundwherever the NDK lives elsewhere (CI, a shared SDK), unlike the mainNativeBuildpath. Extracted sharedMobDev.NdkVersion.{root,host,toolchain_bin,sysroot}/0(on the existing env-awaresdk_root/0) and routedcpp_archive,nx_eigen_nif, andnative_build'sndk_sysrootthrough them — one source of truth, so the NDK path can't diverge again. (MOB-89)
[0.6.22] - 2026-07-11
Added
ios_release_screenshotconfig — opt mob's public-APIscreenshotNIF into release builds. Companion to mob 0.7.20 (mob#71), which carvesscreenshot/3into#if !MOB_RELEASE || defined(MOB_ENABLE_SCREENSHOT). Settingconfig :mob_dev, ios_release_screenshot: trueexportsMOB_ENABLE_SCREENSHOT=1, and the generatedrelease_device.shcompilesmob_nif.mwith${MOB_ENABLE_SCREENSHOT:+-DMOB_ENABLE_SCREENSHOT}— so a release build can ship the screenshot NIF (letting an agent SEE a shipped app's screen to error-correct) while the private synthetic-input NIFs (tap/type) stay stripped. Default is unchanged and byte-identical (screenshot stays stripped); enabling it is a deliberate choice because the NIF captures the app's own window with no OS prompt or indicator. (#39)
[0.6.21] - 2026-07-08
Fixed
- iOS app icons are flattened opaque so App Store upload validation accepts
them. A transparent source icon (a common design — a rounded badge on
transparent corners) produced transparent iOS icons and tripped altool error
90717 ("Invalid large app icon … can't be transparent or contain an alpha
channel").
IconGenerator.write_ios_iconsnow composites an alpha-bearing source onto an opaque background (Image.flatten!) before resizing — using the explicit--adaptive-bgcolour when given, else the same colour sampled for the Android adaptive background, so both platforms stay consistent; an opaque source is left untouched. Android icons keep their transparency (adaptive foregrounds + legacy launcher icons need it), and the bundled fallbackmob_logoiOS assets were pre-flattened. Verified: a transparent-badge icon now passes App Store validation and reaches TestFlight. (#37)
[0.6.20] - 2026-07-07
Fixed
- iOS release builds now compile + link activated-plugin NIFs.
mix mob.release --iosproduced a binary that failed to link for any app with NIF plugins —Undefined symbols: _<module>_nif_init, referenced from _erts_static_nif_tab in driver_tab_ios.o. iOS statically links every NIF into the single app binary (nodlopenunder the App Store sandbox), so the generateddriver_tab_ios.creferences each activated plugin's<module>_nif_init, but the release path (release.ex→release_device.sh) hand-compiled a fixed object list and never touched plugins (the dev path already compiled them viabuild.zig -Dplugin_c_nifs).release_env/2now emitsMOB_PLUGIN_IOS_NIF_SOURCES+MOB_PLUGIN_IOS_FRAMEWORKSfromMobDev.Plugin.activated()(via the new pure, testedMobDev.Release.plugin_ios_build_env/1);release_device.shcompiles each plugin NIF source with-DSTATIC_ERLANG_NIF_LIBNAME=<basename>+-fmodules(framework autolink) and links the objects plus the declared frameworks. Verified end-to-end: a 10-plugin app links and the IPA validates against its App Store distribution profile. Scope: coversnif_sources(lang: :c | :objc) +ios_frameworks; pluginswift_filesand:cpp_archivestatic archives on the release path remain a follow-up. (#36) - Test:
MobDev.Release.HelpersTest's git fixture no longer inherits the ambient git environment. Run inside a git hook (.githooks/pre-push), git exportsGIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE; the fixture'sgitcommands inherited them and operated on the outer repo instead of their tmpdir, crashing setup — so the suite passed standalone but failed only on push. The fixture now clears those vars on everygitinvocation. (#36)
[0.6.19] - 2026-07-07
Added
- Plugins can contribute AndroidManifest
<application>components andres/files. Two new optionalandroid:manifest keys —manifest_application_snippets(XML fragments spliced into the app's<application>block, idempotent perandroid:name) andres_files(plugin-relative paths copied into the appres/tree at their derivedres/<type>/<file>destination, path-contained + host-clobber-guarded + signed). This closes the gap that forced plugins needing a<service>/<receiver>/<provider>+ resource (e.g.mob_nfc's HCEHostApduService+apduservice.xml) to make it a manualhost_requirement. NewMobDev.Plugin.Merge.android_manifest_snippets/1+android_res_files/1gatherers (classified in the cross-plugin conflict surface), manifest-schema validation, andNativeBuildsplice + copy (ledger-pruned likebridge_kt). (MOB-39)
Changed
- Plugin contributions merged into host-owned build files are now reversible
(going forward). Plugin
<uses-permission>s,<application>components, and Gradle deps are fenced in a regenerated-each-build managed region (MobDev.Plugin.ManagedBlock) inAndroidManifest.xml/build.gradle, so removing a plugin drops its fenced contributions (no more dangling<service>/ orphan permission / orphan dep) while hand-authored content outside the fence is untouched. Idempotent (strip(place(x)) == x); de-dupes against existing entries;stripis orphan-BEGIN-safe (never deletes host lines between a stray marker and a real region). Forward-only: entries an older mob_dev already appended unfenced are indistinguishable from hand-authored ones, so they're left in place (not double-added, not auto-removed) — regenerate the app, or remove them by hand, to fence them. (MOB-40)
Fixed
mix mob.new_pluginno longer scaffolds plugins pinned to the abandonedmob ~> 0.6.MobDev.Plugin.Scaffoldhard-coded{:mob, "~> 0.6"}in the generatedmix.exsandmob_version: "~> 0.6"in every tier's manifest, so a freshly scaffolded plugin could not activate against the published mob 0.7.x (installed :mob 0.7.x does not satisfy mob_version "~> 0.6"). The requirement is now derived at scaffold time from the mob actually resolved in the project (Scaffold.detect_mob_requirement/0), falling back to a single@fallback_mob_requirementconstant ("~> 0.7") when mob isn't loadable.mix.exsand the manifest always agree. AScaffoldtest pins the default and asserts a generated manifest validates against a matching mob version, so the pin can't silently lag a future mob release. (#21)
[0.6.18] - 2026-07-05
Added
mix mob.provisioncan authenticate via an App Store Connect API key, so an unattended user — a CI runner or a headless agent account with no GUI login — can provision without a signed-in Xcode Apple ID account. SetAPP_STORE_CONNECT_KEY_ID,APP_STORE_CONNECT_ISSUER_ID, andAPP_STORE_CONNECT_API_KEY_PATH(the downloadedAuthKey_<id>.p8) and the task passes them toxcodebuildas-authenticationKeyID/-authenticationKeyIssuerID/-authenticationKeyPath. All three or none (a partial set raises, naming what's missing); with none set the signed-in Xcode account is used exactly as before. Pure, testedMix.Tasks.Mob.Provision.asc_auth_args/1. Signing still needs the certificate + private key in an unlocked keychain — the API key only authorizes the profile/device calls. (#31)
[0.6.17] - 2026-06-29
Fixed
mix mob.connectcrashed on a Mac set up only for iOS. The dist-port collision check shelled out toadb forward --list, andSystem.cmd("adb", …)raises:enoentfor a missing binary rather than returning a non-zero exit — inside a linkedTask, so the crash propagated and killed the whole connect.MobDev.Tunnel.run_adb/1now resolvesadbviaSystem.find_executable/1first (mirroringDiscovery.Android.list_devices/0) and returns{:error, …}when it's absent, so the port scan degrades to "no forwards" and iOS-only Macs work with no Android platform-tools installed. (#29)
Added
mix mob.connect --ios-only/--android-onlyrestrict discovery to one platform — handy when a phone for another project is plugged in. Set it project-wide inmob.exswithconfig :mob_dev, platforms: [:ios]. New pure helpersMobDev.Config.parse_platforms/1andMix.Tasks.Mob.Connect.resolve_platforms/2. (#29)
[0.6.16] - 2026-06-24
Added
- Plugins can contribute array-valued iOS plist keys (e.g.
UIBackgroundModes).apply_plugin_plist_keys!previously skipped any non-scalarios.plist_keysvalue as unsupported; a list value now merges into the host Info.plist array (creating it if absent, appending only the missing string entries, deduped) instead of clobbering it. So one plugin can addbluetooth-centralwhile another (e.g.mob_background) keepsaudio. Merge decision extracted to the pure, testedNativeBuild.plist_array_additions/2.
[0.6.15] - 2026-06-23
Security
- Bumped
req0.5.18 → 0.6.2 (pullsfinch0.22.0 → 0.23.0), clearing EEF-CVE-2026-49755 (HIGH) and EEF-CVE-2026-49756 (LOW) flagged bymix mob.security_scan.reqis a transitive dep (viaigniter); the bump stays withinigniter's~> 0.5requirement.
[0.6.14] - 2026-06-23
Added
:extra_static_libshook on:static_nifsentries — a Mob app can now link external per-ABI static archives alongside its project NIF archives. Some project NIFs intentionally declareexternsymbols and don't host-link their backing archive (avoiding a host/device archive mismatch duringmix compile); this lets the native app link resolve those symbols against the correct per-ABI.a. Entries require concrete per-ABI keys (:ios_sim,:ios_device,:android_arm64,:android_arm32,:android_x86_64), and the matching archive paths are appended to the existing-Dproject_rust_libs=link argument.-D<module>_static=trueis emitted only on the ABIs where the entry applies, and iOS project-NIF filtering is now per-ABI so a device-only guarded entry doesn't leak into simulator args. Also passes Zigler 0.16's required generated-build flags when re-driving staged Zig NIF builds and resolves Zigler dotfile sources withmatch_dot: true. Verified on a physical SM-T577U (arm64-v8a) tablet via a Ghostty VT NIF. (#24)
[0.6.13] - 2026-06-20
Changed
mix mob.deploy --nativenow preserves on-device app data when the signing key matches. The Android install path previously ran an unconditionaladb uninstallbeforeadb install, which wipedMOB_DATA_DIR(on-device identity, screen stores) on every native deploy — even an in-place update signed with the same (e.g. committed) debug keystore. It now attemptsadb install -rfirst and only falls back to uninstall + install when the in-place update is genuinely rejected (INSTALL_FAILED_UPDATE_INCOMPATIBLEfrom a signature mismatch,INSTALL_FAILED_VERSION_DOWNGRADE, etc.). Apps that pin a committed debug keystore now keep their identity across--nativeredeploys. Decision logic extracted toNativeBuild.needs_clean_reinstall?/2and unit-tested.
Fixed
mix mob.deploy --native --androidnow fails fast with the real cause whenzigis missing (landed in code before 0.6.13; previously undocumented). The Android JNI build is driven bybuild.zig; withzigoff PATH it used to print a yellow "skipping build.zig step" warning and fall through to a CMake fallback that references C sources mob 0.7+ no longer ships, dying ~150 lines later with a misleadingCannot find source file: .../mob_nif.c. It now aborts before Gradle with an actionable message (install zig 0.15.x, verify withmix mob.doctor) whenbuild.zigis present,zigis absent, and the legacy C sources are gone. Decision extracted toNativeBuild.zig_build_plan/3. (#20)
[0.6.12] - 2026-06-19
Fixed
- 16 KB page-size alignment enforced at build time for every app. Android
15+ devices use 16 KB memory pages and Google Play requires every bundled
.soto have 16 KB-aligned LOAD segments. The-Wl,-z,max-page-size=16384link flag lives in the app'sbuild.zig, which is copied once atmix mob.newand never regenerated — so apps generated before the template carried the flag kept linking 4 KB-aligned.soand failed Play.mix mob.deploy --nativenow reads the app'sbuild.zigand, if its-sharedlink lacks the flag, injects it before linking (idempotent — a no-op when already present, e.g. the current mob_new template or a hand-fixed app). Pure core ininject_page_size_flag/1.
[0.6.11] - 2026-06-19
Added
mix mob.adopt— installs Mob into an existing Phoenix project, the Igniter-based install-into-existing counterpart tomix mob.new(which generates from scratch). Composable: the orchestrator runs the sub-installersmob.adopt.{deps,bridge,screen,mob_app,mob_exs,native,finalize}, each invokable independently. Default LV-bridge mode wireswindow.mobthrough a LiveViewphx-hookand generates amob_app.exthat boots the host Phoenix endpoint on-device (SQLite Repo assumed);--no-live-viewgenerates a thin-client shell whose WebView opens a deployed server. Pre-1.0: refuses loudly (via Igniter issues) on umbrella / non-Phoenix / heavily-customisedapp.jsor root layout / non-SQLite LV hosts rather than risk breaking the app. The native Android/iOS trees (--android/--ios) render from mob_new's templates and require the mob_new archive installed (mix archive.install hex mob_new) — mob_new stays the single source of native templates; the Elixir-side adoption needs no archive. Contributed as mob_new#8 by @ken-kost and relocated here — adopt is an Igniter task that mutates an existing project (likemob.add_nif/mob.enable), so it belongs in mob_dev (a Hex dep), not in mob_new (a self-contained Mix archive that can't carry Igniter). Seedecisions/2026-06-19-mob-adopt-lives-in-mob_dev.md. The shared patcher/generator helpers are duplicated from mob_new intoMobDev.Adopt.{Patcher,Generator}pending the Phase-5 Igniter reunification.
[0.6.10] - 2026-06-19
Added
- Plugin
:cpp_archiveNIFs — ship a C++ static library (e.g. an Nx backend) from a plugin. Manifest-driven cross-compile +--whole-archivestatic link (rule #11), with<module>_nif_initsymbol verification and duplicate-init cross-validation. (#18)
Fixed
- cpp_archive builds now fail fast with a named error on an unsupported
Android ABI (e.g. the x86_64 emulator) instead of silently skipping — which
previously deferred an unresolved
<module>_nif_initto an on-device link failure. - The plugin manifest now requires a lowercase
:moduleatom for:cpp_archiveentries (fail-loud instead of fail-open /libnil.a).
[0.6.9] - 2026-06-18
Fixed
mix mob.publish --androidnow commits when Google requireschangesNotSentForReview=true. Uploading a release while the app is under policy review (or otherwise can't auto-send for review) made the Play Edits:commitfail with HTTP 400 ("Please set the query parameter changesNotSentForReview to true"), discarding the whole edit so nothing reached the track.commit_edit/3now detects that 400 and retries the commit with?changesNotSentForReview=true; the changes land on the track and are sent for review from the Play Console UI. Verified uploading Io v13 to the internal track.
[0.6.8] - 2026-06-18
Added
mix mob.connect --only <serial>(alias--device/-d, repeatable). Restricts the run to devices whose serial/udid contains the given substring. Without it, connect attaches to every running device, so one slow or locked device (typically a plugged-in physical iPhone whose app restart blocks) could stall the whole session before any node connected. Verified end-to-end against a single Android phone:mix mob.connect --only ZY22CRLMWKtunnels, restarts, and connectslivebook_mob_android_zy22crlmwk@127.0.0.1on its serial-derived port 9633, then RPC into the device BEAM works (read live state, eval code).
[0.6.7] - 2026-06-18
Fixed
mix mob.connectreliability — dist ports keyed by device serial, not run index. The Mac runs one shared EPMD; assigning ports as9100 + indexmeant every project's first device claimed 9100, so two phones (or two projects' device-0) registered the same port andadb forward tcp:9100could only reach one — the other silently timed out. Ports are now derived from the device serial (Tunnel.serial_base_port/1, a crc32 hash into 9100..9899) and bumped past any port another live node/forward already holds (assign_dist_port/2). A given phone always gets the same unique port across runs and projects, and deploy and connect agree on it.Tunnel.setup/2→setup/1(port is now serial-derived, not index-passed).- Stale-tunnel cleanup.
Tunnel.setupremoves the device's own old forwards first (scoped to that serial), so prior runs no longer leave duplicate/wrong forwards that poison the next connect. - Real diagnostics on connect failure. A timed-out node now reports why — app not running / Standby-killed, dist never registered in EPMD, registered at a different port, no forward, or cookie mismatch — instead of a black-box "timed out".
[0.6.6] - 2026-06-18
Fixed
- Android native build skips ABIs the app's
build.zigdoesn't handle instead of hard-failing. mob_dev builds arm64-v8a/armeabi-v7a/x86_64 by default, but an app's app-ownedbuild.zig(copied atmix mob.newtime) may predate x86_64 support (mob_new < 0.4.5) and reject-Dabi=x86_64. That used to fail the whole native build — aborting before theio.mob.plugin.MobPluginBootstrapregen, so the next gradle build then failed on an unresolved bootstrap. Now each ABI is pre-flighted against the build.zig (build_zig_supports_abi?/2) and unsupported ones are skipped with a warning (gradleabiFilterswouldn't ship them anyway). Real failures of a SUPPORTED ABI still halt the build.
[0.6.5] - 2026-06-17
Changed
- Default OTP runtime → Elixir 1.20.1 (
@otp_hash7d46fdd4→5c9c69fc). Same OTP-29 / erts-17.0 / OpenSSL 3.4.0 base; the bundled Elixir stdlib (elixir/logger/eex) is swapped rc.5 → 1.20.1 across all five tarballs (android, android-arm32, android-x86_64, ios-sim, ios-device), published as theotp-5c9c69fcrelease on GenericJam/mob.bundled_versions.exsadds the5c9c69fcbundle and flipsactive_hash. Backward compatible — apps pick up 1.20.1 on their nextmix mob.deploy(recompile against the new runtime). NOTE: the major.minor skew check treats rc.5 and 1.20.1 as both "1.20", so it does NOT warn on this transition; the stdlib swap is what makes beams load.
[0.6.4] - 2026-06-16
Added
- Android x86_64 emulator support (resolves GenericJam/mob#20). The
x86_64ABI is now wired throughout:OtpDownloader.ensure_android("x86_64")(#11), the x86_64 native build path (zig_build_android_objects,ensure_jni_libs,otp_dir_for_abi), and anandroid_x86_64target inmix mob.release.otpplus thescripts/release/*x86_64*build scripts. Theotp-android-x86_64-<hash>.tar.gzruntime is published on theotp-<hash>release. This is the slice x86_64 Linux / CI hosts need, where ARM emulation isn't available.
[0.6.3] - 2026-06-16
Fixed
- exqlite NIF symlink now picks the device's actual ABI. The Android
deployer hardcoded
lib/arm64for thesqlite3_nif.sosymlink target, so on a 32-bit (armeabi-v7a) device the link dangled, exqlite was:nif_not_loaded, and any generated app using ecto_sqlite3 crashed on boot. It now probeslib/<abi>/libsqlite3_nif.so(Android extracts only the active ABI). Found + fixed verifying the showcase on a 32-bit Moto E — SQLite migrations now run and the app boots.
[0.6.2] - 2026-06-15
Fixed
- Create an app-level driver_tab when a NIF-bearing plugin is active. A
generated app ships no drivertab and links against mob's core static-NIF
table — which has no plugin entries. So a plugin's
<module>_nif_initlinked but never registered, and the NIF was:nif_not_loadedon device (the home rendered, the Kotlin/permission bridge worked, but the actual capability call crashed).regen_driver_tab!now creates `priv/generated/driver_tab*.zig` (core + plugin entries) when plugins contribute NIFs and the app has none. Found + fixed verifying the showcase app on a physical Android phone and the iOS simulator (location demo now returns a real fix on both).
[0.6.1] - 2026-06-15
Fixed
- Prune orphaned plugin artifacts when a plugin is removed. Plugin tier-3
merges copy files into the host tree (bridge Kotlin into the Kotlin sourceSet,
migrations, images); these lingered after a plugin was dropped, and an
orphaned bridge
.ktcould break the Gradle compile.NativeBuild's kotlin / migration / image merges now ledger what they write (per concern, underpriv/generated/.mob_plugin_artifacts/) and delete what a prior build produced but the current one no longer does — so add/remove of a plugin is clean in both directions.
[0.6.0] - 2026-06-12
Added
- Style packages, tokens-only tier:
MobDev.Style(priv/mob_style.exs loader + validator),config :mob, :styles/:default_styleactivation, runtime-manifest emission (misconfiguration fails the build), andmix mob.styles. ui_componentsexpand:form honored (pure-Elixir composites): validated native-XOR-expand; expand-only plugins classify tier 2 but hot-push; the runtime manifest carriescompositesfor boot registration.mix mob.doctor: pre-plugin build.zig detection (missing-Dplugin_*options) and the host_requirements lane.host_requirementsmanifest key printed by every native build;mix mob.new_pluginscaffolds starter test suites for every tier.
Changed
- Native builds auto-regenerate the static-NIF driver_tab (was a checked-in artifact whose staleness produced runtime
:nif_not_loaded). mix mob.regen_plugin_manifestloads the host app first — spec-v2 generators may call host modules (mob_ash), not just read config.
Fixed
- Dep detection uses
Mix.Project.deps_paths, not stale_builddirs — ends the spurious MLX-404 downloads for apps that never dep emlx. - ExSlop is registered as a credo PLUGIN (it had silently never run).
[0.5.17]
Added
mix mob.new_pluginscaffolds a starter test suite for every tier (test/test_helper.exs+test/<name>_test.exs): tiers 1–4 get stdlib-only structural manifest checks (required keys, NIF stub loadable,native_direxists, screen modules compile) with a pointer atmix mob.validate_pluginfor the full validator; tier 0 gets a compile smoke test. New plugins start covered instead of starting at zero tests.- Plugin
host_requirementsmanifest key. A plugin can declare human-readable host-app obligations the build can't automate (e.g. the AndroidManifest<service android:foregroundServiceType="mediaProjection">fragment mob_screencast needs, or a captureFileProvider). The manifest validator enforces the shape,MobDev.Plugin.Merge.host_requirements/1gathers them, and every native build prints them as a warning block — previously forgetting the manual step built + booted clean and only failed at first feature use. mix mob.doctordetects pre-plugin build files. When plugins are activated but abuild.zig/build_device.zigdeclares nob.optionfor the-Dplugin_*flags the native build emits, doctor warns with the exact missing options — previously Zig rejected the unknown flag half a build in.
Changed
mix mob.deploy --nativeregenerates the static-NIF driver table on every build (whatever formats the project uses, zig and/or c), exactly like the runtime plugin manifest: it's derived state. A stale checked-indriver_tab_*used to link a newly activated plugin's<module>_nif_initwithout registering it, so every NIF call raised:nif_not_loadedat runtime with nothing pointing at the cause.
Added
mix mob.new_pluginscaffolds tiers 3 (multi-screen) and 4 (sub-app), not just 0–2. Tier 3 emits twoMob.Screenmodules + a:screens/:migrationsmanifest + a namespaced Ecto migration; tier 4 emits a lifecycle module + supervised worker + notification handler + settings editor screen + a:lifecycle/:settings/:notificationsmanifest. Generated manifests validate and modules compile against real mob.- Cross-plugin conflict detection.
MobDev.Plugin.Validator.conflict_surface/0classifies every merge gatherer;cross_validate/1fails the build when two activated plugins clash on any shared resource — screen route, component atom, iOS/Android native view key, migrationrepo_namespace, NIF module, Swift/JNI source basename, Android bridge class, iOS plist key, supervised worker name, or notification match. A completeness meta-test forces every new shared-resource field to be classified; a property-based fuzzer checks detection is sound + complete across random N-plugin sets. A single plugin declaring a cross-platform NIF (one iOS + one Android entry sharing a:module) is correctly not flagged.
Changed
mix mob.deploy --nativeregenerates the runtime plugin manifest (priv/generated/mob_plugins.exs) on every build, not only whenconfig :mob, :pluginschanges. Adding/changing a plugin's tier-3/4 sections previously shipped a stale manifest (the new sections silently didn't activate on device); it is now derived state, always rebuilt before bundling — like the driver table.
Fixed
- iOS simulator deploy now boots from a clean
mix mob.deploy --native(was device-only). Three gaps made the sim deploy incomplete vs the device path, so the sim crashed on boot even though the device worked:- The Elixir-distribution apps
elixir/loggerwere staged only underlib/<app>/ebin, which the sim'smob_beam.mdoesn't add to the code path — boot failed atensure_all_started(:elixir)with "elixir.app not found". They're now flattened into the flat BEAMS_DIR alongsideeex(which already needed this), where the path resolves. priv/was only partially staged (repo/migrations), soApplication.app_dir(:<app>, "priv/cacerts.pem")was:enoentandMob.Certs.load_cacerts!crashed the boot. The wholepriv/is now rsynced into the flat dir (cacerts,mix/hexebins, vendored static, …), matching the device release.Paths.sim_runtime_dir/0fell back to/tmp/otp-ios-simfor zig-based projects (noios/build.sh), but the runtime is synced to~/.mob/runtime/ios-sim— so the launcher and staging disagreed. It now recognizesios/build.zigand returns the default runtime dir. Verified: a cleanmob.deploy --nativeto an iPhone 11 Pro Max sim boots Io, Phoenix endpoint up, embedded Livebook home renders — no manual runtime fixups.
- The Elixir-distribution apps
[0.5.16]
Fixed
- Gate the plugin flags on the iOS device build path too. 0.5.15 gated the plugin-flag emission for Android and the iOS simulator build, but
zig_build_binary_ios_devicestill emitted-Dplugin_swift_files/-Dplugin_frameworks(and generated the bootstrap, makingplugin_swift_filesalways non-empty) unconditionally — somix mob.deploy --nativeto a physical iPhone broke on an app scaffolded before the plugin system (invalid option: -Dplugin_swift_files). The device path now mirrors the sim path: bootstrap + flags only when plugins are activated. Verifiedmix mob.deploy --nativeto a physical iPhone — full OTP, Phoenix endpoint up, LiveView connected, embedded Livebook home rendered.
[0.5.15]
Fixed
mix mob.release --android --no-slimnow actually ships the full OTP tree. The Android release stripped OTP libs unconditionally (OtpAssetBundle.build/2was called with no opts), so--no-slimwas silently ignored on Android.slimis now threadedbuild_aab → OtpAssetBundle.build(slim:); withslim: falsethe OTP tree ships untouched. Required for apps that run arbitrary user code at runtime (e.g. an embedded Livebook host doingMix.install) — stripping any OTP lib (inets,ssl,xmerl,runtime_tools, …) is a latent crash when a user's deps need it. Default staysslim: true.- iOS
--no-slimrelease passes App Store validation. The always-on Apple-policy strip clearederts-*/binandpriv/binbut missed standalone executables inside OTP libs (e.g.erl_interface/bin/erl_call), which App Store validation rejects (90171). Nowlib/*/bin/*executables are stripped too (always on), keeping every lib's.beam/.app— so a full-OTP--no-slimbundle is still Apple-compliant. - Native builds no longer break on pre-plugin app scaffolding.
native_build.exemitted-Dplugin_c_nifs/-Dplugin_zig_nifs/-Dplugin_jni_sources(Android) and-Dplugin_swift_files/-Dplugin_frameworks(iOS) unconditionally, but an app scaffolded before the plugin system has no such options in itsbuild.zigand Zig rejects the unknown-Dflag. These flags (and the iOS plugin bootstrap) are now emitted only when plugins are activated; a plugin-awarebuild.zigdefaults them to""so behaviour is unchanged there.
[0.5.14]
Fixed
- iOS release:
erl_errno_id_unknownshim written with a literal\n. The weak-stub line inrelease_device.shusedprintf '%s\\n'inside the~S(raw) heredoc, so bash received both backslashes andprintfwrote a literal backslash-nintoerl_errno_id_compat.c— clang then rejected the trailing}\nandmix mob.release --iosfailed. Nowprintf '%s\n'(one backslash) emits a real newline. Regression guard added torelease_script_test. - iOS release: clear preflight when
priv/generated/driver_tab_ios.cis missing. The release links a per-app static-NIF driver table, but the dev build uses the built-in Zig table andmix mob.regen_driver_tabdefaults to Zig, so a project that never ran it with--format cdied deep inrelease_device.shwith a crypticcc: no such file.build_ipanow fails early with the exact command to run (mix mob.regen_driver_tab --format c).
[0.5.13]
Fixed
- iOS deploy now ships the whole
priv/, not justpriv/repo/migrations+priv/static.MobDev.Release's iOS bundler copied only migrations andpriv/static, so apps that bundle extra runtime assets underpriv/—:mix/:hexebins for on-deviceMix.install, or a vendored library's ownpriv/(e.g. Livebook'spriv/static+priv/livebook) — silently never reached the device. Now rsyncs all ofpriv/, matching the Android deployer. Unblocks on-deviceMix.installand embedded Livebook on iOS. Verified on a physical iPhone:priv/mix/ebin(103 beams) andpriv/livebook/staticpresent on device, embedded Livebook serves, andMix.install([{:short_uuid, "~> 0.1"}])returns:ok.
[0.5.12]
Changed
- OTP runtime bumped to
7d46fdd4(Elixir 1.20.0-rc.5).@otp_hashnow points at theotp-7d46fdd4release: all four platform tarballs (ios-sim, ios-device, android, android-arm32) bundle Elixir 1.20.0-rc.5 matched to the OTP-29 erts. Completes the 1.19.5 to 1.20 runtime migration the bundled-versions manifest was already staged for. Verified end-to-end on a physical iPhone and a physical Android (Moto G):System.version1.20.0-rc.5, OTP 29,Mix.install([{:short_uuid, "~> 0.1"}])returns:okwith the dep compiled on-device.
Fixed
- iOS
{spawn, <linked-in driver>}now works (ertserts_open_driver). The iOS-build#ifdef __IOS__guard returned BADARG for anyopen_portwhose spawntype included the EXECUTABLE bit (i.e. plain{spawn, Name}), firing before the linked-in-driver name lookup. This brokeram_file({spawn, "ram_file_drv"}), and therefore `file:open(, [:ram]),erl_tarin-memory extract,hex_tarball.unpack, andMix.installon iOS. The guard now fires only when no linked-in driver matched the name. Bundled in the7d46fdd4` OTP tarballs.
[0.5.11]
Added
mob.exs :project_swift_sourcesconfig key — optional list of extra Swift sources to compile into the iOS app module alongside Mob's bridge sources. Threaded into bothzig_build_binary_ios_simandzig_build_binary_ios_deviceas-Dproject_swift_sources=<absolute,paths>. Comma-containing entries are rejected at the boundary; nil/[] is a no-op. Pairs with mob_new'sproject_swift_sourcesbuild hook (mob_new#5). Originally proposed by @dl-alexandre.
[0.5.10]
Added
mix mob.deploy --dist-port Nand--node-suffix Sflags — manual override path for the BEAM-distribution surface. When set, all targeted devices use the same value (use with--device <id>to be explicit). Nil falls back to per-device auto-allocation (Tunnel.dist_port(idx)+Discovery.Android.device_node_suffix/ SIMULATOR_UDID-derived suffix). Resolves theregister/listen error: no_reg_reply_from_epmdsymptom seen when running multiple sims/emulators of the same app concurrently for cross-platform visual comparison.MobDev.Devicestruct gains a:node_suffixfield for plumbing the override per-device alongside:dist_port. Nil keeps auto-derive.MobDev.Discovery.IOS.launch_app/3accepts:node_suffixopt and forwards asSIMCTL_CHILD_MOB_NODE_SUFFIXto the launched sim. Companion tomob 0.6.10'sMOB_NODE_SUFFIXsupport inmob_beam.m.MobDev.Discovery.IOS.build_simctl_env/2— pure helper extracted fromlaunch_app/3so override behaviour is unit-testable without spawningsimctl. 7 new tests cover dist_port + node_suffix override paths.
Changed
MobDev.Connector.restart_app/1pattern-matches:node_suffixfromDevicein both Android + iOS-sim variants, threading the value to the launchers.MobDev.Deployer.deploy_all/1accepts top-level:dist_port+:node_suffixopts; threaded throughdeploy_androidanddeploy_ios_simulator.
[0.5.9]
Changed
mix mob.enable tflitenow injects{:nx_tflite_mob, "~> 0.0.3"}(Hex) instead of the GitHub-branch form.nx_tflite_mobv0.0.3 went live on Hex with 16 integration tests + a reproducible Mac host build path (see its CHANGELOG). Downstream Mob apps now get version-pinned deps + cleanmix deps.treeoutput, instead of a transientgithub:checkout.
Notes
- The Mac host-build path in
nx_tflite_mobis for that package's own test suite, not for downstream consumers — production phone builds viamix mob.deploy --nativecontinue to use the prebuilt Android AAR + iOS xcframework that mob_dev'sMobDev.TfliteDownloaderfetches.
[0.5.8]
Added
- End-to-end
mix mob.enable tflite— what 0.5.7 promised as "lands in 0.5.8".MobDev.NativeBuildnow auto-detects the:nx_tflite_mobdep and threads the full TFLite path through Android + iOS sim + iOS device build pipelines:maybe_build_tflite/1→MobDev.TfliteDownloader.ensure/1+MobDev.TfliteNif.build/2for each target archtflite_zig_args_android/1emits-Dtflite_static=true -Dtflite_lib=…for the per-ABI Android linktflite_zig_args_ios/1emits-Dtflite_static=true -Dtflite_dir=… -Dtflite_framework_dir=…for the iOS linkcopy_tflite_runtime_lib_android/2dropslibtensorflowlite_jni.sointoandroid/app/src/main/jniLibs/<abi>/during the assemble step
copy_tflite_frameworks_ios/3(kept as future-compat hook) — see the iOS-deploy-fix gotcha below- 13 new tests covering the public NativeBuild plumbing
(
native_build_tflite_test.exs), bringing the TFLite suite to 76 passing total
Fixed
- iOS deploy: TFLite framework binaries are MH_OBJECT, not
MH_DYLIB. TFLite's iOS xcframework slices ship their binaries as
filetype=1 relocatable objects, which the linker statically pulls
into the app's main Mach-O at build time. Trying to embed them as
runtime
.frameworkbundles tripped iOS install twice during this cut: first on missing per-framework Info.plist (which CocoaPods generates), then on "code signature version no longer supported" (iOS 26+ rejects v1 signatures, and codesign only makes v3 sigs for MH_EXECUTE/MH_DYLIB). The fix is to do nothing — the framework search-path arg already covers everything at build time. - Resolve
:nx_tflite_mobviaMix.Project.deps_paths()rather thanPath.join(deps_path, "nx_tflite_mob"). The latter assumes the dep landed indeps/(hex / git deps do), butpath:deps consume in-place from the user's source tree.
Verified on real hardware
- Moto G Power 5G (BXM-8-256, Android 15): 75-117 ms YOLOv8n via
NNAPI /
mtk-gpu_shim - iPhone SE 3rd gen (A15, iOS 26.4): 24 ms YOLOv8n via Core ML → ANE (FP16 model; 214/385 nodes delegated)
[0.5.7]
Added
mix mob.enable tflite— wires TensorFlow Lite into a Mob project on iOS AND Android. Adds{:nx_tflite_mob, ...}to deps and generates<App>.TfliteInit(returns per-platform default delegate opts — NNAPI/mtk-gpu_shimon Android, Core ML delegate on iOS). The static-NIF table entry%{module: :tflite_nif, guard: "MOB_STATIC_TFLITE_NIF"}is registered inMobDev.StaticNifs.default_nifs/0, so the zig build picks it up automatically oncetflite_static=trueis set.MobDev.TfliteDownloader— fetchestensorflow-lite-2.16.1.aar(Maven Central, Android) andTensorFlowLiteC-2.17.0.tar.gz(dl.google.com, iOS) into~/.mob/cache/. HonoursMOB_CACHE_DIRfor test redirection andMOB_TFLITE_LOCAL_TARBALL_DIRfor offline iteration.MobDev.TfliteNif— cross-compilestflite_nif.c(from the:nx_tflite_mobdep) per-arch and archives aslibtflite_nif.afor static linking. MirrorsMobDev.NxEigenNifshape. Validates the produced symbol (tflite_nif_nif_init) before declaring success.
Bundle size impact: ~3-4 MB extracted (Android libtensorflowlite_jni.so),
~20-30 MB on iOS (TensorFlowLiteC + CoreML + Metal frameworks). Apps
that don't enable TFLite pay zero size cost — the guard keeps the
static-NIF table entry inactive.
End-to-end deploy (mob.deploy --native auto-build + runtime-lib
embedding) lands in 0.5.8; this release ships the building blocks.
[0.5.6]
Added
CLAUDE.md"Release flow" section pointing at the canonical process inmob/RELEASE.md(URL form so it resolves without a local mob checkout). mob_dev specifics: the pre-push hook additionally runsmix mob.security_scanhere (this is the only repo that ships the scanner), and the OTP tarball workflow stays separate frommix.exsversion bumps..githooks/pre-push— same script shipped in mob (cheap preflight always, release preflight whenmix.exschanged). Themob.security_scanstep is gated viamix helpavailability so the same hook script works in all three repos.
[0.5.5]
Fixed
- Android 15 segfault on launch (Pixel 7+, after the OS rolled out via OTA). Bumps
@otp_hashfrom550d7b78→d9045670to pick up OTP tarballs cross-compiled with-Wl,-z,max-page-size=16384. Without the flag, every.soin the bundled OTP runtime (crypto.so,asn1rt_nif.so,dyntrace.so, etc.) shipped with 4KB-aligned ELFPT_LOADsegments. Android 15 enforces 16KB alignment on devices with 16KB-page kernels and refuses to load misaligned libs, crashing the app at startup. New tarballs are 16KB-aligned (Align=0x4000).
[0.5.4]
Fixed
- HexDocs source links pointed at the non-existent
mainbranch — corrected tomasterso each</>glyph in generated docs opens the actual source file.
Added
.github/workflows/test.yml— runsmix test,mix format --check-formatted,mix credo --strict, andmix mob.security_scanon push to master and on every PR..github/workflows/release.yml— on tag push, creates a GitHub Release whose body is the matching## [X.Y.Z]section from this changelog.
[0.5.3]
Changed
guides/nifs.md— rewrote the "Nx backends on mobile" section to match the current state (mix mob.enable nxeigennow real,mix mob.enable mlxincludes the Metal GPU path on iOS device, EXLA "why not" preserved).guides/nifs.md— restructured the multi-Rust-NIF section to lead with filmor's preferred shape (one Rustler crate per app, multiple#[rustler::nif]functions inside it). Multi-crate static linking remains supported and documented as an escape hatch, with the specific tradeoffs called out.
[0.5.2]
Added
mix mob.enable nxeigen— wires NxEigen (Eigen C++ CPU backend) into a Mob app. Builds as a C++:static_nifsentry, cross-compiled per arch (arm64-ios,arm64-iossim,arm64-android,armv7a-android). FFT support uses Eigen's bundled kissfft.- EMLX Metal GPU enabled on iOS device.
lib/mob_dev/mlx_downloader.exnow fetches the Metal-enabledlibmlx.a+mlx.metallibbundle;lib/mob_dev/native_build.ex#maybe_bundle_mlx_metallib/2copies the precompiled kernel library into the .app at build time, soEMLX.Backendwithdevice: :gpuworks on device without runtime kernel compilation. scripts/release/mlx/ios_device_metal.sh+ supporting build scripts for producing the Metal-enabled tarball;scripts/release/mlx/patches/0001-ios-metal-build.patchpatches MLX 0.25.1's CMakeLists to switch SDK frommacosxtoiphoneosbased onCMAKE_SYSTEM_NAME.
[0.5.1] and earlier
Earlier releases predate this changelog; consult the tag list and the per-tag commit messages for history.