src/util/publish/pkgmogrify.py
author Tim Foster <tim.s.foster@oracle.com>
Fri, 02 Nov 2012 11:42:49 +1300
changeset 2826 cae308eb6426
parent 2790 ad2a0669d661
child 3143 f6fac0617411
child 3230 30355eee0c43
permissions -rwxr-xr-x
7185135 i18n messages should use parameterized values 7188804 pkg5 should depend on gnu-gettext 7194773 pkg.mo should move to pkg:/package/pkg

#!/usr/bin/python2.6
#
# 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) 2009, 2012, Oracle and/or its affiliates. All rights reserved.
#

import getopt
import gettext
import locale
import os
import re
import shlex
import sys
import traceback
import warnings

import pkg.actions
import pkg.misc as misc
from pkg.misc import PipeError

macros  = {}
includes = []
appends  = []
transforms = []
printinfo = []


def usage(errmsg="", exitcode=2):
        """Emit a usage message and optionally prefix it with a more specific
        error message.  Causes program to exit."""

        if errmsg:
                print >> sys.stderr, "pkgmogrify: %s" % errmsg

        print _("""\
Usage:
        pkgmogrify [-vi] [-I includedir ...] [-D macro=value ...]
            [-O outputfile] [-P printfile] [inputfile ...]""")
        sys.exit(exitcode)

