src/pkgdep.py
author Bart Smaalders <Bart.Smaalders@Oracle.COM>
Thu, 30 Mar 2017 17:05:02 -0700
changeset 3537 03bba058e598
parent 3476 cc1b291b79d2
permissions -rw-r--r--
20973899 Installation of zones in parallel may fail with: [Errno 17] File exists

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

import errno
import getopt
import gettext
import locale
import os
import six
import sys
import traceback
import warnings

import pkg
import pkg.actions as actions
import pkg.client.api as api
import pkg.client.api_errors as api_errors
import pkg.client.progress as progress
import pkg.manifest as manifest
import pkg.misc as misc
import pkg.publish.dependencies as dependencies
from pkg.misc import msg, emsg, PipeError
from pkg.client.pkgdefs import EXIT_OK, EXIT_OOPS, EXIT_BADOPT

CLIENT_API_VERSION = 82
PKG_CLIENT_NAME = "pkgdepend"

DEFAULT_SUFFIX = ".res"

def format_update_error(e):
        # This message is displayed to the user whenever an
        # ImageFormatUpdateNeeded exception is encountered.
        emsg("\n")
        emsg(str(e))
        emsg(_("To continue, the target image must be upgraded "
            "before it can be used.  See pkg(1) update-format for more "
            "information."))

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

        if cmd:
                text = "{0}: {1}".format(cmd, text)
        else:
                # If we get passed something like an Exception, we can convert
                # it down to a string.
                text = str(text)

        # If the message starts with whitespace, assume that it should come
        # *before* the command-name prefix.
        text_nows = text.lstrip()
        ws = text[:len(text) - len(text_nows)]

        # This has to be a constant value as we can't reliably get our actual
        # program name on all platforms.
        emsg(ws + "pkgdepend: " + text_nows)

def usage(usage_error=None, cmd=None, retcode=EXIT_BADOPT):
        """Emit a usage message and optionally prefix it with a more specific
        error message.  Causes program to exit."""

        if usage_error:
                error(usage_error, cmd=cmd)
        emsg (_("""\
Usage:
        pkgdepend [options] command [cmd_options] [operands]

Subcommands:
        pkgdepend generate [-IMm] -d dir [-d dir] [-D name=value] [-k path]
            manifest_file
        pkgdepend resolve [-EmoSv] [-d output_dir]
            [-e external_package_file]... [-s suffix] manifest_file ...

Options:
        -R dir
        --help or -?
Environment:
        PKG_IMAGE"""))

        sys.exit(retcode)

