Installing device modules
A bench holds several versions of a module side by side. A pinned instance keeps running the release it was verified with while a newer one is installed next to it; rolling back is pointing the instance at the version that is still there.
Layout
Source |
Environment |
Record |
|---|---|---|
PyPI release |
|
|
git checkout |
|
|
Two instances pinned to the same release share one environment. Two
instances of the same package at different versions do not interfere.
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; 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 spec?
a PyPI spec is served by the pinned version (any PyPI version when unpinned — the newest install wins);
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/version, vendor, description. It is learned
once — from the catalogue when a module is installed there, from any git
install (the URL), or when the catalogue 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 catalogue is thereby linked to one
repository, and its releases to the same project.
The installer — Installer
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 topackages/<package>/<version>/. Staging first is what lets an unpinned spec end up under the version it resolved to, and--relocatableis what makes the move safe.git — clone (or fetch)
repos/<package>/<ref>/, check out the ref, fast-forward when on a branch,uv sync. 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
env 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 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 the presenter asks the
hub for it first (Bench.learn_repository, one lookup, remembered). 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 the retry and not the
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 (the catalogue prefers a release, which needs no
git on the bench), but the developer loop from afar needs the repository:
plesty check, tests/field_test.py and plesty init field-test run
inside the checkout. A release is what a finished module gets; a checkout
is what a module under development gets.