Second failure from the Server 2019 test. The previous fix worked - msiexec went from exit 1639 (ERROR_INVALID_COMMAND_LINE, which is why it printed its usage dialog) to exit 1603 (ERROR_INSTALL_FAILURE), so the command line parses now and the MSI itself is failing. It failed in 1.1 seconds. An MSI that dies that fast has not begun installing; it has failed a launch condition. MySQL 8.4 requires the Visual C++ redistributable and a bare Windows Server does not ship it - the same runtime mysql.exe and mysqldump.exe import, which was visible when their DLL dependencies were trimmed and went unnoticed. Stage 0 now installs VC_redist.x64.exe from the bundle before touching MySQL, skipping it when vcruntime140.dll is already present, and fails with a sentence naming the requirement if the redistributable is absent from the bundle altogether. vcredist\ is an optional locked payload. msiexec also gets /l*v now. A bare 1603 names neither the failing action nor the reason, and it is the most common MySQL install failure - diagnosing this one took a launch-condition inference rather than a log. The MSI log lands beside the installer's own in ProgramData, so the next failure is readable instead of guessed at.
142 lines
6.4 KiB
Bash
Executable File
142 lines
6.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# Stage a lean per-site bundle next to ShopDBFlask.iss, ready for Inno Setup.
|
|
#
|
|
# The bundle is built FOR ONE SITE from its plugin profile (ADR-013): plugins the
|
|
# site did not choose are absent from the payload entirely. Build one installer
|
|
# per site, not one universal installer.
|
|
#
|
|
# Usage: build-installer.sh <site-profile.json> [repo-path]
|
|
#
|
|
# The wheelhouse cannot be built here. Wheels are cp314 win_amd64 and must be
|
|
# produced ON Windows with the matching Python:
|
|
# pip download -r requirements.txt -d wheels --only-binary=:all:
|
|
# Copy that wheels\ directory in before compiling.
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROFILE="${1:?usage: build-installer.sh <site-profile.json> [repo-path]}"
|
|
REPO="${2:-$HOME/projects/shopdb-flask}"
|
|
BUNDLE="$HERE/bundle"
|
|
|
|
[ -f "$PROFILE" ] || { echo "profile not found: $PROFILE"; exit 1; }
|
|
[ -d "$REPO" ] || { echo "repo not found: $REPO"; exit 1; }
|
|
|
|
echo "==> Staging lean app tree from $PROFILE"
|
|
rm -rf "$BUNDLE/app"
|
|
bash "$REPO/scripts/build-site.sh" "$PROFILE" "$BUNDLE/app"
|
|
|
|
# build-site.sh emits the SPA as frontend-dist; the installer's web.config and
|
|
# static route expect frontend\dist.
|
|
if [ -d "$BUNDLE/app/frontend-dist" ]; then
|
|
mkdir -p "$BUNDLE/app/frontend"
|
|
rm -rf "$BUNDLE/app/frontend/dist"
|
|
mv "$BUNDLE/app/frontend-dist" "$BUNDLE/app/frontend/dist"
|
|
fi
|
|
# The /shopdb-based build, used when the operator picks the subpath deployment.
|
|
if [ -d "$BUNDLE/app/frontend-dist-subpath" ]; then
|
|
rm -rf "$BUNDLE/app/frontend/dist-subpath"
|
|
mv "$BUNDLE/app/frontend-dist-subpath" "$BUNDLE/app/frontend/dist-subpath"
|
|
fi
|
|
|
|
# Tell the .iss which plugins this bundle actually carries, so the wizard's
|
|
# plugin page always matches the payload instead of a hand-maintained list.
|
|
echo "==> Writing plugins.iss"
|
|
PLUGINS=$(ls "$BUNDLE/app/plugins" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
|
|
# The subpath SPA is built with its base path compiled in, so whether the wizard
|
|
# can OFFER a subpath install is a property of the bundle, not a runtime choice.
|
|
SUBPATH_ALIAS_BUILT=""
|
|
if [ -f "$BUNDLE/app/frontend/dist-subpath/.alias" ]; then
|
|
SUBPATH_ALIAS_BUILT="$(cat "$BUNDLE/app/frontend/dist-subpath/.alias")"
|
|
fi
|
|
|
|
cat > "$HERE/plugins.iss" <<EOF
|
|
; GENERATED by build-installer.sh - do not edit.
|
|
; The plugins present in bundle\\app\\plugins at build time.
|
|
#define AvailablePlugins "$PLUGINS"
|
|
; The alias the subpath SPA was built for, or empty if this bundle has no
|
|
; subpath build - in which case the wizard must not offer that option.
|
|
#define SubpathAlias "$SUBPATH_ALIAS_BUILT"
|
|
EOF
|
|
echo " $PLUGINS"
|
|
|
|
# The product version, read from the code rather than restated here. A hardcoded
|
|
# AppVersion in the .iss had drifted two minor versions from shopdb/__init__.py.
|
|
echo "==> Writing version.iss"
|
|
APPVERSION=$(sed -n "s/^__version__ = '\\(.*\\)'/\\1/p" "$REPO/shopdb/__init__.py" | head -1)
|
|
[ -n "$APPVERSION" ] || { echo "could not read __version__ from shopdb/__init__.py"; exit 1; }
|
|
cat > "$HERE/version.iss" <<EOF
|
|
; GENERATED by build-installer.sh from shopdb/__init__.py - do not edit.
|
|
#define AppVersion "$APPVERSION"
|
|
EOF
|
|
echo " $APPVERSION"
|
|
|
|
# From THIS directory, which is the reviewed copy under version control. These
|
|
# used to be copied from $HOME/Downloads, so the installer logic that shipped was
|
|
# not the logic that was committed, and the build only worked on one machine.
|
|
echo "==> Copying installer scripts"
|
|
mkdir -p "$BUNDLE"
|
|
for f in shopdb-install.ps1 shopdb-preflight.ps1 bundle-lock.ps1; do
|
|
[ -f "$HERE/$f" ] || { echo "installer script missing from the repo: $f"; exit 1; }
|
|
cp "$HERE/$f" "$BUNDLE/"
|
|
done
|
|
|
|
echo ""
|
|
echo "Bundle staged at: $BUNDLE"
|
|
for d in app wheels python httpplatformhandler urlrewrite vcredist mysqlclient mysql; do
|
|
if [ -d "$BUNDLE/$d" ]; then
|
|
printf ' %-20s %s\n' "$d" "$(du -sh "$BUNDLE/$d" | cut -f1)"
|
|
else
|
|
printf ' %-20s absent\n' "$d"
|
|
fi
|
|
done
|
|
echo ""
|
|
echo " plugins shipped: $(ls "$BUNDLE/app/plugins" 2>/dev/null | tr '\n' ' ')"
|
|
|
|
# --- payload verification ---------------------------------------------------
|
|
# The third-party payload is the part git does not record: the wheels, the Python
|
|
# installer and the MSIs that run as SYSTEM on the target server. It must be
|
|
# EXACTLY what bundle-lock.json describes - no missing file, no stale extra wheel
|
|
# left over from a previous build, no changed content - or this is not a bundle
|
|
# anyone reviewed. Previously a missing wheelhouse printed MISSING and the script
|
|
# still exited 0, so an empty bundle compiled into a shippable installer and the
|
|
# failure surfaced on an air-gapped server with no way to fix it.
|
|
#
|
|
# ALLOW_UNLOCKED=1 downgrades this to a warning, for assembling a bundle before
|
|
# its lock exists. A bundle built that way must not be shipped.
|
|
echo ""
|
|
echo "==> Verifying the third-party payload against bundle-lock.json"
|
|
if python3 "$HERE/verify_bundle_lock.py" "$BUNDLE" "$HERE/bundle-lock.json"; then
|
|
echo " payload matches the lock"
|
|
# Ships WITH the bundle: the installer re-checks the payload on the target
|
|
# server before running any of it, so tampering between build and install is
|
|
# caught too.
|
|
cp "$HERE/bundle-lock.json" "$BUNDLE/"
|
|
elif [ "${ALLOW_UNLOCKED:-0}" = "1" ]; then
|
|
echo ""
|
|
echo " ALLOW_UNLOCKED=1: continuing anyway. DO NOT SHIP this bundle."
|
|
else
|
|
echo ""
|
|
echo " The bundle is not what the lock describes."
|
|
echo ""
|
|
echo " Add the missing pieces by hand:"
|
|
echo " wheels/ pip download -r requirements.txt --only-binary=:all: \\"
|
|
echo " --platform win_amd64 --python-version 314 \\"
|
|
echo " --implementation cp --abi cp314 -d wheels"
|
|
echo " python/ python-3.14.x-amd64.exe"
|
|
echo " httpplatformhandler/ httpPlatformHandler_amd64.msi"
|
|
echo " urlrewrite/ rewrite_amd64.msi (client-IP rule; see README)"
|
|
echo " vcredist/ VC_redist.x64.exe (MySQL requires it)"
|
|
echo " mysql/ mysql-8.4.x-winx64.msi (bundled-database option only)"
|
|
echo ""
|
|
echo " If the payload changed ON PURPOSE, regenerate and COMMIT the lock:"
|
|
echo " pwsh ./refresh-bundle-lock.ps1 # review the diff"
|
|
echo " pwsh ./refresh-bundle-lock.ps1 -Yes # write it"
|
|
echo ""
|
|
echo " To stage a bundle before its lock exists: ALLOW_UNLOCKED=1 $0 ..."
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "Then compile on Windows: iscc ShopDBFlask.iss"
|
|
echo "(Inno Setup 6.6.0 or newer - the wizard uses the windows11 custom style.)"
|