def generate(args):
        """Produce a list of file dependencies from a manfiest and a proto
        area."""
        try:
                opts, pargs = getopt.getopt(args, "d:D:Ik:Mm?",
                    ["help"])
        except getopt.GetoptError as e:
                usage(_("illegal global option -- {0}").format(e.opt))

        remove_internal_deps = True
        echo_manf = False
        show_missing = False
        show_usage = False
        isa_paths = []
        run_paths = []
        platform_paths = []
        dyn_tok_conv = {}
        proto_dirs = []

        for opt, arg in opts:
                if opt == "-d":
                        if not os.path.isdir(arg):
                                usage(_("The proto directory {0} could not be "
                                    "found.".format(arg)), retcode=EXIT_BADOPT)
                        proto_dirs.append(os.path.abspath(arg))
                elif opt == "-D":
                        try:
                                dyn_tok_name, dyn_tok_val = arg.split("=", 1)
                        except:
                                usage(_("-D arguments must be of the form "
                                    "'name=value'."))
                        if not dyn_tok_name[0] == "$":
                                dyn_tok_name = "$" + dyn_tok_name
                        dyn_tok_conv.setdefault(dyn_tok_name, []).append(
                            dyn_tok_val)
                elif opt == "-I":
                        remove_internal_deps = False
                elif opt == "-k":
                        run_paths.append(arg)
                elif opt == "-m":
                        echo_manf = True
                elif opt == "-M":
                        show_missing = True
                elif opt in ("--help", "-?"):
                        show_usage = True
        if show_usage:
                usage(retcode=EXIT_OK)
        if len(pargs) > 2 or len(pargs) < 1:
                usage(_("Generate only accepts one or two arguments."))

        if "$ORIGIN" in dyn_tok_conv:
                usage(_("ORIGIN may not be specified using -D. It will be "
                    "inferred from the\ninstall paths of the files."))

        retcode = EXIT_OK

        manf = pargs[0]

        if not os.path.isfile(manf):
                usage(_("The manifest file {0} could not be found.").format(manf),
                    retcode=EXIT_BADOPT)

        if len(pargs) > 1:
                if not os.path.isdir(pargs[1]):
                        usage(_("The proto directory {0} could not be found.").format(
                            pargs[1]), retcode=EXIT_BADOPT)
                proto_dirs.insert(0, os.path.abspath(pargs[1]))
        if not proto_dirs:
                usage(_("At least one proto directory must be provided."),
                    retcode=EXIT_BADOPT)

        try:
                ds, es, ws, ms, pkg_attrs = dependencies.list_implicit_deps(manf,
                    proto_dirs, dyn_tok_conv, run_paths, remove_internal_deps)
        except (actions.MalformedActionError, actions.UnknownActionError) as e:
                error(_("Could not parse manifest {manifest} because of the "
                    "following line:\n{line}").format(manifest=manf,
                    line=e.actionstr))
                return EXIT_OOPS
        except api_errors.ApiException as e:
                error(e)
                return EXIT_OOPS

        if echo_manf:
                fh = open(manf, "r")
                for l in fh:
                        msg(l.rstrip())
                fh.close()

        for d in sorted(ds):
                msg(d)

        for key, value in six.iteritems(pkg_attrs):
                msg(actions.attribute.AttributeAction(**{key: value}))

        if show_missing:
                for m in ms:
                        emsg(m)
        for w in ws:
                emsg(w)

        for e in es:
                emsg(e)
                retcode = EXIT_OOPS
        return retcode

