# The installer A bench holds several versions of a module side by side, and a declaration has to be matched to one of them before anything can start. This page is the mechanism behind [Install and update modules](../usage/modules.md). ## Layout | Source | Environment | Record | |---|---|---| | PyPI release | `/packages///` — a `uv` venv | `/packages//.json` | | git checkout | `/repos///` — a clone; its `.venv` (from `uv sync`) is the environment | `/packages//git-.json`, with `path` pointing at the checkout | Records are keyed by *slot*: the version for a release, `git-` for a checkout. A checkout moves with every pull, so it is kept by the ref it tracks rather than by a commit — and two refs at the same commit never collide. ## The inventory — `DeviceManager` The model side only keeps the books. `InstalledVersion` is one record: ```python InstalledVersion(package="plesty-pm100d", version="0.2.1", source="pypi", python="3.12.4") InstalledVersion( package="plesty-pm100d", version="0.4.0.dev3+abc1234", source="git", git="https://…/plesty-pm100d.git", ref="exp", commit="abc1234", path="~/.plesty/server/repos/plesty-pm100d/exp", ) ``` `DeviceManager.resolve(spec)` is the question the presenter asks before starting anything: *is there an installed version that serves this declaration?* - a PyPI spec is served by the pinned version — or, unpinned, by any PyPI version, the newest install winning; - a git spec is served by an install from the same repository at the same ref (or commit prefix); - the two never cross: a checkout never serves a PyPI spec and vice versa. `None` means *install first*. ## The package record — `PackageInfo` Beside the install records, `/packages//package.json` keeps what the bench knows about the module *as a project*: its repository URL, project page, latest release tag and version, vendor, description. It is learned once — from the Hub when a module is installed there, from any git install (the URL), or when the Hub is consulted — and never asked again. The declaration form's URL/ref/version drop-downs, the `.env.example` fetch for release installs, and the project link shown under a device all read it. A package installed from the Hub is thereby tied to one repository, and its releases to the same project. ## `Installer.install(spec)` Makes an environment exist and returns its record: - **PyPI** — `uv venv --relocatable --python ">=3.12"` into a staging directory, `uv pip install --upgrade `, read the version that actually landed (`uv pip list --format json`), then move the directory to `packages///`. Staging first is what lets an *unpinned* spec end up under the version it resolved to, and `--relocatable` is what makes the move safe. - **git** — clone (or fetch) `repos///`, check out the ref, fast-forward when on a branch, `uv sync --no-dev` — the module's authoring toolchain (pytest, mypy, plesty-sdk) is not what a bench runs, and every console script it would add is one more file an on-access scanner can hold during the install. The record stores the commit and the checkout path. A pulled branch reports a new version, so the previous record of that checkout is dropped and replaced. `install()` is a no-op when `resolve()` already finds a satisfying record whose interpreter still exists; `force=True` reinstalls. `update(installed)` is "install again, in place": a git checkout is pulled and re-synced, a PyPI environment reinstalled at the same version. `uninstall(installed)` removes the directory and forgets the record. `uv` and `git` are the only tools used, driven through an injectable `Runner`; every line they print goes to an `Echo` — the job log when the install runs as a job, which is how the GUI shows the progress of a minutes-long install instead of silence. ## Where `git` comes from Neither tool has to be on `PATH`. `uv` is looked up there; `git` is looked up there, then where Windows puts it (`%ProgramFiles%\Git\cmd`, `%LOCALAPPDATA%\Programs\Git\cmd`), then under `/tools/git`. A Windows bench that has none gets one: `gitbin.install_git` downloads the portable MinGit build pinned in `config.yaml`, checks it against the pinned SHA-256, refuses any archive member that would write outside the target, and unpacks it into `/tools/git` — no installer and no administrator. `PLESTY_SERVER_AUTO_GIT=0` turns that off and reports instead. A POSIX bench is told to install git from its package manager: system-wide is the right place there. ## When the release is not on the index A module is on the hub before it is on PyPI: the registry knows a release from its **tag**, and the package reaches the index only once the release job has run. So a release install that finds nothing is a routine state of a young module, not a broken bench — and the same version is in the repository, tagged `v`, which is the ref a spec with a version already resolves to. `Installer.install` therefore falls back: when a release install fails and the package record knows a repository, it installs that repository at `v` (or `latest_tag` when the spec named no version) and says so in the log. A bench that has never seen the module knows no repository — which is precisely the first install of a young module — so `module install` asks the hub for it first (`Bench.learn_repository`, one lookup, remembered). `install NAME` and `start` do not: they go straight to the installer, so on a bench that has never seen the module they get no fallback. Without a known repository the release failure stands as it is, and when the tag fails too the error carries both reasons — so a bench that is simply offline does not report a confusing git error for a PyPI problem. ## Pre-release dependencies A module under development depends on a `.dev` release of plesty-lib, and `uv` resolves only stable versions unless it is told otherwise — it fails with a hint naming the pre-release. The installer retries that one command with `--prerelease=allow` and says so in the log. It is a retry and not a rule, so an install with a stable answer still gets the stable one. **Whether it fires depends on the bench's `uv`.** For a requirement whose own specifier names a pre-release (`plesty-lib>=0.3.5.dev3`), uv 0.11.10 refuses and the retry rescues the install; uv 0.12.5 resolves it first time and the retry never runs. Both benches end up with the same environment — one of them says so in its log. So the retry is not dead code: it is what keeps an older uv working, and it still fires on any uv for a specifier that does *not* name the pre-release it needs. ## Why a checkout and not just a wheel Hub devices publish to PyPI, and the Hub prefers a release because it needs no git on the bench. But the developer loop from afar needs the repository: `tests/field_test.py` runs *inside the checkout*, with its environment. (The authoring tools — `plesty check`, `plesty init field-test` — are left out of that environment by `--no-dev`, and belong on the developer's own checkout.) A release is what a finished module gets; a checkout is what a module under development gets.