17824 api install and update should allow specifying "latest" version of packages
authorShawn Walker <shawn.walker@oracle.com>
Thu, 10 Feb 2011 14:06:18 -0800
changeset 2224 dba01788128c
parent 2223 2da8c49e53dc
child 2225 d990c9b225c6
17824 api install and update should allow specifying "latest" version of packages 17827 MatchingDotSequence plays in the wrong pool 17830 use of wildcards in versions forces glob matching of package name 17842 api install, update, list, and info should allow "rooted" package names
src/man/pkg.1.txt
src/modules/client/api.py
src/modules/client/api_errors.py
src/modules/client/imageplan.py
src/modules/fmri.py
src/modules/version.py
src/tests/api/t_api_list.py
src/tests/api/t_fmri.py
src/tests/api/t_pkg_api_install.py
src/tests/cli/t_pkg_image_update.py
src/tests/cli/t_pkg_info.py
src/tests/cli/t_pkg_install.py
src/tests/cli/t_pkg_list.py
src/tests/cli/t_pkgrecv.py
src/tests/cli/t_variants.py
--- a/src/man/pkg.1.txt	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/man/pkg.1.txt	Thu Feb 10 14:06:18 2011 -0800
@@ -158,7 +158,8 @@
 
           Installs and updates packages to the newest version that match
           pkg_fmri_pattern allowed by the packages installed in the
-          image.
+          image.  To explicitly request the latest version of a package,
+          use 'latest' for the pattern version (e.g. 'vim@latest').
 
           Some configuration files may be renamed or replaced during the
           install process.  For more information on how the package system
@@ -233,7 +234,9 @@
           With no arguments, or if '*' is one of the patterns provided,
           update all installed packages in the current image to the newest
           version allowed by the constraints imposed on the system by
-          installed packages and publisher configuration.
+          installed packages and publisher configuration.  To explicitly
+          request the latest version of a package, use 'latest' for the
+          pattern version (e.g. 'vim@latest').
 
           If pkg_fmri_pattern is provided, update will replace packages
           that are installed, and that match pkg_fmri_pattern, with the
@@ -314,7 +317,9 @@
 
           With -f and -a, list all versions of all packages for all
           variants regardless of incorporation constraints or installed
-          state.
+          state.  To explicitly list the latest version of a package when
+          using these options, use 'latest' for the pattern version (e.g.
+          'vim@latest').
 
           With -g, use the specified package repository or archive as the
           source of package data for the operation.  This option may be
--- a/src/modules/client/api.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/modules/client/api.py	Thu Feb 10 14:06:18 2011 -0800
@@ -1931,42 +1931,21 @@
                 illegals = []
                 pat_tuples = {}
                 pat_versioned = False
-                for pat in patterns:
-                        try:
-                                if "@" in pat:
-                                        # Mark that a pattern containing
-                                        # version information was found.
-                                        pat_versioned = True
-
-                                if "*" in pat or "?" in pat:
-                                        matcher = self.MATCH_GLOB
-
-                                        # XXX By default, matching FMRIs
-                                        # currently do not also use
-                                        # MatchingVersion.  If that changes,
-                                        # this should change too.
-                                        parts = pat.split("@", 1)
-                                        if len(parts) == 1:
-                                                npat = pkg.fmri.MatchingPkgFmri(
-                                                    pat, brelease)
-                                        else:
-                                                npat = pkg.fmri.MatchingPkgFmri(
-                                                    parts[0], brelease)
-                                                npat.version = \
-                                                    pkg.version.MatchingVersion(
-                                                    str(parts[1]), brelease)
-                                elif pat.startswith("pkg:/"):
-                                        matcher = self.MATCH_EXACT
-                                        npat = pkg.fmri.PkgFmri(pat,
-                                            brelease)
-                                else:
-                                        matcher = self.MATCH_FMRI
-                                        npat = pkg.fmri.PkgFmri(pat,
-                                            brelease)
-                                pat_tuples[pat] = (npat.tuple(), matcher)
-                        except (pkg.fmri.FmriError,
-                            pkg.version.VersionError), e:
-                                illegals.append(e)
+                latest_pats = set()
+                for pat, error, pfmri, matcher in self.parse_fmri_patterns(
+                    patterns):
+                        if error:
+                                illegals.append(error)
+                                continue
+
+                        if "@" in pat:
+                                # Mark that a pattern contained version
+                                # information.  This is used for a listing
+                                # optimization later on.
+                                pat_versioned = True
+                        if getattr(pfmri.version, "match_latest", None):
+                                latest_pats.add(pat)
+                        pat_tuples[pat] = (pfmri.tuple(), matcher)
 
                 if illegals:
                         raise apx.InventoryException(illegal=illegals)
@@ -2174,6 +2153,16 @@
                                                         omit_ver = True
                                                         continue
 
+                                        if pat in latest_pats and \
+                                            nlist[pkg_stem] > 1:
+                                                # Package allowed by pattern,
+                                                # but isn't the "latest"
+                                                # version.
+                                                if omit_package is None:
+                                                        omit_package = True
+                                                omit_ver = True
+                                                continue
+
                                         # If this entry matched at least one
                                         # pattern, then ensure it is returned.
                                         omit_package = False
@@ -3476,28 +3465,39 @@
                         matcher = None
                         npat = None
                         try:
