#!/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 [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 [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" < 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" < 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.)"