public inbox for gentoo-commits@lists.gentoo.org
 help / color / mirror / Atom feed
* [gentoo-commits] proj/portage:master commit in: lib/_emerge/, lib/portage/tests/emerge/, lib/portage/tests/dep/, ...
@ 2023-12-10 22:01 Sam James
  0 siblings, 0 replies; only message in thread
From: Sam James @ 2023-12-10 22:01 UTC (permalink / raw
  To: gentoo-commits

commit:     af550b8b5cb91f27b26d6800c3b4cdd2d86a46e6
Author:     Sam James <sam <AT> gentoo <DOT> org>
AuthorDate: Mon Sep  4 15:38:18 2023 +0000
Commit:     Sam James <sam <AT> gentoo <DOT> org>
CommitDate: Sun Dec 10 22:01:48 2023 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=af550b8b

ebuild: inject implicit libc RDEPEND

Inject >=${LIBC_PROVIDER}-${VERSION_OF_LIBC_PROVIDER} into RDEPEND.

We already try to upgrade the virtual/libc provider and its deps aggressively
but that's not so helpful if there's a binpkg for, say, one of its deps available
built against a newer glibc - which ends up breaking the system because our
glibc isn't new enough (symbol versioning).

This is a long-standing problem for binpkg users and one of the last showstoppers
for being able to use them reliably.

Note that this applies to source installs too, although it matters less there;
it'll make downgrading libc a bit harder for people who want to do that, but users
are already prevented from doing that (pkg_* check) for glibc, and I don't really
see it as a bad thing to effectively propagate this to other libcs.

To fully solve the problem, we should arguably do at least one of the following:
1) Implicit >= on anything (not just libc) which installs ELF (at least a PROVIDES);
2) Implement the suggestion in bug #753500 based on analysis of used versioned symbols (*).

But libc is really the critical one and the one where things explode pretty badly,
especially combined with us trying to Do The Right Thing for non-binpkg cases
(aggressively upgrading libc before anything else). The other cases don't matter
so much as they get upgraded later and by that point, the library is usually done.

(It's not really clear if downgrading musl works and even if it is supported,
I'm not really sure it's a supported case at all, so I'm not bothering to carve
out an exception here. It'd make this far less elegant and I don't see any benefit
to doing so.)

(*) util-linux is one of the examples I have in mind here as justification
for either point.

Bug: https://bugs.gentoo.org/753500
Bug: https://bugs.gentoo.org/913628
Signed-off-by: Sam James <sam <AT> gentoo.org>

 lib/_emerge/actions.py                           |   7 +-
 lib/_emerge/depgraph.py                          |  22 +
 lib/portage/dep/__init__.py                      |  23 +-
 lib/portage/dep/libc.py                          |  83 ++++
 lib/portage/dep/meson.build                      |   1 +
 lib/portage/package/ebuild/doebuild.py           |  56 ++-
 lib/portage/tests/dep/meson.build                |   1 +
 lib/portage/tests/dep/test_libc.py               |  81 ++++
 lib/portage/tests/emerge/meson.build             |   1 +
 lib/portage/tests/emerge/test_actions.py         |  23 +-
 lib/portage/tests/emerge/test_libc_dep_inject.py | 551 +++++++++++++++++++++++
 11 files changed, 831 insertions(+), 18 deletions(-)

diff --git a/lib/_emerge/actions.py b/lib/_emerge/actions.py
index dbd9707a82..ae8796531e 100644
--- a/lib/_emerge/actions.py
+++ b/lib/_emerge/actions.py
@@ -41,6 +41,7 @@ from portage.dbapi._expand_new_virt import expand_new_virt
 from portage.dbapi.IndexedPortdb import IndexedPortdb
 from portage.dbapi.IndexedVardb import IndexedVardb
 from portage.dep import Atom, _repo_separator, _slot_separator
+from portage.dep.libc import find_libc_deps
 from portage.exception import (
     InvalidAtom,
     InvalidData,
@@ -2786,10 +2787,8 @@ def relative_profile_path(portdir, abs_profile):
 
 def get_libc_version(vardb: portage.dbapi.vartree.vardbapi) -> list[str]:
     libcver = []
-    libclist = set()
-    for atom in expand_new_virt(vardb, portage.const.LIBC_PACKAGE_ATOM):
-        if not atom.blocker:
-            libclist.update(vardb.match(atom))
+    libclist = find_libc_deps(vardb, True)
+
     if libclist:
         for cpv in sorted(libclist):
             libc_split = portage.catpkgsplit(cpv)[1:]

diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py
index 59c78c7354..4612ac2049 100644
--- a/lib/_emerge/depgraph.py
+++ b/lib/_emerge/depgraph.py
@@ -36,6 +36,7 @@ from portage.dep import (
     match_from_list,
     _repo_separator,
 )
+from portage.dep.libc import find_libc_deps, strip_libc_deps
 from portage.dep._slot_operator import ignore_built_slot_operator_deps, strip_slots
 from portage.eapi import eapi_has_strong_blocks, eapi_has_required_use, _get_eapi_attrs
 from portage.exception import (
@@ -2984,6 +2985,19 @@ class depgraph:
             else:
                 depvars = Package._runtime_keys
 
+            eroot = pkg.root_config.settings["EROOT"]
+            try:
+                libc_deps = self._frozen_config._libc_deps_cache[eroot]
+            except (AttributeError, KeyError) as e:
+                if isinstance(e, AttributeError):
+                    self._frozen_config._libc_deps_cache = {}
+
+                self._frozen_config._libc_deps_cache[eroot] = find_libc_deps(
+                    self._frozen_config._trees_orig[eroot]["vartree"].dbapi,
+                    False,
+                )
+            libc_deps = self._frozen_config._libc_deps_cache[eroot]
+
             # Use _raw_metadata, in order to avoid interaction
             # with --dynamic-deps.
             try:
@@ -2996,6 +3010,10 @@ class depgraph:
                         token_class=Atom,
                     )
                     strip_slots(dep_struct)
+                    # This strip_libc_deps call is done with non-realized deps;
+                    # we can change that later if we're having trouble with
+                    # matching/intersecting them.
+                    strip_libc_deps(dep_struct, libc_deps)
                     built_deps.append(dep_struct)
             except InvalidDependString:
                 changed = True
@@ -3009,6 +3027,10 @@ class depgraph:
                         token_class=Atom,
                     )
                     strip_slots(dep_struct)
