# Copyright 2024 Caleb Connolly # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations import enum from collections.abc import Generator from pathlib import Path, PosixPath, PurePosixPath import pmb.config from pmb.core.arch import Arch from .context import get_context class ChrootType(enum.Enum): ROOTFS = "rootfs" BUILDROOT = "buildroot" INSTALLER = "installer" NATIVE = "native" IMAGE = "image" def __str__(self) -> str: return self.name class Chroot: __type: ChrootType __name: str def __init__(self, suffix_type: ChrootType, name: str | Arch | None = ""): # We use the native chroot as the buildroot when building for the host arch if suffix_type == ChrootType.BUILDROOT and isinstance(name, Arch): if name.is_native(): suffix_type = ChrootType.NATIVE name = "" self.__type = suffix_type self.__name = str(name or "") self.__validate() def __validate(self) -> None: """ Ensures that this suffix follows the correct format. """ if self.__type not in ChrootType._member_map_.values(): raise ValueError(f"Invalid chroot type: '{self.__type}'") # A buildroot suffix must have a name matching one of alpines # architectures. if self.__type == ChrootType.BUILDROOT and self.arch not in Arch.supported(): raise ValueError(f"Invalid buildroot suffix: '{self.__name}'") # A rootfs or installer suffix must have a name matching a device. if self.__type == ChrootType.INSTALLER or self.__type == ChrootType.ROOTFS: # FIXME: pmb.helpers.devices.find_path() requires args parameter pass # A native suffix must not have a name. if self.__type == ChrootType.NATIVE and self.__name != "": raise ValueError(f"The native suffix can't have a name but got: '{self.__name}'") if self.__type == ChrootType.IMAGE and not Path(self.__name).exists(): raise ValueError(f"Image file '{self.__name}' does not exist") # rootfs suffixes must have a valid device name if self.__type == ChrootType.ROOTFS and (len(self.__name) < 3 or "-" not in self.__name): raise ValueError(f"Invalid device name: '{self.__name}'") def __str__(self) -> str: if len(self.__name) > 0 and self.type != ChrootType.IMAGE: return f"{self.__type.value}_{self.__name}" else: return self.__type.value @property def dirname(self) -> str: return f"chroot_{self}" @property def path(self) -> Path: return Path(get_context().config.work, self.dirname) def exists(self) -> bool: return (self / "bin/sh").is_symlink() def is_mounted(self) -> bool: return self.exists() and pmb.helpers.mount.ismount(self.path / "etc/apk/keys") @property def arch(self) -> Arch: if self.type == ChrootType.NATIVE: return Arch.native() if self.type == ChrootType.BUILDROOT: return Arch.from_str(self.name) # FIXME: this is quite delicate as it will only be valid # for certain pmbootstrap commands... It was like this # before but it should be fixed. arch = pmb.parse.deviceinfo().arch if arch is not None: return arch raise ValueError(f"Invalid chroot suffix: {self} (wrong device chosen in 'init' step?)") def __eq__(self, other: object) -> bool: if isinstance(other, str): return str(self) == other or self.path == Path(other) or self.name == other if isinstance(other, PosixPath): return self.path == other if not isinstance(other, Chroot): return NotImplemented return self.type == other.type and self.name == other.name def __truediv__(self, other: object) -> Path: if isinstance(other, PosixPath) or isinstance(other, PurePosixPath): # Convert the other path to a relative path # FIXME: we should avoid creating absolute paths that we actually want # to make relative to the chroot... other = other.relative_to("/") if other.is_absolute() else other return self.path.joinpath(other) if isinstance(other, str): return self.path.joinpath(other.strip("/")) return NotImplemented def __rtruediv__(self, other: object) -> Path: if isinstance(other, PosixPath) or isinstance(other, PurePosixPath): # Important to produce a new Path object here, otherwise we # end up with one object getting shared around and modified # and lots of weird stuff happens. return Path(other) / self.path if isinstance(other, str): # This implicitly creates a new Path object return other / self.path return NotImplemented @property def type(self) -> ChrootType: return self.__type @property def name(self) -> str: return self.__name @staticmethod def native() -> Chroot: return Chroot(ChrootType.NATIVE) @staticmethod def buildroot(arch: Arch) -> Chroot: return Chroot(ChrootType.BUILDROOT, arch) @staticmethod def rootfs(device: str) -> Chroot: return Chroot(ChrootType.ROOTFS, device) @staticmethod def from_str(s: str) -> Chroot: """ Generate a Suffix from a suffix string like "buildroot_aarch64" """ parts = s.split("_", 1) stype = parts[0] if len(parts) == 2: # Will error if stype isn't a valid ChrootType # The name will be validated by the Chroot constructor return Chroot(ChrootType(stype), parts[1]) # "native" is the only valid suffix type, the constructor(s) # will validate that stype is "native" return Chroot(ChrootType(stype)) @staticmethod def iter_patterns() -> Generator[str, None, None]: """ Generate suffix patterns for all valid suffix types """ for stype in ChrootType: if stype == ChrootType.NATIVE: yield f"chroot_{stype.value}" else: yield f"chroot_{stype.value}_*" @staticmethod def glob() -> Generator[Path, None, None]: """ Glob all initialized chroot directories """ for pattern in Chroot.iter_patterns(): yield from Path(get_context().config.work).glob(pattern)