When direnv evaluates the shellHook (DIRENV_DIR is set), skip mix local, service startup, pre-commit install, and the welcome banner. These operations block direnv and trigger nix profile cleanup hangs on macOS. Only run them in interactive nix develop sessions.
334 lines
9.9 KiB
Nix
334 lines
9.9 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"
|
||
|
||
# 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
|
||
|
||
# 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 ""
|
||
fi
|
||
'';
|
||
}
|