proj/portage:master commit in: lib/portage/dbapi/, lib/portage/tests/dbapi/

"Matt Turner" <[email protected]>
Newsgroups gmane.linux.gentoo.cvs
Message-ID <1785956369.69724ed20005966524ccf27f46a75d35294f42ba.mattst88@gentoo>
commit:     69724ed20005966524ccf27f46a75d35294f42ba
Author:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
AuthorDate: Sun Aug  2 22:30:09 2026 +0000
Commit:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
CommitDate: Wed Aug  5 18:59:29 2026 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=69724ed2

vartree: split preserved lib graph analysis into its own function

_find_unused_preserved_libs() builds a graph of the preserved libraries
and their consumers, decides which of them have no consumers left, and
maps the result back to the packages that own them, all in one place.
Move the second step into _find_unneeded_preserved_nodes(), which
depends on nothing but the graph, so that it can be tested without a
dblink instance and so that changes to it are easy to review.

The only functional change is that the warning about a preserved symlink
whose target is not preserved is now emitted in a single sorted pass,
rather than sorted within each round of the peel.

Signed-off-by: Matt Turner <mattst88 <AT> gentoo.org>

 lib/portage/dbapi/vartree.py                   | 83 +++++++++++++++++---------
 lib/portage/tests/dbapi/meson.build            |  1 +
 lib/portage/tests/dbapi/test_preserved_libs.py | 48 +++++++++++++++
 3 files changed, 104 insertions(+), 28 deletions(-)

diff --git a/lib/portage/dbapi/vartree.py b/lib/portage/dbapi/vartree.py
index c827d4a54..e15a258aa 100644
--- a/lib/portage/dbapi/vartree.py
+++ b/lib/portage/dbapi/vartree.py
@@ -1728,6 +1728,38 @@ class vartree:
         self.populated = 1
 
 
