pm/ is bundled package-manager code outside this lane. pm.receipt.latest()
(hermes pm status) reads the same dir pm writes, so nothing is lost: the
updater embeds the pm sync section in memory (last_for_update) and its own
receipts stay under the root home.
The writers moved to the root home; the readers did not. `hermes logs update` /
`handoff`, `hermes logs list`, the debug bundle, the dashboard's durable
update-completion marker, the post-swap hand-off file and pm's sync receipts
(same update_receipts dir) now use get_default_hermes_root(), so a sticky
profile no longer hides the update log.
An install still on its legacy 3.11 venv runs its first `hermes update`
from that interpreter, which downloads uv and Python 3.14 with plain
urllib before PM's worker (and truststore) exists. Windows can cache Let's
Encrypt "Root YR" in its intermediate store without ISRG Root X1; GitHub's
asset CDN now chains through Root YR, so 3.11 rejected every download with
"unable to get issuer certificate" while PowerShell and 3.14 accepted it.
Python 3.13+ sets VERIFY_X509_PARTIAL_CHAIN by default. Set it on the PM
downloader's opener so the legacy interpreter applies the same rule;
hostname, expiry and pinned sha256 checks are unchanged.
Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
Follow-up to the salvaged per-package pin isolation (#125400). Once a
failing pin no longer aborts the run, the surviving pins still run the
venv sync, whose receipt finalizes `ok` as latest.json. `hermes pm
status` and the desktop sync card read latest.json, so a run that left a
package on its old pin and exited 1 was recorded as a clean success.
Write a kind=update `failed` receipt after the sync, with one failed step
per pin, whenever any pin failed (also when every pin failed, which
previously wrote no receipt at all). Trim the tests to the two
invariants and assert the receipt in the mixed case.
_apply_pins let any exception from _pin_artifacts propagate: one broken
pin - a package's resolution bug (gh's 3-part target split) or one
target's upstream pool skew (Termux rolling pool 404 while nodejs.org
is ahead) - aborted the entire run, blocked every unrelated package,
and left lockfile.save() unreached, so any failure meant zero progress.
Catch per package, pin everything that succeeds, install the pinned
set, print a 'pinned X, failed Y' summary, and exit non-zero when
anything failed. A run where every pin fails still leaves the lockfile
untouched.
Fixes#125386
flatten_single_dir renames a tree extracted seconds earlier, while
Defender or another scanner still holds it, and ffmpeg/agent-browser
installs died on the package/bin rename with [WinError 5] (#131884).
The entry displace/restore renames in install.py had the same bare call.
pm.filesystem.retry_held retries EACCES/EPERM/EBUSY for about 3s and
raises anything else immediately; the hoist, merge_tree and the entry
swaps use it.
Co-authored-by: bighamx <10262931+bighamx@users.noreply.github.com>
Co-authored-by: lzmuhioin <1814826+lzmuhioin@users.noreply.github.com>
The root file is project context and was 38.7k chars, past the context-file budget
(20k floor, 30,720 on a 128k window), so every session lost its middle: routing table,
testing rules, code-shape rules. It now keeps the two invariants, a condensed rubric
(every rule kept), environment, structure, one-line rules that apply everywhere and the
routing table (11.8k).
Moved verbatim to where they load: Testing -> new tests/AGENTS.md; dependency pinning and
PM ownership -> new pm/AGENTS.md; machine facts -> new hermes_platform/AGENTS.md;
session-scoped tool availability -> tools/AGENTS.md; the full rubric with its real-close
examples -> developer-guide/contributing.md. Pointers in lint, checker messages, docs
and test comments name the new homes.
Moves llamacpp-cpu, -cuda, -vulkan, -hip and -metal from b11146 to
b11370 (bed0a8566). Only digests change: every pinned asset keeps its
name, no command-line option or server route Hermes uses went away, and
the Windows cudart zips are byte-identical, so they keep their digests.
Installed b11146 engines read as outdated and offer the update.
The Linux builds keep the same glibc floors (2.34 for x64 CPU/Vulkan,
2.38 elsewhere), so the bundled jammy libgomp still covers them.
The libgomp .deb pinned for Linux engines came from Ubuntu 24.04 and
needs glibc 2.38. llama-server's $ORIGIN RUNPATH loads that copy ahead
of the host's, and the dynamic loader stops at the missing symbol
version, so the x64 CPU and Vulkan engines (glibc 2.34, built on 22.04)
failed PM's --version verify on Ubuntu 22.04, Debian 12, RHEL 9 and
Amazon Linux 2023.
Ubuntu 22.04's libgomp1 (gcc-12 12-20220319-1ubuntu1) needs glibc 2.34
and provides GOMP_4.5, the newest version any b11146 engine references.
The arm64, CUDA x64 and ROCm x64 builds already need glibc 2.38 from
upstream.
Verified through pm.install.ensure() in containers: the old pin fails
verification on Ubuntu 22.04; the new pin installs and runs on Ubuntu
22.04/24.04/26.04, Debian 12/13/testing and Fedora (x64, CPU and
Vulkan) and on Ubuntu 26.04 arm64, loading libgomp from the engine
directory.
run_completion runs in the OLD process after the checkout swap, and its
contract is never to import application helpers from the replacement tree.
Importing pm.libatomic there crashed the update when the new tree lacked
the module (downgrade or older pin) and on architectures where
current_target() raises. Run the pre-install as its own process from the
new tree, ignore its outcome, and let the in-lock repair report.
Also: gate on an openable /dev/tty first (cheapest check, and the detached
child exits immediately), treat an unsupported architecture as "skip",
bound the sudo -n probe, and drop the timeout on the interactive install
so a slow prompt or mirror is never killed mid-dpkg (it runs outside PM's
lock). Found in the final-head review of #132664.
hermes update's completion runs PM in a start_new_session child with no
controlling terminal, so the in-child sudo -v warm-up could never read a
password and the cold password-sudo update from #124926 still failed. A
cached ticket was also the wrong currency: under sudo's default per-tty
timestamps a ticket minted in one session is not visible to another.
Install the package itself, interactively, at the last point that still
owns the terminal: run_completion() before it detaches, plus the existing
ensure_tools_for_sync() entry for the other update paths. Gate on an
openable /dev/tty instead of isatty() (a detached child inherits a tty
stdin and a forwarded-isatty stdout). Inside PM's lock the repair is
unchanged: sudo -n, stdin closed. Reported by @JoaoMarcos44 on #132664.
The update warm-up resolved sudo on its own, so on a glibc host with no
supported package manager it still asked for a password the repair then
never used. Derive the prompt from the same _host_install_command and
_command_plan the repair runs, compute the target once, and correct the
module docstring now that there are two entry points.
The libatomic repair can only use sudo -n under PM's install lock, and
hermes update enters PM directly without install.sh's prerequisites
warm-up, so a cold password-sudo host still failed the update from
#124926. Before publishing tools, ask once on an interactive glibc host
that still lacks libatomic.so.1 and whose sudo would prompt. Reported by
@JoaoMarcos44 on #132664.
The hook glued the remedy into the reason, so the error ended with two
fixes, the second ("retry, or run hermes pm doctor") wrong for a missing
host library. Return (reason, remedy) and pass the remedy through. When the
package installs but Node still cannot load it, say that instead of telling
the user to run the command that just succeeded.
Every root that depends on node (npm, browser tools) re-stages it after a
failure, so a host without libatomic ran the package manager under PM's
install lock once per dependent root. Cache the outcome for the process.
Also use platform.freedesktop_os_release() instead of a hand parser, and
run apt-get update once when a minimal image has empty package lists.
facts.json under a candidate root only says that root synced dependencies
for the checkout, and a borrowing launch writes that too. Ranking the root
the checkout sits in first fixed a default-root borrower of a custom-root
checkout but not the reverse: a checkout the default root keeps in
`install.sh --dir <dir>`, launched with HERMES_HOME=<dir>, made <dir> the
owner after its first sync, so the next launch rebound the shared launchers
to <dir>'s interpreter.
Only the owner publishes the checkout's launcher, and it execs the owner's
store Python. Among the roots holding state, the one whose tools/ holds that
live interpreter is the owner. Without a live launcher, the parent root
still outranks the platform default.
Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
owning_home_root() treated any candidate root holding facts.json as the
owner and returned None when the active root was one of them. A checkout
owned by a custom root (`install.sh --hermes-home <custom>`) and launched
from the platform default root borrowed correctly once; that launch's own
sync then wrote <default>/installs/<key>/facts.json, and every later launch
took the default root for the owner: its own tool store, launcher
publication and update tail, the behaviour this layer exists to stop.
The root the checkout sits in now outranks the platform default, and only
that one owner is compared with the active root.
Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
Dependency state is scoped per data root (<root>/installs/<key>, and the
store under <root>/tools when the install stamp names no runtimeDir, as
source stamps never do). A launch of a shared checkout under another
root -- a test's temporary HERMES_HOME, a per-task home, a CI service
home -- therefore treated the checkout as a fresh install of its own:
it downloaded a full tool store into that root (~0.5 GB), synced a venv,
then ran the checkout's update tail against the shared tree (launchers
repointed at the temporary interpreter, web/TUI/desktop rebuilt,
install stamp and bootstrap receipt rewritten) under an update lock that
lives in the borrowing home. Deleting the home then bricked `hermes`,
and concurrent borrowers raced each other's product builds (#123238).
pm.environments.owning_home_root() identifies the root that installed
the checkout: the platform default root or the root the checkout sits
in, whichever holds committed state (facts.json) for it, when that state
is not the active root's own (the owner, one of its profiles, or a home
whose installs link back to it). A fresh install and a custom root that
owns its tree resolve to None and keep today's behaviour.
- store_root(): an unstamped store resolves to the owner's tools/, so a
borrowing launch uses the interpreter the checkout's launchers already
exec and downloads no tool store of its own. HERMES_RUNTIME_DIR and a
stamped runtimeDir still win.
- prepare_launch(): a borrowing root syncs its own dependency
generation, as a process spawned under a live update does, and returns
its relaunch interpreter. It never arms or runs the tail, never
publishes launchers and never removes the owner's update markers.
Tests: tests/hermes_cli/test_borrowed_home_launch.py (10). Each half
fails without its change: store_root's two borrowing cases, and the
prepare_launch tail/marker cases.
(cherry picked from commit 8c427c8389634f6cebc3a8ced9a626c6354e8a18)
b11146 ships ubuntu-cuda-13.4-arm64 (+ cudart), so llamacpp-cuda gains
linux-arm64, auto resolves NVIDIA arm64 Linux to CUDA, and the desktop's
localModels flag ships on Linux arm64 too.
Every upstream Linux llama.cpp build (CPU included) links the system
libgomp, which minimal hosts (WSL, containers) lack, and a normal install
never touches the system package manager. Each Linux engine now pins
Ubuntu 24.04's libgomp1 .deb (glibc 2.38, the floor upstream already sets)
and stages libgomp.so.1 beside llama-server, where the $ORIGIN RUNPATH
finds it; the artifact mirror covers the pool URL after 24.04's EOL.
DebPackage's ar+tar unpack becomes pm.package.unpack_deb and reads zstd
data.tar (Ubuntu's default) with the stdlib codec PM's 3.14 runtime has.
Pre-PM engines on Linux carry only llama.cpp's own archives, so adoption
maps their manifests to those and records them as outdated (usable now,
update offered) instead of failing to match.
Verified on an RTX Spark N1X (WSL, Ubuntu 26.04, no system libgomp):
ensure_engine() for cuda, cpu and vulkan through PM's real download/stage/
verify path; ldd resolves libgomp from the entry; llama-server serves
completions (CUDA0 at 445 tok/s). linux-x64 cpu/vulkan staged through the
same path with no NEEDED library missing beyond glibc/libstdc++/OpenSSL 3.
stage_launcher resolved the store python through
pm.environments.store_root, which honors HERMES_RUNTIME_DIR. Any process
spawned with that variable set (PM's runtime_environment, the desktop
toolchain, an e2e fixture home) that reached launcher publication minted
.hermes/bin wrappers bound to the foreign store. When that store lived
under a scratch tree, its later cleanup deleted the interpreter, and the
gateway sat in an exit-127 crash loop after the next reboot (#131745).
Publication now resolves the tree's own store: resolve_store_python
gains honor_runtime_override (default True, so execution-side callers
keep today's behavior), and the persisting paths — stage_launcher, the
gateway service publication gate, expose_cli and the CLI publish entry —
pass False.
(cherry picked from commit 77370d46611efe1338c39c64f4951ab08dd32f1f)
Review of #131847: the retry ran for every rmtree failure on every OS.
On POSIX an unreadable directory hands the callback os.open, and
func(path) raised TypeError (missing flags), which escaped the OSError
set-aside path and failed an install that had already published. And
os.chmod followed a symlink inside an unwritable directory, rewriting
the mode of a file outside the tree to 0200.
The retry now only fires for a PermissionError from os.unlink/os.rmdir
on Windows, and clears the link's own attribute where chmod can follow
links; everything else raises as plain rmtree does.
The salvaged commit imported hermes_cli.fs_utils.rmtree_force inside
_remove_entry. PM installs run before the rest of the tree exists: the
Docker toolchain stage copies only pm/, so every image build died with
"No module named 'hermes_cli.fs_utils'" on its first fetch cleanup.
pm/filesystem.py is the stdlib-only home for exactly this kind of
primitive. hermes_cli/fs_utils.py stays as is; it is part of the frozen
old-updater surface.
A read-only file (PortableGit's etc/hosts) inside a replaced entry made
_remove_entry fail every attempt. Since the held-tree fix the install no
longer aborts, but the tree is set aside and gc can never reclaim it, so
every replaced git entry leaked. The live Windows case requires the
replaced tree to be gone outright after a reinstall. Also trims the
salvaged comment to its why.
`_remove_entry` used a bare `shutil.rmtree`. Git for Windows' PortableGit
archive ships `etc/hosts` read-only, and Windows refuses to unlink a
read-only file, so removing the rotated `.previous-git-*` entry raised
WinError 5 on every attempt. The retry loop cannot help here: a read-only
attribute is not a transient handle, so all 5 retries failed and the
whole update aborted with:
Source update preparation failed: git: install failed:
[WinError 5] Access is denied:
'...\.previous-git-2.53.0+3-win32-x64\etc\hosts'
The abort happened before the checkout was reset, so `hermes update` made
no progress at all and the install stayed pinned indefinitely.
Use the existing `hermes_cli.fs_utils.rmtree_force`, which clears the
read-only bit before deleting and whose docstring already describes this
exact scenario. It is imported lazily to match how the rest of `pm`
reaches into `hermes_cli`.
Every cleanup that runs after a publish or restore has finished deletes
garbage: the displaced .previous-* entry, a restored .displaced-* tree,
the fetch-* archives. A running Hermes process keeps the replaced
interpreter's DLLs mapped, so on Windows that delete can fail far past
the retry window and turned a completed update into "install failed:
[WinError 5]" (#124807).
One helper now owns that cleanup for all four sites. When the delete
fails it renames the tree to .reclaim-<uuid> (Windows permits renaming a
tree with mapped images): the old slot is free for the next publish of
the same entry, no restore can ever pick the half-deleted tree up, and
the next install or `hermes pm gc` deletes it once the hold is gone.
This replaces the sidecar marker for restored displacements (#128955)
and the warn-and-keep for previous entries from the salvaged commit:
a kept .previous-<entry> blocked the next publish of that entry, could
be restored half-deleted, and was unreachable by gc after a version bump.
`activate` is bash syntax, so fish users had no way to take on the dev
environment. `activate.fish` does what `activate` does: snapshots the keys it
will change, applies the composed env, defines a `hermes` function that
refuses outside the worktree, adds the prompt prefix, and defines
`deactivate`. It shells out to the same bash library, so composition is not
reimplemented.
`pm.environments --format fish` emits the env as `set -gx` statements.
fish's read-only names (`PWD`, `SHLVL`, `_`, ...) are skipped because
`set -gx PWD` aborts the script, and values named `*PATH` stay lists.
`hermes_compose_env` takes the dialect (`sh` or `fish`) as its second argument.
`activate` doubled as the thing scripts sourced non-interactively, so it
carried `--` argument guards and every caller had to tolerate its prompt and
`hermes` function. Composition (setup sync, bootstrap Python, pm env) now
lives in scripts/_activation.sh, and there are two consumers:
- `activate`: interactive, reversible (`deactivate`).
- new `scripts/run-in-hermes-env CMD...`: applies the env to one process and
execs into it. Replaces `scripts/_hermes-python`; every Python script's
shebang and `scripts/run_tests.sh` (which re-executes itself under it,
however it was launched) use it.
`pm.environments --format sh` emits the env as shell exports, so the inline
JSON-to-export Python is gone from `activate`. An install that did not build
the test environment now reads as stale for every consumer.
Tests are split along the same seams: activation support fixtures, bash/ps1
activation, env-script quoting, and the runner's staleness.
Upstream first published ubuntu-cuda archives after b10964, which is why
llamacpp-cuda was Windows-only and NVIDIA Linux users fell back to Vulkan.
b11146 (the v0.5.0 release) ships ubuntu-cuda-13.4-x64, so pin every llama.cpp
backend to it and add linux-x64 to llamacpp-cuda.
- The engine archive carries no CUDA libraries, so the Linux target pins the
versioned cudart tarball too (Windows' cudart zip has no build tag in its
name). The engine's RUNPATH is $ORIGIN, so stage() hoists the cudart
tarball's top-level dir beside llama-server.
- Upstream dropped win-cuda-13.3 in the same window, so win32-x64 moves to
13.4, matching win32-arm64. 13.x drivers run any 13.x runtime.
- linux-arm64 is left out: not verified on hardware.
Verified: ensure_engine("cuda") through PM's real download/stage/verify path
on an RTX 4060 Ti; llama-server --list-devices reports CUDA0.
The catalog plugin declares the Supermemory SDK in its own pyproject, so
the plugin manager installs it with the plugin. Removed the extra, its
exclude-newer row and the pm/extras.py import anchor; `uv lock` removed
only the supermemory package. The mem0 extra and its win-arm64 gate stay.
28edf7c75a made stage() chmod the native agent-browser binary, but
entries staged before it (lazy installs since the pm store landed) stay
0644 forever: every browser_* call fails with PermissionError and the
store never restages because the entry verifies. Restore the bit in
place when the entry is looked up on its own target (modes are not part
of the pinned digest); an entry that cannot be chmodded counts as not
installed so ensure() restages it.
The isolation filter now drops only settings that redirect WHERE packages
resolve from (index URLs, find-links, strategy, index credentials); transport
knobs (UV_NATIVE_TLS / UV_INSECURE_HOST / UV_HTTP_TIMEOUT) survive — corporate
networks need them to reach the pinned wheel URLs at all.
Addresses the review note on the no_config scope in the #125071 triage.
stage_runtime (the pinned pm/ self-repair builder) passes no_config=True,
but that only appended uv's --no-config, which removes config FILES. The
pip-mirror bridge (pm.index_config) reaches uv as env vars instead, so a
user's pip.conf index-url still rode into the runtime sync via
runtime_environment() -> _base_environment(env=None). Re-resolving the
official-index pm/uv.lock against a mirror changes the resolution, and
uv sync --locked refuses — breaking the top-level update / doctor /
pm-repair recovery paths on any machine with a pip mirror (#124418).
When no_config is set, strip forwarded index/transport knobs from the
child env: a fully-pinned graph never needs a mirror to resolve, and the
default path keeps the bridge so mirrored/air-gapped application installs
still work.
The runtime venv installs the core project editable against the PM
build snapshot (workspace/), so venv/bin/hermes always imports that
frozen copy. PM only rebuilds the venv on lock/extras/python-pin/plugin
changes, so a source-only update leaves the copy stale while the
gateway keeps running the checkout via activate_dependencies's
sys.path override. Any child that resolves `hermes` off PATH (terminal
tool, cron, kanban workers) still finds venv/bin first and runs the
stale snapshot instead — including `gateway install`/`restart`, which
then pins launchd/systemd to the snapshot's launcher path.
Prepend the checkout's own .hermes/bin ahead of venv/bin in
activate_dependencies so `hermes`/`hermes-acp` always resolve to the
launcher that puts the checkout first, regardless of what else lives
in the venv. This also fixes tools/environments/local.py's
_resolve_hermes_bin_dir(), whose shutil.which("hermes") now finds the
launcher dir instead of the venv's console script, so no separate guard
on PROJECT_ROOT in generate_launchd_plist / systemd unit generation is
needed: the snapshot's hermes_cli is never the running one.
Fixes#124627
pm/lock.json and the Npm/AgentBrowser templates record registry.npmjs.org, and
pinned_source handed that URL straight to the downloader, so ~/.npmrc /
npm_config_registry were ignored and closed networks could not provision npm or
agent-browser. pinned_source now routes public-npm URLs through the configured
registry (npm's own precedence: npm_config_registry, then the user npmrc); the
lock's SHA256 still verifies the bytes and progress keeps the lockfile URL.
npm_dist_tags reads from the same registry.
The E2E cell test_npm_registry_mirror_serves_pm_npm_download drops its gate.
Co-authored-by: funky-xamarin <30426178+Wenfengcheng@users.noreply.github.com>
Post-write .js/.ts lint on the local backend runs PM's node/npx instead of
the terminal shell's PATH (user dirs first). pm.activate always puts the
store dirs in front, also on drift, activating only packages whose whole
chain is installed. tui_gateway puts the store dirs ahead of ~/.local/bin.
Bare npx/npm/node/uv/uvx MCP commands resolve to PM's copies; the system
fallback tables are gone and an absolute command: stays the user's.
Computer use and browser use are meant to work out of the box. The PM rewrite
(3d12e86ef1) and the MSIX installer rework (47f4ab3a17) dropped the
install-time cua-driver fetch (7060ac7bed) and the Browser Use CLI install
(baa6b2e34d). On a fresh install the computer_use check_fn therefore stayed
False, so the tool never reached the model and its lazy ensure could not fire,
and browser_exec quietly fell back to the built-in tools.
- cua-driver is a default PM package, so the installers, a bare
`hermes pm install` and `hermes update` carry it on every target it builds
for. Adds the missing Android gap (the lock has no bionic artifact).
- The shared default-tool step, which the installers (via source completion)
and `hermes update` both run, provisions the Browser Use CLI for the default
and explicit Browser Use backends. `--skip-browser` declines it along with
agent-browser, and `off`/Camofox never use it.
- Installers regain --skip-computer-use / -SkipComputerUse (recorded as
`--without cua-driver`).
- The update message stops calling every default "browser tools".
- Refusal, non-zero exit and timeout now raise InstallError (pm/client.py
catches only InstallError; TimeoutExpired escaped entirely). The off-Windows
refusal names a real remedy instead of the generic 'retry'.
- stdin/stdout/stderr -> DEVNULL: with capture_output the stub's RunProgram
children inherit the pipes and hold run() open past the stub's exit (and
CPython re-communicates without a timeout after a kill). The stub prints
nothing under -y, so the output tail was dead code.
- TemporaryDirectory(ignore_cleanup_errors) as in Npm.unpack; unpack empties
its destination like extract(); drop redundant local imports.
- Tests: CompletedProcess over hand-rolled stubs, no raising=False, timeout
case added, change-detector pin-suffix test removed.
CI run 36189416163 failed after "git: unpacking": executing the cached
fetch-<sha> PortableGit PE in place left it handle-held (Defender
on-execute scan / the stub's RunProgram child chain) past pm's ~2 s
_remove_entry retry, so download cleanup raised WinError 32. Review
(teknium1, 5 threads) adds the rest:
- Git.unpack now copies the artifact into a .sfx-* dir beside the
staging tree and executes the copy; pm's .staging-* teardown
(ignore_errors) owns that path, so any hold lands on a disposable
path and the cache dir only ever holds read handles
- refuse off-Windows with the cross-host trade-off stated instead of a
raw PermissionError; documented in package-management.md and the PR
- the extractor is a GUI-subsystem stub that is silent under -y: error
messages now carry the exit code and the usual causes (disk full,
path length, antivirus) instead of promising captured output; the
docstring no longer claims "no GUI" and records that the stub shows
an Extracting window and runs the vendor post-install
- install.ps1: WaitForExit(600000) + Kill() mirrors pm's timeout=600,
Fail reports the exit code and the silence
- tests: the OS-refused-exec assertion and the not-in-text change
detectors are replaced by subprocess-argv invariants (scratch copy
location, exit-code message, off-Windows guard before any execution)
Related to #122512
(cherry picked from commit adaf76a286bebe30c89aee4174df27f1945b60c9)
Windows 10 boxes whose System32 tar.exe cannot run the bzip2 filter die
at stage=prerequisites with "unable to run program bzip2 -d" while
extracting the pinned Git-2.53.0.3 tar.bz2 (#122512). Repin git for
both win32 targets to git-for-windows' PortableGit self-extracting 7z,
which carries its own extractor and the bundled usr/bin/bash.exe:
- pm/lock.json: new artifact urls + sha256 (the pin authority)
- scripts/install.ps1: generated fragment regenerated; Get-PinnedGit
downloads the SFX and waits on it explicitly (the stub is a
GUI-subsystem exe, so PowerShell's & does not wait); the System32
tar invocation and its msys symlink excludes go away
- pm/packages.py: Git.fetch_url/Git.unpack run the self-extractor
after the sha256-verified download
- pm/store.py: drop the now-callerless git_msys branch of extract_tar
- tests: RED->GREEN test runs the real Get-PinnedGit against a
bzip2-less System32 tar.exe stub with no bzip2 on PATH; the three
obsolete tar-contract tests and the install.ps1/PM msys-links parity
test are replaced by a no-external-decompressor contract test
Closes#122512
(cherry picked from commit 4915304213495d3207ec6cd659e57cd16ef08d60)
The Windows and Linux rows pointed at autobuild-2026-09-10-15-31, a daily
BtbN has deleted, so a fresh install 404s wherever the artifact mirror is
also refused. autobuild-2026-08-31-13-27 is the month-end build BtbN keeps
for two years; its n9.0.1-11 snapshot matches the locked 9.0.1 label and
is what the updated `pm update ffmpeg` resolves. Digests from the GitHub
release API; install-verified on win32-x64 and linux-arm64.
Co-authored-by: Yuan Li <dskwelmcy@163.com>
BtbN deletes daily autobuild tags after 14 days and keeps the last build
of each month for two years (util/prunetags.sh). btbn_index took the
newest daily, so every `pm update ffmpeg` wrote a pin that 404s two weeks
later (#125350, #122240). It now skips the current month's tags (still
dailies the next build displaces) and every tag but the last in each
finished month, so the tool only ever pins a retained build.
packages.py and security_packages.py imported the private _MUSL_TARGETS
across modules; make it a public store constant next to ALL_TARGETS.
The _pin_tool hash memo is an unrelated optimisation, out of scope here.
Skip glibc-linked FFmpeg on musl in both default install and update roots.
Select musl uv during the standalone shell bootstrap, and let the native
userland resolve libc before bootstrap Python build metadata.
Add regression checks for the closure, target precedence, and installer pins.