commit: aae28fd06a45ad1ba0c3cb62bcbf294a46d7f876 Author: Thomas Bracht Laumann Jespersen <t <AT> laumann <DOT> xyz> AuthorDate: Wed Jan 21 05:11:58 2026 +0000 Commit: Sam James <sam <AT> gentoo <DOT> org> CommitDate: Wed Jan 21 06:03:11 2026 +0000 URL: https://gitweb.gentoo.org/proj/gentoolkit.git/commit/?id=aae28fd0
eclean-pkg: clean out debuginfo tarballs For each binpkg identified for removal, construct its corresponding debuginfo tarball name, check if it exists and add it to the removal list as well. The reported size for each package that is removed includes the debuginfo tarball sizes. The structure returned by findPackages() is changed from a list of files to a tuple of (binpkg, Optional[debugpack]) and the cleaning and prompting is adapted to handle this new structure. Notably, now pretend_clean() has to deal with the files for each entry being either a list (for files) or a tuple (for binpkgs). Closes: https://bugs.gentoo.org/967112 Signed-off-by: Thomas Bracht Laumann Jespersen <t <AT> laumann.xyz> Part-of: https://codeberg.org/gentoo/gentoolkit/pulls/3 Signed-off-by: Sam James <sam <AT> gentoo.org> pym/gentoolkit/eclean/clean.py | 90 +++++++++++++++++++++++++++++++++++++---- pym/gentoolkit/eclean/output.py | 5 ++- pym/gentoolkit/eclean/search.py | 31 ++++++++++++-- 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/pym/gentoolkit/eclean/clean.py b/pym/gentoolkit/eclean/clean.py index 776c6a4..c77d9e2 100644 --- a/pym/gentoolkit/eclean/clean.py +++ b/pym/gentoolkit/eclean/clean.py @@ -40,7 +40,7 @@ class CleanUp: clean_size = 0 # clean all entries one by one; sorting helps reading for key in sorted(clean_dict): - clean_size += self._clean_files(clean_dict[key], key, file_type) + clean_size += self._clean_files(clean_dict[key], key) # return total size of deleted or to delete files clean_size += self._clean_vcs_src(vcs) return clean_size @@ -61,7 +61,7 @@ class CleanUp: clean_size = 0 # clean all entries one by one; sorting helps reading for key in sorted(clean_dict): - clean_size += self._clean_files(clean_dict[key], key, file_type) + clean_size += self._clean_binary_package(clean_dict[key], key) # run 'emaint --fix' here if clean_size: @@ -88,10 +88,19 @@ class CleanUp: # tally all entries one by one; sorting helps reading if vcs: clean_size += self._clean_vcs_src(vcs, pretend=True) - for key in sorted(clean_dict): - key_size = self._get_size(clean_dict[key]) - self.controller(key_size, key, clean_dict[key], file_type) - clean_size += key_size + if file_type == "file": + for key in sorted(clean_dict): + key_size = self._get_size(clean_dict[key]) + self.controller(key_size, key, clean_dict[key], file_type) + clean_size += key_size + else: + # binary package + for key in sorted(clean_dict): + (binpkg, debugpack) = clean_dict[key] + key_size = self._get_size([binpkg, debugpack] if debugpack else [binpkg]) + self.controller(key_size, key, clean_dict[key], file_type) + clean_size += key_size + return clean_size def _get_size(self, key): @@ -111,7 +120,72 @@ class CleanUp: print(pp.error("Error: %s" % str(er)), file=sys.stderr) return key_size - def _clean_files(self, files, key, file_type): + def _get_size_valid_symlink(self, file_): + """ + Get a file size, attempting to remove broken symlinks when + possible (attempting to remove any broken symlinks results in + this function returning 0). + """ + try: + statinfo = os.stat(file_) + if statinfo.st_nlink == 1: + return statinfo.st_size + except OSError as er: + if os.path.exists(os.readlink(file_)): + print(pp.error("Could not get stat info for:" + file_), file=sys.stderr) + print(pp.error("Error: %s" % str(er)), file=sys.stderr) + else: + try: + os.remove(file_) + print( + pp.error("Removed broken symbolic link " + file_), + file=sys.stderr, + ) + except OSError as er: + print( + pp.error("Error deleting broken symbolic link " + file_), + file=sys.stderr, + ) + print(pp.error("Error: %s" % str(er)), file=sys.stderr) + return 0 + + def _clean_binary_package(self, files: tuple[str, str | None], key: str): + # collect files + rm_files = [] + rm_size = 0 + + if (sz := self._get_size_valid_symlink(files[0])): + rm_files.append(files[0]) + rm_size += sz + + if files[1] and (sz := self._get_size_valid_symlink(files[1])): + rm_files.append(files[1]) + rm_size += sz + + if not rm_size: + return 0 + + # optionally prompt for removal + clean_size = 0 + if self.controller(rm_size, key, files, "binary package"): + for file_ in rm_files: + # ... try to delete it. + try: + statinfo = os.stat(file_) + os.unlink(file_) + # only count size if successfully deleted and not a link + if statinfo.st_nlink == 1: + clean_size += statinfo.st_size + try: + os.rmdir(os.path.dirname(file_)) + except OSError: + pass + except OSError as er: + print(pp.error("Could not delete " + file_), file=sys.stderr) + print(pp.error("Error: %s" % str(er)), file=sys.stderr) + return clean_size + + def _clean_files(self, files, key): """File removal function.""" clean_size = 0 for file_ in files: @@ -141,7 +215,7 @@ class CleanUp: file=sys.stderr, ) print(pp.error("Error: %s" % str(er)), file=sys.stderr) - if self.controller(statinfo.st_size, key, file_, file_type): + if self.controller(statinfo.st_size, key, file_, "file"): # ... try to delete it. try: os.unlink(file_) diff --git a/pym/gentoolkit/eclean/output.py b/pym/gentoolkit/eclean/output.py index 04fb493..e02ccce 100644 --- a/pym/gentoolkit/eclean/output.py +++ b/pym/gentoolkit/eclean/output.py @@ -135,7 +135,10 @@ class OutputControl: """ if not self.options["quiet"]: # pretty print mode - print(self.prettySize(size, True), self.pkg_color(key)) + if file_type == "binary package" and clean_list[1] is not None: + print(self.prettySize(size, True), self.pkg_color(key), "(+ debug tarball)") + else: + print(self.prettySize(size, True), self.pkg_color(key)) elif self.options["pretend"] or self.options["interactive"]: # file list mode if file_type == "checkout": diff --git a/pym/gentoolkit/eclean/search.py b/pym/gentoolkit/eclean/search.py index 0a51ce9..97b5f8d 100644 --- a/pym/gentoolkit/eclean/search.py +++ b/pym/gentoolkit/eclean/search.py @@ -38,6 +38,11 @@ DEPRECATED = pp.warn(deprecated_message) debug_modules = [] +# Location for debuginfo tarballs +PACKDEBUG_PATH = "/usr/lib/debug/.tarball" +# suffix (including extension) of debug tarballs +PACKDEBUG_TARBALL_SUFFIX = "debug.tar.xz" + def dprint(module, message): if module in debug_modules: @@ -580,6 +585,24 @@ def _deps_equal(deps_a, eapi_a, deps_b, eapi_b, libc_deps, uselist=None, cpv=Non return deps_a == deps_b +def _find_debuginfo_tarball(cpv: portage.versions._pkg_str, cp: str): + """ + From a CPV, identify and check for a matching debuginfo tarball. + + Returns None if no such file exists + """ + pf = portage.catsplit(cpv)[1] + if cpv.build_id is None: + debuginfo_tarball = "-".join([pf, PACKDEBUG_TARBALL_SUFFIX]) + else: + debuginfo_tarball = "-".join([pf, str(cpv.build_id), PACKDEBUG_TARBALL_SUFFIX]) + + debuginfo_path = os.path.join(PACKDEBUG_PATH, cp, debuginfo_tarball) + if not os.path.exists(debuginfo_path): + return None + return debuginfo_path + + def findPackages( options: dict[str, bool], exclude: Optional[dict] = None, @@ -638,7 +661,7 @@ def findPackages( # Dictionary of binary packages to clean. Organized as cpv->[pkgs] in order # to support FEATURES=binpkg-multi-instance. - dead_binpkgs: dict[str, list[str]] = {} + dead_binpkgs: dict[str, tuple[str, str | None]] = {} keep_binpkgs = {} def mk_binpkg_key(cpv): @@ -683,7 +706,8 @@ def findPackages( binpkg_key = mk_binpkg_key(drop_cpv) binpkg_path = bin_dbapi.bintree.getname(drop_cpv) - dead_binpkgs.setdefault(binpkg_key, []).append(binpkg_path) + debuginfo_path = _find_debuginfo_tarball(drop_cpv, cp) + dead_binpkgs[binpkg_key] = (binpkg_path, debuginfo_path) if new_time < old_time: continue @@ -727,7 +751,8 @@ def findPackages( del keep_binpkgs[cpv_key] binpkg_path = bin_dbapi.bintree.getname(cpv) - dead_binpkgs.setdefault(binpkg_key, []).append(binpkg_path) + debuginfo_path = _find_debuginfo_tarball(cpv, cp) + dead_binpkgs[binpkg_key] = (binpkg_path, debuginfo_path) try: invalid_paths = bin_dbapi.bintree.invalid_paths except AttributeError:
