aprs.me/nix/shell.nix
2026-07-17 16:42:53 -05:00

304 lines
9.4 KiB
Nix
Raw 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
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
'';
}