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.
This commit is contained in:
Caleb Connolly 2023-06-29 05:25:00 +01:00 committed by Oliver Smith
parent b31dee9ec3
commit 198f302a36
No known key found for this signature in database
GPG key ID: 5AE7F5513E0885CB
90 changed files with 1600 additions and 1121 deletions

154
pmb/core/chroot.py Normal file
View file

@ -0,0 +1,154 @@
# 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}_*"