Source code for craft_platforms.charm._build
# 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/>.
"""Charmcraft-specific platforms information."""
import itertools
from typing import Collection, List, Optional, Sequence
from craft_platforms import (
_architectures,
_buildinfo,
_distro,
_errors,
_platforms,
_utils,
)
DEFAULT_ARCHITECTURES: Collection[_architectures.DebianArchitecture] = (
_architectures.DebianArchitecture.AMD64,
_architectures.DebianArchitecture.ARM64,
_architectures.DebianArchitecture.PPC64EL,
_architectures.DebianArchitecture.RISCV64,
_architectures.DebianArchitecture.S390X,
)
"""Default architectures for building a charm.
If no platforms are defined, the charm will be built on and for these architectures.
"""
def _validate_base_definition(
base: Optional[str],
build_base: Optional[str],
platform_name: Optional[str],
platform: Optional[_platforms.PlatformDict],
) -> None:
"""Validate that a base is defined correctly in the data used to create a build.
The rules are:
- a base must be defined in only one place
- each platform must build on and build for the same base
:raises ValueError: If the base is not defined correctly in the build data.
"""
if not (platform_name or base or build_base):
raise _errors.RequiresBaseError(
message="No base, build-base, or platforms are declared.",
resolution="Declare a base or build-base.",
)
if not platform_name:
return
# validate base defined in the platform name
platform_base, _ = _platforms.parse_base_and_name(platform_name=platform_name)
if platform:
if platform_base:
raise _errors.InvalidMultiBaseError(
message=(
f"Platform {platform_name!r} declares a base in the platform's "
"name and declares 'build-on' and 'build-for' entries."
),
resolution=(
"Either remove the base from the platform's name or remove the "
"'build-on' and 'build-for' entries for the platform."
),
)
# create a set of the bases defined in the build-on and build-for entries
bases = set()
for entry in [
*_utils.vectorize(platform["build-on"]),
*_utils.vectorize(platform["build-for"]),
]:
distro_base, _ = _architectures.parse_base_and_architecture(arch=entry)
bases.add(str(distro_base) if distro_base else None)
if len(bases) == 0:
# an empty set means no bases are defined
build_on_for_base = None
elif len(bases) == 1:
# a set with one element means the same base was defined for all entries
build_on_for_base = next(iter(bases))
else:
# otherwise there are multiple bases defined or some entries missing bases
raise _errors.InvalidMultiBaseError(
message=(
f"Platform {platform_name!r} has mismatched bases in the 'build-on' "
"and 'build-for' entries."
),
resolution=(
"Use the same base for all 'build-on' and 'build-for' entries for "
"the platform."
),
)
else:
build_on_for_base = None
if (platform_base or build_on_for_base) and (base or build_base):
raise _errors.InvalidMultiBaseError(
message=f"Platform {platform_name!r} declares a base and a top-level base "
"or build-base is declared.",
resolution=(
"Remove the base from the platform's name or remove the top-level base "
"or build-base."
),
)
if not (platform_base or build_on_for_base) and not (base or build_base):
raise _errors.RequiresBaseError(
message=(
"No base or build-base is declared and no base is declared "
"in the platforms section."
),
resolution="Declare a base or build-base.",
)
def _get_base_from_build_data(
base: Optional[str],
build_base: Optional[str],
platform_name: Optional[str],
platform: Optional[_platforms.PlatformDict],
) -> _distro.DistroBase:
"""Get the base from a data used to create a build.
:returns: The base to use for a build.
:raises ValueError: If the base is not defined correctly in the build data.
"""
_validate_base_definition(
base=base,
build_base=build_base,
platform_name=platform_name,
platform=platform,
)
if build_base:
return _distro.DistroBase.from_str(build_base)
if base:
return _distro.DistroBase.from_str(base)
if platform_name:
platform_base, _ = _platforms.parse_base_and_name(platform_name=platform_name)
if platform_base:
return platform_base
# build-on and build-for entries all have the same base, so we only
# need to check one of them
if platform:
build_for_base, _ = _architectures.parse_base_and_architecture(
arch=_utils.vectorize(platform["build-for"])[0]
)
if build_for_base:
return build_for_base
# if this is raised, then the validator is not working correctly
raise ValueError("Could not determine the base for the build.")
[docs]def get_platforms_charm_build_plan(
base: Optional[str],
platforms: Optional[_platforms.Platforms],
build_base: Optional[str] = None,
) -> Sequence[_buildinfo.BuildInfo]:
"""Generate the build plan for a platforms-based charm.
Platforms-based charms are charms that don't use the deprecated ``bases``
field in their ``charmcraft.yaml``.
Multi-base recipes are supported. A multi-base recipe defines the base
within the ``platform`` field instead of defining ``base`` and
``build-base``. For each platform, the base is either prefixed to the
platform name or prefixed to every ``build-on`` and ``build-for` entry.
In both cases, the prefixed base is delimited with a colon (``<base>:``).
:param base: The run-time environment for the charm, formatted as
``distribution@series``. If the ``build-base`` is unset, then the ``base``
determines the build environment.
:param build_base: The build environment to using when building the charm,
formatted as ``distribution@series``.
:param platforms: The mapping of platform names to ``PlatformDicts``. If
the ``base`` and ``build-base`` are unset, then the base must be defined
in the platforms.
:raises ValueError: If the build plan can't be created due to invalid base
and platform definitions.
:returns: A build plan describing the environments where the charm can build
and where the charm can run.
"""
if platforms is None:
distro_base = _get_base_from_build_data(
base=base,
build_base=build_base,
platform_name=None,
platform=None,
)
# If no platforms are specified, build for all default architectures without
# an option of cross-compiling.
return [
_buildinfo.BuildInfo(
platform=arch.value,
build_on=arch,
build_for=arch,
build_base=distro_base,
)
for arch in DEFAULT_ARCHITECTURES
]
build_plan: List[_buildinfo.BuildInfo] = []
for platform_name, platform in platforms.items():
distro_base = _get_base_from_build_data(
base=base,
build_base=build_base,
platform_name=platform_name,
platform=platform,
)
if platform is None:
_, arch_str = _platforms.parse_base_and_name(platform_name)
# This is a workaround for Python 3.10.
# In python 3.12+ we can just check:
# `if platform_name not in _architectures.DebianArchitecture`
try:
arch = _architectures.DebianArchitecture(arch_str)
except ValueError:
raise ValueError(
f"Platform name {platform_name!r} is not a valid Debian architecture. "
"Specify a build-on and build-for.",
) from None
build_plan.append(
_buildinfo.BuildInfo(
platform=platform_name,
build_on=arch,
build_for=arch,
build_base=distro_base,
),
)
else:
for build_on, build_for in itertools.product(
_utils.vectorize(platform["build-on"]),
_utils.vectorize(platform["build-for"]),
):
_, build_on_arch = _architectures.parse_base_and_architecture(
arch=build_on
)
if build_on_arch == "all":
raise ValueError(
f"Platform {platform_name!r} has an invalid 'build-on' entry of 'all'."
)
_, build_for_arch = _architectures.parse_base_and_architecture(
arch=build_for
)
build_plan.append(
_buildinfo.BuildInfo(
platform=platform_name,
build_on=build_on_arch,
build_for=build_for_arch,
build_base=distro_base,
),
)
return build_plan