towerops/nix/shell.nix
Graham McInitre 07a772a7f9 fix: pre-compile test deps in shellHook so mix test always works
Add MIX_ENV=test mix deps.compile after NIF build. The _build/test
tree is ephemeral (cleared when nix devShell derivation changes), which
causes 'Mox is not available' errors. This ensures test deps like Mox
are ready before the user ever runs mix test.
2026-07-16 09:33:55 -05:00

374 lines
12 KiB
Nix
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
lib,
stdenv,
mkShell,
writeShellScriptBin,
# Elixir/Erlang
elixir,
# Databases and caches
postgresql_17,
redis,
# SNMP tools
net-snmp,
# Build tools for C NIF
gcc,
gnumake,
pkg-config,
# Development tools
git-lfs,
inotify-tools,
# LSPs and formatters
elixir-ls,
nixfmt,
shellcheck,
# Pre-commit hooks
pre-commit-hooks,
}:
let
# PostgreSQL with TimescaleDB and PostGIS extensions
pg = postgresql_17.withPackages (ps: [
ps.timescaledb
ps.postgis
]);
# PostgreSQL data directory (local to project)
pgDataDir = ".nix-postgres";
pgPort = "5432";
pgHost = "localhost";
# Redis data directory (local to project)
redisDataDir = ".nix-redis";
redisPort = "6379";
# Service management flag
servicesFlag = ".nix-services-started";
# Version file to detect when PG config changes (extensions added/removed, version bump)
pgVersionFile = ".nix-postgres-version";
# Script to start PostgreSQL and Redis
startServices = writeShellScriptBin "start-services" ''
set -e
echo "🚀 Starting development services..."
# Check if PG version/extensions changed and reinit if needed.
# Must run BEFORE the servicesFlag guard so already-running clusters
# get reinitialized when extensions or versions change.
pg_needs_reinit=false
if [ -d "${pgDataDir}" ] && [ -f "${pgVersionFile}" ]; then
if [ "$(cat ${pgVersionFile})" != "${pg}" ]; then
echo " PostgreSQL configuration changed (extensions or version updated)"
pg_needs_reinit=true
fi
fi
if [ -d "${pgDataDir}" ] && [ ! -f "${pgVersionFile}" ]; then
pg_needs_reinit=true
fi
if $pg_needs_reinit; then
echo "🗑 Stopping old PostgreSQL and removing cluster..."
${pg}/bin/pg_ctl -D ${pgDataDir} stop -m fast 2>/dev/null || true
rm -rf ${pgDataDir}
rm -f ${servicesFlag}
fi
# Check if already started (with correct version)
if [ -f "${servicesFlag}" ]; then
echo " Services already running (remove ${servicesFlag} to force restart)"
exit 0
fi
# Initialize PostgreSQL if needed
if [ ! -d "${pgDataDir}" ]; then
echo "📦 Initializing PostgreSQL database..."
${pg}/bin/initdb -D ${pgDataDir} -U $USER --encoding=UTF8 --locale=en_US.UTF-8
# Configure PostgreSQL
cat >> ${pgDataDir}/postgresql.conf << EOF
# Development configuration
max_connections = 100
shared_buffers = 128MB
fsync = off # Faster for development (DO NOT use in production)
synchronous_commit = off
full_page_writes = off
# TimescaleDB
shared_preload_libraries = 'timescaledb'
timescaledb.telemetry_level = off
EOF
# Record current PG version for future change detection
echo -n "${pg}" > ${pgVersionFile}
fi
# Check if PostgreSQL is already running on this port
if ${pg}/bin/pg_isready -h ${pgHost} -p ${pgPort} > /dev/null 2>&1; then
echo " PostgreSQL already running on port ${pgPort}"
else
# Start PostgreSQL
echo "🐘 Starting PostgreSQL on port ${pgPort}..."
${pg}/bin/pg_ctl -D ${pgDataDir} -l ${pgDataDir}/logfile -o "-p ${pgPort}" start
# Wait for PostgreSQL to be ready
for i in {1..30}; do
if ${pg}/bin/pg_isready -h ${pgHost} -p ${pgPort} > /dev/null 2>&1; then
break
fi
if [ $i -eq 30 ]; then
echo " PostgreSQL failed to start within 30 seconds"
echo " Check ${pgDataDir}/logfile for details"
exit 1
fi
sleep 1
done
echo " PostgreSQL started successfully"
fi
# Create postgres role if it doesn't exist (needed by some tools)
${pg}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -tc \
"SELECT 1 FROM pg_roles WHERE rolname='postgres'" | grep -q 1 || \
${pg}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -c \
"CREATE ROLE postgres WITH SUPERUSER LOGIN;"
# Create development and test databases
for db in towerops_dev towerops_test; do
${pg}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -tc \
"SELECT 1 FROM pg_database WHERE datname='$db'" | grep -q 1 || \
${pg}/bin/createdb -h ${pgHost} -p ${pgPort} -U $USER $db
done
# Enable TimescaleDB and PostGIS extensions for dev and test databases
for db in towerops_dev towerops_test; do
${pg}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d $db -c \
"CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;" 2>/dev/null || \
echo " TimescaleDB not available for $db (optional)"
${pg}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d $db -c \
"CREATE EXTENSION IF NOT EXISTS postgis CASCADE;" 2>/dev/null || \
echo " PostGIS not available for $db (optional)"
done
echo " PostgreSQL started successfully"
# Check if Redis is already running on this port
if ${redis}/bin/redis-cli -p ${redisPort} ping > /dev/null 2>&1; then
echo " Redis already running on port ${redisPort}"
else
# Start Redis
echo "📮 Starting Redis on port ${redisPort}..."
mkdir -p ${redisDataDir}
${redis}/bin/redis-server --daemonize yes \
--port ${redisPort} \
--dir ${redisDataDir} \
--save "" \
--appendonly no
# Wait for Redis to be ready
for i in {1..10}; do
if ${redis}/bin/redis-cli -p ${redisPort} ping > /dev/null 2>&1; then
break
fi
if [ $i -eq 10 ]; then
echo " Redis failed to start within 10 seconds"
exit 1
fi
sleep 1
done
echo " Redis started successfully"
fi
# Mark services as started
touch ${servicesFlag}
echo ""
echo " Development environment ready!"
echo ""
echo "PostgreSQL: ${pgHost}:${pgPort} (databases: towerops_dev, towerops_test)"
echo "Redis: ${pgHost}:${redisPort}"
echo ""
'';
# Script to stop PostgreSQL and Redis
stopServices = writeShellScriptBin "stop-services" ''
set -e
echo "🛑 Stopping development services..."
# Stop PostgreSQL
if [ -d "${pgDataDir}" ]; then
${pg}/bin/pg_ctl -D ${pgDataDir} stop -m fast 2>/dev/null || true
echo " PostgreSQL stopped"
fi
# Stop Redis
${redis}/bin/redis-cli -p ${redisPort} shutdown 2>/dev/null || true
echo " Redis stopped"
# Remove services flag
rm -f ${servicesFlag}
echo " Services stopped successfully"
'';
# Pre-commit hooks configuration
pre-commit-check = pre-commit-hooks.run {
src = ../.;
hooks = {
# Elixir formatting
mix-format = {
enable = true;
name = "mix format";
entry = "mix format --check-formatted";
files = "\\.(ex|exs)$";
pass_filenames = false;
};
# Credo linting
credo = {
enable = true;
name = "credo";
entry = "mix credo --strict";
files = "\\.(ex|exs)$";
pass_filenames = false;
};
# Nix formatting
nixfmt = {
enable = true;
entry = "${nixfmt}/bin/nixfmt";
};
# Shellcheck for shell scripts
shellcheck.enable = true;
};
};
in
mkShell {
name = "towerops-dev";
# Development tools
buildInputs = [
# Elixir/Erlang
elixir
# Databases (PostgreSQL with TimescaleDB)
pg
redis
# SNMP tools
net-snmp
# C NIF build tools
gcc
gnumake
pkg-config
# Development tools
git-lfs
# LSPs and formatters
elixir-ls
nixfmt
shellcheck
# Service management scripts
startServices
stopServices
]
++ lib.optionals stdenv.isLinux [
# Linux-only tools
inotify-tools # For Mix file watching
];
# Environment variables
shellHook = ''
# Color codes
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Set Mix and Hex home to local directories (avoid polluting $HOME)
export MIX_HOME="$PWD/.nix-mix"
export HEX_HOME="$PWD/.nix-hex"
mkdir -p "$MIX_HOME" "$HEX_HOME"
# PostgreSQL connection
export PGHOST="${pgHost}"
export PGPORT="${pgPort}"
export PGUSER="$USER"
export DATABASE_URL="ecto://$USER@${pgHost}:${pgPort}/towerops_dev"
# Redis connection
export REDIS_URL="redis://${pgHost}:${redisPort}/0"
# Development secrets (DO NOT use in production)
export SECRET_KEY_BASE="dev_secret_key_base_at_least_64_bytes_long_for_phoenix_to_accept_it_safely"
export CLOAK_KEY="nQWmgQYhoCNXA3PAxwriKxLyPHAOWH9VgpLkBOXrowM="
export RELEASE_COOKIE="dev_release_cookie_for_distributed_erlang"
# Development environment
export MIX_ENV=dev
export PHX_HOST=localhost
export PORT=4000
# Ensure Erlang can find NIF libraries
export ERL_AFLAGS="-kernel shell_history enabled"
# Skip heavy setup when running inside direnv (DIRENV_DIR is set).
# direnv evaluates the shellHook to load env vars only; running mix,
# starting services, or installing pre-commit hooks would block the
# prompt and cause nix profile cleanup hangs on macOS.
if [ -z "$DIRENV_DIR" ]; then
# Running in nix develop (interactive shell)
# Install Hex and Rebar3 if not already installed
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
# Pre-compile test dependencies so mix test works immediately.
# The _build/test tree is ephemeral (cleared when nix devShell changes).
MIX_ENV=test mix deps.compile 2>/dev/null || true
# Auto-start services on first shell entry.
# Run in a detached subshell to prevent nix develop from waiting
# on daemonized PostgreSQL/Redis child processes.
if [ ! -f "${servicesFlag}" ]; then
( ${startServices}/bin/start-services </dev/null & )
fi
# Install pre-commit hooks
${pre-commit-check.shellHook}
# Welcome message
echo ""
echo -e "''${BLUE}''${NC}"
echo -e "''${BLUE} ''${NC}"
echo -e "''${BLUE}''${NC} ''${GREEN}Towerops Development Environment''${NC} ''${BLUE}''${NC}"
echo -e "''${BLUE} ''${NC}"
echo -e "''${BLUE}''${NC}"
echo ""
echo -e "''${GREEN}Available commands:''${NC}"
echo " mix phx.server - Start Phoenix development server"
echo " mix test - Run test suite"
echo " mix precommit - Run pre-commit checks (format, credo, test)"
echo " start-services - Start PostgreSQL and Redis"
echo " stop-services - Stop PostgreSQL and Redis"
echo " mix ecto.reset - Reset database (drop, create, migrate, seed)"
echo ""
echo -e "''${GREEN}Services:''${NC}"
echo " PostgreSQL: ${pgHost}:${pgPort}"
echo " Redis: ${pgHost}:${redisPort}"
echo ""
echo -e "''${GREEN}Documentation:''${NC}"
echo " See CLAUDE.md and docs/nix.md for more information"
echo ""
fi
'';
}