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 |
|
|
git 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 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 --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.