prop/nix/shell.nix
Graham McInitre a8ecee32b0 feat: add Nix flake dev environment with Erlang 29.0.3 + Elixir 1.20.2
- Add flake.nix + flake.lock pinning nixpkgs-unstable
- Add nix/shell.nix with PostgreSQL, Rust, Elixir LSP, pre-commit hooks
- Add shell.nix flake-compat shim for non-flake nix-shell users
- Bump Dockerfile to Elixir 1.20.2 / OTP 29.0.3
- Add nix.md with setup and troubleshooting docs
- Gitignore Nix runtime state and generated pre-commit config

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 12:57:10 -05:00

280 lines
8.1 KiB
Nix

{
lib,
stdenv,
mkShell,
writeShellScriptBin,
# Elixir/Erlang
elixir,
# Database
postgresql_16,
# Rust toolchain
rustup,
rust-analyzer,
# Development tools
git-lfs,
inotify-tools,
# LSPs and formatters
elixir-ls,
nixfmt,
shellcheck,
# Pre-commit hooks
pre-commit-hooks,
}:
let
# PostgreSQL data directory (local to project)
pgDataDir = ".nix-postgres";
pgPort = "5432";
pgHost = "localhost";
# Service management flag
servicesFlag = ".nix-services-started";
# Script to start PostgreSQL
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..."
${postgresql_16}/bin/initdb -D ${pgDataDir} -U $USER --encoding=UTF8 --locale=en_US.UTF-8
# Configure PostgreSQL for development (fast but unsafe for production)
cat >> ${pgDataDir}/postgresql.conf << EOF
# Development configuration
max_connections = 100
shared_buffers = 128MB
fsync = off
synchronous_commit = off
full_page_writes = off
EOF
fi
# Check if PostgreSQL is already running on this port
if ${postgresql_16}/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}..."
${postgresql_16}/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 ${postgresql_16}/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)
${postgresql_16}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -tc \
"SELECT 1 FROM pg_roles WHERE rolname='postgres'" | grep -q 1 || \
${postgresql_16}/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 prop_dev prop_test; do
${postgresql_16}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -tc \
"SELECT 1 FROM pg_database WHERE datname='$db'" | grep -q 1 || \
${postgresql_16}/bin/createdb -h ${pgHost} -p ${pgPort} -U $USER $db
done
# Mark services as started
touch ${servicesFlag}
echo ""
echo "Development environment ready!"
echo ""
echo "PostgreSQL: ${pgHost}:${pgPort} (databases: prop_dev, prop_test)"
echo ""
'';
# Script to stop PostgreSQL
stopServices = writeShellScriptBin "stop-services" ''
set -e
echo "Stopping development services..."
# Stop PostgreSQL
if [ -d "${pgDataDir}" ]; then
${postgresql_16}/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;
};
# Rust formatting and linting
cargo-fmt = {
enable = true;
name = "cargo fmt";
entry = "cargo fmt --all --check";
files = "\\.rs$";
pass_filenames = false;
};
cargo-clippy = {
enable = true;
name = "cargo clippy";
entry = "cargo clippy --all-targets -- -D warnings";
files = "\\.rs$";
pass_filenames = false;
};
# Nix formatting
nixfmt = {
enable = true;
entry = "${nixfmt}/bin/nixfmt";
};
# Shellcheck for shell scripts
shellcheck.enable = true;
};
};
in
mkShell {
name = "prop-dev";
# Development tools
buildInputs = [
# Elixir/Erlang
elixir
# Database
postgresql_16
# Rust toolchain
rustup
rust-analyzer
# 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 = ''
# 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}/prop_dev"
# 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 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"
# Rust: set up stable toolchain if not already installed
if ! rustup show active-toolchain 2>/dev/null | grep -q stable; then
rustup default stable 2>/dev/null || true
fi
# 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 ""
echo " "
echo " Prop Microwave Propagation Prediction "
echo " "
echo ""
echo ""
echo "Available commands:"
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"
echo " stop-services - Stop PostgreSQL"
echo " mix ecto.reset - Reset database (drop, create, migrate, seed)"
echo " cargo build - Build Rust crates (cd rust/prop_grid_rs)"
echo " cargo test - Run Rust tests"
echo ""
echo "Services:"
echo " PostgreSQL: ${pgHost}:${pgPort}"
echo ""
echo "Documentation:"
echo " See CLAUDE.md and nix.md for more information"
echo ""
'';
}