Plugin installer: the record keeps only dependencies whose version changed, so removing one plugin breaks another #597
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#597
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the installer and runninginstall_wheelandremoveagainst a plugins directory in a temporary folder, with the installer replaced by one that lays down dist-info directories.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/installing.py:783-790(install_wheel, thearrivedlist),plugins/installing.py:865-885(unshared_dependencies),plugins/installing.py:852-862(remove) andplugins/installing.py:266-304(pin_owners).install_wheelwrites into a plugin'sdependenciesonly the packages whose version in the plugins directory differs from before the install.unshared_dependencies()reference-counts from those lists, so the count is wrong whenever a package was already there: a second plugin needing it records nothing, and an upgrade or reinstall records an empty list. Removing a plugin then deletes packages another plugin is using, or leaves its own behind for good. Related to #246 (closed), which introduced the reference count; this is an incomplete fix.plugins/installing.py:957-964(install_wheel, thearrivedlist),plugins/installing.py:1039-1059(unshared_dependencies),plugins/installing.py:1026-1036(remove), andplugins/installing.py:440-478(pin_owners, which treats any unclaimed package as Postulo's).shared==2.0; its record lists["shared==2.0"]. Plugin B, which also needsshared, is installed next;before.get("shared") == "2.0", so B's record lists[]. Removing A deletessharedfrom the volume. B then fails to import in every process that loads it afresh, is logged and skipped, and its notifications or syncs stop for everybody using it.sharedunchanged, rewrites A's record withdependencies: []. Removing A afterwards leavessharedon the volume. Because no record claims it,pin_owners()then counts it as one of Postulo's own pins, and an A 1.2 needingshared==3.0is refused with "shared: needs 3.0, and Postulo has 2.0.", which is untrue.Both reproduced in the snapshot: "record {'plug-a': ['shared==2.0'], 'plug-b': []}", then after removing A only
plug_bwas left; and "A 1.0 ['shared==2.0'] / A 1.1 []", the false refusal, andsharedleft behind after removal.Requires-Distfrom the plugin's own dist-info through the dist-infos in the plugins directory (evaluating markers), or take the resolution the installer reports (uv pip install --report/pip install --report).unshared_dependencies()is then correct as written, and an upgrade keeps the shared packages it still needs.Done when
tests/test_plugin_rollback.pyhas tests, using the fake installer extended to lay down dependency dist-infos, that two plugins installed one after the other both list a shared dependency and removing either keeps it; and that an upgrade and a same-version reinstall keep the dependency list.