Skip to content

install: manifest is overwritten, not reconciled — files dropped between versions become permanently orphaned (uninstall is not pristine) #15

Description

@crippledgeek

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:

  1. 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.
  2. 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

  1. Install a build that ships file X (e.g. lcevc split archives, pre-829b927).
  2. Update to a version whose recipe no longer installs X (post-829b927).
  3. ./mediaforge.sh build && ./mediaforge.sh install into the same prefix — X is untouched on disk and absent from the new manifest.
  4. ./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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions