# 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 Any, Collection, Dict, Iterable, 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.")
def _gen_build_plan_for_base(base: Dict[str, Any]) -> Iterable[_buildinfo.BuildInfo]:
if "build-on" not in base:
base = {"build-on": [base], "run-on": [base]}
for build_base in base["build-on"]:
build_archs = build_base.get("architectures", DEFAULT_ARCHITECTURES)
for run_base, build_arch in itertools.product(base["run-on"], build_archs):
run_archs = run_base.get("architectures", [build_arch])
run_archs_str = "-".join(run_archs)
yield _buildinfo.BuildInfo(
f"{build_base['name']}-{build_base['channel']}-{run_archs_str}",
build_on=_architectures.DebianArchitecture(build_arch),
build_for=run_archs[0],
build_base=_distro.DistroBase(
build_base["name"], build_base["channel"]
),
)
[docs]
def get_bases_charm_build_plan(
bases: Sequence[Dict[str, Any]],
) -> Sequence[_buildinfo.BuildInfo]:
"""Get a build plan for a legacy "bases" based charm."""
plan: List[_buildinfo.BuildInfo] = []
for base in bases:
plan.extend(_gen_build_plan_for_base(base))
return plan
def get_charm_build_plan(
project_data: Dict[str, Any],
) -> Sequence[_buildinfo.BuildInfo]:
if "platforms" in project_data:
return get_platforms_charm_build_plan(
base=project_data.get("base"),
build_base=project_data.get("build-base"),
platforms=project_data.get(
"platforms",
),
)
if "bases" in project_data:
return get_bases_charm_build_plan(project_data["bases"])
raise NotImplementedError("Unknown charm type with no bases or platforms.")