#!/usr/bin/env python3 # Copyright 2011 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 import datetime import optparse import os.path import random import re import socket import subprocess import xmlrpc.client from portage.package.ebuild.getmaskingstatus import getmaskingstatus from portage.xml.metadata import MetaDataXML import portage.versions from common import login, retry if __name__ == "__main__": parser = optparse.OptionParser() parser.add_option("--arch", dest="arch", action="append", help="Gentoo arch to use, e.g. x86, amd64, ... Can be passed multiple times.") parser.add_option("--days", dest="days", type=int, default=30, help="Number of days in the tree after stabilization is possible.") parser.add_option("--repo", dest="repo", help="Path to portage git repository") parser.add_option("--category", dest="category", help="Portage category filter (default is all categories)") parser.add_option("--exclude", dest="exclude", default=".*(kde-base|sci|lisp|perl-core|virtual|gnome|ruby|x11|mono|dotnet|games|xfce|xfburn|mousepad|orage|xfbib|thunar|ristretto|pragha|xfmpc|parole|midori|gigolo|rodent|xfwm|girara|zathura|leechcraft).*", help="Regular expression for excluded packages.") parser.add_option("-o", "--output", dest="output_filename", default="stabilization-candidates.txt", help="Output filename for generated stabilization candidates list.") (options, args) = parser.parse_args() if not options.arch: parser.error("--arch option is required") if not options.repo: parser.error("--repo option is required") if args: parser.error("unrecognized command-line args") url = 'https://bugs.gentoo.org/xmlrpc.cgi' print('You will be prompted for your Gentoo Bugzilla username and password (%s).' % url) bugzilla = xmlrpc.client.ServerProxy(url) user, login_data = login(bugzilla) final_candidates = [] now = datetime.datetime.now() for cp in portage.portdb.cp_all(): if options.category and not cp.startswith(options.category + "/"): continue cvs_path = os.path.join(options.repo, cp) try: metadata = MetaDataXML(os.path.join(cvs_path, 'metadata.xml'), '/usr/portage/metadata/herds.xml') except IOError: continue maintainer_split = metadata.format_maintainer_string().split(' ', 1) maintainer = maintainer_split[0] if len(maintainer_split) > 1: other_maintainers = maintainer_split[1].split(',') else: other_maintainers = [] if options.exclude: if re.match(options.exclude, cp): continue if re.match(options.exclude, maintainer): continue skip = False for m in other_maintainers: if re.match(options.exclude, m): skip = True break if skip: continue best_stable = portage.versions.best(portage.portdb.match(cp)) if not best_stable: continue print('Working on %s...' % cp, end=' ') candidates = [] for cpv in portage.portdb.cp_list(cp): # Only consider higher versions than best stable. if portage.versions.pkgcmp(portage.versions.pkgsplit(cpv), portage.versions.pkgsplit(best_stable)) != 1: continue # Eliminate alpha, beta, pre, rc, and so on packages. is_unstable = False for suffix in portage.versions.endversion_keys: if ("_" + suffix) in portage.versions.pkgsplit(cpv)[1]: is_unstable = True break if is_unstable: continue # Eliminate 'live' packages. Obviously have some false positives, # but it'd be much worse to miss something. There are variations # like -r9999 or .9999 in the tree. if '99' in cpv: continue # Eliminate hard masked packages among others. if getmaskingstatus(cpv) not in [['~%s keyword' % arch] for arch in options.arch]: continue candidates.append(cpv) if not candidates: print('no candidates') continue candidates.sort(key=portage.versions.cpv_sort_key()) candidates.reverse() # Only consider the best version for stabilization. # It's usually better tested, and often maintainers refuse # to stabilize anything else, e.g. bug #391607. best_candidate = str(candidates[0]) pv = portage.versions.catsplit(best_candidate)[1] try: git_log = subprocess.check_output( ['git', 'log', '--date-order', '--date=short', '--format=%cd', '%s.ebuild' % pv], cwd=os.path.join(options.repo, cp)) changelog_date_str = git_log.splitlines()[0].decode('utf-8') changelog_date = datetime.datetime.strptime(changelog_date_str, '%Y-%m-%d') if now - changelog_date < datetime.timedelta(days=options.days): print('not old enough') continue except subprocess.CalledProcessError as ex: print('git error: ' + ex.output) continue keywords = portage.db["/"]["porttree"].dbapi.aux_get(best_candidate, ['KEYWORDS'])[0] missing_arch = False for arch in options.arch: if arch not in keywords: missing_arch = True break if missing_arch: print('not keyworded ~arch') continue # Do not risk trying to stabilize a package with known bugs. @retry(socket.error) def get_package_bugs(): params = {} params['Bugzilla_token'] = login_data['token'] params['summary'] = cp return [x for x in bugzilla.Bug.search(params)['bugs'] if x['is_open'] and x['severity'] not in ['enhancement', 'QA']] bugs = get_package_bugs() if bugs: print('has bugs') continue # Protection against filing a stabilization bug twice. @retry(socket.error) def get_package_bugs(): params = {} params['Bugzilla_token'] = login_data['token'] params['summary'] = best_candidate return bugzilla.Bug.search(params)['bugs'] bugs = get_package_bugs() if bugs: print('version has bugs') continue ebuild_name = portage.versions.catsplit(best_candidate)[1] + ".ebuild" ebuild_path = os.path.join(cvs_path, ebuild_name) manifest_path = os.path.join(cvs_path, 'Manifest') try: original_contents = open(ebuild_path).read() manifest_contents = open(manifest_path).read() except IOError as e: print(e) continue try: for arch in options.arch: subprocess.check_output(["ekeyword", arch, ebuild_name], cwd=cvs_path) subprocess.check_output(["repoman", "manifest"], cwd=cvs_path) subprocess.check_output(["repoman", "full"], cwd=cvs_path) except subprocess.CalledProcessError: print('repoman error') continue finally: f = open(ebuild_path, "w") f.write(original_contents) f.close() f = open(manifest_path, "w") f.write(manifest_contents) f.close() with open(options.output_filename, 'a') as f: f.write('# %s %s\n' % (maintainer, ', '.join(other_maintainers))) f.write('%s\n' % best_candidate) print((best_candidate, maintainer, other_maintainers)) params = {} params['Bugzilla_token'] = login_data['token'] bugzilla.User.logout(params)