#!/usr/bin/env bash
# =============================================================================
# build_wordcraft.sh - builds WordCraft (Rust, egui) on OpenIndiana (illumos)
# =============================================================================
# HOWTO
#   1. Log in as root. Internet access is required (pkg.openindiana.org, github.com,
#      static.rust-lang.org, crates.io).
#   2. Start it (with bash!):
#        bash /root/build_wordcraft.sh              everything: checks, packages, Rust, sources, build
#        bash /root/build_wordcraft.sh --check      only check resources and system
#        bash /root/build_wordcraft.sh --deps       only install missing packages and Rust
#        bash /root/build_wordcraft.sh --cli        only build wordcraft-cli (no GUI, no GUI packages)
#        bash /root/build_wordcraft.sh --report     show the error report of the last run
#      Options: -j N  build jobs (default: derived from CPU and RAM)
#               --ref TAG   branch or tag of the sources   --repo URL   other repository
#               --force     resource check only warns (abort only for e.g. a missing linker)
#               --no-update keep the existing source checkout (no git pull; avoids a rebuild after upstream changes)
#               --fast      build without LTO (about 3x faster, slightly slower program); for test rounds
#               --bundle    only (re)create the ready-to-copy folder from the existing binaries (see 4.)
#   3. Duration: packages 5-15 min, Rust 5 min, build 20-60 min (depends on CPU and RAM).
#   4. Result:    /root/wordcraft-build/wordcraft/target/release/wordcraft-cli
#                 /root/wordcraft-build/wordcraft/target/release/wordcraft      (GUI)
#      Ready to copy: /root/wordcraft-build/wordcraft/target/bundle/wordcraft-<version>-illumos-amd64/ (and .tar.gz/.zip)
#                 a self-contained, runnable folder: copy it to another OpenIndiana machine and start ./wordcraft;
#                 "sh install.sh setup" (in that folder) repairs the permissions, adds the desktop starter and, as root,
#                 the application menu "Artcraft"; double click on the starter then starts WordCraft
#      Logs:      /root/wordcraft-build/logs/  (one file per step and attempt)
#                 summary.log  progress with timestamps      errors.txt  only the important error lines
#   5. On errors the script recognizes known causes (missing library or header, rustc too
#      old, too little RAM, missing linker/C compiler), fixes them and rebuilds up to
#      3 times. If an error remains: send summary.log and errors.txt (bash ... --report).
#   6. Start the GUI (in the MATE session at the screen, not over ssh without X):
#        /root/wordcraft-build/wordcraft/target/release/wordcraft --sample
#      Without a GPU driver Mesa uses software rendering (slower, but works).
#   7. Install if wanted:  cp target/release/wordcraft target/release/wordcraft-cli /usr/local/bin/
# =============================================================================

# --- 1. Must run with bash (these lines are deliberately POSIX compatible) -----
if [ -z "${BASH_VERSION:-}" ]; then
  echo "ERROR: this script needs bash. Start it with:  bash $0" >&2
  exit 1
fi
if [ "${BASH_VERSINFO[0]:-0}" -lt 3 ]; then
  echo "ERROR: bash $BASH_VERSION is too old (version 3 or newer is required)" >&2
  exit 1
fi

# --- Settings ----------------------------------------------------------------
BASE=/root/wordcraft-build
LOGDIR=$BASE/logs
SRC=$BASE/wordcraft
REPO=https://github.com/storytold/wordcraft
REF=""
MODE=all
JOBS=""
FORCE=0
NOUPDATE=0
FAST=0
MIN_RAM_MB=3000      # RAM + swap below this: abort (rustc needs 1-2 GB per job)
WARN_RAM_MB=8000     # below this: only 1-2 jobs
MIN_DISK_MB=12000    # free space for sources, dependencies and target/
RUST_MIN=1.85        # egui / edition 2024 need a recent rustc
MAXTRY=6             # porting to illumos needs several rounds of "fix and rebuild"
MISSING=""
FIXED=" "
GUI=1

mkdir -p "$LOGDIR" || { echo "ERROR: cannot create $LOGDIR" >&2; exit 1; }

ts()   { date '+%Y-%m-%d %H:%M:%S'; }
say()  { echo "[$(date +%H:%M:%S)] $*"; echo "[$(ts)] $*" >> "$LOGDIR/summary.log"; }
warn() { say "WARNING: $*"; }
die()  { say "ERROR: $*"; exit 1; }

usage() { sed -n '/^# HOWTO/,/^# =====/p' "$0" | sed 's/^# \{0,1\}//'; }

while [ $# -gt 0 ]; do
  case "$1" in
    -h|--help|help) usage; exit 0 ;;
    --check)  MODE=check ;;
    --deps)   MODE=deps ;;
    --cli)    MODE=cli; GUI=0 ;;
    --report) MODE=report ;;
    --force)  FORCE=1 ;;
    --no-update) NOUPDATE=1 ;;
    --fast)   FAST=1 ;;
    --bundle) MODE=bundle ;;
    -j)       shift; JOBS=${1:-} ;;
    --ref)    shift; REF=${1:-} ;;
    --repo)   shift; REPO=${1:-} ;;
    *) echo "Unknown option: $1" >&2; usage; exit 2 ;;
  esac
  shift
done

# --- Report ------------------------------------------------------------------
if [ "$MODE" = report ]; then
  echo "=== $LOGDIR/summary.log (last 40 lines) ==="
  tail -40 "$LOGDIR/summary.log" 2>/dev/null
  echo; echo "=== $LOGDIR/errors.txt (last 80 lines) ==="
  tail -80 "$LOGDIR/errors.txt" 2>/dev/null
  exit 0
fi

# --- Helpers -----------------------------------------------------------------
# run_logged LOGFILE COMMAND... : output goes only to the log, a sign of life every 30 s
run_logged() {
  local logf=$1; shift
  echo "### $(ts) START: $*" > "$logf"
  "$@" >> "$logf" 2>&1 &
  local pid=$! t0 n=0
  t0=$(date +%s)
  while kill -0 "$pid" 2>/dev/null; do
    sleep 2; n=$((n + 1))
    if [ $((n % 15)) -eq 0 ] && kill -0 "$pid" 2>/dev/null; then
      echo "    ... running for $(( $(date +%s) - t0 )) s: $(tail -1 "$logf" | cut -c1-80)"
    fi
  done
  wait "$pid"; local rc=$?
  echo "### $(ts) END rc=$rc" >> "$logf"
  return $rc
}

# collect_errors LOGFILE : append the important lines to errors.txt
collect_errors() {
  {
    echo "=== $(ts) $1 ==="
    grep -nE -B1 -A6 '^error|^error\[|could not compile|linking with|ld: fatal|fatal error:|was not found|not found|requires rustc|No space left|SIGKILL|Cannot allocate|panicked' "$1" | head -150
    echo
  } >> "$LOGDIR/errors.txt"
}