def add_transform(transform, filename, lineno):
        """This routine adds a transform tuple to the list used
        to process actions."""

        # strip off transform
        s = transform[10:]
        # make error messages familiar
        transform = "<" + transform + ">"

        try:
                index = s.index("->")
        except ValueError:
                raise RuntimeError, _("Missing -> in transform")
        matching = s[0:index].strip().split()
        types = [a for a in matching if "=" not in a]
        attrdict = pkg.actions.attrsfromstr(" ".join([a for a in matching if "=" in a]))

        for a in attrdict:
                try:
                        attrdict[a] = re.compile(attrdict[a])
                except re.error, e:
                        raise RuntimeError, \
                            _("transform (%(transform)s) has regexp error "
                            "(%(err)s) in matching clause"
                            ) % {"transform": transform, "err": e}

        op = s[index+2:].strip().split(None, 1)

        # use closures to encapsulate desired operation

        if op[0] == "drop":
                if len(op) > 1:
                        raise RuntimeError, \
                            _("transform (%s) has 'drop' operation syntax error"
                            ) % transform
                operation = lambda a, m, p, f, l: None

        elif op[0] == "set":
                try:
                        attr, value = shlex.split(op[1])
                except ValueError:
                        raise RuntimeError, \
                            _("transform (%s) has 'set' operation syntax error"
                            ) % transform
                def set_func(action, matches, pkg_attrs, filename, lineno):
                        newattr = substitute_values(attr, action, matches,
                            pkg_attrs, filename, lineno)
                        newval = substitute_values(value, action, matches,
                            pkg_attrs, filename, lineno)
                        if newattr == "action.hash":
                                if hasattr(action, "hash"):
                                        action.hash = newval
                        else:
                                action.attrs[newattr] = newval
                        return action
                operation = set_func

        elif op[0] == "default":
                try:
                        attr, value = shlex.split(op[1])
                except ValueError:
                        raise RuntimeError, \
                            _("transform (%s) has 'default' operation syntax error"
                            ) % transform

                def default_func(action, matches, pkg_attrs, filename, lineno):
                        newattr = substitute_values(attr, action, matches,
                            pkg_attrs, filename, lineno)
                        if newattr not in action.attrs:
                                newval = substitute_values(value, action,
                                    matches, pkg_attrs, filename, lineno)
                                action.attrs[newattr] = newval
                        return action
                operation = default_func

        elif op[0] == "abort":
                if len(op) > 1:
                        raise RuntimeError, _("transform (%s) has 'abort' "
                            "operation syntax error") % transform

                def abort_func(action, matches, pkg_attrs, filename, lineno):
                        sys.exit(0)

                operation = abort_func

        elif op[0] == "exit":
                exitval = 0
                msg = None

                if len(op) == 2:
                        args = op[1].split(None, 1)
                        try:
                                exitval = int(args[0])
                        except ValueError:
                                raise RuntimeError, _("transform (%s) has 'exit' "
                                    "operation syntax error: illegal exit value") % \
                                    transform
                        if len(args) == 2:
                                msg = args[1]

                def exit_func(action, matches, pkg_attrs, filename, lineno):
                        if msg:
                                newmsg = substitute_values(msg, action,
                                    matches, pkg_attrs, filename, lineno,
                                    quote=True)
                                print >> sys.stderr, newmsg
                        sys.exit(exitval)

                operation = exit_func

        elif op[0] == "add":
                try:
                        attr, value = shlex.split(op[1])
                except ValueError:
                        raise RuntimeError, \
                            _("transform (%s) has 'add' operation syntax error"
                            ) % transform

                def add_func(action, matches, pkg_attrs, filename, lineno):
                        newattr = substitute_values(attr, action, matches,
                            pkg_attrs, filename, lineno)
                        newval = substitute_values(value, action, matches,
                            pkg_attrs, filename, lineno)
                        if newattr in action.attrs:
                                av = action.attrs[newattr]
                                if isinstance(av, list):
                                        action.attrs[newattr].append(newval)
                                else:
                                        action.attrs[newattr] = [ av, newval ]
                        else:
                                action.attrs[newattr] = newval
                        return action
                operation = add_func

        elif op[0] == "edit":
                if len(op) < 2:
                        raise RuntimeError, \
                            _("transform (%s) has 'edit' operation syntax error"
                            ) % transform

                args = shlex.split(op[1])
                if len(args) not in [2, 3]:
                        raise RuntimeError, \
                            _("transform (%s) has 'edit' operation syntax error"
                            ) % transform
                attr = args[0]

                # Run args[1] (the regexp) through substitute_values() with a
                # bunch of bogus values to see whether it triggers certain
                # exceptions.  If it does, then substitution would have
                # occurred, and we can't compile the regex now, but wait until
                # we can correctly run substitute_values().
                try:
                        substitute_values(args[1], None, [], None, None, None)
                        regexp = re.compile(args[1])
                except (AttributeError, RuntimeError):
                        regexp = args[1]
                except re.error, e:
                        raise RuntimeError, \
                            _("transform (%(transform)s) has 'edit' operation "
                            "with malformed regexp (%(err)s)") % \
                            {"transform": transform, "err": e}

                if len(args) == 3:
                        replace = args[2]
                else:
                        replace = ""

                def replace_func(action, matches, pkg_attrs, filename, lineno):
                        newattr = substitute_values(attr, action, matches,
                            pkg_attrs, filename, lineno)
                        newrep = substitute_values(replace, action, matches,
                            pkg_attrs, filename, lineno)
                        val = attrval_as_list(action.attrs, newattr)

                        if not val:
                                return action

                        # It's now appropriate to compile the regexp, if there
                        # are substitutions to be made.  So do the substitution
                        # and compile the result.
                        if isinstance(regexp, basestring):
                                rx = re.compile(substitute_values(regexp,
                                    action, matches, pkg_attrs, filename, lineno))
                        else:
                                rx = regexp

                        try:
                                action.attrs[newattr] = [
                                    rx.sub(newrep, v)
                                    for v in val
                                ]
                        except re.error, e:
                                raise RuntimeError, \
                                    _("transform (%(transform)s) has edit "
                                    "operation with replacement string regexp "
                                    "error %(err)e") % \
                                    {"transform": transform, "err": e}
                        return action

                operation = replace_func

        elif op[0] == "delete":
                if len(op) < 2:
                        raise RuntimeError, \
                            _("transform (%s) has 'delete' operation syntax error"
                            ) % transform

                args = shlex.split(op[1])
                if len(args) != 2:
                        raise RuntimeError, \
                            _("transform (%s) has 'delete' operation syntax error"
                            ) % transform
                attr = args[0]

                try:
                        regexp = re.compile(args[1])
                except re.error, e:
                        raise RuntimeError, \
                            _("transform (%(transform)s) has 'delete' operation"
                            "with malformed regexp (%(err)s)") % \
                            {"transform": transform, "err": e}

                def delete_func(action, matches, pkg_attrs, filename, lineno):
                        val = attrval_as_list(action.attrs, attr)
                        if not val:
                                return action
                        try:
                                new_val = [
                                    v
                                    for v in val
                                    if not regexp.search(v)
                                ]

                                if new_val:
                                        action.attrs[attr] = new_val
                                else:
                                        del action.attrs[attr]
                        except re.error, e:
                                raise RuntimeError, \
                                    _("transform (%(transform)s) has delete "
                                    "operation with replacement string regexp "
                                    "error %(err)e") % \
                                    {"transform": transform, "err": e}
                        return action

                operation = delete_func

        elif op[0] == "print":
                if len(op) > 2:
                        raise RuntimeError, _("transform (%s) has 'print' "
                            "operation syntax error") % transform

                if len(op) == 1:
                        msg = ""
                else:
                        msg = op[1]

                def print_func(action, matches, pkg_attrs, filename, lineno):
                        newmsg = substitute_values(msg, action, matches,
                            pkg_attrs, filename, lineno, quote=True)

                        printinfo.append("%s" % newmsg)
                        return action

                operation = print_func

        elif op[0] == "emit":
                if len(op) > 2:
                        raise RuntimeError, _("transform (%s) has 'emit' "
                            "operation syntax error") % transform

                if len(op) == 1:
                        msg = ""
                else:
                        msg = op[1]

                def emit_func(action, matches, pkg_attrs, filename, lineno):
                        newmsg = substitute_values(msg, action, matches,
                            pkg_attrs, filename, lineno, quote=True)

                        if not newmsg.strip() or newmsg.strip()[0] == "#":
                                return (newmsg, action)
                        try:
                                return (pkg.actions.fromstr(newmsg), action)
                        except (pkg.actions.MalformedActionError,
                            pkg.actions.UnknownActionError,
                            pkg.actions.InvalidActionError), e:
                                raise RuntimeError(e)

                operation = emit_func

        else:
                raise RuntimeError, _("unknown transform operation '%s'") % op[0]

        transforms.append((types, attrdict, operation, filename, lineno, transform))

