Source code for craft_parts.errors

# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2021-2023 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 warranty of
# MERCHANTABILITY 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/>.

"""Craft parts errors."""

import dataclasses
import pathlib
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Union

if TYPE_CHECKING:
    from pydantic.error_wrappers import ErrorDict, Loc


[docs]@dataclasses.dataclass(repr=True) class PartsError(Exception): """Unexpected error. :param brief: Brief description of error. :param details: Detailed information. :param resolution: Recommendation, if any. """ brief: str details: Optional[str] = None resolution: Optional[str] = None def __str__(self) -> str: components = [self.brief] if self.details: components.append(self.details) if self.resolution: components.append(self.resolution) return "\n".join(components)
[docs]class FeatureError(PartsError): """A feature is not configured as expected.""" def __init__(self, message: str) -> None: self.message = message brief = message resolution = "This operation cannot be executed." super().__init__(brief=brief, resolution=resolution)
[docs]class PartDependencyCycle(PartsError): """A dependency cycle has been detected in the parts definition.""" def __init__(self) -> None: brief = "A circular dependency chain was detected." resolution = "Review the parts definition to remove dependency cycles." super().__init__(brief=brief, resolution=resolution)
[docs]class InvalidApplicationName(PartsError): """The application name contains invalid characters. :param name: The invalid application name. """ def __init__(self, name: str): self.name = name brief = f"Application name {name!r} is invalid." resolution = ( "Valid application names contain letters, underscores or numbers, " "and must start with a letter." ) super().__init__(brief=brief, resolution=resolution)
[docs]class InvalidPartName(PartsError): """An operation was requested on a part that's not in the parts specification. :param part_name: The invalid part name. """ def __init__(self, part_name: str): self.part_name = part_name brief = f"A part named {part_name!r} is not defined in the parts list." resolution = "Review the parts definition and make sure it's correct." super().__init__(brief=brief, resolution=resolution)
[docs]class InvalidArchitecture(PartsError): """The machine architecture is not supported. :param arch_name: The unsupported architecture name. """ def __init__(self, arch_name: str): self.arch_name = arch_name brief = f"Architecture {arch_name!r} is not supported." resolution = "Make sure the architecture name is correct." super().__init__(brief=brief, resolution=resolution)
[docs]class PartSpecificationError(PartsError): """A part was not correctly specified. :param part_name: The name of the part being processed. :param message: The error message. """ def __init__(self, *, part_name: str, message: str): self.part_name = part_name self.message = message brief = f"Part {part_name!r} validation failed." details = message resolution = f"Review part {part_name!r} and make sure it's correct." super().__init__(brief=brief, details=details, resolution=resolution)
[docs] @classmethod def from_validation_error( cls, *, part_name: str, error_list: List["ErrorDict"] ) -> "PartSpecificationError": """Create a PartSpecificationError from a pydantic error list. :param part_name: The name of the part being processed. :param error_list: A list of dictionaries containing pydantic error definitions. """ formatted_errors: List[str] = [] for error in error_list: loc = error.get("loc") msg = error.get("msg") if not (loc and msg) or not isinstance(loc, tuple): continue field = cls._format_loc(loc) if msg == "field required": formatted_errors.append(f"- field {field!r} is required") elif msg == "extra fields not permitted": formatted_errors.append(f"- extra field {field!r} not permitted") else: formatted_errors.append(f"- {msg} in field {field!r}") return cls(part_name=part_name, message="\n".join(formatted_errors))
@classmethod def _format_loc(cls, loc: "Loc") -> str: """Format location.""" loc_parts = [] for loc_part in loc: if isinstance(loc_part, str): loc_parts.append(loc_part) elif isinstance(loc_part, int): # Integer indicates an index. Go back and fix up previous part. previous_part = loc_parts.pop() previous_part += f"[{loc_part}]" loc_parts.append(previous_part) else: raise RuntimeError(f"unhandled loc: {loc_part}") loc_str = ".".join(loc_parts) # Filter out internal __root__ detail. loc_str = loc_str.replace(".__root__", "") return loc_str
[docs]class CopyTreeError(PartsError): """Failed to copy or link a file tree. :param message: The error message. """ def __init__(self, message: str): self.message = message brief = f"Failed to copy or link file tree: {message}." resolution = "Make sure paths and permissions are correct." super().__init__(brief=brief, resolution=resolution)
[docs]class CopyFileNotFound(PartsError): """An attempt was made to copy a file that doesn't exist. :param name: The file name. """ def __init__(self, name: str): self.name = name brief = f"Failed to copy {name!r}: no such file or directory." super().__init__(brief=brief)
[docs]class XAttributeError(PartsError): """Failed to read or write an extended attribute. :param action: The action being performed. :param key: The extended attribute key. :param path: The file path. :param is_write: Whether this is an attribute write operation. """ def __init__(self, key: str, path: str, is_write: bool = False): self.key = key self.path = path self.is_write = is_write action = "write" if is_write else "read" brief = f"Unable to {action} extended attribute." details = f"Failed to {action} attribute {key!r} on {path!r}." resolution = "Make sure your filesystem supports extended attributes." super().__init__(brief=brief, details=details, resolution=resolution)
[docs]class XAttributeTooLong(PartsError): """Failed to write an extended attribute because key and/or value is too long. :param key: The extended attribute key. :param value: The extended attribute value. :param path: The file path. """ def __init__(self, key: str, value: str, path: str): self.key = key self.value = value self.path = path brief = "Failed to write attribute: key and/or value is too long." details = f"key={key!r}, value={value!r}" super().__init__(brief=brief, details=details)
[docs]class UndefinedPlugin(PartsError): """The part didn't define a plugin and the part name is not a valid plugin name. :param part_name: The name of the part with no plugin definition. """ def __init__(self, *, part_name: str): self.part_name = part_name brief = f"Plugin not defined for part {part_name!r}." resolution = f"Review part {part_name!r} and make sure it's correct." super().__init__(brief=brief, resolution=resolution)
[docs]class InvalidPlugin(PartsError): """A request was made to use a plugin that's not registered. :param plugin_name: The invalid plugin name." :param part_name: The name of the part defining the invalid plugin. """ def __init__(self, plugin_name: str, *, part_name: str): self.plugin_name = plugin_name self.part_name = part_name brief = f"Plugin {plugin_name!r} in part {part_name!r} is not registered." resolution = f"Review part {part_name!r} and make sure it's correct." super().__init__(brief=brief, resolution=resolution)
[docs]class PluginNotStrict(PartsError): """A request was made to use a plugin that's not strict. :param plugin_name: The plugin name. :param part_name: The name of the part defining the plugin. """ def __init__(self, plugin_name: str, *, part_name: str): self.plugin_name = plugin_name self.part_name = part_name brief = f"Plugin {plugin_name!r} in part {part_name!r} cannot be used." details = ( "Only plugins that are capable of building in strict mode are allowed." ) super().__init__(brief=brief, details=details)
[docs]class OsReleaseIdError(PartsError): """Failed to determine the host operating system identification string.""" def __init__(self) -> None: brief = "Unable to determine the host operating system ID." super().__init__(brief=brief)
[docs]class OsReleaseNameError(PartsError): """Failed to determine the host operating system name.""" def __init__(self) -> None: brief = "Unable to determine the host operating system name." super().__init__(brief=brief)
[docs]class OsReleaseVersionIdError(PartsError): """Failed to determine the host operating system version.""" def __init__(self) -> None: brief = "Unable to determine the host operating system version ID." super().__init__(brief=brief)
[docs]class OsReleaseCodenameError(PartsError): """Failed to determine the host operating system version codename.""" def __init__(self) -> None: brief = "Unable to determine the host operating system codename." super().__init__(brief=brief)
[docs]class FilesetError(PartsError): """An invalid fileset operation was performed. :param name: The name of the fileset. :param message: The error message. """ def __init__(self, *, name: str, message: str) -> None: self.name = name self.message = message brief = f"{name!r} fileset error: {message}." resolution = "Review the parts definition and make sure it's correct." super().__init__(brief=brief, resolution=resolution)
[docs]class FilesetConflict(PartsError): """Inconsistent stage to prime filtering. :param conflicting_files: A set containing the conflicting file names. """ def __init__(self, conflicting_files: Set[str]) -> None: self.conflicting_files = conflicting_files brief = "Failed to filter files: inconsistent 'stage' and 'prime' filesets." details = ( f"The following files have been excluded in the 'stage' fileset, " f"but included by the 'prime' fileset: {conflicting_files!r}." ) resolution = ( "Make sure that the files included in 'prime' are also included in 'stage'." ) super().__init__(brief=brief, details=details, resolution=resolution)
[docs]class FileOrganizeError(PartsError): """Failed to organize a file layout. :param part_name: The name of the part being processed. :param message: The error message. """ def __init__(self, *, part_name: str, message: str) -> None: self.part_name = part_name self.message = message brief = f"Failed to organize part {part_name!r}: {message}." super().__init__(brief=brief)
[docs]class PartFilesConflict(PartsError): """Different parts list the same files with different contents. :param part_name: The name of the part being processed. :param other_part_name: The name of the conflicting part. :param conflicting_files: The list of conflicting files. """ def __init__( self, *, part_name: str, other_part_name: str, conflicting_files: List[str] ) -> None: self.part_name = part_name self.other_part_name = other_part_name self.conflicting_files = conflicting_files indented_conflicting_files = (f" {i}" for i in conflicting_files) file_paths = "\n".join(sorted(indented_conflicting_files)) brief = ( "Failed to stage: parts list the same file " "with different contents or permissions." ) details = ( f"Parts {part_name!r} and {other_part_name!r} list the following " f"files, but with different contents or permissions:\n" f"{file_paths}" ) super().__init__(brief=brief, details=details)
[docs]class StageFilesConflict(PartsError): """Files from a part conflict with files already being staged. :param part_name: The name of the part being processed. :param conflicting_files: The list of confictling files. """ def __init__(self, *, part_name: str, conflicting_files: List[str]) -> None: self.part_name = part_name self.conflicting_files = conflicting_files indented_conflicting_files = (f" {i}" for i in conflicting_files) file_paths = "\n".join(sorted(indented_conflicting_files)) brief = "Failed to stage: part files conflict with files already being staged." details = ( f"The following files in part {part_name!r} are already being staged " f"with different content:\n" f"{file_paths}" ) super().__init__(brief=brief, details=details)
[docs]class PluginEnvironmentValidationError(PartsError): """Plugin environment validation failed at runtime. :param part_name: The name of the part being processed. """ def __init__(self, *, part_name: str, reason: str): self.part_name = part_name self.reason = reason brief = f"Environment validation failed for part {part_name!r}: {reason}." super().__init__(brief=brief)
[docs]class PluginPullError(PartsError): """Plugin pull script failed at runtime. :param part_name: The name of the part being processed. """ def __init__(self, *, part_name: str): self.part_name = part_name brief = f"Failed to run the pull script for part {part_name!r}." super().__init__(brief=brief)
[docs]class PluginBuildError(PartsError): """Plugin build script failed at runtime. :param part_name: The name of the part being processed. """ def __init__(self, *, part_name: str): self.part_name = part_name brief = f"Failed to run the build script for part {part_name!r}." super().__init__(brief=brief)
[docs]class PluginCleanError(PartsError): """Script to clean strict build preparation failed at runtime. :param part_name: The name of the part being processed. """ def __init__(self, *, part_name: str): self.part_name = part_name brief = f"Failed to run the clean script for part {part_name!r}." super().__init__(brief=brief)
[docs]class InvalidControlAPICall(PartsError): """A control API call was made with invalid parameters. :param part_name: The name of the part being processed. :param scriptlet_name: The name of the scriptlet that originated the call. :param message: The error message. """ def __init__(self, *, part_name: str, scriptlet_name: str, message: str): self.part_name = part_name self.scriptlet_name = scriptlet_name self.message = message brief = ( f"{scriptlet_name!r} in part {part_name!r} executed an invalid control " f"API call: {message}." ) resolution = "Review the scriptlet and make sure it's correct." super().__init__(brief=brief, resolution=resolution)
[docs]class ScriptletRunError(PartsError): """A scriptlet execution failed. :param part_name: The name of the part being processed. :param scriptlet_name: The name of the scriptlet that failed to execute. :param exit_code: The execution error code. """ def __init__(self, *, part_name: str, scriptlet_name: str, exit_code: int): self.part_name = part_name self.scriptlet_name = scriptlet_name self.exit_code = exit_code brief = ( f"{scriptlet_name!r} in part {part_name!r} failed with code {exit_code}." ) resolution = "Review the scriptlet and make sure it's correct." super().__init__(brief=brief, resolution=resolution)
[docs]class CallbackRegistrationError(PartsError): """Error in callback function registration. :param message: the error message. """ def __init__(self, message: str): self.message = message brief = f"Callback registration error: {message}." super().__init__(brief=brief)
[docs]class StagePackageNotFound(PartsError): """Failed to install a stage package. :param part_name: The name of the part being processed. :param package_name: The name of the package. """ def __init__(self, *, part_name: str, package_name: str): self.part_name = part_name self.package_name = package_name brief = f"Stage package not found in part {part_name!r}: {package_name}." super().__init__(brief=brief)
[docs]class OverlayPackageNotFound(PartsError): """Failed to install an overlay package. :param part_name: The name of the part being processed. :param message: the error message. """ def __init__(self, *, part_name: str, package_name: str): self.part_name = part_name self.package_name = package_name brief = f"Overlay package not found in part {part_name!r}: {package_name}." super().__init__(brief=brief)
[docs]class InvalidAction(PartsError): """An attempt was made to execute an action with invalid parameters. :param message: The error message. """ def __init__(self, message: str): self.message = message brief = f"Action is invalid: {message}." super().__init__(brief=brief)
[docs]class OverlayPlatformError(PartsError): """A project using overlays was processed on a non-Linux platform.""" def __init__(self) -> None: brief = "The overlay step is only supported on Linux." super().__init__(brief=brief)
[docs]class OverlayPermissionError(PartsError): """A project using overlays was processed by a non-privileged user.""" def __init__(self) -> None: brief = "Using the overlay step requires superuser privileges." super().__init__(brief=brief)
[docs]class DebError(PartsError): """A "deb"-related command failed.""" def __init__( self, deb_path: pathlib.Path, command: List[str], exit_code: int ) -> None: brief = ( f"Failed when handling {deb_path}: " f"command {command!r} exited with code {exit_code}." ) resolution = "Make sure the deb file is correctly specified." super().__init__(brief=brief, resolution=resolution)
[docs]class PartitionError(PartsError): """Errors related to partitions.""" def __init__( self, partition: str, brief: str, *, details: Optional[str] = None, resolution: Optional[str] = None, ) -> None: self.partition = partition super().__init__(brief=brief, details=details, resolution=resolution)
[docs]class InvalidPartitionError(PartitionError): """Partition is not valid for this application.""" def __init__( self, partition: str, path: Union[str, pathlib.Path], valid_partitions: Iterable[str], ) -> None: self.valid_partitions = valid_partitions super().__init__( partition, brief=f"Invalid partition {partition!r} in path {str(path)!r}", details="Valid partitions: " + ", ".join(valid_partitions), resolution="Correct the invalid partition name and try again.", )
[docs]class PartitionWarning(PartitionError, Warning): """Warnings for partition-related items.""" def __init__( self, partition: str, brief: str, *, details: Optional[str] = None, resolution: Optional[str] = None, ) -> None: super().__init__( partition=partition, brief=brief, details=details, resolution=resolution ) Warning.__init__(self)