version_ge() { # version_ge ACTUAL WANTED
  [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -1)" = "$2" ]
}

fetch() { # fetch URL TARGETFILE
  if command -v curl >/dev/null 2>&1; then curl -fL --retry 3 -o "$2" "$1"
  elif command -v wget >/dev/null 2>&1; then wget -q -O "$2" "$1"
  else return 127; fi
}

# --- 2. Check resources and system ---------------------------------------------
check_system() {
  say "=== Checks ($(uname -sr), bash $BASH_VERSION) ==="
  [ "$(uname -s)" = SunOS ] || die "not illumos/OpenIndiana (uname: $(uname -s))"
  [ "$(id -u)" = 0 ] || die "please run as root (pkg install needs root)"

  CPUS=$(psrinfo 2>/dev/null | wc -l | tr -d ' ')
  [ -n "$CPUS" ] && [ "$CPUS" -ge 1 ] 2>/dev/null || CPUS=1
  RAM_MB=$(prtconf 2>/dev/null | awk '/^Memory size:/ {print $3; exit}')
  [ -n "$RAM_MB" ] || RAM_MB=0
  SWAP_MB=$(swap -l 2>/dev/null | awk 'NR>1 {s += $4} END {print int(s / 2048)}')   # blocks of 512 bytes -> MB
  [ -n "$SWAP_MB" ] || SWAP_MB=0
  mkdir -p "$BASE"
  DISK_MB=$(df -k "$BASE" 2>/dev/null | awk 'NR==2 {print int($4 / 1024)}')
  [ -n "$DISK_MB" ] || DISK_MB=0
  say "CPU cores: $CPUS | RAM: ${RAM_MB} MB | swap: ${SWAP_MB} MB | free in $BASE: ${DISK_MB} MB"

  local bad=0
  if [ $((RAM_MB + SWAP_MB)) -lt $MIN_RAM_MB ]; then
    say "NOT ENOUGH MEMORY: RAM+swap $((RAM_MB + SWAP_MB)) MB, at least $MIN_RAM_MB MB needed."
    say "  Fix: increase the VM RAM or add swap:  zfs create -V 4G rpool/swap2 && swap -a /dev/zvol/dsk/rpool/swap2"
    bad=1
  fi
  if [ "$DISK_MB" -lt $MIN_DISK_MB ]; then
    say "NOT ENOUGH DISK SPACE: ${DISK_MB} MB free, at least $MIN_DISK_MB MB needed (the Rust target directory gets large)."
    bad=1
  fi
  if [ $bad -eq 1 ]; then
    if [ $FORCE -eq 1 ]; then warn "continuing because of --force"; else die "not enough resources (use --force to try anyway)"; fi
  fi
  [ "$RAM_MB" -lt $WARN_RAM_MB ] && warn "less than $WARN_RAM_MB MB RAM: the build works, but with few jobs and slowly"

  if [ -z "$JOBS" ]; then # rustc: about 2 GB per job
    JOBS=$(( (RAM_MB + SWAP_MB / 2) / 2000 ))
    [ "$JOBS" -gt "$CPUS" ] && JOBS=$CPUS
    [ "$JOBS" -lt 1 ] && JOBS=1
  fi
  say "Build jobs: $JOBS"

  if command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1; then
    for u in https://pkg.openindiana.org/hipster/ https://github.com https://static.rust-lang.org https://index.crates.io/config.json; do
      if fetch "$u" /dev/null >/dev/null 2>&1; then say "Network ok: $u"; else warn "not reachable: $u"; fi
    done
  else
    warn "neither curl nor wget is installed (curl is installed with the packages)"
  fi
}

# --- 3. Packages -------------------------------------------------------------
pkg_resolve() { # glob -> shortest matching package name in the repository
  pkg list -a -H "$1" 2>/dev/null | awk '{print $1}' | sort -u | awk '{print length($0), $0}' | sort -n | head -1 | cut -d' ' -f2-
}

ensure_pkg() { # ensure_pkg LABEL GLOB
  local label=$1 glob=$2 name rc
  [ -n "$glob" ] || { say "no search pattern for '$label'"; MISSING="$MISSING $label"; return 1; }
  name=$(pkg list -H "$glob" 2>/dev/null | awk '{print $1}' | head -1)   # fast local check first (the repository query is slow)
  if [ -n "$name" ]; then say "already installed: $name"; return 0; fi
  name=$(pkg_resolve "$glob")
  if [ -z "$name" ]; then say "no package found for '$label' ($glob)"; MISSING="$MISSING $label"; return 1; fi
  if pkg list -H "$name" >/dev/null 2>&1; then say "already installed: $name"; return 0; fi
  say "installing: $name"
  pkg install --accept "$name" >> "$LOGDIR/pkg.log" 2>&1; rc=$?
  if [ $rc -eq 0 ] || [ $rc -eq 4 ]; then say "ok: $name"; return 0; fi   # pkg exit code 4 = nothing to do
  say "ERROR: pkg install $name failed (rc=$rc), see $LOGDIR/pkg.log"; MISSING="$MISSING $label"; return 1
}

ensure_gcc() {
  setup_env   # also finds an existing gcc under /usr/gcc/N/bin (not in PATH by default)
  command -v gcc >/dev/null 2>&1 && return 0
  local g list
  # Candidates, modern versions only (10-30): developer/gcc-49 and friends mean gcc 4.9, which is far too old.
  # First the versions whose runtime is already installed (they match this OS build), then all others, newest first.
  # The newest repository version may need a newer OS build than the installed one (pkg then fails), so try the next.
  list=$( { pkg list -H 'system/library/gcc-*-runtime' 2>/dev/null | awk '{print $1}' | sed 's/.*gcc-\([0-9]*\)-runtime.*/developer\/gcc-\1/' | sort -t- -k2 -rn
            pkg list -a -H 'developer/gcc-*' 2>/dev/null | awk '{print $1}' | grep -E '^developer/gcc-[0-9]+$' | sort -t- -k2 -rn
          } | awk -F- '$NF >= 10 && $NF <= 30' | awk '!seen[$0]++')
  [ -n "$list" ] || { MISSING="$MISSING gcc"; say "no gcc package found"; return 1; }
  for g in $list; do
    if ensure_pkg gcc "$g"; then
      setup_env
      command -v gcc >/dev/null 2>&1 && return 0
    fi
  done
  MISSING="$MISSING gcc"
  say "no usable gcc could be installed (see pkg.log). Maybe the OS is older than the repository: update it with 'pkg update' (new boot environment, reboot) and start again"
  return 1
}