-                                if "*" in pat or "?" in pat:
-                                        # XXX By default, matching FMRIs
-                                        # currently do not also use
-                                        # MatchingVersion.  If that changes,
-                                        # this should  change too.
-                                        parts = pat.split("@", 1)
-                                        if len(parts) == 1:
-                                                npat = fmri.MatchingPkgFmri(pat,
-                                                    brelease)
-                                        else:
-                                                npat = fmri.MatchingPkgFmri(
-                                                    parts[0], brelease)
-                                                npat.version = \
-                                                    pkg.version.MatchingVersion(
-                                                    str(parts[1]), brelease)
+                                parts = pat.split("@", 1)
+                                pat_stem = parts[0]
+                                pat_ver = None
+                                if len(parts) > 1:
+                                        pat_ver = parts[1]
+
+                                if "*" in pat_stem or "?" in pat_stem:
                                         matcher = self.MATCH_GLOB
-                                elif pat.startswith("pkg:/"):
-                                        npat = fmri.PkgFmri(pat, brelease)
+                                elif pat_stem.startswith("pkg:/") or \
+                                    pat_stem.startswith("/"):
                                         matcher = self.MATCH_EXACT
                                 else:
-                                        npat = pkg.fmri.PkgFmri(pat, brelease)
                                         matcher = self.MATCH_FMRI
+
+                                if matcher == self.MATCH_GLOB:
+                                        npat = fmri.MatchingPkgFmri(pat_stem,
+                                            brelease)
+                                else:
+                                        npat = fmri.PkgFmri(pat_stem, brelease)
+
+                                if not pat_ver:
+                                        # Do nothing.
+                                        pass
+                                elif "*" in pat_ver or "?" in pat_ver or \
+                                    pat_ver == "latest":
+                                        npat.version = \
+                                            pkg.version.MatchingVersion(pat_ver,
+                                                brelease)
+                                else:
+                                        npat.version = \
+                                            pkg.version.Version(pat_ver,
+                                                brelease)
+
                         except (fmri.FmriError, pkg.version.VersionError), e:
                                 # Whatever the error was, return it.
                                 error = e
--- a/src/modules/client/api_errors.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/modules/client/api_errors.py	Thu Feb 10 14:06:18 2011 -0800
@@ -419,7 +419,7 @@
                         res += ["\t%s" % p for p in self.installed]
 
                 if self.multispec:
