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
37 changes: 29 additions & 8 deletions lib/mob_dev/native_build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5358,12 +5358,19 @@ defmodule MobDev.NativeBuild do
# lacks: without an app-level table the plugin's `<module>_nif_init` links yet
# never registers, so `load_nif/2` falls back to dlopen and the NIF is
# `:nif_not_loaded` on device. So when plugins contribute NIFs and the app has
# no table yet, create one (zig — the default format). Public for tests (and
# exercised on every `build_all`).
# no table yet, create one (zig — the default format). The same goes for the
# opt-in built-in NIFs mob's core tables lack (`mix mob.enable tflite |
# nxeigen`): their guarded entries are in `StaticNifs.default_nifs/0` but not
# in the core Android table, so a blank app with nx_tflite_mob linked TFLite
# yet never registered it (MOB-394). Public for tests (and exercised on every
# `build_all`).
@spec regen_driver_tab!() :: :ok
def regen_driver_tab! do
formats =
__regen_formats__(__driver_tab_formats__(&File.exists?/1), __plugin_nifs_present__())
__regen_formats__(
__driver_tab_formats__(&File.exists?/1),
__plugin_nifs_present__() or builtin_nifs_without_core_entry_enabled?()
)

for fmt <- formats do
paths = Mix.Tasks.Mob.RegenDriverTab.target_paths(fmt)
Expand All @@ -5388,7 +5395,7 @@ defmodule MobDev.NativeBuild do
existed? = File.exists?(path)
File.mkdir_p!(Path.dirname(path))
File.write!(path, src)
verb = if existed?, do: "regenerated (was stale)", else: "created (plugin NIFs)"
verb = if existed?, do: "regenerated (was stale)", else: "created"
IO.puts(" ✓ driver_tab #{verb}: #{path}")
end
end
Expand All @@ -5404,16 +5411,30 @@ defmodule MobDev.NativeBuild do
MobDev.Plugin.Merge.nifs(MobDev.Plugin.activated()) != []
end

# Is a guarded built-in NIF of `StaticNifs.default_nifs/0` that mob's core
# tables don't register enabled, judged by the same dep check the native
# build uses to compile and link it? EMLX is left out: it is iOS-only and
# mob's core iOS table already registers it, so a generated table gains
# nothing and would make old app build files (no `tflite_static` option)
# fail to compile it. sqlite3_nif is in the core iOS table too.
defp builtin_nifs_without_core_entry_enabled? do
Enum.any?(MobDev.StaticNifs.default_nifs(), fn
%{guard: "MOB_STATIC_TFLITE_NIF"} -> tflite_in_project?()
%{guard: "MOB_STATIC_NX_EIGEN_NIF"} -> nxeigen_in_project?()
_ -> false
end)
end

@doc false
# Pure kernel: which driver_tab formats to (re)generate. An app with existing
# tables keeps its format(s). An app with NONE normally generates nothing (it
# links against mob's core table) — UNLESS a NIF-bearing plugin is active, in
# which case it needs its own table (core + plugin entries), defaulting to
# zig. Public for tests.
# links against mob's core table) — UNLESS a NIF-bearing plugin or an opt-in
# built-in NIF is active, in which case it needs its own table (core + those
# entries), defaulting to zig. Public for tests.
@spec __regen_formats__([:zig | :c], boolean()) :: [:zig | :c]
def __regen_formats__([], true), do: [:zig]
def __regen_formats__([], false), do: []
def __regen_formats__(existing, _plugin_nifs?), do: existing
def __regen_formats__(existing, _needs_app_table?), do: existing

defp warn_host_requirements! do
case __host_requirements_warning__(
Expand Down
68 changes: 68 additions & 0 deletions test/mob_dev/native_build_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,74 @@ defmodule MobDev.NativeBuildTest do
File.rm_rf!(dir)
end
end

# MOB-394: a blank app with `mix mob.enable tflite` had no generated table,
# so it linked against mob's core Android table, which has no TFLite entry:
# NxTfliteMob loaded as :nif_not_loaded on the device.
test "creates a table with the TFLite entry for a host with nx_tflite_mob" do
host = regen_host([{:nx_tflite_mob, "nx_tflite_mob"}])

for table <- ["driver_tab_android.zig", "driver_tab_ios.zig"] do
assert File.read!(Path.join(host, "priv/generated/#{table}")) =~
"extern fn tflite_nif_nif_init()"
end
end

test "creates a table with the NxEigen entry for a host with nx_eigen" do
host = regen_host([{:nx_eigen, "nx_eigen"}])

for table <- ["driver_tab_android.zig", "driver_tab_ios.zig"] do
assert File.read!(Path.join(host, "priv/generated/#{table}")) =~
"extern fn nx_eigen_nif_init()"
end
end

# EMLX is iOS-only and mob's core iOS table already registers it.
test "creates no table for a host whose only built-in NIF is in the core table" do
host = regen_host([{:emlx, "emlx"}])

refute File.exists?(Path.join(host, "priv/generated"))
end

# A host Mix project (path deps on stub packages) with regen_driver_tab!/0
# run inside it. Returns the host dir; the scratch root goes on test exit.
defp regen_host(deps) do
suffix = System.unique_integer([:positive])
root = Path.join(System.tmp_dir!(), "mob_driver_tab_host_#{suffix}")
on_exit(fn -> File.rm_rf!(root) end)
host = Path.join(root, "host")
File.mkdir_p!(host)

dep_specs =
for {app, dir} <- deps do
dep_dir = Path.join(root, dir)
File.mkdir_p!(dep_dir)

File.write!(Path.join(dep_dir, "mix.exs"), """
defmodule Stub#{suffix}#{Macro.camelize(dir)}.MixProject do
use Mix.Project
def project, do: [app: #{inspect(app)}, version: "0.1.0"]
end
""")

"{#{inspect(app)}, path: #{inspect(dep_dir)}}"
end

File.write!(Path.join(host, "mix.exs"), """
defmodule Host#{suffix}.MixProject do
use Mix.Project
def project, do: [app: :host, version: "0.1.0", deps: [#{Enum.join(dep_specs, ", ")}]]
end
""")

Mix.ProjectStack.on_clean_slate(fn ->
Mix.Project.in_project(:"host#{suffix}", host, fn _ ->
assert :ok = NativeBuild.regen_driver_tab!()
end)
end)

host
end
end

describe "__merge_android_manifest_components__/2" do
Expand Down
Loading