forked from Mirror/pmbootstrap
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>
213 lines
6.5 KiB
Python
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)
|