Current section

Files

Jump to
mob_dev lib mix tasks mob.regen_driver_tab.ex
Raw

lib/mix/tasks/mob.regen_driver_tab.ex

defmodule Mix.Tasks.Mob.RegenDriverTab do
use Mix.Task
alias MobDev.StaticNifs
@shortdoc "Regenerate priv/generated/driver_tab_*.zig from :static_nifs"
@moduledoc """
Regenerates the per-app static-NIF table source files in
`priv/generated/driver_tab_ios.zig` and `priv/generated/driver_tab_android.zig`.
These files are linked **before** `libbeam.a` so they override BEAM's empty
built-in `erts_static_nif_tab[]`. Without them, `load_nif/2` falls back to
`dlopen`, which is broken on iOS (App Store rejects bundled `.dylibs`) and
on Android (RTLD_LOCAL hides parent's `enif_*` symbols from children).
mix mob.regen_driver_tab # regenerate both platforms (default: Zig)
mix mob.regen_driver_tab --format c # emit driver_tab_*.c instead (hand-editable C)
mix mob.regen_driver_tab --check # verify on-disk matches manifest, exit non-zero on drift
## Format
Zig is the default as of Phase 6a iter 4 — it uses comptime gates
instead of `#ifdef` for guarded NIFs (e.g. iOS `sqlite3_nif`
device-only), and Zig's `export` keyword produces the same C-ABI
symbols libbeam.a expects. `--format c` is still supported for
anyone who wants a hand-editable C dispatch table. Both formats
produce equivalent behavior at link time.
C NIF authors are unaffected by the default: their `c_src/<name>.c`
files compile via the usual path, and the Zig dispatch table calls
their `<name>_nif_init()` function through standard C ABI (`extern
fn <name>_nif_init() callconv(.c)`).
## Where the NIF list comes from
`MobDev.StaticNifs.default_nifs/0` baked-in defaults are merged with any
`:static_nifs` set in `mob.exs`:
config :mob_dev,
static_nifs: [
%{module: :my_native, archs: [:all]}
]
See `MobDev.StaticNifs` for the entry schema and arch values.
## Why a Mix task and not a build-time generator
The output is committed to the app's repo so reviewers can see what's in
the static-link surface. It also lets non-Mob build tools (e.g. xcodebuild
invoked outside `mix mob.deploy`) pick up the file as plain source. The
task is fast (deterministic file generation) so re-running it on every
`mix compile` is cheap.
## Drift detection
`mob.doctor` runs `--check` mode and reports any drift between the
manifest and the on-disk files. CI can do the same.
"""
@ios_c_path "priv/generated/driver_tab_ios.c"
@android_c_path "priv/generated/driver_tab_android.c"
@ios_zig_path "priv/generated/driver_tab_ios.zig"
@android_zig_path "priv/generated/driver_tab_android.zig"
@impl Mix.Task
def run(args) do
{opts, _, _} = OptionParser.parse(args, strict: [check: :boolean, format: :string])
nifs = resolved_nifs()
format = parse_format(opts[:format])
case validate_all(nifs) do
:ok -> :ok
{:error, msg} -> Mix.raise(":static_nifs invalid — #{msg}")
end
ios_src = StaticNifs.generate(:ios, nifs, format: format) |> IO.iodata_to_binary()
android_src = StaticNifs.generate(:android, nifs, format: format) |> IO.iodata_to_binary()
paths = target_paths(format)
if opts[:check] do
check_mode(ios_src, android_src, paths)
else
write_mode(ios_src, android_src, paths)
end
end
# Default is :zig as of Phase 6a iter 4 — for projects whose
# `ios/build.zig` was generated with the addZigObject helper (mob_new
# post-iter-2). Older projects without that helper fall back to :c
# because their build.zig's addCObject → addCSourceFile path can't
# compile a .zig source (Zig 0.17-dev rejects the conflicting
# -Mroot=./ + positional .zig combination). Pass `--format c`/`--format zig`
# explicitly to override the auto-detect.
defp parse_format(nil), do: detect_default_format()
defp parse_format("zig"), do: :zig
defp parse_format("c"), do: :c
defp parse_format(other) do
Mix.raise("Unknown --format #{inspect(other)}. Supported: zig, c.")
end
defp detect_default_format do
case File.read("ios/build.zig") do
{:ok, content} ->
if String.contains?(content, "addZigObject") do
:zig
else
:c
end
_ ->
# No project-side build.zig (rare — e.g. running from a fresh
# template directory). Default to Zig; if there's no build
# pipeline yet the file is harmless until one's added.
:zig
end
end
@doc false
@spec resolved_nifs() :: [StaticNifs.nif_entry()]
def resolved_nifs do
# mob.exs isn't auto-imported by Mix.Config — every other task that
# reads from it goes through Config.Reader.read! directly. Match that
# pattern here. Application.get_env stays as a secondary source so
# MIX_CONFIG=... or programmatic Application.put_env still wins for
# tests that want to bypass the file.
user =
MobDev.Config.load_mob_config()
|> Keyword.get(:static_nifs, Application.get_env(:mob_dev, :static_nifs, []))
StaticNifs.resolve(user)
end
@doc false
@spec target_paths() :: %{ios: String.t(), android: String.t()}
def target_paths, do: target_paths(:c)
@doc false
@spec target_paths(:c | :zig) :: %{ios: String.t(), android: String.t()}
def target_paths(:c), do: %{ios: @ios_c_path, android: @android_c_path}
def target_paths(:zig), do: %{ios: @ios_zig_path, android: @android_zig_path}
defp validate_all(nifs) do
Enum.reduce_while(nifs, :ok, fn entry, :ok ->
case StaticNifs.validate_entry(entry) do
:ok -> {:cont, :ok}
{:error, msg} -> {:halt, {:error, "#{inspect(entry)}: #{msg}"}}
end
end)
end
defp write_mode(ios_src, android_src, %{ios: ios_path, android: android_path}) do
File.mkdir_p!(Path.dirname(ios_path))
File.mkdir_p!(Path.dirname(android_path))
write_if_changed(ios_path, ios_src)
write_if_changed(android_path, android_src)
end
defp write_if_changed(path, new_content) do
case File.read(path) do
{:ok, ^new_content} ->
Mix.shell().info(" ✓ #{path} (unchanged)")
_ ->
File.write!(path, new_content)
Mix.shell().info(" ✓ #{path} (regenerated)")
end
end
defp check_mode(ios_src, android_src, %{ios: ios_path, android: android_path}) do
drifts =
[{ios_path, ios_src}, {android_path, android_src}]
|> Enum.filter(fn {path, expected} ->
File.read(path) != {:ok, expected}
end)
case drifts do
[] ->
Mix.shell().info("✓ driver_tab files match :static_nifs")
:ok
paths ->
msg =
paths
|> Enum.map(fn {path, _} -> " - #{path}" end)
|> Enum.join("\n")
Mix.raise(
"driver_tab drift detected — these files don't match :static_nifs:\n#{msg}\n\n" <>
"Run `mix mob.regen_driver_tab` to fix."
)
end
end
end