# Packaging and releases One repository, two distributions on PyPI, one tag. | Distribution | Contains | Built by | Installed with | |---|---|---|---| | **`plesty-bench`** | `plesty/bench` — the client | the project's own `pyproject.toml` (`hub-release` of `plesty-standard-ci`) | `uv tool install plesty-bench` (developer machine) | | **`plesty-server`** | `plesty/server` — the bench application (CLI, console, agent); depends on `plesty-bench==`, `plesty-lib`; extra `gui` = `plesty-lib[gui]` | `packaging/server_dist.py` in `release:server-wheel` | `uv tool install "plesty-server[gui]"` (bench) | Tagging `vX.Y.Z` on `main` publishes both through PyPI trusted publishing (OIDC — GitLab namespace `plesty/core`, project `plesty-bench`, `.gitlab-ci.yml`), one publisher per distribution told apart by the job's GitLab environment (`pypi-bench`, `pypi-server`), and creates the GitLab Release. ## Building the Windows installer `packaging/windows/build-installer.ps1` has two modes, and the difference is what the wizard installs: ```powershell powershell -ExecutionPolicy Bypass -File .\packaging\windows\build-installer.ps1 -Mode Test powershell -ExecutionPolicy Bypass -File .\packaging\windows\build-installer.ps1 -Mode Release -Version 0.2.5 ``` The `-ExecutionPolicy Bypass` is not optional on a default box: an unsigned `.ps1` will not run otherwise. And every `.ps1` here is **ASCII only** — Windows PowerShell 5.1 reads a file without a BOM as cp1252, where the third byte of an em-dash decodes to a smart quote, which PowerShell accepts as a string delimiter. One em-dash in a comment silently opens a string and the file stops parsing. **Release** is what ships: the wizard runs `uv tool install "plesty-server[gui]=="` against PyPI, so the version must be a real release — the Inno script refuses a `.dev` or `+local` version outright. `install.sh` has the same two modes on macOS and Linux — `--mode=test` builds both wheels from the checkout and installs those, `--mode=release` (the default) installs from PyPI — plus `--launcher-only` for a build that is already installed some other way. **Test** is how a branch is put through the real wizard: the version is the working tree's (`0.3.0.dev3+0ca7c6c`), both distributions are built from the tree, and the **wheels are carried inside the installer**, so what gets installed is the code in front of you. The output is named `…-TEST-…` and every wizard page says so. That distinction is not cosmetic. The wizard is a process context of its own — a process descended from `setup.exe` cannot traverse a directory junction, so `uv` behaves differently under it than under a shell — and a defect of that kind can only be reproduced by running the real installer. Without a test mode the only way to exercise the wizard was to ship a release first. ## The light installers The installers carry **no application code**. They install `uv` if it is missing and run `uv tool install "plesty-server[gui]=="`, which downloads the application and its dependencies from PyPI into uv's tool environment; then they make the application visible: | Platform | Installer | What it adds | |---|---|---| | Windows | `plesty-server--windows-setup.exe` — Inno Setup (`packaging/windows/plesty-server.iss`), built by `packaging/windows/build-installer.ps1`, ~2 MB, per-user, no admin | Start-menu entries (*PLESTY Bench*, *console*, *Update*, *Uninstall*), optional desktop shortcut, uninstaller in "Apps & features" | | macOS | `curl -LsSf \| sh` (`packaging/install.sh`; `--launcher-only` makes just the entry, for a build installed some other way) | `~/Applications/PLESTY Bench.app` — a launcher with the mark as icon, so it appears in Launchpad | | Linux | the same `install.sh` | a `.desktop` entry in the application menu | `plesty-server` / `plesty-server-gui` land on uv's bin dir (`~/.local/bin`); the console remembers the last fleet it used (`/last-fleet`) so a double-click reopens it. Updating a bench is `uv tool upgrade plesty-server` (Start-menu entry *Update PLESTY Bench* on Windows). ### The pinned version Both installers install the release they were built for, never *latest* and never a development version — a bench is exactly the release it downloaded, and `uv tool upgrade plesty-server` is the only thing that moves it forward. The pin must be a version that exists on PyPI, so it comes from the tag, not from the working tree: `installer:windows` derives it from `$CI_COMMIT_TAG`, and `plesty-server.iss` refuses to compile when it carries `.dev` or a `+` local segment (what versioningit yields off a tag — `0.2.3.dev7+abc1234`, `0.2.2+modified` — none of which are published). `release:assets` stamps the same version into `install.sh` in place of `@PLESTY_VERSION@`; run from a checkout the placeholder survives and the install is unpinned. `PLESTY_SERVER_VERSION` overrides it, and `release:server-wheel` fails if the version it publishes is not the tag. An `installer/` branch builds a test installer against the last release (or `INSTALLER_VERSION`), so the wizard can be tried end to end before tagging. ### Signing Unsigned installers are refused by name: Windows SmartScreen says *"Windows protected your PC — Unknown publisher"*, macOS Gatekeeper *"unidentified developer"*. Both need a paid identity; the build is wired for it but ships unsigned until one exists. | | What removes the warning | Wired | |---|---|---| | Windows | An Authenticode certificate. `installer:windows` passes Inno's `SignTool` when `WINDOWS_SIGN_COMMAND` (protected CI variable, a `signtool` command line with Inno's `$f` placeholder) is set, and signs the uninstaller too. OV certificates still accumulate SmartScreen reputation over the first downloads; EV is trusted immediately. | yes — set the variable | | macOS | A Developer ID Application certificate (Apple Developer Program) plus notarization with `notarytool` and a stapled ticket. This needs a real signed bundle shipped from CI rather than the launcher `install.sh` writes on the machine. | no | `install.sh` does clear `com.apple.quarantine` from the launcher it writes and ad-hoc signs it, so a locally built bundle opens by double-click — the bundle is created on the machine at the user's request, and the flag it inherits from the downloaded script is what Gatekeeper would otherwise object to. That is not a substitute for Developer ID on anything downloaded as an app. ## The CI jobs on a tag | Job | Does | |---|---| | `hub-release` (template) | publishes `plesty-bench` | | `release:server-wheel` | `server_dist.py` → publishes `plesty-server` (same version, same trusted publisher) | | `installer:windows` | `saas-windows-medium-amd64`: `.ico` from the mark (Pillow), Inno Setup via chocolatey → `setup.exe` | | `release:assets` | stamps the tag into `install.sh`, uploads it and `setup.exe` to the generic package registry (permanent) and creates the Release: installer, install script, both PyPI links | ## What the bench still needs `uv` (the installers install it) and `git` for modules installed from a repository; Python is not needed — `uv` manages interpreters for the tool environment and for every device environment. ## Re-executing itself — `plesty/server/reexec.py` Jobs and the detached launcher are `plesty-server` processes too: from a tool environment or a checkout that is `python -m plesty.server …` / `python -m plesty.server.services.launch` with the environment's interpreter. `reexec` is the one module that would also know a frozen bundle, should one ever be built again. ## Headless bench: `fleet run` ```bash plesty-server fleet run --agent # bring the fleet up, keep it up, serve plesty-bench clients ``` `Bench.run()` brings the autostart devices up, then sweeps every 5 s: a device found `exited` is started again after a backoff that doubles per consecutive crash (1, 2, 4 … 60 s) and resets once it answers a probe; devices stopped on purpose are left alone. `SIGINT`/`SIGTERM`/`SIGHUP` (Ctrl-C, a service stop, the terminal closing) end the loop and — unless `--leave-running` — bring the servers down, so a service manager stopping `fleet run` stops the bench cleanly. `--agent` serves `plesty-bench` clients from the same process.