Implement license visibility filtering for GLEP 23, bug #17367, and bug #152593.
authorZac Medico <zmedico@gentoo.org>
Tue, 20 Mar 2007 09:52:15 +0000 (09:52 -0000)
committerZac Medico <zmedico@gentoo.org>
Tue, 20 Mar 2007 09:52:15 +0000 (09:52 -0000)
svn path=/main/trunk/; revision=6251

NEWS
pym/emerge/__init__.py
pym/portage/__init__.py
pym/portage/const.py
pym/portage/dbapi/porttree.py

diff --git a/NEWS b/NEWS
index 08f2669b755bb05061b8a368876ba090106a72b9..c73f3acb5a62fe32e2dec03f745f456dd4441f62 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -9,6 +9,7 @@ portage-2.1.3
 * Add ** as new token for package.keywords to bypass the keyword visibility layer
 * Namespace sanitizing: move all portage related code into portage.* namespace,
   rename portage_foo modules to portage.foo (but keep symlinks for compability)
+* Add license visibility filtering (GLEP 23)
 
 portage-2.1.2
 -------------
index 2c64acada736af5511507d512510297509273b80..5b38f95afbbdde45e2e2772b1bb6ae71dcbeafb3 100644 (file)
@@ -1678,16 +1678,32 @@ class depgraph:
                                                        print "\n!!! "+red("All ebuilds that could satisfy ")+green(xinfo)+red(" have been masked.")
                                                        print "!!! One of the following masked packages is required to complete your request:"
                                                        oldcomment = ""
+                                                       shown_licenses = []
                                                        for p in alleb:
                                                                mreasons = portage.getmaskingstatus(p,
                                                                        settings=pkgsettings, portdb=portdb)
                                                                print "- "+p+" (masked by: "+", ".join(mreasons)+")"
-                                                               comment, filename = portage.getmaskingreason(p,
-                                                                       settings=pkgsettings, portdb=portdb, return_location=True)
-                                                               if comment and comment != oldcomment:
-                                                                       print filename+":"
-                                                                       print comment
-                                                                       oldcomment = comment
+                                                               if "package.mask" in mreasons:
+                                                                       comment, filename = \
+                                                                               portage.getmaskingreason(p,
+                                                                               settings=pkgsettings, portdb=portdb,
+                                                                               return_location=True)
+                                                                       if comment and comment != oldcomment:
+                                                                               print filename+":"
+                                                                               print comment
+                                                                               oldcomment = comment
+                                                               licenses = portdb.aux_get(p, ["LICENSE"])[0]
+                                                               missing_licenses = []
+                                                               for l in pkgsettings.getMissingLicenses(
+                                                                       licenses, p):
+                                                                       l_path = portdb.findLicensePath(l)
+                                                                       if l in shown_licenses:
+                                                                               continue
+                                                                       msg = ("A copy of the '%s' license" + \
+                                                                       " is located at '%s'.") % (l, l_path)
+                                                                       print msg
+                                                                       print
+                                                                       shown_licenses.append(l)
                                                        print
                                                        print "For more information, see MASKED PACKAGES section in the emerge man page or "
                                                        print "refer to the Gentoo Handbook."
index 12980463253f55621486c09e3081756307103713..660e114e0b507fd8989cc55620994eb806991836 100644 (file)
@@ -907,6 +907,9 @@ class config:
                        self.dirVirtuals = copy.deepcopy(clone.dirVirtuals)
                        self.treeVirtuals = copy.deepcopy(clone.treeVirtuals)
                        self.features = copy.deepcopy(clone.features)
+
+                       self._accept_license = copy.deepcopy(clone._accept_license)
+                       self._plicensedict = copy.deepcopy(clone._plicensedict)
                else:
 
                        # backupenv is for calculated incremental variables.
@@ -1208,6 +1211,7 @@ class config:
 
                        self.pusedict = {}
                        self.pkeywordsdict = {}
+                       self._plicensedict = {}
                        self.punmaskdict = {}
                        abs_user_config = os.path.join(config_root,
                                USER_CONFIG_PATH.lstrip(os.path.sep))
@@ -1260,6 +1264,17 @@ class config:
                                        if not self.pkeywordsdict.has_key(cp):
                                                self.pkeywordsdict[cp] = {}
                                        self.pkeywordsdict[cp][key] = pkgdict[key]
+                               
+                               #package.license
+                               licdict = grabdict_package(os.path.join(
+                                       abs_user_config, "package.license"), recursive=1)
+                               for k, v in licdict.iteritems():
+                                       cp = dep_getkey(k)
+                                       cp_dict = self._plicensedict.get(cp)
+                                       if not cp_dict:
+                                               cp_dict = {}
+                                               self._plicensedict[cp] = cp_dict
+                                       cp_dict[k] = self.expandLicenseTokens(v)
 
                                #package.unmask
                                pkgunmasklines = grabfile_package(
@@ -1334,6 +1349,12 @@ class config:
                                else:
                                        self.pprovideddict[mycatpkg]=[x]
 
+                       # parse licensegroups
+                       self._license_groups = {}
+                       for x in locations:
+                               self._license_groups.update(
+                                       grabdict(os.path.join(x, "license_groups")))
+
                        # reasonable defaults; this is important as without USE_ORDER,
                        # USE will always be "" (nothing set)!
                        if "USE_ORDER" not in self:
@@ -1369,6 +1390,18 @@ class config:
                        self["PORTAGE_PYM_PATH"] = PORTAGE_PYM_PATH
                        self.backup_changes("PORTAGE_PYM_PATH")
 
+                       # Expand license groups
+                       # This has to do be done for each config layer before regenerate()
+                       # in order for incremental negation to work properly.
+                       if local_config:
+                               for c in self.configdict.itervalues():
+                                       v = c.get("ACCEPT_LICENSE")
+                                       if not v:
+                                               continue
+                                       v = " ".join(self.expandLicenseTokens(v.split()))
+                                       c["ACCEPT_LICENSE"] = v
+                                       del c, v
+
                        for var in ("PORTAGE_INST_UID", "PORTAGE_INST_GID"):
                                try:
                                        self[var] = str(int(self.get(var, "0")))
@@ -1382,6 +1415,20 @@ class config:
                        self.regenerate()
                        self.features = portage.util.unique_array(self["FEATURES"].split())
 
+                       if local_config:
+                               self._accept_license = \
+                                       set(self.get("ACCEPT_LICENSE", "").split())
+                               # In order to enforce explicit acceptance for restrictive
+                               # licenses that require it, "*" will not be allowed in the
+                               # user config.  Don't enforce this until license groups are
+                               # fully implemented in the tree.
+                               #self._accept_license.discard("*")
+                               if not self._accept_license:
+                                       self._accept_license = set(["*"])
+                       else:
+                               # repoman will accept any license
+                               self._accept_license = set(["*"])
+
                        if "gpg" in self.features:
                                if not os.path.exists(self["PORTAGE_GPG_DIR"]) or \
                                        not os.path.isdir(self["PORTAGE_GPG_DIR"]):
@@ -1437,6 +1484,51 @@ class config:
                                writemsg("!!! %s\n" % str(e),
                                        noiselevel=-1)
 
+       def expandLicenseTokens(self, tokens):
+               """ Take a token from ACCEPT_LICENSE or package.license and expand it
+               if it's a group token (indicated by @) or just return it if it's not a
+               group.  If a group is negated then negate all group elements."""
+               expanded_tokens = []
+               for x in tokens:
+                       expanded_tokens.extend(self._expandLicenseToken(x, None))
+               return expanded_tokens
+
+       def _expandLicenseToken(self, token, traversed_groups):
+               negate = False
+               rValue = []
+               if token.startswith("-"):
+                       negate = True
+                       license_name = token[1:]
+               else:
+                       license_name = token
+               if not license_name.startswith("@"):
+                       rValue.append(token)
+                       return rValue
+               group_name = license_name[1:]
+               if not traversed_groups:
+                       traversed_groups = set()
+               license_group = self._license_groups.get(group_name)
+               if group_name in traversed_groups:
+                       writemsg(("Circular license group reference" + \
+                               " detected in '%s'\n") % group_name, noiselevel=-1)
+                       rValue.append("@"+group_name)
+               elif license_group:
+                       traversed_groups.add(group_name)
+                       for l in license_group:
+                               if l.startswith("-"):
+                                       writemsg(("Skipping invalid element %s" + \
+                                               " in license group '%s'\n") % (l, group_name),
+                                               noiselevel=-1)
+                               else:
+                                       rValue.extend(self._expandLicenseToken(l, traversed_groups))
+               else:
+                       writemsg("Undefined license group '%s'\n" % group_name,
+                               noiselevel=-1)
+                       rValue.append("@"+group_name)
+               if negate:
+                       rvalue = ["-" + token for token in rValue]
+               return rValue
+
        def validate(self):
                """Validate miscellaneous settings and display warnings if necessary.
                (This code was previously in the global scope of portage.py)"""
@@ -1656,6 +1748,49 @@ class config:
                if has_changed:
                        self.reset(keeping_pkg=1,use_cache=use_cache)
 
+       def getMissingLicenses(self, licenses, cpv):
+               cpdict = self._plicensedict.get(dep_getkey(cpv), None)
+               acceptable_licenses = self._accept_license.copy()
+               if cpdict:
+                       for atom in match_to_list(cpv, cpdict.keys()):
+                               acceptable_licenses.update(cpdict[atom])
+               if "*" in acceptable_licenses:
+                       return []
+               if "?" in licenses:
+                       self.setcpv(cpv)
+               license_struct = portage.dep.paren_reduce(licenses)
+               license_struct = portage.dep.use_reduce(
+                       license_struct, uselist=self["USE"].split())
+               license_struct = portage.dep.dep_opconvert(license_struct)
+               return self._getMissingLicenses(license_struct, acceptable_licenses)
+
+       def _getMissingLicenses(self, license_struct, acceptable_licenses):
+               if not license_struct:
+                       return []
+               if license_struct[0] == "||":
+                       ret = []
+                       for element in license_struct[1:]:
+                               if isinstance(element, list):
+                                       if element:
+                                               ret.append(self._getMissingLicenses(element))
+                               else:
+                                       if element in acceptable_licenses:
+                                               return []
+                                       ret.append(element)
+                       # Return all masked licenses, since we don't know which combination
+                       # (if any) the user will decide to unmask.
+                       return flatten(ret)
+
+               ret = []
+               for element in license_struct:
+                       if isinstance(element, list):
+                               if element:
+                                       ret.extend(self._getMissingLicenses(element))
+                       else:
+                               if element not in acceptable_licenses:
+                                       ret.append(element)
+               return ret
+
        def setinst(self,mycpv,mydbapi):
                self.modifying()
                if len(self.virtuals) == 0:
@@ -4479,7 +4614,7 @@ def getmaskingreason(mycpv, settings=None, portdb=None, return_location=False):
 
 def getmaskingstatus(mycpv, settings=None, portdb=None):
        if settings is None:
-               settings = globals()["settings"]
+               settings = config(clone=globals()["settings"])
        if portdb is None:
                portdb = globals()["portdb"]
        mysplit = catpkgsplit(mycpv)
@@ -4508,7 +4643,8 @@ def getmaskingstatus(mycpv, settings=None, portdb=None):
 
        # keywords checking
        try:
-               mygroups, eapi = portdb.aux_get(mycpv, ["KEYWORDS", "EAPI"])
+               mygroups, licenses, eapi = portdb.aux_get(
+                       mycpv, ["KEYWORDS", "LICENSE", "EAPI"])
        except KeyError:
                # The "depend" phase apparently failed for some reason.  An associated
                # error message will have already been printed to stderr.
@@ -4563,6 +4699,21 @@ def getmaskingstatus(mycpv, settings=None, portdb=None):
 
        if kmask:
                rValue.append(kmask+" keyword")
+
+       try:
+               missing_licenses = settings.getMissingLicenses(licenses, mycpv)
+               if missing_licenses:
+                       allowed_tokens = set(["||", "(", ")"])
+                       allowed_tokens.update(missing_licenses)
+                       license_split = licenses.split()
+                       license_split = [x for x in license_split \
+                               if x in allowed_tokens]
+                       msg = license_split[:]
+                       msg.append("license(s)")
+                       rValue.append(" ".join(msg))
+       except portage.exception.InvalidDependString, e:
+               rValue.append("LICENSE: "+str(e))
+
        return rValue
 
 
index fa1187fa7303cffb599d68da1620b0a1ebdc9187..28e6f36ff32685bad90a66d552d68d7c0a53a50e 100644 (file)
@@ -48,7 +48,10 @@ 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"]
+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"]
 
index 08e6961492adf2463e7246ec31ec62cce423fd42..4118a4da4cffd26bb063c5a1534815e2462d6c7f 100644 (file)
@@ -6,7 +6,7 @@ from portage.dep import use_reduce, paren_reduce, dep_getslot, dep_getkey, \
        match_from_list, match_to_list
 from portage.exception import OperationNotPermitted, PortageException, \
        UntrustedSignature, SecurityViolation, InvalidSignature, MissingSignature, \
-       FileNotFound
+       FileNotFound, InvalidDependString
 from portage.manifest import Manifest
 from portage.output import red
 from portage.util import ensure_dirs, writemsg, apply_recursive_permissions
@@ -109,7 +109,7 @@ class portdbapi(dbapi):
                                self.auxdb[x] = self.auxdbmodule(
                                        self.depcachedir, x, filtered_auxdbkeys, gid=portage_gid)
                # Selectively cache metadata in order to optimize dep matching.
-               self._aux_cache_keys = set(["EAPI", "KEYWORDS", "SLOT"])
+               self._aux_cache_keys = set(["EAPI", "KEYWORDS", "LICENSE", "SLOT"])
                self._aux_cache = {}
 
        def _init_cache_dirs(self):
@@ -156,6 +156,15 @@ class portdbapi(dbapi):
                        return ""
                return mydig+"/files/digest-"+mysplit[-1]
 
+       def findLicensePath(self, license_name):
+               mytrees = self.porttrees[:]
+               mytrees.reverse()
+               for x in mytrees:
+                       license_path = os.path.join(x, "licenses", license_name)
+                       if os.access(license_path, os.R_OK):
+                               return license_path
+               return None
+
        def findname(self,mycpv):
                return self.findname2(mycpv)[0]
 
@@ -624,13 +633,14 @@ class portdbapi(dbapi):
 
                accept_keywords = self.mysettings["ACCEPT_KEYWORDS"].split()
                pkgdict = self.mysettings.pkeywordsdict
+               aux_keys = ["KEYWORDS", "LICENSE", "EAPI"]
                for mycpv in mylist:
                        try:
-                               keys, eapi = self.aux_get(mycpv, ["KEYWORDS", "EAPI"])
+                               keys, licenses, eapi = self.aux_get(mycpv, aux_keys)
                        except KeyError:
                                continue
                        except PortageException, e:
-                               writemsg("!!! Error: aux_get('%s', ['KEYWORDS', 'EAPI'])\n" % \
+                               writemsg("!!! Error: aux_get('%s', %s)\n" % (mycpv, aux_keys),
                                        mycpv, noiselevel=-1)
                                writemsg("!!! %s\n" % str(e), noiselevel=-1)
                                del e
@@ -675,6 +685,11 @@ class portdbapi(dbapi):
                                        hasstable = True
                        if not match and ((hastesting and "~*" in pgroups) or (hasstable and "*" in pgroups) or "**" in pgroups):
                                match=1
+                       try:
+                               if self.mysettings.getMissingLicenses(licenses, mycpv):
+                                       match = 0
+                       except InvalidDependString:
+                               match = 0
                        if match and eapi_is_supported(eapi):
                                newlist.append(mycpv)
                return newlist