Packages
mob_dev
0.5.1
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
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