+def _find_unneeded_preserved_nodes(lib_graph, preserved_nodes):
+    """
+    Given a graph of libraries in which the parents of a node are its
+    consumers, find the preserved libraries which are not needed by
+    anything.
+
+    Repeatedly take the preserved libraries which have no consumers left
+    and drop them from the graph, which may in turn leave other preserved
+    libraries without consumers.
+
+    @param lib_graph: graph in which an edge from a consumer (parent) to a
+            library (child) means that the consumer links against the library.
+            It is modified in place.
+    @type lib_graph: digraph
+    @param preserved_nodes: the subset of nodes in lib_graph which are
+            preserved libraries
+    @type preserved_nodes: set
+    @rtype: set
+    @return: the subset of preserved_nodes which is not needed
+    """
+    unneeded_nodes = set()
+
+    while lib_graph:
+        root_nodes = preserved_nodes.intersection(lib_graph.root_nodes())
+        if not root_nodes:
+            break
+        lib_graph.difference_update(root_nodes)
+        unneeded_nodes.update(root_nodes)
+
+    return unneeded_nodes
+
+
 class dblink:
     """
     This class provides an interface to the installed package database
@@ -3718,35 +3750,30 @@ class dblink:
                     break
 
         cpv_lib_map = {}
-        while lib_graph:
-            root_nodes = preserved_nodes.intersection(lib_graph.root_nodes())
-            if not root_nodes:
-                break
-            lib_graph.difference_update(root_nodes)
-            unlink_list = set()
-            for node in root_nodes:
-                unlink_list.update(node.alt_paths)
-            unlink_list = sorted(unlink_list)
-            for obj in unlink_list:
-                cpv = path_cpv_map.get(obj)
-                if cpv is None:
-                    # This means that a symlink is in the preserved libs
-                    # registry, but the actual lib it points to is not.
-                    self._display_merge(
-                        _(
-                            "!!! symlink to lib is preserved, "
-                            "but not the lib itself:\n!!! '%s'\n"
-                        )
-                        % (obj,),
-                        level=logging.ERROR,
-                        noiselevel=-1,
+        unlink_list = set()
+        for node in _find_unneeded_preserved_nodes(lib_graph, preserved_nodes):
+            unlink_list.update(node.alt_paths)
+
+        for obj in sorted(unlink_list):
+            cpv = path_cpv_map.get(obj)
+            if cpv is None:
+                # This means that a symlink is in the preserved libs
+                # registry, but the actual lib it points to is not.
+                self._display_merge(
+                    _(
+                        "!!! symlink to lib is preserved, "
+                        "but not the lib itself:\n!!! '%s'\n"
                     )
-                    continue
-                removed = cpv_lib_map.get(cpv)
-                if removed is None:
-                    removed = set()
-                    cpv_lib_map[cpv] = removed
-                removed.add(obj)
+                    % (obj,),
+                    level=logging.ERROR,
+                    noiselevel=-1,
+                )
+                continue
+            removed = cpv_lib_map.get(cpv)
+            if removed is None:
+                removed = set()
+                cpv_lib_map[cpv] = removed
+            removed.add(obj)
 
         return cpv_lib_map
 

diff --git a/lib/portage/tests/dbapi/meson.build b/lib/portage/tests/dbapi/meson.build
index f7a360e1c..6d83f11f7 100644
--- a/lib/portage/tests/dbapi/meson.build
+++ b/lib/portage/tests/dbapi/meson.build
@@ -5,6 +5,7 @@ py.install_sources(
         'test_bintree_build_id.py',
         'test_fakedbapi.py',
         'test_portdb_cache.py',
+        'test_preserved_libs.py',
         '__init__.py',
         '__test__.py',
     ],

diff --git a/lib/portage/tests/dbapi/test_preserved_libs.py b/lib/portage/tests/dbapi/test_preserved_libs.py
new file mode 100644
index 000000000..6cd881e31
--- /dev/null
+++ b/lib/portage/tests/dbapi/test_preserved_libs.py
@@ -0,0 +1,48 @@
+# Copyright 2026 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+from portage.dbapi.vartree import _find_unneeded_preserved_nodes
+from portage.tests import TestCase
+from portage.util.digraph import digraph
+
+
+class FindUnneededPreservedNodesTestCase(TestCase):
+    """
+    Tests for the graph analysis which decides that a preserved library
+    has no consumers left. In the graph, the parents of a node are its
+    consumers.
+    """
+
+    def _build(self, edges, preserved):
+        # Each edge is a (consumer, library) pair.
+        graph = digraph()
+        for node in preserved:
+            graph.add(node, None)
+        for consumer, lib in edges:
+            graph.add(lib, consumer)
+        return graph, set(preserved)
+
+    def testNoConsumers(self):
+        graph, preserved = self._build([], ["libfoo"])
+        self.assertEqual(_find_unneeded_preserved_nodes(graph, preserved), {"libfoo"})
+
+    def testInstalledConsumer(self):
+        graph, preserved = self._build([("bar", "libfoo")], ["libfoo"])
+        self.assertEqual(_find_unneeded_preserved_nodes(graph, preserved), set())
+
+    def testChainOfPreservedLibs(self):
+        # libbar is preserved and consumes libfoo, and nothing consumes
+        # libbar, so both are unneeded.
+        graph, preserved = self._build([("libbar", "libfoo")], ["libfoo", "libbar"])
+        self.assertEqual(
+            _find_unneeded_preserved_nodes(graph, preserved),
+            {"libfoo", "libbar"},
+        )
+
+    def testChainWithInstalledConsumer(self):
+        # An installed consumer at the head of the chain keeps everything
+        # in the chain alive.
+        graph, preserved = self._build(
+            [("baz", "libbar"), ("libbar", "libfoo")], ["libfoo", "libbar"]
+        )
+        self.assertEqual(_find_unneeded_preserved_nodes(graph, preserved), set())
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.