-                        s = _("The following different patterns specify the"
+                        s = _("The following different patterns specify the "
                               "same package(s):")
                         res += [s]
                         for t in self.multispec:
--- a/src/modules/client/imageplan.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/modules/client/imageplan.py	Thu Feb 10 14:06:18 2011 -0800
@@ -2061,7 +2061,7 @@
                 pubs     = []
                 versions = []
 
-                wildcard_patterns = []
+                wildcard_patterns = set()
 
                 renamed_fmris = defaultdict(set)
                 obsolete_fmris = []
@@ -2071,29 +2071,56 @@
                 # print patterns, match_type, pub_ranks, installed_pubs
 
                 # figure out which kind of matching rules to employ
-                try:
-                        for pat in patterns:
-                                if "*" in pat or "?" in pat:
+                brelease = self.image.attrs["Build-Release"]
+                latest_pats = set()
+                for pat in patterns:
+                        try:
+                                parts = pat.split("@", 1)
+                                pat_stem = parts[0]
+                                pat_ver = None
+                                if len(parts) > 1:
+                                        pat_ver = parts[1]
+
+                                if "*" in pat_stem or "?" in pat_stem:
                                         matcher = pkg.fmri.glob_match
-                                        fmri = pkg.fmri.MatchingPkgFmri(
-                                                                pat, "5.11")
-                                        wildcard_patterns.append(pat)
-                                elif pat.startswith("pkg:/"):
+                                        wildcard_patterns.add(pat)
+                                elif pat_stem.startswith("pkg:/") or \
+                                    pat_stem.startswith("/"):
                                         matcher = pkg.fmri.exact_name_match
-                                        fmri = pkg.fmri.PkgFmri(pat,
-                                                            "5.11")
                                 else:
                                         matcher = pkg.fmri.fmri_match
-                                        fmri = pkg.fmri.PkgFmri(pat,
-                                                            "5.11")
+
+                                if matcher == pkg.fmri.glob_match:
+                                        fmri = pkg.fmri.MatchingPkgFmri(
+                                            pat_stem, brelease)
+                                else:
+                                        fmri = pkg.fmri.PkgFmri(
+                                            pat_stem, brelease)
+
+                                if not pat_ver:
+                                        # Do nothing.
+                                        pass
+                                elif "*" in pat_ver or "?" in pat_ver or \
+                                    pat_ver == "latest":
+                                        fmri.version = \
+                                            pkg.version.MatchingVersion(pat_ver,
+                                                brelease)
+                                else:
+                                        fmri.version = \
+                                            pkg.version.Version(pat_ver,
+                                                brelease)
+
+                                if pat_ver and \
+                                    getattr(fmri.version, "match_latest", None):
+                                        latest_pats.add(pat)
 
                                 matchers.append(matcher)
-                                pubs.append(fmri.get_publisher())
+                                pubs.append(fmri.publisher)
                                 versions.append(fmri.version)
                                 fmris.append(fmri)
-
-                except pkg.fmri.IllegalFmri, e:
-                        illegals.append(e)
+                        except (pkg.fmri.FmriError,
+                            pkg.version.VersionError), e:
+                                illegals.append(e)
 
                 # Create a dictionary of patterns, with each value being a
                 # dictionary of pkg names & fmris that match that pattern.
@@ -2243,6 +2270,39 @@
                                         if pkg_name in targets:
                                                 del ret[p][pkg_name]
 
+                # Discard all but the newest version of each match.
+                if latest_pats:
+                        # Rebuild ret based on latest version of every package.
+                        latest = {}
+                        nret = {}
+                        for p in patterns:
+                                if p not in latest_pats or not ret[p]:
+                                        nret[p] = ret[p]
+                                        continue
+
+                                nret[p] = {}
+                                for pkg_name in ret[p]:
+                                        nret[p].setdefault(pkg_name, [])
+                                        for f in ret[p][pkg_name]:
+                                                nver = latest.get(f.pkg_name,
+                                                    None)
+                                                latest[f.pkg_name] = max(nver,
+                                                    f.version)
+                                                if f.version == latest[f.pkg_name]:
+                                                        # Allow for multiple
+                                                        # FMRIs of the same
+                                                        # latest version.
+                                                        nret[p][pkg_name] = [
+                                                            e for e in nret[p][pkg_name]
+                                                            if e.version == f.version
+                                                        ]
+                                                        nret[p][pkg_name].append(f)
+
+                        # Assign new version of ret and discard latest list.
+                        ret = nret
+                        del latest
+
+                # Determine match failures.
                 matchdict = {}
                 for p in patterns:
                         l = len(ret[p])
--- a/src/modules/fmri.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/modules/fmri.py	Thu Feb 10 14:06:18 2011 -0800
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
 #
 
 import fnmatch
@@ -114,11 +114,10 @@
 
         __slots__ = ["version", "publisher", "pkg_name", "_hash"]
 
-        def __init__(self, fmri, build_release = None, publisher = None):
-                """XXX pkg:/?pkg_name@version not presently supported."""
+        def __init__(self, fmri, build_release=None, publisher=None):
                 fmri = fmri.rstrip()
 
-                veridx, nameidx = PkgFmri.gen_fmri_indexes(fmri)
+                veridx, nameidx, pubidx = PkgFmri._gen_fmri_indexes(fmri)
 
                 if veridx != None:
                         try:
@@ -131,9 +130,10 @@
                 else:
                         self.version = veridx = None
 
-                self.publisher = publisher
-                if fmri.startswith("pkg://"):
-                        self.publisher = fmri[6:nameidx - 1]
+                if pubidx != None:
+                        self.publisher = fmri[pubidx:nameidx - 1]
+                else:
+                        self.publisher = publisher
 
                 if veridx != None:
                         self.pkg_name = fmri[nameidx:veridx]
@@ -154,7 +154,7 @@
                 return PkgFmri(str(self))
 
         @staticmethod
-        def gen_fmri_indexes(fmri):
+        def _gen_fmri_indexes(fmri):
                 """Return a tuple of offsets, used to extract different
                 components of the FMRI."""
 
@@ -162,20 +162,44 @@
                 if veridx == -1:
                         veridx = None
 
+                pubidx = None
                 if fmri.startswith("pkg://"):
-                        nameidx = fmri.find("/", 6)
+                        nameidx = fmri.find("/", 6, veridx)
                         if nameidx == -1:
                                 raise IllegalFmri(fmri,
                                     IllegalFmri.SYNTAX_ERROR,
-                                    detail="Missing '/' after publisher name")
+                                    detail=_("Missing '/' after "
+                                        "publisher name"))
+
+                        # Publisher starts after //.
+                        pubidx = 6
+
                         # Name starts after / which terminates publisher
                         nameidx += 1
+
                 elif fmri.startswith("pkg:/"):
+                        # Name starts after / which terminates scheme
                         nameidx = 5
+                elif fmri.startswith("//"):
+                        nameidx = fmri.find("/", 2, veridx)
+                        if nameidx == -1:
+                                raise IllegalFmri(fmri,
+                                    IllegalFmri.SYNTAX_ERROR,
+                                    detail=_("Missing '/' after "
+                                        "publisher name"))
+
+                        # Publisher starts after //.
+                        pubidx = 2
+
+                        # Name starts after / which terminates publisher
+                        nameidx += 1
+                elif fmri.startswith("/"):
+                        # Name starts after / which terminates scheme
+                        nameidx = 1
                 else:
                         nameidx = 0
 
-                return (veridx, nameidx)
+                return (veridx, nameidx, pubidx)
 
         def get_publisher(self):
                 """Return the name of the publisher that is contained
@@ -475,7 +499,7 @@
         substring that is the FMRI's pkg_name."""
         fmri = fmri.rstrip()
 
-        veridx, nameidx = PkgFmri.gen_fmri_indexes(fmri)
+        veridx, nameidx, pubidx = PkgFmri._gen_fmri_indexes(fmri)
 
         if veridx:
                 pkg_name = fmri[nameidx:veridx]
--- a/src/modules/version.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/modules/version.py	Thu Feb 10 14:06:18 2011 -0800
@@ -131,6 +131,13 @@
         """A subclass of DotSequence with (much) weaker rules about its format.
         This is intended to accept user input with wildcard characters."""
 
+        #
+        # We employ the Flyweight design pattern for dotsequences, since they
+        # are used immutably, are highly repetitive (0.5.11 over and over) and,
+        # for what they contain, are relatively expensive memory-wise.
+        #
+        __matching_dotseq_pool = weakref.WeakValueDictionary()
+
         @staticmethod
         def dotsequence_val(elem):
                 # Do this first; if the string is zero chars or non-numeric
@@ -140,6 +147,16 @@
 
                 return DotSequence.dotsequence_val(elem)
 
+        def __new__(cls, dotstring):
+                ds = MatchingDotSequence.__matching_dotseq_pool.get(dotstring,
+                    None)
+                if ds is not None:
+                        return ds
+
+                ds = list.__new__(cls)
+                cls.__matching_dotseq_pool[dotstring] = ds
+                return ds
+
         def __init__(self, dotstring):
                 try:
                         list.__init__(self,
@@ -486,20 +503,20 @@
                                 if not other.release.is_subsequence(
                                     self.release):
                                         return False
-                        elif other.release and other.release != "*":
+                        elif other.release and str(other.release) != "*":
                                 return False
 
                         if other.branch and self.branch:
                                 if not other.branch.is_subsequence(self.branch):
                                         return False
-                        elif other.branch and other.branch != "*":
+                        elif other.branch and str(other.branch) != "*":
                                 return False
 
                         if self.timestr and other.timestr:
                                 if not (other.timestr == self.timestr or \
                                     other.timestr == "*"):
                                         return False
-                        elif other.timestr and other.timestr != "*":
+                        elif other.timestr and str(other.timestr) != "*":
                                 return False
 
                         return True
@@ -592,12 +609,20 @@
         """An alternative for Version with (much) weaker rules about its format.
         This is intended to accept user input with globbing characters."""
 
+
+        __slots__ = ["match_latest", "__original"]
+
         def __init__(self, version_string, build_string):
-                # XXX If illegally formatted, raise exception.
-
                 if version_string is None or not len(version_string):
                         raise IllegalVersion, "Version cannot be empty"
 
+                if version_string == "latest":
+                        # Treat special "latest" syntax as equivalent to '*' for
+                        # version comparison purposes.
+                        self.match_latest = True
+                        version_string = "*"
+                else:
+                        self.match_latest = False
 
                 (release, build_release, branch, timestr), ignored = \
                     self.split(version_string)
@@ -641,6 +666,8 @@
                 self.__original = outstr
 
         def __str__(self):
+                if self.match_latest:
+                        return "latest"
                 return self.__original
 
         def get_timestamp(self):
@@ -682,21 +709,22 @@
                 if not isinstance(other, Version):
                         return False
 
-                if self.release == "*" and other.release != "*":
+                if str(self.release) == "*" and str(other.release) != "*":
                         return True
                 if self.release < other.release:
                         return True
                 if self.release != other.release:
                         return False
 
-                if self.build_release == "*" and other.build_release != "*":
+                if str(self.build_release) == "*" and \
+                    str(other.build_release) != "*":
                         return True
                 if self.build_release < other.build_release:
                         return True
                 if self.build_release != other.build_release:
                         return False
 
-                if self.branch == "*" and other.branch != "*":
+                if str(self.branch) == "*" and str(other.branch) != "*":
                         return True
                 if self.branch < other.branch:
                         return True
@@ -718,21 +746,22 @@
                 if not isinstance(other, Version):
                         return True
 
-                if self.release == "*" and other.release != "*":
+                if str(self.release) == "*" and str(other.release) != "*":
                         return False
                 if self.release > other.release:
                         return True
                 if self.release != other.release:
                         return False
 
-                if self.build_release == "*" and other.build_release != "*":
+                if str(self.build_release) == "*" and \
+                    str(other.build_release) != "*":
                         return False
                 if self.build_release > other.build_release:
                         return True
                 if self.build_release != other.build_release:
                         return False
 
-                if self.branch == "*" and other.branch != "*":
+                if str(self.branch) == "*" and str(other.branch) != "*":
                         return False
                 if self.branch > other.branch:
                         return True
--- a/src/tests/api/t_api_list.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/api/t_api_list.py	Thu Feb 10 14:06:18 2011 -0800
@@ -415,6 +415,7 @@
                 # Compare returned and expected.
                 self.assertPrettyEqual(returned, expected)
 
+                self.debug(pprint.pformat(returned))
                 if num_expected is not None:
                         self.assertEqual(len(returned), num_expected)
 
--- a/src/tests/api/t_fmri.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/api/t_fmri.py	Thu Feb 10 14:06:18 2011 -0800
@@ -21,8 +21,7 @@
 #
 
 #
-# Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
+# Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
 #
 
 import testutils
@@ -42,7 +41,7 @@
             "never": " `~!@#$%^&*()=[{]}\\|;:\",<>?",
             "always": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                 "0123456789",
-            "after-first": "_/-.+",
+            "after-first": "_-.+",
         }
 
         def setUp(self):
@@ -225,14 +224,21 @@
                                     (char, char2)
                                 fmri.PkgFmri(valid_name)
 
+                # Test '/' == 'pkg:/'; '//' == 'pkg://'.
+                for vn in ("/[email protected],5.11-0", "//publisher/[email protected],5.11-0"):
+                        pfmri = fmri.PkgFmri(vn)
+                        self.assertEqual(pfmri.pkg_name, "test")
+                        if vn.startswith("//"):
+                                self.assertEqual(pfmri.publisher, "publisher")
+                        else:
+                                self.assertEqual(pfmri.publisher, None)
+
         def testbadfmri_pkgname(self):
                 self.assertRaises(fmri.IllegalFmri, fmri.PkgFmri,
                     "application//[email protected],5.11-0.96")
                 self.assertRaises(fmri.IllegalFmri, fmri.PkgFmri,
                     "application/office/@0.5.11,5.11-0.96")
                 self.assertRaises(fmri.IllegalFmri, fmri.PkgFmri,
-                    "/[email protected],5.11-0.96")
-                self.assertRaises(fmri.IllegalFmri, fmri.PkgFmri,
                     "app/[email protected],5.11-0.96")
 
         def testgoodfmris_dots(self):
--- a/src/tests/api/t_pkg_api_install.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/api/t_pkg_api_install.py	Thu Feb 10 14:06:18 2011 -0800
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
 #
 
 import testutils
@@ -794,7 +794,7 @@
 
                 api_obj.reset()
                 pkg5unittest.eval_assert_raises(api_errors.PlanCreationException,
-                    check_illegal, api_obj.plan_uninstall, ["/foo"], False)
+                    check_illegal, api_obj.plan_uninstall, ["_foo"], False)
 
                 self.pkgsend_bulk(self.rurl, self.foo10)
 
@@ -804,7 +804,7 @@
 
                 api_obj.reset()
                 pkg5unittest.eval_assert_raises(api_errors.PlanCreationException,
-                    check_illegal, api_obj.plan_uninstall, ["/foo"], False)
+                    check_illegal, api_obj.plan_uninstall, ["_foo"], False)
 
                 api_obj.reset()
                 pkg5unittest.eval_assert_raises(api_errors.PlanCreationException,
@@ -834,7 +834,7 @@
 
                 api_obj.reset()
                 pkg5unittest.eval_assert_raises(api_errors.PlanCreationException,
-                    check_illegal, api_obj.plan_install, ["/foo"])
+                    check_illegal, api_obj.plan_install, ["_foo"])
 
         def test_catalog_v0(self):
                 """Test install from a publisher's repository that only supports
--- a/src/tests/cli/t_pkg_image_update.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/cli/t_pkg_image_update.py	Thu Feb 10 14:06:18 2011 -0800
@@ -20,7 +20,7 @@
 # CDDL HEADER END
 #
 
-# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
 
 import testutils
 if __name__ == "__main__":
@@ -253,6 +253,14 @@
                 self.pkg("update '*@*'")
                 self.pkg("info [email protected] [email protected] [email protected]")
 
+                # Now rollback everything to 1.0, and then verify that
+                # '@latest' will take everything to the latest version.
+                self.pkg("update '*@1.0'")
+                self.pkg("info [email protected] [email protected] [email protected]")
+
+                self.pkg("update '*@latest'")
+                self.pkg("info [email protected] [email protected] [email protected]")
+
 
 if __name__ == "__main__":
         unittest.main()
--- a/src/tests/cli/t_pkg_info.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/cli/t_pkg_info.py	Thu Feb 10 14:06:18 2011 -0800
@@ -20,7 +20,7 @@
 # CDDL HEADER END
 #
 
-# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
 
 import testutils
 if __name__ == "__main__":
@@ -94,8 +94,8 @@
                 self.pkg("info pkg:/man@", exit=1)
 
                 # Bug 4878
-                self.pkg("info -r /usr/bin/stunnel", exit=1)
-                self.pkg("info /usr/bin/stunnel", exit=1)
+                self.pkg("info -r _usr/bin/stunnel", exit=1)
+                self.pkg("info _usr/bin/stunnel", exit=1)
 
 		# bad version
                 self.pkg("install jade")
--- a/src/tests/cli/t_pkg_install.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/cli/t_pkg_install.py	Thu Feb 10 14:06:18 2011 -0800
@@ -357,13 +357,6 @@
                 self.dc.stop()
 
         def test_basics_5(self):
-                """ Add [email protected], install [email protected]. """
-
-                self.pkgsend_bulk(self.rurl, self.xbar11)
-                self.image_create(self.rurl)
-                self.pkg("install [email protected]", exit=1)
-
-        def test_basics_6(self):
                 """ Install [email protected], upgrade to [email protected].
                 Boring should be left alone, while
                 foo gets upgraded as needed"""
@@ -380,6 +373,25 @@
                 self.pkg("list")
                 self.pkg("list [email protected] [email protected] [email protected]")
 
+        def test_basics_6(self):
+                """Verify that '@latest' will install the latest version
+                of a package."""
+
+                self.pkgsend_bulk(self.rurl, (self.bar10, self.bar11,
+                    self.foo10, self.foo11, self.foo12, self.boring10,
+                    self.boring11))
+                self.image_create(self.rurl)
+
+                self.pkg("install '*@latest'")
+                self.pkg("info [email protected] [email protected] [email protected]")
+
+        def test_basics_7(self):
+                """ Add [email protected], install [email protected]. """
+
+                self.pkgsend_bulk(self.rurl, self.xbar11)
+                self.image_create(self.rurl)
+                self.pkg("install [email protected]", exit=1)
+
         def test_basics_mdns(self):
                 """ Send empty package [email protected], install and uninstall """
 
@@ -1548,6 +1560,10 @@
                 # demonstrate that [email protected] prevents package movement
                 self.pkg("install [email protected] [email protected]", exit=1)
 
+                # ...again, but using @latest.
+                self.pkg("install bronze@latest amber@latest", exit=1)
+                self.pkg("update bronze@latest amber@latest", exit=1)
+
                 # Now update to get new versions of amber and bronze
                 self.pkg("update")
 
@@ -2258,7 +2274,7 @@
         pkg_name_valid_chars = {
             "never": " `~!@#$%^&*()=[{]}\\|;:\",<>?",
             "always": "09azAZ",
-            "after-first": "_/-.+",
+            "after-first": "_-.+",
             "at-end": "09azAZ_-.+",
         }
 
--- a/src/tests/cli/t_pkg_list.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/cli/t_pkg_list.py	Thu Feb 10 14:06:18 2011 -0800
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
 #
 
 import testutils
@@ -67,13 +67,18 @@
             open [email protected]
             close """
 