def resolve(args, img_dir):
        """Take a list of manifests and resolve any file dependencies, first
        against the other published manifests and then against what is installed
        on the machine."""
        out_dir = None
        echo_manifest = False
        output_to_screen = False
        suffix = None
        verbose = False
        use_system_to_resolve = True
        constraint_files = []
        extra_external_info = False
        try:
                opts, pargs = getopt.getopt(args, "d:e:Emos:Sv")
        except getopt.GetoptError as e:
                usage(_("illegal global option -- {0}").format(e.opt))
        for opt, arg in opts:
                if opt == "-d":
                        out_dir = arg
                elif opt == "-e":
                        constraint_files.append(arg)
                elif opt == "-E":
                        extra_external_info = True
                elif opt == "-m":
                        echo_manifest = True
                elif opt == "-o":
                        output_to_screen = True
                elif opt == "-s":
                        suffix = arg
                elif opt == "-S":
                        use_system_to_resolve = False
                elif opt == "-v":
                        verbose = True

        if (out_dir or suffix) and output_to_screen:
                usage(_("-o cannot be used with -d or -s"))

        manifest_paths = [os.path.abspath(fp) for fp in pargs]

        for manifest in manifest_paths:
                if not os.path.isfile(manifest):
                        usage(_("The manifest file {0} could not be found.").format(
                            manifest), retcode=EXIT_BADOPT)

        if out_dir:
                out_dir = os.path.abspath(out_dir)
                if not os.path.isdir(out_dir):
                        usage(_("The output directory {0} is not a directory.").format(
                            out_dir), retcode=EXIT_BADOPT)

        provided_image_dir = True
        pkg_image_used = False
        if img_dir == None:
                orig_cwd = None
                try:
                        orig_cwd = os.getcwd()
                except OSError:
                        # May be unreadable by user or have other problem.
                        pass

                img_dir, provided_image_dir = api.get_default_image_root(
                    orig_cwd=orig_cwd)
                if os.environ.get("PKG_IMAGE"):
                        # It's assumed that this has been checked by the above
                        # function call and hasn't been removed from the
                        # environment.
                        pkg_image_used = True

        if not img_dir:
                error(_("Could not find image.  Use the -R option or set "
                    "$PKG_IMAGE to the\nlocation of an image."))
                return EXIT_OOPS

        system_patterns = misc.EmptyI
        if constraint_files:
                system_patterns = []
                for f in constraint_files:
                        try:
                                with open(f, "r") as fh:
                                        for l in fh:
                                                l = l.strip()
                                                if l and not l.startswith("#"):
                                                        system_patterns.append(
                                                            l)
                        except EnvironmentError as e:
                                if e.errno in (errno.ENOENT, errno.EISDIR):
                                        error("{0}: '{1}'".format(
                                            e.args[1], e.filename),
                                            cmd="resolve")
                                        return EXIT_OOPS
                                raise api_errors._convert_error(e)
                if not system_patterns:
                        error(_("External package list files were provided but "
                            "did not contain any fmri patterns."))
                        return EXIT_OOPS
        elif use_system_to_resolve:
                system_patterns = ["*"]

        # Becuase building an ImageInterface permanently changes the cwd for
        # python, it's necessary to do this step after resolving the paths to
        # the manifests.
        try:
                api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
                    progress.QuietProgressTracker(), None, PKG_CLIENT_NAME,
                    exact_match=provided_image_dir)
        except api_errors.ImageNotFoundException as e:
                if e.user_specified:
                        if pkg_image_used:
                                error(_("No image rooted at '{0}' "
                                    "(set by $PKG_IMAGE)").format(e.user_dir))
                        else:
                                error(_("No image rooted at '{0}'").format(
                                    e.user_dir))
                else:
                        error(_("No image found."))
                return EXIT_OOPS
        except api_errors.PermissionsException as e:
                error(e)
                return EXIT_OOPS
        except api_errors.ImageFormatUpdateNeeded as e:
                # This should be a very rare error case.
                format_update_error(e)
                return EXIT_OOPS

        try:
                pkg_deps, errs, warnings, unused_fmris, external_deps = \
                    dependencies.resolve_deps(manifest_paths, api_inst,
                        system_patterns, prune_attrs=not verbose)
        except (actions.MalformedActionError, actions.UnknownActionError) as e:
                error(_("Could not parse one or more manifests because of "
                    "the following line:\n{0}").format(e.actionstr))
                return EXIT_OOPS
        except dependencies.DependencyError as e:
                error(e)
                return EXIT_OOPS
        except api_errors.ApiException as e:
                error(e)
                return EXIT_OOPS
        ret_code = EXIT_OK

        if output_to_screen:
                ret_code = pkgdeps_to_screen(pkg_deps, manifest_paths,
                    echo_manifest)
        elif out_dir:
                ret_code = pkgdeps_to_dir(pkg_deps, manifest_paths, out_dir,
                    suffix, echo_manifest)
        else:
                ret_code = pkgdeps_in_place(pkg_deps, manifest_paths, suffix,
                    echo_manifest)

        if extra_external_info:
                if constraint_files and unused_fmris:
                        msg(_("\nThe following fmris matched a pattern in a "
                            "constraint file but were not used in\ndependency "
                            "resolution:"))
                        for pfmri in sorted(unused_fmris):
                                msg("\t{0}".format(pfmri))
                if not constraint_files and external_deps:
                        msg(_("\nThe following fmris had dependencies resolve "
                            "to them:"))
                        for pfmri in sorted(external_deps):
                                msg("\t{0}".format(pfmri))

        for e in errs:
                if ret_code == EXIT_OK:
                        ret_code = EXIT_OOPS
                emsg(e)
        for w in warnings:
                emsg(w)
        return ret_code

