From: Fabian Groffen Date: Fri, 19 Jan 2007 19:07:50 +0000 (-0000) Subject: Merged from trunk 5657:5659 X-Git-Url: http://git.tremily.us/gitweb.cgi?a=commitdiff_plain;h=3da391bd67615875e376df6f3faccecddeeda983;p=portage.git Merged from trunk 5657:5659 merge glep42 into trunk (WarnerBro) merge didn't bring in portage_news, no idea why (WarnerBro) svn path=/main/branches/prefix/; revision=5725 --- diff --git a/bin/emerge b/bin/emerge index d38bd337c..46783befd 100755 --- a/bin/emerge +++ b/bin/emerge @@ -3525,6 +3525,25 @@ def chk_updated_info_files(root, infodirs, prev_mtimes, retval): def post_emerge(settings, mtimedb, retval): + """ + Misc. things to run at the end of a merge session. + + Update Info Files + Update Config Files + Update News Items + Commit mtimeDB + Exit Emerge + + @param settings: Configuration settings (typically portage.settings) + @type settings: portage.config() + @param mtimedb: The mtimeDB to store data needed across merge invocations + @type mtimedb: MtimeDB class instance + @param retval: Emerge's return value + @type retval: Int + @rype: None + @returns: + 1. Calls sys.exit(retval) + """ target_root = settings["ROOT"] info_mtimes = mtimedb["info"] config_protect = settings.get("CONFIG_PROTECT","").split() @@ -3542,6 +3561,16 @@ def post_emerge(settings, mtimedb, retval): normalize_path(target_root + EPREFIX), infodirs, info_mtimes, retval) chk_updated_cfg_files(normalize_path(target_root + EPREFIX), config_protect) + + NEWS_PATH = os.path.join( "metadata", "news" ) + UNREAD_PATH = os.path.join( target_root, portage.PRIVATE_PATH, 'news') + repo_id = 'gentoo' + unreadItems = checkUpdatedNewsItems(target_root, NEWS_PATH, UNREAD_PATH, repo_id) + if unreadItems: + print colorize("WARN", " * IMPORTANT:"), + print "%s news items need reading." % unreadItems + print colorize("WARN", " *"), + print "Type something to read the news" mtimedb.commit() sys.exit(retval) @@ -3580,6 +3609,28 @@ def chk_updated_cfg_files(target_root, config_protect): print " "+yellow("*")+" Type "+green("emerge --help config")+" to learn how to update config files." print +def checkUpdatedNewsItems( root, NEWS_PATH, UNREAD_PATH, repo_id ): + """ + Examines news items in repodir + '/' + NEWS_PATH and attempts to find unread items + Returns the number of unread (yet relevent) items. + + @param root: + @type root: + @param NEWS_PATH: + @type NEWS_PATH: + @param UNREAD_PATH: + @type UNREAD_PATH: + @param repo_id: + @type repo_id: + @rtype: Integer + @returns: + 1. The number of unread but relevant news items. + + """ + from portage_news import NewsManager + manager = NewsManager( root, NEWS_PATH, UNREAD_PATH ) + return manager.getUnreadItems( repo_id, update=True ) + def is_valid_package_atom(x): try: testkey = portage.dep_getkey(x) diff --git a/bin/portageq b/bin/portageq index 3c863aa37..51c50442b 100755 --- a/bin/portageq +++ b/bin/portageq @@ -260,6 +260,17 @@ def envvar(argv): else: print portage.settings[arg] +def get_repos(argv): + """ + Returns all repos with names (repo_name file) argv[0] = $ROOT + """ + print " ".join(portage.db[argv[0]]["porttree"].dbapi.getRepositories()) + +def get_repo_path(argv): + """ + Returns the path to the repo named argv[1], argv[0] = $ROOT + """ + print portage.db[argv[0]]["porttree"].dbapi.getRepositoryPath(argv[1]) #----------------------------------------------------------------------------- # diff --git a/pym/portage.py b/pym/portage.py index 9ea00896e..94bce04ff 100644 --- a/pym/portage.py +++ b/pym/portage.py @@ -71,7 +71,8 @@ try: MOVE_BINARY, PRELINK_BINARY, WORLD_FILE, MAKE_CONF_FILE, MAKE_DEFAULTS_FILE, \ DEPRECATED_PROFILE_FILE, USER_VIRTUALS_FILE, EBUILD_SH_ENV_FILE, \ INVALID_ENV_FILE, CUSTOM_MIRRORS_FILE, CONFIG_MEMORY_FILE,\ - INCREMENTALS, EAPI, MISC_SH_BINARY, EPREFIX + INCREMENTALS, EAPI, MISC_SH_BINARY, REPO_NAME_LOC, REPO_NAME_FILE, \ + EPREFIX from portage_data import ostype, lchown, userland, secpass, uid, wheelgid, \ portage_uid, portage_gid, userpriv_groups @@ -4534,19 +4535,34 @@ def getmaskingstatus(mycpv, settings=None, portdb=None): return rValue class portagetree: - def __init__(self, root="/", virtual=None, clone=None, settings=None): + def __init__(self, root="/", virtual=None, clone=None, settings=None, portroot=None): + """ + Constructor for a PortageTree + + Note: Portroot was added for GLEP 42 functionality and defaults to the $PORTDIR + env variable. + + @param root: ${ROOT}, defaults to '/', see make.conf(5) + @type root: String/Path + @param virtual: UNUSED + @type virtual: No Idea + @param clone: Set this if you want a copy of Clone + @type clone: Existing portagetree Instance + @param settings: Portage Configuration object (portage.settings) + @type settings: Instance of portage.config + """ if clone: - self.root=clone.root - self.portroot=clone.portroot - self.pkglines=clone.pkglines + self.root = clone.root + self.portroot = clone.portroot + self.pkglines = clone.pkglines else: - self.root=root + self.root = root if settings is None: settings = globals()["settings"] self.settings = settings - self.portroot=settings["PORTDIR"] - self.virtual=virtual + self.portroot = settings["PORTDIR"] + self.virtual = virtual self.dbapi = portdbapi( settings["PORTDIR"], mysettings=settings) @@ -5539,6 +5555,14 @@ class portdbapi(dbapi): self.porttrees = [self.porttree_root] + \ [os.path.realpath(t) for t in self.mysettings["PORTDIR_OVERLAY"].split()] + self.treemap = {} + for path in self.porttrees: + try: + repo_name = open( os.path.join( path , REPO_NAME_LOC ) ,'r').readline().rstrip() + self.treemap[repo_name] = path + except (OSError,IOError): + pass + self.auxdbmodule = self.mysettings.load_best_module("portdbapi.auxdbmodule") self.auxdb = {} self._init_cache_dirs() @@ -5610,6 +5634,24 @@ class portdbapi(dbapi): def findname(self,mycpv): return self.findname2(mycpv)[0] + def getRepositoryPath( self, repository_id ): + """ + This function is required for GLEP 42 compliance; given a valid repository ID + it must return a path to the repository + TreeMap = { id:path } + """ + if repository_id in self.treemap: + return self.treemap[repository_id] + return None + + def getRepositories( self ): + """ + This function is required for GLEP 42 compliance; it will return a list of + repository ID's + TreeMap = { id:path } + """ + return [k for k in self.treemap.keys() if k] + def findname2(self, mycpv, mytree=None): """ Returns the location of the CPV, and what overlay it was in. diff --git a/pym/portage_const.py b/pym/portage_const.py index 51a19f9a2..d5cda0111 100644 --- a/pym/portage_const.py +++ b/pym/portage_const.py @@ -47,6 +47,9 @@ CUSTOM_MIRRORS_FILE = USER_CONFIG_PATH+"/mirrors" CONFIG_MEMORY_FILE = PRIVATE_PATH + "/config" COLOR_MAP_FILE = USER_CONFIG_PATH + "/color.map" +REPO_NAME_FILE = "repo_name" +REPO_NAME_LOC = "profiles" + "/" + REPO_NAME_FILE + INCREMENTALS=["USE","USE_EXPAND","USE_EXPAND_HIDDEN","FEATURES","ACCEPT_KEYWORDS","ACCEPT_LICENSE","CONFIG_PROTECT_MASK","CONFIG_PROTECT","PRELINK_PATH","PRELINK_PATH_MASK"] EBUILD_PHASES = ["setup", "unpack", "compile", "test", "install", "preinst", "postinst", "prerm", "postrm", "other"] diff --git a/pym/portage_news.py b/pym/portage_news.py new file mode 100644 index 000000000..f02b7938f --- /dev/null +++ b/pym/portage_news.py @@ -0,0 +1,241 @@ +# portage: news management code +# Copyright 2006 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Id$ + +from portage_const import PRIVATE_PATH, INCREMENTALS, PROFILE_PATH +from portage import config, vartree, vardbapi, portdbapi +from portage_util import ensure_dirs +from portage_data import portage_gid +from portage_locks import lockfile, unlockfile + +import os, re + +class NewsManager(object): + """ + This object manages GLEP 42 style news items. It will cache news items + that have previously shown up and notify users when there are relevant news + items that apply to their packages that the user has not previously read. + + Creating a news manager requires: + root - typically ${ROOT} see man make.conf and man emerge for details + NEWS_PATH - path to news items; usually $REPODIR/metadata/news + UNREAD_PATH - path to the news.repoid.unread file; this helps us track news items + + """ + + TIMESTAMP_FILE = "news-timestamp" + + def __init__( self, root, NEWS_PATH, UNREAD_PATH, LANGUAGE_ID='en' ): + self.NEWS_PATH = NEWS_PATH + self.UNREAD_PATH = UNREAD_PATH + self.TIMESTAMP_PATH = os.path.join( root, PRIVATE_PATH, NewsManager.TIMESTAMP_FILE ) + self.target_root = root + self.LANGUAGE_ID = LANGUAGE_ID + self.config = config( config_root = os.environ.get("PORTAGE_CONFIGROOT", "/"), + target_root = root, config_incrementals = INCREMENTALS) + self.vdb = vardbapi( settings = self.config, root = root, + vartree = vartree( root = root, settings = self.config ) ) + self.portdb = portdbapi( porttree_root = self.config["PORTDIR"], mysettings = self.config ) + + # Ensure that the unread path exists and is writable. + dirmode = 02070 + modemask = 02 + ensure_dirs(self.UNREAD_PATH, mode=dirmode, mask=modemask, gid=portage_gid) + + def updateItems( self, repoid ): + """ + Figure out which news items from NEWS_PATH are both unread and relevant to + the user (according to the GLEP 42 standards of relevancy). Then add these + items into the news.repoid.unread file. + """ + + repos = self.portdb.getRepositories() + if repoid not in repos: + raise ValueError("Invalid repoID: %s" % repoid) + + if os.path.exists(self.TIMESTAMP_PATH): + timestamp = os.stat(self.TIMESTAMP_PATH).st_mtime + else: + timestamp = 0 + + path = os.path.join( self.portdb.getRepositoryPath( repoid ), self.NEWS_PATH ) + news = os.listdir( path ) + updates = [] + for item in news: + try: + file = os.path.join( path, item, item + "." + self.LANGUAGE_ID + ".txt") + tmp = NewsItem( file , timestamp ) + except TypeError: + continue + + if tmp.isRelevant( profile=os.readlink(PROFILE_PATH), config=config, vardb=self.vdb): + updates.append( tmp ) + del path + + path = os.path.join( self.UNREAD_PATH, "news-" + repoid + ".unread" ) + lockfile( path ) + unread_file = open( path, "a" ) + for item in updates: + unread_file.write( item.path + "\n" ) + + unread_file.close() + unlockfile(path) + + # Touch the timestamp file + f = open(self.TIMESTAMP_PATH, "w") + f.close() + + def getUnreadItems( self, repoid, update=False ): + """ + Determine if there are unread relevant items in news.repoid.unread. + If there are unread items return their number. + If update is specified, updateNewsItems( repoid ) will be called to + check for new items. + """ + + if update: + self.updateItems( repoid ) + + unreadfile = os.path.join( self.UNREAD_PATH, "news-"+ repoid +".unread" ) + lockfile(unreadfile) + if os.path.exists( unreadfile ): + unread = open( unreadfile ).readlines() + if len(unread): + return len(unread) + unlockfile(unread) + +_installedRE = re.compile("Display-If-Installed:(.*)\n") +_profileRE = re.compile("Display-If-Profile:(.*)\n") +_keywordRE = re.compile("Display-If-Keyword:(.*)\n") + +class NewsItem(object): + """ + This class encapsulates a GLEP 42 style news item. + It's purpose is to wrap parsing of these news items such that portage can determine + whether a particular item is 'relevant' or not. This requires parsing the item + and determining 'relevancy restrictions'; these include "Display if Installed" or + "display if arch: x86" and so forth. + + Creation of a news item involves passing in the path to the particular news item. + + """ + + def __init__( self, path, cache_mtime = 0 ): + """ + For a given news item we only want if it path is a file and it's + mtime is newer than the cache'd timestamp. + """ + if not os.path.isfile( path ): + raise TypeError + if not os.stat( path ).st_mtime > cache_mtime: + raise TypeError + self.path = path + self._parsed = False + + def isRelevant( self, vardb, config, profile ): + """ + This function takes a dict of keyword arguments; one should pass in any + objects need to do to lookups (like what keywords we are on, what profile, + and a vardb so we can look at installed packages). + Each restriction will pluck out the items that are required for it to match + or raise a ValueError exception if the required object is not present. + """ + + if not len(self.restrictions): + return True # no restrictions to match means everyone should see it + + kwargs = { 'vardb' : vardb, + 'config' : config, + 'profile' : profile } + + for restriction in self.restrictions: + if restriction.checkRestriction( **kwargs ): + return True + + return False # No restrictions were met; thus we aren't relevant :( + + def parse( self ): + lines = open(self.path).readlines() + self.restrictions = [] + for line in lines: + #Optimization to ignore regex matchines on lines that + #will never match + if not line.startswith("D"): + continue + match = _installedRE.match( line ) + if match: + self.restrictions.append( + DisplayInstalledRestriction( match.groups()[0].strip().rstrip() ) ) + continue + match = _profileRE.match( line ) + if match: + self.restrictions.append( + DisplayProfileRestriction( match.groups()[0].strip().rstrip() ) ) + continue + match = _keywordRE.match( line ) + if match: + self.restrictions.append( + DisplayKeywordRestriction( match.groups()[0].strip().rstrip() ) ) + continue + self._parsed = True + + def __getattr__( self, attr ): + if not self._parsed: + self.parse() + return self.__dict__[attr] + +class DisplayRestriction(object): + """ + A base restriction object representing a restriction of display. + news items may have 'relevancy restrictions' preventing them from + being important. In this case we need a manner of figuring out if + a particular item is relevant or not. If any of it's restrictions + are met, then it is displayed + """ + + def checkRestriction( self, **kwargs ): + raise NotImplementedError("Derived class should over-ride this method") + +class DisplayProfileRestriction(DisplayRestriction): + """ + A profile restriction where a particular item shall only be displayed + if the user is running a specific profile. + """ + + def __init__( self, profile ): + self.profile = profile + + def checkRestriction( self, **kwargs ): + if self.profile == kwargs['profile']: + return True + return False + +class DisplayKeywordRestriction(DisplayRestriction): + """ + A keyword restriction where a particular item shall only be displayed + if the user is running a specific keyword. + """ + + def __init__( self, keyword ): + self.keyword = keyword + + def checkRestriction( self, **kwargs ): + if kwargs['config']["ARCH"] == self.keyword: + return True + return False + +class DisplayInstalledRestriction(DisplayRestriction): + """ + An Installation restriction where a particular item shall only be displayed + if the user has that item installed. + """ + + def __init__( self, cpv ): + self.cpv = cpv + + def checkRestriction( self, **kwargs ): + vdb = kwargs['vardb'] + if vdb.match( self.cpv ): + return True + return False