Skip to content

fix: clear duplicate copies of managed tools - #107

Merged
catinspace-au merged 5 commits into
mainfrom
fix/no-strays
Oct 6, 2026
Merged

catinspace-au merged 5 commits into
mainfrom
fix/no-strays

Conversation

@catinspace-au

Copy link
Copy Markdown
Contributor

Copies of managed tools left behind by hand installs now get cleared, so each tool has one copy and it updates with the host. The cleanup only removes what it can prove is a duplicate. Anything it is unsure of stays and shows up in the end-of-run report.

  • --tags removals / soe runs clear:
    • user copies in ~/.local/bin and ~/go/bin when the roles' system copy exists and is a different file. A link to the managed copy, a dangling link, a directory, a tool the roles do not manage, and a bin directory that is itself a link all stay. yq, tea, sd and act are not touched, because other programs share those names. A version pinned on purpose in ~/.local/bin is removed.
    • hand-built release packages of tools the roles install to /usr/local/bin. This is an explicit list (macbash, git-scrub, dive, golangci-lint, k9s on Ubuntu), and only applies when no repository offers the package and its removal takes nothing else.
    • the distro Go, when the roles' /usr/local/go resolves under /usr/local, runs, carries its own src tree and has its profile drop-in. go and gofmt are linked into /usr/local/bin so systemd units still find Go.
    • duplicates in cargo homes: sccache, sd, fnm, uv and uvx, and the stale ~/.cargo/bin left by a relocated CARGO_HOME. ~/.cargo counts as stale only when the host itself exports the relocated home. A role-only override warns instead. Packages whose binaries all went are deregistered, so cargo install-update cannot bring them back.
  • Every package purge, including Docker Desktop and the distro Rust removal, goes through system_cleanup/tasks/purge_packages.yml. It marks the direct dependencies the purge would orphan as manual first. Without that, hyperi-update's unattended autoremove takes gcc, binutils and libc headers afterwards. It runs in the C locale, forces dnf to list orphans, and stops the purge if the plan does not add up.
  • Fedora's distro Rust removal no longer fails on rust-std-static.
  • cargo-tarpaulin, which the role used to install, is retired on every run.
  • hyperi-update:
    • the systemd unit reads /etc/environment;
    • the script asks the login shell for CARGO_HOME/RUSTUP_HOME when they are unset;
    • ~/go/bin and /usr/local/go/bin are on its PATH;
    • refetch only touches copies in /usr/local/bin;
    • it exits 1 when a step fails, so a failed timer run shows as failed.
  • contributor's typos and alint install into the effective cargo home.

Tested in containers: the remediation scenario converges twice and verifies clean on ubuntu:26.04, ubuntu:24.04 and fedora:44, plus two 26.04 hosts with /usr/local/go linked into, or copied from, the distro tree. The fixture asserts the Go purge really would orphan gcc, and verify asserts it did not. Also covered:

  • German-locale Fedora;
  • a dnf config with clean_requirements_on_remove off;
  • a role-only cargo home override;
  • check mode;
  • a noisy login profile.

Not run: a full developer-rust or developer-go install converge, the timer under systemd, and macOS.

Done when a converged box has one copy of each managed tool and autoremove has nothing of ours to take.

A tool installed by hand next to the one the roles manage leaves two copies, and the one in ~/.local/bin, ~/go/bin or a cargo home wins on PATH while nothing updates it. Each copy now goes only where the managed copy exists and is a different file (device and inode after following links), so the only working copy is never the one removed.

The removals tag clears user-level copies of the tools the roles install system-wide, and purges a .deb or .rpm that duplicates a /usr/local/bin tool when no repository offers it and a simulated removal takes nothing else. developer-go removes the distro Go once /usr/local/go is in, kept with a message when another package needs it.

developer-rust deregisters cargo-home copies of sccache, sd and fnm with cargo uninstall --root, so cargo install-update cannot bring them back, and removes uv and uvx left by the old installer. cargo-tarpaulin is retired from every cargo home, including one cargo has no record of, which used to fail the uninstall. Where CARGO_HOME is relocated, binaries the effective home also holds are removed from the old ~/.cargo/bin, and its caches and anything installed only there stay.

Relocation is now decided by comparing the directories, not their paths. A ~/.cargo symlinked to the relocated home read as stale, so the live config.toml was renamed to config.toml.superseded on every run.

