pmbootstrap-meow/pmb/core/chroot.py
Caleb Connolly 198f302a36
treewide: add a Chroot type and adopt pathlib.Path (MR 2252)
Introduce a new module: pmb.core to contain explicitly typed pmbootstrap
API. The first component being Suffix and SuffixType. This explicitly
defines what suffixes are possible, future changes should aim to further
constrain this API (e.g. by validating against available device
codenames or architectures for buildroot suffixes).

Additionally, migrate the entire codebase over to using pathlib.Path.
This is a relatively new part of the Python standard library that uses a
more object oriented model for path handling. It also uses strong type
hinting and has other features that make it much cleaner and easier to
work with than pure f-strings. The Chroot class overloads the "/"
operator the same way the Path object does, allowing one to write paths
relative to a given chroot as:

builddir = chroot / "home/pmos/build"

The Chroot class also has a string representation ("native", or
"rootfs_valve-jupiter"), and a .path property for directly accessing the
absolute path (as a Path object).

The general idea here is to encapsulate common patterns into type hinted
code, and gradually reduce the amount of assumptions made around the
codebase so that future changes are easier to implement.

As the chroot suffixes are now part of the Chroot class, we also
implement validation for them, this encodes the rules on suffix naming
and will cause a runtime exception if a suffix doesn't follow the rules.
2024-06-23 12:38:37 +02:00

154 lines
4.3 KiB
Python

# Copyright 2024 Caleb Connolly
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import enum
from typing import Generator, Optional
from pathlib import Path, PosixPath
import pmb.config
class ChrootType(enum.Enum):
ROOTFS = "rootfs"
BUILDROOT = "buildroot"
INSTALLER = "installer"
NATIVE = "native"
def __str__(self) -> str:
return self.name
class Chroot:
__type: ChrootType
__name: str
def __init__(self, suffix_type: ChrootType, name: Optional[str] = ""):
self.__type = suffix_type
self.__name = name or ""
self.__validate()
def __validate(self) -> None:
"""
Ensures that this suffix follows the correct format.
"""
valid_arches = [
"x86",
"x86_64",
"aarch64",
"armhf", # XXX: remove this?
"armv7",
"riscv64",
]
# A buildroot suffix must have a name matching one of alpines
# architectures.
if self.__type == ChrootType.BUILDROOT and self.__name not in valid_arches:
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: "
f"'{self.__name}'")
def __str__(self) -> str:
if len(self.__name) > 0:
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(pmb.config.work, self.dirname)
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):
# 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):
return other / self.path
if isinstance(other, str):
return other / self.path
return NotImplemented
def type(self) -> ChrootType:
return self.__type
def name(self) -> str:
return self.__name
@staticmethod
def native() -> Chroot:
return Chroot(ChrootType.NATIVE)
@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}_*"