magicciv/scripts/dev-setup/linux.sh

462 lines
19 KiB
Bash
Raw Normal View History

#!/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 <fedora-pkg> <debian-pkg>
# 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 ""