def substitute_values(msg, action, matches, pkg_attrs, filename=None, lineno=None, quote=False):
        """Substitute tokens in messages which can be expanded to the action's
        attribute values."""

        newmsg = ""
        prevend = 0
        for i in re.finditer("%\((.+?)\)|%\{(.+?)\}", msg):
                m = i.string[slice(*i.span())]
                assert m[1] in "({"
                if m[1] == "(":
                        group = 1
                elif m[1] == "{":
                        group = 2
                d = {}
                if ";" in i.group(group):
                        attrname, args = i.group(group).split(";", 1)
                        tokstream = shlex.shlex(args)
                        for tok in tokstream:
                                if tok == ";":
                                        tok = tokstream.get_token()
                                eq = tokstream.get_token()
                                if eq == "" or eq == ";":
                                        val = True
                                else:
                                        assert(eq == "=")
                                        val = tokstream.get_token()
                                        if ('"', '"') == (val[0], val[-1]):
                                                val = val[1:-1]
                                        elif ("'", "'") == (val[0], val[-1]):
                                                val = val[1:-1]
                                d[tok] = val
                else:
                        attrname = i.group(group)

                d.setdefault("quote", quote)

                if d.get("noquote", None):
                        d["quote"] = False

                if group == 2:
                        attr = pkg_attrs.get(attrname, d.get("notfound", None))
                        if attr and len(attr) == 1:
                                attr = attr[0]
                else:
                        if attrname == "pkg.manifest.lineno":
                                attr = str(lineno)
                        elif attrname == "pkg.manifest.filename":
                                attr = str(filename)
                        elif attrname == "action.hash":
                                attr = getattr(action, "hash",
                                    d.get("notfound", None))
                        elif attrname == "action.key":
                                attr = action.attrs.get(action.key_attr,
                                    d.get("notfound", None))
                        elif attrname == "action.name":
                                attr = action.name
                        else:
                                attr = action.attrs.get(attrname,
                                    d.get("notfound", None))

                if attr is None:
                        raise RuntimeError, _("attribute '%s' not found") % \
                            attrname

                def q(s):
                        if " " in s or "'" in s or "\"" in s or s == "":
                                if "\"" not in s:
                                        return '"%s"' % s
                                elif "'" not in s:
                                        return "'%s'" % s
                                else:
                                        return '"%s"' % s.replace("\"", "\\\"")
                        else:
                                return s

                if not d["quote"]:
                        q = lambda x: x

                if isinstance(attr, basestring):
                        newmsg += msg[prevend:i.start()] + \
                            d.get("prefix", "") + q(attr) + d.get("suffix", "")
                else:
                        newmsg += msg[prevend:i.start()] + \
                            d.get("sep", " ").join([
                                d.get("prefix", "") + q(v) + d.get("suffix", "")
                                for v in attr
                            ])
                prevend = i.end()

        newmsg += msg[prevend:]

        # Now see if there are any backreferences to match groups
        msg = newmsg
        newmsg = ""
        prevend = 0
        backrefs = sum((
            group
            for group in (
                match.groups()
                for match in matches
                if match.groups()
            )
        ), (None,))
        for i in re.finditer(r"%<\d>", msg):
                ref = int(i.string[slice(*i.span())][2:-1])

                if ref == 0 or ref > len(backrefs) - 1:
                        raise RuntimeError, _("no match group %(group)d "
                            "(max %(maxgroups)d)") % \
                            {"group": ref, "maxgroups": len(backrefs) - 1}

                newmsg += msg[prevend:i.start()] + backrefs[ref]
                prevend = i.end()

        newmsg += msg[prevend:]
        return newmsg