setup_env() {
  local d
  export PATH="$HOME/.cargo/bin:$PATH"
  d=$(ls -d /usr/gcc/*/bin 2>/dev/null | sort -t/ -k4 -n | tail -1)   # newest gcc, not in PATH by default
  [ -n "$d" ] && case ":$PATH:" in *":$d:"*) ;; *) export PATH="$PATH:$d" ;; esac
  local pc=""
  for d in /usr/lib/amd64/pkgconfig /usr/lib/64/pkgconfig /usr/lib/pkgconfig /usr/X11/lib/amd64/pkgconfig /usr/X11/lib/pkgconfig /usr/share/pkgconfig; do
    [ -d "$d" ] && pc="$pc:$d"
  done
  export PKG_CONFIG_PATH="${pc#:}${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
  if command -v gcc >/dev/null 2>&1; then
    export CC=gcc CXX=g++ CARGO_TARGET_X86_64_UNKNOWN_ILLUMOS_LINKER=gcc   # rustc looks for "cc" by default
  fi
  if [ -z "${RUSTFLAGS:-}" ]; then   # 64-bit library directories for the linker
    local rf=""
    for d in /usr/lib/amd64 /lib/amd64 /usr/X11/lib/amd64; do [ -d "$d" ] && rf="$rf -C link-arg=-L$d"; done
    [ -n "$rf" ] && export RUSTFLAGS="${rf# }"
  fi
  if [ ! -e /usr/lib/amd64/libgcc_s.so.1 ]; then   # Rust tools need libgcc_s at run time: use the newest gcc runtime as fallback
    d=$(ls -d /usr/gcc/*/lib/amd64 2>/dev/null | sort -t/ -k4 -n | tail -1)
    [ -n "$d" ] && export LD_LIBRARY_PATH_64="$d${LD_LIBRARY_PATH_64:+:$LD_LIBRARY_PATH_64}"
  fi
  export CARGO_BUILD_JOBS=${JOBS:-1} CARGO_TERM_COLOR=never RUST_BACKTRACE=1 CARGO_NET_RETRY=5
  [ $FAST -eq 1 ] && export CARGO_PROFILE_RELEASE_LTO=off   # test rounds: skip the slow link-time optimization
}

install_packages() {
  say "=== Packages ==="
  pkg refresh >> "$LOGDIR/pkg.log" 2>&1 || warn "pkg refresh failed (network? see pkg.log)"
  # Tools, always needed
  ensure_pkg git        '*/git'
  ensure_pkg cmake      '*/cmake'
  ensure_pkg gnu-make   '*/gnu-make'
  ensure_pkg pkg-config '*/pkg-config'
  ensure_pkg binutils   '*/gnu-binutils'
  ensure_pkg curl       '*/curl'
  ensure_pkg ca-certs   '*/ca-bundle'
  ensure_gcc
  ensure_pkg gcc-runtime '*/gcc-runtime'
  # GUI (egui/winit/wgpu): X11, keyboard, fonts, OpenGL
  if [ $GUI -eq 1 ]; then
    for l in libx11 libxcb libxkbcommon libxcursor libxi libxrandr libxrender libxext libxfixes; do ensure_pkg "$l" "*/$l"; done
    ensure_pkg x11-protocols '*/x11-protocols'
    ensure_pkg mesa          '*/mesa'
    ensure_pkg fontconfig    '*/fontconfig'
    ensure_pkg freetype      '*/freetype*'
  fi
  [ -n "$MISSING" ] && warn "not installed:$MISSING (the build will report it if it is really needed)"
  setup_env
}

# --- 4. Rust -----------------------------------------------------------------
rust_ok() {
  command -v rustc >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 || return 1
  local v
  v=$(rustc --version 2>/dev/null | awk '{print $2}'); v=${v%%-*}
  [ -n "$v" ] && version_ge "$v" "$RUST_MIN"
}

install_rust() {
  say "=== Rust ==="
  setup_env
  if rust_ok; then say "Rust ok: $(rustc --version)"; return 0; fi
  command -v rustc >/dev/null 2>&1 && say "existing $(rustc --version) is older than $RUST_MIN"
  if ! command -v rustup >/dev/null 2>&1; then
    say "downloading rustup-init for illumos ..."
    fetch https://static.rust-lang.org/rustup/dist/x86_64-unknown-illumos/rustup-init "$BASE/rustup-init" >> "$LOGDIR/rust.log" 2>&1 \
      || die "cannot download rustup-init (network? see $LOGDIR/rust.log). Alternative: a Rust package from the repository: pkg search rust"
    chmod +x "$BASE/rustup-init"
    run_logged "$LOGDIR/rustup-install.log" "$BASE/rustup-init" -y --profile minimal --default-toolchain stable --no-modify-path \
      || { collect_errors "$LOGDIR/rustup-install.log"; die "rustup installation failed, see $LOGDIR/rustup-install.log"; }
  else
    run_logged "$LOGDIR/rustup-update.log" rustup update stable || warn "rustup update failed"
  fi
  setup_env
  rust_ok || die "Rust is not usable or too old after the installation: $(rustc --version 2>&1)"
  say "Rust ok: $(rustc --version) / $(cargo --version)"
}

# --- 5. Sources --------------------------------------------------------------
fetch_source() {
  say "=== Sources ==="
  command -v git >/dev/null 2>&1 || die "git is missing"
  if [ -d "$SRC/.git" ] && [ $NOUPDATE -eq 1 ]; then
    say "using the existing checkout (--no-update)"
  elif [ -d "$SRC/.git" ]; then
    run_logged "$LOGDIR/git.log" git -C "$SRC" pull --ff-only || warn "git pull failed (the existing checkout is used)"
  else
    run_logged "$LOGDIR/git.log" git clone --depth 1 ${REF:+--branch "$REF"} "$REPO" "$SRC" \
      || { collect_errors "$LOGDIR/git.log"; die "git clone failed, see $LOGDIR/git.log"; }
  fi
  say "Sources: $(git -C "$SRC" log -1 --format='%h %s (%ci)' 2>/dev/null)"
}

# --- 6. Recognize and fix errors ---------------------------------------------
fixed_before() { case "$FIXED" in *" $1 "*) return 0 ;; esac; return 1; }   # every fix is tried only once (no endless loop)
mark_fixed()   { FIXED="$FIXED$1 "; }

