Source code for craft_parts.infos

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

"""Project, part and step information classes."""

import logging
import platform
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from pydantic_yaml import YamlModel

from craft_parts import errors
from craft_parts.dirs import ProjectDirs
from craft_parts.parts import Part
from craft_parts.steps import Step

if TYPE_CHECKING:
    from craft_parts.state_manager import states


logger = logging.getLogger(__name__)


_var_name_pattern = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


[docs]class ProjectVar(YamlModel): """Project variables that can be updated using craftctl.""" value: str updated: bool = False
# pylint: disable-next=too-many-instance-attributes,too-many-public-methods
[docs]class ProjectInfo: """Project-level information containing project-specific fields. :param application_name: A unique identifier for the application using Craft Parts. :param project_name: Name of the project being built. :param cache_dir: The path to store cached packages and files. If not specified, a directory under the application name entry in the XDG base directory will be used. :param arch: The architecture to build for. Defaults to the host system architecture. :param parallel_build_count: The maximum number of concurrent jobs to be used to build each part of this project. :param strict_mode: Only allow plugins capable of building in strict mode. :param project_dirs: The project work directories. :param project_name: The name of the project. :param project_vars_part_name: Project variables can be set only if the part name matches this name. :param project_vars: A dictionary containing the project variables. :param custom_args: Any additional arguments defined by the application when creating a :class:`LifecycleManager`. :param partitions: A list of partitions. """ def __init__( self, *, application_name: str, cache_dir: Path, arch: str = "", base: str = "", parallel_build_count: int = 1, strict_mode: bool = False, project_dirs: Optional[ProjectDirs] = None, project_name: Optional[str] = None, project_vars_part_name: Optional[str] = None, project_vars: Optional[Dict[str, str]] = None, partitions: Optional[List[str]] = None, **custom_args: Any, # custom passthrough args ): if not project_dirs: project_dirs = ProjectDirs(partitions=partitions) pvars = project_vars or {} self._application_name = application_name self._cache_dir = Path(cache_dir).expanduser().resolve() self._set_machine(arch) self._base = base # base usage is deprecated self._parallel_build_count = parallel_build_count self._strict_mode = strict_mode self._dirs = project_dirs self._project_name = project_name self._project_vars_part_name = project_vars_part_name self._project_vars = {k: ProjectVar(value=v) for k, v in pvars.items()} self._partitions = partitions self._custom_args = custom_args self.global_environment: Dict[str, str] = {} self.execution_finished = False def __getattr__(self, name: str) -> Any: if hasattr(self._dirs, name): return getattr(self._dirs, name) if name in self._custom_args: return self._custom_args[name] raise AttributeError(f"{self.__class__.__name__!r} has no attribute {name!r}") @property def custom_args(self) -> List[str]: """Return the list of custom argument names.""" return list(self._custom_args.keys()) @property def application_name(self) -> str: """Return the name of the application using craft-parts.""" return self._application_name @property def cache_dir(self) -> Path: """Return the directory used to store cached files.""" return self._cache_dir @property def arch_build_on(self) -> str: """The architecture we are building on.""" return self._host_machine["deb"] @property def arch_build_for(self) -> str: """The architecture we are building for.""" return self._machine["deb"] @property def arch_triplet_build_on(self) -> str: """The machine-vendor-os triplet for the platform we are building on.""" return self._host_machine["triplet"] @property def arch_triplet_build_for(self) -> str: """The machine-vendor-os triplet for the platform we are building for.""" return self._machine["triplet"] @property def arch_triplet(self) -> str: """Return the machine-vendor-os platform triplet definition.""" return self._machine["triplet"] @property def is_cross_compiling(self) -> bool: """Whether the target and host architectures are different.""" return self._arch != self._host_arch @property def parallel_build_count(self) -> int: """Return the maximum allowable number of concurrent build jobs.""" return self._parallel_build_count @property def strict_mode(self) -> bool: """Return whether this project must be built in 'strict' mode.""" return self._strict_mode @property def host_arch(self) -> str: """Return the host architecture used for debs, snaps and charms.""" return self._host_machine["deb"] @property def target_arch(self) -> str: """Return the target architecture used for debs, snaps and charms.""" return self._machine["deb"] @property def base(self) -> str: """Return the project build base.""" return self._base @property def dirs(self) -> ProjectDirs: """Return the project's work directories.""" return self._dirs @property def project_name(self) -> Optional[str]: """Return the name of the project using craft-parts.""" return self._project_name @property def project_vars_part_name(self) -> Optional[str]: """Return the name of the part that can set project vars.""" return self._project_vars_part_name @property def project_options(self) -> Dict[str, Any]: """Obtain a project-wide options dictionary.""" return { "application_name": self.application_name, "arch_triplet": self.arch_triplet, "target_arch": self.target_arch, "project_vars_part_name": self._project_vars_part_name, "project_vars": self._project_vars, } @property def partitions(self) -> Optional[List[str]]: """Return the project's partitions.""" return self._partitions
[docs] def set_project_var( self, name: str, value: str, raw_write: bool = False, *, part_name: Optional[str] = None, ) -> None: """Set the value of a project variable. Variable values can be set once. Project variables are not intended for logic construction in user scripts, setting it multiple times is likely to be an error. :param name: The project variable name. :param value: The new project variable value. :param part_name: If not None, variable setting is restricted to the named part. :param raw_write: Whether the variable is written without access verifications. :raise ValueError: If there is no custom argument with the given name. :raise RuntimeError: If a write-once variable is set a second time, or if a part name is specified and the variable is set from a different part. """ self._ensure_valid_variable_name(name) if raw_write: self._project_vars[name].value = value self._project_vars[name].updated = True return if self._project_vars[name].updated: raise RuntimeError(f"variable {name!r} can be set only once") if self._project_vars_part_name == part_name: self._project_vars[name].value = value self._project_vars[name].updated = True elif not self._project_vars_part_name: raise RuntimeError( f"variable {name!r} can only be set in a part that " "adopts external metadata" ) else: raise RuntimeError( f"variable {name!r} can only be set " f"in part {self._project_vars_part_name!r}" )
[docs] def get_project_var(self, name: str, *, raw_read: bool = False) -> str: """Get the value of a project variable. Variables must be consumed by the application only after the lifecycle execution ends to prevent unexpected behavior if steps are skipped. :param name: The project variable name. :param raw_read: Whether the variable is read without access verifications. :return: The value of the variable. :raise ValueError: If there is no project variable with the given name. :raise RuntimeError: If the variable is consumed during the lifecycle execution. """ self._ensure_valid_variable_name(name) if not raw_read and not self.execution_finished: raise RuntimeError( f"cannot consume variable {name!r} during lifecycle execution" ) return self._project_vars[name].value
def _ensure_valid_variable_name(self, name: str) -> None: """Raise an error if variable name is invalid. :param name: The variable name to verify. """ if not _var_name_pattern.match(name): raise ValueError(f"{name!r} is not a valid variable name") if name not in self._project_vars: raise ValueError(f"{name!r} not in project variables") def _set_machine(self, arch: Optional[str]) -> None: """Initialize host and target machine information based on the architecture. :param arch: The architecture to use. If empty, assume the host system architecture. """ # set host machine and arch self._host_arch = _get_host_architecture() host_machine = _ARCH_TRANSLATIONS.get(self._host_arch) if not host_machine: raise errors.InvalidArchitecture(self._host_arch) self._host_machine = host_machine # set target machine and arch if not arch: arch = self._host_arch logger.debug("Setting target machine to %s", arch) machine = _ARCH_TRANSLATIONS.get(arch) if not machine: raise errors.InvalidArchitecture(arch) self._arch = arch self._machine = machine
[docs]class PartInfo: """Part-level information containing project and part fields. :param project_info: The project information. :param part: The part we want to obtain information from. """ def __init__( self, project_info: ProjectInfo, part: Part, ): self._project_info = project_info self._part_name = part.name self._part_src_dir = part.part_src_dir self._part_src_subdir = part.part_src_subdir self._part_build_dir = part.part_build_dir self._part_build_subdir = part.part_build_subdir self._part_install_dir = part.part_install_dir self._part_state_dir = part.part_state_dir self._part_cache_dir = part.part_cache_dir self.build_attributes = part.spec.build_attributes.copy() def __getattr__(self, name: str) -> Any: # Use composition and attribute cascading to avoid setting attributes # cumulatively in the init method. if hasattr(self._project_info, name): return getattr(self._project_info, name) raise AttributeError(f"{self.__class__.__name__!r} has no attribute {name!r}") @property def project_info(self) -> ProjectInfo: """Return the project information.""" return self._project_info @property def part_name(self) -> str: """Return the name of the part we're providing information about.""" return self._part_name @property def part_src_dir(self) -> Path: """Return the subdirectory containing the part's source code.""" return self._part_src_dir @property def part_src_subdir(self) -> Path: """Return the subdirectory in source containing the source subtree (if any).""" return self._part_src_subdir @property def part_build_dir(self) -> Path: """Return the subdirectory containing the part's build tree.""" return self._part_build_dir @property def part_build_subdir(self) -> Path: """Return the subdirectory in build containing the source subtree (if any).""" return self._part_build_subdir @property def part_install_dir(self) -> Path: """Return the subdirectory to install the part's build artifacts.""" return self._part_install_dir @property def part_state_dir(self) -> Path: """Return the subdirectory containing this part's lifecycle state.""" return self._part_state_dir @property def part_cache_dir(self) -> Path: """Return the subdirectory containing this part's cache directory.""" return self._part_cache_dir
[docs] def set_project_var( self, name: str, value: str, *, raw_write: bool = False ) -> None: """Set the value of a project variable. Variable values can be set once. Project variables are not intended for logic construction in user scripts, setting it multiple times is likely to be an error. :param name: The project variable name. :param value: The new project variable value. :param raw_write: Whether the variable is written without access verifications. :raise ValueError: If there is no custom argument with the given name. :raise RuntimeError: If a write-once variable is set a second time, or if a part name is specified and the variable is set from a different part. """ self._project_info.set_project_var( name, value, part_name=self._part_name, raw_write=raw_write )
[docs] def get_project_var(self, name: str, *, raw_read: bool = False) -> str: """Get the value of a project variable. Variables must be consumed by the application only after the lifecycle execution ends to prevent unexpected behavior if steps are skipped. :param name: The project variable name. :param raw_read: Whether the variable is read without access verifications. :return: The value of the variable. :raise ValueError: If there is no project variable with the given name. :raise RuntimeError: If the variable is consumed during the lifecycle execution. """ return self._project_info.get_project_var(name, raw_read=raw_read)
[docs]class StepInfo: """Step-level information containing project, part, and step fields. :param part_info: The part information. :param step: The step we want to obtain information from. """ def __init__( self, part_info: PartInfo, step: Step, ): self._part_info = part_info self.step = step self.step_environment: Dict[str, str] = {} self.state: "Optional[states.StepState]" = None def __getattr__(self, name: str) -> Any: if hasattr(self._part_info, name): return getattr(self._part_info, name) raise AttributeError(f"{self.__class__.__name__!r} has no attribute {name!r}")
def _get_host_architecture() -> str: """Obtain the host system architecture.""" machine = platform.machine() return _PLATFORM_MACHINE_TRANSLATIONS.get(machine.lower(), machine) _PLATFORM_MACHINE_TRANSLATIONS: Dict[str, str] = { # Maps other possible ``platform.machine()`` values to the arch translations below. "arm64": "aarch64", "armv7hl": "armv7l", "i386": "i686", "amd64": "x86_64", "x64": "x86_64", } _ARCH_TRANSLATIONS: Dict[str, Dict[str, str]] = { "aarch64": { "kernel": "arm64", "deb": "arm64", "uts_machine": "aarch64", "cross-compiler-prefix": "aarch64-linux-gnu-", "triplet": "aarch64-linux-gnu", "core-dynamic-linker": "lib/ld-linux-aarch64.so.1", }, "armv7l": { "kernel": "arm", "deb": "armhf", "uts_machine": "arm", "cross-compiler-prefix": "arm-linux-gnueabihf-", "triplet": "arm-linux-gnueabihf", "core-dynamic-linker": "lib/ld-linux-armhf.so.3", }, "i686": { "kernel": "x86", "deb": "i386", "uts_machine": "i686", "triplet": "i386-linux-gnu", }, "ppc": { "kernel": "powerpc", "deb": "powerpc", "uts_machine": "powerpc", "cross-compiler-prefix": "powerpc-linux-gnu-", "triplet": "powerpc-linux-gnu", }, "ppc64le": { "kernel": "powerpc", "deb": "ppc64el", "uts_machine": "ppc64el", "cross-compiler-prefix": "powerpc64le-linux-gnu-", "triplet": "powerpc64le-linux-gnu", "core-dynamic-linker": "lib64/ld64.so.2", }, "riscv64": { "kernel": "riscv64", "deb": "riscv64", "uts_machine": "riscv64", "cross-compiler-prefix": "riscv64-linux-gnu-", "triplet": "riscv64-linux-gnu", "core-dynamic-linker": "lib/ld-linux-riscv64-lp64d.so.1", }, "s390x": { "kernel": "s390", "deb": "s390x", "uts_machine": "s390x", "cross-compiler-prefix": "s390x-linux-gnu-", "triplet": "s390x-linux-gnu", "core-dynamic-linker": "lib/ld64.so.1", }, "x86_64": { "kernel": "x86", "deb": "amd64", "uts_machine": "x86_64", "triplet": "x86_64-linux-gnu", "core-dynamic-linker": "lib64/ld-linux-x86-64.so.2", }, }