304 lines
9.4 KiB
Nix
304 lines
9.4 KiB
Nix
{
|
||
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 </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}aprs.me 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 format - Format Elixir code"
|
||
echo " mix credo --strict - Run static analysis"
|
||
echo " start-services - Start PostgreSQL"
|
||
echo " stop-services - Stop PostgreSQL"
|
||
echo " mix ecto.reset - Reset database (drop, create, migrate, seed)"
|
||
echo ""
|
||
echo -e "''${GREEN}Services:''${NC}"
|
||
echo " PostgreSQL: ${pgHost}:${pgPort}"
|
||
echo ""
|
||
fi
|
||
'';
|
||
}
|