461 lines
19 KiB
Bash
Executable file
461 lines
19 KiB
Bash
Executable file
#!/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 ""
|