From 4962c2160c7cfb0227022ae581d2f7b2cecd9a9f Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Wed, 22 Jul 2026 22:00:35 -0400 Subject: [PATCH 1/3] eclean-pkg: document --changed-deps and --changed-subslot in man page Signed-off-by: Matt Turner --- man/eclean.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/man/eclean.1 b/man/eclean.1 index db2c9946..4b8c46ea 100644 --- a/man/eclean.1 +++ b/man/eclean.1 @@ -111,6 +111,10 @@ Only effective with \-\-deep. Without \-\-deep, no VCS cleaning is done. .TP \fB\-i, \-\-ignore\-failure\fP ignore the failure to locate PKGDIR This is only useful when scripting to ignore an otherwise fatal error. +.TP +\fB\-\-changed\-deps\fP delete packages for which ebuild dependencies have changed +.TP +\fB\-\-changed\-subslot\fP delete packages with := deps on subslots no longer in the tree .SH "EXCLUSION FILES" Exclusions files are lists of packages names or categories you want to protect in particular. This may be useful to protect more binary packages for some system From e46cf1de297d3908d4d9ddf71ab7d65eb1f79427 Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Tue, 21 Jul 2026 01:14:26 -0400 Subject: [PATCH 2/3] eclean-pkg: add --changed-iuse to clean binpkgs with stale IUSE When PYTHON_COMPAT gains a new target, ebuilds grow a new default-disabled IUSE flag (e.g. python_targets_python3_14). Existing binary packages lack that flag, so Portage refuses to install them and there is no way to make them usable again -- they are orphaned and will only ever be rebuilt from source. Add --changed-iuse to delete such binpkgs. The comparison ignores USE flag defaults and only considers whether the set of flags matches the current ebuild's IUSE. Closes: https://bugs.gentoo.org/965888 Signed-off-by: Matt Turner --- man/eclean.1 | 4 ++++ pym/gentoolkit/eclean/cli.py | 9 +++++++++ pym/gentoolkit/eclean/search.py | 30 ++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/man/eclean.1 b/man/eclean.1 index 4b8c46ea..21bd07d0 100644 --- a/man/eclean.1 +++ b/man/eclean.1 @@ -115,6 +115,10 @@ This is only useful when scripting to ignore an otherwise fatal error. \fB\-\-changed\-deps\fP delete packages for which ebuild dependencies have changed .TP \fB\-\-changed\-subslot\fP delete packages with := deps on subslots no longer in the tree +.TP +\fB\-\-changed\-iuse\fP delete packages whose IUSE no longer matches the ebuild +This is useful when, for instance, PYTHON_COMPAT changes and adds a new +python_targets_* flag: the old binary package can never be a cache hit again. .SH "EXCLUSION FILES" Exclusions files are lists of packages names or categories you want to protect in particular. This may be useful to protect more binary packages for some system diff --git a/pym/gentoolkit/eclean/cli.py b/pym/gentoolkit/eclean/cli.py index 09c85f38..e85231ed 100644 --- a/pym/gentoolkit/eclean/cli.py +++ b/pym/gentoolkit/eclean/cli.py @@ -266,6 +266,11 @@ def printUsage(_error=None, help=None, unresolved_invalids=None): + " - delete packages with := deps on subslots no longer in tree", file=out, ) + print( + yellow(" --changed-iuse") + + " - delete packages whose IUSE no longer matches the ebuild", + file=out, + ) print( yellow(" --no-clean-invalid") + " - Skip cleaning invalid binpkgs", @@ -425,6 +430,8 @@ def optionSwitch(option, opts, action=None): options["changed-deps"] = True elif o in ("--changed-subslot"): options["changed-subslot"] = True + elif o in ("--changed-iuse"): + options["changed-iuse"] = True elif o in ("-i", "--ignore-failure"): options["ignore-failure"] = True elif o in ("-u", "--unique-use"): @@ -480,6 +487,7 @@ def optionSwitch(option, opts, action=None): "ignore-failure", "changed-deps", "changed-subslot", + "changed-iuse", "unique-use", "no-clean-invalid", ] @@ -497,6 +505,7 @@ def optionSwitch(option, opts, action=None): options["verbose"] = False options["changed-deps"] = False options["changed-subslot"] = False + options["changed-iuse"] = False options["ignore-failure"] = False options["no-clean-invalid"] = False options["unique-use"] = False diff --git a/pym/gentoolkit/eclean/search.py b/pym/gentoolkit/eclean/search.py index 2fd31dea..341e3a42 100644 --- a/pym/gentoolkit/eclean/search.py +++ b/pym/gentoolkit/eclean/search.py @@ -657,6 +657,23 @@ def _check_subslot_deps(deps, eapi, port_dbapi, uselist=None, cpv=None, verbose= return False +def _iuse_changed(iuse_binpkg, iuse_ebuild): + """Check if the set of IUSE flags differs between binpkg and ebuild. + + USE flag defaults (a leading + or -) are ignored; only the presence of a + flag matters. Portage cannot use a binpkg whose IUSE no longer matches the + ebuild's (e.g. after PYTHON_COMPAT adds a new python_targets_* flag), so + such a binpkg is orphaned and can be removed. + + Returns True if the IUSE flags differ (binpkg should be removed). + """ + + def _flags(iuse): + return frozenset(flag.lstrip("+-") for flag in iuse.split()) + + return _flags(iuse_binpkg) != _flags(iuse_ebuild) + + def _find_debuginfo_tarball(cpv: portage.versions._pkg_str, cp: str): """ From a CPV, identify and check for a matching debuginfo tarball. @@ -806,11 +823,15 @@ def mk_binpkg_key(cpv): # Exclude if binpkg exists in the porttree and not --deep if not destructive and port_dbapi.cpv_exists(cpv): - if not options["changed-deps"] and not options["changed-subslot"]: + if ( + not options["changed-deps"] + and not options["changed-subslot"] + and not options["changed-iuse"] + ): continue dep_keys = ("RDEPEND", "PDEPEND") - keys = ("EAPI", "USE") + dep_keys + keys = ("EAPI", "USE", "IUSE") + dep_keys binpkg_metadata = dict(zip(keys, bin_dbapi.aux_get(cpv, keys))) deps_binpkg = " ".join(binpkg_metadata[key] for key in dep_keys) uselist = frozenset(binpkg_metadata["USE"].split()) @@ -842,6 +863,11 @@ def mk_binpkg_key(cpv): ): should_remove = True + if not should_remove and options["changed-iuse"]: + ebuild_iuse = port_dbapi.aux_get(cpv, ["IUSE"])[0] + if _iuse_changed(binpkg_metadata["IUSE"], ebuild_iuse): + should_remove = True + if not should_remove: continue From 067048247d20a094ac4922d4e02dddc37b99816c Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Tue, 21 Jul 2026 01:14:57 -0400 Subject: [PATCH 3/3] eclean-pkg: add --unreachable to enable all --changed-* options The set of reasons a binary package can no longer be a Portage cache hit keeps growing (--changed-deps, --changed-subslot, --changed-iuse), and users generally just want all of them cleaned. Add --unreachable as a convenience that enables every --changed-* option at once, so new reasons are picked up automatically without users having to learn each flag. Closes: https://bugs.gentoo.org/977235 Signed-off-by: Matt Turner --- man/eclean.1 | 4 ++++ pym/gentoolkit/eclean/cli.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/man/eclean.1 b/man/eclean.1 index 21bd07d0..92b65a19 100644 --- a/man/eclean.1 +++ b/man/eclean.1 @@ -119,6 +119,10 @@ This is only useful when scripting to ignore an otherwise fatal error. \fB\-\-changed\-iuse\fP delete packages whose IUSE no longer matches the ebuild This is useful when, for instance, PYTHON_COMPAT changes and adds a new python_targets_* flag: the old binary package can never be a cache hit again. +.TP +\fB\-\-unreachable\fP delete packages that can no longer be a Portage cache hit +This enables all of the \-\-changed\-* options at once, so any binary package +that Portage can no longer reuse is removed regardless of why. .SH "EXCLUSION FILES" Exclusions files are lists of packages names or categories you want to protect in particular. This may be useful to protect more binary packages for some system diff --git a/pym/gentoolkit/eclean/cli.py b/pym/gentoolkit/eclean/cli.py index e85231ed..1b4386ad 100644 --- a/pym/gentoolkit/eclean/cli.py +++ b/pym/gentoolkit/eclean/cli.py @@ -271,6 +271,12 @@ def printUsage(_error=None, help=None, unresolved_invalids=None): + " - delete packages whose IUSE no longer matches the ebuild", file=out, ) + print( + yellow(" --unreachable") + + " - delete packages that can no longer be a cache hit" + + " (enables all --changed-* options)", + file=out, + ) print( yellow(" --no-clean-invalid") + " - Skip cleaning invalid binpkgs", @@ -432,6 +438,10 @@ def optionSwitch(option, opts, action=None): options["changed-subslot"] = True elif o in ("--changed-iuse"): options["changed-iuse"] = True + elif o in ("--unreachable"): + options["changed-deps"] = True + options["changed-subslot"] = True + options["changed-iuse"] = True elif o in ("-i", "--ignore-failure"): options["ignore-failure"] = True elif o in ("-u", "--unique-use"): @@ -488,6 +498,7 @@ def optionSwitch(option, opts, action=None): "changed-deps", "changed-subslot", "changed-iuse", + "unreachable", "unique-use", "no-clean-invalid", ]