+        hierfoo10 = """
+            open hier/[email protected],5.11-0
+            close """
+
         def setUp(self):
                 pkg5unittest.ManyDepotTestCase.setUp(self, ["test1", "test2",
                     "test2"])
 
                 self.rurl1 = self.dcs[1].get_repo_url()
                 self.pkgsend_bulk(self.rurl1, (self.foo1, self.foo10,
-                    self.foo11, self.foo12, self.foo121, self.food12))
+                    self.foo11, self.foo12, self.foo121, self.food12,
+                    self.hierfoo10))
 
                 # Ensure that the second repo's packages have exactly the same
                 # timestamps as those in the first ... by copying the repo over.
@@ -110,7 +115,9 @@
                     ("foo 1.2.1-0 known -----\n"
                     "foo (test2) 1.2.1-0 known -----\n"
                     "food 1.2-0 known -----\n"
-                    "food (test2) 1.2-0 known -----\n")
+                    "food (test2) 1.2-0 known -----\n"
+                    "hier/foo 1.0-0 known -----\n"
+                    "hier/foo (test2) 1.0-0 known -----\n")
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
 
@@ -127,7 +134,9 @@
                     "foo (test2) 1.0-0 known u----\n"
                     "foo (test2) 1-0 known u----\n"
                     "food 1.2-0 known -----\n"
-                    "food (test2) 1.2-0 known -----\n")
+                    "food (test2) 1.2-0 known -----\n"
+                    "hier/foo 1.0-0 known -----\n"
+                    "hier/foo (test2) 1.0-0 known -----\n")
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
 
@@ -144,9 +153,14 @@
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
 
