#!/bin/sh # Preflight: check that this machine has what the stack needs, record where each # tool lives, and set up tools/.venv - before any `wikitool` command can run. # # tools/preflight.sh check, record, set up # tools/preflight.sh --set rg=/opt/rg/rg use a path the user named # # As the release asset `preflight.sh` - a copy with no tools/prerequisites.txt next # to it - the script is the first install step instead: it downloads the stack # release named in RELEASE_ARCHIVE_URL / RELEASE_CHECKSUM_URL, checks the sha256, # unpacks it into the folder it lies in (or --into ), removes itself there, and # runs the copy of this script inside the unpacked tree, which does everything above. # That folder has to be empty apart from this script and a .git (an empty clone of the # instance's own repository). # # preflight.sh [--into ] [--archive ] [--set =]... # # --archive uses a local tarball instead of a download; .sha256 has to lie # next to it. Release builds fill the two URL lines below; a tree copy leaves them empty. # # Exit 0: everything is in place and .wikitool-tools.json is complete. # Exit 42: the user has to act. The output says what, why, the command that fixes # it and what happens next; show it verbatim and wait (AGENTS.md ยง Tool # error contract). Never install anything on the user's behalf. # Exit 1: this script was called wrongly, a download or unpack failed (nothing is # unpacked then), or the tree next to it is incomplete. # # POSIX sh on purpose: it has to run before Python is known to exist, under dash, # bash and the Git Bash that Claude Code uses on Windows. The PowerShell # counterpart is tools/preflight.ps1; what both check is tools/prerequisites.txt. # The procedure around this script: instructions/preflight.md. # # Two variables exist for the test suite and are read nowhere else: # CHEMENU_PREFLIGHT_PLATFORM (windows|macos|linux) and # CHEMENU_PREFLIGHT_LONGPATHS (0|1) stand in for `uname` and the registry. set -u DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P) ROOT=$(dirname -- "$DIR") MANIFEST="$DIR/prerequisites.txt" TOOLS_FILE="$ROOT/.wikitool-tools.json" VENV="$DIR/.venv" REQUIREMENTS="$DIR/requirements.txt" STAMP="$VENV/.chemenu-requirements.sha256" SELF="tools/preflight.sh" # Filled in by the release build, in the copy attached to the release; empty in a tree. RELEASE_ARCHIVE_URL='' RELEASE_CHECKSUM_URL='' PYPROBE='import sys; print("%d.%d" % sys.version_info[:2]); print(sys.executable)' usage() { cat <<'EOF' usage: tools/preflight.sh [--set =]... preflight.sh [--into ] [--archive ] [--set =]... Checks the tools this stack needs (tools/prerequisites.txt), records their paths in .wikitool-tools.json and sets up tools/.venv. Exit 0 means ready; exit 42 means the user has to act - the output says how. The second form is the release asset: it downloads the release (or takes --archive), checks its sha256, unpacks it into its own folder or --into (empty, or holding only .git), and runs the preflight inside it. EOF } # --- arguments ---------------------------------------------------------------- SETS="" INTO="" ARCHIVE="" while [ $# -gt 0 ]; do case "$1" in --into|--archive) if [ $# -lt 2 ] || [ -z "$2" ]; then echo "preflight: $1 needs a path" >&2 exit 1 fi if [ "$1" = --into ]; then INTO=$2; else ARCHIVE=$2; fi shift ;; --set) if [ $# -lt 2 ]; then echo "preflight: --set needs =" >&2 exit 1 fi SETS="$SETS $2" shift ;; --set=*) SETS="$SETS ${1#--set=}" ;; -h|--help) usage; exit 0 ;; *) echo "preflight: unknown argument: $1" >&2; usage >&2; exit 1 ;; esac shift done ASSET=0 if [ ! -f "$MANIFEST" ]; then ASSET=1 elif [ -n "$INTO$ARCHIVE" ]; then echo "preflight: --into and --archive belong to the release asset; inside a stack tree there is nothing to unpack." >&2 exit 1 fi # --- platform ----------------------------------------------------------------- if [ -n "${CHEMENU_PREFLIGHT_PLATFORM:-}" ]; then PLATFORM=$CHEMENU_PREFLIGHT_PLATFORM else case "$(uname -s 2>/dev/null)" in MINGW*|MSYS*|CYGWIN*) PLATFORM=windows ;; Darwin) PLATFORM=macos ;; *) PLATFORM=linux ;; esac fi # A path as the rest of the machine spells it: under Git Bash `/c/Tools/rg.exe` # becomes `C:\Tools\rg.exe`, which is what Python and PowerShell read back. native_path() { if [ "$PLATFORM" = windows ] && command -v cygpath >/dev/null 2>&1; then cygpath -w "$1" else printf '%s\n' "$1" fi } # --- problems and the guidance block -------------------------------------------- PROBLEMS=0 GUIDE="" BAD_SET=0 # problem problem() { PROBLEMS=$((PROBLEMS + 1)) fix=$(printf '%s\n' "$3" | sed '2,$s/^/ /') GUIDE="$GUIDE $PROBLEMS) $1 Why: $2 Fix: $fix Next: Tell the agent once this is done - it runs this check again. " } stop() { if [ "$PROBLEMS" -eq 1 ]; then noun="1 thing needs" else noun="$PROBLEMS things need" fi echo "" echo "STOP - $noun your attention before this wiki can run." echo "(Agent: show this output to the user exactly as it is, then wait. Do not install" echo "anything yourself and do not work around it.)" printf '%s' "$GUIDE" exit 42 } longpaths_enabled() { if [ -n "${CHEMENU_PREFLIGHT_LONGPATHS:-}" ]; then [ "$CHEMENU_PREFLIGHT_LONGPATHS" = 1 ] return fi # MSYS would rewrite the `/v` into a path without the exclusion. An unreadable # key counts as "off": the limit then protects a machine it did not have to. answer=$(MSYS2_ARG_CONV_EXCL='*' reg query 'HKLM\SYSTEM\CurrentControlSet\Control\FileSystem' \ /v LongPathsEnabled 2>/dev/null) || return 1 case "$answer" in *0x1*) return 0 ;; esac return 1 } # Length in UTF-16 code units, which is what MAX_PATH counts. utf16_length() { if command -v iconv >/dev/null 2>&1; then bytes=$(printf '%s' "$1" | iconv -f UTF-8 -t UTF-16LE 2>/dev/null | wc -c | tr -d ' ') if [ "${bytes:-0}" -gt 0 ]; then echo $((bytes / 2)) return fi fi echo ${#1} } # --- asset mode: the release asset unpacks the stack, then hands over ------------------ # # Only when no tools/prerequisites.txt lies next to this script. Nothing here reads # the tree; the folder limit comes out of the archive itself. The tools the tree copy # checks (Python, git, rg) are not needed until that copy runs. WORK="" cleanup() { [ -z "$WORK" ] || rm -rf "$WORK" [ -z "${UNPACK:-}" ] || rm -rf "$UNPACK" } asset_fail() { # echo "preflight: $1" >&2 exit 1 } sha256_of() { # if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 else shasum -a 256 "$1" | cut -d' ' -f1 fi } # Whether the install folder holds anything besides this script and a .git - the # two things an empty folder may already carry: the script was downloaded into it, and # the user may have cloned the instance's own, still empty repository there. target_is_occupied() { [ -d "$TARGET" ] || return 1 self_name=${0##*/} target_real=$(CDPATH='' cd -- "$TARGET" && pwd -P) for entry in "$TARGET"/* "$TARGET"/.[!.]* "$TARGET"/..?*; do [ -e "$entry" ] || [ -L "$entry" ] || continue name=${entry##*/} [ "$name" = .git ] && continue [ "$target_real" = "$DIR" ] && [ "$name" = "$self_name" ] && continue return 0 done return 1 } asset_mode() { # GNU tar reads `C:` as a host name; Git Bash has to hand it `/c/...`. if [ "$PLATFORM" = windows ] && command -v cygpath >/dev/null 2>&1; then [ -z "$ARCHIVE" ] || ARCHIVE=$(cygpath -u "$ARCHIVE") [ -z "$INTO" ] || INTO=$(cygpath -u "$INTO") fi if [ -n "$INTO" ]; then case "$INTO" in /*) TARGET=$INTO ;; *) TARGET="$(pwd)/$INTO" ;; esac else TARGET=$DIR fi TARGET=${TARGET%/} if { [ -e "$TARGET" ] || [ -L "$TARGET" ]; } && [ ! -d "$TARGET" ]; then asset_fail "$(native_path "$TARGET") exists and is not a folder - nothing was unpacked." fi if target_is_occupied; then asset_fail "$(native_path "$TARGET") is not empty - nothing was unpacked. The wiki installs into an empty folder (an empty git clone is fine): put this script into one and run it there, or pass --into ." fi if [ -z "$ARCHIVE" ] && { [ -z "$RELEASE_ARCHIVE_URL" ] || [ -z "$RELEASE_CHECKSUM_URL" ]; }; then asset_fail "this copy of the script does not come from a release (it has no download address). Download preflight.sh from the release page, or pass --archive ." fi if [ -n "$ARCHIVE" ]; then [ -f "$ARCHIVE" ] || asset_fail "--archive: $ARCHIVE is not a file." [ -f "$ARCHIVE.sha256" ] || asset_fail "--archive: $ARCHIVE.sha256 is missing - the checksum file has to lie next to the tarball." fi # What has to be here for the download, tested before anything is fetched. missing="" if [ -z "$ARCHIVE" ] && ! command -v curl >/dev/null 2>&1; then missing="$missing curl"; fi command -v tar >/dev/null 2>&1 || missing="$missing tar" command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1 || missing="$missing sha256sum" if [ -n "$missing" ]; then case "$PLATFORM" in macos) hint="brew install$missing" ;; windows) hint="Git for Windows (https://gitforwindows.org) provides all of them in Git Bash." ;; *) hint="sudo apt install$(printf '%s' "$missing" | sed 's/ sha256sum/ coreutils/')" ;; esac problem "Tools needed to fetch and unpack the stack could not be found:$missing" \ "the first step downloads the release, checks its checksum and unpacks it" \ "$hint" stop fi trap cleanup EXIT WORK=$(mktemp -d "${TMPDIR:-/tmp}/chemenu-preflight.XXXXXX") || asset_fail "could not create a temporary folder." if [ -n "$ARCHIVE" ]; then archive=$ARCHIVE checksum=$ARCHIVE.sha256 else archive="$WORK/${RELEASE_ARCHIVE_URL##*/}" checksum="$WORK/${RELEASE_CHECKSUM_URL##*/}" echo "Downloading $RELEASE_ARCHIVE_URL" curl -fsSL -o "$archive" "$RELEASE_ARCHIVE_URL" || asset_fail "the download failed: $RELEASE_ARCHIVE_URL (check the internet connection, then run this again)." curl -fsSL -o "$checksum" "$RELEASE_CHECKSUM_URL" || asset_fail "the download failed: $RELEASE_CHECKSUM_URL (check the internet connection, then run this again)." fi want=$(sed -n '1s/^\([0-9A-Fa-f]\{64\}\).*/\1/p' "$checksum" | tr 'A-F' 'a-f') [ -n "$want" ] || asset_fail "the checksum file holds no sha256 in its first line - nothing was unpacked." have=$(sha256_of "$archive" | tr 'A-F' 'a-f') if [ "$want" != "$have" ]; then asset_fail "the sha256 of the archive does not match its checksum file (expected $want, got $have) - nothing was unpacked. Delete the download and run this again." fi echo "sha256 OK" listing=$(tar -tzf "$archive" 2>/dev/null) || asset_fail "the archive could not be read - nothing was unpacked." tops=$(printf '%s\n' "$listing" | sed 's|^\./||; s|/.*||; /^\.\{0,1\}$/d') top=$(printf '%s\n' "$tops" | head -n 1) other=$(printf '%s\n' "$tops" | grep -v -x -F -- "$top" | head -n 1) if [ -z "$top" ] || [ -n "$other" ]; then asset_fail "the archive does not hold exactly one top-level folder - nothing was unpacked." fi manifest=$(tar -xOzf "$archive" "$top/tools/prerequisites.txt" 2>/dev/null) \ || asset_fail "the archive holds no $top/tools/prerequisites.txt - nothing was unpacked." limit=$(printf '%s\n' "$manifest" | sed -n 's/^limit|install_dir_max|\([0-9][0-9]*\).*/\1/p' | head -n 1) if [ "$PLATFORM" = windows ] && [ -n "$limit" ] && ! longpaths_enabled; then folder=$(native_path "$TARGET") length=$(utf16_length "$folder") if [ "$length" -gt "$limit" ]; then problem "The folder this wiki would be installed in is too long ($length characters, at most $limit): $folder" \ "Windows on this computer only allows paths of up to 259 characters, and the wiki's own files need the rest" \ "Use a shorter folder such as C:\\Chemenu - put this script there and run it again, or pass --into C:\\Chemenu. Alternatively, someone with administrator rights can turn on long paths in Windows." stop fi fi # Unpacked inside the target rather than beside it: the folder above may be a drive # root nobody can write to. Only then are the entries moved up, one by one. mkdir -p "$TARGET" || asset_fail "could not create $(native_path "$TARGET")." UNPACK="$TARGET/.chemenu-unpack.$$" mkdir "$UNPACK" || asset_fail "could not create a temporary folder in the target." tar -xzf "$archive" -C "$UNPACK" || asset_fail "unpacking failed - nothing was installed." [ -d "$UNPACK/$top" ] || asset_fail "unpacking produced no $top folder - nothing was installed." for entry in "$UNPACK/$top"/* "$UNPACK/$top"/.[!.]* "$UNPACK/$top"/..?*; do [ -e "$entry" ] || [ -L "$entry" ] || continue mv "$entry" "$TARGET/" || asset_fail "could not move ${entry##*/} into $(native_path "$TARGET") - the folder holds part of the stack now; empty it (keep .git) and run this again." done rm -rf "$UNPACK" UNPACK="" # The release asset is not part of the stack; left in the folder, it would end up in # the instance's first commit beside the tree's own tools/preflight.sh. if [ "$(CDPATH='' cd -- "$TARGET" && pwd -P)" = "$DIR" ]; then rm -f -- "$DIR/${0##*/}" fi [ -f "$TARGET/tools/preflight.sh" ] || asset_fail "the unpacked stack has no tools/preflight.sh." echo "Unpacked into $(native_path "$TARGET")." echo "If a later step stops, run tools/preflight.sh from inside that folder." echo "" set -- while IFS= read -r entry; do [ -n "$entry" ] || continue set -- "$@" --set "$entry" done < while IFS='|' read -r kind name rest; do case "$kind" in ''|'#'*) continue ;; esac if [ "$kind" = "$1" ] && [ "$name" = "$2" ]; then printf '%s|%s|%s\n' "$kind" "$name" "$rest" | cut -d'|' -f"$3" return 0 fi done < "$MANIFEST" return 1 } TOOLS="" while IFS='|' read -r kind name minimum platforms rest; do case "$kind" in ''|'#'*) continue ;; esac [ "$kind" = tool ] || continue case "$name" in *[!a-z0-9]*|'') echo "preflight: bad tool name '$name' in $MANIFEST" >&2; exit 1 ;; esac if [ "$platforms" = all ] || [ "$platforms" = "$PLATFORM" ]; then TOOLS="$TOOLS $name" fi done < "$MANIFEST" # The install command for this machine's package manager, or every known one. install_hint() { # choco=$(manifest_field tool "$1" 7) winget=$(manifest_field tool "$1" 8) brew=$(manifest_field tool "$1" 9) apt=$(manifest_field tool "$1" 10) pacman=$(manifest_field tool "$1" 11) hint="" case "$PLATFORM" in windows) if command -v choco >/dev/null 2>&1 && [ "$choco" != - ]; then hint="$choco (in a terminal opened as administrator)" elif command -v winget >/dev/null 2>&1 && [ "$winget" != - ]; then hint=$winget fi ;; macos) if command -v brew >/dev/null 2>&1 && [ "$brew" != - ]; then hint=$brew; fi ;; *) if command -v apt-get >/dev/null 2>&1 && [ "$apt" != - ]; then hint=$apt elif command -v pacman >/dev/null 2>&1 && [ "$pacman" != - ]; then hint=$pacman fi ;; esac if [ -n "$hint" ]; then printf '%s\n' "$hint" return fi printf 'Install it with the package manager this computer uses, for example:\n' for entry in "choco:$choco" "winget:$winget" "brew:$brew" "apt:$apt" "pacman:$pacman"; do case "$entry" in *:-) continue ;; esac printf ' %s\n' "${entry#*:}" done } # --- version helpers ------------------------------------------------------------ # major.minor of the first version-looking token in $1 ("git version 2.47.1" -> 2.47) version_of() { printf '%s\n' "$1" | sed -n '1{s/^[^0-9]*\([0-9][0-9]*\)\(\.[0-9][0-9]*\)\{0,1\}.*/\1\2/p;}' } version_ge() { # , both major[.minor] have=$1 want=$2 case "$have" in *.*) ;; *) have="$have.0" ;; esac case "$want" in *.*) ;; *) want="$want.0" ;; esac hm=${have%%.*} hn=${have#*.} wm=${want%%.*} wn=${want#*.} [ "$hm" -gt "$wm" ] || { [ "$hm" -eq "$wm" ] && [ "$hn" -ge "$wn" ]; } } # --- finding tools -------------------------------------------------------------- # The Microsoft Store's app-execution aliases: a python.exe that opens the Store # instead of running anything. Never executed, never recorded. is_store_alias() { case "$1" in *[Ww]indows[Aa]pps/*|*[Ww]indows[Aa]pps\\*) return 0 ;; esac return 1 } # Every executable called on PATH, in PATH order, store aliases dropped. on_path() { set -f old_ifs=$IFS IFS=: for d in $PATH; do [ -n "$d" ] || d=. for f in "$d/$1" "$d/$1.exe"; do if [ -f "$f" ] && [ -x "$f" ] && ! is_store_alias "$f"; then printf '%s\n' "$f" fi done done IFS=$old_ifs set +f } usable() { # [ -n "$1" ] && [ -f "$1" ] && [ -x "$1" ] && ! is_store_alias "$1" } # Runs a Python candidate; prints "\n". probe_python() { # [extra argument, e.g. -3 for py] if [ $# -gt 1 ]; then "$1" "$2" -c "$PYPROBE" 2>/dev/null | tr -d '\r' else "$1" -c "$PYPROBE" 2>/dev/null | tr -d '\r' fi } # The value recorded for in .wikitool-tools.json, unescaped. recorded() { [ -f "$TOOLS_FILE" ] || return 1 value=$(sed -n "s/^ *\"$1\": *\"\(.*\)\",\{0,1\} *\$/\1/p" "$TOOLS_FILE" | head -n 1) [ -n "$value" ] || return 1 printf '%s\n' "$value" | sed 's/\\"/"/g; s/\\\\/\\/g' } set_value() { # -> the path given with --set, if any printf '%s\n' "$SETS" | sed -n "s/^$1=//p" | head -n 1 } # --- --set: every named path has to work, or nothing is written ------------------- for entry in $(printf '%s\n' "$SETS" | sed -n 's/^\([^=]*\)=.*/\1/p'); do case " $TOOLS " in *" $entry "*) ;; *) echo "preflight: --set names '$entry', which is not a tool this platform needs:$TOOLS" >&2 exit 1 ;; esac done # --- python ------------------------------------------------------------------------- PY="" PY_VERSION="" PY_OLD="" PY_MIN=$(manifest_field tool python 3) try_python() { # [extra] -> sets PY/PY_VERSION on success, PY_OLD when too old out=$(probe_python "$@") || return 1 version=$(printf '%s\n' "$out" | sed -n 1p) executable=$(printf '%s\n' "$out" | sed -n 2p) [ -n "$version" ] && [ -n "$executable" ] || return 1 if version_ge "$version" "$PY_MIN"; then PY=$executable PY_VERSION=$version return 0 fi PY_OLD="$version $1" return 1 } python_extra() { # py and py.exe are the launcher: they need -3 case "${1##*/}" in py|py.exe) printf '%s\n' -3 ;; esac } given=$(set_value python) if [ -n "$given" ]; then extra=$(python_extra "$given") if ! usable "$given" || ! try_python "$given" $extra; then problem "The path given for Python does not work: $given" \ "every wikitool command runs on Python $PY_MIN or newer, and this path did not start one" \ "Check the path - it has to be the python executable itself, version $PY_MIN or newer. Then run: $SELF --set python=" BAD_SET=1 fi else previous=$(recorded python) || previous="" if [ -n "$previous" ] && usable "$previous"; then try_python "$previous" || true fi if [ -z "$PY" ]; then if [ "$PLATFORM" = windows ]; then order="python: py:-3 python3:" else order="python3: python:" fi for candidate in $order; do name=${candidate%%:*} extra=${candidate#*:} found=$(on_path "$name") [ -n "$found" ] || continue # A here-document, not a pipe: `break 2` has to leave the outer loop # of this shell, and a path may contain spaces ("Program Files"). while IFS= read -r path; do if [ -n "$extra" ]; then try_python "$path" "$extra" && break 2 else try_python "$path" && break 2 fi done < REPORT="$REPORT$(printf '%-8s %-7s %-8s %s' "$1" "$2" "$3" "$4") " } if [ -n "$PY" ]; then report OK python "$PY_VERSION" "$PY" eval "FOUND_python=\$PY" elif [ "$BAD_SET" -eq 0 ]; then label=$(manifest_field tool python 5) why=$(manifest_field tool python 6) if [ -n "$PY_OLD" ]; then report TOO_OLD python "${PY_OLD%% *}" "${PY_OLD#* }" problem "$label ${PY_OLD%% *} is too old - this stack needs $PY_MIN or newer." "$why" \ "$(install_hint python) Or, if a newer one is already installed, give its full path: $SELF --set python=" else report MISSING python - - problem "$label $PY_MIN or newer was not found." "$why" \ "$(install_hint python) Or, if it is installed somewhere this check did not look, give its full path: $SELF --set python=" fi fi for tool in $TOOLS; do [ "$tool" = python ] && continue label=$(manifest_field tool "$tool" 5) why=$(manifest_field tool "$tool" 6) minimum=$(manifest_field tool "$tool" 3) given=$(set_value "$tool") path="" if [ -n "$given" ]; then if usable "$given"; then path=$given else problem "The path given for $label does not work: $given" "$why" \ "Check the path - it has to be the $tool executable itself. Then run: $SELF --set $tool=" BAD_SET=1 continue fi else previous=$(recorded "$tool") || previous="" if [ -n "$previous" ] && usable "$previous"; then path=$previous else path=$(on_path "$tool" | head -n 1) fi fi if [ -z "$path" ]; then report MISSING "$tool" - - problem "$label was not found." "$why" \ "$(install_hint "$tool") Or, if it is installed somewhere this check did not look, give its full path: $SELF --set $tool=" continue fi version=$(version_of "$("$path" --version 2>/dev/null | tr -d '\r')") if [ "$minimum" != - ] && { [ -z "$version" ] || ! version_ge "$version" "$minimum"; }; then report TOO_OLD "$tool" "${version:-unknown}" "$path" problem "$label ${version:-of unknown version} is too old - this stack needs $minimum or newer." "$why" \ "$(install_hint "$tool")" continue fi native=$(native_path "$path") report OK "$tool" "${version:--}" "$native" eval "FOUND_$tool=\$native" done # --- install folder length (Windows, long paths off) ------------------------------ if [ "$PLATFORM" = windows ] && ! longpaths_enabled; then limit=$(manifest_field limit install_dir_max 3) folder=$(native_path "$ROOT") length=$(utf16_length "$folder") if [ "$length" -gt "$limit" ]; then problem "The folder this wiki is installed in is too long ($length characters, at most $limit): $folder" \ "Windows on this computer only allows paths of up to 259 characters, and the wiki's own files need the rest" \ "Move the wiki to a shorter folder, for example C:\\Chemenu, and run this check there. Alternatively, someone with administrator rights can turn on long paths in Windows." fi fi # --- record what was found ---------------------------------------------------------- json_string() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' } write_tools_file() { # tmp="$TOOLS_FILE.tmp.$$" { printf '{\n "schema": 1,\n "complete": %s,\n "tools": {' "$1" separator="" for tool in $TOOLS; do eval "value=\${FOUND_$tool:-}" [ -n "$value" ] || continue printf '%s\n "%s": "%s"' "$separator" "$tool" "$(json_string "$value")" separator="," done printf '\n }\n}\n' } > "$tmp" && mv -f "$tmp" "$TOOLS_FILE" } printf '%s' "$REPORT" if [ "$BAD_SET" -eq 1 ]; then stop # nothing is written for a path that does not work fi write_tools_file false if [ "$PROBLEMS" -gt 0 ]; then stop fi # --- tools/.venv ---------------------------------------------------------------------- venv_python() { if [ -x "$VENV/bin/python" ]; then printf '%s\n' "$VENV/bin/python" elif [ -f "$VENV/Scripts/python.exe" ]; then printf '%s\n' "$VENV/Scripts/python.exe" else return 1 fi } last_lines() { printf '%s\n' "$1" | tail -n 5 | sed 's/^/ /' } VPY=$(venv_python) || VPY="" if [ -z "$VPY" ] || ! "$VPY" -m pip --version >/dev/null 2>&1; then if ! output=$("$PY" -m venv --clear "$VENV" 2>&1) || ! VPY=$(venv_python); then fix="Python said: $(last_lines "$output")" if [ "$PLATFORM" = linux ] && command -v apt-get >/dev/null 2>&1; then fix="sudo apt install python3-venv $fix" fi problem "The wiki's own Python environment (tools/.venv) could not be created." \ "wikitool runs in that environment, so that nothing it installs touches the rest of the computer" \ "$fix" stop fi fi want=$("$VPY" -c 'import hashlib, sys; print(hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest())' \ "$REQUIREMENTS" 2>/dev/null | tr -d '\r') have=$(cat "$STAMP" 2>/dev/null) || have="" if [ -z "$want" ] || [ "$want" != "$have" ]; then if ! output=$("$VPY" -m pip install --disable-pip-version-check --quiet -r "$REQUIREMENTS" 2>&1); then problem "The libraries wikitool needs could not be installed into tools/.venv." \ "they are downloaded once from the internet; without them no wikitool command starts" \ "Check that this computer can reach the internet (a proxy or a security program can block it; if so, ask whoever looks after this computer). pip said: $(last_lines "$output")" stop fi printf '%s\n' "$want" > "$STAMP" fi echo "OK venv - $(native_path "$VENV")" write_tools_file true echo "" echo "Preflight passed. Tool paths are recorded in .wikitool-tools.json; tools/wikitool is ready." exit 0