Cross-compiles a plugin's lang: :cpp_archive NIF — a set of C/C++ sources —
into lib<module>.a for one target ABI, and verifies the NIF-init symbol is
present. The archive is then static-linked into the app's single signed native
binary (same slot as crypto.a / libnx_eigen.a).
This is the generic, manifest-driven generalization of MobDev.NxEigenNif:
the sources, include dirs, CXXFLAGS, and expected symbol all come from the
plugin's Merge.static_archives/2 spec rather than being hardcoded. It exists
because the single-source :c/:zig plugin NIF path (compiled inline by
build.zig) can't express a C++ build with external headers (Eigen/Fine),
RTTI/exceptions, a per-arch hardening flag set, and an archive output.
Why static-link (same as every on-device NIF)
- Android. A separately-
dlopen'd.soinheritsRTLD_LOCAL, hiding the BEAM'senif_*symbols →on_loadfails. Static-linking lets the BEAM resolve the init symbol viadlsym(RTLD_DEFAULT)on the app binary. - iOS. The App Store forbids loading unsigned dylibs /
dlopen; the NIF must already be in the signed binary.
What the builder forces vs. what the plugin controls
The builder forces only -fPIC (a static lib linked into a shared object must
be position-independent) and the compile/output flags (-c -o). Everything
else — C++ standard, optimization, visibility, exceptions, the
-DSTATIC_ERLANG_NIF_LIBNAME=… that fixes the emitted init symbol, and any
Android hardening — is the plugin's via :cxxflags / :cxxflags_android /
:cxxflags_ios (and :cflags* for .c sources), so a plugin author keeps
control of its own ABI. One exception: Arm-only flags (-mbranch-protection=)
in the Android lists are dropped on :android_x86_64, where clang rejects
them, so one Android flag list can serve every ABI.
C and Objective-C sources
A .c or .m entry in :sources is compiled by the target's C driver
(clang, not clang++) with :cflags / :cflags_android / :cflags_ios in
place of the CXXFLAGS (forced -fPIC, the target ABI flags and the -I
includes still apply). Mixed C/C++ libraries such as ggml need this: their
.c files are not valid C++. An Objective-C NIF (.m) archived beside an
Objective-C++ renderer (.mm) needs it too: clang++ compiles a .m as
Objective-C, and a C++ flag such as -std=gnu++17 is then a hard error.
Every other extension goes to clang++, which picks the language from the
extension (.cpp/.cc C++, .mm Objective-C++).
Object files are named <basename>-<hash of the source path>.o, so two sources
with the same basename in different directories (ggml's quants.c and
arch/arm/quants.c) both land in the archive. Sources compile in parallel.
Summary
Functions
Archive filename for a NIF module — lib<module>.a.
Compile + archive + verify a cpp_archive spec (from Merge.static_archives/2)
for one target_id. Returns {:ok, %{module:, archive:, objects:}} or a
tagged error.
Whether a source compiles through the C driver (.c, .m) rather than clang++. Pure.
Assemble the full CFLAGS for one target's .c / .m sources: the same shape as
cxxflags/3 (forced -fPIC, target ABI flags, plugin flags, -I includes in
order) but reading :cflags and :cflags_android / :cflags_ios, so no C++
flag (-std=c++17, -fno-rtti, …) reaches the C compiler. Pure.
Parse nm output and confirm the expected init symbol is exported (T).
Mirrors MobDev.NxEigenNif.check_symbol_present/3. Pure.
Assemble the full CXXFLAGS for one target: forced -fPIC + the target's
intrinsic ABI flags (e.g. armv7 flags for android_arm32), then the plugin's
base :cxxflags, then the target's platform-specific flags
(:cxxflags_android / :cxxflags_ios), then -I for each resolved include
dir (order preserved). Arm-only flags (-mbranch-protection=) are dropped on
:android_x86_64. Pure — silent flag drops are the regression class this
whole module guards against, so it's directly testable.
Object-file name for a source: <basename>-<8 hex of the source path's SHA-256>.o. Stable across builds, and distinct for same-named sources in
different directories (a plain <basename>.o let the second overwrite the
first, silently dropping its symbols from the archive). Pure.
Resolve a spec's :sources/:includes (a mix of absolute strings and
{:dep, name, subpath} tokens left by Merge.static_archives/2) to absolute
paths, resolving dep tokens against deps_path. Pure.
All target ABIs a cpp_archive can be built for.
Functions
Archive filename for a NIF module — lib<module>.a.
@spec build(map(), atom(), keyword()) :: {:ok, map()} | MobDev.Release.Errors.t()
Compile + archive + verify a cpp_archive spec (from Merge.static_archives/2)
for one target_id. Returns {:ok, %{module:, archive:, objects:}} or a
tagged error.
Options:
:out_dir— where the archive + per-arch objs land. Required.:erts_include— per-targeterts-VSN/include/dir (carrieserl_nif.h). Required.:deps_path— for resolving{:dep, …}include tokens (defaults toMix.Project.deps_path/0).:ndk_root— Android NDK root (Android targets; default derived).
Whether a source compiles through the C driver (.c, .m) rather than clang++. Pure.
Assemble the full CFLAGS for one target's .c / .m sources: the same shape as
cxxflags/3 (forced -fPIC, target ABI flags, plugin flags, -I includes in
order) but reading :cflags and :cflags_android / :cflags_ios, so no C++
flag (-std=c++17, -fno-rtti, …) reaches the C compiler. Pure.
@spec check_symbol_present(binary(), String.t(), Path.t()) :: :ok | MobDev.Release.Errors.t()
Parse nm output and confirm the expected init symbol is exported (T).
Mirrors MobDev.NxEigenNif.check_symbol_present/3. Pure.
Assemble the full CXXFLAGS for one target: forced -fPIC + the target's
intrinsic ABI flags (e.g. armv7 flags for android_arm32), then the plugin's
base :cxxflags, then the target's platform-specific flags
(:cxxflags_android / :cxxflags_ios), then -I for each resolved include
dir (order preserved). Arm-only flags (-mbranch-protection=) are dropped on
:android_x86_64. Pure — silent flag drops are the regression class this
whole module guards against, so it's directly testable.
Object-file name for a source: <basename>-<8 hex of the source path's SHA-256>.o. Stable across builds, and distinct for same-named sources in
different directories (a plain <basename>.o let the second overwrite the
first, silently dropping its symbols from the archive). Pure.
Resolve a spec's :sources/:includes (a mix of absolute strings and
{:dep, name, subpath} tokens left by Merge.static_archives/2) to absolute
paths, resolving dep tokens against deps_path. Pure.
@spec targets() :: [atom()]
All target ABIs a cpp_archive can be built for.