+                    # This strip_libc_deps call is done with non-realized deps;
+                    # we can change that later if we're having trouble with
+                    # matching/intersecting them.
+                    strip_libc_deps(dep_struct, libc_deps)
                     unbuilt_deps.append(dep_struct)
 
                 changed = built_deps != unbuilt_deps

diff --git a/lib/portage/dep/__init__.py b/lib/portage/dep/__init__.py
index a544221532..04f857c999 100644
--- a/lib/portage/dep/__init__.py
+++ b/lib/portage/dep/__init__.py
@@ -57,7 +57,10 @@ from portage.versions import (
     ververify,
 )
 import portage.cache.mappings
+from typing import TYPE_CHECKING
 
+if TYPE_CHECKING:
+    import _emerge.Package
 
 # \w is [a-zA-Z0-9_]
 
@@ -1724,7 +1727,7 @@ class Atom(str):
                 )
 
     @property
-    def slot_operator_built(self):
+    def slot_operator_built(self) -> bool:
         """
         Returns True if slot_operator == "=" and sub_slot is not None.
         NOTE: foo/bar:2= is unbuilt and returns False, whereas foo/bar:2/2=
@@ -1733,7 +1736,7 @@ class Atom(str):
         return self.slot_operator == "=" and self.sub_slot is not None
 
     @property
-    def without_repo(self):
+    def without_repo(self) -> "Atom":
         if self.repo is None:
             return self
         return Atom(
@@ -1741,7 +1744,7 @@ class Atom(str):
         )
 
     @property
-    def without_slot(self):
+    def without_slot(self) -> "Atom":
         if self.slot is None and self.slot_operator is None:
             return self
         atom = remove_slot(self)
@@ -1751,7 +1754,7 @@ class Atom(str):
             atom += str(self.use)
         return Atom(atom, allow_repo=True, allow_wildcard=True)
 
-    def with_repo(self, repo):
+    def with_repo(self, repo) -> "Atom":
         atom = remove_slot(self)
         if self.slot is not None or self.slot_operator is not None:
             atom += _slot_separator
@@ -1766,7 +1769,7 @@ class Atom(str):
             atom += str(self.use)
         return Atom(atom, allow_repo=True, allow_wildcard=True)
 
-    def with_slot(self, slot):
+    def with_slot(self, slot) -> "Atom":
         atom = remove_slot(self) + _slot_separator + slot
         if self.repo is not None:
             atom += _repo_separator + self.repo
@@ -1779,7 +1782,7 @@ class Atom(str):
             "Atom instances are immutable", self.__class__, name, value
         )
 
-    def intersects(self, other):
+    def intersects(self, other: "Atom") -> bool:
         """
         Atoms with different cpv, operator or use attributes cause this method
         to return False even though there may actually be some intersection.
@@ -1809,7 +1812,7 @@ class Atom(str):
 
         return False
 
-    def evaluate_conditionals(self, use):
+    def evaluate_conditionals(self, use: set) -> "Atom":
         """
         Create an atom instance with any USE conditionals evaluated.
         @param use: The set of enabled USE flags
@@ -1837,7 +1840,9 @@ class Atom(str):
             _use=use_dep,
         )
 
-    def violated_conditionals(self, other_use, is_valid_flag, parent_use=None):
+    def violated_conditionals(
+        self, other_use: set, is_valid_flag: callable, parent_use=None
+    ) -> "Atom":
         """
         Create an atom instance with any USE conditional removed, that is
         satisfied by other_use.
@@ -1900,7 +1905,7 @@ class Atom(str):
         memo[id(self)] = self
         return self
 