def __resolve_echo_line(l):
        """Given a line from a manifest, determines whether that line should
        be repeated in the output file if echo manifest has been set."""

        try:
                act = actions.fromstr(l.rstrip())
        except KeyboardInterrupt:
                raise
        except actions.ActionError:
                return True
        else:
                return not act.name == "depend"

def __echo_manifest(pth, out_func, strip_newline=False):
        try:
                with open(pth, "r") as fh:
                        text = ""
                        act = ""
                        for l in fh:
                                text += l
                                act += l.rstrip()
                                if act.endswith("\\"):
                                        act = act.rstrip("\\")
                                        continue
                                if __resolve_echo_line(act):
                                        if strip_newline:
                                                text = text.rstrip()
                                        elif text[-1] != "\n":
                                                text += "\n"
                                        out_func(text)
                                text = ""
                                act = ""
                        if text != "" and __resolve_echo_line(act):
                                if text[-1] != "\n":
                                        text += "\n"
                                out_func(text)
        except EnvironmentError:
                ret_code = EXIT_OOPS
                emsg(_("Could not open {0} to echo manifest").format(
                    manifest_path))

def pkgdeps_to_screen(pkg_deps, manifest_paths, echo_manifest):
        """Write the resolved package dependencies to stdout.

        'pkg_deps' is a dictionary that maps a path to a manifest to the
        dependencies that were resolved for that manifest.

        'manifest_paths' is a list of the paths to the manifests for which
        file dependencies were resolved.

        'echo_manifest' is a boolean which determines whether the original
        manifest will be written out or not."""

        ret_code = EXIT_OK
        first = True
        for p in manifest_paths:
                if not first:
                        msg("\n\n")
                first = False
                msg("# {0}".format(p))
                if echo_manifest:
                        __echo_manifest(p, msg, strip_newline=True)
                for d in pkg_deps[p]:
                        msg(d)
        return ret_code

def write_res(deps, out_file, echo_manifest, manifest_path):
        """Write the dependencies resolved, and possibly the manifest, to the
        destination file.

        'deps' is a list of the resolved dependencies.

        'out_file' is the path to the destination file.

        'echo_manifest' determines whether to repeat the original manifest in
        the destination file.

        'manifest_path' the path to the manifest which generated the
        dependencies."""

        ret_code = EXIT_OK
        try:
                out_fh = open(out_file, "w")
        except EnvironmentError:
                ret_code = EXIT_OOPS
                emsg(_("Could not open output file {0} for writing").format(
                    out_file))
                return ret_code
        if echo_manifest:
                __echo_manifest(manifest_path, out_fh.write)
        for d in deps:
                out_fh.write("{0}\n".format(d))
        out_fh.close()
        return ret_code

def pkgdeps_to_dir(pkg_deps, manifest_paths, out_dir, suffix, echo_manifest):
        """Given an output directory, for each manifest given, writes the
        dependencies resolved to a file in the output directory.

        'pkg_deps' is a dictionary that maps a path to a manifest to the
        dependencies that were resolved for that manifest.

        'manifest_paths' is a list of the paths to the manifests for which
        file dependencies were resolved.

        'out_dir' is the path to the directory into which the dependency files
        should be written.

        'suffix' is the string to append to the end of each output file.

        'echo_manifest' is a boolean which determines whether the original
        manifest will be written out or not."""

        ret_code = EXIT_OK
        if not os.path.exists(out_dir):
                try:
                        os.makedirs(out_dir)
                except EnvironmentError as e:
                        e_dic = {"dir": out_dir}
                        if len(e.args) > 0:
                                e_dic["err"] = e.args[1]
                        else:
                                e_dic["err"] = e.args[0]
                        emsg(_("Out dir {out_dir} does not exist and could "
                            "not be created. Error is: {err}").format(**e_dic))
                        return EXIT_OOPS
        if suffix and suffix[0] != ".":
                suffix = "." + suffix
        for p in manifest_paths:
                out_file = os.path.join(out_dir, os.path.basename(p))
                if suffix:
                        out_file += suffix
                tmp_rc = write_res(pkg_deps[p], out_file, echo_manifest, p)
                if not ret_code:
                        ret_code = tmp_rc
        return ret_code

