Roughly 2500 lines of tested installer had been living in ~/Downloads and an untracked folder - nothing was under version control. It goes here rather than in a repo of its own because it depends on application internals: the `flask plugin` verbs, site-profile.json, MOUNT_PATH, and the plugin registry. Versioned separately it would drift out of step with the thing it installs. Contents: the read-only preflight, the staged installer (bundled MySQL, runtime, schema, IIS, verify, uninstall), the operator console, the Inno Setup wizard, the bundle builder and the artwork generator. bundle/ and Output/ are ignored - regenerable, and ~220MB. plugins.iss is ignored because build-installer.sh generates it from the staged payload. The artwork IS committed so a Windows build box does not need Python and cairosvg. Verified end to end on Windows Server 2025 against a bundled MySQL 8.0 and an existing MySQL 5.6: fresh install, upgrade with backup and rollback, re-run idempotency, uninstall, and both deployment methods including switching between them. Not yet verified: a hypervisor-level air-gapped run, and any load from a real browser (every HTTP check so far used curl, which sends no Origin header).
Windows installer
Builds a single self-contained .exe that installs ShopDB-Flask on an
air-gapped Windows Server. Nothing here ever touches the network at install
time: Python, the wheels, the SPA and (optionally) MySQL all ship inside it.
Lives with the application on purpose. The installer depends on app internals -
flask plugin verbs, site-profile.json, MOUNT_PATH, the plugin registry -
so a separate repo would drift out of step with the thing it installs.
Files
| File | What it is |
|---|---|
shopdb-preflight.ps1 |
Stage 1. Read-only. Changes nothing, reports what this server is missing. |
shopdb-install.ps1 |
Stages 0 and 2-5 plus uninstall. All the actual work. |
shopdb-admin.ps1 |
Operator console installed alongside the app: status, restart, logs, backup, plugins. |
ShopDBFlask.iss |
Inno Setup wizard. A thin wrapper - it collects input and runs the stages. |
build-installer.sh |
Stages the bundle from a site profile. |
make-branding.py |
Generates wizard artwork and the icon from frontend/public/*.svg. |
*.bmp, shopdb.ico |
Generated artwork, committed so a Windows build box needs no Python. |
Building
# 1. Stage the bundle for a site's plugin set.
./build-installer.sh ../../site-profile.example.json
# 2. Add the pieces that cannot be built on Linux:
# bundle/wheels/ ~47 cp314 win_amd64 wheels, built ON Windows:
# pip download -r requirements.txt -d wheels --only-binary=:all:
# bundle/python/ python-3.14.x-amd64.exe
# bundle/httpplatformhandler/ httpPlatformHandler_amd64.msi
# bundle/mysql/ mysql-8.0.x-winx64.msi (bundled-database option only)
# 3. Compile on Windows.
iscc ShopDBFlask.iss
The wheelhouse is cp314-locked. A different Python minor version means a different wheelhouse; the installer will not use a Python it did not install.
Deployment methods
Chosen in the wizard, and the bundle carries a SPA build for each because Vite compiles the base path in - it cannot be switched at install time.
- Its own site on a port (default 8090).
- Subpath under an existing site, e.g.
http://<server-fqdn>/shopdb/. Needs no new DNS record. Three things must agree - the IIS application alias,MOUNT_PATHin.env, and the SPA's build-time base - so the alias is fixed per bundle (SUBPATH_ALIAS, defaultshopdb) and the installer refuses if the bundle's build does not match what was asked for.
Switching between methods removes the other one's IIS artifact and reconciles
MOUNT_PATH and CORS_ORIGINS, so a server never ends up with both.
Upgrades
Run a newer installer over an existing install. It:
- backs the database up first, verifies the dump is complete, and refuses to migrate if it cannot;
- restores from that backup if migrations fail, and reports honestly that DDL the failed migration committed cannot be undone;
- refuses to run a bundle older than what is installed;
- keeps
.envunless new credentials are supplied, and copies it aside first; - stops the app pool before replacing files, then starts it again.
Whether a run is an upgrade is decided by probing the target database, not by whether the app directory exists - a rebuilt server pointed at an existing database is an upgrade, and treating it as fresh would drop tables.
Testing notes
Verified end to end on Windows Server 2025 against both a bundled MySQL 8.0 and an existing MySQL 5.6: fresh install, upgrade, re-run idempotency, failure and rollback, uninstall, and both deployment methods including switching between them.
Not yet verified: a fully air-gapped run with the network disabled at the
hypervisor, and any load in a real browser (all HTTP checks so far used curl,
which sends no Origin header - so CORS_ORIGINS is untested in anger).