-    def match(self, pkg):
+    def match(self, pkg: "_emerge.Package"):
         """
         Check if the given package instance matches this atom.
 

diff --git a/lib/portage/dep/libc.py b/lib/portage/dep/libc.py
new file mode 100644
index 0000000000..db88432cbc
--- /dev/null
+++ b/lib/portage/dep/libc.py
@@ -0,0 +1,83 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+from portage.dep import Atom
+from portage.const import LIBC_PACKAGE_ATOM
+from portage.dbapi._expand_new_virt import expand_new_virt
+
+import portage.dbapi.porttree
+
+
+def find_libc_deps(portdb: portage.dbapi.porttree.dbapi, realized: bool = False):
+    """Finds libc package for a ROOT via portdb.
+
+    Parameters
+    ----------
+    portdb : dbapi
+        dbapi instance for portdb (for installed packages).
+    realized : bool
+        Request installed atoms rather than the installed package satisfying LIBC_PACKAGE_ATOM.
+
+    Returns
+    -------
+    list
+        List of libc packages (or atoms if realized is passed).
+    """
+
+    libc_pkgs = set()
+
+    for atom in expand_new_virt(
+        portdb,
+        LIBC_PACKAGE_ATOM,
+    ):
+        if atom.blocker:
+            continue
+
+        if not realized:
+            # Just the raw packages were requested (whatever satifies the virtual)
+            libc_pkgs.add(atom)
+            continue
+
+        # This will give us something like sys-libs/glibc:2.2, but we want to know
+        # what installed atom actually satifies that.
+        try:
+            libc_pkgs.add(portdb.match(atom)[0])
+        except IndexError:
+            continue
+
+    return libc_pkgs
+
+
+def strip_libc_deps(dep_struct: list, libc_deps: set):
+    """Strip libc dependency out of a given dependency strucutre.
+
+    Parameters
+    ----------
+    dep_struct: list
+        List of package dependencies (atoms).
+
+    libc_deps: set
+        List of dependencies satisfying LIBC_PACKAGE_ATOM to be
+        stripped out of any dependencies.
+
+    Returns
+    -------
+    list
+        List of dependencies with any matching libc_deps removed.
+    """
+    # We're going to just grab the libc provider for ROOT and
+    # strip out any dep for the purposes of --changed-deps.
+    # We can't go off versions, even though it'd be more precise
+    # (see below), because we'd end up with FPs and unnecessary
+    # --changed-deps results far too often.
+    #
+    # This penalizes a bit the case where someone adds a
+    # minimum (or maximum) version of libc explicitly in an ebuild
+    # without a new revision, but that's extremely rare, and doesn't
+    # feel like it changes the balance for what we prefer here.
+
+    for i, x in reversed(list(enumerate(dep_struct))):
+        # We only need to bother if x is an Atom because we know the deps
+        # we inject are simple & flat.
+        if isinstance(x, Atom) and any(x.cp == libc_dep.cp for libc_dep in libc_deps):
+            del dep_struct[i]

diff --git a/lib/portage/dep/meson.build b/lib/portage/dep/meson.build
index ea1e8cad62..d2379d8cb8 100644
--- a/lib/portage/dep/meson.build
+++ b/lib/portage/dep/meson.build
@@ -1,6 +1,7 @@
 py.install_sources(
     [
         'dep_check.py',
+        'libc.py',
         '_dnf.py',
         '_slot_operator.py',
         '__init__.py',

diff --git a/lib/portage/package/ebuild/doebuild.py b/lib/portage/package/ebuild/doebuild.py
index 346c989acc..956f8c0489 100644
--- a/lib/portage/package/ebuild/doebuild.py
+++ b/lib/portage/package/ebuild/doebuild.py
@@ -76,6 +76,7 @@ from portage.dep import (
     paren_enclose,
     use_reduce,
 )
+from portage.dep.libc import find_libc_deps
 from portage.eapi import (
     eapi_exports_KV,
     eapi_exports_merge_type,
@@ -118,7 +119,7 @@ from portage.util.futures.executor.fork import ForkExecutor
 from portage.util.path import first_existing
 from portage.util.socks5 import get_socks5_proxy
 from portage.util._dyn_libs.dyn_libs import check_dyn_libs_inconsistent
-from portage.versions import _pkgsplit
+from portage.versions import _pkgsplit, pkgcmp
 from _emerge.BinpkgEnvExtractor import BinpkgEnvExtractor
 from _emerge.EbuildBuildDir import EbuildBuildDir
 from _emerge.EbuildPhase import EbuildPhase
@@ -2569,6 +2570,7 @@ def _post_src_install_write_metadata(settings):
         ) as f:
             f.write(f"{v}\n")
 
+
 def _preinst_bsdflags(mysettings):
     if bsd_chflags:
         # Save all the file flags for restoration later.
@@ -2848,6 +2850,48 @@ def _reapply_bsdflags_to_image(mysettings):
         )
 
 
+def _inject_libc_dep(build_info_dir, mysettings):
+    #
+    # We could skip this for non-binpkgs but there doesn't seem to be much
+    # value in that, as users shouldn't downgrade libc anyway.
+    injected_libc_depstring = []
+    for libc_realized_atom in find_libc_deps(
+        QueryCommand.get_db()[mysettings["EROOT"]]["vartree"].dbapi, True
+    ):
+        if pkgcmp(mysettings.mycpv, libc_realized_atom) is not None:
+            # We don't want to inject deps on ourselves (libc)
+            injected_libc_depstring = []
+            break
+
+        injected_libc_depstring.append(f">={libc_realized_atom}")
+
+    rdepend_file = os.path.join(build_info_dir, "RDEPEND")
+    # Slurp the existing contents because we need to mangle it a bit
+    # It'll look something like (if it exists):
+    # ```
+    # app-misc/foo dev-libs/bar
+    # <newline>
+    # ````
+    rdepend = None
+    if os.path.exists(rdepend_file):
+        with open(rdepend_file, encoding="utf-8") as f:
+            rdepend = f.readlines()
+        rdepend = "\n".join(rdepend).strip()
+
+    # For RDEPEND, we want an implicit dependency on >=${PROVIDER_OF_LIBC}
+    # to avoid runtime breakage when merging binpkgs, see bug #753500.
+    #
+    if injected_libc_depstring:
+        if rdepend:
+            rdepend += f" {' '.join(injected_libc_depstring).strip()}"
+        else:
+            # The package doesn't have an RDEPEND, so make one up.
+            rdepend = " ".join(injected_libc_depstring)
+
+        with open(rdepend_file, "w", encoding="utf-8") as f:
+            f.write(f"{rdepend}\n")
+
+
 def _post_src_install_soname_symlinks(mysettings, out):
     """
     Check that libraries in $D have corresponding soname symlinks.