def pkgdeps_in_place(pkg_deps, manifest_paths, suffix, echo_manifest):
        """Given an output directory, for each manifest given, writes the
        dependencies resolved to a file in the output directory.

        'pkg_deps' is a dictionary that maps a path to a manifest to the
        dependencies that were resolved for that manifest.

        'manifest_paths' is a list of the paths to the manifests for which
        file dependencies were resolved.

        'out_dir' is the path to the directory into which the dependency files
        should be written.

        'suffix' is the string to append to the end of each output file.

        'echo_manifest' is a boolean which determines whether the original
        manifest will be written out or not."""

        ret_code = EXIT_OK
        if not suffix:
                suffix = DEFAULT_SUFFIX
        if suffix[0] != ".":
                suffix = "." + suffix
        for p in manifest_paths:
                out_file = p + suffix
                tmp_rc = write_res(pkg_deps[p], out_file, echo_manifest, p)
                if not ret_code:
                        ret_code = tmp_rc
        return ret_code

def main_func():
        try:
                opts, pargs = getopt.getopt(sys.argv[1:], "R:?",
                    ["help"])
        except getopt.GetoptError as e:
                usage(_("illegal global option -- {0}").format(e.opt))

        show_usage = False
        img_dir = None
        for opt, arg in opts:
                if opt == "-R":
                        img_dir = arg
                elif opt in ("--help", "-?"):
                        show_usage = True

        subcommand = None
        if pargs:
                subcommand = pargs.pop(0)
                if subcommand == "help":
                        show_usage = True

        if show_usage:
                usage(retcode=EXIT_OK)
        elif not subcommand:
                usage()

        if subcommand == "generate":
                if img_dir:
                        usage(_("generate subcommand doesn't use -R"))
                return generate(pargs)
        elif subcommand == "resolve":
                return resolve(pargs, img_dir)
        else:
                usage(_("unknown subcommand '{0}'").format(subcommand))

#
# Establish a specific exit status which means: "python barfed an exception"
# so that we can more easily detect these in testing of the CLI commands.
#
if __name__ == "__main__":
        misc.setlocale(locale.LC_ALL, "", error)
        gettext.install("pkg", "/usr/share/locale",
            codeset=locale.getpreferredencoding())
        misc.set_fd_limits(printer=error)

        # Make all warnings be errors.
        warnings.simplefilter('error')
        if six.PY3:
                # disable ResourceWarning: unclosed file
                warnings.filterwarnings("ignore", category=ResourceWarning)

        try:
                __ret = main_func()
        except api_errors.MissingFileArgumentException as e:
                error("The manifest file {0} could not be found.".format(e.path))
                __ret = EXIT_OOPS
        except api_errors.VersionException as __e:
                error(_("The {cmd} command appears out of sync with the lib"
                    "raries provided\nby pkg:/package/pkg. The client version "
                    "is {client} while the library\nAPI version is {api}").format(
                    cmd=PKG_CLIENT_NAME,
                    client=__e.received_version,
                    api=__e.expected_version
                    ))
                __ret = EXIT_OOPS
        except api_errors.ApiException as e:
                error(e)
                __ret = EXIT_OOPS
        except RuntimeError as _e:
                emsg("{0}: {1}".format(PKG_CLIENT_NAME, _e))
                __ret = EXIT_OOPS
        except (PipeError, KeyboardInterrupt):
                # We don't want to display any messages here to prevent
                # possible further broken pipe (EPIPE) errors.
                __ret = EXIT_OOPS
        except SystemExit as _e:
                raise _e
        except:
                traceback.print_exc()
                error(misc.get_traceback_message())
                __ret = 99
        sys.exit(__ret)