- Remove 'trap ... EXIT' that kills services when entering the dev shell - Fix DATABASE_URL: $USER → $USER (no backslash) so bash expands it instead of passing the literal string 'USER' as the PG role name
326 lines
9.5 KiB
Nix
326 lines
9.5 KiB
Nix
{
|
||
lib,
|
||
stdenv,
|
||
mkShell,
|
||
writeShellScriptBin,
|
||
# Elixir/Erlang
|
||
elixir,
|
||
# Databases and caches
|
||
postgresql_16,
|
||
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 extension
|
||
pg = postgresql_16.withPackages (ps: [ ps.timescaledb ]);
|
||
|
||
# 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";
|
||
|
||
# Script to start PostgreSQL and Redis
|
||
startServices = writeShellScriptBin "start-services" ''
|
||
set -e
|
||
|
||
echo "🚀 Starting development services..."
|
||
|
||
# Check if already started
|
||
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
|
||
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 extension 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)"
|
||
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"
|
||
|
||
# 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
|
||
|
||
# Auto-start services on first shell entry
|
||
if [ ! -f "${servicesFlag}" ]; then
|
||
${startServices}/bin/start-services
|
||
|
||
# Run migrations if databases exist and Mix deps are available
|
||
if [ -d "deps" ] && mix ecto.migrate 2>/dev/null; then
|
||
echo "✓ Database migrations applied"
|
||
fi
|
||
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 ""
|
||
'';
|
||
}
|