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.

Layout

Source

Environment

Record

PyPI release

<home>/packages/<package>/<version>/ — a uv venv

<home>/packages/<package>/<version>.json

git checkout

<home>/repos/<package>/<ref>/ — a clone; its .venv (from uv sync) is the environment

<home>/packages/<package>/git-<ref>.json, with path pointing at the checkout

Records are keyed by slot: the version for a release, git-<ref> 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:

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, <home>/packages/<package>/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 <requirement>, read the version that actually landed (uv pip list --format json), then move the directory to packages/<package>/<version>/. 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/<package>/<ref>/, 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 <home>/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 <home>/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<version>, 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<version> (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.