# patch_no_wayland : OpenIndiana has no libwayland (no wayland-client.pc), but several GUI crates (winit, wl-clipboard-rs)
# link against it at build time. The winit feature "wayland-dlopen" loads the Wayland libraries at run time instead
# (wayland-sys/dlopen: no pkg-config, no linking), so the build no longer needs them and the X11 backend is used here.
# Edits only apps/wordcraft/Cargo.toml of the local checkout; the original is kept as Cargo.toml.orig-wayland.
# Returns 0 if something was changed.
patch_no_wayland() {
  local f=apps/wordcraft/Cargo.toml
  cd "$SRC" || return 1
  say "Cause: no Wayland libraries on this system -> enabling winit feature 'wayland-dlopen' (Wayland is loaded at run time, original saved as $f.orig-wayland)"
  [ -f "$f" ] || return 1
  if [ -f "$f.orig-wayland" ]; then cp -p "$f.orig-wayland" "$f"; else cp -p "$f" "$f.orig-wayland"; fi   # always start from the original
  sed '/^winit = /s/"x11"/"x11", "wayland-dlopen"/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
  cmp -s "$f" "$f.orig-wayland" && { warn "no 'winit = ... \"x11\"' line found in $f, nothing patched"; return 1; }
  say "patched: $f ($(grep '^winit = ' "$f" | cut -c1-110))"
  return 0
}

# patch_wayland_backend : the crate wayland-backend (pure Rust Wayland implementation, pulled in by winit and the
# clipboard crate) does not know illumos: its epoll code is limited to linux/android/redox and its socket code uses
# MSG_CMSG_CLOEXEC, which illumos does not have. illumos does have epoll (rustix supports it), so a local copy of the
# crate gets "illumos" added to those cfg lists (the same handling as redox/macOS: CLOEXEC is set afterwards) and is
# used through [patch.crates-io] in the workspace Cargo.toml. The original Cargo.toml is kept as Cargo.toml.orig-patch.
# Returns 0 if something was changed.
patch_wayland_backend() {
  local f
  crate_copy wayland-backend || return 1
  say "Cause: wayland-backend $CRATE_VER has no illumos support -> using a patched local copy ($CRATE_DIR)"
  for f in src/rs/server_impl/common_poll.rs src/rs/server_impl/handle.rs src/rs/socket.rs; do
    sed 's/target_os = "redox"/target_os = "redox", target_os = "illumos"/' "$CRATE_DIR/$f" > "$CRATE_DIR/$f.tmp" && mv "$CRATE_DIR/$f.tmp" "$CRATE_DIR/$f"
  done
  grep -q 'target_os = "illumos"' "$CRATE_DIR/src/rs/socket.rs" || { warn "patch of $CRATE_DIR did not apply"; return 1; }
  register_patches
}

# patch_winit : winit has no is_main_thread() for illumos (only linux, the BSDs). The NetBSD variant (the Rust main
# thread is named "main") works everywhere, so illumos/solaris are added to its cfg.
patch_winit() {
  local f
  crate_copy winit || return 1
  say "Cause: winit $CRATE_VER lacks is_main_thread() for illumos -> using a patched local copy ($CRATE_DIR)"
  f=$CRATE_DIR/src/platform_impl/linux/mod.rs
  sed '/^#\[cfg(target_os = "netbsd")\]$/{
N
/fn is_main_thread/s/target_os = "netbsd"/any(target_os = "netbsd", target_os = "illumos", target_os = "solaris")/
}' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
  grep -q 'target_os = "illumos"' "$f" || { warn "patch of $f did not apply"; return 1; }
  register_patches
}

# patch_rfd : rfd (native file dialogs) only has backends for linux and the BSDs (xdg-portal over D-Bus, optional GTK3).
# illumos is a unix system with D-Bus like those, so it gets added to the same cfg lists (anchor: "openbsd").
patch_rfd() {
  local f
  crate_copy rfd || return 1
  say "Cause: rfd $CRATE_VER has no file dialog backend for illumos -> using a patched local copy ($CRATE_DIR)"
  # Cargo.toml too: the dependencies of the portal backend (libc, wayland-*, pollster) are listed per target OS
  for f in src/backend.rs src/file_dialog.rs Cargo.toml; do
    [ -f "$CRATE_DIR/$f" ] || continue
    sed 's/target_os = "openbsd"/target_os = "openbsd", target_os = "illumos"/' "$CRATE_DIR/$f" > "$CRATE_DIR/$f.tmp" && mv "$CRATE_DIR/$f.tmp" "$CRATE_DIR/$f"
  done
  grep -q 'target_os = "illumos"' "$CRATE_DIR/src/backend.rs" || { warn "patch of $CRATE_DIR did not apply"; return 1; }
  register_patches
}

# patch_wgpu_gles : this one is not a compile error but a start-up panic ("No wgpu backend feature that is implemented
# for the target platform was enabled"): wgpu and wgpu-core switch their OpenGL (GLES/EGL) backend on only for
# windows/linux/android/freebsd in their build.rs. The backend code itself (wgpu-hal) has no such restriction, so illumos
# is added to those two aliases (vulkan stays off: no Vulkan driver here). Done before every GUI build, but only once
# per version: unchanged copies keep their timestamps, so cargo does not rebuild everything each time.
patch_wgpu_gles() {
  local v1 v2 f
  cd "$SRC" || return 1
  v1=$(awk '/^name = "wgpu"$/ {getline; gsub(/[^0-9.]/, "", $0); print; exit}' Cargo.lock)
  v2=$(awk '/^name = "wgpu-core"$/ {getline; gsub(/[^0-9.]/, "", $0); print; exit}' Cargo.lock)
  [ -n "$v1" ] && [ -n "$v2" ] || return 0   # no wgpu in this version of the sources
  # helper crate of wgpu-core: it adds wgpu-hal (and with it the gles feature) only for windows/linux/android/freebsd/netbsd
  # -> without illumos here wgpu-hal has no gles backend ("cannot find value Gles in module hal::api")
  if [ ! -f "$BASE/patches/wgpu-core-deps-windows-linux-android/.illumos-hal-r1" ]; then
    crate_copy wgpu-core-deps-windows-linux-android || return 1
    f=$CRATE_DIR/Cargo.toml
    sed 's/target_os = "netbsd"))/target_os = "netbsd", target_os = "illumos"))/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
    grep -q 'target_os = "illumos"' "$f" || { warn "patch of $f did not apply"; return 1; }
    echo 1 > "$CRATE_DIR/.illumos-hal-r1"
    register_patches
  fi
  # wgpu-core: IndirectValidation::new() creates compute pipelines at device creation; with software OpenGL (Mesa softpipe)
  # that fails and returns DeviceError::Lost ("Parent device is lost"). The validation is optional: switch it off instead.
  if [ -d "$BASE/patches/wgpu-core/src" ] && [ ! -f "$BASE/patches/wgpu-core/.illumos-noindirect-r1" ]; then
    f=$BASE/patches/wgpu-core/src/device/resource.rs
    perl -0pi -e 's/Some\(crate::indirect_validation::IndirectValidation::new\((.*?)\)\?\)/match crate::indirect_validation::IndirectValidation::new($1) {\n                Ok(iv) => Some(iv),\n                Err(_) => {\n                    log::warn!("indirect validation disabled (not supported by this driver)");\n                    None\n                }\n            }/s' "$f"
    grep -q 'indirect validation disabled' "$f" || { warn "patch of $f did not apply"; return 1; }
    echo 1 > "$BASE/patches/wgpu-core/.illumos-noindirect-r1"
  fi
  if [ "$(cat "$BASE/patches/wgpu/.illumos-gles-r2" 2>/dev/null)" = "$v1" ] && [ "$(cat "$BASE/patches/wgpu-core/.illumos-gles-r2" 2>/dev/null)" = "$v2" ]; then
    return 0
  fi
  say "wgpu $v1 / wgpu-core $v2 enable the OpenGL backend only on windows/linux/android/freebsd -> patched local copies add illumos"
  crate_copy wgpu || return 1
  f=$CRATE_DIR/build.rs
  sed 's/target_os = "freebsd", Emscripten), feature = "gles"/target_os = "freebsd", target_os = "illumos", Emscripten), feature = "gles"/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
  grep -q 'target_os = "illumos"' "$f" || { warn "patch of $f did not apply"; return 1; }
  echo "$v1" > "$CRATE_DIR/.illumos-gles-r2"
  crate_copy wgpu-core || return 1
  f=$CRATE_DIR/build.rs
  sed 's/all(windows_linux_android, feature = "gles")/all(any(windows_linux_android, target_os = "illumos"), feature = "gles")/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
  grep -q 'target_os = "illumos"' "$f" || { warn "patch of $f did not apply"; return 1; }
  # Cargo.toml: wgpu-core switches the wgpu-hal backends on through the helper crate wgpu-core-deps-windows-linux-android,
  # which is a dependency only for windows/linux/android/freebsd -> add illumos there. The vulkan feature would pull in the
  # Vulkan backend too (ash); there is no Vulkan driver here, so it is emptied and only OpenGL (gles) is enabled.
  f=$CRATE_DIR/Cargo.toml
  sed -e '/dependencies.wgpu-core-deps-windows-linux-android\]/s/target_os = "freebsd"/target_os = "freebsd", target_os = "illumos"/' \
      -e 's/^vulkan = \["wgpu-core-deps-windows-linux-android\/vulkan"\]/vulkan = []/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
  grep -q 'target_os = "illumos"' "$f" || { warn "patch of $f did not apply"; return 1; }
  echo "$v2" > "$CRATE_DIR/.illumos-gles-r2"
  register_patches
}

