commit:     d284813525a9547a4132cb7b380cfacb3e08226a
Author:     Zac Medico <zmedico <AT> gentoo <DOT> org>
AuthorDate: Sat Nov 15 15:46:52 2025 +0000
Commit:     Zac Medico <zmedico <AT> gentoo <DOT> org>
CommitDate: Sat Nov 15 15:46:52 2025 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=d2848135

Implement use.stable and package.use.stable for EAPI 9

Since USE_ORDER has a repo component, also support repo-level
use.stable and package.use.stable.

Bug: https://bugs.gentoo.org/965968
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>

 lib/portage/eapi.py                                |  7 ++
 lib/portage/package/ebuild/_config/UseManager.py   | 28 +++++++-
 lib/portage/package/ebuild/config.py               | 32 ++++++++++
 lib/portage/tests/resolver/ResolverPlayground.py   |  2 +
 lib/portage/tests/resolver/meson.build             |  1 +
 .../tests/resolver/test_profile_use_stable.py      | 74 ++++++++++++++++++++++
 6 files changed, 143 insertions(+), 1 deletion(-)

diff --git a/lib/portage/eapi.py b/lib/portage/eapi.py
index 92376b5777..e980c9bf99 100644
--- a/lib/portage/eapi.py
+++ b/lib/portage/eapi.py
@@ -112,6 +112,10 @@ def eapi_has_repo_deps(eapi: str) -> bool:
     return _get_eapi_attrs(eapi).repo_deps
 
 
+def eapi_supports_use_stable(eapi: str) -> bool:
+    return _get_eapi_attrs(eapi).use_stable
+
+
 def eapi_supports_stable_use_forcing_and_masking(eapi: str) -> bool:
     return _get_eapi_attrs(eapi).stablemask
 
