aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-16 01:22:24 -0500
committerCraig Jennings <c@cjennings.net>2026-05-16 01:22:24 -0500
commitb00c419780cb6b15972e3aae3e584677da2c40e3 (patch)
tree37315f345ead53d0ef941a044494fc91af74b169
parentf53de255ffb94f72339b3922520dfb0579520779 (diff)
downloadarchsetup-b00c419780cb6b15972e3aae3e584677da2c40e3.tar.gz
archsetup-b00c419780cb6b15972e3aae3e584677da2c40e3.zip
Install setup-chess prerequisites automatically
-rw-r--r--scripts/setup-chess-README.md116
-rwxr-xr-xscripts/setup-chess.sh481
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)."