+                # Test 'rooted' name.
+                self.pkg("list -afH //test1/[email protected],5.11-0")
+                output = self.reduceSpaces(self.output)
+                self.assertEqualDiff(expected, output)
+
         def test_02(self):
                 """List all "[email protected]", regardless of publisher, with "pkg:/"
-                prefix."""
+                or '/' prefix."""
                 self.pkg("list -afH pkg:/[email protected],5.11-0")
                 expected = \
                     "foo 1.0-0 known u----\n" \
@@ -154,6 +168,11 @@
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
 
+                # Test 'rooted' name.
+                self.pkg("list -afH /[email protected],5.11-0")
+                output = self.reduceSpaces(self.output)
+                self.assertEqualDiff(expected, output)
+
         def test_03(self):
                 """List all "[email protected]", regardless of publisher, without "pkg:/"
                 prefix."""
@@ -169,45 +188,52 @@
                 """List all versions of package foo, regardless of publisher."""
                 self.pkg("list -aHf foo")
                 expected = \
-                    "foo         1.2.1-0 known -----\n" \
-                    "foo         1.2-0   known u----\n" \
-                    "foo         1.1-0   known u----\n" \
-                    "foo         1.0-0   known u----\n" \
-                    "foo         1-0     known u----\n" \
-                    "foo (test2) 1.2.1-0 known -----\n" \
-                    "foo (test2) 1.2-0   known u----\n" \
-                    "foo (test2) 1.1-0   known u----\n" \
-                    "foo (test2) 1.0-0   known u----\n" \
-                    "foo (test2) 1-0     known u----\n"
+                    "foo              1.2.1-0 known -----\n" \
+                    "foo              1.2-0   known u----\n" \
+                    "foo              1.1-0   known u----\n" \
+                    "foo              1.0-0   known u----\n" \
+                    "foo              1-0     known u----\n" \
+                    "foo (test2)      1.2.1-0 known -----\n" \
+                    "foo (test2)      1.2-0   known u----\n" \
+                    "foo (test2)      1.1-0   known u----\n" \
+                    "foo (test2)      1.0-0   known u----\n" \
+                    "foo (test2)      1-0     known u----\n" \
+                    "hier/foo         1.0-0 known -----\n" \
+                    "hier/foo (test2) 1.0-0 known -----\n"
                 output = self.reduceSpaces(self.output)
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
 
                 self.pkg("list -aH foo")
                 expected = \
-                    "foo         1.2.1-0 known -----\n" \
-                    "foo (test2) 1.2.1-0 known -----\n"
+                    "foo              1.2.1-0 known -----\n" \
+                    "foo (test2)      1.2.1-0 known -----\n" \
+                    "hier/foo         1.0-0 known -----\n" \
+                    "hier/foo (test2) 1.0-0 known -----\n"
                 output = self.reduceSpaces(self.output)
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
 
-
         def test_05(self):
                 """Show [email protected] from both depots, but 1.1 only from test2."""
                 self.pkg("list -aHf [email protected] pkg://test2/[email protected]")
                 expected = \
-                    "foo         1.0-0 known u----\n" \
-                    "foo (test2) 1.1-0 known u----\n" \
-                    "foo (test2) 1.0-0 known u----\n"
+                    "foo              1.0-0 known u----\n" \
+                    "foo (test2)      1.1-0 known u----\n" \
+                    "foo (test2)      1.0-0 known u----\n" \
+                    "hier/foo         1.0-0 known -----\n" \
+                    "hier/foo (test2) 1.0-0 known -----\n"
                 output = self.reduceSpaces(self.output)
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
 
                 self.pkg("list -aHf [email protected] pkg://test2/[email protected]")
                 expected = \
-                    "foo         1.0-0 known u----\n" \
-                    "foo (test2) 1.1-0 known u----\n" \
-                    "foo (test2) 1.0-0 known u----\n"
+                    "foo              1.0-0 known u----\n" \
+                    "foo (test2)      1.1-0 known u----\n" \
+                    "foo (test2)      1.0-0 known u----\n" \
+                    "hier/foo         1.0-0 known -----\n" \
+                    "hier/foo (test2) 1.0-0 known -----\n"
                 output = self.reduceSpaces(self.output)
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
@@ -270,7 +296,7 @@
                 # that of an empty repository.  The package should still be
                 # shown as known for test1 and installed for test2.
                 self.pkg("set-publisher -O %s test2" % self.rurl3)
-                self.pkg("list -aHf [email protected]")
+                self.pkg("list -aHf /[email protected]")
                 expected = \
                     "foo 1.0-0 known u----\n" + \
                     "foo (test2) 1.0-0 installed u----\n"
@@ -284,7 +310,7 @@
                 # for test2.
                 self.pkg("unset-publisher test2")
                 self.pkg("set-publisher -O %s test2" % self.rurl3)
-                self.pkg("list -aHf [email protected]")
+                self.pkg("list -aHf /[email protected]")
                 expected = \
                     "foo 1.0-0 known u----\n" + \
                     "foo (test2) 1.0-0 installed u----\n"
@@ -393,52 +419,62 @@
 
                 self.pkg("list -aHf foo*")
                 expected = \
-                    "foo         1.2.1-0 known -----\n" \
-                    "foo         1.2-0   known u----\n" \
-                    "foo         1.1-0   known u----\n" \
-                    "foo         1.0-0   known u----\n" \
-                    "foo         1-0     known u----\n" \
-                    "foo (test2) 1.2.1-0 known -----\n" \
-                    "foo (test2) 1.2-0   known u----\n" \
-                    "foo (test2) 1.1-0   known u----\n" \
-                    "foo (test2) 1.0-0   known u----\n" \
-                    "foo (test2) 1-0     known u----\n" \
-                    "food        1.2-0   known -----\n" \
-                    "food (test2) 1.2-0  known -----\n"
+                    "foo              1.2.1-0 known -----\n" \
+                    "foo              1.2-0   known u----\n" \
+                    "foo              1.1-0   known u----\n" \
+                    "foo              1.0-0   known u----\n" \
+                    "foo              1-0     known u----\n" \
+                    "foo (test2)      1.2.1-0 known -----\n" \
+                    "foo (test2)      1.2-0   known u----\n" \
+                    "foo (test2)      1.1-0   known u----\n" \
+                    "foo (test2)      1.0-0   known u----\n" \
+                    "foo (test2)      1-0     known u----\n" \
+                    "food             1.2-0   known -----\n" \
+                    "food (test2)     1.2-0   known -----\n"
 
                 output = self.reduceSpaces(self.output)
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
-                self.pkg("list -aHf 'fo*'")
-                output = self.reduceSpaces(self.output)
-                self.assertEqualDiff(expected, output)
-                self.pkg("list -aHf '*fo*'")
+                self.pkg("list -aHf '/fo*'")
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
                 self.pkg("list -aHf 'f?o*'")
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
 
+                expected += \
+                    "hier/foo         1.0-0   known -----\n" \
+                    "hier/foo (test2) 1.0-0   known -----\n"
+                expected = self.reduceSpaces(expected)
+                self.pkg("list -aHf '*fo*'")
+                output = self.reduceSpaces(self.output)
+                self.assertEqualDiff(expected, output)
+
                 self.pkg("list -aH foo*")
                 expected = \
-                    "foo         1.2.1-0 known -----\n" \
-                    "foo (test2) 1.2.1-0 known -----\n" \
-                    "food        1.2-0   known -----\n" \
-                    "food (test2) 1.2-0  known -----\n"
+                    "foo              1.2.1-0 known -----\n" \
+                    "foo (test2)      1.2.1-0 known -----\n" \
+                    "food             1.2-0   known -----\n" \
+                    "food (test2)     1.2-0   known -----\n" \
 
                 output = self.reduceSpaces(self.output)
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
-                self.pkg("list -aH 'fo*'")
-                output = self.reduceSpaces(self.output)
-                self.assertEqualDiff(expected, output)
-                self.pkg("list -aH '*fo*'")
+                self.pkg("list -aH '/fo*'")
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
                 self.pkg("list -aH 'f?o*'")
                 output = self.reduceSpaces(self.output)
                 self.assertEqualDiff(expected, output)
 
+                expected += \
+                    "hier/foo         1.0-0   known -----\n" \
+                    "hier/foo (test2) 1.0-0   known -----\n"
+                expected = self.reduceSpaces(expected)
+                self.pkg("list -aH '*fo*'")
+                output = self.reduceSpaces(self.output)
+                self.assertEqualDiff(expected, output)
+
                 for pat, ecode in (("foo food", 0), ("bogus", 1),
                     ("foo bogus", 3), ("foo food bogus", 3),
                     ("bogus quirky names", 1), ("'fo*' bogus", 3),
@@ -447,7 +483,7 @@
 
         def test_13_multi_name(self):
                 """Test for multiple name match listing."""
-                self.pkg("list -aHf foo*@1.2")
+                self.pkg("list -aHf /foo*@1.2")
                 expected = \
                     "foo          1.2.1-0 known -----\n" + \
                     "foo          1.2-0   known u----\n" + \
@@ -459,7 +495,7 @@
                 expected = self.reduceSpaces(expected)
                 self.assertEqualDiff(expected, output)
 
-                self.pkg("list -aH foo*@1.2")
+                self.pkg("list -aH /foo*@1.2")
                 expected = \
                     "foo          1.2.1-0 known -----\n" + \
                     "foo  (test2) 1.2.1-0 known -----\n" + \
@@ -486,6 +522,41 @@
                 # Last, test all at once.
                 self.pkg("list %s" % " ".join(pats), exit=1)
 
+        def test_15_latest(self):
+                """Verify that FMRIs using @latest work as expected."""
+
+                self.pkg("list -aHf foo@latest")
+                expected = \
+                    "foo              1.2.1-0 known -----\n" \
+                    "foo (test2)      1.2.1-0 known -----\n" \
+                    "hier/foo         1.0-0 known -----\n" \
+                    "hier/foo (test2) 1.0-0 known -----\n"
+                output = self.reduceSpaces(self.output)
+                expected = self.reduceSpaces(expected)
+                self.assertEqualDiff(expected, output)
+
+                self.pkg("list -aHf foo@latest [email protected] //test2/[email protected]")
+                expected = \
+                    "foo              1.2.1-0 known -----\n" \
+                    "foo              1.1-0   known u----\n" \
+                    "foo (test2)      1.2.1-0 known -----\n" \
+                    "foo (test2)      1.2-0   known u----\n" \
+                    "foo (test2)      1.1-0   known u----\n" \
+                    "hier/foo         1.0-0 known -----\n" \
+                    "hier/foo (test2) 1.0-0 known -----\n"
+                output = self.reduceSpaces(self.output)
+                expected = self.reduceSpaces(expected)
+                self.assertEqualDiff(expected, output)
+
+                self.pkg("list -aHf /hier/foo@latest //test1/foo@latest")
+                expected = \
+                    "foo              1.2.1-0 known -----\n" \
+                    "hier/foo         1.0-0 known -----\n" \
+                    "hier/foo (test2) 1.0-0 known -----\n"
+                output = self.reduceSpaces(self.output)
+                expected = self.reduceSpaces(expected)
+                self.assertEqualDiff(expected, output)
+
 
 class TestPkgListSingle(pkg5unittest.SingleDepotTestCase):
         # Destroy test space every time.
--- a/src/tests/cli/t_pkgrecv.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/cli/t_pkgrecv.py	Thu Feb 10 14:06:18 2011 -0800
@@ -379,7 +379,7 @@
                 # Retrieve bronze using -m all-timestamps and a version pattern.
                 # This should only retrieve bronze20_1 and bronze20_2.
                 self.pkgrecv(self.durl1, "--raw -m all-timestamps -r -k "
-                    "-d %s %s" % (self.tempdir, "[email protected]"))
+                    "-d %s %s" % (self.tempdir, "/[email protected]"))
 
                 # Verify that only expected packages were retrieved.
                 expected = [
@@ -494,7 +494,7 @@
                 expected."""
 
                 # Setup a repository with packages from multiple publishers.