# crate_copy NAME : copy the registry source of NAME (the version from Cargo.lock) to $BASE/patches/NAME;
# sets CRATE_DIR and CRATE_VER
crate_copy() {
  local name=$1 reg
  CRATE_VER=$(awk -v n="$name" '$0 == "name = \"" n "\"" {getline; gsub(/[^0-9.]/, "", $0); print; exit}' "$SRC/Cargo.lock")
  reg=$(ls -d /root/.cargo/registry/src/*/"$name"-"$CRATE_VER" 2>/dev/null | head -1)
  { [ -n "$CRATE_VER" ] && [ -d "$reg" ]; } || { warn "sources of $name not found in the cargo registry (version '$CRATE_VER')"; return 1; }
  CRATE_DIR=$BASE/patches/$name
  rm -rf "$CRATE_DIR"; mkdir -p "$BASE/patches" && cp -R "$reg" "$CRATE_DIR"
}

# register_patches : [patch.crates-io] in the workspace Cargo.toml for every crate in $BASE/patches
# (the original Cargo.toml is kept as Cargo.toml.orig-patch and is always the starting point)
register_patches() {
  local d lines="$BASE/patches.lines" n=0
  cd "$SRC" || return 1
  [ -f Cargo.toml.orig-patch ] || cp -p Cargo.toml Cargo.toml.orig-patch
  : > "$lines"
  for d in "$BASE"/patches/*; do
    [ -d "$d" ] || continue
    echo "$(basename "$d") = { path = \"$d\" }" >> "$lines"; n=$((n + 1))
  done
  cp -p Cargo.toml.orig-patch Cargo.toml
  if grep -q '^\[patch.crates-io\]' Cargo.toml; then
    awk -v f="$lines" 'BEGIN { while ((getline l < f) > 0) add = add l "\n" } { print } /^\[patch.crates-io\]/ { printf "%s", add }' Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml
  else
    { echo; echo "[patch.crates-io]"; cat "$lines"; } >> Cargo.toml
  fi
  say "patched: Cargo.toml ([patch.crates-io] with $n local crate copies: $(ls "$BASE/patches" | tr '\n' ' '))"
  return 0
}

# diagnose_and_fix LOGFILE : 0 = something was fixed (a new attempt is worthwhile), 1 = nothing to do
diagnose_and_fix() {
  local logf=$1 did=1 n h

  # wayland-backend does not compile on illumos: patched local copy
  if grep -q 'could not compile `wayland-backend`' "$logf" && ! fixed_before wbackend; then
    mark_fixed wbackend
    patch_wayland_backend && did=0
  fi

  # rfd: no file dialog backend for illumos: patched local copy
  if grep -qE 'DialogImpl. is not satisfied|could not compile `rfd`' "$logf" && ! fixed_before rfd; then
    mark_fixed rfd
    patch_rfd && did=0
  fi

  # winit: is_main_thread() is missing for illumos: patched local copy
  if grep -q 'cannot find function `is_main_thread`' "$logf" && ! fixed_before winitmain; then
    mark_fixed winitmain
    patch_winit && did=0
  fi

  # Wayland libraries do not exist on OpenIndiana: load them at run time instead of linking (X11 is used here)
  if grep -qE 'wayland-client|wayland-sys|wayland-protocols' "$logf" && ! fixed_before wayland; then
    mark_fixed wayland
    patch_no_wayland && did=0
  fi

  # Rust too old
  if grep -qE 'requires rustc|is not supported by|feature `edition2024` is required|newer version of Cargo|rustc [0-9.]+ is too old' "$logf" && ! fixed_before rust; then
    say "Cause: Rust is too old -> updating"
    mark_fixed rust
    if command -v rustup >/dev/null 2>&1; then run_logged "$LOGDIR/rustup-update.log" rustup update stable; else install_rust; fi
    did=0
  fi

  # pkg-config missing
  if grep -qE 'pkg-config: (command )?not found|Could not run `pkg-config`|pkg-config.*(not installed|not found)' "$logf" && ! fixed_before pkgconfig; then
    say "Cause: pkg-config is missing -> installing"
    mark_fixed pkgconfig; ensure_pkg pkg-config '*/pkg-config' && did=0
  fi

  # C compiler / linker missing
  if grep -qE 'linker `(cc|gcc)` not found|cc: (command )?not found|error: linker' "$logf" && ! fixed_before gcc; then
    say "Cause: C compiler/linker is missing -> installing gcc"
    mark_fixed gcc; ensure_gcc && { setup_env; did=0; }
  fi

  # Library not found (pkg-config): "system library `x` required by crate" or "Package x was not found"
  for n in $( { grep -oE 'system library `[^`]+`' "$logf" | sed 's/.*`\(.*\)`/\1/'
                grep -oE "Package '?[A-Za-z0-9_.+-]+'? (was )?not found" "$logf" | sed "s/^Package '\{0,1\}//; s/'\{0,1\} .*//"
                grep -oE "No package '[^']+' found" "$logf" | sed "s/No package '//; s/' found//"
              } 2>/dev/null | sort -u); do
    n=${n%%[<>=]*}
    case "$n" in wayland*) fixed_before wayland && continue ;; esac   # handled above (X11-only build)
    fixed_before "lib-$n" && continue
    mark_fixed "lib-$n"
    say "Cause: library '$n' is missing (pkg-config) -> looking for a package"
    { ensure_pkg "$n" "*/lib$n" || ensure_pkg "$n" "*/$n"; } && did=0
  done

  # Linker: -lXYZ not found
  for n in $(grep -oE 'library -l[A-Za-z0-9_+.-]+: not found|cannot find -l[A-Za-z0-9_+.-]+' "$logf" | grep -oE '\-l[A-Za-z0-9_+.-]+' | sed 's/^-l//' | sort -u); do
    fixed_before "ld-$n" && continue
    mark_fixed "ld-$n"
    say "Cause: the linker cannot find -l$n -> looking for a package"
    { ensure_pkg "lib$n" "*/lib$n" || ensure_pkg "$n" "*/$n"; } && { setup_env; did=0; }
  done

  # Header missing: "fatal error: X11/Xlib.h: No such file"
  for h in $(grep -oE 'fatal error: [A-Za-z0-9_/.+-]+: No such file' "$logf" | sed 's/fatal error: //; s/: No such file//' | sort -u); do
    fixed_before "h-$h" && continue
    mark_fixed "h-$h"
    n=${h%%/*}; [ "$n" = "$h" ] && n=${h%.h}
    say "Cause: header '$h' is missing -> looking for a package for '$n'"
    case "$n" in
      X11) ensure_pkg libx11 '*/libx11'; ensure_pkg x11-protocols '*/x11-protocols'; did=0 ;;
      openssl) ensure_pkg openssl '*/openssl*' && did=0 ;;
      *) { ensure_pkg "lib$n" "*/lib$n" || ensure_pkg "$n" "*/$n"; } && did=0 ;;
    esac
  done

  # OpenSSL development files
  if grep -qE 'failed to run custom build command for `openssl-sys`' "$logf" && ! fixed_before openssl; then
    say "Cause: OpenSSL development files are missing -> installing"
    mark_fixed openssl; ensure_pkg openssl '*/openssl*' && did=0
  fi

  # libclang (bindgen)
  if grep -qE 'Unable to find libclang|couldn.t find any valid shared libraries matching' "$logf" && ! fixed_before clang; then
    say "Cause: libclang is missing -> installing clang (then set LIBCLANG_PATH=/usr/llvm/<ver>/lib if needed)"
    mark_fixed clang
    ensure_pkg clang "$(pkg list -a -H 'developer/clang-*' 2>/dev/null | awk '{print $1}' | grep -E '^developer/clang-[0-9]+$' | sort -t- -k2 -n | tail -1)" && did=0
  fi

  # Out of memory: continue with 1 job
  if grep -qE 'SIGKILL|signal: 9|Cannot allocate memory|out of memory|memory allocation of' "$logf" && ! fixed_before mem; then
    mark_fixed mem
    if [ "${JOBS:-1}" -gt 1 ]; then
      JOBS=1; export CARGO_BUILD_JOBS=1
      say "Cause: not enough memory -> continuing with 1 job (more swap helps as well)"
      did=0
    else
      warn "not enough memory even with 1 job: increase swap/RAM (see --check)"
    fi
  fi

  # Causes that cannot be fixed automatically: only a clear hint
  if grep -qE 'No space left on device' "$logf"; then
    warn "disk is full: free some space (rm -rf $SRC/target or check the ZFS quota), then start again"
  fi
  if grep -qE 'Could not resolve host|failed to download|spurious network error|SSL (connect )?error' "$logf"; then
    warn "network problem while downloading crates (are crates.io/github.com reachable? proxy/DNS?)"
  fi
  if [ $did -ne 0 ] && grep -qE '^error\[E[0-9]+\]|^error: could not compile' "$logf"; then
    warn "compiler error in the source code (not a package problem, possibly illumos specific): see errors.txt, the first errors:"
    grep -E -A4 '^error(\[E[0-9]+\])?:' "$logf" | head -30 | sed 's/^/    /'
  fi
  return $did
}

