#!/usr/bin/env bash # Linux dev environment setup for Magic Civilization # Tuned for Bluefin / Silverblue / Fedora-atomic (no system dnf on immutable base) # Fallbacks for traditional Fedora (dnf) and Debian/Ubuntu (apt) # Usage: ./scripts/dev-setup/linux.sh [--skip-godot] [--skip-rust] set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' DIM='\033[2m' NC='\033[0m' SKIP_GODOT=false SKIP_RUST=false for arg in "$@"; do case "$arg" in --skip-godot) SKIP_GODOT=true ;; --skip-rust) SKIP_RUST=true ;; --help|-h) echo "Usage: $0 [--skip-godot] [--skip-rust]" echo " --skip-godot Skip Flatpak Godot installation" echo " --skip-rust Skip Rust toolchain installation" exit 0 ;; esac done REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" ok() { echo -e " ${GREEN}OK${NC} $1"; } skip() { echo -e " ${DIM}SKIP${NC} $1"; } info() { echo -e " ${BLUE}...${NC} $1"; } warn() { echo -e " ${YELLOW}WARN${NC} $1"; } fail() { echo -e " ${RED}FAIL${NC} $1"; } INSTALLED=() ALREADY=() SKIPPED=() FAILED=() # ── Distro detection ───────────────────────────────────────────────── # DISTRO_FAMILY: one of {atomic, fedora, debian, other} # PKG_MGR: package manager command (or "" on atomic) # IS_ATOMIC: true/false — immutable base (Bluefin/Silverblue/Kinoite) DISTRO_FAMILY="other" PKG_MGR="" IS_ATOMIC=false if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release case "${ID:-}${ID_LIKE:-}" in *bluefin*|*silverblue*|*kinoite*|*ublue*|*coreos*) DISTRO_FAMILY="atomic"; IS_ATOMIC=true ;; esac # rpm-ostree presence is a stronger signal for atomic systems if command -v rpm-ostree &>/dev/null && [ "$IS_ATOMIC" != "true" ]; then if rpm-ostree status --json 2>/dev/null | grep -q '"booted"'; then DISTRO_FAMILY="atomic"; IS_ATOMIC=true fi fi if [ "$IS_ATOMIC" != "true" ]; then case "${ID:-}${ID_LIKE:-}" in *fedora*|*rhel*|*centos*) DISTRO_FAMILY="fedora"; PKG_MGR="dnf" ;; *debian*|*ubuntu*) DISTRO_FAMILY="debian"; PKG_MGR="apt-get" ;; esac fi fi check_or_install() { local name="$1" local check_cmd="$2" local install_cmd="$3" local skip_flag="${4:-false}" if [ "$skip_flag" = "true" ]; then skip "$name (--skip flag)" SKIPPED+=("$name") return 0 fi if eval "$check_cmd" &>/dev/null; then ok "$name (already installed)" ALREADY+=("$name") return 0 fi info "Installing $name..." if eval "$install_cmd"; then ok "$name" INSTALLED+=("$name") else fail "$name" FAILED+=("$name") return 1 fi } # Wrapper that chooses the right system install command for the current distro. # Usage: sys_install # On atomic systems, prints a warn+skip (system packages require reboot layering). sys_install() { local fedora_pkg="$1" local debian_pkg="$2" if [ "$IS_ATOMIC" = "true" ]; then warn "system package install skipped on atomic distro (would require rpm-ostree + reboot): $fedora_pkg" return 1 fi case "$DISTRO_FAMILY" in fedora) sudo dnf install -y "$fedora_pkg" ;; debian) sudo apt-get update && sudo apt-get install -y "$debian_pkg" ;; *) warn "unknown distro — cannot auto-install $fedora_pkg"; return 1 ;; esac } echo "" echo -e "${BLUE}Magic Civilization — Linux Dev Setup${NC}" echo -e "${DIM}distro: ${ID:-unknown} ${VERSION:-}${NC}" echo -e "${DIM}family: $DISTRO_FAMILY atomic: $IS_ATOMIC pkg-mgr: ${PKG_MGR:-n/a}${NC}" echo -e "${DIM}Flatpak Godot + Rust + wasm-pack + gdtoolkit + pnpm + cargo extras${NC}" echo "" # ── Flatpak (and Flathub remote) ───────────────────────────────────── echo -e "${BLUE}[1/7] Flatpak${NC}" if command -v flatpak &>/dev/null; then ok "flatpak ($(flatpak --version | awk '{print $NF}'))" ALREADY+=("flatpak") else info "Installing flatpak..." if sys_install flatpak flatpak; then ok "flatpak" INSTALLED+=("flatpak") else fail "flatpak — install manually per your distro, then re-run" FAILED+=("flatpak") fi fi if command -v flatpak &>/dev/null; then if flatpak remotes --user 2>/dev/null | grep -q flathub; then ok "flathub remote (user)" else info "Adding flathub user remote..." flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo ok "flathub remote" INSTALLED+=("flathub-remote") fi fi # ── Godot 4 (flatpak) ──────────────────────────────────────────────── echo -e "${BLUE}[2/7] Godot 4 (flatpak)${NC}" if [ "$SKIP_GODOT" = "true" ]; then skip "Godot 4 (--skip-godot flag)" SKIPPED+=("godot") elif ! command -v flatpak &>/dev/null; then skip "Godot 4 (flatpak missing)" SKIPPED+=("godot") else if flatpak info --user org.godotengine.Godot &>/dev/null \ || flatpak info org.godotengine.Godot &>/dev/null; then ok "Godot 4 (flatpak, already installed)" ALREADY+=("godot") else info "Installing Godot flatpak (user scope)..." if flatpak install --user -y flathub org.godotengine.Godot; then ok "Godot 4 (flatpak)" INSTALLED+=("godot") else fail "Godot flatpak install" FAILED+=("godot") fi fi fi # ── Godot export templates (must match editor version exactly) ─────── # Independent of --skip-godot: if flatpak Godot is already installed, we still # need matching templates to export. Only skip when Godot itself is unavailable. echo -e "${BLUE}[2c] Godot export templates${NC}" if ! command -v flatpak &>/dev/null || ! flatpak info --user org.godotengine.Godot &>/dev/null 2>&1; then skip "Godot export templates (flatpak Godot missing)" SKIPPED+=("godot-templates") else # Discover flatpak Godot version _godot_ver=$(flatpak info --user org.godotengine.Godot 2>/dev/null | awk '/Version:/{print $2}') if [ -z "$_godot_ver" ]; then warn "could not determine Godot version" SKIPPED+=("godot-templates") else _tpl_dir="$HOME/.var/app/org.godotengine.Godot/data/godot/export_templates/${_godot_ver}.stable" if [ -f "$_tpl_dir/linux_release.x86_64" ] && [ -f "$_tpl_dir/linux_debug.x86_64" ]; then ok "Godot ${_godot_ver} templates ($_tpl_dir)" ALREADY+=("godot-templates") else info "Downloading Godot ${_godot_ver} export templates..." _tpz_url="https://github.com/godotengine/godot-builds/releases/download/${_godot_ver}-stable/Godot_v${_godot_ver}-stable_export_templates.tpz" _tpz_tmp="$(mktemp -d)/templates.tpz" _parent_dir="$HOME/.var/app/org.godotengine.Godot/data/godot/export_templates" mkdir -p "$_parent_dir" if curl -fL --retry 3 -o "$_tpz_tmp" "$_tpz_url" 2>&1 | tail -1; then # .tpz is a zip; extracts to a top-level "templates/" folder. _unzip_dir="$(mktemp -d)" if unzip -q -o "$_tpz_tmp" -d "$_unzip_dir"; then if [ -d "$_unzip_dir/templates" ]; then rm -rf "$_tpl_dir" mv "$_unzip_dir/templates" "$_tpl_dir" ok "Godot ${_godot_ver} templates installed to $_tpl_dir" INSTALLED+=("godot-templates") else fail "templates.tpz layout unexpected (no templates/ folder inside)" FAILED+=("godot-templates") fi else fail "unzip failed for $_tpz_tmp" FAILED+=("godot-templates") fi rm -rf "$_unzip_dir" else fail "download failed: $_tpz_url" warn "Install manually: Godot editor → Editor → Manage Export Templates → Download" FAILED+=("godot-templates") fi rm -f "$_tpz_tmp" fi fi fi # ── System build prereqs (only on mutable distros) ─────────────────── echo -e "${BLUE}[2b] build prerequisites${NC}" if [ "$IS_ATOMIC" = "true" ]; then skip "build prereqs (atomic distro — install via toolbox/distrobox or ujust if missing)" SKIPPED+=("build-prereqs") else # pkg-config + openssl headers are needed for some cargo crates (cargo-deny, etc.) for pkg_pair in "pkg-config pkg-config" "openssl-devel libssl-dev" "gcc gcc"; do read -r fed deb <<<"$pkg_pair" bin="${fed%%-*}" if command -v "$bin" &>/dev/null || pkg-config --exists "$fed" 2>/dev/null; then ok "$fed" ALREADY+=("$fed") else info "Installing $fed..." if sys_install "$fed" "$deb"; then ok "$fed" INSTALLED+=("$fed") else warn "$fed — install manually if cargo builds fail" fi fi done fi # ── Rust toolchain ─────────────────────────────────────────────────── echo -e "${BLUE}[3/7] Rust toolchain${NC}" if [ "$SKIP_RUST" = "true" ]; then skip "Rust (--skip-rust flag)" SKIPPED+=("rust") else if command -v rustc &>/dev/null; then ok "Rust $(rustc --version | awk '{print $2}')" ALREADY+=("rust") else info "Installing Rust via rustup..." curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable # shellcheck disable=SC1090 source "$HOME/.cargo/env" ok "Rust $(rustc --version | awk '{print $2}')" INSTALLED+=("rust") fi # Ensure rustup's shims are on PATH for the rest of this script if [ -f "$HOME/.cargo/env" ]; then # shellcheck disable=SC1090 source "$HOME/.cargo/env" fi echo -e "${BLUE}[3b] wasm32-unknown-unknown target${NC}" if rustup target list --installed | grep -q wasm32-unknown-unknown; then ok "wasm32 target" ALREADY+=("wasm32-target") else info "Adding wasm32-unknown-unknown target..." rustup target add wasm32-unknown-unknown ok "wasm32 target" INSTALLED+=("wasm32-target") fi echo -e "${BLUE}[3c] x86_64-unknown-linux-gnu target${NC}" if rustup target list --installed | grep -q x86_64-unknown-linux-gnu; then ok "linux-gnu target" ALREADY+=("linux-gnu-target") else info "Adding x86_64-unknown-linux-gnu target..." rustup target add x86_64-unknown-linux-gnu ok "linux-gnu target" INSTALLED+=("linux-gnu-target") fi fi # ── wasm-pack ──────────────────────────────────────────────────────── echo -e "${BLUE}[4/7] wasm-pack${NC}" if [ "$SKIP_RUST" = "true" ]; then skip "wasm-pack (--skip-rust flag)" SKIPPED+=("wasm-pack") else check_or_install "wasm-pack" \ "command -v wasm-pack" \ "cargo install wasm-pack" fi # ── Python + gdtoolkit ─────────────────────────────────────────────── echo -e "${BLUE}[5/7] gdtoolkit (gdlint + gdformat)${NC}" # On atomic, pip --user is the only writable path; regular distros same check_or_install "gdtoolkit" \ "command -v gdlint" \ "pip3 install --user gdtoolkit || pip3 install --break-system-packages --user gdtoolkit" # Ensure ~/.local/bin is on PATH so gdlint is reachable if ! echo ":$PATH:" | grep -q ":$HOME/.local/bin:"; then warn "~/.local/bin not on PATH — gdlint may not be callable. Add: export PATH=\"\$HOME/.local/bin:\$PATH\"" fi # ── Node.js + pnpm ────────────────────────────────────────────────── echo -e "${BLUE}[6/7] Node.js${NC}" if command -v node &>/dev/null; then ok "Node $(node --version)" ALREADY+=("node") else if command -v fnm &>/dev/null; then info "Installing Node via fnm..." fnm install --lts # shellcheck disable=SC1090 eval "$(fnm env --use-on-cd)" ok "Node $(node --version)" INSTALLED+=("node") elif [ "$IS_ATOMIC" = "true" ]; then info "Installing fnm (node version manager) first..." curl -fsSL https://fnm.vercel.app/install | bash # shellcheck disable=SC1090 eval "$(fnm env --use-on-cd)" 2>/dev/null || true if command -v fnm &>/dev/null; then fnm install --lts eval "$(fnm env --use-on-cd)" ok "Node $(node --version) (via fnm)" INSTALLED+=("node") else fail "fnm install (restart shell and re-run, or install Node manually)" FAILED+=("node") fi else info "Installing Node..." if sys_install nodejs nodejs; then ok "Node $(node --version)" INSTALLED+=("node") else fail "Node install" FAILED+=("node") fi fi fi echo -e "${BLUE}[6b] pnpm${NC}" check_or_install "pnpm" \ "command -v pnpm" \ "npm install -g pnpm || (curl -fsSL https://get.pnpm.io/install.sh | sh -)" # ── cargo extras: binstall + machete + deny + nextest + llvm-cov ───── if [ "$SKIP_RUST" != "true" ]; then echo -e "${BLUE}[6c] cargo-binstall${NC}" check_or_install "cargo-binstall" \ "command -v cargo-binstall" \ "curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash" _cargo_install_tool() { local bin="$1"; local crate="${2:-$1}" if command -v cargo-binstall &>/dev/null; then check_or_install "$bin" "command -v $bin" "cargo binstall --no-confirm $crate" else check_or_install "$bin" "command -v $bin" "cargo install $crate" fi } echo -e "${BLUE}[6d] cargo-machete (dead-deps)${NC}" _cargo_install_tool cargo-machete echo -e "${BLUE}[6e] cargo-deny (security+licenses)${NC}" _cargo_install_tool cargo-deny echo -e "${BLUE}[6f] cargo-nextest (fast test runner)${NC}" _cargo_install_tool cargo-nextest echo -e "${BLUE}[6g] cargo-llvm-cov (coverage)${NC}" _cargo_install_tool cargo-llvm-cov fi # ── Project dependencies ───────────────────────────────────────────── echo -e "${BLUE}[7/7] Project dependencies${NC}" if [ -f "$REPO_ROOT/pnpm-lock.yaml" ] && command -v pnpm &>/dev/null; then info "Running pnpm install..." if (cd "$REPO_ROOT" && pnpm install 2>&1); then ok "pnpm dependencies" else warn "pnpm install had errors (private registry packages may need VPN/auth)" fi else skip "pnpm install (no pnpm-lock.yaml or pnpm missing)" fi # ── Verify ─────────────────────────────────────────────────────────── echo "" echo -e "${BLUE}─────────────────────────────────────────────────${NC}" echo -e "${BLUE} Verification${NC}" echo -e "${BLUE}─────────────────────────────────────────────────${NC}" verify_cmd() { local label="$1" local cmd="$2" if eval "$cmd" &>/dev/null; then local version version=$(eval "$3" 2>/dev/null || echo "installed") echo -e " ${GREEN}OK${NC} $label ${DIM}($version)${NC}" else echo -e " ${RED}--${NC} $label" fi } verify_cmd "flatpak" "command -v flatpak" "flatpak --version" verify_cmd "godot (flatpak)" "flatpak info --user org.godotengine.Godot || flatpak info org.godotengine.Godot" \ "echo 'org.godotengine.Godot'" verify_cmd "rustc" "command -v rustc" "rustc --version" verify_cmd "cargo" "command -v cargo" "cargo --version" verify_cmd "wasm-pack" "command -v wasm-pack" "wasm-pack --version" verify_cmd "gdlint" "command -v gdlint" "gdlint --version 2>&1 || echo installed" verify_cmd "gdformat" "command -v gdformat" "gdformat --version 2>&1 || echo installed" verify_cmd "node" "command -v node" "node --version" verify_cmd "pnpm" "command -v pnpm" "pnpm --version" verify_cmd "cargo-binstall" "command -v cargo-binstall" "cargo-binstall --version 2>&1 || echo installed" verify_cmd "cargo-machete" "command -v cargo-machete" "cargo-machete --version 2>&1 || echo installed" verify_cmd "cargo-deny" "command -v cargo-deny" "cargo-deny --version 2>&1 || echo installed" verify_cmd "cargo-nextest" "command -v cargo-nextest" "cargo-nextest --version 2>&1 || echo installed" verify_cmd "cargo-llvm-cov" "command -v cargo-llvm-cov" "cargo-llvm-cov --version 2>&1 || echo installed" # ── Summary ────────────────────────────────────────────────────────── echo "" echo -e "${BLUE}─────────────────────────────────────────────────${NC}" echo -e "${BLUE} Summary${NC}" echo -e "${BLUE}─────────────────────────────────────────────────${NC}" [ ${#INSTALLED[@]} -gt 0 ] && echo -e " ${GREEN}Installed:${NC} ${INSTALLED[*]}" [ ${#ALREADY[@]} -gt 0 ] && echo -e " ${DIM}Already had:${NC} ${ALREADY[*]}" [ ${#SKIPPED[@]} -gt 0 ] && echo -e " ${YELLOW}Skipped:${NC} ${SKIPPED[*]}" [ ${#FAILED[@]} -gt 0 ] && echo -e " ${RED}Failed:${NC} ${FAILED[*]}" if [ ${#FAILED[@]} -gt 0 ]; then echo "" echo -e " ${RED}Some tools failed to install. Fix the errors above and re-run.${NC}" exit 1 fi echo "" echo -e " ${GREEN}Ready to go.${NC} Try:" echo -e " ${DIM}./run verify${NC} — full lint + test pipeline" echo -e " ${DIM}./run test:golden${NC} — cross-language golden-vector parity" echo -e " ${DIM}./run autoplay 1${NC} — single-seed 500-turn simulation" echo "" # ── Optional: forgejo-runner install + register + persist ────────── if $WITH_RUNNER; then echo "" echo -e "${BLUE}─────────────────────────────────────────────────${NC}" echo -e "${BLUE} Forgejo runner (Linux / amd64 / $(hostname -s))${NC}" echo -e "${BLUE}─────────────────────────────────────────────────${NC}" # shellcheck source=../run/common.sh source "$REPO_ROOT/scripts/run/common.sh" # shellcheck source=./lib/runner.sh source "$REPO_ROOT/scripts/dev-setup/lib/runner.sh" export RUNNER_OS=linux export RUNNER_ARCH=amd64 export RUNNER_NAME="$(hostname -s)" export RUNNER_LABELS="self-hosted,linux,${RUNNER_NAME}" runner_install_binary runner_register UNIT="$HOME/.config/systemd/user/forgejo-runner.service" mkdir -p "$(dirname "$UNIT")" cat > "$UNIT" </dev/null || true echo " runner: systemd user unit enabled (linger=on, persists across reboot)" runner_verify_online || true fi