#!/usr/bin/python # Copyright 1999-2017 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Id$ # Author: Christian Ruppert __version__ = "git" # works just with stable keywords! MAIN_ARCH = "auto" # can be overridden by -m ARCH TARGET_ARCH = "auto" # can be overridden by -t ARCH # auto means e.g.: # MAIN_ARCH = amd64 # TARGET_ARCH = ~amd64 # That will show you general stable candidates for amd64. # The arch will be taken from your portage settings (e.g. make.conf). ################################ # do not change anything below # ################################ from os.path import join, basename from sys import stderr, stdout from os import stat from time import time from xml.dom import minidom, NotFoundErr from xml.parsers.expat import ExpatError # TODO: just import needed stuff to safe memory/time and maybe use "as foo" import portage import portage.versions from optparse import OptionParser from time import gmtime, strftime # override/change portage module settings def _portage_settings(var, value, settings=None): if not settings: settings = portage.settings settings.unlock() settings[var] = value # backup_changes is very important since it can cause trouble, # if we do not backup our changes! settings.backup_changes(var) settings.lock() # add stuff to our imlate dict def _add_ent(imlate, cat, pkg, ver, our_ver): if not cat in list(imlate.keys()): imlate[cat] = {} if not pkg in list(imlate[cat].keys()): imlate[cat][pkg] = [] imlate[cat][pkg].append(ver) imlate[cat][pkg].append(our_ver) return imlate def _fill(width, line, fill=" "): while len(line) < width: line = f"{str(line)}{str(fill)}" return line # create a hopefully pretty result def show_result(conf, pkgs): # X - len(colX) = space to fill col1 = -1 col2 = -1 for cat in pkgs: for pkg in pkgs[cat]: col1 = max(col1, len(f"{cat}/{pkg}")) col2 = max(col2, len(pkgs[cat][pkg][1])) col1 += 1 col2 += 1 _header = "%s candidates for 'gentoo' on '%s'" _helper = "{}{}{}".format( _fill(col1, "category/package[:SLOT])"), _fill(col2, "our version"), "best version", ) _cand = "" header = "" if conf["FILE"] == "stdout": out = stdout elif conf["FILE"] == "stderr": out = stderr else: out = open(conf["FILE"], "w") if conf["STABLE"] and conf["KEYWORD"]: _cand = "%i Stable and %i Keyword(~)" % ( conf["STABLE_SUM"], conf["KEYWORD_SUM"], ) elif conf["STABLE"]: _cand = "%i Stable" % conf["STABLE_SUM"] elif conf["KEYWORD"]: _cand = "%i Keyword(~)" % conf["KEYWORD_SUM"] header = _header % (_cand, conf["MAIN_ARCH"]) print("Generated on: %s" % conf["TIME"], file=out) print(_fill(len(header), "", "="), file=out) print(header, file=out) print(_fill(len(header), "", "="), file=out) print(file=out) print(_helper, file=out) print(_fill(len(_helper), "", "-"), file=out) for cat in sorted(pkgs.keys()): for pkg in sorted(pkgs[cat].keys()): print( "%s%s%s" % ( _fill(col1, (f"{cat}/{pkg}")), _fill(col2, pkgs[cat][pkg][1]), pkgs[cat][pkg][0], ), file=out, ) if conf["FILE"] != "stdout": out.close() def _get_metadata(metadata, element, tag): values = [] try: metadatadom = minidom.parse(metadata) except ExpatError as e: raise ExpatError( "%s: %s" % ( metadata, e, ) ) try: elements = metadatadom.getElementsByTagName(element) if not elements: return values except NotFoundErr: return values try: for _element in elements: node = _element.getElementsByTagName(tag) try: values.append(node[0].childNodes[0].data) except IndexError: pass except NotFoundErr: raise NotFoundErr("%s: Malformed input: missing 'flag' tag(s)" % (metadata)) metadatadom.unlink() return values def is_maintainer(maintainer, metadata): data = [] if maintainer == None: return True mtainer = maintainer.split(",") data = _get_metadata(metadata, "maintainer", "email") if not data and len(maintainer) == 0: return True elif not data and len(maintainer) > 0: return False else: for addy in data: for contact in mtainer: if addy == contact: return True if addy.startswith(contact): return True return False # fetch a list of arch (just stable) packages # -* is important to be sure that just arch is used def get_packages(conf): _pkgs = {} _portage_settings( "ACCEPT_KEYWORDS", ("-* %s" % str(conf["TARGET_ARCH"])), conf["portdb"].settings ) for cp in conf["portdb"].dbapi.cp_all(): cpvrs = [] slots = {} if conf["USER_PKGS"]: if not cp in conf["USER_PKGS"] and not basename(cp) in conf["USER_PKGS"]: continue # None is important to match also on empty string if conf["MAINTAINER"] != None: if not is_maintainer( conf["MAINTAINER"], join(conf["PORTDIR"], cp, "metadata.xml") ): continue cpvrs = conf["portdb"].dbapi.match(cp) for cpvr in cpvrs: slot = conf["portdb"].dbapi.aux_get(cpvr, ["SLOT"])[0] if not slot in slots: slots[slot] = [] slots[slot].append(cpvr) for slot in sorted(slots): cpvr = portage.versions.best(slots[slot]) if cpvr: (cat, pkg, ver, rev) = portage.versions.catpkgsplit(cpvr) if not cat in list(_pkgs.keys()): _pkgs[cat] = {} if not pkg in list(_pkgs[cat].keys()): _pkgs[cat][pkg] = [] if rev != "r0": ver = f"{ver}-{rev}" _pkgs[cat][pkg].append(ver) return _pkgs # compare get_packages() against MAIN_ARCH def get_imlate(conf, pkgs): _portage_settings( "ACCEPT_KEYWORDS", ("-* %s" % str(conf["MAIN_ARCH"])), conf["portdb"].settings ) stable = str(conf["MAIN_ARCH"].lstrip("~")) testing = "~%s" % stable exclude = "-%s" % stable exclude_all = "-*" imlate = {} for cat in sorted(pkgs.keys()): for pkg in sorted(pkgs[cat].keys()): for vr in pkgs[cat][pkg]: cpvr = "" abs_pkg = "" kwds = "" our = "" our_ver = "" mtime = 0 slot = 0 # 0 = none(default), 1 = testing(~arch), 2 = stable(arch), # 3 = exclude(-arch), 4 = exclude_all(-*) # -* would be overridden by ~arch or arch kwd_type = 0 cpvr = f"{cat}/{pkg}-{vr}" # absolute ebuild path for mtime check abs_pkg = join(conf["PORTDIR"], cat, pkg, basename(cpvr)) abs_pkg = "%s.ebuild" % str(abs_pkg) kwds = conf["portdb"].dbapi.aux_get(cpvr, ["KEYWORDS"])[0] # FIXME: %s is bad.. maybe even cast it, else there are issues because its unicode slot = ":%s" % conf["portdb"].dbapi.aux_get(cpvr, ["SLOT"])[0] if slot == ":0": slot = "" # sorted() to keep the right order # e.g. -* first, -arch second, arch third and ~arch fourth # -* -foo ~arch # example: -* would be overridden by ~arch for kwd in sorted(kwds.split()): if kwd == stable: kwd_type = 2 break elif kwd == exclude: kwd_type = 3 break elif kwd == exclude_all: kwd_type = 4 elif kwd == testing: kwd_type = 1 break # ignore -arch and already stabilized packages if kwd_type == 3 or kwd_type == 2: continue # drop packages with -* and without ~arch or arch # even if there is another version which includes arch or ~arch if kwd_type == 4: continue # drop "stable candidates" with mtime < 30 days # Shall we use gmtime/UTC here? if kwd_type == 1: mtime = int((time() - stat(abs_pkg).st_mtime) / 60 / 60 / 24) if mtime < conf["MTIME"]: continue # look for an existing stable version our = portage.versions.best( conf["portdb"].dbapi.match(f"{cat}/{pkg}{slot}") ) if our: _foo = portage.versions.pkgsplit(our) our_ver = _foo[1] if _foo[2] != "r0": our_ver = f"{our_ver}-{_foo[2]}" else: our_ver = "" # we just need the version if > our_ver if our_ver: if portage.versions.vercmp(our_ver, vr) >= 0: continue if kwd_type == 1 and conf["STABLE"]: imlate = _add_ent(imlate, cat, (f"{pkg}{slot}"), vr, our_ver) conf["STABLE_SUM"] += 1 elif kwd_type == 0 and conf["KEYWORD"]: conf["KEYWORD_SUM"] += 1 imlate = _add_ent(imlate, cat, (f"~{pkg}{slot}"), vr, our_ver) return imlate # fetch portage related settings def get_settings(conf=None): if not isinstance(conf, dict) and conf: raise TypeError("conf must be dict() or None") if not conf: conf = {} # TODO: maybe we should improve it a bit ;) mysettings = portage.config( config_incrementals=portage.const.INCREMENTALS, local_config=False ) if conf["MAIN_ARCH"] == "auto": conf["MAIN_ARCH"] = "%s" % mysettings["ACCEPT_KEYWORDS"].split(" ")[0].lstrip( "~" ) if conf["TARGET_ARCH"] == "auto": conf["TARGET_ARCH"] = "~%s" % mysettings["ACCEPT_KEYWORDS"].split(" ")[ 0 ].lstrip("~") # TODO: exclude overlay categories from check if conf["CATEGORIES"]: _mycats = [] for _cat in conf["CATEGORIES"].split(","): _cat = _cat.strip() _mycats.append(_cat) if _cat not in mysettings.categories: raise ValueError("invalid category for -C switch '%s'" % _cat) mysettings.categories = _mycats # maybe thats not necessary because we override porttrees below.. _portage_settings("PORTDIR_OVERLAY", "", mysettings) trees = portage.create_trees() trees["/"]["porttree"].settings = mysettings portdb = trees["/"]["porttree"] portdb.dbapi.settings = mysettings portdb.dbapi.porttrees = [portage.portdb.porttree_root] # does it make sense to remove _all_ useless stuff or just leave it as it is? # portdb.dbapi._aux_cache_keys.clear() # portdb.dbapi._aux_cache_keys.update(["EAPI", "KEYWORDS", "SLOT"]) conf["PORTDIR"] = portage.settings["PORTDIR"] conf["portdb"] = portdb return conf # just for standalone def main(): conf = {} pkgs = {} parser = OptionParser(version="%prog " + __version__) parser.usage = "%prog [options] [category/package] ..." parser.disable_interspersed_args() parser.add_option( "-f", "--file", dest="filename", action="store", type="string", help="write result into FILE [default: %default]", metavar="FILE", default="stdout", ) parser.add_option( "-m", "--main", dest="main_arch", action="store", type="string", help="set main ARCH (e.g. your arch) [default: %default]", metavar="ARCH", default=MAIN_ARCH, ) parser.add_option( "-t", "--target", dest="target_arch", action="store", type="string", help="set target ARCH (e.g. x86) [default: %default]", metavar="ARCH", default=TARGET_ARCH, ) parser.add_option( "--mtime", dest="mtime", action="store", type="int", help="set minimum MTIME in days [default: %default]", metavar="MTIME", default=30, ) # TODO: leave a good comment here (about True/False) :) parser.add_option( "-s", "--stable", dest="stable", action="store_true", default=False, help="just show stable candidates (e.g. -s and -k is the default result) [default: True]", ) parser.add_option( "-k", "--keyword", dest="keyword", action="store_true", default=False, help="just show keyword candidates (e.g. -s and -k is the default result) [default: True]", ) parser.add_option( "-M", "--maintainer", dest="maintainer", action="store", type="string", help="Show only packages from the specified maintainer", metavar="MAINTAINER", default=None, ) parser.add_option( "-C", "--category", "--categories", dest="categories", action="store", default=None, metavar="CATEGORIES", help="just check in the specified category/categories (comma separated) [default: %default]", ) (options, args) = parser.parse_args() if len(args) > 0: conf["USER_PKGS"] = args else: conf["USER_PKGS"] = [] # cleanup optparse try: parser.destroy() except AttributeError: # to be at least python 2.4 compatible del parser._short_opt del parser._long_opt del parser.defaults # generated timestamp (UTC) conf["TIME"] = strftime("%a %b %d %H:%M:%S %Z %Y", gmtime()) # package counter conf["KEYWORD_SUM"] = 0 conf["STABLE_SUM"] = 0 if not options.main_arch in portage.archlist and options.main_arch != "auto": raise ValueError("invalid MAIN ARCH defined!") if not options.target_arch in portage.archlist and options.target_arch != "auto": raise ValueError("invalid TARGET ARCH defined!") conf["MAIN_ARCH"] = options.main_arch conf["TARGET_ARCH"] = options.target_arch conf["FILE"] = options.filename conf["MTIME"] = options.mtime if not options.stable and not options.keyword: conf["STABLE"] = True conf["KEYWORD"] = True else: conf["STABLE"] = options.stable conf["KEYWORD"] = options.keyword conf["CATEGORIES"] = options.categories conf["MAINTAINER"] = options.maintainer # append to our existing conf = get_settings(conf) pkgs = get_packages(conf) pkgs = get_imlate(conf, pkgs) show_result(conf, pkgs) if __name__ == "__main__": main()