commit: e13a6647093a34c008b75fee02094edc7df5da0c Author: Jethro Donaldson <devel <AT> jro <DOT> nz> AuthorDate: Wed Dec 3 11:40:22 2025 +0000 Commit: Sam James <sam <AT> gentoo <DOT> org> CommitDate: Mon Feb 16 06:54:15 2026 +0000 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=e13a6647
emerge: add --getbinpkg-include --getbinpkg-exclude command line opts Allow specification of which binary packages can or cannot be satisfied using remote binary packages, with a interface consistent with the --usepkg-include and --usepkg-exclude options. These additional options influence fetching of remote binaries only and have no effect unless -g is also supplied or implied. Signed-off-by: Jethro Donaldson <devel <AT> jro.nz> Suggested-by: Sam James <sam <AT> gentoo.org> Part-of: https://github.com/gentoo/portage/pull/1527 Signed-off-by: Sam James <sam <AT> gentoo.org> lib/_emerge/actions.py | 53 ++- lib/_emerge/depgraph.py | 27 +- lib/_emerge/main.py | 12 + lib/portage/_sets/base.py | 16 + lib/portage/dbapi/bintree.py | 46 +- lib/portage/tests/dbapi/test_bintree.py | 12 +- lib/portage/tests/resolver/ResolverPlayground.py | 85 +++- .../tests/resolver/test_binpackage_selection.py | 528 +++++++++++++++++++++ .../tests/sets/base/test_wildcard_package_set.py | 30 ++ man/emerge.1 | 16 + 10 files changed, 784 insertions(+), 41 deletions(-) diff --git a/lib/_emerge/actions.py b/lib/_emerge/actions.py index 1136336916..472feb135f 100644 --- a/lib/_emerge/actions.py +++ b/lib/_emerge/actions.py @@ -55,7 +55,7 @@ warn = create_color_func("WARN") from portage.package.ebuild._ipc.QueryCommand import QueryCommand from portage.package.ebuild.fetch import _hide_url_passwd from portage._sets import load_default_config, SETPREFIX -from portage._sets.base import InternalPackageSet +from portage._sets.base import InternalPackageSet, WildcardPackageSet from portage.util import ( cmp_sort_key, normalize_path, @@ -81,12 +81,7 @@ from portage.binpkg import get_binpkg_format from _emerge.clear_caches import clear_caches from _emerge.create_depgraph_params import create_depgraph_params from _emerge.Dependency import Dependency -from _emerge.depgraph import ( - backtrack_depgraph, - depgraph, - resume_depgraph, - _wildcard_set, -) +from _emerge.depgraph import backtrack_depgraph, depgraph, resume_depgraph from _emerge.emergelog import emergelog from _emerge.is_valid_package_atom import is_valid_package_atom from _emerge.main import profile_check @@ -171,6 +166,10 @@ def action_build( kwargs["add_repos"] = (quickpkg_vardb,) try: kwargs["pretend"] = "--pretend" in emerge_config.opts + if "--getbinpkg-exclude" in emerge_config.opts: + kwargs["getbinpkg_exclude"] = emerge_config.opts["--getbinpkg-exclude"] + if "--getbinpkg-include" in emerge_config.opts: + kwargs["getbinpkg_include"] = emerge_config.opts["--getbinpkg-include"] emerge_config.target_config.trees["bintree"].populate( getbinpkgs="--getbinpkg" in emerge_config.opts, **kwargs ) @@ -434,6 +433,14 @@ def action_build( kwargs["add_repos"] = ( emerge_config.running_config.trees["vartree"].dbapi, ) + if "--getbinpkg-exclude" in emerge_config.opts: + kwargs["getbinpkg_exclude"] = emerge_config.opts[ + "--getbinpkg-exclude" + ] + if "--getbinpkg-include" in emerge_config.opts: + kwargs["getbinpkg_include"] = emerge_config.opts[ + "--getbinpkg-include" + ] try: root_trees["bintree"].populate( @@ -2807,12 +2814,16 @@ def adjust_config(myopts, settings): def binpkg_selection_config(opts, settings): + atoms = " ".join(opts.pop("--getbinpkg-exclude", [])).split() + getbinpkg_exclude = WildcardPackageSet(atoms) + atoms = " ".join(opts.pop("--getbinpkg-include", [])).split() + getbinpkg_include = WildcardPackageSet(atoms) atoms = " ".join(opts.pop("--usepkg-exclude", [])).split() - usepkg_exclude = _wildcard_set(atoms) + usepkg_exclude = WildcardPackageSet(atoms) atoms = " ".join(opts.pop("--usepkg-include", [])).split() - usepkg_include = _wildcard_set(atoms) + usepkg_include = WildcardPackageSet(atoms) - # warn if include/exclude lists overlap on command line + # --usepkg-include and --usepkg-exclude may not overlap conflicted_atoms = usepkg_exclude.getAtoms().intersection(usepkg_include.getAtoms()) if conflicted_atoms: writemsg( @@ -2841,6 +2852,24 @@ def binpkg_selection_config(opts, settings): ) usepkg_include.clear() + # --getbinpkg-include and --getbinpkg-exclude may not overlap + conflicted_atoms = getbinpkg_exclude.getAtoms().intersection( + getbinpkg_include.getAtoms() + ) + if conflicted_atoms: + writemsg( + "\n!!! The following atoms appear in both the --getbinpkg-exclude " + "and --getbinpkg-include command line arguments:\n" + "\n %s\n" % ("\n ".join(conflicted_atoms)) + ) + for a in conflicted_atoms: + getbinpkg_exclude.remove(a) + getbinpkg_include.remove(a) + + if not getbinpkg_exclude.isEmpty(): + opts["--getbinpkg-exclude"] = list(getbinpkg_exclude) + if not getbinpkg_include.isEmpty(): + opts["--getbinpkg-include"] = list(getbinpkg_include) if not usepkg_exclude.isEmpty(): opts["--usepkg-exclude"] = list(usepkg_exclude) if not usepkg_include.isEmpty(): @@ -3634,6 +3663,10 @@ def run_action(emerge_config): ) kwargs["pretend"] = "--pretend" in emerge_config.opts + if "--getbinpkg-exclude" in emerge_config.opts: + kwargs["getbinpkg_exclude"] = emerge_config.opts["--getbinpkg-exclude"] + if "--getbinpkg-include" in emerge_config.opts: + kwargs["getbinpkg_include"] = emerge_config.opts["--getbinpkg-include"] try: mytrees["bintree"].populate( diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py index 96586e9c69..17c5e54900 100644 --- a/lib/_emerge/depgraph.py +++ b/lib/_emerge/depgraph.py @@ -52,7 +52,7 @@ from portage.output import colorize, create_color_func, darkgreen, green bad = create_color_func("BAD") from portage.package.ebuild.getmaskingstatus import _getmaskingstatus, _MaskReason from portage._sets import SETPREFIX -from portage._sets.base import InternalPackageSet +from portage._sets.base import InternalPackageSet, WildcardPackageSet from portage.dep._slot_operator import evaluate_slot_operator_equal_deps from portage.util import ConfigProtect, new_protect_filename from portage.util import cmp_sort_key, writemsg, writemsg_stdout @@ -131,17 +131,6 @@ class _scheduler_graph_config: self.mergelist = mergelist -def _wildcard_set(atoms): - pkgs = InternalPackageSet(allow_wildcard=True) - for x in atoms: - try: - x = Atom(x, allow_wildcard=True, allow_repo=False) - except portage.exception.InvalidAtom: - x = Atom("*/" + x, allow_wildcard=True, allow_repo=False) - pkgs.add(x) - return pkgs - - class _frozen_depgraph_config: def __init__(self, settings, trees, myopts, params, spinner): self.settings = settings @@ -204,19 +193,19 @@ class _frozen_depgraph_config: self._required_set_names = {"world"} atoms = " ".join(myopts.get("--exclude", [])).split() - self.excluded_pkgs = _wildcard_set(atoms) + self.excluded_pkgs = WildcardPackageSet(atoms) atoms = " ".join(myopts.get("--reinstall-atoms", [])).split() - self.reinstall_atoms = _wildcard_set(atoms) + self.reinstall_atoms = WildcardPackageSet(atoms) atoms = " ".join(myopts.get("--usepkg-exclude", [])).split() - self.usepkg_exclude = _wildcard_set(atoms) + self.usepkg_exclude = WildcardPackageSet(atoms) atoms = " ".join(myopts.get("--usepkg-include", [])).split() - self.usepkg_include = _wildcard_set(atoms) + self.usepkg_include = WildcardPackageSet(atoms) atoms = " ".join(myopts.get("--useoldpkg-atoms", [])).split() - self.useoldpkg_atoms = _wildcard_set(atoms) + self.useoldpkg_atoms = WildcardPackageSet(atoms) atoms = " ".join(myopts.get("--rebuild-exclude", [])).split() - self.rebuild_exclude = _wildcard_set(atoms) + self.rebuild_exclude = WildcardPackageSet(atoms) atoms = " ".join(myopts.get("--rebuild-ignore", [])).split() - self.rebuild_ignore = _wildcard_set(atoms) + self.rebuild_ignore = WildcardPackageSet(atoms) self.rebuild_if_new_rev = "--rebuild-if-new-rev" in myopts self.rebuild_if_new_ver = "--rebuild-if-new-ver" in myopts diff --git a/lib/_emerge/main.py b/lib/_emerge/main.py index 7a3371807d..5eef25193e 100644 --- a/lib/_emerge/main.py +++ b/lib/_emerge/main.py @@ -571,6 +571,16 @@ def parse_opts(tmpcmdline, silent=False): "help": "fetch binary packages only", "choices": true_y_or_n, }, + "--getbinpkg-exclude": { + "help": "A space separated list of package names or slot atoms. " + + "Emerge will not fetch matching remote binary packages. ", + "action": "append", + }, + "--getbinpkg-include": { + "help": "A space separated list of package names or slot atoms. " + + "Emerge will not fetch non-matching remote binary packages. ", + "action": "append", + }, "--usepkg-exclude": { "help": "A space separated list of package names or slot atoms. " + "Emerge will ignore matching binary packages. ", @@ -890,6 +900,8 @@ def parse_opts(tmpcmdline, silent=False): candidate_bad_options = ( (myoptions.exclude, "exclude"), + (myoptions.getbinpkg_exclude, "getbinpkg-exclude"), + (myoptions.getbinpkg_include, "getbinpkg-include"), (myoptions.reinstall_atoms, "reinstall-atoms"), (myoptions.rebuild_exclude, "rebuild-exclude"), (myoptions.rebuild_ignore, "rebuild-ignore"), diff --git a/lib/portage/_sets/base.py b/lib/portage/_sets/base.py index 4c9f4914ab..2f51137863 100644 --- a/lib/portage/_sets/base.py +++ b/lib/portage/_sets/base.py @@ -252,3 +252,19 @@ class DummyPackageSet(PackageSet): return DummyPackageSet(atoms=atoms) singleBuilder = classmethod(singleBuilder) + + +class WildcardPackageSet(InternalPackageSet): + def __init__(self, initial_atoms, allow_repo=False): + super().__init__(initial_atoms, allow_wildcard=True, allow_repo=allow_repo) + + def _implicitWildcarding(self, atom): + if isinstance(atom, Atom): + return atom + try: + return Atom(atom, allow_wildcard=True, allow_repo=self._allow_repo) + except InvalidAtom: + return Atom("*/" + atom, allow_wildcard=True, allow_repo=self._allow_repo) + + def update(self, atoms): + super().update(self._implicitWildcarding(a) for a in atoms) diff --git a/lib/portage/dbapi/bintree.py b/lib/portage/dbapi/bintree.py index 781d9fd4f3..b6056896e7 100644 --- a/lib/portage/dbapi/bintree.py +++ b/lib/portage/dbapi/bintree.py @@ -27,6 +27,7 @@ from portage.exception import ( PortagePackageException, SignatureException, ) +from portage._sets.base import WildcardPackageSet from portage.localization import _ from portage.output import colorize from portage.package.ebuild.profile_iuse import iter_iuse_vars @@ -904,6 +905,8 @@ class binarytree: def populate( self, getbinpkgs=False, + getbinpkg_exclude=None, + getbinpkg_include=None, getbinpkg_refresh=False, verbose=False, add_repos=(), @@ -916,6 +919,8 @@ class binarytree: @param getbinpkgs: include remote packages @type getbinpkgs: bool + @param getbinpkg_exclude: list of remote atoms to exclude + @param getbinpkg_include: list of remote atoms to include @param getbinpkg_refresh: attempt to refresh the cache of remote package metadata if getbinpkgs is also True @type getbinpkg_refresh: bool @@ -1000,6 +1005,8 @@ class binarytree: getbinpkg_refresh=getbinpkg_refresh, pretend=pretend, verbose=verbose, + getbinpkg_exclude=getbinpkg_exclude, + getbinpkg_include=getbinpkg_include, ) finally: @@ -1397,7 +1404,16 @@ class binarytree: return ret.check_returncode() - def _populate_remote(self, getbinpkg_refresh=True, pretend=False, verbose=False): + def _populate_remote( + self, + getbinpkg_refresh=True, + pretend=False, + verbose=False, + getbinpkg_exclude=None, + getbinpkg_include=None, + ): + from portage.util import writemsg + self._remote_has_index = False self._remotepkgs = {} @@ -1411,10 +1427,21 @@ class binarytree: else: gpkg_only = False + atoms = " ".join(getbinpkg_exclude or []).split() + getbinpkg_exclude = WildcardPackageSet(atoms) + atoms = " ".join(getbinpkg_include or []).split() + getbinpkg_include = WildcardPackageSet(atoms) + # Order by descending priority. for repo in reversed(list(self._binrepos_conf.values())): self._populate_remote_repo( - repo, getbinpkg_refresh, pretend, verbose, gpkg_only + repo, + getbinpkg_refresh, + pretend, + verbose, + gpkg_only, + getbinpkg_exclude, + getbinpkg_include, ) def _populate_remote_repo( @@ -1424,6 +1451,8 @@ class binarytree: pretend: bool, verbose: bool, gpkg_only: bool, + getbinpkg_exclude: WildcardPackageSet, + getbinpkg_include: WildcardPackageSet, ): from portage.package.ebuild.fetch import _hide_url_passwd from portage.util import atomic_ofstream, writemsg @@ -1778,6 +1807,8 @@ class binarytree: # The current user doesn't have permission to cache the # file, but that's alright. if pkgindex: + have_getbinpkg_exclude = not getbinpkg_exclude.isEmpty() + have_getbinpkg_include = not getbinpkg_include.isEmpty() remote_base_uri = pkgindex.header.get("URI", base_url) for d in pkgindex.packages: cpv = _pkg_str( @@ -1787,6 +1818,17 @@ class binarytree: db=self.dbapi, repoconfig=repo, ) + + # Respect remote binary exclude and include lists if defined + in_getbinpkg_exclude = ( + have_getbinpkg_exclude and getbinpkg_exclude.containsCPV(cpv) + ) + in_getbinpkg_include = ( + not have_getbinpkg_include or getbinpkg_include.containsCPV(cpv) + ) + if in_getbinpkg_exclude or not in_getbinpkg_include: + continue + # Local package instances override remote instances # with the same instance_key. if self.dbapi.cpv_exists(cpv): diff --git a/lib/portage/tests/dbapi/test_bintree.py b/lib/portage/tests/dbapi/test_bintree.py index 537556aff5..a1d58e0867 100644 --- a/lib/portage/tests/dbapi/test_bintree.py +++ b/lib/portage/tests/dbapi/test_bintree.py @@ -143,7 +143,11 @@ class BinarytreeTestCase(TestCase): bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=settings) bt.populate(getbinpkgs=True, getbinpkg_refresh=refresh) ppopulate_remote.assert_called_once_with( - getbinpkg_refresh=refresh, pretend=False, verbose=False + getbinpkg_refresh=refresh, + pretend=False, + verbose=False, + getbinpkg_exclude=None, + getbinpkg_include=None, ) @patch("portage.dbapi.bintree.BinRepoConfigLoader") @@ -189,7 +193,11 @@ class BinarytreeTestCase(TestCase): bt = binarytree(pkgdir=os.getenv("TMPDIR", "/tmp"), settings=settings) bt.populate(getbinpkgs=True) ppopulate_remote.assert_called_once_with( - getbinpkg_refresh=False, pretend=False, verbose=False + getbinpkg_refresh=False, + pretend=False, + verbose=False, + getbinpkg_exclude=None, + getbinpkg_include=None, ) @patch("portage.data.secpass", 2) diff --git a/lib/portage/tests/resolver/ResolverPlayground.py b/lib/portage/tests/resolver/ResolverPlayground.py index aa7fbea70a..a66ed54944 100644 --- a/lib/portage/tests/resolver/ResolverPlayground.py +++ b/lib/portage/tests/resolver/ResolverPlayground.py @@ -44,6 +44,27 @@ from _emerge.Package import Package from _emerge.RootConfig import RootConfig +def _combine_repo_config(conf, lines): + merged = lines.copy() + for section in reversed(conf): + header = f"[{section}]" + if header in merged: + index = merged.index(header) + 1 + conflicts = [any(l.startswith(k) for l in lines) for k in conf[section]] + if any(conflicts): + reserved = ",".join(conf[section].keys()) + raise AssertionError( + f"cannot override config attributes [{reserved}] set by ResolverPlayground" + ) + else: + merged.insert(0, header) + index = 1 + for entry in conf[section].items(): + merged.insert(index, ("%s = %s" % entry)) + index += 1 + return merged + + class ResolverPlayground: """ This class helps to create the necessary files on disk and @@ -77,7 +98,6 @@ class ResolverPlayground: "use.force", "use.mask", "use.stable", - "layout.conf", ) ) @@ -120,6 +140,7 @@ class ResolverPlayground: self, ebuilds={}, binpkgs={}, + binrepos={}, installed={}, profile={}, repo_configs={}, @@ -234,6 +255,10 @@ class ResolverPlayground: # Make sure the main repo is always created self._get_repo_dir("test_repo") + self._binrepos = {} + for binrepo in binrepos: + self._get_binrepo_dir(binrepo) + self._create_distfiles(distfiles) self._create_ebuilds(ebuilds) self._create_installed(installed) @@ -245,9 +270,13 @@ class ResolverPlayground: self.settings, self.trees = self._load_config() self.gpg = None - self._create_binpkgs(binpkgs) + self._create_binpkgs(self.pkgdir, binpkgs) self._create_ebuild_manifests(ebuilds) + for binrepo, binpkgs in binrepos.items(): + binrepo_dir = self._get_binrepo_dir(binrepo) + self._create_binpkgs(binrepo_dir, binpkgs) + portage.util.noiselimit = 0 def reload_config(self): @@ -283,6 +312,23 @@ class ResolverPlayground: return self._repositories[repo]["location"] + def _get_binrepo_dir(self, binrepo): + """ + Create the binrepo directory if needed. + """ + if binrepo not in self._binrepos: + self._binrepos["DEFAULT"] = {"frozen": "yes"} + + repo_path = os.path.join(self.eroot, "var", "binrepos", binrepo) + self._binrepos[binrepo] = {"sync-uri": repo_path} + + try: + os.makedirs(repo_path) + except OSError: + pass + + return self._binrepos[binrepo]["sync-uri"] + def _create_distfiles(self, distfiles): os.makedirs(self.distdir) for k, v in distfiles.items(): @@ -352,7 +398,7 @@ class ResolverPlayground: f"command failed with returncode {result.returncode}: {egencache_cmd}" ) - def _create_binpkgs(self, binpkgs): + def _create_binpkgs(self, repo_dir, binpkgs): # When using BUILD_ID, there can be multiple instances for the # same cpv. Therefore, binpkgs may be an iterable instead of # a dict. @@ -382,7 +428,6 @@ class ResolverPlayground: metadata["PF"] = pf metadata["BINPKG_FORMAT"] = binpkg_format - repo_dir = self.pkgdir category_dir = os.path.join(repo_dir, cat) if "BUILD_ID" in metadata: if binpkg_format == "xpak": @@ -415,8 +460,8 @@ class ResolverPlayground: else: raise InvalidBinaryPackageFormat(binpkg_format) - bintree = binarytree(pkgdir=self.pkgdir, settings=self.settings) - bintree.populate(force_reindex=True) + bintree = binarytree(pkgdir=repo_dir, settings=self.settings) + bintree.populate(force_reindex=True) def _create_installed(self, installed): for cpv in installed: @@ -633,6 +678,12 @@ class ResolverPlayground: configs = user_config.copy() configs["make.conf"] = make_conf_lines + if self._binrepos: + binrepos_conf_lines = list(user_config.get("binrepos.conf", ())) + configs["binrepos.conf"] = _combine_repo_config( + self._binrepos, binrepos_conf_lines + ) + for config_file, lines in configs.items(): if config_file not in self.config_files: raise ValueError(f"Unknown config file: '{config_file}'") @@ -738,6 +789,13 @@ class ResolverPlayground: elif options.get("--prune"): action = "prune" + if "--getbinpkgonly" in options: + options["--getbinpkg"] = True + options["--usepkgonly"] = True + + if "--getbinpkg" in options: + options["--usepkg"] = True + if "--usepkgonly" in options: options["--usepkg"] = True @@ -750,6 +808,14 @@ class ResolverPlayground: portage.util.noiselimit = -2 _emerge.emergelog._disable = True + if self._binrepos: + self.trees[self.eroot]["bintree"].populate( + getbinpkgs=options.get("--getbinpkg", False), + getbinpkg_exclude=options.get("--getbinpkg-exclude", None), + getbinpkg_include=options.get("--getbinpkg-include", None), + pretend=options.get("--pretend", False), + ) + # NOTE: frozen_config could be cached and reused if options and params were constant. params_action = ( "remove" if action in ("dep_check", "depclean", "prune") else action @@ -1020,9 +1086,12 @@ def _mergelist_str(x, depgraph): mergelist_str = x.cpv + build_id_str + repo_str if x.built: if x.operation == "merge": - desc = x.type_name + desc = [x.type_name] else: - desc = x.operation + desc = [x.operation] + if x.remote: + desc.append("remote") + desc = ",".join(desc) mergelist_str = f"[{desc}]{mergelist_str}" if x.root != depgraph._frozen_config._running_root.root: mergelist_str += "{targetroot}" diff --git a/lib/portage/tests/resolver/test_binpackage_selection.py b/lib/portage/tests/resolver/test_binpackage_selection.py index 2d6df54ee1..ac2f10ec63 100644 --- a/lib/portage/tests/resolver/test_binpackage_selection.py +++ b/lib/portage/tests/resolver/test_binpackage_selection.py @@ -16,6 +16,12 @@ class BinPkgSelectionTestCase(TestCase): "app-misc/baz-1.0": {}, } + pkgs_no_deps_newer = { + "app-misc/foo-1.1": {}, + "app-misc/bar-1.1": {}, + "app-misc/baz-1.1": {}, + } + pkgs_with_deps_newer = { "app-misc/foo-1.1": {"RDEPEND": "app-misc/bar"}, "app-misc/bar-1.1": {"RDEPEND": "app-misc/baz"}, @@ -52,6 +58,528 @@ class BinPkgSelectionTestCase(TestCase): playground.cleanup() +# test --getbinpkg-exclude option +class GetBinPkgExcludeTestCase(BinPkgSelectionTestCase): + + def testGetBinPkgExcludeOpt(self): + binpkgs = self.pkgs_no_deps + ebuilds = self.pkgs_no_deps + + binrepos = {"test_binrepo": self.pkgs_no_deps} + + test_cases = ( + # --getbinpkg-exclude to have no effect without --getbinpkg + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg-exclude": ["foo"]}, + mergelist=[ + "app-misc/foo-1.0", + "app-misc/bar-1.0", + "app-misc/baz-1.0", + ], + ), + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--usepkgonly": True, "--getbinpkg-exclude": ["foo"]}, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + # --getbinpkg-exclude with unmatched atom excludes no remote binaries + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["dev-libs/foo"]}, + mergelist=[ + "[binary,remote]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.0", + "[binary,remote]app-misc/baz-1.0", + ], + ), + # --getbinpkg-exclude in conflict with --getbinpkg-include to have no effect + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-exclude": ["foo"], + "--getbinpkg-include": ["foo"], + }, + mergelist=[ + "[binary,remote]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.0", + "[binary,remote]app-misc/baz-1.0", + ], + ), + # --getbinpkg-exclude in conflict with --getbinpkg-include to not + # interfere with non-overlapping --getbinpkg-exclude + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-exclude": ["foo", "bar"], + "--getbinpkg-include": ["foo"], + }, + mergelist=[ + "[binary,remote]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary,remote]app-misc/baz-1.0", + ], + ), + # request all packages and --getbinpkg-exclude with single atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["foo"]}, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.0", + "[binary,remote]app-misc/baz-1.0", + ], + ), + # request all packages and --getbinpkg-exclude with multiple atoms + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["foo", "bar"]}, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary,remote]app-misc/baz-1.0", + ], + ), + # request all packages and --getbinpkg-exclude with wildcard + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["app-misc/b*"]}, + mergelist=[ + "[binary,remote]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + # combined use of --getbinpkg-exclude and --usepkg-exclude can have + # a complimentary effect (leaving some remote binaries selected)... + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-exclude": ["app-misc/b*"], + "--usepkg-exclude": ["baz"], + }, + mergelist=[ + "[binary,remote]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "app-misc/baz-1.0", + ], + ), + # ...or an overriding effect with no remote binaries selected. depends + # on the overlap in the specified atoms + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-exclude": ["app-misc/b*"], + "--usepkg-exclude": ["foo"], + }, + mergelist=[ + "app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + ) + + self.runBinPkgSelectionTest( + test_cases, binpkgs=binpkgs, binrepos=binrepos, ebuilds=ebuilds + ) + + def testGetBinPkgExcludeFallbacks(self): + binpkgs = self.pkgs_no_deps + ebuilds = self.pkgs_no_deps | self.pkgs_no_deps_newer + + binrepos = {"test_binrepo": self.pkgs_no_deps_newer} + + test_cases = ( + # prefer newer ebuild over old local binary where --getbinpkg-exclude + # prevents fetching newer remote binary and --usepkgonly is not used + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["foo"]}, + mergelist=[ + "app-misc/foo-1.1", + "[binary,remote]app-misc/bar-1.1", + "[binary,remote]app-misc/baz-1.1", + ], + ), + # --usepkgonly excludes newer ebuilds and so forces fallback on older + # local binary where --getbinpkg-exclude is used + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--usepkgonly": True, + "--getbinpkg": True, + "--getbinpkg-exclude": ["foo"], + }, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.1", + "[binary,remote]app-misc/baz-1.1", + ], + ), + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + # currently --getbinpkgonly is equivalent to previous test + "--getbinpkgonly": True, + "--getbinpkg-exclude": ["foo"], + }, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.1", + "[binary,remote]app-misc/baz-1.1", + ], + ), + ) + + self.runBinPkgSelectionTest( + test_cases, binpkgs=binpkgs, binrepos=binrepos, ebuilds=ebuilds + ) + + def testGetBinPkgExcludeSlot(self): + ebuilds = self.pkgs_with_slots + binpkgs = self.pkgs_with_slots + + binrepos = {"test_binrepo": self.pkgs_with_slots} + + test_cases = ( + # request all packages and --getbinpkg-exclude with single slot atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["foo:2"]}, + mergelist=[ + "[binary]app-misc/foo-2.0", + "[binary,remote]app-misc/bar-2.0", + "[binary,remote]app-misc/baz-2.0", + ], + ), + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["foo:2"]}, + mergelist=[ + "[binary]app-misc/foo-2.0", + "[binary,remote]app-misc/bar-2.0", + "[binary,remote]app-misc/baz-2.0", + ], + ), + # request all packages and --getbinpkg-exclude with wildcard slot atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-exclude": ["app-misc/b*:2"]}, + mergelist=[ + "[binary,remote]app-misc/foo-2.0", + "[binary]app-misc/bar-2.0", + "[binary]app-misc/baz-2.0", + ], + ), + # request all packages and --getbinpkg-exclude with unmatched slot atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-exclude": ["app-misc/foo:1"], + }, + mergelist=[ + "[binary,remote]app-misc/foo-2.0", + "[binary,remote]app-misc/bar-2.0", + "[binary,remote]app-misc/baz-2.0", + ], + ), + ) + + self.runBinPkgSelectionTest( + test_cases, binpkgs=binpkgs, binrepos=binrepos, ebuilds=ebuilds + ) + + +# test --getbinpkg-include option +class GetBinPkgIncludeTestCase(BinPkgSelectionTestCase): + + def testGetBinPkgIncludeOpt(self): + binpkgs = self.pkgs_no_deps + ebuilds = self.pkgs_no_deps + + binrepos = {"test_binrepo": self.pkgs_no_deps} + + test_cases = ( + # --getbinpkg-include to have no effect without --getbinpkg + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg-include": ["foo"]}, + mergelist=[ + "app-misc/foo-1.0", + "app-misc/bar-1.0", + "app-misc/baz-1.0", + ], + ), + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--usepkgonly": True, "--getbinpkg-include": ["foo"]}, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + # --getbinpkg-include with unmatched atom excludes all remote binaries + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-include": ["dev-libs/foo"]}, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + # request all packages and --getbinpkg-include with single atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-include": ["foo"]}, + mergelist=[ + "[binary,remote]app-misc/foo-1.0", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + # request all packages and --getbinpkg-include with multiple atoms + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-include": ["foo", "bar"]}, + mergelist=[ + "[binary,remote]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + # request all packages and --getbinpkg-include with wildcard + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-include": ["app-misc/b*"]}, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.0", + "[binary,remote]app-misc/baz-1.0", + ], + ), + # --getbinpkg-include in conflict with --getbinpkg-exclude to not + # interfere with non-overlapping --getbinpkg-include + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-exclude": ["foo"], + "--getbinpkg-include": ["foo", "bar"], + }, + mergelist=[ + "[binary]app-misc/foo-1.0", + "[binary,remote]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + # combined use of --getbinpkg-include and --usepkg-include can have + # a complimentary effect (leaving some remote binaries selected)... + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-include": ["app-misc/b*"], + "--usepkg-include": ["baz"], + }, + mergelist=[ + "app-misc/foo-1.0", + "app-misc/bar-1.0", + "[binary,remote]app-misc/baz-1.0", + ], + ), + # ...or an overriding effect with no remote binaries selected. depends + # on the overlap in the specified atoms + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--getbinpkg": True, + "--getbinpkg-include": ["app-misc/b*"], + "--usepkg-include": ["foo"], + }, + mergelist=[ + "[binary]app-misc/foo-1.0", + "app-misc/bar-1.0", + "app-misc/baz-1.0", + ], + ), + ) + + self.runBinPkgSelectionTest( + test_cases, binpkgs=binpkgs, binrepos=binrepos, ebuilds=ebuilds + ) + + def testGetBinPkgIncludeFallbacks(self): + binpkgs = self.pkgs_no_deps + ebuilds = self.pkgs_no_deps | self.pkgs_no_deps_newer + + binrepos = {"test_binrepo": self.pkgs_no_deps_newer} + + test_cases = ( + # prefer newer ebuild over old local binary where --getbinpkg-include + # prevents fetching newer remote binary and --usepkgonly is not used + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-include": ["foo"]}, + mergelist=[ + "[binary,remote]app-misc/foo-1.1", + "app-misc/bar-1.1", + "app-misc/baz-1.1", + ], + ), + # --usepkgonly excludes newer ebuilds and so forces fallback on older + # local binary where --getbinpkg-include is used + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + "--usepkgonly": True, + "--getbinpkg": True, + "--getbinpkg-include": ["foo"], + }, + mergelist=[ + "[binary,remote]app-misc/foo-1.1", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={ + # currently --getbinpkgonly is equivalent to previous test + "--getbinpkgonly": True, + "--getbinpkg-include": ["foo"], + }, + mergelist=[ + "[binary,remote]app-misc/foo-1.1", + "[binary]app-misc/bar-1.0", + "[binary]app-misc/baz-1.0", + ], + ), + ) + + self.runBinPkgSelectionTest( + test_cases, binpkgs=binpkgs, binrepos=binrepos, ebuilds=ebuilds + ) + + def testGetBinPkgIncludeSlot(self): + ebuilds = self.pkgs_with_slots + binpkgs = self.pkgs_with_slots + + binrepos = {"test_binrepo": self.pkgs_with_slots} + + test_cases = ( + # request all packages and --getbinpkg-include with single slot atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-include": ["foo:2"]}, + mergelist=[ + "[binary,remote]app-misc/foo-2.0", + "[binary]app-misc/bar-2.0", + "[binary]app-misc/baz-2.0", + ], + ), + # request all packages and --getbinpkg-include with wildcard slot atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--getbinpkg": True, "--getbinpkg-include": ["app-misc/b*:2"]}, + mergelist=[ + "[binary]app-misc/foo-2.0", + "[binary,remote]app-misc/bar-2.0", + "[binary,remote]app-misc/baz-2.0", + ], + ), + # request all packages and --getbinpkg-include with unmatched slot atom + ResolverPlaygroundTestCase( + self.pkg_atoms, + success=True, + ignore_mergelist_order=True, + options={"--usepkg": True, "--getbinpkg-include": ["app-misc/foo:1"]}, + mergelist=[ + "[binary]app-misc/foo-2.0", + "[binary]app-misc/bar-2.0", + "[binary]app-misc/baz-2.0", + ], + ), + ) + + self.runBinPkgSelectionTest( + test_cases, binpkgs=binpkgs, binrepos=binrepos, ebuilds=ebuilds + ) + + # test --usepkg-exclude option class UsePkgExcludeTestCase(BinPkgSelectionTestCase): diff --git a/lib/portage/tests/sets/base/test_wildcard_package_set.py b/lib/portage/tests/sets/base/test_wildcard_package_set.py new file mode 100644 index 0000000000..b53c121177 --- /dev/null +++ b/lib/portage/tests/sets/base/test_wildcard_package_set.py @@ -0,0 +1,30 @@ +# Copyright 2026 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +from portage.dep import Atom +from portage.exception import InvalidAtom +from portage.tests import TestCase +from portage._sets.base import WildcardPackageSet + + +class WildcardPackageSetTestCase(TestCase): + """Test case for WildcardPackageSet""" + + def testWildcardPackageSet(self): + ambig_atoms = {"A", "B", "C"} + norm_atoms = {"dev-libs/A", ">=dev-libs/A-1"} + wild_atoms = {"dev-libs/*", "*/B"} + sets = {"@world", "@installed", "@system"} + + w1 = WildcardPackageSet(initial_atoms=norm_atoms) + w2 = WildcardPackageSet(initial_atoms=wild_atoms) + self.assertRaises(InvalidAtom, WildcardPackageSet, initial_atoms=sets) + + self.assertEqual(w1.getAtoms(), norm_atoms) + self.assertEqual(w2.getAtoms(), wild_atoms) + + w3 = WildcardPackageSet(initial_atoms=ambig_atoms) + self.assertEqual(w3.getAtoms(), {"*/" + a for a in ambig_atoms}) + + w4 = WildcardPackageSet(initial_atoms=(Atom(a) for a in norm_atoms)) + self.assertEqual(w4.getAtoms(), norm_atoms) diff --git a/man/emerge.1 b/man/emerge.1 index 4ed28e67b1..a7b5fa0cf1 100644 --- a/man/emerge.1 +++ b/man/emerge.1 @@ -632,6 +632,22 @@ merging.) This option is identical to \fB\-g\fR, as above, except binaries from the remote server are preferred over local packages if they are not identical. .TP +.BR "\-\-getbinpkg\-exclude " ATOMS +A space separated list of package names or slot atoms. Emerge will not fetch +matching remote binary packages. This option only influences fetching of +remote binary packages, local binary packages are still considered even if +listed here. To define an explicit binary package blacklist use +\fB\-\-usepkg\-exclude\fR instead, which also affects remote binaries when +\fB-g\fR is used. +.TP +.BR "\-\-getbinpkg\-include " ATOMS +A space separated list of package names or slot atoms. Emerge will not fetch +non-matching remote binary packages. This option only influences fetching of +remote binary packages, local binary packages are still considered even if +not listed here. To define an explicit binary package whitelist use +\fB\-\-usepkg\-include\fR instead, which also affects remote binaries when +\fB-g\fR is used. +.TP .BR \-\-ignore-default-opts Causes \fIEMERGE_DEFAULT_OPTS\fR (see \fBmake.conf\fR(5)) to be ignored. .TP
