Waiting for a certificate from the organisation's own certificate authority rather than buying one from a public CA. Every server this installer runs on is centrally managed and already trusts that root, so an internally issued Authenticode certificate removes the unknown-publisher warning exactly where it matters; a public certificate would buy trust on machines this software never reaches. Notes the interim measure that costs nothing: publish the SHA-256 through a channel separate from the installer, since a hash beside the file is only as trustworthy as write access to that location. Wording avoids naming internal infrastructure, since docs/ is published.
185 lines
7.3 KiB
Markdown
185 lines
7.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 <site-profile.json> <repo-path>
|
|
```
|
|
|
|
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 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
|