# --- 7. Build ----------------------------------------------------------------
# build_target NAME : first --bin NAME, otherwise -p NAME
build_target() {
  local name=$1 try=1 logf rc
  cd "$SRC" || die "source directory $SRC is missing"
  while [ $try -le $MAXTRY ]; do
    setup_env
    logf="$LOGDIR/build-$name-attempt$try.log"
    say "building $name (attempt $try/$MAXTRY, jobs ${JOBS}) -> $logf"
    run_logged "$logf" cargo build --release -j "$JOBS" --bin "$name"; rc=$?
    if [ $rc -ne 0 ] && grep -qE 'no bin target named|did not match any|no bin target' "$logf"; then
      say "no binary '$name', trying package -p $name"
      logf="$LOGDIR/build-$name-attempt$try-p.log"
      run_logged "$logf" cargo build --release -j "$JOBS" -p "$name"; rc=$?
    fi
    if [ $rc -eq 0 ]; then say "OK: $name built"; return 0; fi
    collect_errors "$logf"
    say "build of $name failed (rc=$rc), analyzing $logf"
    if diagnose_and_fix "$logf"; then say "cause fixed, trying again"; try=$((try + 1)); else say "no automatic fix found"; return 1; fi
  done
  say "not successful after $MAXTRY attempts"
  return 1
}

