#!/usr/bin/env bash # Generate a gitignored docker-compose.override.yml (busy host ports → free ones) # and start the local stack. Invoked by `make up-local`. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" COMPOSE="${COMPOSE:-docker compose}" OVERRIDE="${LOCAL_OVERRIDE:-docker-compose.override.yml}" PREFERRED_SIMULATOR="${SIMULATOR_PORT:-8006}" PREFERRED_POSTGRES="${POSTGRES_HOST_PORT:-5432}" PREFERRED_TLS="${TLS_GATEWAY_PORT:-8443}" port_in_use() { local port="$1" if command -v nc >/dev/null 2>&1; then nc -z 127.0.0.1 "$port" >/dev/null 2>&1 return $? fi python3 - "$port" <<'PY' import socket, sys port = int(sys.argv[1]) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.25) try: sys.exit(0 if s.connect_ex(("127.0.0.1", port)) == 0 else 1) finally: s.close() PY } ours_on_port() { local port="$1" # Host publish lines look like: 0.0.0.0:8006->8006/tcp or 127.0.0.1:5432->5432/tcp $COMPOSE ps --format '{{.Ports}}' 2>/dev/null \ | grep -E "(^|[, ])(([0-9.]+|\[::\]):)?${port}->" >/dev/null 2>&1 } pick_port() { local preferred="$1" local label="$2" local max=$((preferred + 100)) local p="$preferred" while [ "$p" -le "$max" ]; do if ! port_in_use "$p" || ours_on_port "$p"; then if [ "$p" -ne "$preferred" ]; then echo "Port ${preferred} busy for ${label} → using ${p}" >&2 fi echo "$p" return 0 fi p=$((p + 1)) done echo "No free host port near ${preferred} for ${label}" >&2 exit 1 } SIMULATOR_HOST_PORT="$(pick_port "$PREFERRED_SIMULATOR" "simulator")" POSTGRES_HOST_PORT="$(pick_port "$PREFERRED_POSTGRES" "postgres")" TLS_HOST_PORT="$(pick_port "$PREFERRED_TLS" "tls-gateway")" cat >"$OVERRIDE" <