@@ -191,6 +195,7 @@ _eapi_attrs = collections.namedtuple(
         "src_prepare_src_configure",
         "src_uri_arrows",
         "stablemask",
+        "use_stable",
         "strong_blocks",
         "symlink_rewrite",
         "sysroot",
@@ -273,6 +278,7 @@ def _get_eapi_attrs(eapi_str: Optional[str]) -> _eapi_attrs:
             src_prepare_src_configure=True,
             src_uri_arrows=True,
             stablemask=True,
+            use_stable=True,
             strong_blocks=True,
             symlink_rewrite=False,
             sysroot=True,
@@ -314,6 +320,7 @@ def _get_eapi_attrs(eapi_str: Optional[str]) -> _eapi_attrs:
             src_prepare_src_configure=eapi >= Eapi("2"),
             src_uri_arrows=eapi >= Eapi("2"),
             stablemask=eapi >= Eapi("5"),
+            use_stable=eapi >= Eapi("9"),
             strong_blocks=eapi >= Eapi("2"),
             symlink_rewrite=eapi <= Eapi("8"),
             sysroot=eapi >= Eapi("7"),

diff --git a/lib/portage/package/ebuild/_config/UseManager.py 
b/lib/portage/package/ebuild/_config/UseManager.py
index 3827ba27a7..1da54f4f0c 100644
--- a/lib/portage/package/ebuild/_config/UseManager.py
+++ b/lib/portage/package/ebuild/_config/UseManager.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2021 Gentoo Authors
+# Copyright 2010-2025 Gentoo Authors
 # Distributed under the terms of the GNU General Public License v2
 
 __all__ = ("UseManager",)
@@ -13,6 +13,7 @@ from portage.dep import (
     _get_useflag_re,
 )
 from portage.eapi import (
+    eapi_supports_use_stable,
     eapi_supports_stable_use_forcing_and_masking,
 )
 from portage.localization import _
@@ -38,10 +39,12 @@ class UseManager:
         #      repositories
         # --------------------------------
         #      use.mask                        _repo_usemask_dict
+        #      use.stable                      _repo_use_stable_dict
         #      use.stable.mask                 _repo_usestablemask_dict
         #      use.force                       _repo_useforce_dict
         #      use.stable.force                _repo_usestableforce_dict
         #      package.use.mask                _repo_pusemask_dict
+        #      package.use.stable              _repo_puse_stable_dict
         #      package.use.stable.mask         _repo_pusestablemask_dict
         #      package.use.force               _repo_puseforce_dict
         #      package.use.stable.force        _repo_pusestableforce_dict
@@ -49,10 +52,12 @@ class UseManager:
         #      profiles
         # --------------------------------
         #      use.mask                        _usemask_list
+        #      use.stable                      _use_stable_list
         #      use.stable.mask                 _usestablemask_list
         #      use.force                       _useforce_list
         #      use.stable.force                _usestableforce_list
         #      package.use.mask                _pusemask_list
+        #      package.use.stable              _puse_stable_list
         #      package.use.stable.mask         _pusestablemask_list
         #      package.use                     _pkgprofileuse
         #      package.use.force               _puseforce_list
@@ -78,6 +83,11 @@ class UseManager:
         self._repo_usemask_dict = 
self._parse_repository_files_to_dict_of_tuples(
             "use.mask", repositories
         )
+        self._repo_use_stable_dict = 
self._parse_repository_files_to_dict_of_tuples(
+            "use.stable",
+            repositories,
+            eapi_filter=eapi_supports_use_stable,
+        )
         self._repo_usestablemask_dict = 
self._parse_repository_files_to_dict_of_tuples(
             "use.stable.mask",
             repositories,
@@ -94,6 +104,11 @@ class UseManager:
         self._repo_pusemask_dict = 
self._parse_repository_files_to_dict_of_dicts(
             "package.use.mask", repositories
         )
+        self._repo_puse_stable_dict = 
self._parse_repository_files_to_dict_of_dicts(
+            "package.use.stable",
+            repositories,
+            eapi_filter=eapi_supports_use_stable,
+        )
         self._repo_pusestablemask_dict = 
self._parse_repository_files_to_dict_of_dicts(
             "package.use.stable.mask",
             repositories,
@@ -114,6 +129,11 @@ class UseManager:
         self._usemask_list = self._parse_profile_files_to_tuple_of_tuples(
             "use.mask", profiles
         )
+        self._use_stable_list = self._parse_profile_files_to_tuple_of_tuples(
+            "use.stable",
+            profiles,
+            eapi_filter=eapi_supports_use_stable,
+        )
         self._usestablemask_list = 
self._parse_profile_files_to_tuple_of_tuples(
             "use.stable.mask",
             profiles,
@@ -130,6 +150,12 @@ class UseManager:
         self._pusemask_list = self._parse_profile_files_to_tuple_of_dicts(
             "package.use.mask", profiles
         )
+        self._puse_stable_list = self._parse_profile_files_to_tuple_of_dicts(
+            "package.use.stable",
+            profiles,
+            eapi_filter=eapi_supports_use_stable,
+            juststrings=True,
+        )
         self._pusestablemask_list = 
self._parse_profile_files_to_tuple_of_dicts(
             "package.use.stable.mask",
             profiles,

diff --git a/lib/portage/package/ebuild/config.py 
b/lib/portage/package/ebuild/config.py
index 77485fac85..06adb006ff 100644
--- a/lib/portage/package/ebuild/config.py
+++ b/lib/portage/package/ebuild/config.py
@@ -1898,6 +1898,7 @@ class config:
                     pkginternaluse_list.append(x)
             pkginternaluse = " ".join(pkginternaluse_list)
 
+        stable = self._isStable(cpv_slot) if hasattr(cpv_slot, "_metadata") 
else None
         eapi_attrs = _get_eapi_attrs(eapi)
 
         if pkginternaluse != self.configdict["pkginternal"].get("USE", ""):
@@ -1922,12 +1923,30 @@ class config:
                     # make a copy, since we might modify it with
                     # package.use settings
                     d = d.copy()
+
+                # package.use.stable > package.use > use.stable
+                if stable:
+                    x = self._use_manager._repo_use_stable_dict.get(repo, ())
+                    if x:
+                        d["USE"] = d.get("USE", "") + " " + " ".join(x)
+
                 cpdict = self._use_manager._repo_puse_dict.get(repo, 
{}).get(cp)
                 if cpdict:
                     repo_puse = ordered_by_atom_specificity(cpdict, cpv_slot)
                     if repo_puse:
                         for x in repo_puse:
                             d["USE"] = d.get("USE", "") + " " + " ".join(x)
+
+                if stable:
+                    cpdict = 
self._use_manager._repo_puse_stable_dict.get(repo, {}).get(
+                        cp
+                    )
+                    if cpdict:
+                        repo_puse = ordered_by_atom_specificity(cpdict, 
cpv_slot)
+                        if repo_puse:
+                            for x in repo_puse:
+                                d["USE"] = d.get("USE", "") + " " + " ".join(x)
+
                 if d:
                     repo_env.append(d)
 
@@ -1942,11 +1961,24 @@ class config:
         for i, pkgprofileuse_dict in 
enumerate(self._use_manager._pkgprofileuse):
             if self.make_defaults_use[i]:
                 defaults.append(self.make_defaults_use[i])
+
+            # package.use.stable > package.use > use.stable
+            if stable and self._use_manager._use_stable_list[i]:
+                defaults.append(" 
".join(self._use_manager._use_stable_list[i]))
+
             cpdict = pkgprofileuse_dict.get(cp)
             if cpdict:
                 pkg_defaults = ordered_by_atom_specificity(cpdict, cpv_slot)
                 if pkg_defaults:
                     defaults.extend(pkg_defaults)
+
+            if stable:
+                cpdict = self._use_manager._puse_stable_list[i].get(cp)
+                if cpdict:
+                    pkg_defaults = ordered_by_atom_specificity(cpdict, 
cpv_slot)
+                    if pkg_defaults:
+                        defaults.extend(pkg_defaults)
+
         defaults = " ".join(defaults)
         if defaults != self.configdict["defaults"].get("USE", ""):
             self.configdict["defaults"]["USE"] = defaults

diff --git a/lib/portage/tests/resolver/ResolverPlayground.py 
b/lib/portage/tests/resolver/ResolverPlayground.py
index 3e2cc6ec70..9d4aa25acc 100644
--- a/lib/portage/tests/resolver/ResolverPlayground.py
+++ b/lib/portage/tests/resolver/ResolverPlayground.py
@@ -65,11 +65,13 @@ class ResolverPlayground:
             "package.use",
             "package.use.force",
             "package.use.mask",
+            "package.use.stable",
             "package.use.stable.force",
             "package.use.stable.mask",
             "soname.provided",
             "use.force",
             "use.mask",
+            "use.stable",
             "layout.conf",
         )
     )

diff --git a/lib/portage/tests/resolver/meson.build 
b/lib/portage/tests/resolver/meson.build
index 653b0536e1..df7944259b 100644
--- a/lib/portage/tests/resolver/meson.build
+++ b/lib/portage/tests/resolver/meson.build
@@ -51,6 +51,7 @@ py.install_sources(
         'test_perl_rebuild_bug.py',
         'test_profile_default_eapi.py',
         'test_profile_package_set.py',
+        'test_profile_use_stable.py',
         'test_rebuild.py',
         'test_rebuild_ghostscript.py',
         'test_regular_slot_change_without_revbump.py',

diff --git a/lib/portage/tests/resolver/test_profile_use_stable.py 
b/lib/portage/tests/resolver/test_profile_use_stable.py
new file mode 100644
index 0000000000..0e38fbacc2
--- /dev/null
+++ b/lib/portage/tests/resolver/test_profile_use_stable.py
@@ -0,0 +1,74 @@
+# Copyright 2025 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+from portage.tests import TestCase
+from portage.tests.resolver.ResolverPlayground import (
+    ResolverPlayground,
+    ResolverPlaygroundTestCase,
+)
+
+
+class ProfileUseStableTestCase(TestCase):
+    def testProfileUseStable(self):
+        profile = {
+            "eapi": ("9-pre1",),
+            "package.use": ("app-misc/A -a d",),
+            "package.use.stable": ("app-misc/A a -b -c f",),
+            "use.stable": ("a", "b", "c", "e"),
+        }
+
+        user_config = {
+            "package.accept_keywords": ("=app-misc/A-2 ~x86",),
+        }
+
+        ebuilds = {
+            "app-misc/A-1": {"EAPI": "8", "KEYWORDS": "x86", "IUSE": "a b c d 
e f"},
+            "app-misc/A-2": {"EAPI": "8", "KEYWORDS": "~x86", "IUSE": "a b c d 
e f"},
+            "app-misc/B-1": {
+                "EAPI": "8",
+                # package.use.stable > package.use > use.stable
+                "RDEPEND": "=app-misc/A-1[a,-b,-c,d,e,f]",
+            },
+            "app-misc/C-1": {
+                "EAPI": "8",
+                # package.use.stable and use.stable do not apply due to 
unstable keyword
+                "RDEPEND": "=app-misc/A-2[-a,-b,-c,d,-e,-f]",
+            },
+        }
+
+        test_cases = (
+            # Test stable package
+            ResolverPlaygroundTestCase(
+                ["app-misc/B"],
+                success=True,
+                mergelist=[
+                    "app-misc/A-1",
+                    "app-misc/B-1",
+                ],
+            ),
+            # Test unstable package
+            ResolverPlaygroundTestCase(
+                ["app-misc/C"],
+                success=True,
+                mergelist=[
+                    "app-misc/A-2",
+                    "app-misc/C-1",
+                ],
+            ),
+        )
+
+        playground = ResolverPlayground(
+            debug=False,
+            ebuilds=ebuilds,
+            profile=profile,
+            user_config=user_config,
+        )
+
+        try:
+            for test_case in test_cases:
+                playground.run_TestCase(test_case)
+                self.assertEqual(test_case.test_success, True, 
test_case.fail_msg)
+        finally:
+            # Disable debug so that cleanup works.
+            playground.debug = False
+            playground.cleanup()

Reply via email to