Source code for craft_platforms._errors
# 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, 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]
[docs]@dataclasses.dataclass()
class CraftPlatformsError(Exception):
"""Signal a program error with a lot of information to report."""
message: str
"""The main message to the user about this error."""
details: Optional[str] = None
"""The full error details which originated the error situation."""
resolution: Optional[str] = None
"""An extra line indicating to the user how the error may be fixed or avoided (to be
shown together with ``message``)."""
docs_url: Optional[str] = None
"""An URL to point the user to documentation (to be shown together with ``message``)."""
doc_slug: Optional[str] = None
"""The slug to the user documentation. Needs a base url to form a full address.
Note that ``docs_url`` has preference if it is set."""
logpath_report: bool = True
"""Whether the location of the log filepath should be presented in the screen as the
final message."""
reportable: bool = True
"""If an error report should be sent to some error-handling backend (like Sentry)."""
retcode: int = 1
"""The code to return when the application finishes."""
def __post_init__(self) -> None:
super().__init__(self.message)
if self.doc_slug and not self.doc_slug.startswith("/"):
self.doc_slug = f"/{self.doc_slug}"
def __eq__(self, other: object) -> bool:
if isinstance(other, CraftPlatformsError):
return (
self.message == other.message
and self.details == other.details
and self.resolution == other.resolution
and self.docs_url == other.docs_url
and self.logpath_report == other.logpath_report
and self.reportable == other.reportable
and self.retcode == other.retcode
and self.doc_slug == other.doc_slug
)
if isinstance(other, CraftError) and isinstance(other, Exception):
if (
self.args != other.args
or self.details != other.details
or self.resolution != other.resolution
):
return False
for attr in (
"message",
"docs_url",
"docs_slug",
"logpath_report",
"reportable",
"retcode",
):
if hasattr(other, attr) and getattr(other, attr) != getattr(self, attr):
return False
return True
return NotImplemented
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'."""
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.",
)
[docs]class AllSinglePlatformError(BuildForAllError):
"""Error when multiple build-for architectures are defined, but one is 'all'."""
def __init__(
self,
platforms: Collection[str],
) -> None:
bfa_platforms = ",".join(platforms)
super().__init__(
message=f"build-for: all requires exactly one platform definition ({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 unspecified."""
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,
)
[docs]class InvalidPlatformNameError(CraftPlatformsError, ValueError):
"""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 InvalidPlatformError(CraftPlatformsError, ValueError):
"""Error when a specified platform is invalid."""
def __init__(
self,
platform_name: str,
*,
details: Optional[str] = None,
resolution: str,
docs_url: Optional[str] = None,
doc_slug: Optional[str] = None,
) -> None:
self.platform_name = platform_name
super().__init__(
message=f"platform {platform_name!r} is invalid",
details=details,
resolution=resolution,
docs_url=docs_url,
doc_slug=doc_slug,
)
[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."""