Source code for craft_platforms._platforms

# This file is part of craft-platforms.
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.
"""Platform related models."""

import itertools
import typing
from typing import Collection, Dict, List, Optional, Sequence, Tuple, Union

import annotated_types
from typing_extensions import Annotated

from craft_platforms import _architectures, _buildinfo, _distro, _errors, _utils

RESERVED_PLATFORM_NAMES = frozenset(
    (
        "any",  # "any" is used for `for` grammar.
        "*",  # This could be confused for "all".
    )
)

PlatformDict = typing.TypedDict(
    "PlatformDict",
    {
        "build-on": Union[Sequence[str], str],
        "build-for": Union[Annotated[Sequence[str], annotated_types.Len(1)], str],
    },
)
"""The platforms where an artifact is built and where the resulting artifact runs."""


Platforms = Dict[Union[_architectures.DebianArchitecture, str], Optional[PlatformDict]]
"""A mapping of platforms names to ``PlatformDicts``.

A ``PlatformDict`` is not required if the platform name is a supported Debian architecture.
"""


[docs] def get_platforms_build_plan( base: Union[str, _distro.DistroBase], platforms: Platforms, build_base: Optional[str] = None, *, allow_all_and_architecture_dependent: bool = False, ) -> Sequence[_buildinfo.BuildInfo]: """Generate the build plan for a platforms-based artifact. :param base: The target base :param platforms: A dictionary of the platforms. :param build_base: The build base, if declared. :param allow_all_and_architecture_dependent: whether to allow architecture-dependent platforms and architecture-independent platforms to coexist. This does not change the fact that only one architecture-independent platform can exist. """ if isinstance(base, _distro.DistroBase): distro_base = base else: distro_base = _distro.DistroBase.from_str(build_base or base) build_plan: List[_buildinfo.BuildInfo] = [] used_reserved_names = RESERVED_PLATFORM_NAMES & platforms.keys() if used_reserved_names: if len(used_reserved_names) == 1: raise _errors.InvalidPlatformNameError( f"Platform name {next(iter(used_reserved_names))!r} is reserved.", resolution="Use a different platform name, perhaps 'all' for platform-agnostic artifacts.", ) used_reserved_names_str = ", ".join( f"{name!r}" for name in sorted(used_reserved_names) ) raise _errors.InvalidPlatformNameError( f"Reserved platform names used: {used_reserved_names_str}", resolution="Change the platform names for these platforms.", ) for platform_name, platform in platforms.items(): if platform is None: # This is a workaround for Python 3.10. # In python 3.12+ we can just check: # `if platform_name not in _architectures.DebianArchitecture` try: architecture = _architectures.DebianArchitecture(platform_name) except ValueError: raise _errors.InvalidDebianArchPlatformNameError( platform_name ) from None build_plan.append( _buildinfo.BuildInfo( platform=platform_name, build_on=architecture, build_for=architecture, build_base=distro_base, ), ) else: for build_on, build_for in itertools.product( _utils.vectorize(platform["build-on"]), _utils.vectorize(platform.get("build-for", [platform_name])), ): build_plan.append( _buildinfo.BuildInfo( platform=platform_name, build_on=_architectures.DebianArchitecture(build_on), build_for=( "all" if build_for == "all" else _architectures.DebianArchitecture(build_for) ), build_base=distro_base, ), ) build_for_archs = {info.build_for for info in build_plan} if "all" in build_for_archs: _validate_build_for_all( build_plan, build_for_archs, allow_all_and_architecture_dependent=allow_all_and_architecture_dependent, ) return build_plan
def _validate_build_for_all( build_plan: Collection[_buildinfo.BuildInfo], build_for_archs: Collection[str], *, allow_all_and_architecture_dependent: bool, ) -> None: """Validate the build plan if there's a "build-for: all".""" platforms_with_all = { info.platform for info in build_plan if info.build_for == "all" } if allow_all_and_architecture_dependent: if len(platforms_with_all) > 1: raise _errors.AllInMultiplePlatformsError(platforms_with_all) platforms_with_arch_dependent = { info.platform for info in build_plan if info.build_for != "all" } platforms_with_all_and_another = ( platforms_with_all & platforms_with_arch_dependent ) if platforms_with_all_and_another: raise _errors.AllOnlyBuildInPlatformError(platforms_with_all_and_another) else: if len(platforms_with_all) > 1: raise _errors.AllSinglePlatformError(platforms_with_all) if len(build_for_archs) > 1: raise _errors.AllOnlyBuildError(platforms_with_all) def parse_base_and_name(platform_name: str) -> Tuple[Optional[_distro.DistroBase], str]: """Get the platform name and optional base from a platform name. The platform name may have an optional base prefix as '[<base>:]<platform>'. :param platform_name: The name of the platform. :returns: A tuple of the DistroBase and the platform name. :raises ValueError: If the base is invalid. """ if ":" in platform_name: base_str, _, name = platform_name.partition(":") base = _distro.DistroBase.from_str(base_str) else: base = None name = platform_name return base, name