feat: preflight as a release asset - download, verify, unpack, then run the tree preflight (#151, C)
Files changed: - .gitea/workflows/release.yml - CHANGES.md - INSTALL.md - README.md - VERSION - instructions/dev/testing-conventions.md - instructions/preflight.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/tests/test_preflight.py - tools/chemenu/tests/test_preflight_pwsh.py - tools/preflight.ps1 - tools/preflight.sh
This commit is contained in:
1 parent
210e0c8286
commit
c33e8cdfb1
13 files changed
+1020
-88
No files matched your search
+199
-27
@@ -5,11 +5,23 @@
|
||||
# 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 <script folder>/chemenu (or --into <path>), and runs the copy of
|
||||
# this script inside the unpacked tree, which does everything above.
|
||||
#
|
||||
# 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, or the tree next to it is incomplete.
|
||||
# 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
|
||||
@@ -31,23 +43,41 @@ 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 <script folder>/chemenu or --into,
|
||||
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
|
||||
@@ -65,8 +95,11 @@ ${1#--set=}" ;;
|
||||
shift
|
||||
done
|
||||
|
||||
ASSET=0
|
||||
if [ ! -f "$MANIFEST" ]; then
|
||||
echo "preflight: $MANIFEST is missing - this script has to run from inside an unpacked stack tree." >&2
|
||||
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
|
||||
|
||||
@@ -124,6 +157,170 @@ stop() {
|
||||
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
|
||||
}
|
||||
|
||||
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/chemenu"
|
||||
fi
|
||||
TARGET=${TARGET%/}
|
||||
if [ -e "$TARGET" ] || [ -L "$TARGET" ]; then
|
||||
asset_fail "$(native_path "$TARGET") already exists - nothing was unpacked. Choose another folder with --into <path>, or move the existing one away."
|
||||
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" \
|
||||
"Run this again with a shorter folder, for example: --into C:\\\\Chemenu
|
||||
Alternatively, someone with administrator rights can turn on long paths in Windows."
|
||||
stop
|
||||
fi
|
||||
fi
|
||||
|
||||
parent=$(dirname -- "$TARGET")
|
||||
mkdir -p "$parent" || asset_fail "could not create $parent."
|
||||
UNPACK="$parent/.chemenu-unpack.$$"
|
||||
mkdir "$UNPACK" || asset_fail "could not create a temporary folder next to the target."
|
||||
tar -xzf "$archive" -C "$UNPACK" || asset_fail "unpacking failed - the target was not created."
|
||||
[ -d "$UNPACK/$top" ] || asset_fail "unpacking produced no $top folder - the target was not created."
|
||||
mv "$UNPACK/$top" "$TARGET" || asset_fail "could not move the unpacked stack to $(native_path "$TARGET")."
|
||||
rm -rf "$UNPACK"
|
||||
UNPACK=""
|
||||
|
||||
[ -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>
|
||||
@@ -398,31 +595,6 @@ done
|
||||
|
||||
# --- install folder length (Windows, long paths off) ------------------------------
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
if [ "$PLATFORM" = windows ] && ! longpaths_enabled; then
|
||||
limit=$(manifest_field limit install_dir_max 3)
|
||||
folder=$(native_path "$ROOT")
|
||||
|
||||
Reference in new issue
Block a user