Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ Full module documentation: [hexdocs.pm/mob_dev](https://hexdocs.pm/mob_dev).

---

## [0.5.15]

### Fixed
- **`mix mob.release --android --no-slim` now actually ships the full OTP tree.** The Android release stripped OTP libs unconditionally (`OtpAssetBundle.build/2` was called with no opts), so `--no-slim` was silently ignored on Android. `slim` is now threaded `build_aab → OtpAssetBundle.build(slim:)`; with `slim: false` the OTP tree ships untouched. Required for apps that run arbitrary user code at runtime (e.g. an embedded Livebook host doing `Mix.install`) — stripping any OTP lib (`inets`, `ssl`, `xmerl`, `runtime_tools`, …) is a latent crash when a user's deps need it. Default stays `slim: true`.
- **iOS `--no-slim` release passes App Store validation.** The always-on Apple-policy strip cleared `erts-*/bin` and `priv/bin` but missed standalone executables inside OTP libs (e.g. `erl_interface/bin/erl_call`), which App Store validation rejects (90171). Now `lib/*/bin/*` executables are stripped too (always on), keeping every lib's `.beam`/`.app` — so a full-OTP `--no-slim` bundle is still Apple-compliant.
- **Native builds no longer break on pre-plugin app scaffolding.** `native_build.ex` emitted `-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 its `build.zig` and Zig rejects the unknown `-D` flag. These flags (and the iOS plugin bootstrap) are now emitted only when plugins are activated; a plugin-aware `build.zig` defaults them to `""` so behaviour is unchanged there.

## [0.5.14]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion lib/mix/tasks/mob.release.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ defmodule Mix.Tasks.Mob.Release do

Mix.Task.run("compile")

case MobDev.ReleaseAndroid.build_aab() do
case MobDev.ReleaseAndroid.build_aab(slim: Keyword.get(opts, :slim, true)) do
{:ok, path} ->
Mix.shell().info("")
Mix.shell().info("#{green()}✓ Release build complete#{reset()}")
Expand Down
65 changes: 47 additions & 18 deletions lib/mob_dev/native_build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,22 @@ defmodule MobDev.NativeBuild do
"-Dndk_sysroot=#{ndk_sysroot()}",
"-Dapp_name=#{app_name}",
"-Dproject_root=#{project_root}",
"-Dexqlite_src=#{Path.join(project_root, "deps/exqlite/c_src")}",
"-Dplugin_c_nifs=#{plugin_c_nifs}",
"-Dplugin_zig_nifs=#{plugin_zig_nifs}",
"-Dplugin_jni_sources=#{plugin_jni_sources}"
"-Dexqlite_src=#{Path.join(project_root, "deps/exqlite/c_src")}"
]

# Only emit -Dplugin_* when non-empty. A plugin-aware build.zig defaults
# these to "" (so omitting them is equivalent there), but an app scaffolded
# before the plugin system has no such option and Zig rejects the unknown
# -D flag. Gating keeps non-plugin apps on older mob scaffolding building.
plugin_args =
for {name, val} <- [
{"plugin_c_nifs", plugin_c_nifs},
{"plugin_zig_nifs", plugin_zig_nifs},
{"plugin_jni_sources", plugin_jni_sources}
],
val != "",
do: "-D#{name}=#{val}"

# `project_nif_zig_args/1` also emits `-Dproject_root=` (since the
# iOS templates need it and don't have a baseline equivalent). The
# Android base_args above already supply it for the existing
Expand All @@ -289,6 +299,7 @@ defmodule MobDev.NativeBuild do

args =
base_args ++
plugin_args ++
nif_args_no_root ++
nxeigen_zig_args_android(nxeigen_archive) ++
tflite_zig_args_android(tflite_build)
Expand Down Expand Up @@ -2203,19 +2214,28 @@ defmodule MobDev.NativeBuild do
# declare. Raises with the full list of drifts when any are found.
MobDev.Plugin.Validator.raise_on_capability_drift!(activated_plugins)

# Generated bootstrap Swift gets compiled alongside the plugins' own
# Swift files, so it lands in the same `-Dplugin_swift_files` arg.
# That keeps the build.zig template surface unchanged — one flag, one
# split-and-compile loop — and means the bootstrap function ends up in
# MobApp-Swift.h's module the same way plugin views do.
bootstrap_path = generate_ios_plugin_bootstrap(build_dir)
# Generated bootstrap Swift gets compiled alongside the plugins' own Swift
# files via -Dplugin_swift_files. But that flag (and the bootstrap, which
# makes it always non-empty) only matters when plugins are activated: an app
# scaffolded before the plugin system has no plugin_swift_files option in
# ios/build.zig, and mob's pre-plugin Swift never calls the bootstrap. So
# when nothing is activated, skip the bootstrap and leave the flags empty
# (omitted below) — keeping non-plugin apps on older mob building.
{plugin_swift_files, plugin_frameworks} =
if activated_plugins == [] do
{"", ""}
else
bootstrap_path = generate_ios_plugin_bootstrap(build_dir)

plugin_swift_files =
(MobDev.Plugin.Merge.swift_files(activated_plugins) ++ [bootstrap_path])
|> Enum.join(",")
swift =
(MobDev.Plugin.Merge.swift_files(activated_plugins) ++ [bootstrap_path])
|> Enum.join(",")

plugin_frameworks =
activated_plugins |> MobDev.Plugin.Merge.ios_frameworks() |> Enum.join(",")
frameworks =
activated_plugins |> MobDev.Plugin.Merge.ios_frameworks() |> Enum.join(",")

{swift, frameworks}
end

base_args = [
"build",
Expand All @@ -2230,14 +2250,23 @@ defmodule MobDev.NativeBuild do
"-Denif_keepalive=#{Path.join(build_dir, "enif_keepalive.c")}",
"-Dproject_ios_dir=#{Path.expand("ios")}",
"-Dmodule_name=#{display_name}",
"-Dproject_swift_sources=#{project_swift_sources}",
"-Dplugin_swift_files=#{plugin_swift_files}",
"-Dplugin_frameworks=#{plugin_frameworks}"
"-Dproject_swift_sources=#{project_swift_sources}"
]

# Omit -Dplugin_* when empty (no plugins) so apps on pre-plugin ios/build.zig
# don't choke on unknown options; a plugin-aware build.zig defaults them to "".
plugin_args =
for {name, val} <- [
{"plugin_swift_files", plugin_swift_files},
{"plugin_frameworks", plugin_frameworks}
],
val != "",
do: "-D#{name}=#{val}"

with {:ok, nif_args} <- project_nif_zig_args(:ios_sim) do
args =
base_args ++
plugin_args ++
nif_args ++
mlx_zig_args(mlx_dir) ++
nxeigen_zig_args_ios(nxeigen_archive) ++
Expand Down
19 changes: 13 additions & 6 deletions lib/mob_dev/otp_asset_bundle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,19 @@ defmodule MobDev.OtpAssetBundle do

case System.cmd("cp", ["-R", source <> "/.", staging], stderr_to_stdout: true) do
{_, 0} ->
prefixes = compute_strip_set(opts)
strip_otp_libs(staging, prefixes)
strip_standalone_execs(staging)
strip_static_archives(staging)
strip_source_and_headers(staging)
strip_beam_chunks(staging)
# slim: false ships the OTP tree untouched. Required for apps that run
# arbitrary user code at runtime (e.g. an embedded Livebook host doing
# Mix.install) — we can't know which OTP libs (inets, ssl, xmerl,
# runtime_tools, …) a user's deps will need, so stripping any is unsafe.
if Keyword.get(opts, :slim, true) do
prefixes = compute_strip_set(opts)
strip_otp_libs(staging, prefixes)
strip_standalone_execs(staging)
strip_static_archives(staging)
strip_source_and_headers(staging)
strip_beam_chunks(staging)
end

{:ok, staging}

{out, _} ->
Expand Down
6 changes: 6 additions & 0 deletions lib/mob_dev/release.ex
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,12 @@ defmodule MobDev.Release do
find "$OTP_BUNDLE" -type f \( -name "*.so" -o -name "*.a" \) -delete
find "$OTP_BUNDLE" -path "*/priv/bin/*" -type f -delete
find "$OTP_BUNDLE/$ERTS_VSN/bin" -type f -delete 2>/dev/null || true
# Standalone executables inside OTP libs (e.g. erl_interface/bin/erl_call)
# are also rejected by App Store validation (90171) and can't exec on iOS
# anyway. Remove every lib/*/bin/* executable while keeping the libs'
# .beam/.app — so a --no-slim full-OTP bundle (needed for runtime Mix.install)
# still passes Apple's "no standalone executables" rule.
find "$OTP_BUNDLE/lib" -path "*/bin/*" -type f -delete 2>/dev/null || true

# ── Slim strips (gated; opt out with `mix mob.release --no-slim`) ──
# Each step echoes a tagged header AND the bundle size delta so a
Expand Down
9 changes: 5 additions & 4 deletions lib/mob_dev/release_android.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ defmodule MobDev.ReleaseAndroid do
`{:error, reason}`.
"""
@spec build_aab(keyword()) :: {:ok, Path.t()} | {:error, String.t()}
def build_aab(_opts \\ []) do
def build_aab(opts \\ []) do
app_name = Mix.Project.config()[:app] |> to_string()
slim = Keyword.get(opts, :slim, true)

with :ok <- check_android_project(),
log("Ensuring Android OTP runtime..."),
{:ok, otp_arm64} <- MobDev.OtpDownloader.ensure_android("arm64-v8a"),
log("Staging OTP tree + app BEAMs..."),
{:ok, staging} <- stage_otp_tree(otp_arm64, app_name),
log("Building otp.zip (stripping unused OTP libs)..."),
{:ok, info} <- build_zip(staging),
{:ok, info} <- build_zip(staging, slim),
_ = File.rm_rf!(staging),
log(
" #{info.zipped_files} files, " <>
Expand Down Expand Up @@ -228,10 +229,10 @@ defmodule MobDev.ReleaseAndroid do

# ── otp.zip ──────────────────────────────────────────────────────────────────

defp build_zip(staging) do
defp build_zip(staging, slim) do
zip_path = Path.expand(Path.join(@app_assets, "otp.zip"))
File.mkdir_p!(Path.dirname(zip_path))
MobDev.OtpAssetBundle.build(staging, zip_path)
MobDev.OtpAssetBundle.build(staging, zip_path, slim: slim)
end

# ── Gradle ───────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule MobDev.MixProject do
def project do
[
app: :mob_dev,
version: "0.5.14",
version: "0.5.15",
elixir: "~> 1.19",
description: "Development tooling for the Mob mobile framework",
source_url: "https://github.com/genericjam/mob_dev",
Expand Down
21 changes: 21 additions & 0 deletions test/mob_dev/otp_asset_bundle_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ defmodule MobDev.OtpAssetBundleTest do
end
end

test "slim: false ships the OTP tree untouched (no lib stripping)" do
source = build_fake_otp_tree()

target_zip =
Path.join(System.tmp_dir!(), "mob_otp_test_noslim_#{:rand.uniform(999_999)}.zip")

try do
assert {:ok, _} = OtpAssetBundle.build(source, target_zip, slim: false)
{listing, 0} = System.cmd("unzip", ["-l", target_zip], stderr_to_stdout: true)

# With slim: false, libs that the default strip would remove survive —
# required for apps running arbitrary user code (Mix.install) where any
# OTP lib (inets, ssl, runtime_tools, …) might be needed at runtime.
assert listing =~ "lib/megaco-1.0.0/"
assert listing =~ "lib/wx-1.0.0/"
after
File.rm_rf!(source)
File.rm(target_zip)
end
end

test "respects :keep_prefixes — opts can re-add a stripped lib" do
source = build_fake_otp_tree()
target_zip = Path.join(System.tmp_dir!(), "mob_otp_test_keep_#{:rand.uniform(999_999)}.zip")
Expand Down
Loading