# 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/>.
"""Error classes for craft-platforms."""
import dataclasses
import os
import typing
from typing import Collection, Iterable, Literal, Optional
# Workaround for Windows...
EX_DATAERR = getattr(os, "EX_DATAERR", 65)
@typing.runtime_checkable
class CraftError(typing.Protocol):
"""A protocol for determining whether an object is a craft error."""
args: typing.Collection[str]
details: Optional[str]
resolution: Optional[str]
class EmptyBuildError(CraftPlatformsError, ValueError):
"""Errors where either build-on or build-for is empty."""
def __init__(self, platform: str, key: Literal["build-on", "build-for"]) -> None:
super().__init__(
message=f"'{key}' must contain an architecture",
details=f"No {key} architectures in platform '{platform}'",
resolution=f"Add a '{key}' architecture to platform '{platform}'",
)
class BuildForAllError(CraftPlatformsError, ValueError):
"""Errors related to build-for: all."""
[docs]
class AllOnlyBuildError(BuildForAllError):
"""Error when multiple build-for architectures are defined, but one is 'all'.
Example:
platforms:
this:
build-on: [riscv64]
build-for: [all]
that:
build-on: [arm64]
build-for: [all]
"""
def __init__(
self,
platforms: Iterable[str],
) -> None:
bfa_platforms = ",".join(platforms)
super().__init__(
message="build-for: all must be the only build-for architecture",
details=f"build-for: all defined in platforms: {bfa_platforms}",
resolution="Provide only one platform with only build-for: all or remove 'all' from build-for options.",
)
class AllOnlyBuildInPlatformError(BuildForAllError):
"""A single platform builds for all and an architecture.
Example:
platforms:
illegal:
build-on: [riscv64]
build-for: [riscv64, all]
"""
def __init__(
self,
platforms: Iterable[str],
) -> None:
bfa_platforms = ",".join(platforms)
super().__init__(
message="'all' must be the only build-for architecture in a platform",
details=f"invalid platforms: {bfa_platforms}",
resolution="Provide only one platform with only build-for: all or remove 'all' from build-for options.",
)
class AllInMultiplePlatformsError(BuildForAllError):
"""Error when build-for: 'all' exists in multiple platforms.
N.B. This only gets raised if platform-dependent and platform-independent builds
are allowed simultaneously. Otherwise ``AllSinglePlatformError`` is raised.
"""
def __init__(
self,
platforms: Collection[str],
) -> None:
bfa_platforms = ",".join(platforms)
super().__init__(
message=f"build-for: all can only be in one platform ({len(platforms)} provided)",
details=f"build-for: all defined in platforms: {bfa_platforms}",
resolution="Provide only one platform with only build-for: all or remove 'all' from build-for options.",
)
[docs]
class NeedBuildBaseError(CraftPlatformsError, ValueError):
"""Error when ``base`` requires a ``build_base``, but none is specified."""
def __init__(self, base: str) -> None:
super().__init__(
message=f"base '{base}' requires a 'build-base', but none is specified",
resolution="Specify a build-base.",
retcode=EX_DATAERR,
)
class InvalidDebianArchPlatformNameError(InvalidPlatformNameError):
"""Error when a specified platform name is not a Debian architecture."""
def __init__(self, platform_name: str) -> None:
self.platform_name = platform_name
super().__init__(
message=f"platform name {platform_name!r} is not a valid Debian architecture and needs 'build-on' and 'build-for' specified",
resolution=f"Specify 'build-on' and 'build-for' values under the {platform_name!r} entry.",
)
[docs]
class InvalidBaseError(CraftPlatformsError, ValueError):
"""Error when a specified base name is invalid."""
def __init__(
self,
base: str,
*,
message: Optional[str] = None,
resolution: Optional[str] = None,
docs_url: Optional[str] = None,
build_base: bool = False,
) -> None:
self.base = base
if resolution is None:
resolution = "Ensure the base matches the <distro>@<series> pattern and is a supported series."
if not message:
message = (
f"build-base '{base}' is unknown or invalid"
if build_base
else f"base '{base}' is unknown or invalid"
)
super().__init__(message=message, resolution=resolution, docs_url=docs_url)
[docs]
class RequiresBaseError(CraftPlatformsError, ValueError):
"""Error when a base is required in this configuration."""
class InvalidMultiBaseError(CraftPlatformsError, ValueError):
"""Error when a multi-base configuration is invalid."""