@@ -2857,9 +2901,8 @@ def _post_src_install_soname_symlinks(mysettings, out):
     """
 
     image_dir = mysettings["D"]
-    needed_filename = os.path.join(
-        mysettings["PORTAGE_BUILDDIR"], "build-info", "NEEDED.ELF.2"
-    )
+    build_info_dir = os.path.join(mysettings["PORTAGE_BUILDDIR"], "build-info")
+    needed_filename = os.path.join(build_info_dir, "NEEDED.ELF.2")
 
     f = None
     try:
@@ -2879,6 +2922,11 @@ def _post_src_install_soname_symlinks(mysettings, out):
         if f is not None:
             f.close()
 
+    # We do RDEPEND mangling here instead of the natural location
+    # in _post_src_install_write_metadata because NEEDED hasn't been
+    # written yet at that point.
+    _inject_libc_dep(build_info_dir, mysettings)
+
     metadata = {}
     for k in ("QA_PREBUILT", "QA_SONAME_NO_SYMLINK"):
         try:

diff --git a/lib/portage/tests/dep/meson.build b/lib/portage/tests/dep/meson.build
index 2097b02f9e..7350f7775a 100644
--- a/lib/portage/tests/dep/meson.build
+++ b/lib/portage/tests/dep/meson.build
@@ -15,6 +15,7 @@ py.install_sources(
         'test_get_required_use_flags.py',
         'test_isjustname.py',
         'test_isvalidatom.py',
+        'test_libc.py',
         'test_match_from_list.py',
         'test_overlap_dnf.py',
         'test_paren_reduce.py',

diff --git a/lib/portage/tests/dep/test_libc.py b/lib/portage/tests/dep/test_libc.py
new file mode 100644
index 0000000000..6ea96d720b
--- /dev/null
+++ b/lib/portage/tests/dep/test_libc.py
@@ -0,0 +1,81 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+from portage.dep import Atom
+from portage.dep.libc import strip_libc_deps
+from portage.tests import TestCase
+
+
+class LibcUtilStripDeps(TestCase):
+    def testStripSimpleDeps(self):
+        """
+        Test that we strip a basic libc dependency out and return
+        a list of dependencies without it in there.
+        """
+
+        libc_dep = [Atom("=sys-libs/glibc-2.38")]
+
+        original_deps = (
+            [
+                Atom("=sys-libs/glibc-2.38"),
+                Atom("=app-misc/foo-1.2.3"),
+            ],
+            [
+                Atom("=sys-libs/glibc-2.38"),
+            ],
+            [
+                Atom("=app-misc/foo-1.2.3"),
+                Atom("=app-misc/bar-1.2.3"),
+            ],
+        )
+
+        for deplist in original_deps:
+            strip_libc_deps(deplist, libc_dep)
+
+            self.assertFalse(
+                all(libc in deplist for libc in libc_dep),
+                "Stripped deplist contains a libc candidate",
+            )
+
+    def testStripComplexRealizedDeps(self):
+        """
+        Test that we strip pathological libc dependencies out and return
+        a list of dependencies without it in there.
+        """
+
+        # This shouldn't really happen for a 'realized' dependency, but
+        # we shouldn't crash if it happens anyway.
+        libc_dep = [Atom("=sys-libs/glibc-2.38*[p]")]
+
+        original_deps = (
+            [
+                Atom("=sys-libs/glibc-2.38[x]"),
+                Atom("=app-misc/foo-1.2.3"),
+            ],
+            [
+                Atom("=sys-libs/glibc-2.38[p]"),
+            ],
+            [
+                Atom("=app-misc/foo-1.2.3"),
+                Atom("=app-misc/bar-1.2.3"),
+            ],
+        )
+
+        for deplist in original_deps:
+            strip_libc_deps(deplist, libc_dep)
+
+            self.assertFalse(
+                all(libc in deplist for libc in libc_dep),
+                "Stripped deplist contains a libc candidate",
+            )
+
+    def testStripNonRealizedDeps(self):
+        """
+        Check that we strip non-realized libc deps.
+        """
+
+        libc_dep = [Atom("sys-libs/glibc:2.2=")]
+        original_deps = [Atom(">=sys-libs/glibc-2.38-r7")]
+
+        strip_libc_deps(original_deps, libc_dep)
+        self.assertFalse(original_deps, "(g)libc dep was not stripped")

diff --git a/lib/portage/tests/emerge/meson.build b/lib/portage/tests/emerge/meson.build
index b42945123c..0d34cbecf7 100644
--- a/lib/portage/tests/emerge/meson.build
+++ b/lib/portage/tests/emerge/meson.build
@@ -6,6 +6,7 @@ py.install_sources(
         'test_emerge_slot_abi.py',
         'test_global_updates.py',
         'test_baseline.py',
+        'test_libc_dep_inject.py',
         '__init__.py',
         '__test__.py',
     ],

diff --git a/lib/portage/tests/emerge/test_actions.py b/lib/portage/tests/emerge/test_actions.py
index 17e8b7a2b9..cdc087a8e3 100644
--- a/lib/portage/tests/emerge/test_actions.py
+++ b/lib/portage/tests/emerge/test_actions.py
@@ -3,7 +3,11 @@
 
 from unittest.mock import MagicMock, patch
 
-from _emerge.actions import run_action
+from _emerge.actions import get_libc_version, run_action
+
+from portage.const import LIBC_PACKAGE_ATOM
+from portage.dbapi.virtual import fakedbapi
+from portage.dep import Atom
 from portage.tests import TestCase
 
 
@@ -45,3 +49,20 @@ class RunActionTestCase(TestCase):
         bt.populate.assert_called_once_with(
             getbinpkgs=False, getbinpkg_refresh=True, pretend=False
         )
+
+    def testGetSystemLibc(self):
+        """
+        Check that get_libc_version extracts the right version string
+        from the provider LIBC_PACKAGE_ATOM for emerge --info and friends.
+        """
+        settings = MagicMock()
+
+        settings.getvirtuals.return_value = {
+            LIBC_PACKAGE_ATOM: [Atom("=sys-libs/musl-1.2.3")]
+        }
+        settings.__getitem__.return_value = {}
+
+        vardb = fakedbapi(settings)
+        vardb.cpv_inject("sys-libs/musl-1.2.3", {"SLOT": "0"})
+
+        self.assertEqual(get_libc_version(vardb), ["musl-1.2.3"])

diff --git a/lib/portage/tests/emerge/test_libc_dep_inject.py b/lib/portage/tests/emerge/test_libc_dep_inject.py
new file mode 100644
index 0000000000..10a4ae4120
--- /dev/null
+++ b/lib/portage/tests/emerge/test_libc_dep_inject.py
@@ -0,0 +1,551 @@
+# Copyright 2016-2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+import subprocess
+import sys
+import textwrap
+
+import portage
+from portage import os
+from portage import _unicode_decode
+from portage.const import PORTAGE_PYM_PATH, USER_CONFIG_PATH
+from portage.process import find_binary
+from portage.tests import TestCase
+from portage.util import ensure_dirs
+
+from portage.tests.resolver.ResolverPlayground import (
+    ResolverPlayground,
+    ResolverPlaygroundTestCase,
+)
+
+
+class LibcDepInjectEmergeTestCase(TestCase):
+    def testLibcDepInjection(self):
+        """
+        Test whether the implicit libc dependency injection (bug #913628)
+        is correctly added for only ebuilds installing an ELF binary.
+
+        Based on BlockerFileCollisionEmergeTestCase.
+        """
+        debug = False
+
+        install_elf = textwrap.dedent(
+            """
+        S="${WORKDIR}"
+
+        src_install() {
+            insinto /usr/bin
+            # We need an ELF binary for the injection to trigger
+            cp "${BASH}" "${ED}"/usr/bin/${PN} || die
+        }
+        """
+        )
+
+        ebuilds = {
+            "sys-libs/glibc-2.38": {
+                "EAPI": "8",
+                "MISC_CONTENT": install_elf,
+            },
+            "virtual/libc-1": {
+                "EAPI": "8",
+                "RDEPEND": "sys-libs/glibc",
+            },
+            "dev-libs/A-1": {
+                "EAPI": "8",
+                "MISC_CONTENT": install_elf,
+            },
+            "dev-libs/B-1": {
+                "EAPI": "8",
+            },
+            "dev-libs/C-1": {
+                "EAPI": "8",
+                "MISC_CONTENT": install_elf,
+            },
+            "dev-libs/D-1": {
+                "EAPI": "8",
+            },
+            "dev-libs/E-1": {
+                "EAPI": "8",
+                "RDEPEND": ">=dev-libs/D-1",
+                "MISC_CONTENT": install_elf,
+            },
+        }
+
+        world = ("dev-libs/A",)
+
+        playground = ResolverPlayground(ebuilds=ebuilds, world=world, debug=debug)
+        settings = playground.settings
+        eprefix = settings["EPREFIX"]
+        eroot = settings["EROOT"]
+        var_cache_edb = os.path.join(eprefix, "var", "cache", "edb")
+        user_config_dir = os.path.join(eprefix, USER_CONFIG_PATH)
+
+        portage_python = portage._python_interpreter
+        emerge_cmd = (
+            portage_python,
+            "-b",
+            "-Wd",
+            os.path.join(str(self.bindir), "emerge"),
+        )
+
+        test_commands = (
+            # If we install a package with an ELF but no libc provider is installed,
+            # make sure we don't inject anything (we don't want to have some bare RDEPEND with
+            # literally "[]").
+            emerge_cmd
+            + (
+                "--oneshot",
+                "dev-libs/C",
+            ),
+            (
+                lambda: not portage.util.grablines(
+                    os.path.join(
+                        eprefix, "var", "db", "pkg", "dev-libs", "C-1", "RDEPEND"
+                    )
+                ),
+            ),
+            # (We need sys-libs/glibc pulled in and virtual/libc installed)
+            emerge_cmd
+            + (
+                "--oneshot",
+                "virtual/libc",
+            ),
+            # A package NOT installing an ELF binary shouldn't have an injected libc dep
+            # Let's check the virtual/libc one as we already have to merge it to pull in
+            # sys-libs/glibc, but we'll do a better check after too.
+            (
+                lambda: ">=sys-libs/glibc-2.38\n"
+                not in portage.util.grablines(
+                    os.path.join(
+                        eprefix, "var", "db", "pkg", "virtual", "libc-1", "RDEPEND"
+                    )
+                ),
+            ),
+            # A package NOT installing an ELF binary shouldn't have an injected libc dep
+            emerge_cmd
+            + (
+                "--oneshot",
+                "dev-libs/B",
+            ),
+            (
+                lambda: not portage.util.grablines(
+                    os.path.join(
+                        eprefix, "var", "db", "pkg", "dev-libs", "B-1", "RDEPEND"
+                    )
+                ),
+            ),
+            # A package installing an ELF binary should have an injected libc dep
+            emerge_cmd
+            + (
+                "--oneshot",
+                "dev-libs/A",
+            ),
+            (lambda: os.path.exists(os.path.join(eroot, "usr/bin/A")),),
+            (
+                lambda: ">=sys-libs/glibc-2.38\n"
+                in portage.util.grablines(
+                    os.path.join(
+                        eprefix, "var", "db", "pkg", "dev-libs", "A-1", "RDEPEND"
+                    )
+                ),
+            ),
+            # Install glibc again because earlier, no libc was installed, so the injection
+            # wouldn't have fired even if the "are we libc?" check was broken.
+            emerge_cmd
+            + (
+                "--oneshot",
+                "sys-libs/glibc",
+            ),
+            # We don't want the libc (sys-libs/glibc is the provider here) to have an injected dep on itself
+            (
+                lambda: ">=sys-libs/glibc-2.38\n"
+                not in portage.util.grablines(
+                    os.path.join(
+                        eprefix, "var", "db", "pkg", "sys-libs", "glibc-2.38", "RDEPEND"
+                    )
+                ),
+            ),
+            # Make sure we append to, not clobber, RDEPEND
+            emerge_cmd
+            + (
+                "--oneshot",
+                "dev-libs/E",
+            ),
+            (
+                lambda: [">=dev-libs/D-1 >=sys-libs/glibc-2.38\n"]
+                == portage.util.grablines(
+                    os.path.join(
+                        eprefix, "var", "db", "pkg", "dev-libs", "E-1", "RDEPEND"
+                    )
+                ),
+            ),
+        )
+
+        fake_bin = os.path.join(eprefix, "bin")
+        portage_tmpdir = os.path.join(eprefix, "var", "tmp", "portage")
+        profile_path = settings.profile_path
+
+        path = os.environ.get("PATH")
+        if path is not None and not path.strip():
+            path = None
+        if path is None:
+            path = ""
+        else:
+            path = ":" + path
+        path = fake_bin + path
+
+        pythonpath = os.environ.get("PYTHONPATH")
+        if pythonpath is not None and not pythonpath.strip():
+            pythonpath = None
+        if pythonpath is not None and pythonpath.split(":")[0] == PORTAGE_PYM_PATH:
+            pass
+        else:
+            if pythonpath is None:
+                pythonpath = ""
+            else:
+                pythonpath = ":" + pythonpath
+            pythonpath = PORTAGE_PYM_PATH + pythonpath
+
+        env = {
+            "PORTAGE_OVERRIDE_EPREFIX": eprefix,
+            "PATH": path,
+            "PORTAGE_PYTHON": portage_python,
+            "PORTAGE_REPOSITORIES": settings.repositories.config_string(),
+            "PYTHONDONTWRITEBYTECODE": os.environ.get("PYTHONDONTWRITEBYTECODE", ""),
+            "PYTHONPATH": pythonpath,
+            "PORTAGE_INST_GID": str(os.getgid()),
+            "PORTAGE_INST_UID": str(os.getuid()),
+            "FEATURES": "-qa-unresolved-soname-deps -preserve-libs -merge-sync",
+        }
+
+        dirs = [
+            playground.distdir,
+            fake_bin,
+            portage_tmpdir,
+            user_config_dir,
+            var_cache_edb,
+        ]
+
+        true_symlinks = ["chown", "chgrp"]
+
+        # We don't want to make pax-utils a hard-requirement for tests,
+        # so if it's not found, skip the test rather than FAIL it.
+        needed_binaries = {
+            "true": (find_binary("true"), True),
+            "scanelf": (find_binary("scanelf"), False),
+            "find": (find_binary("find"), True),
+        }
+
+        for name, (path, mandatory) in needed_binaries.items():
+            found = path is not None
+
+            if not found:
+                if mandatory:
+                    self.assertIsNotNone(path, f"command {name} not found")
+                else:
+                    self.skipTest(f"{name} not found")
+
+        try:
+            for d in dirs:
+                ensure_dirs(d)
+            for x in true_symlinks:
+                os.symlink(needed_binaries["true"][0], os.path.join(fake_bin, x))
+
+            # We need scanelf, find for the ELF parts (creating NEEDED)
+            os.symlink(needed_binaries["scanelf"][0], os.path.join(fake_bin, "scanelf"))
+            os.symlink(needed_binaries["find"][0], os.path.join(fake_bin, "find"))
+
+            with open(os.path.join(var_cache_edb, "counter"), "wb") as f:
+                f.write(b"100")
+            with open(os.path.join(profile_path, "packages"), "w") as f:
+                f.write("*virtual/libc")
+
+            if debug:
+                # The subprocess inherits both stdout and stderr, for
+                # debugging purposes.
+                stdout = None
+            else:
+                # The subprocess inherits stderr so that any warnings
+                # triggered by python -Wd will be visible.
+                stdout = subprocess.PIPE
+
+            for i, args in enumerate(test_commands):
+                if hasattr(args[0], "__call__"):
+                    self.assertTrue(args[0](), f"callable at index {i} failed")
+                    continue
+
+                if isinstance(args[0], dict):
+                    local_env = env.copy()
+                    local_env.update(args[0])
+                    args = args[1:]
+                else:
+                    local_env = env
+
+                proc = subprocess.Popen(args, env=local_env, stdout=stdout)
+
+                if debug:
+                    proc.wait()
+                else:
+                    output = proc.stdout.readlines()
+                    proc.wait()
+                    proc.stdout.close()
+                    if proc.returncode != os.EX_OK:
+                        for line in output:
+                            sys.stderr.write(_unicode_decode(line))
+
+                self.assertEqual(
+                    os.EX_OK, proc.returncode, f"emerge failed with args {args}"
+                )
+
+            # Check that dev-libs/A doesn't get re-emerged via --changed-deps
+            # after injecting the libc dep. We want to suppress the injected
+            # dep in the changed-deps comparisons.
+            k = ResolverPlaygroundTestCase(
+                ["@world"],
+                options={
+                    "--changed-deps": True,
+                    "--deep": True,
+                    "--update": True,
+                    "--verbose": True,
+                },
+                success=True,
+                mergelist=[],
+            )
+            playground.run_TestCase(k)
+            self.assertEqual(k.test_success, True, k.fail_msg)
+        finally:
+            playground.debug = False
+            playground.cleanup()
+
+    def testBinpkgLibcDepInjection(self):
+        """
+        Test whether the implicit libc dependency injection (bug #913628)
+        correctly forces an upgrade to a newer glibc before merging a binpkg
+        built against it.
+
+        Based on BlockerFileCollisionEmergeTestCase.
+        """
+        debug = False
+
+        install_elf = textwrap.dedent(
+            """
+        S="${WORKDIR}"
+
+        src_install() {
+            insinto /usr/bin
+            # We need an ELF binary for the injection to trigger, so
+            # use ${BASH} given we know it must be around for running ebuilds.
+            cp "${BASH}" "${ED}"/usr/bin/${PN} || die
+        }
+        """
+        )
+
+        ebuilds = {
+            "sys-libs/glibc-2.37": {
+                "EAPI": "8",
+                "MISC_CONTENT": install_elf,
+            },
+            "sys-libs/glibc-2.38": {
+                "EAPI": "8",
+                "MISC_CONTENT": install_elf,
+            },
+            "virtual/libc-1": {
+                "EAPI": "8",
+                "RDEPEND": "sys-libs/glibc",
+            },
+            "dev-libs/A-1": {
+                "EAPI": "8",
+                "MISC_CONTENT": install_elf,
+            },
+            "dev-libs/B-1": {
+                "EAPI": "8",
+            },
+            "dev-libs/C-1": {
+                "EAPI": "8",
+                "MISC_CONTENT": install_elf,
+            },
+        }
+
+        playground = ResolverPlayground(ebuilds=ebuilds, debug=debug)
+        settings = playground.settings
+        eprefix = settings["EPREFIX"]
+        eroot = settings["EROOT"]
+        var_cache_edb = os.path.join(eprefix, "var", "cache", "edb")
+        user_config_dir = os.path.join(eprefix, USER_CONFIG_PATH)
+
+        portage_python = portage._python_interpreter
+        emerge_cmd = (
+            portage_python,
+            "-b",
+            "-Wd",
+            os.path.join(str(self.bindir), "emerge"),
+        )
+
+        test_commands = (
+            # (We need sys-libs/glibc pulled in and virtual/libc installed)
+            emerge_cmd
+            + (
+                "--oneshot",
+                "virtual/libc",
+            ),
+            # A package installing an ELF binary should have an injected libc dep
+            emerge_cmd
+            + (
+                "--oneshot",
+                "dev-libs/A",
+            ),
+            (lambda: os.path.exists(os.path.join(eroot, "usr/bin/A")),),
+            (
+                lambda: ">=sys-libs/glibc-2.38\n"
+                in portage.util.grablines(
+                    os.path.join(
+                        eprefix, "var", "db", "pkg", "dev-libs", "A-1", "RDEPEND"
+                    )
+                ),
+            ),
+            # Downgrade glibc to a version (2.37) older than the version
+            # that dev-libs/A's binpkg was built against (2.38). Below,
+            # we check that it pulls in a newer glibc via a ResolverPlayground
+            # testcase.
+            emerge_cmd
+            + (
+                "--oneshot",
+                "--nodeps",
+                "<sys-libs/glibc-2.38",
+            ),
+        )
+
+        fake_bin = os.path.join(eprefix, "bin")
+        portage_tmpdir = os.path.join(eprefix, "var", "tmp", "portage")
+        profile_path = settings.profile_path
+
+        path = os.environ.get("PATH")
+        if path is not None and not path.strip():
+            path = None
+        if path is None:
+            path = ""
+        else:
+            path = ":" + path
+        path = fake_bin + path
+
+        pythonpath = os.environ.get("PYTHONPATH")
+        if pythonpath is not None and not pythonpath.strip():
+            pythonpath = None
+        if pythonpath is not None and pythonpath.split(":")[0] == PORTAGE_PYM_PATH:
+            pass
+        else:
+            if pythonpath is None:
+                pythonpath = ""
+            else:
+                pythonpath = ":" + pythonpath
+            pythonpath = PORTAGE_PYM_PATH + pythonpath
+
+        env = {
+            "PORTAGE_OVERRIDE_EPREFIX": eprefix,
+            "PATH": path,
+            "PORTAGE_PYTHON": portage_python,
+            "PORTAGE_REPOSITORIES": settings.repositories.config_string(),
+            "PYTHONDONTWRITEBYTECODE": os.environ.get("PYTHONDONTWRITEBYTECODE", ""),
+            "PYTHONPATH": pythonpath,
+            "PORTAGE_INST_GID": str(os.getgid()),
+            "PORTAGE_INST_UID": str(os.getuid()),
+            "FEATURES": "buildpkg",
+        }
+
+        dirs = [
+            playground.distdir,
+            fake_bin,
+            portage_tmpdir,
+            user_config_dir,
+            var_cache_edb,
+        ]
+
+        true_symlinks = ["chown", "chgrp"]
+
+        # We don't want to make pax-utils a hard-requirement for tests,
+        # so if it's not found, skip the test rather than FAIL it.
+        needed_binaries = {
+            "true": (find_binary("true"), True),
+            "scanelf": (find_binary("scanelf"), False),
+            "find": (find_binary("find"), True),
+        }
+
+        for name, (path, mandatory) in needed_binaries.items():
+            found = path is not None
+
+            if not found:
+                if mandatory:
+                    self.assertIsNotNone(path, f"command {name} not found")
+                else:
+                    self.skipTest(f"{name} not found")
+
+        try:
+            for d in dirs:
+                ensure_dirs(d)
+            for x in true_symlinks:
+                os.symlink(needed_binaries["true"][0], os.path.join(fake_bin, x))
+
+            # We need scanelf, find for the ELF parts (creating NEEDED)
+            os.symlink(needed_binaries["scanelf"][0], os.path.join(fake_bin, "scanelf"))
+            os.symlink(needed_binaries["find"][0], os.path.join(fake_bin, "find"))
+
+            with open(os.path.join(var_cache_edb, "counter"), "wb") as f:
+                f.write(b"100")
+            with open(os.path.join(profile_path, "packages"), "w") as f:
+                f.write("*virtual/libc")
+
+            if debug:
+                # The subprocess inherits both stdout and stderr, for
+                # debugging purposes.
+                stdout = None
+            else:
+                # The subprocess inherits stderr so that any warnings
+                # triggered by python -Wd will be visible.
+                stdout = subprocess.PIPE
+
+            for i, args in enumerate(test_commands):
+                if hasattr(args[0], "__call__"):
+                    self.assertTrue(args[0](), f"callable at index {i} failed")
+                    continue
+
+                if isinstance(args[0], dict):
+                    local_env = env.copy()
+                    local_env.update(args[0])
+                    args = args[1:]
+                else:
+                    local_env = env
+
+                proc = subprocess.Popen(args, env=local_env, stdout=stdout)
+
+                if debug:
+                    proc.wait()
+                else:
+                    output = proc.stdout.readlines()
+                    proc.wait()
+                    proc.stdout.close()
+                    if proc.returncode != os.EX_OK:
+                        for line in output:
+                            sys.stderr.write(_unicode_decode(line))
+
+                self.assertEqual(
+                    os.EX_OK, proc.returncode, f"emerge failed with args {args}"
+                )
+
+            # Now check that glibc gets upgraded to the right version
+            # for the binpkg first after we downgraded it earlier, before
+            # merging the dev-libs/A binpkg which needs 2.38.
+            k = ResolverPlaygroundTestCase(
+                ["dev-libs/A"],
+                options={
+                    "--usepkgonly": True,
+                    "--verbose": True,
+                },
+                success=True,
+                mergelist=["[binary]sys-libs/glibc-2.38-1", "[binary]dev-libs/A-1-1"],
+            )
+            playground.run_TestCase(k)
+            self.assertEqual(k.test_success, True, k.fail_msg)
+
+        finally:
+            playground.debug = False
+            playground.cleanup()


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2023-12-10 22:02 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2023-12-10 22:01 [gentoo-commits] proj/portage:master commit in: lib/_emerge/, lib/portage/tests/emerge/, lib/portage/tests/dep/, Sam James

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox