src/modules/flavor/base.py
author Danek Duvall <danek.duvall@oracle.com>
Mon, 05 Aug 2013 10:44:11 -0700
changeset 2935 a4d3f6b9aa6d
parent 2826 cae308eb6426
child 3158 58c9c2c21e67
permissions -rw-r--r--
16462757 pkgdepend needs to support Python 3

#!/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) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
#

import os

import pkg.actions.depend as depend
import pkg.variant as variant

from pkg.portable import PD_DEFAULT_RUNPATH

class DependencyAnalysisError(Exception):

        def __unicode__(self):
                # To workaround python issues 6108 and 2517, this provides a
                # a standard wrapper for this class' exceptions so that they
                # have a chance of being stringified correctly.
                return str(self)


class MissingFile(DependencyAnalysisError):
        """Exception that is raised when a dependency checker can't find the
        file provided."""

        def __init__(self, file_path, dirs=None):
                Exception.__init__(self)
                self.file_path = file_path
                self.dirs = dirs

        def __str__(self):
                if not self.dirs:
                        return _("Couldn't find '%s'") % self.file_path
                else:
                        return _("Couldn't find '%(path)s' in any of the "
                            "specified search directories:\n%(dirs)s") % \
                            {"path": self.file_path,
                            "dirs": "\n".join(
                            ["\t" + d for d in sorted(self.dirs)])}

class MultipleDefaultRunpaths(DependencyAnalysisError):
        """Exception that is raised when multiple $PGKDEPEND_RUNPATH tokens
        are found in a pkg.depend.runpath attribute value."""

        def __init__(self):
                Exception.__init__(self)

        def __str__(self):
                return _(
                    "More than one $PKGDEPEND_RUNPATH token was set on the "
                    "same action in this manifest.")

class InvalidDependBypassValue(DependencyAnalysisError):
        """Exception that is raised when we encounter an incorrect
        pkg.depend.bypass-generate attribute value."""

        def __init__(self, value, error):
                self.value = value
                self.error = error
                Exception.__init__(self)

        def __str__(self):
                return _(
                    "Invalid pkg.depend.bypass-generate value %(val)s: "
                    "%(err)s") % {"val": self.value, "err": self.error}


class InvalidPublishingDependency(DependencyAnalysisError):
        """Exception that is raised when base_names or run_paths as well as
        full_paths are specified for a PublishingDependency."""

        def __init__(self, error):
                self.error = error
                Exception.__init__(self)

        def __str__(self):
                return _(
                    "Invalid publishing dependency: %s") % self.error


class Dependency(depend.DependencyAction):
        """Base, abstract class to represent the dependencies a dependency
        generator can produce."""

        ERROR = 0
        WARNING = 1

        DUMMY_FMRI = "__TBD"
        DEPEND_DEBUG_PREFIX = "pkg.debug.depend"
        DEPEND_TYPE = "require"

        def __init__(self, action, pkg_vars, proto_dir, attrs):
                """Each dependency needs to know the action that generated it
                and the variants for the package containing that action.

                'action' is the action which produced this dependency.

                'pkg_vars' is the list of variants against which the package
                delivering the action was published.

                'proto_dir' is the proto area where the file the action delivers
                lives.

                'attrs' is a dictionary to containing the relevant action tags
                for the dependency.
                """
                self.action = action
                self.pkg_vars = pkg_vars
                self.proto_dir = proto_dir
                self.dep_vars = self.get_variant_combinations()

                attrs.update([
                    ("fmri", self.DUMMY_FMRI),
                    ("type", self.DEPEND_TYPE),
                    ("%s.reason" % self.DEPEND_DEBUG_PREFIX, self.action_path())
                ])

                attrs.update(action.get_variant_template())
                # Only lists are permitted for multi-value action attributes.
                for k, v in attrs.items():
                        if isinstance(v, set):
                                attrs[k] = list(v)

                depend.DependencyAction.__init__(self, **attrs)

        def is_error(self):
                """Return true if failing to resolve this external dependency
                should be considered an error."""

                return True

        def dep_key(self):
                """Return a representation of the location the action depends
                on in a way that is hashable."""

                raise NotImplementedError(_("Subclasses of Dependency must "
                    "implement dep_key. Current class is %s") %
                    self.__class__.__name__)

        def get_variant_combinations(self, satisfied=False):
                """Create the combinations of variants that this action
                satisfies or needs satisfied.

                'satisfied' determines whether the combination produced is
                satisfied or unsatisfied."""

                variants = self.action.get_variant_template()
                variants.merge_unknown(self.pkg_vars)
                return variant.VariantCombinations(variants,
                    satisfied=satisfied)

        def action_path(self):
                """Return the path to the file that generated this dependency.
                """

                return self.action.attrs["path"]

        def __cmp__(self, other):
                """Generic way of ordering two Dependency objects."""

                r = cmp(self.dep_key(), other.dep_key())
                if r == 0:
                        r = cmp(self.action_path(), other.action_path())
                if r == 0:
                        r = cmp(self.__class__.__name__,
                            other.__class__.__name__)
                return r

        def get_vars_str(self):
                """Produce a string representation of the variants that apply
                to the dependency."""

                if self.dep_vars is not None:
                        return " " + " ".join([
                            ("%s=%s" % (k, ",".join(self.dep_vars[k])))
                            for k in sorted(self.dep_vars.keys())
                        ])

                return ""

        @staticmethod
        def make_relative(path, dir):
                """If 'path' is an absolute path, make it relative to the
                directory path given, otherwise, make it relative to root."""
                if path.startswith(dir):
                        path = path[len(dir):]
                return path.lstrip("/")


