{ 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