def attrval_as_list(attrdict, key):
        """Return specified attribute as list;
        an empty list if no such attribute exists"""
        if key not in attrdict:
                return []
        val = attrdict[key]
        if not isinstance(val, list):
                val = [val]
        return val

class PkgAction(pkg.actions.generic.Action):
        name = "pkg"
        def __init__(self, attrs):
                self.attrs = attrs

def apply_transforms(action, pkg_attrs, verbose, act_filename, act_lineno):
        """Apply all transforms to action, returning modified action
        or None if action is dropped"""
        comments = []
        newactions = []
        if verbose:
                comments.append("#  Action: %s" % action)
        for types, attrdict, operation, filename, lineno, transform in transforms:
                if action is None:
                        action = PkgAction(pkg_attrs)
                # skip if types are specified and none match
                if types and action.name not in types:
                        continue
                # skip if some attrs don't exist
                if set(attrdict.keys()) - set(action.attrs.keys()):
                        continue

                # Check to make sure all matching attrs actually match.  The
                # order is effectively arbitrary, since they come from a dict.
                matches = [
                    attrdict[key].match(attrval)
                    for key in attrdict
                    for attrval in attrval_as_list(action.attrs, key)
                ]

                if not all(matches):
                        continue

                s = transform[11:transform.index("->")]
                # Map each pattern to its position in the original match string.
                matchorder = {}
                for attr, match in attrdict.iteritems():
                        # Attributes might be quoted even if they don't need it,
                        # and lead to a mis-match.  These three patterns are all
                        # safe to try.  If we fail to find the match expression,
                        # it's probably because it used different quoting rules
                        # than the action code does, or from these three rules.
                        # It might very well be okay, so we go ahead, but these
                        # oddly quoted patterns will sort at the beginning, and
                        # backref matching may be off.
                        matchorder[match.pattern] = -1
                        for qs in ("%s=%s", "%s=\"%s\"", "%s='%s'"):
                                pos = s.find(qs % (attr, match.pattern))
                                if pos != -1:
                                        matchorder[match.pattern] = pos
                                        break

                # Then sort the matches list by those positions.
                matches.sort(key=lambda x: matchorder[x.re.pattern])

                # time to apply transform operation
                try:
                        if verbose:
                                orig_attrs = action.attrs.copy()
                        action = operation(action, matches, pkg_attrs,
                            act_filename, act_lineno)
                except RuntimeError, e:
                        raise RuntimeError, \
                            "Transform specified in file %s, line %s reports %s" % (
                            filename, lineno, e)
                if isinstance(action, tuple):
                        newactions.append(action[0])
                        action = action[1]
                if verbose:
                        if not action or \
                            not isinstance(action, basestring) and \
                            orig_attrs != action.attrs:
                                comments.append("# Applied: %s (file %s line %s)" % (
                                    transform, filename, lineno))
                                comments.append("#  Result: %s" % action)
                if not action or isinstance(action, basestring):
                        break

        # Any newly-created actions need to have the transforms applied, too.
        newnewactions = []
        for act in newactions:
                if not isinstance(act, basestring):
                        c, al = apply_transforms(act, pkg_attrs, verbose,
                            act_filename, act_lineno)
                        if c:
                                comments.append(c)
                        newnewactions += [a for a in al if a is not None]
                else:
                        newnewactions.append(act)

        if len(comments) == 1:
                comments = []

        if action and action.name != "pkg":
                return (comments, [action] + newnewactions)
        else:
                return (comments, [None] + newnewactions)


