Files
shopdb-flask/deploy/windows/installer/README.md
cproudlock 0fa5f1e910 feat(deploy): add the air-gapped Windows installer
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).
2026-08-03 01:47:34 -04:00

84 lines
3.8 KiB
Markdown

# 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
```bash
# 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_PATH` in `.env`, and the SPA's build-time base - so the alias is fixed
per bundle (`SUBPATH_ALIAS`, default `shopdb`) 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 `.env` unless 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).