#!/usr/bin/python # Copyright 1999-2010 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Id: $ # Author: Christian Ruppert # Python 2.6 compatibility from __future__ import print_function VERSION = "1.0.0" # 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 if __name__ == "__main__": 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 = "%s%s" % ( str( line ), str( fill ) ) return line # create a hopefully pretty result def show_result( conf, pkgs ): # X - len(colX) = space to fill col1 = 40 col2 = 20 _header = "%s candidates for 'gentoo' on '%s'" _helper = "category/package[:SLOT] 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() ): print("%s/" % cat, file=out) for pkg in sorted( pkgs[cat].keys() ): print("%s%s%s" % ( _fill( col1, ( " %s" % 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) if tag == "herd" and (not node or not node[0].childNodes): # print >> stderr, "'%s' is missing a tag or it is empty," % metadata # print >> stderr, "please file a bug at https://bugs.gentoo.org and refer to http://www.gentoo.org/proj/en/devrel/handbook/handbook.xml?part=2&chap=4" values.append("no-herd") continue values.append(node[0].childNodes[0].data) 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 def is_herd(herd, metadata): data = [] if herd == None: return True hrd = herd.split(",") data = _get_metadata(metadata, "pkgmetadata", "herd") if not data and len(herd) == 0: return True elif not data and len(herd) > 0: return False else: for hd in data: for hd2 in hrd: if hd == hd2: return True if hd.startswith(hd2): 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"]: 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 if conf["HERD"] != None: if not is_herd(conf["HERD"], 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 = "%s-%s" % ( 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 = "%s/%s-%s" % ( 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( "%s/%s%s" % ( cat, pkg, slot ) ) ) if our: _foo = portage.versions.pkgsplit( our ) our_ver = _foo[1] if _foo[2] != "r0": our_ver = "%s-%s" % ( 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, ("%s%s" % (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, ( "~%s%s" % (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( "-H", "--herd", dest = "herd", action = "store", type = "string", help = "Show only packages from the specified herd", metavar = "HERD", default = None) # # EXPERIMENTAL # parser.add_option( "-e", "--experimental", dest = "experimental", action = "store_true", default = False, # help = "enables experimental functions/features (have a look for # EXPERIMENTAL comments in the source) [default: %default]" ) 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["EXPERIMENTAL"] = options.experimental conf["CATEGORIES"] = options.categories conf["MAINTAINER"] = options.maintainer conf["HERD"] = options.herd # 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()