class PublishingDependency(Dependency):
        """This class serves as a base for all dependencies.  It handles
        dependencies with multiple files, multiple paths, or both.

        File dependencies are stored either as a list of base_names and
        a list of run_paths, or are expanded, and stored as a list of
        full_paths to each file that could satisfy the dependency.
        """

        def __init__(self, action, base_names, run_paths, pkg_vars, proto_dir,
            kind, full_paths=None):
                """Construct a PublishingDependency object.

                'action' is the action which produced this dependency.

                'base_names' is the list of files of the dependency.

                'run_paths' is the list of directory paths to the file of the
                dependency.

                'pkg_vars' is the list of variants against which the package
                delivering the action was published.

                'proto_dir' is the proto area where the file the action delivers
                lives.  It may be None if the notion of a proto_dir is
                meaningless for a particular PublishingDependency.

                'kind' is the kind of dependency that this is.

                'full_paths' if not None, is used instead of the combination of
                'base_names' and 'run_paths' when defining dependencies where
                exact paths to files matter (for example, SMF dependencies which
                are satisfied by more than one SMF manifest are not searched for
                using the manifest base_name in a list of run_paths, unlike
                python modules, which use $PYTHONPATH.)  Specifying full_paths
                as well as base_names/run_paths combinations is not allowed.
                """

                if full_paths and (base_names or run_paths):
                        # this should never happen, as consumers should always
                        # construct PublishingDependency objects using either
                        # full_paths or a combination of base_names and
                        # run_paths.
                        raise InvalidPublishingDependency(
                            "A dependency was specified using full_paths=%s as "
                            "well as base_names=%s and run_paths=%s" %
                            (full_paths, base_names, run_paths))

                self.base_names = sorted(base_names)

                if full_paths == None:
                        self.full_paths = []
                else:
                        self.full_paths = full_paths

                if proto_dir is None:
                        self.run_paths = sorted(run_paths)
                        # proto_dir is set to "" so that the proto_dir can be
                        # joined unconditionally with other paths.  This makes
                        # the code path in _check_path simpler.
                        proto_dir = ""
                else:
                        self.run_paths = sorted([
                            self.make_relative(rp, proto_dir)
                            for rp in run_paths
                        ])

                attrs = {"%s.type" % self.DEPEND_DEBUG_PREFIX: kind}
                if self.full_paths:
                        attrs["%s.fullpath" % self.DEPEND_DEBUG_PREFIX] = \
                            self.full_paths
                else:
                        attrs.update({
                            "%s.file" %
                            self.DEPEND_DEBUG_PREFIX: self.base_names,
                            "%s.path" %
                            self.DEPEND_DEBUG_PREFIX: self.run_paths,
                        })

                Dependency.__init__(self, action, pkg_vars, proto_dir, attrs)

        def dep_key(self):
                """Return the a value that represents the path of the
                dependency. It must be hashable."""
                if self.full_paths:
                        return (tuple(self.full_paths))
                else:
                        return (tuple(self.base_names), tuple(self.run_paths))

        def _check_path(self, path_to_check, delivered_files):
                """Takes a dictionary of files that are known to exist, and
                returns the path to the file that satisfies this dependency, or
                None if no such delivered file exists."""

                # Using normpath and realpath are ok here because the dependency
                # is being checked against the files, directories, and links
                # delivered in the proto area.
                if path_to_check in delivered_files:
                        return path_to_check
                norm_path = os.path.normpath(os.path.join(self.proto_dir,
                    path_to_check))
                if norm_path in delivered_files:
                        return norm_path

                real_path = os.path.realpath(norm_path)
                if real_path in delivered_files:
                        return real_path

                return None

        def possibly_delivered(self, delivered_files, links, resolve_links,
            orig_dep_vars):
                """Finds a list of files which satisfy this dependency, and the
                variants under which each file satisfies it.  It takes into
                account links and hardlinks.

                'delivered_files' is a dictionary which maps paths to the
                packages that deliver the path and the variants under which the
                path is present.

                'links' is an Entries namedtuple which contains two
                dictionaries.  One dictionary maps package identity to the links
                that it delivers.  The other dictionary, in this case, should be
                empty.

                'resolve_links' is a function which finds the real paths that a
                path can resolve into, given a set of known links.

                'orig_dep_vars' is the set of variants under which this
                dependency exists."""

                res = []
                # A dependency may be built using this dictionary of attributes.
                # Seeding it with the type is necessary to create a Dependency
                # object.
                attrs = {
                        "type":"require"
                }
                def process_path(path_to_check):
                        res = []
                        # Find the potential real paths that path_to_check could
                        # resolve to.
                        res_pths, res_links = resolve_links(
                            path_to_check, delivered_files, links,
                            orig_dep_vars, attrs)
                        for res_pth, res_pfmri, nearest_fmri, res_vc, \
                            res_via_links in res_pths:
                                p = self._check_path(res_pth, delivered_files)
                                if p:
                                        res.append((p, res_vc))
                        return res

                # if this is an expanded dependency, we iterate over the list of
                # full paths
                if self.full_paths:
                        for path_to_check in self.full_paths:
                                res.extend(process_path(path_to_check))

                # otherwise, it's a dependency with run_path and base_names
                # entries
                else:
                        for bn in self.base_names:
                                for rp in self.run_paths:
                                        path_to_check = os.path.normpath(
                                            os.path.join(rp, bn))
                                        res.extend(process_path(path_to_check))
                return res

        def resolve_internal(self, delivered_files, links, resolve_links, *args,
            **kwargs):
                """Determines whether this dependency (self) can be satisfied by
                the other items in the package which delivers it.  A tuple of
                two values is produced.  The first is either None, meaning the
                dependency was satisfied, or self.ERROR, meaning the dependency
                wasn't totally satisfied by the delivered files.  The second
                value is the set of variants for which the dependency isn't
                satisfied.

                'delivered_files' is a dictionary which maps package identity
                to the files the package delivers.

                'links' is an Entries namedtuple which contains two
                dictionaries.  One dictionary maps package identity to the links
                that it delivers.  The other dictionary, in this case, should be
                empty.

                'resolve_links' is a function which finds the real paths a path
                can resolve into given a set of known links.

                '*args' and '**kwargs' are used because subclasses may need
                more information for their implementations. See pkg.flavor.elf
                for an example of this."""

                missing_vars = self.get_variant_combinations()
                orig_dep_vars = self.get_variant_combinations()
                for p, vc in self.possibly_delivered(delivered_files, links,
                    resolve_links, orig_dep_vars):
                        missing_vars.mark_as_satisfied(vc)
                        if missing_vars.is_satisfied():
                                return None, missing_vars
                return self.ERROR, missing_vars


def insert_default_runpath(default_runpath, run_paths):
        """Insert our default search path where the PD_DEFAULT_PATH token was
        found, returning an updated list of run paths."""
        try:
                new_paths = run_paths
                index = run_paths.index(PD_DEFAULT_RUNPATH)
                new_paths = run_paths[:index] + \
                    default_runpath + run_paths[index + 1:]
                if PD_DEFAULT_RUNPATH in new_paths:
                        raise MultipleDefaultRunpaths()
                return new_paths

        except ValueError:
                # no PD_DEFAULT_PATH token, so we override the
                # whole default search path
                return run_paths