# HG changeset patch # User Shawn Walker # Date 1371682448 25200 # Node ID 2bb9f6ffdff938759957cb3a67b3eb14475a9997 # Parent bcafb737718d7b585ee5f0b7e4012debc4d7e558 15654935 pkg facet/variant should also display those implicitly set 15713232 pkg change-facet/change-variant could be faster 16425698 pkg variant subcommand should not require "variant." prefix 16689413 facet/variant subcommands should support wildcards 16689420 pkg facet/variant should offer parsable output format 16689425 pkg facet/variant should have a full test suite 16694970 manifest facets/variants properties can raise misleading exception 16803226 no updates messaging omits linked image name 16808780 pkg variant subcommand could show available variants diff -r bcafb737718d -r 2bb9f6ffdff9 doc/client_api_versions.txt --- a/doc/client_api_versions.txt Wed Jun 12 15:44:50 2013 -0700 +++ b/doc/client_api_versions.txt Wed Jun 19 15:54:08 2013 -0700 @@ -1,5 +1,15 @@ +Version 75: +Compatible with clients using versions 72-74. + + pkg.client.api.ImageInterface has changed as follows: + + * New generator functions 'gen_variants()' and 'gen_facets()' to + retrieve the current list of facets or variants currently + (implicitly or explicitly) set in the image. See 'pydoc + pkg.client.api' for details. + Version 74: -Compatible with clients using version 73, 72. +Compatible with clients using versions 72-74. The PlanDescription now has interfaces to determine whether or not release notes were generated for this operation, whether or not they must be displayed, diff -r bcafb737718d -r 2bb9f6ffdff9 exception_lists/keywords --- a/exception_lists/keywords Wed Jun 12 15:44:50 2013 -0700 +++ b/exception_lists/keywords Wed Jun 19 15:54:08 2013 -0700 @@ -20,9 +20,9 @@ # # -# Copyright 2009 Sun Microsystems, Inc. All rights reserved. -# Use is subject to license terms. +# Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved. # +src/modules/client/api.py src/modules/client/image.py src/client.py diff -r bcafb737718d -r 2bb9f6ffdff9 src/client.py --- a/src/client.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/client.py Wed Jun 19 15:54:08 2013 -0700 @@ -62,7 +62,6 @@ import textwrap import time import traceback - import tempfile import pkg import pkg.actions as actions @@ -92,7 +91,7 @@ import sys sys.exit(1) -CLIENT_API_VERSION = 74 +CLIENT_API_VERSION = 75 PKG_CLIENT_NAME = "pkg" JUST_UNKNOWN = 0 @@ -272,8 +271,8 @@ " [--deny-new-be | --require-new-be] [--be-name name]\n" " ...") - adv_usage["variant"] = _("[-H] []") - adv_usage["facet"] = ("[-H] []") + adv_usage["variant"] = _("[-Haiv] [-F format] [ ...]") + adv_usage["facet"] = ("[-Hai] [-F format] [ ...]") adv_usage["avoid"] = _("[pkg_fmri_pattern] ...") adv_usage["unavoid"] = _("[pkg_fmri_pattern] ...") adv_usage["freeze"] = _("[-n] [-c reason] [pkg_fmri_pattern] ...") @@ -346,10 +345,10 @@ raise ValueError( "Unable to find usage str for %s" % cmd) - usage = cmd_dic[cmd] - if usage is not "": + use_txt = cmd_dic[cmd] + if use_txt is not "": logger.error( - " pkg %(cmd)s %(usage)s" % + " pkg %(cmd)s %(use_txt)s" % locals()) else: logger.error(" pkg %s" % cmd) @@ -996,7 +995,9 @@ elif src != dest: c.append((src, dest)) else: - a.append((src, dest)) + # Changing or repairing package content (e.g. fix, + # change-facet, etc.) + a.append((dest, dest)) def bool_str(val): if val: @@ -1014,8 +1015,15 @@ cond_show(_("Packages to remove:"), "%d", len(r)) cond_show(_("Packages to install:"), "%d", len(i)) cond_show(_("Packages to update:"), "%d", len(c)) + if varcets or mediators: + cond_show(_("Packages to change:"), "%d", len(a)) + else: + cond_show(_("Packages to fix:"), "%d", len(a)) cond_show(_("Mediators to change:"), "%d", len(mediators)) cond_show(_("Variants/Facets to change:"), "%d", len(varcets)) + if not plan.new_be: + cond_show(_("Services to change:"), "%d", + len(plan.services)) if verbose: # Only show space information in verbose mode. @@ -1027,11 +1035,6 @@ _("Estimated space to be consumed:"), misc.bytes_to_str(plan.bytes_added))) - if varcets or mediators: - cond_show(_("Packages to change:"), "%d", len(a)) - else: - cond_show(_("Packages to fix:"), "%d", len(a)) - # only display BE information if we're operating on the # liveroot environment (since otherwise we'll never be # manipulating BEs). @@ -1048,10 +1051,6 @@ status.append((_("Create backup boot environment:"), bool_str(plan.backup_be))) - if not plan.new_be: - cond_show(_("Services to change:"), "%d", - len(plan.services)) - if "boot-archive" in disp: status.append((_("Rebuild boot archive:"), bool_str(plan.update_boot_archive))) @@ -1087,7 +1086,7 @@ if "fmris" in disp: changed = collections.defaultdict(list) - for src, dest in itertools.chain(r, i, c): + for src, dest in itertools.chain(r, i, c, a): if src and dest: if src.publisher != dest.publisher: pparent = "%s -> %s" % (src.publisher, @@ -1095,8 +1094,9 @@ else: pparent = dest.publisher pname = dest.pkg_stem - pver = "%s -> %s" % (src.fmri.version, - dest.fmri.version) + pver = str(src.fmri.version) + if src != dest: + pver += " -> %s" % dest.fmri.version elif dest: pparent = dest.publisher pname = dest.pkg_stem @@ -1123,11 +1123,6 @@ logger.info(" %s" % pver) last_parent = pparent - if len(a): - logger.info(_("Affected fmris:")) - for src, dest in a: - logger.info(" %s", src) - if "services" in disp and not plan.new_be: last_action = None for action, smf_fmri in plan.services: @@ -1324,7 +1319,7 @@ else: s = _("No updates necessary for this image.") if api_inst.ischild(): - s + " (%s)" % api_inst.get_linked_name() + s += " (%s)" % api_inst.get_linked_name() msg(s) return @@ -1475,13 +1470,6 @@ def __api_alloc(imgdir, exact_match, pkg_image_used): - def qv(val): - # Escape shell metacharacters; '\' must be escaped first to - # prevent escaping escapes. - for c in "\\ \t\n'`;&()|^<>?*": - val = val.replace(c, "\\" + c) - return val - progresstracker = get_tracker() try: return api.ImageInterface(imgdir, CLIENT_API_VERSION, @@ -3938,8 +3926,8 @@ duplicate=True) dest_repo = dest_pub.repository if dest_repo.origins and \ - not dest_repo.has_origin(repo_uri): - add_origins = [repo_uri] + not dest_repo.has_origin(repo_uri): + add_origins = [repo_uri] if not src_repo and not add_origins: # The repository doesn't have to provide origin @@ -4746,72 +4734,141 @@ return EXIT_OK -def variant_list(api_inst, args): - """pkg variant [-H] []""" - - omit_headers = False - - opts, pargs = getopt.getopt(args, "H") - - for opt, arg in opts: - if opt == "-H": - omit_headers = True - - # XXX image variants should be accessible through pkg.client.api - variants = img.get_variants() - - for p in pargs: - if p not in variants: - error(_("no such variant: %s") % p, cmd="variant") - return EXIT_OOPS - - if not pargs: - pargs = variants.keys() - - width = max(max([len(p) for p in pargs]), 8) - fmt = "%%-%ss %%s" % width - if not omit_headers: - msg(fmt % ("VARIANT", "VALUE")) - - for p in pargs: - msg(fmt % (p, variants[p])) - +def list_variant(op, api_inst, pargs, omit_headers, output_format, + list_all_items, list_installed, verbose): + """pkg variant [-Haiv] [-F format] [ ...]""" + + subcommand = "variant" + if output_format is None: + output_format = "default" + + # To work around Python 2.x's scoping limits, a list is used. + found = [False] + req_variants = set(pargs) + + def gen_current(): + for (name, val, pvals) in api_inst.gen_variants(variant_list, + patterns=req_variants): + found[0] = True + yield { + "variant": name, + "value": val + } + + def gen_possible(): + for (name, val, pvals) in api_inst.gen_variants(variant_list, + patterns=req_variants): + found[0] = True + for pval in pvals: + yield { + "variant": name, + "value": pval + } + + if verbose: + gen_listing = gen_possible + else: + gen_listing = gen_current + + if list_all_items: + if verbose: + variant_list = api_inst.VARIANT_ALL_POSSIBLE + else: + variant_list = api_inst.VARIANT_ALL + elif list_installed: + if verbose: + variant_list = api_inst.VARIANT_INSTALLED_POSSIBLE + else: + variant_list = api_inst.VARIANT_INSTALLED + else: + if verbose: + variant_list = api_inst.VARIANT_IMAGE_POSSIBLE + else: + variant_list = api_inst.VARIANT_IMAGE + + # VARIANT VALUE + # + # + # ... + field_data = { + "variant" : [("default", "json", "tsv"), _("VARIANT"), ""], + "value" : [("default", "json", "tsv"), _("VALUE"), ""], + } + desired_field_order = (_("VARIANT"), _("VALUE")) + + # Default output formatting. + def_fmt = "%-70s %s" + + # print without trailing newline. + sys.stdout.write(misc.get_listing(desired_field_order, + field_data, gen_listing(), output_format, def_fmt, + omit_headers)) + + if not found[0] and req_variants: + if output_format == "default": + # Don't pollute other output formats. + error(_("no matching variants found"), + cmd=subcommand) + return EXIT_OOPS + + # Successful if no variants exist or if at least one matched. return EXIT_OK -def facet_list(api_inst, args): - """pkg facet [-H] []""" - - omit_headers = False - - opts, pargs = getopt.getopt(args, "H") - - for opt, arg in opts: - if opt == "-H": - omit_headers = True - - # XXX image facets should be accessible through pkg.client.api - facets = img.get_facets() - - for i, p in enumerate(pargs[:]): - if not p.startswith("facet."): - pargs[i] = "facet." + p - - if not pargs: - pargs = facets.keys() - - if pargs: - width = max(max([len(p) for p in pargs]), 8) - else: - width = 8 - - fmt = "%%-%ss %%s" % width - - if not omit_headers: - msg(fmt % ("FACETS", "VALUE")) - - for p in pargs: - msg(fmt % (p, facets[p])) - +def list_facet(op, api_inst, pargs, omit_headers, output_format, list_all_items, + list_installed): + """pkg facet [-Hai] [-F format] [ ...]""" + + subcommand = "facet" + if output_format is None: + output_format = "default" + + # To work around Python 2.x's scoping limits, a list is used. + found = [False] + req_facets = set(pargs) + + facet_list = api_inst.FACET_IMAGE + if list_all_items: + facet_list = api_inst.FACET_ALL + elif list_installed: + facet_list = api_inst.FACET_INSTALLED + + def gen_listing(): + for (name, val) in api_inst.gen_facets(facet_list, + patterns=req_facets): + found[0] = True + + # Values here are intentionally not _(). + yield { + "facet": name, + "value": val and "True" or "False" + } + + # FACET VALUE + # + # + # ... + field_data = { + "facet" : [("default", "json", "tsv"), _("FACET"), ""], + "value" : [("default", "json", "tsv"), _("VALUE"), ""], + } + desired_field_order = (_("FACET"), _("VALUE")) + + # Default output formatting. + def_fmt = "%-70s %s" + + # print without trailing newline. + sys.stdout.write(misc.get_listing(desired_field_order, + field_data, gen_listing(), output_format, def_fmt, + omit_headers)) + + if not found[0] and req_facets: + if output_format == "default": + # Don't pollute other output formats. + error(_("no matching facets found"), + cmd=subcommand) + return EXIT_OOPS + + # Successful if no facets exist or if at least one matched. return EXIT_OK def list_linked(op, api_inst, pargs, @@ -5765,6 +5822,7 @@ "attach_parent" : ("p", ""), "list_available" : ("a", ""), + "list_all_items" : ("a", ""), "output_format" : ("F", "output-format"), "tagged" : ("", "tagged"), @@ -5784,6 +5842,8 @@ "ctlfd" : ("", "ctlfd"), "progfd" : ("", "progfd"), + + "list_installed" : ("i", ""), } # @@ -5807,7 +5867,7 @@ "change-variant" : [change_variant], "contents" : [list_contents], "detach-linked" : [detach_linked, 0], - "facet" : [facet_list], + "facet" : [list_facet], "fix" : [fix_image], "freeze" : [freeze], "help" : [None], @@ -5840,12 +5900,11 @@ "uninstall" : [uninstall], "unset-authority" : [publisher_unset], "unset-property" : [property_unset], - "update-format" : [update_format], "unset-mediator" : [unset_mediator], "unset-publisher" : [publisher_unset], "update" : [update], "update-format" : [update_format], - "variant" : [variant_list], + "variant" : [list_variant], "verify" : [verify_image], "version" : [None], } @@ -5870,6 +5929,31 @@ ("progfd", None), ] +def opts_cb_varcet(api_inst, opts, opts_new): + if opts_new["list_all_items"] and opts_new["list_installed"]: + raise api_errors.InvalidOptionError( + api_errors.InvalidOptionError.INCOMPAT, + ["list_all_items", "list_installed"]) + +opts_list_varcet = \ + options.opts_table_no_headers + \ + [ + opts_cb_varcet, + ("list_all_items", False), + ("list_installed", False), + ("output_format", None) +] + +opts_list_facet = \ + [opts_cb_varcet] + \ + opts_list_varcet + +opts_list_variant = \ + opts_list_varcet + \ + [ + ("verbose", False) +] + opts_list_mediator = \ options.opts_table_no_headers + \ [ @@ -5887,9 +5971,11 @@ ] cmd_opts = { - "mediator" : opts_list_mediator, - "unset-mediator" : opts_unset_mediator, - "remote" : opts_remote, + "facet" : opts_list_facet, + "mediator" : opts_list_mediator, + "unset-mediator" : opts_unset_mediator, + "remote" : opts_remote, + "variant" : opts_list_variant, } @@ -6067,7 +6153,6 @@ # when there is a short and a long version for the same option # we print both to avoid confusion. def get_cli_opt(option): - out = "" try: s, l = opts_mapping[option] if l and not s: @@ -6075,7 +6160,7 @@ elif s and not l: return "-%s" % s else: - return("-%s/--%s" % (s,l)) + return "-%s/--%s" % (s, l) except KeyError: # ignore if we can't find a match # (happens for repeated arguments) diff -r bcafb737718d -r 2bb9f6ffdff9 src/man/pkg.1 --- a/src/man/pkg.1 Wed Jun 12 15:44:50 2013 -0700 +++ b/src/man/pkg.1 Wed Jun 19 15:54:08 2013 -0700 @@ -1,6 +1,6 @@ '\" te .\" Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved. -.TH pkg 1 "26 Feb 2013" "SunOS 5.12" "User Commands" +.TH pkg 1 "19 Apr 2013" "SunOS 5.12" "User Commands" .SH NAME pkg \- Image Packaging System retrieval client .SH SYNOPSIS @@ -115,7 +115,7 @@ .LP .nf -/usr/bin/pkg variant [-H] [variant.\fIvariant_name\fR ...] +/usr/bin/pkg variant [-Haiv] [-F \fIformat\fR] [\fIvariant_pattern\fR ...] .fi .LP @@ -130,7 +130,7 @@ .LP .nf -/usr/bin/pkg facet [-H] [\fIfacet_name\fR ...] +/usr/bin/pkg facet [-Hai] [-F \fIformat\fR] [\fIfacet_pattern\fR ...] .fi .LP @@ -1576,7 +1576,7 @@ .ne 2 .mk .na -\fB\fBpkg variant\fR [\fB-H\fR] [\fBvariant.\fR\fIvariant_name\fR ...]\fR +\fB\fBpkg variant\fR [\fB-Haiv\fR] [\fB-F\fR \fIformat\fR] [\fIvariant_pattern\fR ...]\fR .ad .sp .6 .RS 4n @@ -1585,7 +1585,7 @@ .ne 2 .mk .na -\fB\fBvariant.\fR\fIvariant_name\fR\fR +\fB\fIvariant_pattern\fR\fR .ad .sp .6 .RS 4n @@ -1596,6 +1596,17 @@ .ne 2 .mk .na +\fB\fB-F\fR\fR +.ad +.sp .6 +.RS 4n +Specify an alternative output format. Currently, only \fBtsv\fR (Tab Separated Values) is valid. +.RE + +.sp +.ne 2 +.mk +.na \fB\fB-H\fR\fR .ad .sp .6 @@ -1603,6 +1614,39 @@ Omit the headers from the listing. .RE +.sp +.ne 2 +.mk +.na +\fB\fB-a\fR\fR +.ad +.sp .6 +.RS 4n +Display all variants explicitly set in the image and all variants that are listed in installed packages. This option cannot be combined with \fB-i\fR. +.RE + +.sp +.ne 2 +.mk +.na +\fB\fB-i\fR\fR +.ad +.sp .6 +.RS 4n +Display all variants that are listed in installed packages. This option cannot be combined with \fB-a\fR. +.RE + +.sp +.ne 2 +.mk +.na +\fB\fB-v\fR\fR +.ad +.sp .6 +.RS 4n +Display the possible variant values that can be set for installed packages. This option can be combined with \fB-a\fR and \fB-i\fR. +.RE + .RE .sp @@ -1624,7 +1668,7 @@ .ne 2 .mk .na -\fB\fBpkg facet\fR [\fB-H\fR] [\fIfacet_name\fR ...]\fR +\fB\fBpkg facet\fR [\fB-Hai\fR] [\fB-F\fR \fIformat\fR] [\fIfacet_pattern\fR ...]\fR .ad .sp .6 .RS 4n @@ -1633,7 +1677,7 @@ .ne 2 .mk .na -\fB\fIfacet_name\fR\fR +\fB\fIfacet_pattern\fR\fR .ad .sp .6 .RS 4n @@ -1644,6 +1688,17 @@ .ne 2 .mk .na +\fB\fB-F\fR\fR +.ad +.sp .6 +.RS 4n +Specify an alternative output format. Currently, only \fBtsv\fR (Tab Separated Values) is valid. +.RE + +.sp +.ne 2 +.mk +.na \fB\fB-H\fR\fR .ad .sp .6 @@ -1651,6 +1706,28 @@ Omit the headers from the listing. .RE +.sp +.ne 2 +.mk +.na +\fB\fB-a\fR\fR +.ad +.sp .6 +.RS 4n +Display all facets explicitly set in the image and all facets that are listed in installed packages. This option cannot be combined with \fB-i\fR. +.RE + +.sp +.ne 2 +.mk +.na +\fB\fB-i\fR\fR +.ad +.sp .6 +.RS 4n +Display all facets that are listed in installed packages. This option cannot be combined with \fB-a\fR. +.RE + .RE .sp diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/api_common.py --- a/src/modules/api_common.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/api_common.py Wed Jun 19 15:54:08 2013 -0700 @@ -133,8 +133,9 @@ def __init__(self, pfmri, pkg_stem=None, summary=None, category_info_list=None, states=None, publisher=None, version=None, build_release=None, branch=None, packaging_date=None, - size=None, licenses=None, links=None, hardlinks=None, files=None, - dirs=None, dependencies=None, description=None, attrs=None): + size=None, csize=None, licenses=None, links=None, hardlinks=None, + files=None, dirs=None, dependencies=None, description=None, + attrs=None): self.pkg_stem = pkg_stem self.summary = summary @@ -148,6 +149,7 @@ self.branch = branch self.packaging_date = packaging_date self.size = size + self.csize = csize self.fmri = pfmri self.licenses = licenses self.links = links diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/client/api.py --- a/src/modules/client/api.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/client/api.py Wed Jun 19 15:54:08 2013 -0700 @@ -103,8 +103,8 @@ # things like help(pkg.client.api.PlanDescription) from pkg.client.plandesc import PlanDescription # pylint: disable=W0611 -CURRENT_API_VERSION = 74 -COMPATIBLE_API_VERSIONS = frozenset([72, 73, CURRENT_API_VERSION]) +CURRENT_API_VERSION = 75 +COMPATIBLE_API_VERSIONS = frozenset([72, 73, 74, CURRENT_API_VERSION]) CURRENT_P5I_VERSION = 1 # Image type constants. @@ -253,6 +253,10 @@ needed. Cancel may only be invoked while a cancelable method is running.""" + FACET_ALL = 0 + FACET_IMAGE = 1 + FACET_INSTALLED = 2 + # Constants used to reference specific values that info can return. INFO_FOUND = 0 INFO_MISSING = 1 @@ -268,6 +272,13 @@ MATCH_FMRI = 1 MATCH_GLOB = 2 + VARIANT_ALL = 0 + VARIANT_ALL_POSSIBLE = 1 + VARIANT_IMAGE = 2 + VARIANT_IMAGE_POSSIBLE = 3 + VARIANT_INSTALLED = 4 + VARIANT_INSTALLED_POSSIBLE = 5 + def __init__(self, img_path, version_id, progresstracker, cancel_state_callable, pkg_client_name, exact_match=True, cmdpath=None): @@ -817,6 +828,173 @@ dependencies on this) """ return [a for a in self._img.get_avoid_dict().iteritems()] + def gen_facets(self, facet_list, patterns=misc.EmptyI): + """A generator function that produces tuples of the form: + + ( + name, - (string) facet name (e.g. facet.doc) + value - (boolean) current facet value + ) + + Results are always sorted by facet name. + + 'facet_list' is one of the following constant values indicating + which facets should be returned based on how they were set: + + FACET_ALL + Return all facets set in the image and all + facets listed in installed packages. + + FACET_IMAGE + Return only the facets set in the image. + + FACET_INSTALLED + Return only the facets listed in installed + packages. + + 'patterns' is an optional list of facet wildcard strings to + filter results by.""" + + facets = self._img.cfg.facets + if facet_list != self.FACET_INSTALLED: + # Include all facets set in image. + fimg = set(facets.keys()) + else: + # Don't include any set only in image. + fimg = set() + + # Get all facets found in packages and determine state. + fpkg = set() + excludes = self._img.list_excludes() + if facet_list != self.FACET_IMAGE: + for f in self._img.gen_installed_pkgs(): + # The manifest must be loaded without + # pre-applying excludes so that gen_facets() can + # choose how to filter the actions. + mfst = self._img.get_manifest(f, + ignore_excludes=True) + for facet in mfst.gen_facets(excludes=excludes): + # Use Facets object to determine + # effective facet state. + fpkg.add(facet) + + # Generate the results. + for name in misc.yield_matching("facet.", sorted(fimg | fpkg), + patterns): + # The image's Facets dictionary will return the + # effective value for any facets not explicitly set in + # the image (wildcards or implicit). + yield (name, facets[name]) + + def gen_variants(self, variant_list, patterns=misc.EmptyI): + """A generator function that produces tuples of the form: + + ( + name, - (string) variant name (e.g. variant.arch) + value - (string) current variant value, + possible - (list) list of possible variant values based + on installed packages; empty unless using + *_POSSIBLE variant_list. + ) + + Results are always sorted by variant name. + + 'variant_list' is one of the following constant values indicating + which variants should be returned based on how they were set: + + VARIANT_ALL + Return all variants set in the image and all + variants listed in installed packages. + + VARIANT_ALL_POSSIBLE + Return possible variant values (those found in + any installed package) for all variants set in + the image and all variants listed in installed + packages. + + VARIANT_IMAGE + Return only the variants set in the image. + + VARIANT_IMAGE_POSSIBLE + Return possible variant values (those found in + any installed package) for only the variants set + in the image. + + VARIANT_INSTALLED + Return only the variants listed in installed + packages. + + VARIANT_INSTALLED_POSSIBLE + Return possible variant values (those found in + any installed package) for only the variants + listed in installed packages. + + 'patterns' is an optional list of variant wildcard strings to + filter results by.""" + + variants = self._img.cfg.variants + if variant_list != self.VARIANT_INSTALLED and \ + variant_list != self.VARIANT_INSTALLED_POSSIBLE: + # Include all variants set in image. + vimg = set(variants.keys()) + else: + # Don't include any set only in image. + vimg = set() + + # Get all variants found in packages and determine state. + vpkg = {} + excludes = self._img.list_excludes() + vposs = collections.defaultdict(set) + if variant_list != self.VARIANT_IMAGE: + # Only incur the overhead of reading through all + # installed packages if not just listing variants set in + # image or listing possible values for them. + for f in self._img.gen_installed_pkgs(): + # The manifest must be loaded without + # pre-applying excludes so that gen_variants() + # can choose how to filter the actions. + mfst = self._img.get_manifest(f, + ignore_excludes=True) + for variant, vals in mfst.gen_variants( + excludes=excludes): + # Unlike facets, Variants class doesn't + # handle implicitly set values. + if variant[:14] == "variant.debug.": + # Debug variants are implicitly + # false and are not required + # to be set explicitly in the + # image. + vpkg[variant] = variants.get( + variant, "false") + elif variant not in vimg: + # Although rare, packages with + # unknown variants (those not + # set in the image) can be + # installed as long as content + # does not conflict. For those + # variants, return None. + vpkg[variant] = \ + variants.get(variant) + + if (variant_list == \ + self.VARIANT_ALL_POSSIBLE or + variant_list == \ + self.VARIANT_IMAGE_POSSIBLE or + variant_list == \ + self.VARIANT_INSTALLED_POSSIBLE): + # Build possible list of variant + # values. + vposs[variant].update(set(vals)) + + # Generate the results. + for name in misc.yield_matching("variant.", + sorted(vimg | set(vpkg.keys())), patterns): + try: + yield (name, vpkg[name], sorted(vposs[name])) + except KeyError: + yield (name, variants[name], + sorted(vposs[name])) + def freeze_pkgs(self, fmri_strings, dry_run=False, comment=None, unfreeze=False): """Freeze/Unfreeze one or more packages.""" @@ -3915,7 +4093,7 @@ pub = name = version = None links = hardlinks = files = dirs = \ - size = licenses = cat_info = \ + csize = size = licenses = cat_info = \ description = None if PackageInfo.CATEGORIES in info_needed: @@ -3966,7 +4144,7 @@ mfst, alt_pub=alt_pub) if PackageInfo.SIZE in info_needed: - size = mfst.get_size( + size, csize = mfst.get_size( excludes=excludes) if act_opts & info_needed: @@ -3987,7 +4165,7 @@ mfst.gen_key_attribute_value_by_type( "dir", excludes)) elif PackageInfo.SIZE in info_needed: - size = 0 + size = csize = 0 # Trim response set. if PackageInfo.STATE in info_needed: @@ -4016,7 +4194,7 @@ states=states, publisher=pub, version=release, build_release=build_release, branch=branch, packaging_date=packaging_date, size=size, - pfmri=pfmri, licenses=licenses, + csize=csize, pfmri=pfmri, licenses=licenses, links=links, hardlinks=hardlinks, files=files, dirs=dirs, dependencies=dependencies, description=description, attrs=attrs)) diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/client/imageplan.py --- a/src/modules/client/imageplan.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/client/imageplan.py Wed Jun 19 15:54:08 2013 -0700 @@ -32,7 +32,6 @@ import mmap import operator import os -import simplejson as json import stat import sys import tempfile @@ -45,7 +44,6 @@ import pkg.actions import pkg.actions.driver as driver import pkg.catalog -import pkg.client.actuator as actuator import pkg.client.api_errors as api_errors import pkg.client.indexer as indexer import pkg.client.pkg_solver as pkg_solver @@ -234,7 +232,8 @@ return self.pd._cbytes_avail def __vector_2_fmri_changes(self, installed_dict, vector, - li_pkg_updates=True, new_variants=None, new_facets=None): + li_pkg_updates=True, new_variants=None, new_facets=None, + fmri_changes=None): """Given an installed set of packages, and a proposed vector of package changes determine what, if any, changes should be made to the image. This takes into account different @@ -242,37 +241,26 @@ where the only packages being updated are linked image constraints, etc.""" - cat = self.image.get_catalog(self.image.IMG_CATALOG_KNOWN) - fmri_updates = [] + if fmri_changes is not None: + affected = [f[0] for f in fmri_changes] + else: + affected = None + for a, b in ImagePlan.__dicts2fmrichanges(installed_dict, ImagePlan.__fmris2dict(vector)): if a != b: fmri_updates.append((a, b)) continue - if new_facets is not None or new_variants: - # - # In the case of a facet change we reinstall - # packages since any action in a package could - # have a facet attached to it. - # - # In the case of variants packages should - # declare what variants they contain. Hence, - # theoretically, we should be able to reduce - # the number of package reinstalls by removing - # re-installs of packages that don't declare - # variants. But unfortunately we've never - # enforced this requirement that packages with - # action variant tags declare their variants. - # So now we're stuck just re-installing every - # package. sigh. - # - fmri_updates.append((a, b)) - continue - - if not fmri_updates: - # no planned fmri changes - return [] + + if (new_facets is not None or new_variants): + if affected is None or a in affected: + # If affected list of packages has not + # been predetermined for package fmris + # that are unchanged, or if the fmri + # exists in the list of affected + # packages, add it to the list. + fmri_updates.append((a, a)) if fmri_updates and not li_pkg_updates: # oops. the caller requested no package updates and @@ -288,18 +276,9 @@ self.pd._image_lm = self.image.get_last_modified(string=True) - def __plan_install_solver(self, li_pkg_updates=True, li_sync_op=False, - new_facets=None, new_variants=None, pkgs_inst=None, - reject_list=misc.EmptyI): - """Use the solver to determine the fmri changes needed to - install the specified pkgs, sync the specified image, and/or - change facets/variants within the current image.""" - - if not (new_variants or pkgs_inst or li_sync_op or - new_facets is not None): - # nothing to do - self.pd._fmri_changes = [] - return + def __evaluate_varcets(self, new_variants, new_facets): + """Private helper function used to determine new facet and + variant state for image.""" old_facets = self.image.cfg.facets if new_variants or \ @@ -319,6 +298,18 @@ if new_facets == old_facets: new_facets = None + self.__new_excludes = self.image.list_excludes(new_variants, + new_facets) + + return new_variants, new_facets + + def __plan_install_solver(self, li_pkg_updates=True, li_sync_op=False, + new_facets=None, new_variants=None, pkgs_inst=None, + reject_list=misc.EmptyI, fmri_changes=None): + """Use the solver to determine the fmri changes needed to + install the specified pkgs, sync the specified image, and/or + change facets/variants within the current image.""" + # get ranking of publishers pub_ranks = self.image.get_publisher_ranks() @@ -341,9 +332,6 @@ else: inst_dict = {} - self.__new_excludes = self.image.list_excludes(new_variants, - new_facets) - if new_variants: variants = new_variants else: @@ -374,7 +362,8 @@ self.pd._fmri_changes = self.__vector_2_fmri_changes( installed_dict, new_vector, li_pkg_updates=li_pkg_updates, - new_variants=new_variants, new_facets=new_facets) + new_variants=new_variants, new_facets=new_facets, + fmri_changes=fmri_changes) self.pd._solver_summary = str(solver) if DebugValues["plan"]: @@ -388,6 +377,20 @@ current image.""" self.__plan_op() + + new_variants, new_facets = self.__evaluate_varcets(new_variants, + new_facets) + + if not (new_variants or pkgs_inst or li_sync_op or + new_facets is not None): + # nothing to do + self.pd._fmri_changes = [] + self.pd.state = plandesc.EVALUATED_PKGS + return + + # If we ever actually support changing facets and variants at + # the same time as performing an install, the optimizations done + # for plan_change_varacets should be applied here (shared). self.__plan_install_solver( li_pkg_updates=li_pkg_updates, li_sync_op=li_sync_op, @@ -420,13 +423,144 @@ self.__plan_install(pkgs_inst=pkgs_inst, reject_list=reject_list) + def __get_attr_fmri_changes(self, get_mattrs): + # Attempt to optimize package planning by determining which + # packages are actually affected by changing attributes (e.g., + # facets, variants). This also provides an accurate list of + # affected packages as a side effect (normally, all installed + # packages are seen as changed). This assumes that facets and + # variants are not both changing at the same time. + use_solver = False + cat = self.image.get_catalog( + self.image.IMG_CATALOG_INSTALLED) + cat_info = frozenset([cat.DEPENDENCY]) + + fmri_changes = [] + pt = self.__progtrack + rem_pkgs = self.image.count_installed_pkgs() + + pt.plan_start(pt.PLAN_PKGPLAN, goal=rem_pkgs) + for f in self.image.gen_installed_pkgs(): + m = self.image.get_manifest(f, + ignore_excludes=True) + + # Get the list of attributes involved in this operation + # that the package uses and that have changed. + use_solver, mattrs = get_mattrs(m, use_solver) + if not mattrs: + # Changed attributes unused. + pt.plan_add_progress(pt.PLAN_PKGPLAN) + rem_pkgs -= 1 + continue + + # Changed attributes are used in this package. + fmri_changes.append((f, f)) + + # If any dependency actions are tagged with one + # of the changed attributes, assume the solver + # must be used. + for act in cat.get_entry_actions(f, cat_info): + for attr in mattrs: + if use_solver: + break + if (act.name == "depend" and + attr in act.attrs): + use_solver = True + break + if use_solver: + break + + rem_pkgs -= 1 + pt.plan_add_progress(pt.PLAN_PKGPLAN) + + pt.plan_done(pt.PLAN_PKGPLAN) + pt.plan_all_done() + + return use_solver, fmri_changes + def plan_change_varcets(self, new_facets=None, new_variants=None, reject_list=misc.EmptyI): """Determine the fmri changes needed to change the specified facets/variants.""" - self.__plan_install(new_facets=new_facets, - new_variants=new_variants, reject_list=reject_list) + self.__plan_op() + new_variants, new_facets = self.__evaluate_varcets(new_variants, + new_facets) + + if not new_variants and new_facets is None: + # nothing to do + self.pd._fmri_changes = [] + self.pd.state = plandesc.EVALUATED_PKGS + return + + # By default, we assume the solver must be used. If any of the + # optimizations below can be applied, they'll determine whether + # the solver can be used. + use_solver = True + fmri_changes = None + + # The following use_solver, fmri_changes checks are only known + # to work correctly if only facets or only variants are + # changing; not both. Changing both is not currently supported + # anyway so this shouldn't be a problem. + if new_facets is not None and not new_variants: + old_facets = self.image.cfg.facets + + def get_fattrs(m, use_solver): + # Get the list of facets involved in this + # operation that the package uses. To + # accurately determine which packages are + # actually being changed, we must compare the + # old effective value for each facet that is + # changing with its new effective value. + return use_solver, list( + f + for f in m.gen_facets( + excludes=self.__new_excludes, + patterns=self.pd._changed_facets) + if new_facets[f] != old_facets[f] + ) + + use_solver, fmri_changes = \ + self.__get_attr_fmri_changes(get_fattrs) + + if new_variants and new_facets is None: + nvariants = self.pd._new_variants + + def get_vattrs(m, use_solver): + # Get the list of variants involved in this + # operation that the package uses. + mvars = [] + for (variant, pvals) in m.gen_variants( + excludes=self.__new_excludes, + patterns=nvariants + ): + if nvariants[variant] not in pvals: + # If the new value for the + # variant is unsupported by this + # package, then the solver + # should be triggered so the + # package can be removed. + use_solver = True + mvars.append(variant) + return use_solver, mvars + + use_solver, fmri_changes = \ + self.__get_attr_fmri_changes(get_vattrs) + + if use_solver: + self.__plan_install_solver( + fmri_changes=fmri_changes, + new_facets=new_facets, + new_variants=new_variants, + reject_list=reject_list) + else: + # If solver isn't involved, assume the list of packages + # has been determined. + self.pd._fmri_changes = fmri_changes and \ + fmri_changes or [] + + self.pd.state = plandesc.EVALUATED_PKGS def plan_set_mediators(self, new_mediators): """Determine the changes needed to set the specified mediators. @@ -880,7 +1014,7 @@ # disallow mount points for safety's sake. if my_dev != os.stat(os.path.dirname(dir_loc)).st_dev: - return [], [] + return [], [] # Any explicit or implicitly packaged directories are # ignored; checking all directory entries is cheap. @@ -2272,7 +2406,7 @@ if str(pfmri.version) == "0,5.11" \ and containing_fmri.pkg_name \ not in installed_dict: - return True + return True else: pfmri.pkg_name = \ containing_fmri.pkg_name @@ -2326,7 +2460,7 @@ for note in self.pd.release_notes[1]: if isinstance(note, unicode): note = note.encode("utf-8") - print >>tmpfile, note + print >> tmpfile, note tmpfile.close() self.pd.release_notes_name = os.path.basename(path) diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/client/plandesc.py --- a/src/modules/client/plandesc.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/client/plandesc.py Wed Jun 19 15:54:08 2013 -0700 @@ -481,10 +481,13 @@ """Returns a formatted list of strings representing the variant/facet changes in this plan""" vs, fs = self.varcets - ret = [] - ret.extend(["variant %s: %s" % a for a in vs]) - ret.extend([" facet %s: %s" % a for a in fs]) - return ret + return list(itertools.chain(( + "variant %s: %s" % (name[8:], val) + for (name, val) in vs + ), ( + " facet %s: %s" % (name[6:], val) + for (name, val) in fs + ))) def get_changes(self): """A generation function that yields tuples of PackageInfo @@ -501,10 +504,17 @@ and 'dest_pi' is the new version of the package it is being upgraded to.""" - for pp in sorted(self.pkg_plans, - key=operator.attrgetter("origin_fmri", "destination_fmri")): - yield (PackageInfo.build_from_fmri(pp.origin_fmri), - PackageInfo.build_from_fmri(pp.destination_fmri)) + key = operator.attrgetter("origin_fmri", "destination_fmri") + for pp in sorted(self.pkg_plans, key=key): + sfmri = pp.origin_fmri + dfmri = pp.destination_fmri + if sfmri == dfmri: + sinfo = dinfo = PackageInfo.build_from_fmri( + sfmri) + else: + sinfo = PackageInfo.build_from_fmri(sfmri) + dinfo = PackageInfo.build_from_fmri(dfmri) + yield (sinfo, dinfo) def get_actions(self): """A generator function that yields action change descriptions diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/gui/misc_non_gui.py --- a/src/modules/gui/misc_non_gui.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/gui/misc_non_gui.py Wed Jun 19 15:54:08 2013 -0700 @@ -41,7 +41,7 @@ # The current version of the Client API the PM, UM and # WebInstall GUIs have been tested against and are known to work with. -CLIENT_API_VERSION = 73 +CLIENT_API_VERSION = 75 LOG_DIR = "/var/tmp" LOG_ERROR_EXT = "_error.log" LOG_INFO_EXT = "_info.log" diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/gui/preferences.py --- a/src/modules/gui/preferences.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/gui/preferences.py Wed Jun 19 15:54:08 2013 -0700 @@ -19,7 +19,7 @@ # # CDDL HEADER END # -# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. # import g11nsvc as g11nsvc @@ -32,7 +32,7 @@ import pygtk pygtk.require("2.0") except ImportError: - sys.exit(1) + sys.exit(1) import pkg.client.api_errors as api_errors import pkg.client.api as api import pkg.gui.misc as gui_misc @@ -536,16 +536,15 @@ break facets = None if manifest != None: - manifest.gen_actions(()) - facets = manifest.facets + facets = list(manifest.gen_facets()) if debug and facets != None: print "DEBUG facets from system/locale:", facets facetlocales = [] if facets == None: return facetlocales - - for facet_key in facets.keys(): + + for facet_key in facets: if not facet_key.startswith(LOCALE_PREFIX): continue m = re.match(LOCALE_MATCH, facet_key) @@ -629,7 +628,7 @@ selected = row[enumerations.LOCALE_SELECTED] locale = row[enumerations.LOCALE] lang = locale.split("_")[0] - + if not lang_locale_dict.has_key(lang): lang_locale_dict[lang] = {} lang_locale_dict[lang][locale] = selected @@ -766,7 +765,7 @@ def __on_preferencescancel_clicked(self, widget): self.w_preferencesdialog.hide() - + def __on_preferencesclose_clicked(self, widget): error_dialog_title = _("Preferences") text = self.w_gsig_name_entry.get_text() diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/lint/engine.py --- a/src/modules/lint/engine.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/lint/engine.py Wed Jun 19 15:54:08 2013 -0700 @@ -40,7 +40,7 @@ import urllib2 PKG_CLIENT_NAME = "pkglint" -CLIENT_API_VERSION = 73 +CLIENT_API_VERSION = 75 pkg.client.global_settings.client_name = PKG_CLIENT_NAME class LintEngineException(Exception): diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/manifest.py --- a/src/modules/manifest.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/manifest.py Wed Jun 19 15:54:08 2013 -0700 @@ -29,11 +29,12 @@ import hashlib import os import tempfile -from itertools import groupby, chain, repeat, izip +from itertools import groupby, chain, product, repeat, izip from operator import itemgetter import pkg.actions as actions import pkg.client.api_errors as apx +import pkg.facet as facet import pkg.misc as misc import pkg.portable as portable import pkg.variant as variant @@ -123,8 +124,7 @@ self.fmri = pfmri self._cache = {} - self._facets = None # facets seen in package - self._variants = None # variants seen in package + self._absent_cache = [] self.actions = [] self.actions_bytype = {} self.attributes = {} # package-wide attributes @@ -367,13 +367,267 @@ ) yield a + def _gen_attrs_to_str(self): + """Generate set action supplemental data containing all facets + and variants from self.actions and size information. Each + returned line must be newline-terminated.""" + + emit_variants = "pkg.variant" not in self + emit_facets = "pkg.facet" not in self + emit_sizes = "pkg.size" not in self and "pkg.csize" not in self + + if not any((emit_variants, emit_facets, emit_sizes)): + # Package already has these attributes. + return + + # List of possible variants and possible values for them. + variants = defaultdict(set) + + # Seed with declared set of variants as actions may be common to + # both and so will not be tagged with variant. + for name in self.attributes: + if name[:8] == "variant.": + variants[name] = set(self.attributes[name]) + + # List of possible facets and under what variant combinations + # they were seen. + facets = defaultdict(set) + + # Unique (facet, value) (variant, value) combinations. + refs = defaultdict(lambda: defaultdict(int)) + + for a in self.gen_actions(): + name = a.name + attrs = a.attrs + if name == "set": + if attrs["name"][:12] == "pkg.variant": + emit_variants = False + elif attrs["name"][:9] == "pkg.facet": + emit_facets = False + + afacets = [] + avariants = [] + for attr, val in attrs.iteritems(): + if attr[:8] == "variant.": + variants[attr].add(val) + avariants.append((attr, val)) + elif attr[:6] == "facet.": + afacets.append((attr, val)) + + for name, val in afacets: + # Facet applicable to this particular variant + # combination. + varkey = tuple(sorted(avariants)) + facets[varkey].add(name) + + # This *must* be sorted to ensure reproducible set + # action generation for sizes and to ensure each + # combination is actually unique. + varcetkeys = tuple(sorted(chain(afacets, avariants))) + refs[varcetkeys]["csize"] += misc.get_pkg_otw_size(a) + if name == "signature": + refs[varcetkeys]["csize"] += \ + a.get_action_chain_csize() + refs[varcetkeys]["size"] += a.get_size() + + # Prevent scope leak. + afacets = avariants = attrs = varcetkeys = None + + if emit_variants: + # Unnecessary if we can guarantee all variants will be + # declared at package level. Omit the "variant." prefix + # from attribute values since that's implicit and can be + # added back when the action is parsed. + yield "%s\n" % AttributeAction(None, name="pkg.variant", + value=sorted(v[8:] for v in variants)) + + # Emit a set action for every variant used with possible values + # if one does not already exist. + for name in variants: + # merge_facets needs the variant values sorted and this + # is desirable when generating the variant attr anyway. + variants[name] = sorted(variants[name]) + if name not in self.attributes: + yield "%s\n" % AttributeAction(None, name=name, + value=variants[name]) + + if emit_facets: + # Get unvarianted facet set. + cfacets = facets.pop((), set()) + + # For each variant combination, remove unvarianted + # facets since they are common to all variants. + for varkey, fnames in facets.items(): + fnames.difference_update(cfacets) + if not fnames: + # No facets unique to this combo; + # discard. + del facets[varkey] + + # If all possible variant combinations supported by the + # package have at least one facet, then the intersection + # of facets for all variants can be merged with the + # common set. + merge_facets = len(facets) > 0 + if merge_facets: + # Determine unique set of variant combinations + # seen for faceted actions. + vcombos = set(( + tuple( + vpair[0] + for vpair in varkey + ) + for varkey in facets + )) + + # For each unique variant combination, determine + # if the cartesian product of all variant values + # supported by the package for the combination + # has been seen. In other words, if the + # combination is ((variant.arch,)) and the + # package supports (i386, sparc), then both + # (variant.arch, i386) and (variant.arch, sparc) + # must exist. This code assumes variant values + # for each variant are already sorted. + for pair in chain.from_iterable( + product(*( + tuple((name, val) + for val in variants[name]) + for name in vcombo) + ) + for vcombo in vcombos + ): + if pair not in facets: + # If any combination the package + # supports has not been seen for + # one or more facets, then some + # facets are unique to one or + # more combinations. + merge_facets = False + break + + if merge_facets: + # Merge the facets common to all variants if safe; + # if we always merged them, then facets only + # used by a single variant (think i386-only or + # sparc-only content) would be seen unvarianted + # (that's bad). + vfacets = facets.values() + vcfacets = vfacets[0].intersection(*vfacets[1:]) + + if vcfacets: + # At least one facet is shared between + # all variant combinations; move the + # common ones to the unvarianted set. + cfacets.update(vcfacets) + + # Remove facets common to all combos. + for varkey, fnames in facets.items(): + fnames.difference_update(vcfacets) + if not fnames: + # No facets unique to + # this combo; discard. + del facets[varkey] + + # Omit the "facet." prefix from attribute values since + # that's implicit and can be added back when the action + # is parsed. + val = sorted(f[6:] for f in cfacets) + if not val: + # If we don't do this, action stringify will + # emit this as "set name=pkg.facet" which is + # then transformed to "set name=name + # value=pkg.facet". Not what we wanted, but is + # expected for historical reasons. + val = "" + + # Always emit an action enumerating the list of facets + # common to all variants, even if there aren't any. + # That way if there are also no variant-specific facets, + # package operations will know that no facets are used + # by the package instead of having to scan the whole + # manifest. + yield "%s\n" % AttributeAction(None, + name="pkg.facet.common", value=val) + + # Now emit a pkg.facet action for each variant + # combination containing the list of facets unique to + # that combination. + for varkey, fnames in facets.iteritems(): + # A unique key for each combination is needed, + # and using a hash obfuscates that interface + # while giving us a reliable way to generate + # a reproducible, unique identifier. The key + # string below looks like this before hashing: + # variant.archi386variant.debug.osnetTrue... + key = hashlib.sha1( + "".join("%s%s" % v for v in varkey) + ).hexdigest() + + # Omit the "facet." prefix from attribute values + # since that's implicit and can be added back + # when the action is parsed. + act = AttributeAction(None, + name="pkg.facet.%s" % key, + value=sorted(f[6:] for f in fnames)) + attrs = act.attrs + # Tag action with variants. + for v in varkey: + attrs[v[0]] = v[1] + yield "%s\n" % act + + # Emit pkg.[c]size attribute for [compressed] size of package + # for each facet/variant combination. + csize = 0 + size = 0 + for varcetkeys in refs: + rcsize = refs[varcetkeys]["csize"] + rsize = refs[varcetkeys]["size"] + + if not varcetkeys: + # For unfaceted/unvarianted actions, keep a + # running total so a single [c]size action can + # be generated. + csize += rcsize + size += rsize + continue + + if emit_sizes and (rcsize > 0 or rsize > 0): + # Only emit if > 0; actions may be + # faceted/variant without payload. + + # A unique key for each combination is needed, + # and using a hash obfuscates that interface + # while giving us a reliable way to generate + # a reproducible, unique identifier. The key + # string below looks like this before hashing: + # facet.docTruevariant.archi386... + key = hashlib.sha1( + "".join("%s%s" % v for v in varcetkeys) + ).hexdigest() + + # The sizes are abbreviated in the name of byte + # conservation. + act = AttributeAction(None, + name="pkg.sizes.%s" % key, + value=["csz=%s" % rcsize, "sz=%s" % rsize]) + attrs = act.attrs + for v in varcetkeys: + attrs[v[0]] = v[1] + yield "%s\n" % act + + if emit_sizes: + act = AttributeAction(None, name="pkg.sizes.common", + value=["csz=%s" % csize, "sz=%s" % size]) + yield "%s\n" % act + def _actions_to_dict(self, references): """create dictionary of all actions referenced explicitly or implicitly from self.actions... include variants as values; collapse variants where possible""" refs = {} - # build a dictionary containing all directories tagged w/ + # build a dictionary containing all actions tagged w/ # variants for a in self.actions: v, f = a.get_varcet_keys() @@ -419,6 +673,117 @@ return list(s) + def gen_facets(self, excludes=EmptyI, patterns=EmptyI): + """A generator function that returns the supported facet + attributes (strings) for this package based on the specified (or + current) excludes that also match at least one of the patterns + provided. Facets must be true or false so a list of possible + facet values is not returned.""" + + if self.excludes == excludes: + excludes = EmptyI + assert excludes == EmptyI or self.excludes == EmptyI + + try: + facets = self["pkg.facet"] + except KeyError: + facets = None + + if facets is not None and excludes == EmptyI: + # No excludes? Then use the pre-determined set of + # facets. + for f in misc.yield_matching("facet.", facets, patterns): + yield f + return + + # If different excludes were specified, then look for pkg.facet + # actions containing the list of facets. + found = False + seen = set() + for a in self.gen_actions_by_type("set", excludes=excludes): + if a.attrs["name"][:10] == "pkg.facet.": + # Either a pkg.facet.common action or a + # pkg.facet.X variant-specific action. + found = True + val = a.attrlist("value") + if len(val) == 1 and val[0] == "": + # No facets. + continue + + for f in misc.yield_matching("facet.", ( + "facet.%s" % n + for n in val + ), patterns): + if f in seen: + # Prevent duplicates; it's + # possible a given facet may be + # valid for more than one unique + # variant combination that's + # allowed by current excludes. + continue + + seen.add(f) + yield f + + if not found: + # Fallback to sifting actions to yield possible. + facets = self._get_varcets(excludes=excludes)[1] + for f in misc.yield_matching("facet.", facets, patterns): + yield f + + def gen_variants(self, excludes=EmptyI, patterns=EmptyI): + """A generator function that yields a list of tuples of the form + (variant, [values]). Where 'variant' is the variant attribute + name (e.g. 'variant.arch') and '[values]' is a list of the + variant values supported by this package. Variants returned are + those allowed by the specified (or current) excludes that also + match at least one of the patterns provided.""" + + if self.excludes == excludes: + excludes = EmptyI + assert excludes == EmptyI or self.excludes == EmptyI + + try: + variants = self["pkg.variant"] + except KeyError: + variants = None + + if variants is not None and excludes == EmptyI: + # No excludes? Then use the pre-determined set of + # variants. + for v in misc.yield_matching("variant.", variants, + patterns): + yield v, self.attributes.get(v, []) + return + + # If different excludes were specified, then look for + # pkg.variant action containing the list of variants. + found = False + variants = defaultdict(set) + for a in self.gen_actions_by_type("set", excludes=excludes): + aname = a.attrs["name"] + if aname == "pkg.variant": + val = a.attrlist("value") + if len(val) == 1 and val[0] == "": + # No variants. + return + for v in val: + found = True + # Ensure variant entries exist (debug + # variants may not) via defaultdict. + variants["variant.%s" % v] + elif aname[:8] == "variant.": + for v in a.attrlist("value"): + found = True + variants[aname].add(v) + + if not found: + # Fallback to sifting actions to get possible. + variants = self._get_varcets(excludes=excludes)[0] + + for v in misc.yield_matching("variant.", variants, patterns): + yield v, variants[v] + def gen_mediators(self, excludes=EmptyI): """A generator function that yields tuples of the form (mediator, mediations) expressing the set of possible mediations for this @@ -592,10 +957,9 @@ self.actions = [] self.actions_bytype = {} - self._variants = None - self._facets = None self.attributes = {} self._cache = {} + self._absent_cache = [] # So we could build up here the type/key_attr dictionaries like # sdict and odict in difference() above, and have that be our @@ -661,12 +1025,6 @@ if excludes and not action.include_this(excludes): return - if self._variants: - # Reset facet/variant cache if needed (if one is set, - # then both are set, so only need to check for one). - self._facets = None - self._variants = None - self.actions.append(action) try: self.actions_bytype[aname].append(action) @@ -681,14 +1039,66 @@ """Fill attribute array w/ set action contents.""" try: keyvalue = action.attrs["name"] - if keyvalue == "fmri": - keyvalue = "pkg.fmri" - if keyvalue not in self.attributes: - self.attributes[keyvalue] = \ - action.attrs["value"] - except KeyError: # ignore broken set actions + if keyvalue[:10] == "pkg.sizes.": + # To reduce manifest bloat, size and csize + # are set on a single action so need splitting + # into separate attributes. + attrval = action.attrlist("value") + for entry in attrval: + szname, szval = entry.split("=", 1) + if szname == "sz": + szname = "pkg.size" + elif szname == "csz": + szname = "pkg.csize" + else: + # Skip unknowns. + continue + + self.attributes.setdefault(szname, 0) + self.attributes[szname] += int(szval) + return + except (KeyError, TypeError, ValueError): + # ignore broken set actions pass + # Ensure facet and variant attributes are always lists. + if keyvalue[:10] == "pkg.facet.": + # Possible facets list is spread over multiple actions. + val = action.attrlist("value") + if len(val) == 1 and val[0] == "": + # No facets. + val = [] + + seen = self.attributes.setdefault("pkg.facet", []) + for f in val: + entry = "facet.%s" % f + if entry not in seen: + # Prevent duplicates; it's possible a + # given facet may be valid for more than + # one unique variant combination that's + # allowed by current excludes. + seen.append(f) + return + elif keyvalue == "pkg.variant": + val = action.attrlist("value") + if len(val) == 1 and val[0] == "": + # No variants. + val = [] + + self.attributes[keyvalue] = [ + "variant.%s" % v + for v in val + ] + return + elif keyvalue[:8] == "variant.": + self.attributes[keyvalue] = action.attrlist("value") + return + + if keyvalue == "fmri": + # Ancient manifest compatibility. + keyvalue = "pkg.fmri" + self.attributes[keyvalue] = action.attrs["value"] + @staticmethod def search_dict(file_path, excludes, return_line=False, log=None): @@ -896,24 +1306,64 @@ "'false'" % ret)) def get_size(self, excludes=EmptyI): - """Returns an integer representing the total size, in bytes, of - the Manifest's data payload. + """Returns an integer tuple of the form (size, csize), where + 'size' represents the total uncompressed size, in bytes, of the + Manifest's data payload, and 'csize' represents the compressed + version of that. - 'excludes' is a list of variants which should be allowed when - calculating the total. - """ + 'excludes' is a list of a list of variants and facets which + should be allowed when calculating the total.""" + if self.excludes == excludes: + excludes = EmptyI + assert excludes == EmptyI or self.excludes == EmptyI + + csize = 0 size = 0 - for a in self.gen_actions(excludes): + + attrs = self.attributes + if ("pkg.size" in attrs and "pkg.csize" in attrs) and \ + (excludes == EmptyI or self.excludes == excludes): + # If specified excludes match loaded excludes, then use + # cached attributes; this is safe as manifest attributes + # are reset or updated every time exclude_content, + # set_content, or add_action is called. + return (attrs["pkg.size"], attrs["pkg.csize"]) + + for a in self.gen_actions(excludes=excludes): size += a.get_size() - return size + csize += misc.get_pkg_otw_size(a) + + if excludes == EmptyI: + # Cache for future calls. + attrs["pkg.size"] = size + attrs["pkg.csize"] = csize + + return (size, csize) - def __load_varcets(self): - """Private helper function to populate list of facets and - variants on-demand.""" + def _get_varcets(self, excludes=EmptyI): + """Private helper function to get list of facets/variants.""" + + variants = defaultdict(set) + facets = defaultdict(set) - self._facets = {} - self._variants = {} + nexcludes = excludes + if nexcludes: + # Facet filtering should never be applied when excluding + # actions; only variant filtering. This is ugly, but + # our current variant/facet filtering system doesn't + # allow you to be selective and various bits in + # pkg.manifest assume you always filter on both so we + # have to fake up a filter for facets. + nexcludes = [ + x for x in excludes + if x.__func__ != facet._allow_facet + ] + # Excludes list must always have zero or two items; so + # fake second entry. + nexcludes.append(lambda x: True) + assert len(nexcludes) == 2 + for action in self.gen_actions(): # append any variants and facets to manifest dict attrs = action.attrs @@ -923,29 +1373,27 @@ continue try: - for v, d in chain( - izip(v_list, repeat(self._variants)), - izip(f_list, repeat(self._facets))): - try: + for v, d in izip(v_list, repeat(variants)): + d[v].add(attrs[v]) + + if not excludes or action.include_this( + nexcludes): + # While variants are package level (you + # can't install a package without + # setting the variant first), facets + # from the current action should only be + # included if the action is not + # excluded. + for v, d in izip(f_list, repeat(facets)): d[v].add(attrs[v]) - except KeyError: - d[v] = set([attrs[v]]) except TypeError: # Lists can't be set elements. raise actions.InvalidActionError(action, _("%(forv)s '%(v)s' specified multiple times") % {"forv": v.split(".", 1)[0], "v": v}) - def __get_facets(self): - if self._facets is None: - self.__load_varcets() - return self._facets - - def __get_variants(self): - if self._variants is None: - self.__load_varcets() - return self._variants - + return (variants, facets) + def __getitem__(self, key): """Return the value for the package attribute 'key'.""" return self.attributes[key] @@ -965,9 +1413,6 @@ def __contains__(self, key): return key in self.attributes - facets = property(lambda self: self.__get_facets()) - variants = property(lambda self: self.__get_variants()) - null = Manifest() class FactoredManifest(Manifest): @@ -1056,8 +1501,6 @@ when downloading new manifests""" self.actions = [] self.actions_bytype = {} - self._variants = None - self._facets = None self.attributes = {} self.loaded = False @@ -1091,24 +1534,47 @@ # Ensure target cache directory and intermediates exist. misc.makedirs(t_dir) - # create per-action type cache; use rename to avoid - # corrupt files if ^C'd in the middle - for n in self.actions_bytype.keys(): + # create per-action type cache; use rename to avoid corrupt + # files if ^C'd in the middle. All action types are considered + # so that empty cache files are created if no action of that + # type exists for the package (avoids full manifest loads + # later). + for n, acts in self.actions_bytype.iteritems(): t_prefix = "manifest.%s." % n - fd, fn = tempfile.mkstemp(dir=t_dir, prefix=t_prefix) - f = os.fdopen(fd, "wb") + try: + fd, fn = tempfile.mkstemp(dir=t_dir, + prefix=t_prefix) + except EnvironmentError, e: + raise apx._convert_error(e) - for a in self.actions_bytype[n]: - f.write("%s\n" % a) - f.close() - os.chmod(fn, PKG_FILE_MODE) - portable.rename(fn, self.__cache_path("manifest.%s" % n)) + f = os.fdopen(fd, "wb") + try: + for a in acts: + f.write("%s\n" % a) + if n == "set": + # Add supplemental action data; yes this + # does mean the cache is not the same as + # retrieved manifest, but that's ok. + # Signature verification is done using + # the raw manifest. + f.writelines(self._gen_attrs_to_str()) + except EnvironmentError, e: + raise apx._convert_error(e) + finally: + f.close() + + try: + os.chmod(fn, PKG_FILE_MODE) + portable.rename(fn, + self.__cache_path("manifest.%s" % n)) + except EnvironmentError, e: + raise apx._convert_error(e) def create_cache(name, refs): try: fd, fn = tempfile.mkstemp(dir=t_dir, - prefix="manifest.dircache.") + prefix=name + ".") with os.fdopen(fd, "wb") as f: f.writelines(refs()) os.chmod(fn, PKG_FILE_MODE) @@ -1215,30 +1681,66 @@ for a in Manifest.gen_actions_by_type(self, atype, excludes): yield a - else: - if excludes == EmptyI: - excludes = self.excludes - assert excludes == self.excludes or \ - self.excludes == EmptyI - # we have a cached copy - use it - mpath = self.__cache_path("manifest.%s" % atype) + return + + if excludes == EmptyI: + excludes = self.excludes + assert excludes == self.excludes or self.excludes == EmptyI - if not os.path.exists(mpath): - return # no such action in this manifest + if atype in self._absent_cache: + # No such action in the manifest; must be done *after* + # asserting excludes are correct to avoid hiding + # failures. + return + # Assume a cached copy exists; if not, tag the action type to + # avoid pointless I/O later. + mpath = self.__cache_path("manifest.%s" % atype) + + try: with open(mpath, "rb") as f: for l in f: a = actions.fromstr(l.rstrip()) if not excludes or \ a.include_this(excludes): yield a + except EnvironmentError, e: + if e.errno == errno.ENOENT: + self._absent_cache.append(atype) + return # no such action in this manifest + raise apx._convert_error(e) - def gen_mediators(self, excludes): + def gen_facets(self, excludes=EmptyI, patterns=EmptyI): + """A generator function that returns the supported facet + attributes (strings) for this package based on the specified (or + current) excludes that also match at least one of the patterns + provided. Facets must be true or false so a list of possible + facet values is not returned.""" + + if not self.loaded and not self.__load_attributes(): + self.__load() + return Manifest.gen_facets(self, excludes=excludes, + patterns=patterns) + + def gen_variants(self, excludes=EmptyI, patterns=EmptyI): + """A generator function that yields a list of tuples of the form + (variant, [values]). Where 'variant' is the variant attribute + name (e.g. 'variant.arch') and '[values]' is a list of the + variant values supported by this package. Variants returned are + those allowed by the specified (or current) excludes that also + match at least one of the patterns provided.""" + + if not self.loaded and not self.__load_attributes(): + self.__load() + return Manifest.gen_variants(self, excludes=excludes, + patterns=patterns) + + def gen_mediators(self, excludes=EmptyI): """A generator function that yields set actions expressing the set of possible mediations for this package. """ self.__load_cached_data("manifest.mediatorcache") - return Manifest.gen_mediators(self, excludes) + return Manifest.gen_mediators(self, excludes=excludes) def __load_attributes(self): """Load attributes dictionary from cached set actions; @@ -1253,8 +1755,21 @@ if not self.excludes or \ a.include_this(self.excludes): self.fill_attributes(a) + return True + def get_size(self, excludes=EmptyI): + """Returns an integer tuple of the form (size, csize), where + 'size' represents the total uncompressed size, in bytes, of the + Manifest's data payload, and 'csize' represents the compressed + version of that. + + 'excludes' is a list of a list of variants and facets which + should be allowed when calculating the total.""" + if not self.loaded and not self.__load_attributes(): + self.__load() + return Manifest.get_size(self, excludes=excludes) + def __getitem__(self, key): if not self.loaded and not self.__load_attributes(): self.__load() diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/misc.py --- a/src/modules/misc.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/misc.py Wed Jun 19 15:54:08 2013 -0700 @@ -32,6 +32,7 @@ import collections import datetime import errno +import fnmatch import getopt import hashlib import itertools @@ -497,8 +498,8 @@ pkg.csize. If that value isn't available, it returns pkg.size. If pkg.size isn't available, return zero.""" - size = action.attrs.get("pkg.csize", 0) - if size == 0: + size = action.attrs.get("pkg.csize") + if size is None: size = action.attrs.get("pkg.size", 0) return int(size) @@ -2428,6 +2429,42 @@ s = s.decode("utf-8", "replace") return s +def yield_matching(pat_prefix, items, patterns): + """Helper function for yielding items that match one of the provided + patterns.""" + + if patterns: + # Normalize patterns and determine whether to glob. + npatterns = [] + for p in patterns: + if pat_prefix: + pat = p.startswith(pat_prefix) and \ + p or (pat_prefix + p) + else: + pat = p + if "*" in p or "?" in p: + pat = re.compile(fnmatch.translate(pat)).match + glob_match = True + else: + glob_match = False + + npatterns.append((pat, glob_match)) + patterns = npatterns + npatterns = None + + for item in items: + for (pat, glob_match) in patterns: + if glob_match: + if pat(item): + break + elif item == pat: + break + else: + if patterns: + continue + # No patterns or matched at least one. + yield item + sigdict = defaultdict(list) diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/server/api.py --- a/src/modules/server/api.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/server/api.py Wed Jun 19 15:54:08 2013 -0700 @@ -327,8 +327,8 @@ states = None links = hardlinks = files = dirs = dependencies = None - summary = size = licenses = cat_info = description = \ - None + summary = csize = size = licenses = cat_info = \ + description = None if cat_opts & info_needed: summary, description, cat_info, dependencies = \ @@ -359,7 +359,8 @@ licenses = self.__licenses(mfst) if PackageInfo.SIZE in info_needed: - size = mfst.get_size(excludes=excludes) + size, csize = mfst.get_size( + excludes=excludes) if act_opts & info_needed: if PackageInfo.LINKS in info_needed: @@ -384,9 +385,10 @@ publisher=pub, version=release, build_release=build_release, branch=branch, packaging_date=packaging_date, size=size, - pfmri=f, licenses=licenses, links=links, - hardlinks=hardlinks, files=files, dirs=dirs, - dependencies=dependencies, description=description)) + csize=csize, pfmri=f, licenses=licenses, + links=links, hardlinks=hardlinks, files=files, + dirs=dirs, dependencies=dependencies, + description=description)) return { self.INFO_FOUND: pis, self.INFO_MISSING: notfound, diff -r bcafb737718d -r 2bb9f6ffdff9 src/modules/server/depot.py --- a/src/modules/server/depot.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/modules/server/depot.py Wed Jun 19 15:54:08 2013 -0700 @@ -70,7 +70,6 @@ import pkg import pkg.actions as actions -import pkg.catalog as catalog import pkg.config as cfg import pkg.fmri as fmri import pkg.indexer as indexer @@ -1325,22 +1324,24 @@ lsummary.seek(0) self.__set_response_expires("info", 86400*365, 86400*365) + size, csize = m.get_size() return """\ - Name: %s - Summary: %s - Publisher: %s - Version: %s - Build Release: %s - Branch: %s -Packaging Date: %s - Size: %s - FMRI: %s + Name: %s + Summary: %s + Publisher: %s + Version: %s + Build Release: %s + Branch: %s + Packaging Date: %s + Size: %s +Compressed Size: %s + FMRI: %s License: %s """ % (name, summary, pub, ver.release, ver.build_release, - ver.branch, ver.get_timestamp().strftime("%c"), - misc.bytes_to_str(m.get_size()), pfmri, lsummary.read()) + ver.branch, ver.get_timestamp().strftime("%c"), misc.bytes_to_str(size), + misc.bytes_to_str(csize), pfmri, lsummary.read()) @cherrypy.tools.response_headers(headers=[( "Content-Type", p5i.MIME_TYPE)]) diff -r bcafb737718d -r 2bb9f6ffdff9 src/pkgdep.py --- a/src/pkgdep.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/pkgdep.py Wed Jun 19 15:54:08 2013 -0700 @@ -43,7 +43,7 @@ import pkg.publish.dependencies as dependencies from pkg.misc import msg, emsg, PipeError -CLIENT_API_VERSION = 73 +CLIENT_API_VERSION = 75 PKG_CLIENT_NAME = "pkgdepend" DEFAULT_SUFFIX = ".res" diff -r bcafb737718d -r 2bb9f6ffdff9 src/sysrepo.py --- a/src/sysrepo.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/sysrepo.py Wed Jun 19 15:54:08 2013 -0700 @@ -59,7 +59,7 @@ orig_cwd = None PKG_CLIENT_NAME = "pkg.sysrepo" -CLIENT_API_VERSION = 73 +CLIENT_API_VERSION = 75 pkg.client.global_settings.client_name = PKG_CLIENT_NAME # exit codes diff -r bcafb737718d -r 2bb9f6ffdff9 src/tests/cli/t_change_facet.py --- a/src/tests/cli/t_change_facet.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/tests/cli/t_change_facet.py Wed Jun 19 15:54:08 2013 -0700 @@ -20,7 +20,7 @@ # CDDL HEADER END # -# Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved. import testutils if __name__ == "__main__": @@ -122,7 +122,7 @@ self.pkg_image_create(self.rurl, additional_args=ic_args) self.pkg("facet") - self.pkg("facet -H 'facet.locale*' | egrep False") + self.pkg("facet -H -F tsv 'facet.locale*' | egrep False") # install a package and verify alist = [self.plist[0]] @@ -145,7 +145,7 @@ # are in effect self.pkg("change-facet -n --parsable=0 wombat=false") self.assertEqualParsable(self.output, - affect_packages=alist, + affect_packages=[], change_facets=[["facet.wombat", False]]) # Again, but this time after removing the publisher cache data @@ -157,7 +157,7 @@ self.pkg("change-facet --no-refresh -n --parsable=0 " "wombat=false", su_wrap=True) self.assertEqualParsable(self.output, - affect_packages=alist, + affect_packages=[], change_facets=[["facet.wombat", False]]) # Again, but this time after removing the cache directory @@ -168,7 +168,7 @@ self.pkg("change-facet --no-refresh -n --parsable=0 " "wombat=false", su_wrap=True) self.assertEqualParsable(self.output, - affect_packages=alist, + affect_packages=[], change_facets=[["facet.wombat", False]]) # change to pick up another file w/ two tags and test the @@ -252,8 +252,8 @@ # Test that setting a non-existent facet to True then removing # it works. self.pkg("change-facet -v foo=True") - self.pkg("facet -H") - self.assertEqual("facet.foo True\n", self.output) + self.pkg("facet -H -F tsv") + self.assertEqual("facet.foo\tTrue\n", self.output) self.pkg("change-facet --parsable=0 foo=None") self.assertEqualParsable(self.output, change_facets=[ ["facet.foo", None]]) @@ -291,12 +291,11 @@ # install a random package and make sure we don't accidentally # change facets. self.pkg("install pkg_B@1.0") - self.pkg("facet -H") - output = self.reduceSpaces(self.output) + self.pkg("facet -H -F tsv") expected = ( - "facet.locale.fr_FR False\n" - "facet.locale.fr False\n") - self.assertEqualDiff(expected, output) + "facet.locale.fr\tFalse\n" + "facet.locale.fr_FR\tFalse\n") + self.assertEqualDiff(expected, self.output) for i in [ 0, 3, 4, 5, 6, 7 ]: self.assert_file_is_there(str(i)) for i in [ 1, 2 ]: @@ -306,12 +305,11 @@ # update an image and make sure we don't accidentally change # facets. self.pkg("update") - self.pkg("facet -H") - output = self.reduceSpaces(self.output) + self.pkg("facet -H -F tsv") expected = ( - "facet.locale.fr_FR False\n" - "facet.locale.fr False\n") - self.assertEqualDiff(expected, output) + "facet.locale.fr\tFalse\n" + "facet.locale.fr_FR\tFalse\n") + self.assertEqualDiff(expected, self.output) for i in [ 0, 3, 4, 5, 6, 7 ]: self.assert_file_is_there(str(i)) for i in [ 1, 2 ]: @@ -333,10 +331,10 @@ # set a facet on an image with no facets self.pkg("change-facet -v locale.fr=False") - self.pkg("facet -H") + self.pkg("facet -H -F tsv") output = self.reduceSpaces(self.output) expected = ( - "facet.locale.fr False\n") + "facet.locale.fr\tFalse\n") self.assertEqualDiff(expected, output) for i in [ 0, 2, 3, 4, 5, 6, 7 ]: self.assert_file_is_there(str(i)) @@ -346,12 +344,11 @@ # set a facet on an image with existing facets self.pkg("change-facet -v locale.fr_FR=False") - self.pkg("facet -H") - output = self.reduceSpaces(self.output) + self.pkg("facet -H -F tsv") expected = ( - "facet.locale.fr_FR False\n" - "facet.locale.fr False\n") - self.assertEqualDiff(expected, output) + "facet.locale.fr\tFalse\n" + "facet.locale.fr_FR\tFalse\n") + self.assertEqualDiff(expected, self.output) for i in [ 0, 3, 4, 5, 6, 7 ]: self.assert_file_is_there(str(i)) for i in [ 1, 2 ]: @@ -361,11 +358,11 @@ # clear a facet while setting a facet on an image with other # facets that aren't being changed self.pkg("change-facet -v locale.fr=None locale.nl=False") - self.pkg("facet -H") + self.pkg("facet -H -F tsv") output = self.reduceSpaces(self.output) expected = ( - "facet.locale.fr_FR False\n" - "facet.locale.nl False\n") + "facet.locale.fr_FR\tFalse\n" + "facet.locale.nl\tFalse\n") self.assertEqualDiff(expected, output) for i in [ 0, 1, 3, 4, 6, 7 ]: self.assert_file_is_there(str(i)) @@ -376,10 +373,10 @@ # clear a facet on an image with other facets that aren't # being changed self.pkg("change-facet -v locale.nl=None") - self.pkg("facet -H") + self.pkg("facet -H -F tsv") output = self.reduceSpaces(self.output) expected = ( - "facet.locale.fr_FR False\n") + "facet.locale.fr_FR\tFalse\n") self.assertEqualDiff(expected, output) for i in [ 0, 1, 3, 4, 5, 6, 7 ]: self.assert_file_is_there(str(i)) @@ -389,7 +386,7 @@ # clear the only facet on an image self.pkg("change-facet -v locale.fr_FR=None") - self.pkg("facet -H") + self.pkg("facet -H -F tsv") self.assertEqualDiff("", self.output) for i in range(8): self.assert_file_is_there(str(i)) diff -r bcafb737718d -r 2bb9f6ffdff9 src/tests/cli/t_change_variant.py --- a/src/tests/cli/t_change_variant.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/tests/cli/t_change_variant.py Wed Jun 19 15:54:08 2013 -0700 @@ -20,7 +20,7 @@ # CDDL HEADER END # -# Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved. import testutils if __name__ == "__main__": @@ -32,6 +32,7 @@ import re import unittest +import pkg.misc as misc from pkg.client.pkgdefs import * class TestPkgChangeVariant(pkg5unittest.SingleDepotTestCase): @@ -69,8 +70,19 @@ add file tmp/pkg_shared/shared/nonglobal_motd mode=0555 owner=root group=bin path=shared/zone_motd variant.opensolaris.zone=nonglobal add file tmp/pkg_shared/unique/global mode=0555 owner=root group=bin path=unique/global variant.opensolaris.zone=global add file tmp/pkg_shared/unique/nonglobal mode=0555 owner=root group=bin path=unique/nonglobal variant.opensolaris.zone=nonglobal + close""" - close""" + pkg_unknown = """ + open unknown@1.0 + add set name=variant.unknown value=bar value=foo + add file tmp/bar path=usr/bin/bar mode=0755 owner=root group=root variant.unknown=bar + add file tmp/foo path=usr/bin/foo mode=0755 owner=root group=root variant.unknown=foo + close + open unknown@2.0 + add set name=variant.unknown value=bar value=foo + add file tmp/bar path=usr/bin/foobar mode=0755 owner=root group=root variant.unknown=bar + add file tmp/foo path=usr/bin/foobar mode=0755 owner=root group=root variant.unknown=foo + close """ # this package intentionally has no variant.arch specification. pkg_inc = """ @@ -109,7 +121,10 @@ "tmp/pkg_shared/shared/global_motd", "tmp/pkg_shared/shared/nonglobal_motd", "tmp/pkg_shared/unique/global", - "tmp/pkg_shared/unique/nonglobal" + "tmp/pkg_shared/unique/nonglobal", + + "tmp/bar", + "tmp/foo" ] def setUp(self): @@ -117,7 +132,8 @@ self.make_misc_files(self.misc_files) self.pkgsend_bulk(self.rurl, (self.pkg_i386, self.pkg_sparc, - self.pkg_shared, self.pkg_inc, self.pkg_cluster)) + self.pkg_shared, self.pkg_inc, self.pkg_cluster, + self.pkg_unknown)) # verify pkg search indexes self.verify_search = True @@ -125,6 +141,16 @@ # verify installed images before changing variants self.verify_install = False + def __assert_variant_matches_tsv(self, expected, errout=None, + exit=0, opts=misc.EmptyI, names=misc.EmptyI, su_wrap=False): + self.pkg("variant %s -H -F tsv %s" % (" ".join(opts), + " ".join(names)), exit=exit, su_wrap=su_wrap) + self.assertEqualDiff(expected, self.output) + if errout: + self.assert_(self.errout != "") + else: + self.assertEqualDiff("", self.errout) + def f_verify(self, path, token=None, negate=False): """Verify that the specified path exists and contains the specified token. If negate is true, then make sure @@ -303,8 +329,12 @@ "variant.opensolaris.zone": v_zone } self.image_create(self.rurl, variants=variants) - self.pkg("variant -H| egrep %s" % ("'variant.arch[ ]*%s'" % v_arch)) - self.pkg("variant -H| egrep %s" % ("'variant.opensolaris.zone[ ]*%s'" % v_zone)) + + exp_tsv = """\ +variant.arch\t%s +variant.opensolaris.zone\t%s +""" % (v_arch, v_zone) + self.__assert_variant_matches_tsv(exp_tsv) # install the specified packages into the image ii_args = "" @@ -325,8 +355,11 @@ # verify the updated image self.i_verify(v_arch2, v_zone2, pl2) - self.pkg("variant -H| egrep %s" % ("'variant.arch[ ]*%s'" % v_arch2)) - self.pkg("variant -H| egrep %s" % ("'variant.opensolaris.zone[ ]*%s'" % v_zone2)) + exp_tsv = """\ +variant.arch\t%s +variant.opensolaris.zone\t%s +""" % (v_arch2, v_zone2) + self.__assert_variant_matches_tsv(exp_tsv) self.image_destroy() @@ -428,6 +461,44 @@ self.cv_test("sparc", "global", ["pkg_cluster"], "i386", "nonglobal", ["pkg_cluster"]) + def test_cv_12_unknown(self): + """Ensure that packages with an unknown variant and + non-conflicting content can be installed and subsequently + altered using change-variant.""" + + self.image_create(self.rurl) + + # Install package with unknown variant and verify both files are + # present. + self.pkg("install -v unknown@1.0") + for fname in ("bar", "foo"): + self.f_verify("usr/bin/%s" % fname, fname) + + # Next, verify upgrade to version of package with unknown + # variant fails if new version delivers conflicting content and + # variant has not been set. + self.pkg("update -vvv unknown@2.0", exit=1) + + # Next, set unknown variant explicitly and verify content + # changes as expected. + self.pkg("change-variant unknown=foo") + + # Verify bar no longer exists... + self.f_verify("usr/bin/bar", "bar", negate=True) + # ...and foo still does. + self.f_verify("usr/bin/foo", "foo") + + # Next, upgrade to version of package with conflicting content + # and verify content changes as expected. + self.pkg("update -vvv unknown@2.0") + + # Verify bar and foo no longer exist... + for fname in ("bar", "foo"): + self.f_verify("usr/bin/%s" % fname, fname, negate=True) + + # ...and that foo variant of foobar is now installed. + self.f_verify("usr/bin/foobar", "foo") + def test_cv_parsable(self): """Test the parsable output of change-variant.""" diff -r bcafb737718d -r 2bb9f6ffdff9 src/tests/cli/t_pkg_install.py --- a/src/tests/cli/t_pkg_install.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/tests/cli/t_pkg_install.py Wed Jun 19 15:54:08 2013 -0700 @@ -8472,8 +8472,8 @@ "tripledupfilec") self.pkg("-D broken-conflicting-action-handling=1 install " "tripledupfilea") - self.pkg("change-variant variant.foo=two") - self.pkg("change-variant variant.foo=one", exit=1) + self.pkg("change-variant -vvv variant.foo=two") + self.pkg("change-variant -vvv variant.foo=one", exit=1) def dir_exists(self, path, mode=None, owner=None, group=None): dir_path = os.path.join(self.get_img_path(), path) diff -r bcafb737718d -r 2bb9f6ffdff9 src/tests/cli/t_pkg_varcet.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/tests/cli/t_pkg_varcet.py Wed Jun 19 15:54:08 2013 -0700 @@ -0,0 +1,591 @@ +#!/usr/bin/python +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + +import testutils +if __name__ == "__main__": + testutils.setup_environment("../../../proto") +import pkg5unittest + +import os +import pkg.fmri as fmri +import pkg.portable as portable +import pkg.misc as misc +import pkg.p5p +import shutil +import stat +import tempfile +import unittest + + +class TestPkgVarcet(pkg5unittest.SingleDepotTestCase): + + # Don't discard repository or setUp() every test. + persistent_setup = True + + pkg_foo = """ + open foo@1.0 + add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root + add file tmp/man path=usr/man/man1/foo.1 mode=0444 owner=root group=root + close + open foo@2.0 + add set name=variant.icecream value=neapolitan value=strawberry + add file tmp/debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=true + add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=false + add file tmp/neapolitan path=etc/icecream mode=0644 owner=root group=root variant.icecream=neapolitan + add file tmp/strawberry path=etc/icecream mode=0644 owner=root group=root variant.icecream=strawberry + add file tmp/man path=usr/man/man1/foo.1 mode=0444 owner=root group=root facet.doc.man=true + add file tmp/doc path=usr/share/foo/README mode=0444 owner=root group=root facet.doc.txt=true + add file tmp/pdf path=usr/share/foo/README.pdf mode=0444 owner=root group=root facet.doc.pdf=true + open foo@3.0 + add set name=pkg.facet value=doc.man value=doc.txt value=doc.pdf + add set name=pkg.variant value=icecream value=debug.foo + add set name=variant.icecream value=neapolitan value=strawberry + add file tmp/debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=true + add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=false + add file tmp/neapolitan path=etc/icecream mode=0644 owner=root group=root variant.icecream=neapolitan + add file tmp/strawberry path=etc/icecream mode=0644 owner=root group=root variant.icecream=strawberry + add file tmp/man path=usr/man/man1/foo.1 mode=0444 owner=root group=root facet.doc.man=true + add file tmp/doc path=usr/share/foo/README mode=0444 owner=root group=root facet.doc.txt=true + add file tmp/pdf path=usr/share/foo/README.pdf mode=0444 owner=root group=root facet.doc.pdf=true + close """ + + pkg_unknown = """ + open unknown@1.0 + add set name=variant.unknown value=bar value=foo + add file tmp/non-debug path=usr/bin/bar mode=0755 owner=root group=root variant.unknown=bar + add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root variant.unknown=foo + close """ + + misc_files = ["tmp/debug", "tmp/non-debug", "tmp/neapolitan", + "tmp/strawberry", "tmp/doc", "tmp/man", "tmp/pdf"] + + def setUp(self): + pkg5unittest.SingleDepotTestCase.setUp(self) + self.make_misc_files(self.misc_files) + self.pkgsend_bulk(self.rurl, [ + getattr(self, p) + for p in dir(self) + if p.startswith("pkg_") and isinstance(getattr(self, p), + basestring) + ]) + + def __assert_varcet_matches_default(self, cmd, expected, errout=None, + exit=0, opts=misc.EmptyI, names=misc.EmptyI, su_wrap=False): + if errout is None and exit != 0: + # Assume there should be error output for non-zero exit + # if not explicitly indicated. + errout = True + + self.pkg("%s %s -H %s" % (cmd, " ".join(opts), " ".join(names)), + exit=exit, su_wrap=su_wrap) + self.assertEqualDiff(expected, self.reduceSpaces(self.output)) + if errout: + self.assert_(self.errout != "") + else: + self.assertEqualDiff("", self.errout) + + def __assert_varcet_matches_tsv(self, cmd, expected, errout=None, + exit=0, opts=misc.EmptyI, names=misc.EmptyI, su_wrap=False): + self.pkg("%s %s -H -F tsv %s" % (cmd, " ".join(opts), + " ".join(names)), exit=exit, su_wrap=su_wrap) + self.assertEqualDiff(expected, self.output) + if errout: + self.assert_(self.errout != "") + else: + self.assertEqualDiff("", self.errout) + + def __assert_varcet_fails(self, cmd, operands, errout=True, exit=1, + su_wrap=False): + self.pkg("%s %s" % (cmd, operands), exit=exit, su_wrap=su_wrap) + if errout: + self.assert_(self.errout != "") + else: + self.assertEqualDiff("", self.errout) + + def __assert_facet_matches_default(self, *args, **kwargs): + self.__assert_varcet_matches_default("facet", *args, + **kwargs) + + def __assert_facet_matches_tsv(self, *args, **kwargs): + self.__assert_varcet_matches_tsv("facet", *args, + **kwargs) + + def __assert_facet_matches(self, exp_def, **kwargs): + exp_tsv = exp_def.replace(" ", "\t") + self.__assert_varcet_matches_default("facet", exp_def, + **kwargs) + self.__assert_varcet_matches_tsv("facet", exp_tsv, + **kwargs) + + def __assert_facet_fails(self, *args, **kwargs): + self.__assert_varcet_fails("facet", *args, **kwargs) + + def __assert_variant_matches_default(self, *args, **kwargs): + self.__assert_varcet_matches_default("variant", *args, + **kwargs) + + def __assert_variant_matches_tsv(self, *args, **kwargs): + self.__assert_varcet_matches_tsv("variant", *args, + **kwargs) + + def __assert_variant_matches(self, exp_def, **kwargs): + exp_tsv = exp_def.replace(" ", "\t") + self.__assert_varcet_matches_default("variant", exp_def, + **kwargs) + self.__assert_varcet_matches_tsv("variant", exp_tsv, + **kwargs) + + def __assert_variant_fails(self, *args, **kwargs): + self.__assert_varcet_fails("variant", *args, **kwargs) + + def __test_foo_facet_upgrade(self, pkg): + # + # Next, verify output after upgrading package to faceted + # version. + # + self.pkg("update %s" % pkg) + + # Verify output for no options and no patterns. + exp_def = """\ +facet.doc.* False +facet.doc.html False +facet.doc.man False +facet.doc.txt True +""" + self.__assert_facet_matches(exp_def) + + # Unmatched because facet is not explicitly set. + self.__assert_facet_fails("doc.pdf") + self.__assert_facet_fails("'*pdf'") + + # Matched case for explicitly set. + exp_def = """\ +facet.doc.* False +facet.doc.txt True +""" + names = ("'facet.doc.[*]'", "doc.txt") + self.__assert_facet_matches(exp_def, names=names) + + # Verify -a output. + exp_def = """\ +facet.doc.* False +facet.doc.html False +facet.doc.man False +facet.doc.pdf False +facet.doc.txt True +""" + opts = ("-a",) + self.__assert_facet_matches(exp_def, opts=opts) + + # Matched case for explicitly set and those in packages. + exp_def = """\ +facet.doc.* False +facet.doc.pdf False +facet.doc.txt True +""" + names = ("'facet.doc.[*]'", "*pdf", "facet.doc.txt") + opts = ("-a",) + self.__assert_facet_matches(exp_def, opts=opts, names=names) + + # Verify -i output. + exp_def = """\ +facet.doc.man False +facet.doc.pdf False +facet.doc.txt True +""" + opts = ("-i",) + self.__assert_facet_matches(exp_def, opts=opts) + + # Unmatched because facet is not used in package. + self.__assert_facet_fails("-i doc.html") + self.__assert_facet_fails("-i '*html'") + + # Matched case in packages. + exp_def = """\ +facet.doc.man False +facet.doc.pdf False +""" + names = ("'facet.*[!t]'",) + opts = ("-i",) + self.__assert_facet_matches(exp_def, opts=opts, names=names) + + exp_def = """\ +facet.doc.pdf False +""" + names = ("'*pdf'",) + opts = ("-i",) + self.__assert_facet_matches(exp_def, opts=opts, names=names) + + # Now uninstall package and verify output (to ensure any + # potentially cached information has been updated). + self.pkg("uninstall foo") + + exp_def = """\ +facet.doc.* False +facet.doc.html False +facet.doc.man False +facet.doc.txt True +""" + + # Output should be the same for both -a and default cases with + # no packages installed. + for opts in ((), ("-a",)): + self.__assert_facet_matches(exp_def, opts=opts) + + # No output expected for -i. + opts = ("-i",) + self.__assert_facet_matches("", opts=opts) + + def test_00_facet(self): + """Verify facet subcommand works as expected.""" + + # create an image + variants = { "variant.icecream": "strawberry" } + self.image_create(self.rurl, variants=variants) + + # Verify invalid options handled gracefully. + self.__assert_facet_fails("-z", exit=2) + self.__assert_facet_fails("-fi", exit=2) + + # + # First, verify output before setting any facets or installing + # any packages. + # + + # Output should be the same for all cases with no facets set and + # no packages installed. + for opts in ((), ("-i",), ("-a",)): + # No operands specified case. + self.__assert_facet_matches("", opts=opts) + + # Unprivileged user case. + self.__assert_facet_matches("", opts=opts, su_wrap=True) + + # Unmatched case. + self.__assert_facet_matches_default("", opts=opts, + names=("bogus",), exit=1) + + # Unmatched case tsv; subtly different as no error + # output is expected. + self.__assert_facet_matches_tsv("", opts=opts, + names=("bogus",), exit=1, errout=False) + + # + # Next, verify output after setting facets. + # + + # Set some facets. + self.pkg("change-facet 'doc.*=False' doc.man=False " + "facet.doc.html=False facet.doc.txt=True") + + exp_def = """\ +facet.doc.* False +facet.doc.html False +facet.doc.man False +facet.doc.txt True +""" + + # Output should be the same for both -a and default cases with + # no packages installed. + for opts in ((), ("-a",)): + self.__assert_facet_matches(exp_def, opts=opts) + + # + # Next, verify output after installing unfaceted package. + # + self.pkg("install foo@1.0") + + # Verify output for no options and no patterns. + exp_def = """\ +facet.doc.* False +facet.doc.html False +facet.doc.man False +facet.doc.txt True +""" + self.__assert_facet_matches(exp_def) + + # Verify -a output. + opts = ("-a",) + self.__assert_facet_matches(exp_def, opts=opts) + + # Verify -i output. + opts = ("-i",) + self.__assert_facet_matches("", opts=opts) + + # Test upgraded package that does not declare all + # facets/variants. + self.__test_foo_facet_upgrade("foo@2.0") + + # Reinstall and then retest with upgraded package that declares + # all facets/variants. + self.pkg("install foo@1.0") + self.__test_foo_facet_upgrade("foo@3.0") + + def __test_foo_variant_upgrade(self, pkg, variants): + # + # Next, verify output after upgrading package to varianted + # version. + # + self.pkg("update %s" % pkg) + + # Verify output for no options and no patterns. + exp_def = """\ +variant.arch %(variant.arch)s +variant.icecream strawberry +variant.opensolaris.zone global +""" % variants + self.__assert_variant_matches(exp_def) + + # Unmatched because variant is not explicitly set. + self.__assert_variant_fails("debug.foo") + self.__assert_variant_fails("'*foo'") + + # Matched case for explicitly set. + exp_def = """\ +variant.arch %(variant.arch)s +variant.opensolaris.zone global +""" % variants + names = ("arch", "'variant.*zone'") + self.__assert_variant_matches(exp_def, names=names) + + # Verify -a output. + exp_def = """\ +variant.arch %(variant.arch)s +variant.debug.foo false +variant.icecream strawberry +variant.opensolaris.zone global +""" % variants + opts = ("-a",) + self.__assert_variant_matches(exp_def, opts=opts) + + # Matched case for explicitly set and those in packages. + exp_def = """\ +variant.arch %(variant.arch)s +variant.debug.foo false +variant.opensolaris.zone global +""" % variants + names = ("'variant.debug.*'", "arch", "'*zone'") + opts = ("-a",) + self.__assert_variant_matches(exp_def, opts=opts, names=names) + + # Verify -i output. + exp_def = """\ +variant.debug.foo false +variant.icecream strawberry +""" % variants + opts = ("-i",) + self.__assert_variant_matches(exp_def, opts=opts) + + # Unmatched because variant is not used in package. + self.__assert_variant_fails("-i opensolaris.zone") + self.__assert_variant_fails("-i '*arch'") + + # Verify -v and -av output. + exp_def = """\ +variant.debug.foo false +variant.debug.foo true +variant.icecream neapolitan +variant.icecream strawberry +""" + for opts in (("-v",), ("-av",)): + self.__assert_variant_matches(exp_def, opts=opts) + + exp_def = """\ +variant.icecream neapolitan +variant.icecream strawberry +""" % variants + names = ("'ice*'",) + opts = ("-av",) + self.__assert_variant_matches(exp_def, opts=opts, names=names) + + # Matched case in packages. + exp_def = """\ +variant.icecream strawberry +""" % variants + names = ("'variant.*[!o]'",) + opts = ("-i",) + self.__assert_variant_matches(exp_def, opts=opts, names=names) + + exp_def = """\ +variant.debug.foo false +""" % variants + names = ("*foo",) + opts = ("-i",) + self.__assert_variant_matches(exp_def, opts=opts, names=names) + + # Now uninstall package and verify output (to ensure any + # potentially cached information has been updated). + self.pkg("uninstall foo") + + exp_def = """\ +variant.arch %(variant.arch)s +variant.icecream strawberry +variant.opensolaris.zone global +""" % variants + + # Output should be the same for both -a and default cases with + # no packages installed. + for opts in ((), ("-a",)): + self.__assert_variant_matches(exp_def, opts=opts) + + # No output expected for -v, -av, -i, or -iv. + for opts in (("-v",), ("-av",), ("-i",), ("-iv",)): + self.__assert_variant_matches("", opts=opts) + + def test_01_variant(self): + """Verify variant subcommand works as expected.""" + + # create an image + self.image_create(self.rurl) + + # Get variant data. + api_obj = self.get_img_api_obj() + variants = dict(v[:-1] for v in api_obj.gen_variants( + api_obj.VARIANT_IMAGE)) + + # Verify invalid options handled gracefully. + self.__assert_variant_fails("-z", exit=2) + self.__assert_variant_fails("-ai", exit=2) + self.__assert_variant_fails("-aiv", exit=2) + + # + # First, verify output before setting any variants or installing + # any packages. + # + + # Output should be the same for -a and default cases with no + # variants set and no packages installed. + exp_def = """\ +variant.arch %(variant.arch)s +variant.opensolaris.zone global +""" % variants + + for opts in ((), ("-a",)): + # No operands specified case. + self.__assert_variant_matches(exp_def, opts=opts) + + # Unprivileged user case. + self.__assert_variant_matches(exp_def, opts=opts, + su_wrap=True) + + # Unmatched case. + self.__assert_variant_matches_default("", opts=opts, + names=("bogus",), exit=1) + + # Unmatched case tsv; subtly different as no error + # output is expected. + self.__assert_variant_matches_tsv("", opts=opts, + names=("bogus",), exit=1, errout=False) + + # No output expected for with no variants set and no packages + # installed for -v, -av, -i, and -iv. + for opts in (("-v",), ("-av",), ("-i",), ("-iv",)): + self.__assert_variant_matches("", opts=opts) + + # + # Next, verify output after setting variants. + # + + # Set some variants. + self.pkg("change-variant variant.icecream=strawberry") + + exp_def = """\ +variant.arch %(variant.arch)s +variant.icecream strawberry +variant.opensolaris.zone global +""" % variants + + # Output should be the same for both -a and default cases with + # no packages installed. + for opts in ((), ("-a",)): + self.__assert_variant_matches(exp_def, opts=opts) + + # + # Next, verify output after installing unvarianted package. + # + self.pkg("install foo@1.0") + + # Verify output for no options and no patterns. + exp_def = """\ +variant.arch %(variant.arch)s +variant.icecream strawberry +variant.opensolaris.zone global +""" % variants + self.__assert_variant_matches(exp_def) + + # Verify -a output. + opts = ("-a",) + self.__assert_variant_matches(exp_def, opts=opts) + + # Verify -v, -av, -i, and -iv output. + for opts in (("-v",), ("-av",), ("-i",), ("-iv",)): + self.__assert_variant_matches("", opts=opts) + + # Test upgraded package that does not declare all + # facets/variants. + self.__test_foo_variant_upgrade("foo@2.0", variants) + + # Reinstall and then retest with upgraded package that declares + # all facets/variants. + self.pkg("install foo@1.0") + self.__test_foo_variant_upgrade("foo@3.0", variants) + + # Next, verify output after installing package with unknown + # variant. + self.pkg("install unknown@1.0") + + # Verify output for no options and no patterns. + exp_def = """\ +variant.arch %(variant.arch)s +variant.icecream strawberry +variant.opensolaris.zone global +""" % variants + self.__assert_variant_matches(exp_def) + + # Verify -a output. + exp_def = """\ +variant.arch %(variant.arch)s +variant.icecream strawberry +variant.opensolaris.zone global +variant.unknown +""" % variants + self.__assert_variant_matches(exp_def, opts=("-a",)) + + # Verify -i output. + exp_def = """\ +variant.unknown +""" % variants + self.__assert_variant_matches(exp_def, opts=("-i",)) + + # Verify -v, -av, and -iv output. + for opts in (("-v",), ("-av",), ("-iv",)): + exp_def = """\ +variant.unknown bar +variant.unknown foo +""" % variants + self.__assert_variant_matches(exp_def, opts=opts) + + +if __name__ == "__main__": + unittest.main() diff -r bcafb737718d -r 2bb9f6ffdff9 src/tests/cli/t_pkgdep_resolve.py --- a/src/tests/cli/t_pkgdep_resolve.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/tests/cli/t_pkgdep_resolve.py Wed Jun 19 15:54:08 2013 -0700 @@ -1116,7 +1116,9 @@ "\n".join( [str(d) for d in pkg_deps[col_path]])) d = pkg_deps[one_dep][0] - self.assertEqual(d.attrs["fmri"], exp_pkg) + self.assertEqual(d.attrs["fmri"], exp_pkg, + "Expected dependency %s; found %s." % (exp_pkg, + d.attrs["fmri"])) col_path = self.make_manifest(self.collision_manf) col_path_num_var = self.make_manifest( diff -r bcafb737718d -r 2bb9f6ffdff9 src/tests/pkg5unittest.py --- a/src/tests/pkg5unittest.py Wed Jun 12 15:44:50 2013 -0700 +++ b/src/tests/pkg5unittest.py Wed Jun 19 15:54:08 2013 -0700 @@ -132,7 +132,7 @@ # Version test suite is known to work with. PKG_CLIENT_NAME = "pkg" -CLIENT_API_VERSION = 73 +CLIENT_API_VERSION = 75 ELIDABLE_ERRORS = [ TestSkippedException, depotcontroller.DepotStateException ]