Merged from trunk 5657:5659
authorFabian Groffen <grobian@gentoo.org>
Fri, 19 Jan 2007 19:07:50 +0000 (19:07 -0000)
committerFabian Groffen <grobian@gentoo.org>
Fri, 19 Jan 2007 19:07:50 +0000 (19:07 -0000)
merge glep42 into trunk (WarnerBro)

merge didn't bring in portage_news, no idea why (WarnerBro)

svn path=/main/branches/prefix/; revision=5725

bin/emerge
bin/portageq
pym/portage.py
pym/portage_const.py
pym/portage_news.py [new file with mode: 0644]

index d38bd337c5fbe1d52e3716c15ddc3eab0e96a23b..46783befd2c7591704d392ed385500901b554fab 100755 (executable)
@@ -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)
index 3c863aa37f29e05c52fc8d55b931a4721a2561d9..51c50442b46c51443d3b9134fd8abad3b20e4987 100755 (executable)
@@ -260,6 +260,17 @@ def envvar(argv):
                else:
                        print portage.settings[arg]
 
+def get_repos(argv):
+       """<root>
+       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):
+       """<root> <repo_id>
+       Returns the path to the repo named argv[1], argv[0] = $ROOT
+       """
+       print portage.db[argv[0]]["porttree"].dbapi.getRepositoryPath(argv[1])
 
 #-----------------------------------------------------------------------------
 #
index 9ea00896ec989422a6fd32e8d58765adda52fe51..94bce04ffad4cd43258abd69ea8ad69dea6d2efd 100644 (file)
@@ -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.
index 51a19f9a2223b2a247596798a624d47ff4bfc3ad..d5cda01119cd18b4022839aafa6a353d06abb3a5 100644 (file)
@@ -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 (file)
index 0000000..f02b793
--- /dev/null
@@ -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