The exe is attached to a release, never committed: most forges reject files over 100 MB inside a repository while allowing release assets far larger, and a committed binary would sit in every future clone forever. Also records that tags must be pushed explicitly. A plain push of the branch does not carry them, so a release had nothing to hang off.
203 lines
8.3 KiB
Markdown
203 lines
8.3 KiB
Markdown
# Building and releasing the Windows installer
|
|
|
|
For whoever builds releases. If you run a server rather than build releases, see
|
|
[UPDATES-WINDOWS.md](UPDATES-WINDOWS.md).
|
|
|
|
Every release is one self-contained `.exe`. It carries the Python runtime, a
|
|
hash-checked wheelhouse, MySQL, the IIS modules and the application, so nothing
|
|
is fetched from the network at install time. That is the point: sites are
|
|
air-gapped.
|
|
|
|
## What you need
|
|
|
|
- A checkout of this repository.
|
|
- Inno Setup 6.6.0 or newer, on Windows. Compiling needs Windows; staging the
|
|
bundle does not.
|
|
- PowerShell, for the bundle lock tools.
|
|
- `uv`, only when dependencies change.
|
|
|
|
## The three kinds of change
|
|
|
|
Almost every release is the first kind.
|
|
|
|
### 1. Application change: bug fix, feature, no new dependency
|
|
|
|
```bash
|
|
# bump __version__ in shopdb/__init__.py first
|
|
./deploy/windows/installer/build-installer.sh deploy/site-profile-universal.json <repo-path>
|
|
```
|
|
|
|
`deploy/site-profile-universal.json` is the profile released builds come from:
|
|
every bundled plugin, so one `.exe` serves any site and the operator ticks what
|
|
that site uses. Build from a narrower profile only when a site genuinely needs a
|
|
lean build (ADR-013). Do not build a release from a profile that is not in the
|
|
repository - the build stops being reproducible the moment that file is
|
|
somewhere else.
|
|
|
|
Then on Windows, in `deploy/windows/installer`:
|
|
|
|
```
|
|
iscc ShopDBFlask.iss
|
|
```
|
|
|
|
`build-installer.sh` restages the application, rebuilds the web interface for
|
|
the chosen feature set, regenerates `plugins.iss` and `version.iss`, and
|
|
re-verifies the third-party payload against `bundle-lock.json`. The lock is
|
|
untouched, because nothing third-party changed.
|
|
|
|
### 2. A dependency is added, removed or moved
|
|
|
|
The only case that touches the lock.
|
|
|
|
```bash
|
|
# edit requirements.in, then
|
|
uv pip compile requirements.in --universal --generate-hashes -o requirements.txt
|
|
|
|
# fetch the wheel for the target runtime, into the wheelhouse
|
|
pip download <name>==<version> -d deploy/windows/installer/bundle/wheels \
|
|
--only-binary=:all: --platform win_amd64 --python-version 314 --no-deps
|
|
```
|
|
|
|
Then regenerate and commit the lock:
|
|
|
|
```powershell
|
|
pwsh ./refresh-bundle-lock.ps1 # review the change
|
|
pwsh ./refresh-bundle-lock.ps1 -Yes # write it
|
|
```
|
|
|
|
**Commit `bundle-lock.json`. That commit is the review.** The lock records every
|
|
third-party file and its SHA-256; the installer refuses to run if the payload it
|
|
carries does not match, so an unreviewed substitution cannot reach a server.
|
|
|
|
Two traps, both of which have already cost a release:
|
|
|
|
- `--universal` is not optional. A resolve done only for the host platform drops
|
|
packages that exist only on Windows, and the install then fails hash checking
|
|
on a package with no entry.
|
|
- Declare **every** runtime import in `requirements.in`, including ones that
|
|
already work locally. `packaging` reached this project only as a test
|
|
dependency, so the full suite passed while a production virtual environment,
|
|
which has no test dependencies, could not import the application at all.
|
|
`tests/test_runtime_dependencies.py` is the gate for this.
|
|
|
|
### 3. A new plugin
|
|
|
|
Ordinary plugin work, plus three installer-specific steps:
|
|
|
|
1. **Stage it.** Add the plugin to the profile you build from. `plugins.iss` is
|
|
generated from what is actually in the bundle, so the wizard offers it with
|
|
no edit to the installer script.
|
|
2. **Decide whether it is ticked by default.** `PluginDefault()` in
|
|
`ShopDBFlask.iss` is an exclusion list: a new plugin defaults to **ticked**
|
|
unless you name it there. This is the one manual edit.
|
|
3. **Register its migrations.** Update `PLUGIN_TABLE_OWNERS` and
|
|
`EXPECTED_HEAD_REVISION`, and give the plugin its own migration chain
|
|
(ADR-008). Existing sites pick up its tables through `plugin upgrade-all`;
|
|
sites that do not tick it have them pruned (ADR-014).
|
|
|
|
## Version numbers
|
|
|
|
`build-installer.sh` reads `__version__` from `shopdb/__init__.py` and writes
|
|
`version.iss` from it. Do not edit `version.iss`: a hardcoded version in the
|
|
installer script had already drifted two minor versions from the code.
|
|
|
|
Bump the version for every release you hand out. The installer compares versions
|
|
and:
|
|
|
|
- refuses a build **older** than what is installed, because migrations only go
|
|
forwards;
|
|
- warns and continues on an **equal** version, treating it as a repair.
|
|
|
|
Equal-version rebuilds are useful while testing and are a poor idea in the
|
|
field, since the server cannot then tell you what it is running.
|
|
|
|
Follow ADR-007 for what each part means.
|
|
|
|
## Do not hand-edit generated files
|
|
|
|
Regenerate these; changes are overwritten without warning:
|
|
|
|
- `version.iss` - from `shopdb/__init__.py`
|
|
- `plugins.iss` - from the plugins actually present in the bundle
|
|
- `requirements.txt` - from `requirements.in` via `uv pip compile`
|
|
- `bundle-lock.json` - via `refresh-bundle-lock.ps1`
|
|
|
|
`waitress` and `tzdata` were once hand-added to `requirements.txt` and vanished
|
|
on the next compile, taking the Windows runtime with them.
|
|
|
|
## Before you hand a build out
|
|
|
|
```bash
|
|
python -m pytest -q # full suite
|
|
./scripts/check-naming-and-style.sh # naming and style
|
|
```
|
|
|
|
The suite includes gates worth knowing about:
|
|
|
|
- `tests/test_runtime_dependencies.py` - every runtime import is declared
|
|
- `tests/test_bundle_lock.py` - the lock covers the payload
|
|
- `tests/test_docs_publishable.py` - `docs/` carries no internal references,
|
|
because it is published
|
|
|
|
`build-installer.sh` refuses to finish if the payload does not match the lock.
|
|
That check is not advisory; do not work around it.
|
|
|
|
## Publishing
|
|
|
|
Alongside the `.exe`, publish:
|
|
|
|
- a `.sha256` file, so the operator can verify what they received;
|
|
- release notes covering what changed and anything needing attention;
|
|
- the version in the filename, so a server's build is identifiable on sight.
|
|
|
|
The installer is attached to a git-forge release as an ASSET, never committed.
|
|
Most forges reject files over 100 MB inside a repository while allowing release
|
|
assets far larger, and a committed binary would sit in every future clone
|
|
forever. Release creation is scripted next to the other deployment scripts and
|
|
verifies the SHA-256 before publishing, because a truncated upload is easiest to
|
|
catch before anyone can download it.
|
|
|
|
Tags have to reach the forge for a release to hang off one: a plain
|
|
`git push <remote> main` does not carry them, which is why the push step uses
|
|
`--follow-tags`.
|
|
|
|
The compiled installer stages a CycloneDX inventory (`sbom.cdx.json`) onto every
|
|
server, which is what answers a security question about a published
|
|
vulnerability without anyone guessing.
|
|
|
|
## Known gaps
|
|
|
|
**The installer is not code-signed.** Every install shows an unknown-publisher
|
|
warning, and the SHA-256 is the only integrity check. This is the significant
|
|
remaining gap before wider distribution: a checksum published next to the file
|
|
protects against corruption, not against someone who can write to that location.
|
|
|
|
The decision taken is to wait for a certificate from the organisation's own
|
|
certificate authority rather than buy one from a public CA. Every server this
|
|
installer runs on is centrally managed, and that CA's root is already trusted on
|
|
those machines, so an internally issued Authenticode certificate removes the
|
|
warning exactly where it matters. A public certificate would buy trust on
|
|
machines this software never reaches.
|
|
|
|
Until then, publish the SHA-256 through a channel SEPARATE from the installer
|
|
itself. A hash sitting beside the file is only as trustworthy as write access to
|
|
that location; a hash the operator gets another way means tampering has to
|
|
succeed twice.
|
|
|
|
Wiring it up afterwards is small: Inno has native SignTool support, so a
|
|
directive in the script and a signtool configuration on the build machine sign
|
|
the installer and its uninstaller. Include a timestamp server, or signatures
|
|
stop verifying when the certificate expires.
|
|
|
|
**Compiling requires Windows.** `build-installer.ps1` exists so the whole
|
|
process can run on a Windows workstation. Nothing about it runs in CI, so a
|
|
release is a deliberate act by a person.
|
|
|
|
## See also
|
|
|
|
- [UPDATES-WINDOWS.md](UPDATES-WINDOWS.md) - what operators should expect
|
|
- [INSTALL-WINDOWS.md](INSTALL-WINDOWS.md) - installing a new site
|
|
- [OPERATE-WINDOWS.md](OPERATE-WINDOWS.md) - running a site
|
|
- `docs/adr/` - ADR-007 versioning, ADR-008 plugin migrations, ADR-013 and
|
|
ADR-014 lean per-site builds
|