-                amber = self.amber10.replace("open ", "open pkg://test2/")
+                amber = self.amber10.replace("open ", "open //test2/")
                 self.pkgsend_bulk(self.durl3, amber)
                 self.pkgrecv(self.durl1, "-d %s [email protected] [email protected]" %
                     self.durl3)
@@ -603,6 +603,8 @@
                 self.pkgrecv(arc_path, "-d %s pkg://test2/amber bronze" %
                     self.durl4)
                 repo = self.dcs[4].get_repo()
+                self.wait_repo(repo.root)
+                self.pkgrecv(repo.root, "--newest")
 
                 # Check for expected publishers.
                 expected = set(["test1", "test2"])
--- a/src/tests/cli/t_variants.py	Fri Feb 11 08:52:34 2011 +1300
+++ b/src/tests/cli/t_variants.py	Thu Feb 10 14:06:18 2011 -0800
@@ -67,6 +67,14 @@
         add set name=variant.mumble value=false
         close"""
 
+        mumblefratz = """
+        open [email protected],5.11-0
+        add set name=variant.mumble value=true
+        close
+        open [email protected],5.11-0
+        add set name=variant.mumble value=false
+        close"""
+
         misc_files = [ 
             "tmp/bronze_sparc/etc/motd",
             "tmp/bronze_i386/etc/motd",
@@ -86,6 +94,7 @@
         def setUp(self):
                 pkg5unittest.SingleDepotTestCase.setUp(self)
                 self.pkgsend_bulk(self.rurl, self.mumble10)
+                self.pkgsend_bulk(self.rurl, self.mumblefratz)
                 self.make_misc_files(self.misc_files)
 
         def test_variant_1(self):
@@ -101,6 +110,22 @@
                 self.pkg("info mumble-true", exit=1)
                 self.pkg("info mumble-false")
 
+        def test_variant_3_latest(self):
+                """Verify that install matching for '@latest' matches the
+                newest version allowed by image variants."""
+
+                self.image_create(self.rurl,
+                    variants={ "variant.mumble": "true" })
+                self.pkg("install mumblefratz@latest")
+                self.pkg("info [email protected]")
+                self.pkg("info [email protected]", exit=1)
+
+                self.image_create(self.rurl,
+                    variants={ "variant.mumble": "false" })
+                self.pkg("install mumblefratz@latest")
+                self.pkg("info [email protected]", exit=1)
+                self.pkg("info [email protected]")
+
         def test_old_zones_pkgs(self):
                 self.__test_common("variant.opensolaris.zone",
                     "opensolaris.zone")