diff --git a/.gitignore b/.gitignore index 5774f9e..4d75cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Cloned repositories (managed by devtools.sh) +# Cloned repositories (managed via repos.conf) teiserver/ bar-lobby/ spads_config_bar/ @@ -8,6 +8,7 @@ BYAR-Chobby bar-db/ bar-live-services/ RecoilEngine +lua-doc-extractor SPADS/ SpringLobbyInterface/ diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..1472365 --- /dev/null +++ b/Justfile @@ -0,0 +1,18 @@ +set dotenv-load + +mod services 'just/services.just' +mod repos 'just/repos.just' +mod engine 'just/engine.just' +mod setup 'just/setup.just' +mod link 'just/link.just' +mod lua 'just/lua.just' +mod docs 'just/docs.just' +mod bar 'just/bar.just' +mod tei 'just/tei.just' + +default: + @just --list --list-submodules + +reset: + just lua::reset + just docs::reset diff --git a/README.md b/README.md index 47a2864..b323b74 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ Everything server-side runs in Docker. The game client runs natively. ```bash git clone https://github.com/thvl3/BAR-Devtools.git cd BAR-Devtools -./devtools.sh init -./devtools.sh up +just setup::init +just services::up ``` -`init` walks you through installing dependencies, cloning repositories, and building Docker images. You only need to run it once. +`setup::init` walks you through installing dependencies, cloning repositories, and building Docker images. You only need to run it once. -`up` starts PostgreSQL and Teiserver. On first run it seeds the database with test data and creates default accounts (~2-3 minutes). Subsequent starts are fast. +`services::up` starts PostgreSQL and Teiserver. On first run it seeds the database with test data and creates default accounts (~2-3 minutes). Subsequent starts are fast. Once running: @@ -30,41 +30,113 @@ Once running: ## Requirements -- **Linux** (Arch, Debian/Ubuntu, or Fedora) +- **Linux** (Arch, Debian/Ubuntu, or Fedora) or **macOS** - **Docker** with Compose V2 - **Git** +- **Bash 4+** (Linux ships this; macOS needs `brew install bash`) +- **[just](https://github.com/casey/just)** -- command runner + +```bash +# Install just +pacman -S just # Arch +dnf install just # Fedora +apt install just # Debian/Ubuntu +brew install just # Homebrew + +# macOS only: install modern bash (macOS ships bash 3.2 which is too old) +brew install bash +``` + +Optional: + - **Node.js** (only needed if running bar-lobby) -`./devtools.sh install-deps` will detect your distro and install what's missing. +`just setup::deps` will detect your distro and install what's missing (except `just` itself). ## Commands -### Getting Started +Run `just` with no arguments to list everything: + +``` +$ just +Available recipes: + ... +``` + +### Setup -| Command | Description | -|---------|-------------| -| `init` | Full first-time setup: install deps, clone repos, build images | -| `install-deps` | Install system packages (docker, git, nodejs) | +| Recipe | Description | +|--------|-------------| +| `just setup::init` | Full first-time setup: install deps, clone repos, build images | +| `just setup::deps` | Install system packages (docker, git, nodejs) | +| `just setup::check` | Check prerequisites and build Docker images | ### Services -| Command | Description | -|---------|-------------| -| `up [lobby] [spads]` | Start services (options are additive) | -| `down` | Stop all services | -| `status` | Show running containers | -| `logs [service]` | Tail logs (postgres, teiserver, spads, or all) | -| `lobby` | Start bar-lobby dev server standalone | -| `shell [service]` | Shell into a container (default: teiserver) | -| `reset` | Destroy all data and rebuild from scratch | +| Recipe | Description | +|--------|-------------| +| `just services::up [lobby] [spads]` | Start services (options are additive) | +| `just services::down` | Stop all services | +| `just services::status` | Show running containers | +| `just services::logs [service]` | Tail logs (postgres, teiserver, spads, or all) | +| `just services::lobby` | Start bar-lobby dev server standalone | +| `just services::shell [service]` | Shell into a container (default: teiserver) | +| `just services::build` | Build Docker images | +| `just services::reset` | Destroy all data and rebuild from scratch | ### Repositories -| Command | Description | -|---------|-------------| -| `clone [group]` | Clone/update repos. Groups: `core`, `extra`, `all` | -| `repos` | Show status of all configured repositories | -| `update` | Pull latest on all cloned repos (fast-forward only) | +| Recipe | Description | +|--------|-------------| +| `just repos::clone [group]` | Clone/update repos. Groups: `core`, `extra`, `all` | +| `just repos::status` | Show status of all configured repositories | +| `just repos::update` | Pull latest on all cloned repos (fast-forward only) | + +### Engine + +| Recipe | Description | +|--------|-------------| +| `just engine::build [cmake-args]` | Build Recoil engine via docker-build-v2 | + +### Game Directory + +| Recipe | Description | +|--------|-------------| +| `just link::status` | Show symlink status | +| `just link::create ` | Symlink a repo into the game directory (engine, chobby, bar) | + +### Lua Tooling + +| Recipe | Description | +|--------|-------------| +| `just lua::build-lde` | Build lua-doc-extractor from local checkout | +| `just lua::library` | Extract Lua docs from RecoilEngine, copy into BAR submodule | +| `just lua::library-reload` | Generate library then restart LuaLS | + +### Documentation + +| Recipe | Description | +|--------|-------------| +| `just docs::generate` | Generate Lua API doc pages | +| `just docs::server` | Generate + start Hugo dev server | +| `just docs::server-only` | Start Hugo dev server without regenerating | + +### BAR (Beyond All Reason) + +| Recipe | Description | +|--------|-------------| +| `just bar::lint` | Lint BAR Lua code (luacheck via lux) | +| `just bar::fmt` | Format BAR Lua code (stylua via lux) | +| `just bar::test` | Run busted unit tests in the BAR container | +| `just bar::integrations` | Run headless integration tests (x86-64 only) | +| `just bar::all` | Run all BAR tests (units + integrations) | +| `just bar::setup-hooks` | Install git pre-commit hook in the BAR repo | + +### Teiserver + +| Recipe | Description | +|--------|-------------| +| `just tei::mix` | Run teiserver mix tests | ## Using Your Own Forks @@ -84,17 +156,27 @@ bar-lobby https://github.com/yourname/bar-lobby.git your-branch core Then clone or re-clone: ```bash -./devtools.sh clone core +just repos::clone core ``` `repos.local.conf` is gitignored so it won't affect anyone else. +### Local paths + +You can also point a repo entry at a local directory instead of cloning. Add a fifth column with the path: + +``` +lua-doc-extractor https://github.com/rhys-vdw/lua-doc-extractor.git main extra ~/code/lua-doc-extractor +``` + +This creates a symlink instead of cloning. + ## Repository Config Format `repos.conf` uses a simple whitespace-delimited format: ``` -# directory url branch group +# directory url branch group [local_path] teiserver https://github.com/beyond-all-reason/teiserver.git master core ``` @@ -102,24 +184,41 @@ teiserver https://github.com/beyond-all-reason/teiserver.git master c - **url** -- git clone URL - **branch** -- branch to checkout - **group** -- `core` (required for the dev stack) or `extra` (optional) +- **local_path** -- (optional) absolute or `~`-relative path to symlink instead of cloning ## Architecture ``` BAR-Devtools/ -├── devtools.sh # Main CLI script -├── repos.conf # Repository sources & branches -├── docker-compose.dev.yml # Service definitions +├── Justfile # Root command runner (lists all modules) +├── just/ +│ ├── services.just # Docker Compose service management +│ ├── repos.just # Git repository operations +│ ├── engine.just # RecoilEngine build +│ ├── setup.just # First-time setup & dependency install +│ ├── link.just # Game directory symlinking +│ ├── lua.just # lua-doc-extractor & Lua library generation +│ ├── docs.just # Hugo documentation server +│ └── test.just # Unit & integration tests +├── scripts/ +│ ├── common.sh # Shared color/logging helpers +│ ├── repos.sh # repos.conf parsing & git operations +│ └── setup.sh # Distro detection, deps, prerequisite checks +├── repos.conf # Repository sources & branches +├── docker-compose.dev.yml # Service definitions ├── docker/ -│ ├── teiserver.dev.Dockerfile # Teiserver dev image (Elixir + Phoenix) -│ ├── teiserver-entrypoint.sh # DB init, seeding, migrations -│ ├── teiserver.dockerignore # Build context optimization -│ ├── setup-spads-bot.exs # Creates SPADS bot account in Teiserver -│ ├── spads-dev-entrypoint.sh # SPADS startup + game data download -│ └── spads_dev.conf # Simplified SPADS config for dev -├── teiserver/ # ← cloned by devtools.sh (gitignored) -├── bar-lobby/ # ← cloned by devtools.sh (gitignored) -└── spads_config_bar/ # ← cloned by devtools.sh (gitignored) +│ ├── teiserver.dev.Dockerfile # Teiserver dev image (Elixir + Phoenix) +│ ├── teiserver-entrypoint.sh # DB init, seeding, migrations +│ ├── teiserver.dockerignore # Build context optimization +│ ├── bar.Dockerfile # BAR test environment (Lua 5.1 + lux) +│ ├── setup-spads-bot.exs # Creates SPADS bot account in Teiserver +│ ├── spads-dev-entrypoint.sh # SPADS startup + game data download +│ └── spads_dev.conf # Simplified SPADS config for dev +├── teiserver/ # ← cloned by just repos::clone (gitignored) +├── bar-lobby/ # ← cloned (gitignored) +├── Beyond-All-Reason/ # ← cloned (gitignored) +├── RecoilEngine/ # ← cloned (gitignored) +└── spads_config_bar/ # ← cloned (gitignored) ``` ### What the Docker stack does @@ -130,8 +229,9 @@ BAR-Devtools/ - Seeds fake data (test users, matchmaking data) - Sets up Tachyon OAuth - Creates a `spadsbot` account with Bot/Moderator roles -- **SPADS** (optional, `up spads`) -- Perl autohost using `badosu/spads:latest`. Downloads game data via `pr-downloader` on first run. Connects to Teiserver via Spring protocol on port 8200. +- **SPADS** (optional, `services::up spads`) -- Perl autohost using `badosu/spads:latest`. Downloads game data via `pr-downloader` on first run. Connects to Teiserver via Spring protocol on port 8200. - **bar-lobby** -- Electron/Vue.js game client, runs natively on the host (not in Docker) +- **BAR test runner** (`test` profile) -- Ubuntu container with Lua 5.1 and [lux](https://github.com/lumen-oss/lux) for running busted unit tests against the Beyond-All-Reason codebase ### Ports @@ -148,8 +248,8 @@ BAR-Devtools/ SPADS is optional and started separately because it requires downloading ~300MB of game data on first run. The download depends on external rapid repositories that can be unreliable. ```bash -./devtools.sh up spads # Start with SPADS -./devtools.sh logs spads # Check SPADS status +just services::up spads # Start with SPADS +just services::logs spads # Check SPADS status ``` The SPADS bot account (`spadsbot` / `password`) is created automatically during Teiserver initialization. @@ -159,21 +259,21 @@ The SPADS bot account (`spadsbot` / `password`) is created automatically during **Port 5432/5433 conflict with host PostgreSQL:** Either stop your local PostgreSQL (`sudo systemctl stop postgresql`) or change the port: ```bash -BAR_POSTGRES_PORT=5434 ./devtools.sh up +BAR_POSTGRES_PORT=5434 just services::up ``` **Teiserver takes forever on first run:** The initial database seeding includes generating fake data. Follow progress with: ```bash -./devtools.sh logs teiserver +just services::logs teiserver ``` **SPADS fails with "No Spring map/mod found":** Game data download may have failed. Check logs and retry: ```bash -./devtools.sh logs spads -./devtools.sh down -./devtools.sh up spads +just services::logs spads +just services::down +just services::up spads ``` **Docker permission denied:** @@ -184,6 +284,6 @@ sudo usermod -aG docker $USER **Nuclear option -- start completely fresh:** ```bash -./devtools.sh reset -./devtools.sh up +just services::reset +just services::up ``` diff --git a/devtools.sh b/devtools.sh index b0c933d..43e32ae 100755 --- a/devtools.sh +++ b/devtools.sh @@ -1,996 +1,32 @@ #!/usr/bin/env bash set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -COMPOSE_FILE="$SCRIPT_DIR/docker-compose.dev.yml" -COMPOSE="docker compose -f $COMPOSE_FILE" -LOBBY_DIR="$SCRIPT_DIR/bar-lobby" -REPOS_CONF="$SCRIPT_DIR/repos.conf" -REPOS_LOCAL="$SCRIPT_DIR/repos.local.conf" - -detect_game_dir() { - if [ -n "${BAR_GAME_DIR:-}" ]; then - echo "$BAR_GAME_DIR" - return 0 - fi - local xdg_state="${XDG_STATE_HOME:-$HOME/.local/state}" - local candidate="$xdg_state/Beyond All Reason" - if [ -d "$candidate" ]; then - echo "$candidate" - return 0 - fi - return 1 -} - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' - -info() { echo -e "${BLUE}[info]${NC} $*"; } -ok() { echo -e "${GREEN}[ok]${NC} $*"; } -warn() { echo -e "${YELLOW}[warn]${NC} $*"; } -err() { echo -e "${RED}[error]${NC} $*"; } -step() { echo -e "${CYAN}[step]${NC} $*"; } - -# =========================================================================== -# Distro detection -# =========================================================================== - -detect_distro() { - if command -v pacman &>/dev/null; then - echo "arch" - elif command -v apt-get &>/dev/null; then - echo "debian" - elif command -v dnf &>/dev/null; then - echo "fedora" - else - echo "unknown" - fi -} - -pkg_install_cmd() { - case "$(detect_distro)" in - arch) echo "sudo pacman -S --needed" ;; - debian) echo "sudo apt install -y" ;; - fedora) echo "sudo dnf install -y" ;; - *) echo "" ;; - esac -} - -# Map generic package names to distro-specific ones -pkg_name() { - local generic="$1" - local distro - distro="$(detect_distro)" - case "${distro}:${generic}" in - arch:docker) echo "docker" ;; - arch:docker-compose) echo "docker-compose" ;; - arch:git) echo "git" ;; - arch:nodejs) echo "nodejs npm" ;; - debian:docker) echo "docker.io" ;; - debian:docker-compose) echo "docker-compose-plugin" ;; - debian:git) echo "git" ;; - debian:nodejs) echo "nodejs npm" ;; - fedora:docker) echo "docker-ce docker-ce-cli containerd.io" ;; - fedora:docker-compose) echo "docker-compose-plugin" ;; - fedora:git) echo "git" ;; - fedora:nodejs) echo "nodejs npm" ;; - *) echo "$generic" ;; - esac -} - -# =========================================================================== -# Prerequisite checks -# =========================================================================== - -check_git() { - if ! command -v git &>/dev/null; then - err "git is not installed." - return 1 - fi - ok "git $(git --version | awk '{print $3}') detected" -} - -check_docker() { - if ! command -v docker &>/dev/null; then - err "Docker is not installed." - return 1 - fi - if ! docker info &>/dev/null; then - err "Docker daemon is not running or current user lacks permissions." - echo "" - echo " Start the daemon: sudo systemctl start docker" - echo " Enable on boot: sudo systemctl enable docker" - echo " Add yourself: sudo usermod -aG docker \$USER (then re-login)" - echo "" - return 1 - fi - if ! docker compose version &>/dev/null; then - err "Docker Compose V2 plugin is not installed." - return 1 - fi - ok "Docker $(docker --version | awk '{print $3}' | tr -d ',') + Compose V2 detected" -} - -check_node() { - if ! command -v node &>/dev/null; then - warn "Node.js not found (needed for bar-lobby only)." - return 1 - fi - ok "Node.js $(node --version) detected" -} - -check_ports() { - local pg_port="${BAR_POSTGRES_PORT:-5433}" - local ports=(4000 "$pg_port" 8200 8201 8888) - local conflict=0 - for port in "${ports[@]}"; do - if ss -tlnp 2>/dev/null | grep -q ":${port} "; then - warn "Port ${port} is already in use" - conflict=1 - fi - done - if [ "$conflict" -eq 1 ]; then - warn "Some ports are in use. Services binding to those ports may fail to start." - else - ok "Required ports available (4000, ${pg_port}, 8200, 8201, 8888)" - fi -} - -check_prerequisites() { - echo -e "${BOLD}Checking prerequisites...${NC}" - echo "" - local failed=0 - check_git || failed=1 - check_docker || failed=1 - check_node || true - check_ports - echo "" - if [ "$failed" -ne 0 ]; then - err "Missing required prerequisites. Run './devtools.sh install-deps' or fix manually." - return 1 - fi -} - -# =========================================================================== -# Repository management -# =========================================================================== - -# Parse repos.conf (with repos.local.conf overrides) into parallel arrays. -# Populates: REPO_DIRS[], REPO_URLS[], REPO_BRANCHES[], REPO_GROUPS[], REPO_LOCAL_PATHS[] -declare -a REPO_DIRS=() REPO_URLS=() REPO_BRANCHES=() REPO_GROUPS=() REPO_LOCAL_PATHS=() - -load_repos_conf() { - REPO_DIRS=(); REPO_URLS=(); REPO_BRANCHES=(); REPO_GROUPS=(); REPO_LOCAL_PATHS=() - local -A seen=() - - _parse_conf() { - local file="$1" - [ -f "$file" ] || return 0 - while IFS= read -r line || [ -n "$line" ]; do - line="${line%%#*}" # strip comments - line="$(echo "$line" | xargs 2>/dev/null || true)" # trim whitespace - [ -z "$line" ] && continue - local dir url branch group local_path - read -r dir url branch group local_path <<< "$line" - [ -z "$dir" ] || [ -z "$url" ] && continue - branch="${branch:-master}" - group="${group:-extra}" - # Expand ~ in local_path - local_path="${local_path/#\~/$HOME}" - seen[$dir]="$url $branch $group $local_path" - done < "$file" - } - - _parse_conf "$REPOS_CONF" - _parse_conf "$REPOS_LOCAL" # local overrides win - - local dir - for dir in "${!seen[@]}"; do - local url branch group local_path - read -r url branch group local_path <<< "${seen[$dir]}" - REPO_DIRS+=("$dir") - REPO_URLS+=("$url") - REPO_BRANCHES+=("$branch") - REPO_GROUPS+=("$group") - REPO_LOCAL_PATHS+=("$local_path") - done -} - -clone_or_update_repo() { - local dir="$1" url="$2" branch="$3" local_path="${4:-}" target="$SCRIPT_DIR/$dir" - - if [ -n "$local_path" ]; then - if [ ! -d "$local_path" ]; then - warn " ${dir}: local path does not exist: ${local_path}" - return 1 - fi - if [ -L "$target" ]; then - local current_link - current_link="$(readlink "$target")" - if [ "$current_link" = "$local_path" ]; then - ok " ${dir}: linked -> ${local_path}" - else - warn " ${dir}: symlink points to ${current_link}, config says ${local_path}" - info " ${dir}: updating symlink..." - rm "$target" - ln -s "$local_path" "$target" - ok " ${dir}: linked -> ${local_path}" - fi - elif [ -d "$target" ]; then - warn " ${dir}: exists as a real directory but config says link to ${local_path}" - warn " ${dir}: remove it manually to use the local path" - else - ln -s "$local_path" "$target" - ok " ${dir}: linked -> ${local_path}" - fi - return 0 - fi - - if [ -d "$target/.git" ]; then - local current_url - current_url="$(git -C "$target" remote get-url origin 2>/dev/null || true)" - if [ "$current_url" != "$url" ] && [ -n "$current_url" ]; then - warn " ${dir}: origin is ${current_url}" - warn " ${dir}: config says ${url}" - warn " ${dir}: add to repos.local.conf to set your preferred remote" - fi - info " ${dir}: fetching latest..." - git -C "$target" fetch origin --quiet 2>/dev/null || warn " ${dir}: fetch failed (offline?)" - local current_branch - current_branch="$(git -C "$target" branch --show-current 2>/dev/null)" - if [ -n "$current_branch" ] && [ "$current_branch" != "$branch" ]; then - info " ${dir}: on branch '${current_branch}' (config says '${branch}')" - fi - else - info " ${dir}: cloning ${url} (branch: ${branch})..." - git clone --branch "$branch" "$url" "$target" 2>&1 | sed 's/^/ /' - fi -} - -cmd_clone() { - local group_filter="${1:-all}" - - load_repos_conf - - if [ "${#REPO_DIRS[@]}" -eq 0 ]; then - err "No repositories found in repos.conf" - exit 1 - fi - - echo -e "${BOLD}=== Cloning / Updating Repositories ===${NC}" - echo "" - - if [ -f "$REPOS_LOCAL" ]; then - info "Using overrides from repos.local.conf" - echo "" - fi - - local i cloned=0 updated=0 skipped=0 linked=0 - for i in "${!REPO_DIRS[@]}"; do - local dir="${REPO_DIRS[$i]}" - local url="${REPO_URLS[$i]}" - local branch="${REPO_BRANCHES[$i]}" - local group="${REPO_GROUPS[$i]}" - local local_path="${REPO_LOCAL_PATHS[$i]}" - - if [ "$group_filter" != "all" ] && [ "$group" != "$group_filter" ]; then - skipped=$((skipped + 1)) - continue - fi - - if [ -n "$local_path" ]; then - clone_or_update_repo "$dir" "$url" "$branch" "$local_path" - linked=$((linked + 1)) - elif [ -d "$SCRIPT_DIR/$dir/.git" ]; then - clone_or_update_repo "$dir" "$url" "$branch" - updated=$((updated + 1)) - else - clone_or_update_repo "$dir" "$url" "$branch" - cloned=$((cloned + 1)) - fi - done - - echo "" - local summary="${cloned} cloned, ${updated} updated, ${skipped} skipped" - [ "$linked" -gt 0 ] && summary+=", ${linked} linked" - ok "Repos: ${summary}" -} - -cmd_repos() { - load_repos_conf - - echo -e "${BOLD}=== Repository Status ===${NC}" - echo "" - printf " ${DIM}%-24s %-8s %-18s %s${NC}\n" "DIRECTORY" "GROUP" "BRANCH" "STATUS" - echo " $(printf '%.0s-' {1..80})" - - local i - for i in "${!REPO_DIRS[@]}"; do - local dir="${REPO_DIRS[$i]}" - local url="${REPO_URLS[$i]}" - local branch="${REPO_BRANCHES[$i]}" - local group="${REPO_GROUPS[$i]}" - local local_path="${REPO_LOCAL_PATHS[$i]}" - local target="$SCRIPT_DIR/$dir" - - local status current_branch - if [ -L "$target" ]; then - local link_dest - link_dest="$(readlink "$target")" - if [ -d "$target/.git" ]; then - current_branch="$(git -C "$target" branch --show-current 2>/dev/null || echo "detached")" - local dirty="" - if ! git -C "$target" diff --quiet 2>/dev/null || ! git -C "$target" diff --cached --quiet 2>/dev/null; then - dirty=" ${YELLOW}*dirty*${NC}" - fi - status="${CYAN}local${NC}${dirty} -> ${link_dest}" - else - status="${RED}broken link${NC} -> ${link_dest}" - current_branch="-" - fi - elif [ -d "$target/.git" ]; then - current_branch="$(git -C "$target" branch --show-current 2>/dev/null || echo "detached")" - local dirty="" - if ! git -C "$target" diff --quiet 2>/dev/null || ! git -C "$target" diff --cached --quiet 2>/dev/null; then - dirty=" ${YELLOW}*dirty*${NC}" - fi - if [ "$current_branch" = "$branch" ]; then - status="${GREEN}ok${NC}${dirty}" - else - status="${YELLOW}branch: ${current_branch}${NC}${dirty}" - fi - else - status="${RED}missing${NC}" - current_branch="-" - fi - - printf " %-24s %-8s %-18s %b\n" "$dir" "$group" "$current_branch" "$status" - done - echo "" -} - -# =========================================================================== -# Dependency installation -# =========================================================================== - -cmd_install_deps() { - echo -e "${BOLD}=== Install System Dependencies ===${NC}" - echo "" - - local distro - distro="$(detect_distro)" - local install_cmd - install_cmd="$(pkg_install_cmd)" - - if [ "$distro" = "unknown" ] || [ -z "$install_cmd" ]; then - err "Unsupported distro. Install these manually: git, docker, docker-compose, nodejs, npm" - exit 1 - fi - - info "Detected distro: ${BOLD}${distro}${NC}" - echo "" - - local missing=() - - if ! command -v git &>/dev/null; then - missing+=("git") - fi - if ! command -v docker &>/dev/null; then - missing+=("docker") - fi - if ! docker compose version &>/dev/null 2>&1; then - missing+=("docker-compose") - fi - if ! command -v node &>/dev/null; then - missing+=("nodejs") - fi - - if [ "${#missing[@]}" -eq 0 ]; then - ok "All dependencies already installed." - echo "" - - if ! docker info &>/dev/null; then - warn "Docker is installed but the daemon isn't running or you lack permissions." - echo "" - echo " sudo systemctl start docker" - echo " sudo systemctl enable docker" - echo " sudo usermod -aG docker \$USER # then re-login" - echo "" - fi - return 0 - fi - - local packages="" - for dep in "${missing[@]}"; do - packages+=" $(pkg_name "$dep")" - done - - info "Missing: ${missing[*]}" - info "Will run: ${install_cmd}${packages}" - echo "" - - read -rp "Install now? [Y/n] " confirm - if [[ "$confirm" =~ ^[Nn]$ ]]; then - echo "Skipped. Install manually and retry." - return 1 - fi - - $install_cmd $packages - - echo "" - - if [[ " ${missing[*]} " == *" docker "* ]]; then - info "Enabling and starting Docker daemon..." - sudo systemctl enable --now docker 2>/dev/null || true - - if ! groups | grep -qw docker; then - info "Adding $USER to the docker group (re-login required)..." - sudo usermod -aG docker "$USER" - warn "You need to log out and back in for Docker group membership to take effect." - warn "After re-login, run: ./devtools.sh init" - return 0 - fi - fi - - ok "Dependencies installed successfully." -} - -# =========================================================================== -# Docker helpers -# =========================================================================== - -install_dockerignore() { - local target="$SCRIPT_DIR/teiserver/.dockerignore" - local source="$SCRIPT_DIR/docker/teiserver.dockerignore" - if [ -f "$source" ] && [ ! -f "$target" ]; then - cp "$source" "$target" - info "Installed .dockerignore for teiserver build context" - fi -} - -cmd_build() { - install_dockerignore - - info "Building Docker images..." - info " - Teiserver: compiling Elixir deps + generating TLS certs" - info " - SPADS: pulling pre-built image (badosu/spads:latest)" - echo "" - $COMPOSE build teiserver - $COMPOSE --profile spads pull spads - echo "" - ok "Images built successfully." -} - -# =========================================================================== -# Engine -# =========================================================================== - -cmd_engine() { - local subcmd="${1:-}" - case "$subcmd" in - build) - shift - local build_script="$SCRIPT_DIR/RecoilEngine/docker-build-v2/build.sh" - if [ ! -f "$build_script" ]; then - err "RecoilEngine not found. Clone it first: ./devtools.sh clone extra" - exit 1 - fi - exec "$build_script" "$@" - ;; - *) - err "Usage: ./devtools.sh engine build [args...]" - echo "" - echo " Wraps RecoilEngine/docker-build-v2/build.sh with full flag pass-through." - echo "" - echo " Examples:" - echo " ./devtools.sh engine build linux" - echo " ./devtools.sh engine build linux -DCMAKE_BUILD_TYPE=Release" - echo " ./devtools.sh engine build linux -DCMAKE_BUILD_TYPE=Release -DTRACY_ENABLE=ON" - echo " ./devtools.sh engine build --help" - exit 1 - ;; - esac -} - -# =========================================================================== -# Game directory linking -# =========================================================================== - -cmd_link() { - local target="${1:-}" - local game_dir - game_dir="$(detect_game_dir 2>/dev/null)" || true - - if [ -z "$target" ]; then - echo -e "${BOLD}=== Symlink Status ===${NC}" - echo "" - if [ -z "$game_dir" ]; then - warn "Game directory not found. Set BAR_GAME_DIR env var or install BAR to the default location." - echo "" - return 0 - fi - info "Game directory: ${game_dir}" - echo "" - - local -A link_map=( - [engine]="$game_dir/engine/local-build" - [chobby]="$game_dir/games/BYAR-Chobby" - [bar]="$game_dir/games/Beyond-All-Reason" - ) - for name in engine chobby bar; do - local link_path="${link_map[$name]}" - if [ -L "$link_path" ]; then - local link_target - link_target="$(readlink -f "$link_path" 2>/dev/null || echo "?")" - printf " %-10s ${GREEN}linked${NC} -> %s\n" "$name" "$link_target" - elif [ -e "$link_path" ]; then - printf " %-10s ${YELLOW}exists (not a symlink)${NC} at %s\n" "$name" "$link_path" - else - printf " %-10s ${DIM}not linked${NC}\n" "$name" - fi - done - echo "" - return 0 - fi - - if [ -z "$game_dir" ]; then - err "Game directory not found. Set BAR_GAME_DIR env var or install BAR to the default location." - exit 1 - fi - - local source_path link_path - case "$target" in - engine) - source_path="$SCRIPT_DIR/RecoilEngine/build-linux/install" - link_path="$game_dir/engine/local-build" - ;; - chobby) - source_path="$SCRIPT_DIR/BYAR-Chobby" - link_path="$game_dir/games/BYAR-Chobby" - ;; - bar) - source_path="$SCRIPT_DIR/Beyond-All-Reason" - link_path="$game_dir/games/Beyond-All-Reason" - ;; - *) - err "Unknown link target: $target" - echo " Valid targets: engine, chobby, bar" - exit 1 - ;; - esac - - if [ ! -e "$source_path" ] && [ ! -L "$source_path" ]; then - err "Source not found: $source_path" - if [ "$target" = "engine" ]; then - echo " Build the engine first: ./devtools.sh engine build linux" - else - echo " Clone the repo first: ./devtools.sh clone extra" - fi - exit 1 - fi - - if [ -L "$link_path" ]; then - info "Replacing existing symlink at $link_path" - rm "$link_path" - elif [ -e "$link_path" ]; then - warn "$link_path already exists and is not a symlink. Skipping." - warn "Remove it manually if you want to replace it." - return 1 - fi - - mkdir -p "$(dirname "$link_path")" - ln -s "$source_path" "$link_path" - ok "Linked $target: $link_path -> $source_path" -} - -# =========================================================================== -# Main commands -# =========================================================================== - -cmd_init() { - echo -e "${BOLD}==========================================${NC}" - echo -e "${BOLD} BAR Dev Environment - First Time Setup${NC}" - echo -e "${BOLD}==========================================${NC}" - echo "" - - step "1/5 Checking & installing dependencies" - echo "" - local deps_ok=0 - if check_git &>/dev/null && check_docker &>/dev/null; then - deps_ok=1 - ok "Core dependencies (git, docker) already installed." - check_node || true - else - cmd_install_deps || { err "Dependency installation failed. Fix and retry."; exit 1; } - deps_ok=1 - fi - echo "" - - step "2/5 Cloning repositories" - echo "" - if [ ! -f "$REPOS_CONF" ]; then - err "repos.conf not found at: $REPOS_CONF" - exit 1 - fi - cmd_clone core - echo "" - - read -rp "Also clone extra repositories (game engine, SPADS source, infra)? [y/N] " extras - if [[ "$extras" =~ ^[Yy]$ ]]; then - cmd_clone extra - echo "" - fi - - step "3/5 Building Docker images" - echo "" - cmd_build - echo "" - - local do_build_engine=0 - if [ -d "$SCRIPT_DIR/RecoilEngine/docker-build-v2" ]; then - step "4/5 Engine build" - echo "" - read -rp "Build engine from source? [y/N] " build_engine - if [[ "$build_engine" =~ ^[Yy]$ ]]; then - do_build_engine=1 - info "Building Recoil engine (this may take a while)..." - "$SCRIPT_DIR/RecoilEngine/docker-build-v2/build.sh" linux - fi - echo "" - else - step "4/5 Engine build" - echo "" - info "RecoilEngine not cloned -- skipping. Clone with: ./devtools.sh clone extra" - echo "" - fi - - step "5/5 Symlinks to game directory" - echo "" - local game_dir - game_dir="$(detect_game_dir 2>/dev/null)" || true - if [ -z "$game_dir" ]; then - info "No game directory detected. Set BAR_GAME_DIR to enable linking." - echo "" - else - local available=() - if [ -d "$SCRIPT_DIR/RecoilEngine" ]; then - available+=("engine") - fi - if [ -d "$SCRIPT_DIR/BYAR-Chobby" ]; then - available+=("chobby") - fi - if [ -d "$SCRIPT_DIR/Beyond-All-Reason" ]; then - available+=("bar") - fi - - if [ "${#available[@]}" -gt 0 ]; then - echo " Available repos to symlink into $game_dir:" - for name in "${available[@]}"; do - case "$name" in - engine) echo -e " ${BOLD}engine${NC} -> $game_dir/engine/local-build/" ;; - chobby) echo -e " ${BOLD}chobby${NC} -> $game_dir/games/BYAR-Chobby/" ;; - bar) echo -e " ${BOLD}bar${NC} -> $game_dir/games/Beyond-All-Reason/" ;; - esac - done - echo "" - warn "This will replace any existing directories at these paths with symlinks." - read -rp "Symlink all? [y/N] " do_link - if [[ "$do_link" =~ ^[Yy]$ ]]; then - BAR_GAME_DIR="$game_dir" - for name in "${available[@]}"; do - cmd_link "$name" - done - fi - else - info "No linkable repos cloned yet." - fi - fi - echo "" - - echo -e "${BOLD}=== Setup Complete ===${NC}" - echo "" - echo " Your workspace is ready. Next steps:" - echo "" - echo -e " ${BOLD}./devtools.sh up${NC} Start Teiserver + PostgreSQL" - echo -e " ${BOLD}./devtools.sh up lobby${NC} ...and launch bar-lobby" - echo -e " ${BOLD}./devtools.sh up spads${NC} ...and start SPADS autohost" - echo -e " ${BOLD}./devtools.sh engine build${NC} Build the Recoil engine" - echo -e " ${BOLD}./devtools.sh link${NC} Show symlink status" - echo -e " ${BOLD}./devtools.sh repos${NC} Show repository status" - echo "" - echo " To use your own forks, copy repos.conf to repos.local.conf" - echo " and edit the URLs/branches. Then run: ./devtools.sh clone" - echo "" -} - -cmd_setup() { - echo -e "${BOLD}=== BAR Dev Environment Setup ===${NC}" - echo "" - check_prerequisites || exit 1 - - local missing_core=0 - load_repos_conf - for i in "${!REPO_DIRS[@]}"; do - if [ "${REPO_GROUPS[$i]}" = "core" ] && [ ! -d "$SCRIPT_DIR/${REPO_DIRS[$i]}/.git" ]; then - missing_core=1 - break - fi - done - - if [ "$missing_core" -eq 1 ]; then - warn "Core repositories are missing. Cloning them now..." - echo "" - cmd_clone core - echo "" - fi - - cmd_build - - echo "" - echo -e " Next steps:" - echo -e " ${BOLD}./devtools.sh up${NC} Start all services" - echo -e " ${BOLD}./devtools.sh up lobby${NC} Start all services + bar-lobby" - echo "" -} - -cmd_up() { - local start_lobby=0 - local with_spads=0 - for arg in "$@"; do - case "$arg" in - lobby|--lobby) start_lobby=1 ;; - spads|--spads) with_spads=1 ;; - esac - done - - install_dockerignore - - if [ "$with_spads" -eq 1 ]; then - info "Starting PostgreSQL, Teiserver, and SPADS..." - $COMPOSE --profile spads up -d --build - else - info "Starting PostgreSQL and Teiserver..." - $COMPOSE up -d --build - fi - - echo "" - info "Waiting for Teiserver to become healthy (first run takes several minutes)..." - echo " Follow progress: ./devtools.sh logs teiserver" - echo "" - - local attempts=0 - local max_attempts=120 - while [ $attempts -lt $max_attempts ]; do - local health - health=$($COMPOSE ps teiserver --format '{{.Health}}' 2>/dev/null || echo "unknown") - case "$health" in - healthy) - ok "Teiserver is healthy!" - break - ;; - unhealthy) - err "Teiserver failed to start. Check logs: ./devtools.sh logs teiserver" - exit 1 - ;; - *) - sleep 5 - attempts=$((attempts + 1)) - if [ $((attempts % 6)) -eq 0 ]; then - info "Still waiting... (${attempts}/${max_attempts}) - health: ${health}" - fi - ;; - esac - done - - if [ $attempts -ge $max_attempts ]; then - err "Timed out waiting for Teiserver. Check logs: ./devtools.sh logs teiserver" - exit 1 - fi - - echo "" - echo -e "${BOLD}=== Services Running ===${NC}" - echo "" - echo -e " ${GREEN}Teiserver Web UI${NC} http://localhost:4000" - echo -e " ${GREEN}Teiserver HTTPS${NC} https://localhost:8888" - echo -e " ${GREEN}Spring Protocol${NC} localhost:8200 (TCP) / :8201 (TLS)" - echo -e " ${GREEN}PostgreSQL${NC} localhost:${BAR_POSTGRES_PORT:-5433}" - echo "" - echo -e " ${BOLD}Login:${NC} root@localhost / password" - echo -e " ${BOLD}SPADS bot:${NC} spadsbot / password" - if [ "$with_spads" -eq 1 ]; then - echo "" - echo -e " SPADS is starting (check: ./devtools.sh logs spads)" - fi - echo "" - - if [ "$start_lobby" -eq 1 ]; then - cmd_lobby - fi -} - -cmd_down() { - info "Stopping all services..." - $COMPOSE --profile spads down - ok "All services stopped." -} - -cmd_status() { - echo -e "${BOLD}=== Service Status ===${NC}" - echo "" - $COMPOSE --profile spads ps -a -} - -cmd_logs() { - local service="${1:-}" - if [ -z "$service" ]; then - $COMPOSE --profile spads logs -f --tail=100 - else - $COMPOSE --profile spads logs -f --tail=100 "$service" - fi -} - -cmd_lobby() { - if [ ! -d "$LOBBY_DIR" ]; then - err "bar-lobby directory not found at: $LOBBY_DIR" - err "Run './devtools.sh clone' to clone repositories first." - exit 1 - fi - - if ! command -v node &>/dev/null; then - err "Node.js is required for bar-lobby. Run './devtools.sh install-deps'." - exit 1 - fi - - info "Installing bar-lobby dependencies..." - cd "$LOBBY_DIR" - npm install - - info "Starting bar-lobby dev server..." - echo " (Ctrl+C to stop the lobby; Docker services keep running)" - echo "" - - __NV_PRIME_RENDER_OFFLOAD=1 \ - __GLX_VENDOR_LIBRARY_NAME=nvidia \ - LC_CTYPE=C \ - npm start -- -- --no-sandbox -} - -cmd_reset() { - echo -e "${YELLOW}${BOLD}This will destroy all data (database, SPADS state, engine cache).${NC}" - read -rp "Are you sure? [y/N] " confirm - if [[ ! "$confirm" =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 0 - fi - - info "Stopping services and removing volumes..." - $COMPOSE --profile spads down -v - - info "Rebuilding images from scratch..." - $COMPOSE build --no-cache teiserver - $COMPOSE --profile spads pull spads - - ok "Reset complete. Run './devtools.sh up' to start fresh." -} - -cmd_shell() { - local service="${1:-teiserver}" - info "Opening shell in ${service}..." - $COMPOSE --profile spads exec "$service" bash -} - -cmd_update() { - echo -e "${BOLD}=== Updating All Repositories ===${NC}" - echo "" - load_repos_conf - - local i - for i in "${!REPO_DIRS[@]}"; do - local dir="${REPO_DIRS[$i]}" - local target="$SCRIPT_DIR/$dir" - if [ -d "$target/.git" ]; then - local branch - branch="$(git -C "$target" branch --show-current 2>/dev/null)" - info "${dir}: pulling ${branch}..." - git -C "$target" pull --ff-only 2>&1 | sed 's/^/ /' || warn " ${dir}: pull failed (conflicts?)" - fi - done - echo "" - ok "Update complete." -} - -# =========================================================================== -# Help -# =========================================================================== - -show_help() { - echo -e "${BOLD}BAR Development Environment${NC}" - echo "" - echo "Usage: ./devtools.sh [args]" - echo "" - echo -e "${BOLD}Getting Started (new developer):${NC}" - echo " init Full first-time setup: install deps, clone repos, build images" - echo " install-deps Install system packages (docker, git, nodejs)" - echo "" - echo -e "${BOLD}Services:${NC}" - echo " setup Check prerequisites and build Docker images" - echo " up [options] Start services. Options: lobby, spads" - echo " down Stop all services" - echo " status Show running services" - echo " logs [service] Tail logs (postgres, teiserver, spads, or all)" - echo " lobby Start bar-lobby dev server" - echo " reset Destroy all data and rebuild from scratch" - echo " shell [svc] Open a shell in a container (default: teiserver)" - echo "" - echo -e "${BOLD}Engine:${NC}" - echo " engine build [args] Build Recoil engine via docker-build-v2" - echo "" - echo -e "${BOLD}Game Directory:${NC}" - echo " link [target] Symlink repos into game directory (engine, chobby, bar)" - echo " With no target, shows status of all links" - echo "" - echo -e "${BOLD}Repositories:${NC}" - echo " clone [group] Clone/update repos (group: core, extra, or all)" - echo " repos Show status of all configured repositories" - echo " update Pull latest on all cloned repositories" - echo "" - echo -e "${BOLD}Examples:${NC}" - echo " ./devtools.sh init # New developer? Start here" - echo " ./devtools.sh up # Start postgres + teiserver" - echo " ./devtools.sh up lobby # Start stack + bar-lobby" - echo " ./devtools.sh up spads lobby # Start everything" - echo " ./devtools.sh engine build linux # Build engine for linux" - echo " ./devtools.sh link # Show symlink status" - echo " ./devtools.sh link engine # Symlink engine to game dir" - echo " ./devtools.sh repos # Check repo status" - echo " ./devtools.sh clone extra # Clone optional repos" - echo " ./devtools.sh logs teiserver # Follow Teiserver logs" - echo "" - echo -e "${BOLD}Configuration:${NC}" - echo " repos.conf Default repository URLs and branches" - echo " repos.local.conf Personal overrides (forks, branches) -- gitignored" - echo " BAR_GAME_DIR Env var: path to BAR game data directory (auto-detected if unset)" - echo "" - echo " To use your own fork of teiserver:" - echo " cp repos.conf repos.local.conf" - echo " # Edit repos.local.conf: change teiserver URL to your fork" - echo " ./devtools.sh clone core" - echo "" - echo -e "${BOLD}Docker Services:${NC}" - echo " postgres PostgreSQL 16 database" - echo " teiserver Elixir lobby server (HTTP :4000, Spring :8200/:8201)" - echo " spads Perl autohost (optional, needs game data)" - echo " bar-lobby Electron game client (runs natively, not in Docker)" - echo "" -} - -# =========================================================================== -# Dispatch -# =========================================================================== - -case "${1:-help}" in - init) cmd_init ;; - install-deps) cmd_install_deps ;; - setup) cmd_setup ;; - up) shift; cmd_up "$@" ;; - down) cmd_down ;; - status) cmd_status ;; - logs) cmd_logs "${2:-}" ;; - lobby) cmd_lobby ;; - reset) cmd_reset ;; - shell) cmd_shell "${2:-teiserver}" ;; - clone) cmd_clone "${2:-all}" ;; - repos) cmd_repos ;; - update) cmd_update ;; - build) cmd_build ;; - engine) shift; cmd_engine "$@" ;; - link) cmd_link "${2:-}" ;; - help|--help|-h) show_help ;; - *) err "Unknown command: $1"; echo ""; show_help; exit 1 ;; -esac +cat <<'EOF' +devtools.sh has been replaced by just recipes. + +Install just: + Arch: pacman -S just + Fedora: dnf install just + Debian/Ubuntu: apt install just + +Then run `just` to see all available commands. + +Command mapping: + ./devtools.sh init -> just setup::init + ./devtools.sh install-deps -> just setup::deps + ./devtools.sh up [lobby|spads] -> just services::up [lobby|spads] + ./devtools.sh down -> just services::down + ./devtools.sh status -> just services::status + ./devtools.sh logs [service] -> just services::logs [service] + ./devtools.sh lobby -> just services::lobby + ./devtools.sh shell [service] -> just services::shell [service] + ./devtools.sh build -> just services::build + ./devtools.sh reset -> just services::reset + ./devtools.sh clone [group] -> just repos::clone [group] + ./devtools.sh repos -> just repos::status + ./devtools.sh update -> just repos::update + ./devtools.sh engine build -> just engine::build + ./devtools.sh link -> just link::status + ./devtools.sh link -> just link::create +EOF +exit 1 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 94603dc..1a8cb62 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -78,6 +78,27 @@ services: - spads_cache:/opt/spads/var - spring_engines:/spring-engines + bar: + build: + context: . + dockerfile: docker/bar.Dockerfile + profiles: ["test"] + volumes: + - ./Beyond-All-Reason:/bar:z + - /bar/.lux + working_dir: /bar + + recoil-docs: + build: + context: ./RecoilEngine/doc/site + platform: linux/amd64 + profiles: ["docs"] + volumes: + - ./RecoilEngine:/recoil:z + working_dir: /recoil/doc/site + ports: + - "1313:1313" + volumes: pgdata: devtools_state: diff --git a/docker/bar.Dockerfile b/docker/bar.Dockerfile new file mode 100644 index 0000000..26cb8f8 --- /dev/null +++ b/docker/bar.Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:24.04 +ARG LUX_VERSION=latest +RUN apt-get update && apt-get install -y --no-install-recommends \ + lua5.1 liblua5.1-dev libreadline-dev \ + build-essential git ca-certificates curl libgpgme11t64 jq \ + && DEB_ARCH=$(dpkg --print-architecture) \ + && DEB_URL=$(curl -fsSL "https://api.github.com/repos/lumen-oss/lux/releases/${LUX_VERSION}" \ + | jq -r --arg arch "$DEB_ARCH" '.assets[] | select(.name | test("_" + $arch + "\\.deb$")) | .browser_download_url') \ + && curl -fsSL "$DEB_URL" -o /tmp/lux.deb \ + && dpkg -i /tmp/lux.deb \ + && rm /tmp/lux.deb \ + && apt-get purge -y jq && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* +RUN lx install-lua \ + && ln -sf /usr/bin/lua5.1 /root/.local/share/lux/tree/5.1/.lua/bin/lua +WORKDIR /bar diff --git a/just/bar.just b/just/bar.just new file mode 100644 index 0000000..520a293 --- /dev/null +++ b/just/bar.just @@ -0,0 +1,89 @@ +set export + +DEVTOOLS_DIR := justfile_directory() +COMPOSE_FILE := DEVTOOLS_DIR / "docker-compose.dev.yml" +COMPOSE := "docker compose -f " + COMPOSE_FILE +BAR_DIR := DEVTOOLS_DIR / "Beyond-All-Reason" +INTEGRATION_COMPOSE := "docker compose -f " + BAR_DIR / "tools" / "headless_testing" / "docker-compose.yml" + +[private] +require-bar: + #!/usr/bin/env bash + if [ ! -d "{{BAR_DIR}}" ]; then + echo "Error: Beyond-All-Reason is not cloned." >&2 + echo "Run: just repos::clone extra" >&2 + exit 1 + fi + +# Lint BAR Lua code (luacheck via lux) +lint *args: require-bar + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + enter_distrobox + (cd "$BAR_DIR" && lx lint {{args}}) + +# Format BAR Lua code (stylua via lux) +fmt *args: require-bar + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + enter_distrobox + (cd "$BAR_DIR" && lx fmt {{args}}) + +# Run busted unit tests +units *args: require-bar + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + enter_distrobox + (cd "$BAR_DIR" && lx test {{args}}) + +# Drop into an interactive test shell (busted on PATH) +test-shell: require-bar + #!/usr/bin/env bash + set -e + source "$DEVTOOLS_DIR/scripts/common.sh" + cd "$BAR_DIR" + echo -e "${GREEN}[ok]${NC} Entering lx test shell (busted is available)." + echo -e "${GREEN}[ok]${NC} Type 'exit' to return." + if [ -n "${DEVTOOLS_DISTROBOX:-}" ] && [ -z "${_DEVTOOLS_IN_DISTROBOX:-}" ] && [ ! -f /run/.containerenv ]; then + exec script -qec "distrobox enter '${DEVTOOLS_DISTROBOX}' -- lx shell --test --no-loader" /dev/null + fi + exec lx shell --test --no-loader + +# Run headless integration tests (x86-64 only) +integrations *args: require-bar + #!/usr/bin/env bash + if [[ "$(uname -m)" == arm64 ]]; then + echo "Error: integration tests require an x86-64 host." >&2 + echo "The Spring engine used by headless tests has no arm64 build." >&2 + exit 1 + fi + {{INTEGRATION_COMPOSE}} up --build --abort-on-container-exit {{args}} + +# Run all BAR tests (unit + integrations) +all: units integrations + +# Install git pre-commit hook in the BAR repo +setup-hooks: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + hook="$BAR_DIR/.git/hooks/pre-commit" + if [ ! -d "$BAR_DIR/.git" ]; then + err "BAR repo not found at $BAR_DIR" + info "Clone it first: just repos::clone extra" + exit 1 + fi + mkdir -p "$(dirname "$hook")" + printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -e' \ + 'echo "[pre-commit] Running lx fmt..."' \ + 'lx fmt' \ + 'echo "[pre-commit] Running lx lint..."' \ + 'lx lint' \ + > "$hook" + chmod +x "$hook" + ok "Installed pre-commit hook at $hook" diff --git a/just/docs.just b/just/docs.just new file mode 100644 index 0000000..a5fb79b --- /dev/null +++ b/just/docs.just @@ -0,0 +1,36 @@ +set export + +DEVTOOLS_DIR := justfile_directory() +RECOIL_DIR := DEVTOOLS_DIR / "RecoilEngine" +COMPOSE_FILE := DEVTOOLS_DIR / "docker-compose.dev.yml" +COMPOSE := "docker compose -f " + COMPOSE_FILE + +# Generate Lua API pages (full pipeline: extract -> JSON -> markdown) +generate: + {{COMPOSE}} run --rm recoil-docs lua_pages + +# Generate everything then start Hugo dev server +server: + {{COMPOSE}} run --rm --service-ports recoil-docs server_full -- --bind 0.0.0.0 + +# Start Hugo dev server without regenerating +server-only: + {{COMPOSE}} run --rm --service-ports recoil-docs server -- --bind 0.0.0.0 + +# Rebuild the docs container image +build: + {{COMPOSE}} build recoil-docs + +# Reset generated doc data files +# TODO: Same workaround as lua::reset. doc/site/data/* are pipeline outputs tracked in git, so +# local docs generation dirties the tree. Prefer these as build-only artifacts (CI publishes +# them) rather than tracked outputs developers must constantly revert. +reset: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + info "Resetting doc site data in RecoilEngine..." + cd "$RECOIL_DIR" + git checkout -- doc/site/data/ + git clean -fd doc/site/data/ + ok "Doc data reset." diff --git a/just/engine.just b/just/engine.just new file mode 100644 index 0000000..1cabdfd --- /dev/null +++ b/just/engine.just @@ -0,0 +1,32 @@ +set export + +DEVTOOLS_DIR := justfile_directory() + +# Build Recoil engine via docker-build-v2 +build *args: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + build_script="$DEVTOOLS_DIR/RecoilEngine/docker-build-v2/build.sh" + if [ ! -f "$build_script" ]; then + err "RecoilEngine not found. Clone it first: just repos::clone extra" + exit 1 + fi + if [ -z "{{args}}" ]; then + err "Usage: just engine::build [cmake-args...]" + echo "" + echo " Examples:" + echo " just engine::build linux" + echo " just engine::build linux -DCMAKE_BUILD_TYPE=Release" + echo " just engine::build linux -DTRACY_ENABLE=ON" + echo " just engine::build --help" + exit 1 + fi + arch_args="" + if [[ ! " {{args}} " =~ " --arch " ]]; then + case "$(uname -m)" in + x86_64) arch_args="--arch amd64" ;; + aarch64|arm64) arch_args="--arch arm64" ;; + esac + fi + exec bash "$build_script" $arch_args {{args}} diff --git a/just/link.just b/just/link.just new file mode 100644 index 0000000..71a58c1 --- /dev/null +++ b/just/link.just @@ -0,0 +1,19 @@ +set export + +DEVTOOLS_DIR := justfile_directory() + +# Show symlink status for game directory +status: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/setup.sh" + cmd_link + +# Symlink a repo into the game directory (engine, chobby, or bar) +create target: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/setup.sh" + cmd_link "{{target}}" diff --git a/just/lua.just b/just/lua.just new file mode 100644 index 0000000..3bca66e --- /dev/null +++ b/just/lua.just @@ -0,0 +1,71 @@ +set export + +DEVTOOLS_DIR := justfile_directory() +RECOIL_DIR := DEVTOOLS_DIR / "RecoilEngine" +BAR_DIR := DEVTOOLS_DIR / "Beyond-All-Reason" +LDE_DIR := DEVTOOLS_DIR / "lua-doc-extractor" + +# Build lua-doc-extractor from local checkout +build-lde: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + if [ ! -d "$LDE_DIR" ]; then + err "lua-doc-extractor not found at $LDE_DIR" + info "Clone it first: just repos::clone extra" + exit 1 + fi + cd "$LDE_DIR" + npm ci && npm run build + ok "lua-doc-extractor built" + +# Generate Lua library from RecoilEngine sources, copy into BAR submodule +library *flags: build-lde + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + LDE="node $LDE_DIR/dist/src/cli.js" + DEST="$RECOIL_DIR/rts/Lua/library/generated" + + info "Extracting Lua docs..." + $LDE \ + --src "$RECOIL_DIR/rts/{Lua,Rml/SolLua}/**/*.cpp" \ + --dest "$DEST" \ + --repo "https://github.com/beyond-all-reason/RecoilEngine/blob/master" \ + {{flags}} + + if [ -d "$BAR_DIR/recoil-lua-library/library" ]; then + info "Copying into BAR submodule..." + clean_dir "$BAR_DIR/recoil-lua-library/library" + mkdir -p "$BAR_DIR/recoil-lua-library/library" + cp -r "$RECOIL_DIR/rts/Lua/library/"* \ + "$BAR_DIR/recoil-lua-library/library/" + ok "Updated $BAR_DIR/recoil-lua-library/library/" + echo " Run 'just lua::reset' to reset Recoil library and BAR recoil-lua-library." + fi + +# Generate library then restart LuaLS so the editor picks up changes +library-reload *flags: (library flags) + -pkill -f lua-language-server + @echo "LuaLS restarting (editor extension will respawn it)" + +# Reset generated Lua library files +# TODO: This is a workaround for the fact that these CI artifacts are in source control and +# generally developers don't want generated output dirtying PRs that only change inputs. +# Prefer recoil-lua-library NOT be a submodule but a local build artifact produced by our +# scripting layer, with CI building and publishing the package without committing it back. +reset: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + info "Resetting Lua library in RecoilEngine..." + cd "$RECOIL_DIR" + git checkout -- rts/Lua/library/ + git clean -fd rts/Lua/library/ + if [ -d "$BAR_DIR/recoil-lua-library" ]; then + info "Resetting recoil-lua-library submodule in BAR..." + cd "$BAR_DIR" + git submodule update --init --force recoil-lua-library + git -C recoil-lua-library clean -fd + fi + ok "Lua library reset." diff --git a/just/repos.just b/just/repos.just new file mode 100644 index 0000000..c59229b --- /dev/null +++ b/just/repos.just @@ -0,0 +1,29 @@ +set export + +DEVTOOLS_DIR := justfile_directory() +REPOS_CONF := DEVTOOLS_DIR / "repos.conf" +REPOS_LOCAL := DEVTOOLS_DIR / "repos.local.conf" + +# Clone or update repos (group: core, extra, or all) +clone group="all": + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/repos.sh" + cmd_clone "{{group}}" + +# Show status of all configured repositories +status: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/repos.sh" + cmd_repos + +# Pull latest on all cloned repositories +update: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/repos.sh" + cmd_update diff --git a/just/services.just b/just/services.just new file mode 100644 index 0000000..b07b2ec --- /dev/null +++ b/just/services.just @@ -0,0 +1,179 @@ +set export + +DEVTOOLS_DIR := justfile_directory() +COMPOSE_FILE := DEVTOOLS_DIR / "docker-compose.dev.yml" +COMPOSE := "docker compose -f " + COMPOSE_FILE +LOBBY_DIR := DEVTOOLS_DIR / "bar-lobby" + +# Start dev services. Options: lobby, spads +up *args: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/setup.sh" + install_dockerignore + + start_lobby=0 + with_spads=0 + for arg in {{args}}; do + case "$arg" in + lobby|--lobby) start_lobby=1 ;; + spads|--spads) with_spads=1 ;; + esac + done + + if [ "$with_spads" -eq 1 ]; then + info "Starting PostgreSQL, Teiserver, and SPADS..." + $COMPOSE --profile spads up -d --build + else + info "Starting PostgreSQL and Teiserver..." + $COMPOSE up -d --build + fi + + echo "" + info "Waiting for Teiserver to become healthy (first run takes several minutes)..." + echo " Follow progress: just services::logs teiserver" + echo "" + + attempts=0 + max_attempts=120 + while [ $attempts -lt $max_attempts ]; do + health=$($COMPOSE ps teiserver --format '{{{{.Health}}}}' 2>/dev/null || echo "unknown") + case "$health" in + healthy) + ok "Teiserver is healthy!" + break + ;; + unhealthy) + err "Teiserver failed to start. Check logs: just services::logs teiserver" + exit 1 + ;; + *) + sleep 5 + attempts=$((attempts + 1)) + if [ $((attempts % 6)) -eq 0 ]; then + info "Still waiting... (${attempts}/${max_attempts}) - health: ${health}" + fi + ;; + esac + done + + if [ $attempts -ge $max_attempts ]; then + err "Timed out waiting for Teiserver. Check logs: just services::logs teiserver" + exit 1 + fi + + echo "" + echo -e "${BOLD}=== Services Running ===${NC}" + echo "" + echo -e " ${GREEN}Teiserver Web UI${NC} http://localhost:4000" + echo -e " ${GREEN}Teiserver HTTPS${NC} https://localhost:8888" + echo -e " ${GREEN}Spring Protocol${NC} localhost:8200 (TCP) / :8201 (TLS)" + echo -e " ${GREEN}PostgreSQL${NC} localhost:${BAR_POSTGRES_PORT:-5433}" + echo "" + echo -e " ${BOLD}Login:${NC} root@localhost / password" + echo -e " ${BOLD}SPADS bot:${NC} spadsbot / password" + if [ "$with_spads" -eq 1 ]; then + echo "" + echo -e " SPADS is starting (check: just services::logs spads)" + fi + echo "" + + if [ "$start_lobby" -eq 1 ]; then + if [ ! -d "$LOBBY_DIR" ]; then + err "bar-lobby directory not found. Run 'just repos::clone' first." + exit 1 + fi + if ! command -v node &>/dev/null; then + err "Node.js is required for bar-lobby. Run 'just setup::deps'." + exit 1 + fi + info "Installing bar-lobby dependencies..." + cd "$LOBBY_DIR" + npm install + info "Starting bar-lobby dev server..." + echo " (Ctrl+C to stop the lobby; Docker services keep running)" + echo "" + __NV_PRIME_RENDER_OFFLOAD=1 \ + __GLX_VENDOR_LIBRARY_NAME=nvidia \ + LC_CTYPE=C \ + npm start -- -- --no-sandbox + fi + +# Stop all services +down: + $COMPOSE --profile spads down + +# Show running services +status: + #!/usr/bin/env bash + echo -e "\033[1m=== Service Status ===\033[0m" + echo "" + $COMPOSE --profile spads ps -a + +# Tail service logs +logs service="": + #!/usr/bin/env bash + if [ -z "{{service}}" ]; then + $COMPOSE --profile spads logs -f --tail=100 + else + $COMPOSE --profile spads logs -f --tail=100 "{{service}}" + fi + +# Start bar-lobby dev server +lobby: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + if [ ! -d "$LOBBY_DIR" ]; then + err "bar-lobby directory not found at: $LOBBY_DIR" + err "Run 'just repos::clone' to clone repositories first." + exit 1 + fi + if ! command -v node &>/dev/null; then + err "Node.js is required for bar-lobby. Run 'just setup::deps'." + exit 1 + fi + info "Installing bar-lobby dependencies..." + cd "$LOBBY_DIR" + npm install + info "Starting bar-lobby dev server..." + echo " (Ctrl+C to stop the lobby; Docker services keep running)" + echo "" + __NV_PRIME_RENDER_OFFLOAD=1 \ + __GLX_VENDOR_LIBRARY_NAME=nvidia \ + LC_CTYPE=C \ + npm start -- -- --no-sandbox + +# Open a shell in a container +shell service="teiserver": + $COMPOSE --profile spads exec {{service}} bash + +# Build Docker images +build: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/setup.sh" + install_dockerignore + info "Building Docker images..." + info " - Teiserver: compiling Elixir deps + generating TLS certs" + info " - SPADS: pulling pre-built image (badosu/spads:latest)" + echo "" + $COMPOSE build teiserver + $COMPOSE --profile spads pull spads + echo "" + ok "Images built successfully." + +# Destroy all data and rebuild from scratch +[confirm("This will destroy all data (database, SPADS state, engine cache). Continue?")] +reset: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + info "Stopping services and removing volumes..." + $COMPOSE --profile spads down -v + info "Rebuilding images from scratch..." + $COMPOSE build --no-cache teiserver + $COMPOSE --profile spads pull spads + ok "Reset complete. Run 'just services::up' to start fresh." diff --git a/just/setup.just b/just/setup.just new file mode 100644 index 0000000..0027dc8 --- /dev/null +++ b/just/setup.just @@ -0,0 +1,33 @@ +set export + +DEVTOOLS_DIR := justfile_directory() +COMPOSE_FILE := DEVTOOLS_DIR / "docker-compose.dev.yml" +COMPOSE := "docker compose -f " + COMPOSE_FILE +REPOS_CONF := DEVTOOLS_DIR / "repos.conf" +REPOS_LOCAL := DEVTOOLS_DIR / "repos.local.conf" + +# Full first-time setup: install deps, clone repos, build images +init *args: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/repos.sh" + source "$DEVTOOLS_DIR/scripts/setup.sh" + cmd_init {{args}} + +# Install system packages (docker, git, nodejs) +deps: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/setup.sh" + cmd_install_deps + +# Check prerequisites and build Docker images +check: + #!/usr/bin/env bash + set -euo pipefail + source "$DEVTOOLS_DIR/scripts/common.sh" + source "$DEVTOOLS_DIR/scripts/repos.sh" + source "$DEVTOOLS_DIR/scripts/setup.sh" + cmd_setup diff --git a/just/tei.just b/just/tei.just new file mode 100644 index 0000000..e5c87a5 --- /dev/null +++ b/just/tei.just @@ -0,0 +1,21 @@ +set export + +DEVTOOLS_DIR := justfile_directory() +COMPOSE_FILE := DEVTOOLS_DIR / "docker-compose.dev.yml" +COMPOSE := "docker compose -f " + COMPOSE_FILE +TEISERVER_DIR := DEVTOOLS_DIR / "teiserver" + +[private] +require-setup: + #!/usr/bin/env bash + if [ ! -d "{{TEISERVER_DIR}}" ]; then + echo "Error: teiserver is not cloned." >&2 + echo "Run: just setup::init" >&2 + exit 1 + fi + +# Run teiserver mix tests +mix *args: require-setup + {{COMPOSE}} run --rm -e MIX_ENV=test --entrypoint "" \ + -v {{TEISERVER_DIR}}/test:/app/test \ + teiserver bash -c "mix ecto.create --quiet 2>/dev/null; mix ecto.migrate --quiet && mix test {{args}}" diff --git a/repos.conf b/repos.conf index f248d81..8d44f79 100644 --- a/repos.conf +++ b/repos.conf @@ -32,5 +32,6 @@ BYAR-Chobby https://github.com/beyond-all-reason/BYAR-Chobby.git bar-db https://github.com/beyond-all-reason/bar-db.git master extra bar-live-services https://github.com/beyond-all-reason/bar-live-services.git main extra RecoilEngine https://github.com/beyond-all-reason/RecoilEngine.git master extra +lua-doc-extractor https://github.com/rhys-vdw/lua-doc-extractor main extra SPADS https://github.com/Yaribz/SPADS.git master extra SpringLobbyInterface https://github.com/Yaribz/SpringLobbyInterface.git master extra diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 0000000..8284f29 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Shared helpers for BAR-Devtools scripts. +# Source this file; it only defines functions and variables. + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +info() { echo -e "${BLUE}[info]${NC} $*"; } +ok() { echo -e "${GREEN}[ok]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +err() { echo -e "${RED}[error]${NC} $*"; } +step() { echo -e "${CYAN}[step]${NC} $*"; } diff --git a/scripts/repos.sh b/scripts/repos.sh new file mode 100644 index 0000000..4c48260 --- /dev/null +++ b/scripts/repos.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# Repository management helpers. +# Expects: DEVTOOLS_DIR, REPOS_CONF, REPOS_LOCAL (exported by Justfile) +# Source scripts/common.sh before this file. + +declare -a REPO_DIRS=() REPO_URLS=() REPO_BRANCHES=() REPO_GROUPS=() REPO_LOCAL_PATHS=() + +load_repos_conf() { + REPO_DIRS=(); REPO_URLS=(); REPO_BRANCHES=(); REPO_GROUPS=(); REPO_LOCAL_PATHS=() + local -A seen=() + + _parse_conf() { + local file="$1" + [ -f "$file" ] || return 0 + while IFS= read -r line || [ -n "$line" ]; do + line="${line%%#*}" + line="$(echo "$line" | xargs 2>/dev/null || true)" + [ -z "$line" ] && continue + local dir url branch group local_path + read -r dir url branch group local_path <<< "$line" + [ -z "$dir" ] || [ -z "$url" ] && continue + branch="${branch:-master}" + group="${group:-extra}" + local_path="${local_path/#\~/$HOME}" + seen[$dir]="$url $branch $group $local_path" + done < "$file" + } + + _parse_conf "$REPOS_CONF" + _parse_conf "$REPOS_LOCAL" + + local dir + for dir in "${!seen[@]}"; do + local url branch group local_path + read -r url branch group local_path <<< "${seen[$dir]}" + REPO_DIRS+=("$dir") + REPO_URLS+=("$url") + REPO_BRANCHES+=("$branch") + REPO_GROUPS+=("$group") + REPO_LOCAL_PATHS+=("$local_path") + done +} + +clone_or_update_repo() { + local dir="$1" url="$2" branch="$3" local_path="${4:-}" target="$DEVTOOLS_DIR/$dir" + + if [ -n "$local_path" ]; then + if [ ! -d "$local_path" ]; then + warn " ${dir}: local path does not exist: ${local_path}" + return 1 + fi + if [ -L "$target" ]; then + local current_link + current_link="$(readlink "$target")" + if [ "$current_link" = "$local_path" ]; then + ok " ${dir}: linked -> ${local_path}" + else + warn " ${dir}: symlink points to ${current_link}, config says ${local_path}" + info " ${dir}: updating symlink..." + rm "$target" + ln -s "$local_path" "$target" + ok " ${dir}: linked -> ${local_path}" + fi + elif [ -d "$target" ]; then + warn " ${dir}: exists as a real directory but config says link to ${local_path}" + warn " ${dir}: remove it manually to use the local path" + else + ln -s "$local_path" "$target" + ok " ${dir}: linked -> ${local_path}" + fi + return 0 + fi + + if [ -d "$target/.git" ]; then + local current_url + current_url="$(git -C "$target" remote get-url origin 2>/dev/null || true)" + if [ "$current_url" != "$url" ] && [ -n "$current_url" ]; then + warn " ${dir}: origin is ${current_url}" + warn " ${dir}: config says ${url}" + warn " ${dir}: add to repos.local.conf to set your preferred remote" + fi + info " ${dir}: fetching latest..." + git -C "$target" fetch origin --quiet 2>/dev/null || warn " ${dir}: fetch failed (offline?)" + local current_branch + current_branch="$(git -C "$target" branch --show-current 2>/dev/null)" + if [ -n "$current_branch" ] && [ "$current_branch" != "$branch" ]; then + info " ${dir}: on branch '${current_branch}' (config says '${branch}')" + fi + else + info " ${dir}: cloning ${url} (branch: ${branch})..." + git clone --recurse-submodules --branch "$branch" "$url" "$target" 2>&1 | sed 's/^/ /' + fi +} + +cmd_clone() { + local group_filter="${1:-all}" + load_repos_conf + + if [ "${#REPO_DIRS[@]}" -eq 0 ]; then + err "No repositories found in repos.conf" + exit 1 + fi + + echo -e "${BOLD}=== Cloning / Updating Repositories ===${NC}" + echo "" + + if [ -f "$REPOS_LOCAL" ]; then + info "Using overrides from repos.local.conf" + echo "" + fi + + local i cloned=0 updated=0 skipped=0 linked=0 + for i in "${!REPO_DIRS[@]}"; do + local dir="${REPO_DIRS[$i]}" + local url="${REPO_URLS[$i]}" + local branch="${REPO_BRANCHES[$i]}" + local group="${REPO_GROUPS[$i]}" + local local_path="${REPO_LOCAL_PATHS[$i]}" + + if [ "$group_filter" != "all" ] && [ "$group" != "$group_filter" ]; then + skipped=$((skipped + 1)) + continue + fi + + if [ -n "$local_path" ]; then + clone_or_update_repo "$dir" "$url" "$branch" "$local_path" + linked=$((linked + 1)) + elif [ -d "$DEVTOOLS_DIR/$dir/.git" ]; then + clone_or_update_repo "$dir" "$url" "$branch" + updated=$((updated + 1)) + else + clone_or_update_repo "$dir" "$url" "$branch" + cloned=$((cloned + 1)) + fi + done + + echo "" + local summary="${cloned} cloned, ${updated} updated, ${skipped} skipped" + [ "$linked" -gt 0 ] && summary+=", ${linked} linked" + ok "Repos: ${summary}" +} + +cmd_repos() { + load_repos_conf + + echo -e "${BOLD}=== Repository Status ===${NC}" + echo "" + printf " ${DIM}%-24s %-8s %-18s %s${NC}\n" "DIRECTORY" "GROUP" "BRANCH" "STATUS" + echo " $(printf '%.0s-' {1..80})" + + local i + for i in "${!REPO_DIRS[@]}"; do + local dir="${REPO_DIRS[$i]}" + local group="${REPO_GROUPS[$i]}" + local target="$DEVTOOLS_DIR/$dir" + + local status current_branch + if [ -L "$target" ]; then + local link_dest + link_dest="$(readlink "$target")" + if [ -d "$target/.git" ]; then + current_branch="$(git -C "$target" branch --show-current 2>/dev/null || echo "detached")" + local dirty="" + if ! git -C "$target" diff --quiet 2>/dev/null || ! git -C "$target" diff --cached --quiet 2>/dev/null; then + dirty=" ${YELLOW}*dirty*${NC}" + fi + status="${CYAN}local${NC}${dirty} -> ${link_dest}" + else + status="${RED}broken link${NC} -> ${link_dest}" + current_branch="-" + fi + elif [ -d "$target/.git" ]; then + current_branch="$(git -C "$target" branch --show-current 2>/dev/null || echo "detached")" + local dirty="" + if ! git -C "$target" diff --quiet 2>/dev/null || ! git -C "$target" diff --cached --quiet 2>/dev/null; then + dirty=" ${YELLOW}*dirty*${NC}" + fi + local branch="${REPO_BRANCHES[$i]}" + if [ "$current_branch" = "$branch" ]; then + status="${GREEN}ok${NC}${dirty}" + else + status="${YELLOW}branch: ${current_branch}${NC}${dirty}" + fi + else + status="${RED}missing${NC}" + current_branch="-" + fi + + printf " %-24s %-8s %-18s %b\n" "$dir" "$group" "$current_branch" "$status" + done + echo "" +} + +cmd_update() { + echo -e "${BOLD}=== Updating All Repositories ===${NC}" + echo "" + load_repos_conf + + local i + for i in "${!REPO_DIRS[@]}"; do + local dir="${REPO_DIRS[$i]}" + local target="$DEVTOOLS_DIR/$dir" + if [ -d "$target/.git" ]; then + local branch + branch="$(git -C "$target" branch --show-current 2>/dev/null)" + info "${dir}: pulling ${branch}..." + git -C "$target" pull --ff-only 2>&1 | sed 's/^/ /' || warn " ${dir}: pull failed (conflicts?)" + fi + done + echo "" + ok "Update complete." +} diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..4f58311 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,498 @@ +#!/usr/bin/env bash +# Setup, dependency installation, and prerequisite checks. +# Expects: DEVTOOLS_DIR, COMPOSE, REPOS_CONF (exported by Justfile) +# Source scripts/common.sh and scripts/repos.sh before this file. + +detect_distro() { + if [[ "$OSTYPE" == darwin* ]]; then + echo "macos" + elif command -v pacman &>/dev/null; then + echo "arch" + elif command -v apt-get &>/dev/null; then + echo "debian" + elif command -v dnf &>/dev/null; then + echo "fedora" + else + echo "unknown" + fi +} + +pkg_install_cmd() { + case "$(detect_distro)" in + macos) echo "brew install" ;; + arch) echo "sudo pacman -S --needed" ;; + debian) echo "sudo apt install -y" ;; + fedora) echo "sudo dnf install -y" ;; + *) echo "" ;; + esac +} + +pkg_name() { + local generic="$1" + local distro + distro="$(detect_distro)" + case "${distro}:${generic}" in + macos:docker) echo "--cask docker" ;; + macos:docker-compose) echo "" ;; + macos:git) echo "git" ;; + macos:nodejs) echo "node" ;; + arch:docker) echo "docker" ;; + arch:docker-compose) echo "docker-compose" ;; + arch:git) echo "git" ;; + arch:nodejs) echo "nodejs npm" ;; + debian:docker) echo "docker.io" ;; + debian:docker-compose) echo "docker-compose-plugin" ;; + debian:git) echo "git" ;; + debian:nodejs) echo "nodejs npm" ;; + fedora:docker) echo "docker-ce docker-ce-cli containerd.io" ;; + fedora:docker-compose) echo "docker-compose-plugin" ;; + fedora:git) echo "git" ;; + fedora:nodejs) echo "nodejs npm" ;; + *) echo "$generic" ;; + esac +} + +check_git() { + if ! command -v git &>/dev/null; then + err "git is not installed." + return 1 + fi + ok "git $(git --version | awk '{print $3}') detected" +} + +check_docker() { + if ! command -v docker &>/dev/null; then + err "Docker is not installed." + return 1 + fi + if ! docker info &>/dev/null; then + err "Docker daemon is not running or current user lacks permissions." + echo "" + if [[ "$OSTYPE" == darwin* ]]; then + echo " Open Docker Desktop to start the daemon." + else + echo " Start the daemon: sudo systemctl start docker" + echo " Enable on boot: sudo systemctl enable docker" + echo " Add yourself: sudo usermod -aG docker \$USER (then re-login)" + fi + echo "" + return 1 + fi + if ! docker compose version &>/dev/null; then + err "Docker Compose V2 plugin is not installed." + return 1 + fi + ok "Docker $(docker --version | awk '{print $3}' | tr -d ',') + Compose V2 detected" +} + +check_node() { + if ! command -v node &>/dev/null; then + warn "Node.js not found (needed for bar-lobby only)." + return 1 + fi + ok "Node.js $(node --version) detected" +} + +check_ports() { + local pg_port="${BAR_POSTGRES_PORT:-5433}" + local ports=(4000 "$pg_port" 8200 8201 8888) + local conflict=0 + for port in "${ports[@]}"; do + if [[ "$OSTYPE" == darwin* ]]; then + if lsof -iTCP:"$port" -sTCP:LISTEN &>/dev/null; then + warn "Port ${port} is already in use" + conflict=1 + fi + else + if ss -tlnp 2>/dev/null | grep -q ":${port} "; then + warn "Port ${port} is already in use" + conflict=1 + fi + fi + done + if [ "$conflict" -eq 1 ]; then + warn "Some ports are in use. Services binding to those ports may fail to start." + else + ok "Required ports available (4000, ${pg_port}, 8200, 8201, 8888)" + fi +} + +check_prerequisites() { + echo -e "${BOLD}Checking prerequisites...${NC}" + echo "" + local failed=0 + check_git || failed=1 + check_docker || failed=1 + check_node || true + check_ports + echo "" + if [ "$failed" -ne 0 ]; then + err "Missing required prerequisites. Run 'just setup::deps' or fix manually." + return 1 + fi +} + +install_dockerignore() { + local target="$DEVTOOLS_DIR/teiserver/.dockerignore" + local source="$DEVTOOLS_DIR/docker/teiserver.dockerignore" + if [ -f "$source" ] && [ ! -f "$target" ]; then + cp "$source" "$target" + info "Installed .dockerignore for teiserver build context" + fi +} + +cmd_install_deps() { + echo -e "${BOLD}=== Install System Dependencies ===${NC}" + echo "" + + local distro + distro="$(detect_distro)" + local install_cmd + install_cmd="$(pkg_install_cmd)" + + if [ "$distro" = "unknown" ] || [ -z "$install_cmd" ]; then + err "Unsupported distro. Install these manually: git, docker, docker-compose, nodejs, npm" + exit 1 + fi + + info "Detected distro: ${BOLD}${distro}${NC}" + echo "" + + local missing=() + + if ! command -v git &>/dev/null; then + missing+=("git") + fi + if ! command -v docker &>/dev/null; then + missing+=("docker") + fi + if ! docker compose version &>/dev/null 2>&1; then + missing+=("docker-compose") + fi + if ! command -v node &>/dev/null; then + missing+=("nodejs") + fi + + if [ "${#missing[@]}" -eq 0 ]; then + ok "All dependencies already installed." + echo "" + + if ! docker info &>/dev/null; then + warn "Docker is installed but the daemon isn't running or you lack permissions." + echo "" + if [[ "$OSTYPE" == darwin* ]]; then + echo " Open Docker Desktop to start the daemon." + else + echo " sudo systemctl start docker" + echo " sudo systemctl enable docker" + echo " sudo usermod -aG docker \$USER # then re-login" + fi + echo "" + fi + return 0 + fi + + local packages="" + for dep in "${missing[@]}"; do + packages+=" $(pkg_name "$dep")" + done + + info "Missing: ${missing[*]}" + info "Will run: ${install_cmd}${packages}" + echo "" + + read -rp "Install now? [Y/n] " confirm + if [[ "$confirm" =~ ^[Nn]$ ]]; then + echo "Skipped. Install manually and retry." + return 1 + fi + + $install_cmd $packages + + echo "" + + if [[ " ${missing[*]} " == *" docker "* ]]; then + if [[ "$OSTYPE" == darwin* ]]; then + warn "Docker Desktop was installed. Open it to finish setup, then re-run: just setup::init" + return 0 + else + info "Enabling and starting Docker daemon..." + sudo systemctl enable --now docker 2>/dev/null || true + + if ! groups | grep -qw docker; then + info "Adding $USER to the docker group (re-login required)..." + sudo usermod -aG docker "$USER" + warn "You need to log out and back in for Docker group membership to take effect." + warn "After re-login, run: just setup::init" + return 0 + fi + fi + fi + + ok "Dependencies installed successfully." +} + +cmd_init() { + local clone_extras=0 + for arg in "$@"; do + case "$arg" in + extras|all) clone_extras=1 ;; + esac + done + + echo -e "${BOLD}==========================================${NC}" + echo -e "${BOLD} BAR Dev Environment - First Time Setup${NC}" + echo -e "${BOLD}==========================================${NC}" + echo "" + + step "1/5 Checking & installing dependencies" + echo "" + local deps_ok=0 + if check_git &>/dev/null && check_docker &>/dev/null; then + deps_ok=1 + ok "Core dependencies (git, docker) already installed." + check_node || true + else + cmd_install_deps || { err "Dependency installation failed. Fix and retry."; exit 1; } + deps_ok=1 + fi + echo "" + + step "2/5 Cloning repositories" + echo "" + if [ ! -f "$REPOS_CONF" ]; then + err "repos.conf not found at: $REPOS_CONF" + exit 1 + fi + cmd_clone core + echo "" + + if [ "$clone_extras" -eq 1 ]; then + cmd_clone extra + echo "" + else + read -rp "Also clone extra repositories (game engine, SPADS source, infra)? [y/N] " extras + if [[ "$extras" =~ ^[Yy]$ ]]; then + cmd_clone extra + echo "" + fi + fi + + step "3/5 Building Docker images" + echo "" + install_dockerignore + info "Building Docker images..." + $COMPOSE build teiserver + $COMPOSE --profile spads pull spads + ok "Images built successfully." + echo "" + + if [ -d "$DEVTOOLS_DIR/RecoilEngine/docker-build-v2" ]; then + step "4/5 Engine build" + echo "" + read -rp "Build engine from source? [y/N] " build_engine + if [[ "$build_engine" =~ ^[Yy]$ ]]; then + local engine_arch + case "$(uname -m)" in + x86_64) engine_arch="amd64" ;; + aarch64|arm64) engine_arch="arm64" ;; + *) engine_arch="amd64" ;; + esac + info "Building Recoil engine (${engine_arch}-linux, this may take a while)..." + bash "$DEVTOOLS_DIR/RecoilEngine/docker-build-v2/build.sh" --arch "$engine_arch" linux + fi + echo "" + else + step "4/5 Engine build" + echo "" + info "RecoilEngine not cloned -- skipping. Clone with: just repos::clone extra" + echo "" + fi + + step "5/5 Symlinks to game directory" + echo "" + local game_dir + game_dir="$(detect_game_dir 2>/dev/null)" || true + if [ -z "$game_dir" ]; then + info "No game directory detected. Set BAR_GAME_DIR to enable linking." + echo "" + else + local available=() + [ -d "$DEVTOOLS_DIR/RecoilEngine" ] && available+=("engine") + [ -d "$DEVTOOLS_DIR/BYAR-Chobby" ] && available+=("chobby") + [ -d "$DEVTOOLS_DIR/Beyond-All-Reason" ] && available+=("bar") + + if [ "${#available[@]}" -gt 0 ]; then + echo " Available repos to symlink into $game_dir:" + for name in "${available[@]}"; do + case "$name" in + engine) echo -e " ${BOLD}engine${NC} -> $game_dir/engine/local-build/" ;; + chobby) echo -e " ${BOLD}chobby${NC} -> $game_dir/games/BYAR-Chobby/" ;; + bar) echo -e " ${BOLD}bar${NC} -> $game_dir/games/Beyond-All-Reason/" ;; + esac + done + echo "" + warn "This will replace any existing directories at these paths with symlinks." + read -rp "Symlink all? [y/N] " do_link + if [[ "$do_link" =~ ^[Yy]$ ]]; then + BAR_GAME_DIR="$game_dir" + for name in "${available[@]}"; do + cmd_link "$name" + done + fi + else + info "No linkable repos cloned yet." + fi + fi + echo "" + + echo -e "${BOLD}=== Setup Complete ===${NC}" + echo "" + echo " Your workspace is ready. Next steps:" + echo "" + echo -e " ${BOLD}just services::up${NC} Start Teiserver + PostgreSQL" + echo -e " ${BOLD}just services::up lobby${NC} ...and launch bar-lobby" + echo -e " ${BOLD}just services::up spads${NC} ...and start SPADS autohost" + echo -e " ${BOLD}just engine::build linux${NC} Build the Recoil engine" + echo -e " ${BOLD}just link::status${NC} Show symlink status" + echo -e " ${BOLD}just repos::status${NC} Show repository status" + echo "" + echo " To use your own forks, copy repos.conf to repos.local.conf" + echo " and edit the URLs/branches. Then run: just repos::clone" + echo "" +} + +cmd_setup() { + echo -e "${BOLD}=== BAR Dev Environment Setup ===${NC}" + echo "" + check_prerequisites || exit 1 + + local missing_core=0 + load_repos_conf + for i in "${!REPO_DIRS[@]}"; do + if [ "${REPO_GROUPS[$i]}" = "core" ] && [ ! -d "$DEVTOOLS_DIR/${REPO_DIRS[$i]}/.git" ]; then + missing_core=1 + break + fi + done + + if [ "$missing_core" -eq 1 ]; then + warn "Core repositories are missing. Cloning them now..." + echo "" + cmd_clone core + echo "" + fi + + install_dockerignore + info "Building Docker images..." + $COMPOSE build teiserver + $COMPOSE --profile spads pull spads + ok "Images built successfully." + + echo "" + echo -e " Next steps:" + echo -e " ${BOLD}just services::up${NC} Start all services" + echo -e " ${BOLD}just services::up lobby${NC} Start all services + bar-lobby" + echo "" +} + +detect_game_dir() { + if [ -n "${BAR_GAME_DIR:-}" ]; then + echo "$BAR_GAME_DIR" + return 0 + fi + local xdg_state="${XDG_STATE_HOME:-$HOME/.local/state}" + local candidate="$xdg_state/Beyond All Reason" + if [ -d "$candidate" ]; then + echo "$candidate" + return 0 + fi + return 1 +} + +cmd_link() { + local target="${1:-}" + local game_dir + game_dir="$(detect_game_dir 2>/dev/null)" || true + + if [ -z "$target" ]; then + echo -e "${BOLD}=== Symlink Status ===${NC}" + echo "" + if [ -z "$game_dir" ]; then + warn "Game directory not found. Set BAR_GAME_DIR env var or install BAR to the default location." + echo "" + return 0 + fi + info "Game directory: ${game_dir}" + echo "" + + local -A link_map=( + [engine]="$game_dir/engine/local-build" + [chobby]="$game_dir/games/BYAR-Chobby" + [bar]="$game_dir/games/Beyond-All-Reason" + ) + for name in engine chobby bar; do + local link_path="${link_map[$name]}" + if [ -L "$link_path" ]; then + local link_target + link_target="$(readlink -f "$link_path" 2>/dev/null || echo "?")" + printf " %-10s ${GREEN}linked${NC} -> %s\n" "$name" "$link_target" + elif [ -e "$link_path" ]; then + printf " %-10s ${YELLOW}exists (not a symlink)${NC} at %s\n" "$name" "$link_path" + else + printf " %-10s ${DIM}not linked${NC}\n" "$name" + fi + done + echo "" + return 0 + fi + + if [ -z "$game_dir" ]; then + err "Game directory not found. Set BAR_GAME_DIR env var or install BAR to the default location." + exit 1 + fi + + local source_path link_path + case "$target" in + engine) + source_path="$DEVTOOLS_DIR/RecoilEngine/build-linux/install" + link_path="$game_dir/engine/local-build" + ;; + chobby) + source_path="$DEVTOOLS_DIR/BYAR-Chobby" + link_path="$game_dir/games/BYAR-Chobby" + ;; + bar) + source_path="$DEVTOOLS_DIR/Beyond-All-Reason" + link_path="$game_dir/games/Beyond-All-Reason" + ;; + *) + err "Unknown link target: $target" + echo " Valid targets: engine, chobby, bar" + exit 1 + ;; + esac + + if [ ! -e "$source_path" ] && [ ! -L "$source_path" ]; then + err "Source not found: $source_path" + if [ "$target" = "engine" ]; then + echo " Build the engine first: just engine::build linux" + else + echo " Clone the repo first: just repos::clone extra" + fi + exit 1 + fi + + if [ -L "$link_path" ]; then + info "Replacing existing symlink at $link_path" + rm "$link_path" + elif [ -e "$link_path" ]; then + warn "$link_path already exists and is not a symlink. Skipping." + warn "Remove it manually if you want to replace it." + return 1 + fi + + mkdir -p "$(dirname "$link_path")" + ln -s "$source_path" "$link_path" + ok "Linked $target: $link_path -> $source_path" +}