All notable changes to mob_dev are documented here.
Format: Keep a Changelog. Versioning: SemVer.
Full module documentation: hexdocs.pm/mob_dev.
[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.
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.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.