#!/usr/bin/env python3 import os import re import subprocess import sys from pathlib import Path from portage.versions import vercmp def get_ebuilds(repo_path): """Find all ebuild files in the repository.""" ebuild_files = list(Path(repo_path).glob('*/*/*.ebuild')) packages = {} for ebuild in ebuild_files: category = ebuild.parent.parent.name package_name = ebuild.parent.name version = ebuild.name.replace(f"{package_name}-", "").replace(".ebuild", "") if (category, package_name) not in packages: packages[(category, package_name)] = [] packages[(category, package_name)].append((version, ebuild)) return packages def get_upstream_info(category, package_name, repo_path): """Get upstream location and available versions from equery meta output.""" try: result = subprocess.run( ['equery', 'meta', f"{category}/{package_name}"], capture_output=True, text=True, check=True ) location = None versions = [] repo_name = None for line in result.stdout.split('\n'): if line.startswith('Location:'): location = line.split(':', 1)[1].strip() elif line.startswith('Keywords:'): # Format: "Keywords: 3.0.67-r1:0:" parts = line.split() if len(parts) >= 2: version_part = parts[1] # "3.0.67-r1:0:" version = version_part.split(':')[0] versions.append(version) # Extract repository name from location path if location: # Location format: /var/db/repos/gentoo/sys-apps/portage # or /var/db/repos/local/dev-util/intel_clc path_parts = location.split('/') if len(path_parts) >= 5 and path_parts[3] == 'repos': repo_name = path_parts[4] # gentoo, local, etc. # Check if this package exists in the main Gentoo repository # or if it's only in our overlay repo_path_abs = Path(repo_path).resolve() location_path = Path(location) if location else None # Package is only in our overlay if the location is within our repo is_only_in_overlay = (location_path and (repo_path_abs in location_path.parents or location_path.parent.parent == repo_path_abs)) return location, versions, repo_name, is_only_in_overlay except subprocess.CalledProcessError: return None, [], None, False # Package might not exist upstream except FileNotFoundError: print("Error: equery command not found. Please install app-portage/gentoolkit.", file=sys.stderr) sys.exit(1) def is_version_obsolete(ebuild_version, upstream_versions, repo_path, upstream_location, is_only_in_overlay): """Check if ebuild version is obsolete compared to upstream versions.""" # If package only exists in our overlay, it can't be obsolete if is_only_in_overlay: return False if not upstream_versions or not upstream_location: return False # Check if upstream location is outside our repo upstream_path = Path(upstream_location) repo_path_abs = Path(repo_path).resolve() # If upstream is in our repo, skip comparison try: if repo_path_abs in upstream_path.parents or upstream_path == repo_path_abs: return False except: pass # If path comparison fails, continue with version check # Find the highest upstream version if not upstream_versions: return False highest_upstream = upstream_versions[0] for version in upstream_versions[1:]: try: if vercmp(version, highest_upstream) > 0: highest_upstream = version except: continue # Skip versions that can't be compared # Compare our version with the highest upstream try: return vercmp(ebuild_version, highest_upstream) < 0 except: return False def list_obsolete(repo_path=Path('.')): """Main function to list obsolete packages.""" repo_path_abs = Path(repo_path).resolve() packages = get_ebuilds(repo_path_abs) obsolete_packages = {} for (category, package_name), ebuilds in packages.items(): upstream_location, upstream_versions, repo_name, is_only_in_overlay = get_upstream_info( category, package_name, repo_path_abs ) # Skip packages that only exist in our overlay if is_only_in_overlay: continue # Skip if no upstream info or no versions found if not upstream_location or not upstream_versions: continue obsolete_ebuilds = [] all_obsolete = True for version, ebuild_path in ebuilds: if is_version_obsolete(version, upstream_versions, repo_path_abs, upstream_location, is_only_in_overlay): obsolete_ebuilds.append(ebuild_path) else: all_obsolete = False if obsolete_ebuilds: if all_obsolete: obsolete_packages[(category, package_name)] = ebuilds[0][1].parent else: obsolete_packages[(category, package_name)] = obsolete_ebuilds return obsolete_packages def main(): if len(sys.argv) < 3 or sys.argv[1] != 'list' or sys.argv[2] != 'obsolete': print("Usage: ebuild-repo-tools.py list obsolete [REPO_PATH]") print(" REPO_PATH: Path to the overlay repository (default: current directory)") sys.exit(1) repo_path = Path(sys.argv[3] if len(sys.argv) > 3 else '.') if not os.path.exists(repo_path): print(f"Error: Repository path '{repo_path}' does not exist.", file=sys.stderr) sys.exit(1) try: obsolete = list_obsolete(repo_path) if not obsolete: print("No obsolete packages found.") return repo_path = repo_path.absolute() for (category, package_name), paths in obsolete.items(): if isinstance(paths, Path): # Directory (all ebuilds obsolete) print(paths.relative_to(repo_path)) else: # List of ebuild paths for path in paths: print(path) except KeyboardInterrupt: print("\nOperation cancelled by user.", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()