towerops/nix/shell.nix
Graham McInitre e58767cf00 fix: guard shellHook heavy ops behind DIRENV_DIR check
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.
2026-07-16 08:54:55 -05:00

334 lines
9.9 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 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
'';
}