typos and alint install into the effective CARGO_HOME, and the soe PATH drop-in and the hyperi-update scripts use ${CARGO_HOME:-$HOME/.cargo}/bin. The Linux update script also puts /usr/local/go/bin on PATH, which a GUI launch otherwise lacks once the distro Go is gone.
Package removals now go through one task file, system_cleanup/tasks/purge_packages.yml, used by the distro Go purge, the hand-installed package purge and the Docker Desktop tombstone. It refuses a removal that would take any other package, and marks everything the removal would orphan as manually installed first. hyperi-update runs apt-get autoremove and dnf autoremove unattended, so without that gcc, binutils and libc6-dev went at the next update after the Go purge.

The distro Go goes only when /usr/local/go/bin/go runs, resolves under /usr/local to a different file than /usr/bin/go, and hyperi-go.sh exists, and only golang* packages are candidates. A /usr/local/go linked into the distro tree had the only Go purged. go and gofmt are now linked into /usr/local/bin so cron, systemd units and non-login shells keep a Go.

A hand-installed package is purged only when it is on an allowlist of upstream release packages (macbash, git-scrub, dive, golangci-lint, k9s), no repository offers it, and the apt lists exist. Repository absence alone purged Ubuntu's yq, sccache and gitleaks wherever the lists were empty.

The user-level sweep drops yq, tea, sd and act, whose names other programs share, skips directories, dangling links and bin directories that are links or resolve outside the home. hyperi-update refetches a static binary only when the copy in /usr/local/bin is its own, so a user-level copy no longer makes it install a second one.

~/.cargo is swept only when the home the host exports resolves to the effective one. A rust_cargo_home the host does not declare deleted cargo, rustc and rustup from the only cargo directory on PATH. It now leaves ~/.cargo and its config.toml alone and records a warning. The exported homes are probed once in the developer role with printenv, so an unexported shell variable no longer counts, and contributor installs typos and alint into the same home.

In the cargo homes, retired and duplicate tools are deregistered before cargo install-update runs, a ~/.cargo package whose binaries all go is deregistered too, and cargo uninstall fails the run unless cargo simply has no record of the package. hyperi-update reads CARGO_HOME and RUSTUP_HOME from the login shell when they are unset, its unit loads /etc/environment, a missing rustup or cargo-install-update beside a cargo home is a failure, and ~/go/bin is on its PATH.
The shared purge read dnf's translated table headers, so under a German locale it found no orphans, removed the distro Go and left gcc, binutils and glibc-devel for the next autoremove. Every parsed command now runs in the C locale, and the purge stops unless dnf's summary count matches the packages it parsed. It also stops when any simulation fails. It marks only the new orphans the purged packages depend on directly (requires and recommends), which keeps their own dependencies too, instead of every new orphan.

Removing what the roles did not install now runs only on a removals or soe run, like the developer role's tombstones: the distro Go purge, the cargo-home duplicates and the stale ~/.cargo sweep. A --tags removals run reaches the cargo-home sweep through a new developer-rust removals.yml, and the cargo-home resolution moved to cargo_home.yml so both paths share it. cargo-tarpaulin is still retired on every run, as this role installed it. A CARGO_HOME the host declares but has not created yet counts as declared, so a first run no longer warns.

The distro Go goes only when /usr/local/go/src/runtime also resolves under /usr/local, because a copy of Ubuntu's tree keeps its standard library as links into /usr/share. go and gofmt in /usr/local/bin are pointed at /usr/local/go only when missing, dangling or already pointing there, and anything else is reported.

The distro Rust removal failed with a depsolve error on any Fedora host with rust-std-static. It now removes every package built from the distro's Rust sources (rust-defaults and rustc-N.NN on Ubuntu) through the same purge.

hyperi-update reads CARGO_HOME and RUSTUP_HOME from sentinel lines the login shell prints into a temporary file, takes only absolute paths, and no longer waits on a child a profile left in the background. Both updaters exit 1 when any step failed, so the systemd unit shows the failure.

The remediation fixture removes dpkg-dev and rpm-build before installing Go and asserts the Go removal would orphan gcc, copies the Go tree with links dereferenced, builds with the managed Go in verify, and adds a gosrc host whose /usr/local/go still links into /usr/share.
A host dnf config with clean_requirements_on_remove=False drops the orphan table from the removal plan. The count check then passed with nothing marked, so gcc was left for autoremove. The plan now forces the setting on. Packages the run keeps, because something depends on them or the plan could not be read, are reported at the end of the run, whose heading now covers both cases.
@catinspace-au
catinspace-au merged commit eab8b8d into main Oct 6, 2026
18 checks passed
@catinspace-au
catinspace-au deleted the fix/no-strays branch October 6, 2026 08:34
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant