fix: register NIF compiler, add Nix shell NIF build, make NIF optional at boot

- Register :towerops_nif compiler in mix.exs so mix compile/test builds the C NIF
- Add 'make -C c_src' to nix devShell shellHook so NIF is pre-built
- Extract init_mib_resolution/0 from Application.start/2 (reduces CC)
- Wrap NIF calls in try/rescue ErlangError for graceful fallback
- Wrap test_helper.exs NIF calls in try/rescue ErlangError
- Add .nix-postgres-version to .gitignore
This commit is contained in:
Graham McInitre 2026-07-16 09:15:53 -05:00
parent b7d8ea9668
commit e6fba0c5a3
5 changed files with 72 additions and 56 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@
.nix-mix/
.nix-hex/
.nix-postgres/
.nix-postgres-version
.nix-redis/
.nix-services-started

View file

@ -41,46 +41,8 @@ defmodule Towerops.Application do
Towerops.Release.migrate()
end
# Configure MIB directories for NIF resolution (before supervisor starts)
Logger.info("Configuring MIB directories for NIF resolution...")
mib_dir = Application.app_dir(:towerops, "priv/mibs")
# Build list of all MIB directories (including subdirectories for vendor MIBs)
mib_dirs =
if File.dir?(mib_dir) do
# Get main directory plus all subdirectories
subdirs =
mib_dir
|> File.ls!()
|> Enum.map(&Path.join(mib_dir, &1))
|> Enum.filter(&File.dir?/1)
[mib_dir | subdirs]
else
[mib_dir]
end
# Set MIBDIRS environment variable for net-snmp (must be set BEFORE init)
# Join with colon for Unix-style path list
mibdirs_env = Enum.join(mib_dirs, ":")
System.put_env("MIBDIRS", mibdirs_env)
System.put_env("MIBS", "ALL")
Logger.info("MIB directories configured: #{length(mib_dirs)} directories")
# Add each directory to net-snmp's search path
Enum.each(mib_dirs, fn dir ->
:ok = ToweropsNative.load_mib_directory(dir)
end)
# Initialize the MIB library (loads all MIBs into memory)
case ToweropsNative.init_mib_library() do
result when result in ["initialized", "already_initialized"] ->
Logger.info("MIB library initialized: #{result}")
other ->
Logger.warning("Unexpected MIB library init result: #{inspect(other)}")
end
# Initialize MIB resolution via C NIF
init_mib_resolution()
# Load antenna pattern registry (priv/antennas/*.ant) into :persistent_term
Antenna.load_registry()
@ -152,6 +114,48 @@ defmodule Towerops.Application do
result
end
# Configure MIB resolution via C NIF. Handles missing NIF gracefully.
defp init_mib_resolution do
Logger.info("Configuring MIB directories for NIF resolution...")
mib_dir = Application.app_dir(:towerops, "priv/mibs")
mib_dirs =
if File.dir?(mib_dir) do
subdirs =
mib_dir
|> File.ls!()
|> Enum.map(&Path.join(mib_dir, &1))
|> Enum.filter(&File.dir?/1)
[mib_dir | subdirs]
else
[mib_dir]
end
mibdirs_env = Enum.join(mib_dirs, ":")
System.put_env("MIBDIRS", mibdirs_env)
System.put_env("MIBS", "ALL")
Logger.info("MIB directories configured: #{length(mib_dirs)} directories")
try do
Enum.each(mib_dirs, fn dir ->
:ok = ToweropsNative.load_mib_directory(dir)
end)
case ToweropsNative.init_mib_library() do
result when result in ["initialized", "already_initialized"] ->
Logger.info("MIB library initialized: #{result}")
other ->
Logger.warning("Unexpected MIB library init result: #{inspect(other)}")
end
rescue
_e in ErlangError ->
Logger.warning("ToweropsNative NIF not loaded — MIB resolution unavailable")
end
end
# Post-startup work runs only when the supervisor came up cleanly. The Task
# supervisor call is wrapped in try/catch so a transient noproc (the named
# supervisor may not be ready or may already be dying) cannot exit the boot

View file

@ -10,7 +10,7 @@ defmodule Towerops.MixProject do
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
compilers: [:phoenix_live_view] ++ Mix.compilers(),
compilers: [:towerops_nif, :phoenix_live_view] ++ Mix.compilers(),
prune_code_paths: false,
listeners: [Phoenix.CodeReloader],
dialyzer: dialyzer(),

View file

@ -326,6 +326,12 @@ mkShell {
mix local.hex --force --if-missing > /dev/null 2>&1
mix local.rebar --force --if-missing > /dev/null 2>&1
# Compile C NIF if not already built (uses Nix-provided gcc/make/net-snmp)
if [ ! -f "priv/towerops_nif.so" ]; then
echo "🔧 Building C NIF..."
make -C c_src
fi
# Auto-start services on first shell entry.
# Run in a detached subshell to prevent nix develop from waiting
# on daemonized PostgreSQL/Redis child processes.

View file

@ -18,22 +18,27 @@ if File.dir?(mib_dir) do
System.put_env("MIBDIRS", mibdirs_env)
System.put_env("MIBS", "ALL")
# Add each directory to net-snmp's search path
Enum.each(mib_dirs, fn dir ->
case ToweropsNative.load_mib_directory(dir) do
:ok -> :ok
# Ignore errors in test environment
{:error, _reason} -> :ok
# Add each directory to net-snmp's search path (NIF may not be loaded)
try do
Enum.each(mib_dirs, fn dir ->
case ToweropsNative.load_mib_directory(dir) do
:ok -> :ok
# Ignore errors in test environment
{:error, _reason} -> :ok
end
end)
# Explicitly initialize the MIB library with correct environment variables
case ToweropsNative.init_mib_library() do
result when result in ["initialized", "already_initialized"] ->
IO.puts("Test MIB library initialized: #{result}")
other ->
IO.puts("Warning: Unexpected MIB library init result: #{inspect(other)}")
end
end)
# Explicitly initialize the MIB library with correct environment variables
case ToweropsNative.init_mib_library() do
result when result in ["initialized", "already_initialized"] ->
IO.puts("Test MIB library initialized: #{result}")
other ->
IO.puts("Warning: Unexpected MIB library init result: #{inspect(other)}")
rescue
_e in ErlangError ->
IO.puts("ToweropsNative NIF not available — MIB resolution disabled in tests")
end
end