def searching_open(filename, try_cwd=False):
        """ implement include hierarchy """

        if filename.startswith("/") or try_cwd == True and \
            os.path.exists(filename):
                try:
                        return filename, file(filename)
                except IOError, e:
                        raise RuntimeError, _("Cannot open file: %s") % e

        for i in includes:
                f = os.path.join(i, filename)
                if os.path.exists(f):
                        try:
                                return f, file(f)
                        except IOError, e:
                                raise RuntimeError, _("Cannot open file: %s") % e

        raise RuntimeError, _("File not found: \'%s\'") % filename

def apply_macros(s):
        """Apply macro subs defined on command line... keep applying
        macros until no translations are found."""
        while s and "$(" in s:
                for key in macros.keys():
                        if key in s:
                                value = macros[key]
                                s = s.replace(key, value)
                                break # look for more substitutions
                else:
                        break # no more substitutable tokens
        return s

def read_file(tp, ignoreincludes):
        """ return the lines in the file as a list of
        tuples containing (line, filename, line number);
        handle continuation and <include "path">"""
        ret = []
        filename, f = tp

        accumulate = ""
        for lineno, line in enumerate(f):
                lineno = lineno + 1 # number from 1
                line = line.strip()
                if not line: # preserve blanks
                        ret.append((line, filename, lineno))
                        continue
                if line.endswith("\\"):
                        accumulate += line[0:-1]
                        continue
                elif accumulate:
                        line = accumulate + line
                        accumulate = ""

                if line:
                        line = apply_macros(line)

                line = line.strip()

                if not line:
                        continue

                try:
                        if line.startswith("<") and line.endswith(">"):
                                if line.startswith("<include"):
                                        if not ignoreincludes:
                                                line = line[1:-1]
                                                line = line[7:].strip()
                                                line = line.strip('"')
                                                ret.extend(read_file(
                                                    searching_open(line),
                                                    ignoreincludes))
                                        else:
                                                ret.append((line, filename, lineno))
                                elif line.startswith("<transform"):
                                        line = line[1:-1]
                                        add_transform(line, filename, lineno)
                                else:
                                        raise RuntimeError, _("unknown command %s") % (
                                                line)
                        else:
                                ret.append((line, filename, lineno))
                except RuntimeError, e:
                        error(_("File %(file)s, line %(line)d: %(exception)s") %
                            {'file': filename,
                             'line': lineno,
                             'exception': e},
                            exitcode=None)
                        raise RuntimeError, "<included from>"

        return ret

def error(text, exitcode=1):
        """Emit an error message prefixed by the command name """

        print >> sys.stderr, "pkgmogrify: %s" % text

        if exitcode != None:
                sys.exit(exitcode)