# make_bundle : KISS deployment: one self-contained folder that can simply be copied to any other OpenIndiana machine
# (the same release of illumos with X11/Mesa installed) and started from there, plus a .tar.gz (and a .zip if zip exists).
#   <name>/wordcraft, wordcraft-cli   launchers (set the library path relative to their own folder)
#   <name>/bin/                       the real programs
#   <name>/lib/libgcc_s.so.1          the one library that is not in the default path on every system
#   <name>/share/                     icons and the templates used by install.sh
#   <name>/install.sh                 desktop | menu | uninstall
make_bundle() {
  local rel="$SRC/target/release" ver name dir lib b miss
  [ -x "$rel/wordcraft" ] || [ -x "$rel/wordcraft-cli" ] || { warn "no binaries in $rel, nothing to bundle"; return 1; }
  ver=$(LD_LIBRARY_PATH_64="${LD_LIBRARY_PATH_64:-}" "$rel/wordcraft-cli" --version 2>/dev/null | awk '{print $2}'); [ -n "$ver" ] || ver=unknown
  name="wordcraft-$ver-illumos-$(isainfo -k 2>/dev/null || echo x86)"
  dir="$SRC/target/bundle/$name"
  say "=== Bundle: $dir ==="
  rm -rf "$dir"; mkdir -p "$dir/bin" "$dir/lib" "$dir/share" || return 1
  for b in wordcraft wordcraft-cli; do
    [ -x "$rel/$b" ] || continue
    cp "$rel/$b" "$dir/bin/$b" && { strip "$dir/bin/$b" 2>/dev/null || true; }   # copies only; a smaller file loads faster
    cat > "$dir/$b" <<'EOF'
#!/bin/sh
# launcher: uses the libraries bundled next to it (do not symlink this file, use install.sh instead)
D=$(cd "$(dirname "$0")" && pwd)
LD_LIBRARY_PATH_64="$D/lib${LD_LIBRARY_PATH_64:+:$LD_LIBRARY_PATH_64}"
export LD_LIBRARY_PATH_64
EOF
    echo "exec \"\$D/bin/$b\" \"\$@\"" >> "$dir/$b"
    chmod 755 "$dir/$b"
  done
  lib=/usr/lib/amd64/libgcc_s.so.1
  [ -f "$lib" ] || lib=$(ls /usr/gcc/*/lib/amd64/libgcc_s.so.1 2>/dev/null | sort -t/ -k4 -n | tail -1)
  [ -f "$lib" ] && cp "$lib" "$dir/lib/" || warn "libgcc_s.so.1 not found, the bundle depends on the target system for it"
  [ -d "$SRC/assets/app-icon/hicolor" ] && cp -R "$SRC/assets/app-icon/hicolor" "$dir/share/icons"
  for b in LICENSE LICENSE-MIT LICENSE-APACHE NOTICE; do [ -f "$SRC/$b" ] && cp "$SRC/$b" "$dir/"; done
  cat > "$dir/share/wordcraft.desktop.in" <<'EOF'
[Desktop Entry]
Type=Application
Name=WordCraft
GenericName=Word Processor
Comment=Write and design documents; open and save Word files
Exec=@DIR@/wordcraft %F
Icon=@DIR@/share/icons/128x128/apps/ai.storyteller.wordcraft.png
Terminal=false
StartupNotify=true
StartupWMClass=ai.storyteller.wordcraft
Categories=Office;WordProcessor;X-Artcraft;
EOF
  cat > "$dir/share/artcraft.directory" <<'EOF'
[Desktop Entry]
Type=Directory
Name=Artcraft
Icon=ai.storyteller.wordcraft
EOF
  cat > "$dir/share/artcraft.menu" <<'EOF'
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://www.freedesktop.org/standards/menu-spec/menu-1.0.dtd">
<Menu>
  <Name>Applications</Name>
  <Menu>
    <Name>Artcraft</Name>
    <Directory>artcraft.directory</Directory>
    <Include><Category>X-Artcraft</Category></Include>
  </Menu>
</Menu>
EOF
  cat > "$dir/install.sh" <<'EOF'
#!/bin/sh
# sh install.sh setup      everything in one step: repairs permissions and line endings, desktop starter for the
#                          current user and, when run as root, the application menu entry "Artcraft" for all users
# sh install.sh check      list the runtime libraries the program needs and whether this system has them
# sh install.sh perms      only repair permissions and line endings (needed after copying through Windows)
# sh install.sh desktop    starter on the desktop of the current user
# sh install.sh menu       entry in the application menu under "Artcraft" for all users (needs root)
# sh install.sh uninstall  removes the menu entry again (needs root)
# The program stays in this folder: copy the folder to its final place (for example /opt/artcraft) first,
# then start this script with "sh" (it works without the executable bit).
D=$(cd "$(dirname "$0")" && pwd)

do_perms() {
  # files that went through Windows may have CRLF line endings and no executable bit
  for f in "$D/wordcraft" "$D/wordcraft-cli" "$D/share/wordcraft.desktop.in" "$D/share/artcraft.directory" "$D/share/artcraft.menu"; do
    [ -f "$f" ] && { tr -d '\015' < "$f" > "$f.tmp" && cat "$f.tmp" > "$f"; rm -f "$f.tmp"; }
  done
  chmod 755 "$D/wordcraft" "$D/wordcraft-cli" "$D/install.sh" "$D/bin/wordcraft" "$D/bin/wordcraft-cli" 2>/dev/null
  chmod -R a+rX "$D"
  echo "permissions and line endings repaired in $D"
}

do_check() {
  # the programs need nothing but the default OpenIndiana desktop: no compiler, no Rust. The GUI loads these at run time.
  miss=0
  for e in libX11.so.6:x11/library/libx11 libxcb.so.1:x11/library/libxcb libxkbcommon.so.0:x11/library/libxkbcommon \
           libXcursor.so.1:x11/library/libxcursor libXi.so:x11/library/libxi libEGL.so.1:x11/library/mesa \
           libfontconfig.so.1:system/library/fontconfig libdbus-1.so.3:-; do
    lib=${e%%:*}; pkg=${e#*:}
    if [ -e "/usr/lib/amd64/$lib" ] || [ -e "/lib/amd64/$lib" ]; then echo "ok       $lib"
    else echo "MISSING  $lib   (pkg install $pkg)"; miss=1; fi
  done
  if [ -e /usr/lib/amd64/libgcc_s.so.1 ] || [ -e /usr/gcc/14/lib/amd64/libgcc_s.so.1 ] || [ -e "$D/lib/libgcc_s.so.1" ]; then echo "ok       libgcc_s.so.1"
  else echo "MISSING  libgcc_s.so.1 (use the launchers ./wordcraft, they take the copy from lib/)"; miss=1; fi
  [ $miss -eq 0 ] && echo "all runtime libraries are present"
  return $miss
}

do_desktop() {
  mkdir -p "$HOME/Desktop"
  sed "s#@DIR@#$D#g" "$D/share/wordcraft.desktop.in" > "$HOME/Desktop/WordCraft.desktop"
  chmod 755 "$HOME/Desktop/WordCraft.desktop"
  gio set "$HOME/Desktop/WordCraft.desktop" metadata::trusted yes >/dev/null 2>&1   # skips the trust question where supported
  echo "created $HOME/Desktop/WordCraft.desktop (if MATE asks once whether to trust it: yes)"
}

do_menu() {
  [ "$(id -u)" = 0 ] || { echo "the menu entry needs root"; return 1; }
  mkdir -p /usr/share/icons/hicolor /etc/xdg/menus/applications-merged
  for s in 16x16 24x24 32x32 48x48 64x64 128x128 256x256 512x512; do
    [ -d "$D/share/icons/$s" ] && { mkdir -p "/usr/share/icons/hicolor/$s/apps"; cp "$D/share/icons/$s/apps/"* "/usr/share/icons/hicolor/$s/apps/"; }
  done
  [ -d "$D/share/icons/scalable" ] && { mkdir -p /usr/share/icons/hicolor/scalable/apps; cp "$D/share/icons/scalable/apps/"* /usr/share/icons/hicolor/scalable/apps/; }
  sed "s#@DIR@#$D#g" "$D/share/wordcraft.desktop.in" > /usr/share/applications/ai.storyteller.wordcraft.desktop
  cp "$D/share/artcraft.directory" /usr/share/desktop-directories/artcraft.directory
  cp "$D/share/artcraft.menu" /etc/xdg/menus/applications-merged/artcraft.menu
  gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1
  echo "menu entry installed. Restart the panel (mate-panel --replace &) or log in again: Applications > Artcraft > WordCraft"
}

do_uninstall() {
  [ "$(id -u)" = 0 ] || { echo "run as root"; return 1; }
  rm -f /usr/share/applications/ai.storyteller.wordcraft.desktop /usr/share/desktop-directories/artcraft.directory /etc/xdg/menus/applications-merged/artcraft.menu
  rm -f /usr/share/icons/hicolor/*/apps/ai.storyteller.wordcraft.*
  echo "menu entry removed (this folder and desktop starters are untouched)"
}

case "$1" in
  setup)     do_perms; do_check; do_desktop; if [ "$(id -u)" = 0 ]; then do_menu; else echo "(menu entry skipped: run as root to add it for all users)"; fi ;;
  check)     do_check ;;
  perms)     do_perms ;;
  desktop)   do_desktop ;;
  menu)      do_menu ;;
  uninstall) do_uninstall ;;
  *)         sed -n '2,8p' "$0" ;;
