Summary
do_install() overwrites the install manifest on every install instead of reconciling it with the previous one. Files installed by an earlier version that the current version no longer ships are never recorded in the new manifest and are never removed by install — so they become permanently untracked orphans that uninstall cannot see.
This falsifies the claim in CLAUDE.md / README.md that uninstall is "fully pristine".
Evidence (real occurrence)
After ./mediaforge.sh uninstall reported success on a ~/.local/mediaforge prefix:
[mediaforge] Removed 1527 files from /home/matte/.local/mediaforge
8 files (1.4M) survived, and the prefix root was left behind:
/home/matte/.local/mediaforge/lib/liblcevc_dec_api.a
/home/matte/.local/mediaforge/lib/liblcevc_dec_api_utility.a
/home/matte/.local/mediaforge/lib/liblcevc_dec_common.a
/home/matte/.local/mediaforge/lib/liblcevc_dec_enhancement.a
/home/matte/.local/mediaforge/lib/liblcevc_dec_extract.a
/home/matte/.local/mediaforge/lib/liblcevc_dec_pipeline.a
/home/matte/.local/mediaforge/lib/liblcevc_dec_pipeline_cpu.a
/home/matte/.local/mediaforge/lib/liblcevc_dec_pixel_processing.a
These are V-Nova's split static archives. Since 829b927 (fix(lcevc): opt-in + merge split archives for static linking, 2026-06-04), recipes/other/lcevc.sh:113 merges them into a single liblcevc_dec.a and deletes the splits from the workspace:
# Drop the split archives: the .pc and FFmpeg reference only -llcevc_dec now,
rm -f "$_libdir"/liblcevc_dec_*.a
So a current install never ships them — but an install from before 829b927 did, and those files are still on disk.
Root cause
lib/install.sh:143 creates an empty manifest accumulator per run:
_manifest_tmp=$(mktemp /tmp/mediaforge-manifest.XXXXXX) \
|| die "Cannot create manifest tmp file in /tmp"
Only files installed by this run are appended to it (via _install_file). Then lib/install.sh:201 overwrites the previous manifest wholesale:
$_priv cp "$_manifest_tmp" "$_manifest"
Two properties combine to orphan files:
do_install never removes anything from $_install_prefix. It only copies in. A file from a previous install stays on disk even when the current build no longer produces it.
- The manifest is replaced, not merged/diffed. The record that those files were ever installed is destroyed.
do_uninstall (lib/install.sh:299-316) then iterates only the manifest:
while IFS= read -r _rel; do
...
_file="$_target/$_rel"
if [ -f "$_file" ]; then
$_priv rm -f "$_file"
...
done < "$_manifest"
Anything not listed is invisible to it, forever. The bottom-up rmdir sweep then correctly refuses to remove lib/ and the prefix root because they are non-empty — so the "isolated prefix removes itself" behaviour silently degrades to "prefix left behind", with no warning.
Impact
- Stale archives linger indefinitely. Any recipe that changes its installed file set (renames, merges, splits, drops a library) orphans the old files on every existing install. lcevc is the instance that surfaced this;
flac removal (f887b97) and the xeve/xevd nested-archive change (16b45fc) are the same shape.
- Silent, and worst for the documented use case. The isolated-prefix pattern (
~/.local/mediaforge, /opt/mediaforge) that BUILDING.md recommends for downstream linking is precisely where a leftover prefix is least expected.
- Stale
.a files can be linked. An orphaned archive that still has a matching .pc (or is referenced by Libs.private) can be picked up by a downstream static link, silently mixing versions.
- Uninstall reports a success count that is not the truth.
Reproduction
- Install a build that ships file
X (e.g. lcevc split archives, pre-829b927).
- Update to a version whose recipe no longer installs
X (post-829b927).
./mediaforge.sh build && ./mediaforge.sh install into the same prefix — X is untouched on disk and absent from the new manifest.
./mediaforge.sh uninstall — reports success; X remains; the prefix root is not removed.
Suggested fix
Reconcile against the previous manifest before overwriting it. In do_install, immediately before lib/install.sh:201:
- If
$_manifest already exists, read it, and for every entry present in the old manifest but absent from $_manifest_tmp, $_priv rm -f that path (applying the same case traversal guard do_uninstall uses at lines 303-308).
- Then run the same bottom-up
rmdir sweep so directories emptied by the prune are removed.
- Then
cp the new manifest into place.
That makes install idempotent with respect to dropped files and preserves the "pristine uninstall" invariant.
Alternative (simpler, more invasive): have do_install call do_uninstall for the target prefix first when a manifest is present — install-over-install becomes replace rather than accumulate. This is a behaviour change worth considering separately, since it briefly leaves the prefix empty.
Either way, a regression test belongs in tests/: install manifest A containing a file, install manifest B without it, assert the file is gone and the prefix is pristine after uninstall.
Environment
- mediaforge
SCRIPT_VERSION="3.0", branch develop @ dc2cb38
- Prefix:
~/.local/mediaforge (isolated, user-owned)
- Linux 7.0.12-arch1-1
Notes
Found while clearing a mediaforge install to validate an unrelated experiment; the leftover .pc-less archives were what exposed it. The 8 orphans have since been removed manually.
Summary
do_install()overwrites the install manifest on every install instead of reconciling it with the previous one. Files installed by an earlier version that the current version no longer ships are never recorded in the new manifest and are never removed byinstall— so they become permanently untracked orphans thatuninstallcannot see.This falsifies the claim in
CLAUDE.md/README.mdthat uninstall is "fully pristine".Evidence (real occurrence)
After
./mediaforge.sh uninstallreported success on a~/.local/mediaforgeprefix:8 files (1.4M) survived, and the prefix root was left behind:
These are V-Nova's split static archives. Since 829b927 (
fix(lcevc): opt-in + merge split archives for static linking, 2026-06-04),recipes/other/lcevc.sh:113merges them into a singleliblcevc_dec.aand deletes the splits from the workspace:So a current install never ships them — but an install from before 829b927 did, and those files are still on disk.
Root cause
lib/install.sh:143creates an empty manifest accumulator per run:Only files installed by this run are appended to it (via
_install_file). Thenlib/install.sh:201overwrites the previous manifest wholesale:Two properties combine to orphan files:
do_installnever removes anything from$_install_prefix. It only copies in. A file from a previous install stays on disk even when the current build no longer produces it.do_uninstall(lib/install.sh:299-316) then iterates only the manifest:Anything not listed is invisible to it, forever. The bottom-up
rmdirsweep then correctly refuses to removelib/and the prefix root because they are non-empty — so the "isolated prefix removes itself" behaviour silently degrades to "prefix left behind", with no warning.Impact
flacremoval (f887b97) and thexeve/xevdnested-archive change (16b45fc) are the same shape.~/.local/mediaforge,/opt/mediaforge) that BUILDING.md recommends for downstream linking is precisely where a leftover prefix is least expected..afiles can be linked. An orphaned archive that still has a matching.pc(or is referenced byLibs.private) can be picked up by a downstream static link, silently mixing versions.Reproduction
X(e.g. lcevc split archives, pre-829b927).X(post-829b927)../mediaforge.sh build && ./mediaforge.sh installinto the same prefix —Xis untouched on disk and absent from the new manifest../mediaforge.sh uninstall— reports success;Xremains; the prefix root is not removed.Suggested fix
Reconcile against the previous manifest before overwriting it. In
do_install, immediately beforelib/install.sh:201:$_manifestalready exists, read it, and for every entry present in the old manifest but absent from$_manifest_tmp,$_priv rm -fthat path (applying the samecasetraversal guarddo_uninstalluses at lines 303-308).rmdirsweep so directories emptied by the prune are removed.cpthe new manifest into place.That makes install idempotent with respect to dropped files and preserves the "pristine uninstall" invariant.
Alternative (simpler, more invasive): have
do_installcalldo_uninstallfor the target prefix first when a manifest is present — install-over-install becomes replace rather than accumulate. This is a behaviour change worth considering separately, since it briefly leaves the prefix empty.Either way, a regression test belongs in
tests/: install manifest A containing a file, install manifest B without it, assert the file is gone and the prefix is pristine after uninstall.Environment
SCRIPT_VERSION="3.0", branchdevelop@ dc2cb38~/.local/mediaforge(isolated, user-owned)Notes
Found while clearing a mediaforge install to validate an unrelated experiment; the leftover
.pc-less archives were what exposed it. The 8 orphans have since been removed manually.