def main_func():
        gettext.install("pkg", "/usr/share/locale",
            codeset=locale.getpreferredencoding())

        outfilename = None
        printfilename = None
        verbose = False
        ignoreincludes = False

        try:
                opts, pargs = getopt.getopt(sys.argv[1:], "ivD:I:O:P:?", ["help"])
                for opt, arg in opts:
                        if opt == "-D":
                                if "=" not in arg:
                                        error(_("macros must be of form name=value"))
                                a = arg.split("=", 1)
                                if a[0] == "":
                                        error(_("macros must be of form name=value"))
                                macros.update([("$(%s)" % a[0], a[1])])
                        if opt == "-i":
                                ignoreincludes = True
                        if opt == "-I":
                                includes.append(arg)
                        if opt == "-O":
                                outfilename = arg
                        if opt == "-P":
                                printfilename = arg
                        if opt == "-v":
                                verbose = True
                        if opt in ("--help", "-?"):
                                usage(exitcode=0)

        except getopt.GetoptError, e:
                usage(_("illegal global option -- %s") % e.opt)

        try:
                if pargs:
                        infiles = [ searching_open(f, try_cwd=True) for f in pargs ]
                else:
                        infiles =  [("<stdin>", sys.stdin)]

                lines = []
        except RuntimeError, e:
                error(_("Error processing input arguments: %s") % e)
        try:
                for f in infiles:
                        lines.extend(read_file(f, ignoreincludes))
                        lines.append((None, f[0], None))
        except RuntimeError, e:
                sys.exit(1)

        output = []

        pkg_attrs = {}
        for line, filename, lineno in lines:
                if line is None:
                        if "pkg.fmri" in pkg_attrs:
                                comment, a = apply_transforms(None, pkg_attrs,
                                    verbose, filename, lineno)
                                output.append((comment, a, None))
                        pkg_attrs = {}
                        continue

                if not line or line.startswith("#") or line.startswith("<"):
                        output.append(([line], [], None))
                        continue

                if line.startswith("$("): #prepended unexpanded macro
                        # doesn't handle nested macros
                        eom = line.index(")") + 1
                        prepended_macro = line[0:eom]
                        line = line[eom:]
                else:
                        prepended_macro = None

                try:
                        act = pkg.actions.fromstr(line)
                except (pkg.actions.MalformedActionError,
                    pkg.actions.UnknownActionError,
                    pkg.actions.InvalidActionError), e:
                        error("File %s line %d: %s" % (filename, lineno, e))
                try:
                        if act.name == "set":
                                name = act.attrs["name"]
                                value = act.attrs["value"]
                                if isinstance(value, basestring):
                                        pkg_attrs.setdefault(name, []).append(value)
                                else:
                                        pkg_attrs.setdefault(name, []).extend(value)
                        comment, a = apply_transforms(act, pkg_attrs, verbose,
                            filename, lineno)
                        output.append((comment, a, prepended_macro))
                except RuntimeError, e:
                        error("File %s line %d: %s" % (filename, lineno, e))

        try:
                if printfilename == None:
                        printfile = sys.stdout
                else:
                        printfile = file(printfilename, "w")

                for p in printinfo:
                        print >> printfile, "%s" % p
        except IOError, e:
                error(_("Cannot write extra data %s") % e)

        try:
                if outfilename == None:
                        outfile = sys.stdout
                else:
                        outfile = file(outfilename, "w")

                emitted = set()
                for comment, actionlist, prepended_macro in output:
                        if comment:
                                for l in comment:
                                        print >> outfile, "%s" % l
                        for i, action in enumerate(actionlist):
                                if action is None:
                                        continue
                                if prepended_macro is None:
                                        s = "%s" % action
                                else:
                                        s = "%s%s" % (prepended_macro, action)
                                # The first action is the original action and
                                # should be printed; later actions are all
                                # emitted and should only be printed if not
                                # duplicates.
                                if i == 0:
                                        print >> outfile, s
                                elif s not in emitted:
                                        print >> outfile, s
                                        emitted.add(s)
        except IOError, e:
                error(_("Cannot write output %s") % e)

        return 0

if __name__ == "__main__":

        # Make all warnings be errors.
        warnings.simplefilter('error')

        try:
                exit_code = main_func()
        except (PipeError, KeyboardInterrupt):
                exit_code = 1
        except SystemExit, __e:
                exit_code = __e
        except Exception, __e:
                traceback.print_exc()
                error(misc.get_traceback_message(), exitcode=None)
                exit_code = 99

        sys.exit(exit_code)