esac
EOF
  chmod 755 "$dir/install.sh"
  cat > "$dir/README.txt" <<'EOF'
WordCraft for illumos (OpenIndiana), built from source by build_wordcraft.sh.
Start:      ./wordcraft            (GUI, in the MATE session at the screen)
            ./wordcraft-cli --help (command line, no display needed)
Install:    copy this folder to its final place (for example /opt/artcraft), then   sh install.sh setup
            (repairs permissions, creates the desktop starter and, as root, the menu entry Artcraft > WordCraft).
            Best transfer: copy the .tar.gz or .zip itself to the machine and unpack it there (keeps the permissions).
Needs:      OpenIndiana with X11 and Mesa/EGL (x11/library/libx11, libxcb, libxkbcommon, libxcursor, libxi, libxrandr,
            libxrender, mesa, fontconfig); no Wayland and no GPU driver required (software rendering works).
Note:       pre-alpha software; work on copies of your documents.
EOF
  miss=$(LD_LIBRARY_PATH_64="$dir/lib" ldd "$dir/bin/wordcraft" 2>&1 | grep -i 'not found')
  [ -n "$miss" ] && warn "libraries missing for the bundled program: $miss"
  # clean-room check: the bundle must run on a default desktop without any build tool (no gcc, no Rust, no cargo in PATH)
  if env -i PATH=/usr/bin:/bin "$dir/wordcraft-cli" --version >/dev/null 2>&1; then
    say "bundle check: wordcraft-cli starts with an empty environment (no compiler or Rust in PATH)"
  else
    warn "bundle check: wordcraft-cli does not start with an empty environment"
  fi
  env -i PATH=/usr/bin:/bin ldd "$dir/bin/wordcraft" 2>&1 | grep -E '/root/|\.cargo|\.rustup|not found' | sed 's/^/    BUILD-HOST DEPENDENCY: /' | tee -a "$LOGDIR/summary.log"
  say "bundle check: runtime libraries on this system:"; sh "$dir/install.sh" check 2>&1 | sed 's/^/    /' | tee -a "$LOGDIR/summary.log"
  (cd "$SRC/target/bundle" && tar cf - "$name" | gzip > "$name.tar.gz") && say "archive: $SRC/target/bundle/$name.tar.gz ($(ls -l "$SRC/target/bundle/$name.tar.gz" | awk '{print $5}') bytes)"
  if command -v zip >/dev/null 2>&1; then
    (cd "$SRC/target/bundle" && rm -f "$name.zip" && zip -qr "$name.zip" "$name") && say "archive: $SRC/target/bundle/$name.zip"
  else
    say "no zip command (pkg install compress/zip), the .tar.gz is the archive; on Windows 7-Zip opens it"
  fi
  say "folder:  $dir   (copy it anywhere and start ./wordcraft; see README.txt and install.sh)"
}

final_report() {
  say "=== Result ==="
  local b
  for b in wordcraft-cli wordcraft; do
    if [ -x "$SRC/target/release/$b" ]; then
      say "done: $SRC/target/release/$b ($(ls -l "$SRC/target/release/$b" | awk '{print $5}') bytes)"
      ldd "$SRC/target/release/$b" 2>&1 | grep -i 'not found' | sed 's/^/    MISSING: /' | tee -a "$LOGDIR/summary.log"
    else
      say "not built: $b"
    fi
  done
  if [ -x "$SRC/target/release/wordcraft-cli" ]; then   # smoke test (only the CLI; the GUI needs a display)
    "$SRC/target/release/wordcraft-cli" --help </dev/null 2>&1 | head -5 | sed 's/^/    cli> /' | tee -a "$LOGDIR/summary.log"
  fi
  say "Logs: $LOGDIR   (errors: $LOGDIR/errors.txt)"
  [ -x "$SRC/target/release/wordcraft" ] && make_bundle
}

# --- Main flow ---------------------------------------------------------------
say "##### build_wordcraft.sh mode=$MODE started #####"
[ "$MODE" = bundle ] && { setup_env; make_bundle; exit $?; }
check_system
[ "$MODE" = check ] && { say "Checks finished."; exit 0; }

install_packages
install_rust
[ "$MODE" = deps ] && { say "Packages and Rust are ready."; exit 0; }

fetch_source
[ $GUI -eq 1 ] && patch_wgpu_gles
rc=0
build_target wordcraft-cli || rc=1
if [ $GUI -eq 1 ]; then build_target wordcraft || rc=1; fi
final_report
if [ $rc -ne 0 ]; then
  say "There were errors. Report:  bash $0 --report"
  exit 1
fi
say "Everything built."
exit 0
