Files
torben a6d07f97c4
CI / verify (push) Successful in 5m19s
CI / pwsh (push) Successful in 1m55s
Release / release (push) Successful in 36s
feat!: installation only from a release, into an empty folder; upstream merge/verify and private-instance.md removed, dist adopt, shell-neutral instructions (#153)
Files changed:
- .gitea/workflows/ci.yml
- .gitea/workflows/release.yml
- AGENTS.md
- CHANGES.md
- DEVELOPMENT.md
- EVALS.md
- INSTALL.md
- README.md
- VERSION
- docs/ownership-and-templates.md
- instructions/CONTRACT.md
- instructions/bootstrap.md
- instructions/dev/dev-setup.md
- instructions/dev/stack-dev/SKILL.md
- instructions/gates.md
- instructions/ingest-large-tree.md
- instructions/kb-profiles.md
- instructions/mcp-read-server.md
- instructions/migrations/3.0.0-authoring-conventions.md
- instructions/preflight.md
- instructions/private-instance.md
- instructions/session-setup.md
- instructions/setup-instance.md
- instructions/upgrade-instance.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/upstream_cmd.py
- tools/chemenu/commands/work_cmd.py
- tools/chemenu/config.py
- tools/chemenu/ownership.py
- tools/chemenu/tests/test_cli.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_instructions_shell.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/tests/test_run_budget.py
- tools/chemenu/tests/test_upstream_cmd.py
- tools/chemenu/toc.py
- tools/preflight.ps1
- tools/preflight.sh
2026-10-01 22:12:09 +02:00

725 lines
27 KiB
Bash
Executable File

#!/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 <path>), 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 <path>] [--archive <tarball>] [--set <tool>=<path>]...
#
# --archive uses a local tarball instead of a download; <tarball>.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 <tool>=<path>]...
preflight.sh [--into <path>] [--archive <tarball>] [--set <tool>=<path>]...
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 <tool>=<path>" >&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 <what> <why> <fix, may span lines>
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() { # <message>
echo "preflight: $1" >&2
exit 1
}
sha256_of() { # <file>
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 <path>."
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 <tarball>."
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 <<EOT
$SETS
EOT
trap - EXIT
cleanup
exec /bin/sh "$TARGET/tools/preflight.sh" "$@"
}
[ "$ASSET" -eq 0 ] || asset_mode
# --- the manifest ---------------------------------------------------------------
manifest_field() { # <kind> <name> <field number, 1-based>
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() { # <tool>
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() { # <have> <want>, 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 <name> 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() { # <path>
[ -n "$1" ] && [ -f "$1" ] && [ -x "$1" ] && ! is_store_alias "$1"
}
# Runs a Python candidate; prints "<major.minor>\n<sys.executable>".
probe_python() { # <path> [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 <name> 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() { # <name> -> 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() { # <path> [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=<full path to 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 <<EOF
$found
EOF
done
fi
fi
# --- the other tools --------------------------------------------------------------
REPORT=""
report() { # <status> <name> <version> <path>
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=<full path to 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=<full path to 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=<full path to $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=<full path to $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() { # <true|false>
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