diff options
| -rw-r--r-- | scripts/setup-chess-README.md | 116 | ||||
| -rwxr-xr-x | scripts/setup-chess.sh | 481 |
2 files changed, 520 insertions, 77 deletions
diff --git a/scripts/setup-chess-README.md b/scripts/setup-chess-README.md new file mode 100644 index 0000000..4199c6f --- /dev/null +++ b/scripts/setup-chess-README.md @@ -0,0 +1,116 @@ +# setup-chess.sh + +## Overview + +Single-shot installer for a chess analysis stack on Arch Linux: the En Croissant GUI, Stockfish, Lc0, and the nine Maia personality engines (1100–1900 Elo). All artifacts land under `$HOME/.local/`; no root-owned files. The script is idempotent — re-runs replace broken components, preserve user-tuned settings in `engines.json`, and keep custom engine IDs stable across invocations. + +Run it once on a fresh Arch install to get a working chess setup, or run it again later (with `--force` or `--component <name>`) to repair a single component without rebuilding the rest. + +## Usage + +``` +./setup-chess.sh # full install / refresh +./setup-chess.sh --force # re-download and rebuild everything +./setup-chess.sh --component lc0 # repair just lc0 +./setup-chess.sh --component lc0 --force # force re-install of just lc0 +``` + +Valid `--component` names: `prereqs`, `en-croissant`, `lc0`, `maia`, `stockfish`, `engines-json`, `desktop`. + +## What the script does + +Each section below maps to one numbered block in `setup-chess.sh`. + +### 1. Prerequisites + +Checks for the tools and libraries used downstream and installs missing pacman packages with `pacman -S --needed --noconfirm` via `sudo` when not running as root. Always-required: `git`, `curl`, `tar`, `coreutils`, `grep`, `desktop-file-utils`, `fuse2`. The build-toolchain deps (`base-devel`, `meson`, `ninja`, `python`, `pkgconf`, `protobuf`, `zlib`, `openblas`, `eigen`) are installed when a healthy system `lc0` is unavailable, since the AUR path can still fail and fall back to a source build. + +### 2. En Croissant AppImage + +Downloads the pinned version of the En Croissant AppImage to `~/.local/bin/en-croissant.AppImage`, verifies its SHA256, and marks it executable. Skipped when the AppImage already exists, unless `--force` is set. + +### 3. Lc0 + +Tries paths in order of speed and durability, gating each on a UCI handshake smoke test (`echo uci | timeout 5 <bin> | grep '^id name Lc0'`): + +1. **Existing `~/.local/bin/lc0` passes** — keep it as-is. +2. **System `/usr/bin/lc0` passes** — symlink it into `~/.local/bin/lc0`. This is the steady state for an AUR install. +3. **AUR helper available** — `yay -S --needed --noconfirm lc0 lc0-network-sm` (or `paru` if that's what's installed), then symlink. Pacman-managed binaries get re-resolved automatically when Abseil, Protobuf, or OpenBLAS bump SONAMEs — see the BLAS conflict note below. +4. **Source build** — clone the upstream `v0.32.1` tag and build with the project's own `build.sh`. Slower (several minutes), and the resulting binary is dynamically linked, so a later library bump can break it. Used only as the last-resort fallback. + +The final binary is run through the smoke test again. If it fails, the script aborts with a pointer to running `lc0` manually to read the load-time error. + +### 4. Maia weights and wrappers + +Downloads the nine `maia-{1100..1900}.pb.gz` files from the CSSLab Maia release `v1.0` into `~/.local/share/maia/`, verifying each against a pinned SHA256. Existing files are re-validated against the pin; a mismatch triggers a re-download. + +Then generates the nine wrapper scripts at `~/.local/bin/maia-{1100..1900}`. Each is a one-line shell script that execs the lc0 binary with the matching `--weights=` flag. Wrappers are regenerated unconditionally so they always point at the current `~/.local/bin/lc0` (handy if step 3 swapped the binary out from under them). + +### 5. Stockfish + +Probes `/proc/cpuinfo` and selects the best Stockfish binary for the CPU — in order, `vnni512`, `avx512`, `avxvnni`, `avx2`, `bmi2`, `sse41-popcnt`, `x86-64`. Downloads the matching tarball from the pinned Stockfish release (`sf_18`), verifies its SHA256, extracts it into `~/.local/share/org.encroissant.app/engines/stockfish/`, and runs a UCI handshake to confirm the binary works. + +### 6. engines.json + +Writes `~/.local/share/org.encroissant.app/engines/engines.json` with Stockfish plus the nine Maia entries. Before writing, backs up the existing file to `engines.json.bak.<timestamp>`. + +The Python that builds the JSON does three things worth knowing: + +- **Engine IDs are preserved by name.** If an entry named `Stockfish` already exists, its UUID is reused; otherwise a fresh one is generated. Same for each Maia engine. This keeps any per-engine state En Croissant tracks (analysis history, custom names) stable across re-runs. +- **User-tuned settings are preserved.** The script ships conservative defaults (`Stockfish: Threads=1, Hash=16, MultiPV=1`), but if the existing entry has different settings, those are kept instead of clobbered. Re-running the script never resets a tuned engine to defaults. +- **Unknown engines are preserved.** Any entry in the existing file whose name isn't one of the ten managed ones (Stockfish + 9 Maias) is appended to the new file as-is. Hand-added engines like Komodo or a second Stockfish build survive a re-run. + +### 7. Desktop file + +Writes `~/.local/share/applications/en-croissant.desktop` so En Croissant appears in app launchers (rofi, fuzzel, Wayland app drawers, etc.) and runs `update-desktop-database` to refresh the cache. + +## Known issues + +### BLAS provider conflict + +The AUR `lc0` package depends on `openblas`, which on Arch is provided by the `blas-openblas` package. `blas-openblas` is mutually exclusive with the reference netlib BLAS bundle (`blas` + `cblas` + `lapack`). A system that has the netlib trio installed will see pacman refuse the lc0 transaction: + +``` +:: blas-openblas-0.3.33-1 and blas-3.12.1-2 are in conflict. Remove blas? [y/N] +error: unresolvable package conflicts detected +``` + +The script's `lc0` section detects this state before invoking the AUR helper (because `--noconfirm` would just abort), prints a warning with the resolution command, and falls through to the source-build path for that run. + +To resolve once and for all: + +``` +sudo pacman -S blas-openblas +``` + +Pacman prompts to remove `blas`, `cblas`, and `lapack` (answer `y`), then installs `blas-openblas`. `blas-openblas` provides the same `libblas.so` / `libcblas.so` / `liblapack.so` ABIs, so any package linked against BLAS (NumPy, SciPy, R, Octave, GIMP G'MIC, OpenCV, etc.) keeps working — and typically runs faster, since OpenBLAS is heavily optimized while the netlib reference impl is not. + +The swap is reversible (`sudo pacman -S blas cblas lapack` flips it back), and most Arch users with scientific software already run on `blas-openblas` by default. + +### En Croissant: Lc0/Maia hangs when playing engine as Black + +When you set up a "play vs engine" game in En Croissant with the engine as Black and yourself (the human) as White with **no clock**, En Croissant fills your `wtime` field with `4294967295` (UINT32_MAX) to signal "no time control." Lc0 parses `wtime` as a bounded integer and rejects anything above ~2.1 billion ms: + +``` +go wtime 4294967295 btime 30000 winc 0 binc 2000 +error out of range value 4294967295 +``` + +The engine never issues `bestmove`. The UI sits there waiting forever. This affects every Maia engine and any other Lc0-based engine, since they all share the same UCI parser. + +**Workaround:** when setting up the game, give White a finite time control. Anything reasonable (60 min + 0 inc, even 1 day) is well below Lc0's limit. The `go: {t: "Nodes", c: 1}` setting that ships in `engines.json` only governs analysis mode, not play mode — the time control comes from the game dialog. + +This is an En Croissant bug; a fix would either omit `wtime` entirely when a side has no clock, or cap the sentinel to a value Lc0 accepts (e.g. `2147483647`). + +## Files this script manages + +``` +~/.local/bin/en-croissant.AppImage En Croissant GUI +~/.local/bin/lc0 Lc0 engine (binary or symlink to /usr/bin/lc0) +~/.local/bin/maia-{1100..1900} Maia wrapper scripts (shell, exec lc0) +~/.local/share/maia/maia-{1100..1900}.pb.gz Maia neural network weights +~/.local/share/org.encroissant.app/engines/ + stockfish/stockfish-ubuntu-x86-64-* Stockfish binary + engines.json Engine registry read by En Croissant +~/.local/share/applications/en-croissant.desktop Launcher entry +``` diff --git a/scripts/setup-chess.sh b/scripts/setup-chess.sh index a19bf1d..890c58b 100755 --- a/scripts/setup-chess.sh +++ b/scripts/setup-chess.sh @@ -1,106 +1,375 @@ #!/usr/bin/env bash set -euo pipefail -# En Croissant + lc0 + Maia + Stockfish setup script -# Targets Arch Linux. No sudo required (prerequisites must be pre-installed). +# En Croissant + lc0 + Maia + Stockfish setup script for Arch Linux. +# +# Idempotent: re-runs skip healthy components, replace broken ones, and +# preserve tuned settings and engine IDs in engines.json. See +# setup-chess-README.md in this directory for usage and known issues. APPIMAGE_DIR="$HOME/.local/bin" APPIMAGE_PATH="$APPIMAGE_DIR/en-croissant.AppImage" LC0_BIN="$HOME/.local/bin/lc0" +LC0_VERSION="v0.32.1" MAIA_WEIGHTS_DIR="$HOME/.local/share/maia" MAIA_BASE_URL="https://github.com/CSSLab/maia-chess/releases/download/v1.0" -STOCKFISH_TAR_URL="https://github.com/official-stockfish/Stockfish/releases/latest/download/stockfish-ubuntu-x86-64-avx2.tar" +ENCROISSANT_VERSION="0.12.1" +ENCROISSANT_APPIMAGE="en-croissant_${ENCROISSANT_VERSION}_amd64.AppImage" +ENCROISSANT_SHA256="87bf7ec483ae396339a6ae0bf785d58a5a7d881ca16715045cee8424dbfee0ef" +ENCROISSANT_URL="https://github.com/franciscoBSalgueiro/en-croissant/releases/download/v${ENCROISSANT_VERSION}/${ENCROISSANT_APPIMAGE}" +STOCKFISH_VERSION="sf_18" +STOCKFISH_RELEASE_URL="https://github.com/official-stockfish/Stockfish/releases/download/${STOCKFISH_VERSION}" STOCKFISH_DIR="$HOME/.local/share/org.encroissant.app/engines/stockfish" -STOCKFISH_BIN="$STOCKFISH_DIR/stockfish-ubuntu-x86-64-avx2" ENGINES_JSON="$HOME/.local/share/org.encroissant.app/engines/engines.json" DESKTOP_FILE="$HOME/.local/share/applications/en-croissant.desktop" +STOCKFISH_FLAVOR="" +STOCKFISH_TAR="" +STOCKFISH_TAR_URL="" +STOCKFISH_TAR_SHA256="" +STOCKFISH_BIN="" +FORCE=0 +COMPONENT="" +LC0_BUILD_DIR="" +TMP_PATHS=() + +declare -A STOCKFISH_SHA256=( + [vnni512]="91d89e0e387faa78607fa00c0ed783d00023b4fd45181230c6b58624a5a65463" + [avx512]="9ddf29d8589d65cbf4b444c29241e711fccac24e6e9f5452480bb9a2e0fddd83" + [avxvnni]="57fb4139ea786c5cb606099cd901bf363d57e0e00c5a3dc7cd98269a6baf041a" + [bmi2]="7b200a3cd8ae6e2b07386cd213058edc91faf05ff77db68604d2f5143c56b69e" + [avx2]="536c0c2c0cf06450df0bfb5e876ef0d3119950703a8f143627f990c7b5417964" + [sse41-popcnt]="dea5016a6d9ab705e5697b093d882fca4677d84d8828f470ee33e76de33cf962" + [x86-64]="5c6f38b02a4da5f3ffe763f27da6c3e743eebefd92b50cb3661623b96696adff" +) + +declare -A MAIA_SHA256=( + [1100]="e1cf1cd0c96b8a4fa6a275f4b9fd54ed1ffebf9fe44641b9fceded310e9619c4" + [1200]="ead4ba953f233ae732999ebc1e2b675378148527ebcfad2f0acbc5e4c224d98e" + [1300]="36195f87bf4761834baa0bf87472b18509a7261a9d7d6f1a8443261369a733f2" + [1400]="d5353ea6766356dad2d28920c6692f37a5f30963767f1a3105d33b4d0af011e8" + [1500]="35ab6f20421d59e1df3b17c5a5016947af4c6761368ef84044a9a9c7619a9a00" + [1600]="d2c9e5948581acf4b9fc0b1e720c5dc0fe64ce80cfc4a239d3f8a42e1176c876" + [1700]="d277eacd792d340a30abb464dc65127254e65cac57abca17facc469889b96478" + [1800]="0031ad7c4256b1fd09fbebd28418d644d68b26cd2a45df4967ccf5c7ec9c4965" + [1900]="e2f565f42d7cd9f122557e6dc4eb84e5bbaedceda1d404dc485d3611c7c97a12" +) + +VALID_COMPONENTS=(prereqs en-croissant lc0 maia stockfish engines-json desktop) + info() { printf '\033[1;34m==> %s\033[0m\n' "$*"; } ok() { printf '\033[1;32m -> %s\033[0m\n' "$*"; } skip() { printf '\033[1;33m -> %s (skipped)\033[0m\n' "$*"; } +warn() { printf '\033[1;33mWARN: %s\033[0m\n' "$*" >&2; } err() { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; } +usage() { + cat <<EOF +Usage: ${0##*/} [--force] [--component <name>] + +Install En Croissant, lc0, Maia weights, and Stockfish for the current user. +Idempotent: existing healthy components are skipped; settings tuned by the +user are preserved across re-runs. See setup-chess-README.md for details. + +Options: + -f, --force Re-download/rebuild even if components look healthy + --component <name> Run only one section; one of: + ${VALID_COMPONENTS[*]} + -h, --help Show this help +EOF +} + +cleanup() { + if [[ -n "${LC0_BUILD_DIR:-}" && -d "$LC0_BUILD_DIR" ]]; then + rm -rf "$LC0_BUILD_DIR" + fi + + if (( ${#TMP_PATHS[@]} )); then + for path in "${TMP_PATHS[@]}"; do + [[ -e "$path" ]] && rm -rf "$path" + done + fi + + return 0 +} +trap cleanup EXIT + +download_file() { + local url=$1 + local dest=$2 + local sha256=${3:-} + local tmp + + mkdir -p "$(dirname "$dest")" + tmp=$(mktemp "$(dirname "$dest")/.download.XXXXXX") + TMP_PATHS+=("$tmp") + + curl -fL --retry 3 --retry-delay 2 -o "$tmp" "$url" + if [[ -n "$sha256" ]]; then + printf '%s %s\n' "$sha256" "$tmp" | sha256sum -c - + fi + mv "$tmp" "$dest" +} + +has_cpu_flag() { + local flag=$1 + + [[ -r /proc/cpuinfo ]] && grep -qm1 -E "(^| )${flag}( |$)" /proc/cpuinfo +} + +# UCI smoke test: send `uci` + `quit`, expect the engine's `id name <prefix>` +# line on stdout within a few seconds. Validates the binary actually loads +# and responds, not just that the file is executable. +# +# Output is buffered to a variable rather than piped into `grep -q` so the +# engine writes its full response (closing cleanly on `quit`) — `grep -q` +# closes stdin on first match and would SIGPIPE the engine, which pipefail +# would then count as a failure. +smoke_test_uci() { + local bin=$1 + local id_prefix=$2 + local out + + [[ -x "$bin" ]] || return 1 + out=$({ printf 'uci\nquit\n'; } | timeout 5 "$bin" 2>/dev/null) || return 1 + grep -qE "^id name ${id_prefix}" <<< "$out" +} + +aur_helper() { + command -v yay 2>/dev/null || command -v paru 2>/dev/null || true +} + +select_stockfish() { + if has_cpu_flag avx512_vnni; then + STOCKFISH_FLAVOR="vnni512" + elif has_cpu_flag avx512f; then + STOCKFISH_FLAVOR="avx512" + elif has_cpu_flag avx_vnni; then + STOCKFISH_FLAVOR="avxvnni" + elif has_cpu_flag avx2; then + STOCKFISH_FLAVOR="avx2" + elif has_cpu_flag bmi2; then + STOCKFISH_FLAVOR="bmi2" + elif has_cpu_flag sse4_1 && has_cpu_flag popcnt; then + STOCKFISH_FLAVOR="sse41-popcnt" + else + STOCKFISH_FLAVOR="x86-64" + fi + + if [[ "$STOCKFISH_FLAVOR" == "x86-64" ]]; then + STOCKFISH_TAR="stockfish-ubuntu-x86-64.tar" + else + STOCKFISH_TAR="stockfish-ubuntu-x86-64-${STOCKFISH_FLAVOR}.tar" + fi + + STOCKFISH_TAR_URL="${STOCKFISH_RELEASE_URL}/${STOCKFISH_TAR}" + STOCKFISH_TAR_SHA256="${STOCKFISH_SHA256[$STOCKFISH_FLAVOR]}" + STOCKFISH_BIN="$STOCKFISH_DIR/${STOCKFISH_TAR%.tar}" +} + +should_run() { + [[ -z "$COMPONENT" || "$COMPONENT" == "$1" ]] +} + +valid_component() { + local c=$1 v + for v in "${VALID_COMPONENTS[@]}"; do + [[ "$v" == "$c" ]] && return 0 + done + return 1 +} + +while (($#)); do + case "$1" in + -f|--force) + FORCE=1 + ;; + --component) + shift + if (($# == 0)); then + err "--component requires an argument" + exit 2 + fi + if ! valid_component "$1"; then + err "Unknown component: $1. Valid: ${VALID_COMPONENTS[*]}" + exit 2 + fi + COMPONENT=$1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + err "Unknown option: $1" + usage + exit 2 + ;; + esac + shift +done + +select_stockfish + # --------------------------------------------------------------------------- # 1. Check prerequisites # --------------------------------------------------------------------------- +if should_run prereqs; then info "Checking prerequisites" missing=() -for cmd in git meson ninja python3 curl; do - command -v "$cmd" &>/dev/null || missing+=("$cmd") -done -# Check libraries via pacman -for pkg in openblas eigen; do - pacman -Qi "$pkg" &>/dev/null || missing+=("$pkg") +declare -A missing_seen=() + +add_missing_package() { + local pkg=$1 + + if [[ -z "${missing_seen[$pkg]:-}" ]]; then + missing+=("$pkg") + missing_seen[$pkg]=1 + fi +} + +for pkg in git curl tar coreutils grep desktop-file-utils fuse2; do + pacman -Qi "$pkg" &>/dev/null || add_missing_package "$pkg" done +# Only require the build toolchain when a healthy system lc0 is unavailable. +# The AUR path is tried first when possible, but it can fail; these packages +# keep the source-build fallback ready. +need_source_build=1 +if smoke_test_uci /usr/bin/lc0 'Lc0'; then + need_source_build=0 +fi + +if (( need_source_build )); then + for pkg in base-devel meson ninja python pkgconf protobuf zlib openblas eigen; do + pacman -Qi "$pkg" &>/dev/null || add_missing_package "$pkg" + done +fi + if (( ${#missing[@]} )); then - err "Missing packages: ${missing[*]}" - echo "Install them with: sudo pacman -S ${missing[*]}" - exit 1 + info "Installing missing packages: ${missing[*]}" + if (( EUID == 0 )); then + pacman -S --needed --noconfirm "${missing[@]}" + else + sudo pacman -S --needed --noconfirm "${missing[@]}" + fi fi ok "All prerequisites found" +fi # --------------------------------------------------------------------------- # 2. Install En Croissant AppImage # --------------------------------------------------------------------------- +if should_run en-croissant; then info "Installing En Croissant AppImage" -if [[ -f "$APPIMAGE_PATH" ]]; then +if [[ -f "$APPIMAGE_PATH" && "$FORCE" -eq 0 ]]; then skip "Already exists at $APPIMAGE_PATH" else mkdir -p "$APPIMAGE_DIR" - # Get latest release AppImage URL from GitHub API - appimage_url=$(curl -sL "https://api.github.com/repos/franciscoBSalgueiro/en-croissant/releases/latest" \ - | python3 -c "import sys,json; assets=json.load(sys.stdin)['assets']; print([a['browser_download_url'] for a in assets if a['name'].endswith('_amd64.AppImage')][0])") - echo " Downloading: $appimage_url" - curl -L -o "$APPIMAGE_PATH" "$appimage_url" + echo " Downloading: $ENCROISSANT_URL" + download_file "$ENCROISSANT_URL" "$APPIMAGE_PATH" "$ENCROISSANT_SHA256" chmod +x "$APPIMAGE_PATH" ok "Installed to $APPIMAGE_PATH" fi +fi # --------------------------------------------------------------------------- -# 3. Build and install lc0 from source +# 3. Install lc0 +# +# Resolution order, each gated by a smoke test: +# a) Existing $LC0_BIN passes UCI handshake — keep it. +# b) System /usr/bin/lc0 passes — symlink it into $LC0_BIN. +# c) AUR helper available — install lc0 + lc0-network-sm via AUR. +# d) Source build from upstream tag. # --------------------------------------------------------------------------- -info "Building lc0" +if should_run lc0; then +info "Installing lc0" -if [[ -x "$LC0_BIN" ]]; then - skip "Already exists at $LC0_BIN" +if smoke_test_uci "$LC0_BIN" 'Lc0' && [[ "$FORCE" -eq 0 ]]; then + skip "Already healthy at $LC0_BIN" else - LC0_BUILD_DIR=$(mktemp -d /tmp/lc0-build.XXXXXX) - echo " Cloning to $LC0_BUILD_DIR" - git clone --recurse-submodules https://github.com/LeelaChessZero/lc0.git "$LC0_BUILD_DIR/lc0" - pushd "$LC0_BUILD_DIR/lc0" >/dev/null - INSTALL_PREFIX="$HOME/.local" ./build.sh - popd >/dev/null mkdir -p "$HOME/.local/bin" - cp "$LC0_BUILD_DIR/lc0/build/release/lc0" "$LC0_BIN" - chmod +x "$LC0_BIN" - ok "Installed to $LC0_BIN" - rm -rf "$LC0_BUILD_DIR" + + # Attempt AUR install when a system lc0 isn't already healthy. + if ! smoke_test_uci /usr/bin/lc0 'Lc0'; then + helper=$(aur_helper) + if [[ -n "$helper" ]]; then + # Surface the BLAS-provider conflict before pacman aborts the + # transaction non-interactively. See setup-chess-README.md. + if pacman -Qi blas &>/dev/null && ! pacman -Qi blas-openblas &>/dev/null; then + warn "lc0 requires the blas-openblas BLAS provider, which conflicts with the installed 'blas' package." + warn "Resolve manually before re-running, then re-invoke this script:" + warn " sudo pacman -S blas-openblas # prompts to remove blas/cblas/lapack" + warn "Falling through to source build path for this run." + else + info "Trying AUR install: $helper -S lc0 lc0-network-sm" + if "$helper" -S --needed --noconfirm lc0 lc0-network-sm; then + ok "Installed lc0 via $helper" + else + warn "AUR install failed; falling back to source build" + fi + fi + fi + fi + + if smoke_test_uci /usr/bin/lc0 'Lc0'; then + rm -f "$LC0_BIN" + ln -s /usr/bin/lc0 "$LC0_BIN" + ok "Linked $LC0_BIN -> /usr/bin/lc0" + else + info "Building lc0 from source (this takes several minutes)" + LC0_BUILD_DIR=$(mktemp -d /tmp/lc0-build.XXXXXX) + echo " Cloning to $LC0_BUILD_DIR" + git clone --branch "$LC0_VERSION" --recurse-submodules \ + https://github.com/LeelaChessZero/lc0.git "$LC0_BUILD_DIR/lc0" + pushd "$LC0_BUILD_DIR/lc0" >/dev/null + INSTALL_PREFIX="$HOME/.local" ./build.sh + popd >/dev/null + rm -f "$LC0_BIN" + cp "$LC0_BUILD_DIR/lc0/build/release/lc0" "$LC0_BIN" + chmod +x "$LC0_BIN" + ok "Built and installed $LC0_BIN" + cleanup + LC0_BUILD_DIR="" + fi + + if ! smoke_test_uci "$LC0_BIN" 'Lc0'; then + err "lc0 at $LC0_BIN failed its UCI handshake after install." + err "Run '$LC0_BIN' manually to see the load-time error." + exit 1 + fi +fi fi # --------------------------------------------------------------------------- -# 4. Download Maia weights +# 4. Download Maia weights (with SHA256 pinning) # --------------------------------------------------------------------------- +if should_run maia; then info "Downloading Maia weights" mkdir -p "$MAIA_WEIGHTS_DIR" for elo in 1100 1200 1300 1400 1500 1600 1700 1800 1900; do file="maia-${elo}.pb.gz" dest="$MAIA_WEIGHTS_DIR/$file" - if [[ -f "$dest" ]]; then - skip "$file already exists" - else - echo " Downloading $file" - curl -sL -o "$dest" "$MAIA_BASE_URL/$file" - ok "$file" + if [[ -f "$dest" && "$FORCE" -eq 0 ]]; then + # Validate existing file matches the pinned hash; redownload if not. + if printf '%s %s\n' "${MAIA_SHA256[$elo]}" "$dest" | sha256sum -c --status -; then + skip "$file already exists (sha256 OK)" + continue + else + warn "$file sha256 mismatch; re-downloading" + fi fi + echo " Downloading $file" + download_file "$MAIA_BASE_URL/$file" "$dest" "${MAIA_SHA256[$elo]}" + ok "$file" done -# --------------------------------------------------------------------------- -# 5. Create Maia wrapper scripts -# --------------------------------------------------------------------------- +# Maia wrapper scripts — thin shell wrappers that exec lc0 with the +# matching weights file. Regenerated unconditionally so they always point +# at the current $LC0_BIN. info "Creating Maia wrapper scripts" mkdir -p "$HOME/.local/bin" @@ -113,85 +382,139 @@ SCRIPT chmod +x "$wrapper" done ok "Created 9 wrapper scripts in ~/.local/bin/" +fi # --------------------------------------------------------------------------- -# 6. Download Stockfish binary +# 5. Install Stockfish # --------------------------------------------------------------------------- +if should_run stockfish; then info "Installing Stockfish" +echo " Selected build: $STOCKFISH_FLAVOR" -if [[ -x "$STOCKFISH_BIN" ]]; then - skip "Already exists at $STOCKFISH_BIN" +if smoke_test_uci "$STOCKFISH_BIN" 'Stockfish' && [[ "$FORCE" -eq 0 ]]; then + skip "Already healthy at $STOCKFISH_BIN" else - mkdir -p "$STOCKFISH_DIR" + STOCKFISH_TMP_TAR=$(mktemp /tmp/stockfish.XXXXXX.tar) + STOCKFISH_TMP_DIR=$(mktemp -d /tmp/stockfish.XXXXXX) + TMP_PATHS+=("$STOCKFISH_TMP_TAR" "$STOCKFISH_TMP_DIR") echo " Downloading Stockfish" - curl -sL "$STOCKFISH_TAR_URL" | tar -xf - -C "$STOCKFISH_DIR" --strip-components=1 + download_file "$STOCKFISH_TAR_URL" "$STOCKFISH_TMP_TAR" "$STOCKFISH_TAR_SHA256" + tar -xf "$STOCKFISH_TMP_TAR" -C "$STOCKFISH_TMP_DIR" --strip-components=1 + if [[ ! -f "$STOCKFISH_TMP_DIR/$(basename "$STOCKFISH_BIN")" ]]; then + err "Stockfish archive did not contain $(basename "$STOCKFISH_BIN")" + exit 1 + fi + mkdir -p "$STOCKFISH_DIR" + cp -a "$STOCKFISH_TMP_DIR"/. "$STOCKFISH_DIR"/ chmod +x "$STOCKFISH_BIN" + + if ! smoke_test_uci "$STOCKFISH_BIN" 'Stockfish'; then + err "Stockfish at $STOCKFISH_BIN failed its UCI handshake after install." + exit 1 + fi ok "Installed to $STOCKFISH_BIN" fi +fi # --------------------------------------------------------------------------- -# 7. Write engines.json +# 6. Write engines.json +# +# Preserves existing engine IDs and user-tuned settings on re-runs. Unknown +# entries in the existing file are kept too so manually-added engines (e.g. +# Komodo) survive a re-run. # --------------------------------------------------------------------------- +if should_run engines-json; then info "Writing engines.json" mkdir -p "$(dirname "$ENGINES_JSON")" +if [[ -f "$ENGINES_JSON" ]]; then + backup="$ENGINES_JSON.bak.$(date +%Y%m%d%H%M%S)" + cp -p "$ENGINES_JSON" "$backup" + ok "Backed up existing engines.json to $backup" +fi + +python3 - "$STOCKFISH_BIN" "$STOCKFISH_VERSION" "$STOCKFISH_TAR_URL" \ + "$HOME/.local/bin" "$ENGINES_JSON" <<'PYEOF' > "$ENGINES_JSON.new" +import json, os, sys, uuid -# Generate a UUID via python3 -genuuid() { python3 -c "import uuid; print(uuid.uuid4())"; } +stockfish_bin, stockfish_version, stockfish_download_link, \ + maia_bin_dir, engines_json_path = sys.argv[1:6] -# Build the JSON with python3 for correctness -python3 - "$STOCKFISH_BIN" "$HOME/.local/bin" <<'PYEOF' > "$ENGINES_JSON" -import json, sys, uuid +existing = {} +existing_order = [] +if os.path.exists(engines_json_path): + try: + with open(engines_json_path) as f: + for e in json.load(f): + name = e.get("name") + if name: + existing[name] = e + existing_order.append(name) + except (json.JSONDecodeError, OSError): + pass -stockfish_bin = sys.argv[1] -maia_bin_dir = sys.argv[2] +def merge(name, defaults): + prev = existing.get(name, {}) + out = dict(defaults) + out["name"] = name + out["id"] = prev.get("id", str(uuid.uuid4())) + # Preserve user-tuned settings if a prior entry exists; otherwise use + # the defaults this script ships. + if "settings" in prev: + out["settings"] = prev["settings"] + return out -engines = [] +managed = [] +managed_names = set() -# Stockfish -engines.append({ +stockfish_defaults = { "type": "local", - "id": str(uuid.uuid4()), - "name": "Stockfish", - "version": "", + "version": stockfish_version, "path": stockfish_bin, "image": "https://upload.wikimedia.org/wikipedia/commons/3/3a/NewLogoSF.png", "elo": 3635, "downloadSize": 79953920, - "downloadLink": "https://github.com/official-stockfish/Stockfish/releases/latest/download/stockfish-ubuntu-x86-64-avx2.tar", + "downloadLink": stockfish_download_link, "loaded": True, "go": {"t": "Infinite"}, "settings": [ {"name": "Threads", "value": 1}, {"name": "Hash", "value": 16}, - {"name": "MultiPV", "value": 1} - ] -}) + {"name": "MultiPV", "value": 1}, + ], +} +managed.append(merge("Stockfish", stockfish_defaults)) +managed_names.add("Stockfish") -# Maia engines for elo in range(1100, 2000, 100): - engines.append({ + name = f"Maia {elo}" + maia_defaults = { "type": "local", - "id": str(uuid.uuid4()), - "name": f"Maia {elo}", "version": "1.0", "path": f"{maia_bin_dir}/maia-{elo}", "image": None, "elo": elo, "loaded": True, "go": {"t": "Nodes", "c": 1}, - "settings": [ - {"name": "MultiPV", "value": 1} - ] - }) + "settings": [{"name": "MultiPV", "value": 1}], + } + managed.append(merge(name, maia_defaults)) + managed_names.add(name) -print(json.dumps(engines, indent=4)) +# Append any non-managed engines from the existing file (manually-added +# user engines) so a re-run never silently drops them. +extras = [existing[n] for n in existing_order if n not in managed_names] + +print(json.dumps(managed + extras, indent=4)) PYEOF -ok "Wrote $ENGINES_JSON with 10 engines" +mv "$ENGINES_JSON.new" "$ENGINES_JSON" +ok "Wrote $ENGINES_JSON" +fi # --------------------------------------------------------------------------- -# 8. Create .desktop file +# 7. Create .desktop file # --------------------------------------------------------------------------- +if should_run desktop; then info "Creating .desktop file" mkdir -p "$(dirname "$DESKTOP_FILE")" @@ -208,6 +531,7 @@ EOF update-desktop-database "$HOME/.local/share/applications/" 2>/dev/null || true ok "Created $DESKTOP_FILE" +fi # --------------------------------------------------------------------------- # Done @@ -221,3 +545,6 @@ echo " Maia weights: $MAIA_WEIGHTS_DIR/" echo " engines.json: $ENGINES_JSON" echo "" echo " Launch with: $APPIMAGE_PATH" +echo "" +echo " See setup-chess-README.md for known issues (BLAS provider conflict," +echo " Maia-as-Black wtime quirk in En Croissant)." |
