1
0
Fork 1
mirror of https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git synced 2025-07-13 11:29:46 +03:00
pmbootstrap/pmb/core/chroot.py
Caleb Connolly cb6cd3bc4c
chroot: apk: safety net when chroot unitialized (MR 2252)
We recently changed how we manage chroots, requiring the user (of the
chroot) to initialize it before doing stuff like installing packages.

There are however still certain edgecases in pmbootstrap where this
doesn't get done (for example qemu/run.py would attempt to install the
kernel package for the device, but we don't initialize the rootfs chroot
in QEMU since that doesn't make sense to do). Check for and catch this
situation explicitly so we don't ruin someones day, but still be loud
about it so we can hopefully catch the remaining instances of this.

Signed-off-by: Caleb Connolly <caleb@postmarketos.org>
2024-06-23 12:38:42 +02:00

213 lines
6.5 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, Union
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: Optional[Union[str, Arch]] = ""):
# 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.
"""
valid_arches = [
"x86",
"x86_64",
"aarch64",
"armhf", # XXX: remove this?
"armv7",
"riscv64",
]
if self.__type not in ChrootType:
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.__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}'")
if self.__type == ChrootType.IMAGE and not Path(self.__name).exists():
raise ValueError(f"Image file '{self.__name}' does not exist")
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)