{ lib, stdenv, mkShell, writeShellScriptBin, # Elixir/Erlang elixir, # Databases postgresql_17, # Build tools for NIF deps (bcrypt_elixir via elixir_make) gcc, gnumake, pkg-config, # Development tools inotify-tools, # LSPs and formatters elixir-ls, nixfmt, shellcheck, # Pre-commit hooks pre-commit-hooks, }: let # PostgreSQL with PostGIS extension pg = postgresql_17.withPackages (ps: [ ps.postgis ]); # PostgreSQL data directory (local to project) pgDataDir = ".nix-postgres"; pgPort = "5432"; pgHost = "localhost"; # 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 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 sized for the test pool (schedulers_online * 16) max_connections = 300 shared_buffers = 128MB fsync = off # Faster for development (DO NOT use in production) synchronous_commit = off full_page_writes = 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 (dev.exs/test.exs connect as postgres) ${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 PASSWORD 'postgres';" # Create development and test databases for db in aprsme_dev aprsme_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 PostGIS extension for dev and test databases for db in aprsme_dev aprsme_test; do ${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" # Mark services as started touch ${servicesFlag} echo "" echo "✨ Development environment ready!" echo "" echo "PostgreSQL: ${pgHost}:${pgPort} (databases: aprsme_dev, aprsme_test)" echo "" ''; # Script to stop PostgreSQL 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 # 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 = "aprsme-dev"; # Development tools buildInputs = [ # Elixir/Erlang elixir # Databases (PostgreSQL with PostGIS) pg # NIF build tools (bcrypt_elixir) gcc gnumake pkg-config # 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 (dev.exs/test.exs connect as postgres:postgres) export PGHOST="${pgHost}" export PGPORT="${pgPort}" export PGUSER="$USER" export DATABASE_URL="ecto://postgres:postgres@${pgHost}:${pgPort}/aprsme_dev" # Development environment export MIX_ENV=dev export PHX_HOST=localhost export PORT=4000 # Erlang shell history 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 # 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 child processes. if [ ! -f "${servicesFlag}" ]; then ( ${startServices}/bin/start-services