Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
pantsbuild.pants / backend / jvm / subsystems / jvm_platform.py
Size: Mime:
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import logging
from functools import total_ordering

from pants.base.exceptions import TaskError
from pants.base.revision import Revision
from pants.java.distribution.distribution import DistributionLocator
from pants.option.option_util import flatten_shlexed_list
from pants.subsystem.subsystem import Subsystem
from pants.util.memo import memoized_method, memoized_property

logger = logging.getLogger(__name__)


class JvmPlatform(Subsystem):
    """Used to keep track of repo compile and runtime settings for jvm targets.

    JvmPlatform covers both compile time and runtime jvm platform settings. A platform is a group of
    compile time and runtime configurations.

    See src/docs/common_tasks/multiple_jvm_versions.md for more detail.
    """

    # NB: These assume a java version number N can be specified as either 'N' or '1.N'
    # (eg, '7' is equivalent to '1.7'). Java stopped following this convention starting with Java 9,
    # so this list does not go past it.
    SUPPORTED_CONVERSION_VERSIONS = (6, 7, 8)

    _COMPILER_CHOICES = ["zinc", "javac", "rsc"]

    class IncompatiblePlatforms(DistributionLocator.Error):
        """The provided platforms cannot all be accommodated."""

    class IllegalDefaultPlatform(TaskError):
        """The --default-platform option was set, but isn't defined in --platforms."""

    class UndefinedJvmPlatform(TaskError):
        """Platform isn't defined."""

        def __init__(self, target, platform_name, platforms_by_name):
            scope_name = JvmPlatform.options_scope
            messages = [
                'Undefined jvm platform "{}" (referenced by {}).'.format(
                    platform_name, target.address.spec if target else "unknown target"
                )
            ]
            if not platforms_by_name:
                messages.append(
                    "In fact, no platforms are defined under {0}. These should typically be"
                    " specified in [{0}] in pants.ini.".format(scope_name)
                )
            else:
                messages.append(
                    "Perhaps you meant one of:{}".format(
                        "".join("\n  {}".format(name) for name in sorted(platforms_by_name.keys()))
                    )
                )
                messages.append(
                    "\nThese are typically defined under [{}] in pants.ini.".format(scope_name)
                )
            super(JvmPlatform.UndefinedJvmPlatform, self).__init__(" ".join(messages))

    options_scope = "jvm-platform"

    @classmethod
    def register_options(cls, register):
        super().register_options(register)
        register(
            "--platforms",
            advanced=True,
            type=dict,
            default={},
            fingerprint=True,
            help="Compile settings that can be referred to by name in jvm_targets.",
        )
        register(
            "--default-platform",
            advanced=True,
            type=str,
            default=None,
            fingerprint=True,
            help="Name of the default platform to use for compilation. If default-runtime-platform"
            " is None, also applies to runtime. Used when targets leave platform unspecified.",
        )
        register(
            "--default-runtime-platform",
            advanced=True,
            type=str,
            default=None,
            fingerprint=True,
            help="Name of the default runtime platform. Used when targets leave runtime_platform"
            " unspecified.",
        )
        register(
            "--compiler",
            advanced=True,
            choices=cls._COMPILER_CHOICES,
            default="rsc",
            fingerprint=True,
            help="Java compiler implementation to use.",
        )

    @classmethod
    def subsystem_dependencies(cls):
        return super().subsystem_dependencies() + (DistributionLocator,)

    def _parse_platform(self, name, platform):
        return JvmPlatformSettings(
            source_level=platform.get("source", platform.get("target")),
            target_level=platform.get("target", platform.get("source")),
            args=platform.get("args", ()),
            jvm_options=platform.get("jvm_options", ()),
            strict=platform.get("strict", None),
            name=name,
        )

    @classmethod
    def preferred_jvm_distribution(cls, platforms, strict=None, jdk=False):
        """Returns a jvm Distribution with a version that should work for all the platforms.

        Any one of those distributions whose version is >= all requested platforms' versions
        can be returned unless strict flag is set.

        :param iterable platforms: An iterable of platform settings.
        :param bool strict: If true, only distribution whose version matches the minimum
          required version can be returned, i.e, the max target_level of all the requested
          platforms. If false, platforms with the strict attribute set to true are treated as
          though they are set to false.
        :param bool jdk: If true, the distribution must be a JDK.
        :returns: Distribution one of the selected distributions.
        """
        return DistributionLocator.cached(
            **cls._preferred_jvm_distribution_args(platforms, strict, jdk)
        )

    @classmethod
    def _preferred_jvm_distribution_args(cls, platforms, strict=None, jdk=False):
        if not platforms:
            return {"jdk": jdk}
        if strict is False:  # treat all the platforms as non-strict
            min_version = max(p.target_level for p in platforms)
            set_max_version = False
        else:
            # treat strict platforms as strict & ensure no non-strict directive is broken
            strict_target_levels = {p.target_level for p in platforms if p.strict}
            lenient_target_levels = {p.target_level for p in platforms if not p.strict}

            if len(strict_target_levels) == 0:
                min_version = max(lenient_target_levels)
                set_max_version = strict
            elif len(strict_target_levels) > 1:
                differing = ", ".join(str(t) for t in strict_target_levels)
                raise cls.IncompatiblePlatforms(
                    f"Multiple strict platforms with differing target releases were found:"
                    f" {differing}"
                )
            else:
                if len(lenient_target_levels) == 0:
                    min_version = next(iter(strict_target_levels))
                    set_max_version = True
                else:
                    strict_level = next(iter(strict_target_levels))
                    non_strict_max = max(t for t in lenient_target_levels)
                    if non_strict_max and non_strict_max > strict_level:
                        raise cls.IncompatiblePlatforms(
                            f"lenient platform with higher minimum version,"
                            f" {non_strict_max}, than strict requirement of"
                            f" {strict_level}"
                        )
                    min_version = strict_level
                    set_max_version = True

        if len(min_version.components) <= 2:  # ensure at least three components.
            min_version = Revision(
                *(min_version.components + [0] * (3 - len(min_version.components)))
            )
        max_version = None
        if set_max_version:
            max_version = Revision(*(min_version.components[0:2] + [9999]))
        return {"minimum_version": min_version, "maximum_version": max_version, "jdk": jdk}

    @memoized_property
    def platforms_by_name(self):
        platforms = self.get_options().platforms or {}
        return {name: self._parse_platform(name, platform) for name, platform in platforms.items()}

    @property
    def _fallback_platform(self):
        logger.warning("No default jvm platform is defined.")
        source_level = JvmPlatform.parse_java_version(DistributionLocator.cached().version)
        target_level = source_level
        platform_name = f"(DistributionLocator.cached().version {source_level})"
        return JvmPlatformSettings(
            source_level=source_level,
            target_level=target_level,
            args=[],
            jvm_options=[],
            name=platform_name,
        )

    @memoized_property
    def default_platform(self):
        name = self.get_options().default_platform
        if not name:
            return self._fallback_platform
        platforms_by_name = self.platforms_by_name
        if name not in platforms_by_name:
            raise self.IllegalDefaultPlatform(
                "The default platform was set to '{0}', but no platform by that name has been "
                "defined. Typically, this should be defined under [{1}] in pants.ini.".format(
                    name, self.options_scope
                )
            )
        return JvmPlatformSettings._copy_as_default(platforms_by_name[name], name=name)

    @memoized_property
    def default_runtime_platform(self):
        name = self.get_options().default_runtime_platform
        if not name:
            return self.default_platform
        platforms_by_name = self.platforms_by_name
        if name not in platforms_by_name:
            raise self.IllegalDefaultPlatform(
                "The default runtime platform was set to '{0}', but no platform by that name has been "
                "defined. Typically, this should be defined under [{1}] in pants.ini.".format(
                    name, self.options_scope
                )
            )
        return JvmPlatformSettings._copy_as_default(platforms_by_name[name], name=name)

    @memoized_method
    def get_platform_by_name(self, name, for_target=None):
        """Finds the platform with the given name.

        If the name is empty or None, returns the default platform.
        If not platform with the given name is defined, raises an error.
        :param str name: name of the platform.
        :param JvmTarget for_target: optionally specified target we're looking up the platform for.
          Only used in error message generation.
        :return: The jvm platform object.
        :rtype: JvmPlatformSettings
        """
        if not name:
            return self.default_platform
        if name not in self.platforms_by_name:
            raise self.UndefinedJvmPlatform(for_target, name, self.platforms_by_name)
        return self.platforms_by_name[name]

    def get_platform_for_target(self, target):
        """Find the platform associated with this target.

        :param JvmTarget target: target to query.
        :return: The jvm platform object.
        :rtype: JvmPlatformSettings
        """
        if not target.payload.platform and target.is_synthetic:
            derived_from = target.derived_from
            platform = derived_from and getattr(derived_from, "platform", None)
            if platform:
                return platform
        return self.get_platform_by_name(target.payload.platform, target)

    def get_runtime_platform_for_target(self, target):
        """Find the runtime platform associated with this target.

        :param JvmTarget,RuntimePlatformMixin target: target to query.
        :return: The jvm platform object.
        :rtype: JvmPlatformSettings
        """
        # Lookup order
        # - target's declared runtime_platform
        # - default runtime_platform
        # - target's declared platform
        # - default platform
        target_runtime_platform = target.payload.runtime_platform
        if not target_runtime_platform and target.is_synthetic:
            derived_from = target.derived_from
            platform = derived_from and getattr(derived_from, "runtime_platform", None)
            if platform:
                return platform
        if target_runtime_platform:
            return self.get_platform_by_name(target_runtime_platform, target)
        elif self.default_runtime_platform:
            return self.default_runtime_platform
        else:
            return self.get_platform_for_target(target)

    @classmethod
    def parse_java_version(cls, version):
        """Parses the java version (given a string or Revision object).

        Handles java version-isms, converting things like '7' -> '1.7' appropriately.

        Truncates input versions down to just the major and minor numbers (eg, 1.6), ignoring extra
        versioning information after the second number.

        :param version: the input version, given as a string or Revision object.
        :return: the parsed and cleaned version, suitable as a javac -source or -target argument.
        :rtype: Revision
        """
        conversion = {str(i): f"1.{i}" for i in cls.SUPPORTED_CONVERSION_VERSIONS}
        if str(version) in conversion:
            return Revision.lenient(conversion[str(version)])

        if not hasattr(version, "components"):
            version = Revision.lenient(version)
        if len(version.components) <= 2:
            return version
        return Revision(*version.components[:2])


@total_ordering
class JvmPlatformSettings:
    """Simple information holder to keep track of common arguments to java compilers."""

    class IllegalSourceTargetCombination(TaskError):
        """Illegal pair of -source and -target flags to compile java."""

    @staticmethod
    def _copy_as_default(original, name):
        """Copies the original with a new name, setting by_default to True."""
        return JvmPlatformSettings(
            source_level=original.source_level,
            target_level=original.target_level,
            args=original.args,
            jvm_options=original.jvm_options,
            name=name,
            by_default=True,
        )

    def __init__(
        self,
        *,
        source_level,
        target_level,
        args,
        jvm_options,
        strict=False,
        name=None,
        by_default=False,
    ):
        """
    :param source_level: Revision object or string for the java source level.
    :param target_level: Revision object or string for the java target level.
    :param list args: Additional arguments to pass to the java compiler.
    :param list jvm_options: Additional jvm options specific to this JVM platform.
    :param boolean strict: Whether to use the target level as a lower bound or an exact requirement.
    :param str name: name to identify this platform.
    :param by_default: True if this value was inferred by omission of a specific platform setting.
    """
        self.source_level = JvmPlatform.parse_java_version(source_level)
        self.target_level = JvmPlatform.parse_java_version(target_level)
        self.args = tuple(flatten_shlexed_list(args or ()))
        self.jvm_options = tuple(flatten_shlexed_list(jvm_options or ()))
        self.strict = strict
        self.name = name
        self._by_default = by_default
        self._validate_source_target()

    def _validate_source_target(self):
        if self.source_level > self.target_level:
            if self.by_default:
                name = f"{self.name} (by default)"
            else:
                name = self.name
            raise self.IllegalSourceTargetCombination(
                "Platform {platform} has java source level {source_level} but target level {target_level}.".format(
                    platform=name, source_level=self.source_level, target_level=self.target_level
                )
            )

    @property
    def by_default(self):
        return self._by_default

    def _tuple(self):
        return (
            self.source_level,
            self.target_level,
            self.args,
            self.jvm_options,
        )

    def __eq__(self, other):
        return self._tuple() == other._tuple()

    # TODO(#6071): decide if this should raise NotImplemented on invalid comparisons
    def __lt__(self, other):
        return self._tuple() < other._tuple()

    def __hash__(self):
        return hash(self._tuple())

    def __str__(self):
        return (
            "JvmPlatformSettings(source={source},target={target},args=({args}),"
            "jvm_options={jvm_options},strict={strict})".format(
                source=self.source_level,
                target=self.target_level,
                args=" ".join(self.args),
                jvm_options=" ".join(self.jvm_options),
                strict=self.strict,
            )
        )