chroot: allow mounting the device rootfs (MR 2252)

Add a new flag --image which can be used to mount the rootfs generated
with "pmbootstrap install".

For now this is quite limited in scope. But it's enough to allow for
building a package, updating it in the QEMU image, and then booting it.

The major "gotcha" with this is that the QEMU uses the kernel and
initramfs from the device chroot unless you run it with --efi.

Signed-off-by: Caleb Connolly <caleb@postmarketos.org>
This commit is contained in:
Caleb Connolly 2024-06-14 07:25:49 +02:00 committed by Oliver Smith
parent d1a86fe702
commit f331b95824
No known key found for this signature in database
GPG key ID: 5AE7F5513E0885CB
8 changed files with 55 additions and 11 deletions

View file

@ -1,5 +1,6 @@
# Copyright 2023 Oliver Smith # Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from pmb.core.chroot import ChrootType
from pmb.core.pkgrepo import pkgrepo_default_path from pmb.core.pkgrepo import pkgrepo_default_path
from pmb.helpers import logging from pmb.helpers import logging
import os import os
@ -9,11 +10,36 @@ import pmb.chroot.binfmt
import pmb.config import pmb.config
import pmb.helpers.run import pmb.helpers.run
import pmb.parse import pmb.parse
import pmb.install.losetup
import pmb.helpers.mount import pmb.helpers.mount
from pmb.core import Chroot from pmb.core import Chroot
from pmb.core.context import get_context from pmb.core.context import get_context
def mount_chroot_image(chroot: Chroot):
"""Mount an IMAGE type chroot, to modify an existing rootfs image. This
doesn't support split images yet!"""
# Make sure everything is nicely unmounted just to be super safe
# this is definitely overkill
pmb.chroot.shutdown()
pmb.install.losetup.detach_all()
chroot_native = Chroot.native()
pmb.chroot.init(chroot_native)
loopdev = pmb.install.losetup.mount(Path("/") / Path(chroot.name).relative_to(chroot_native.path))
pmb.helpers.mount.bind_file(loopdev, chroot_native / "dev/install")
# Set up device mapper bits
pmb.chroot.root(["kpartx", "-u", "/dev/install"], chroot_native)
chroot.path.mkdir(exist_ok=True)
# # The name of the IMAGE chroot is the path to the rootfs image
pmb.helpers.run.root(["mount", "/dev/mapper/install2", chroot.path])
pmb.helpers.run.root(["mount", "/dev/mapper/install1", chroot.path / "boot"])
pmb.config.workdir.chroot_save_init(chroot)
logging.info(f"({chroot}) mounted {chroot.name}")
def create_device_nodes(chroot: Chroot): def create_device_nodes(chroot: Chroot):
""" """
Create device nodes for null, zero, full, random, urandom in the chroot. Create device nodes for null, zero, full, random, urandom in the chroot.
@ -80,6 +106,9 @@ def mount_dev_tmpfs(chroot: Chroot=Chroot.native()):
def mount(chroot: Chroot): def mount(chroot: Chroot):
if chroot.type == ChrootType.IMAGE and not pmb.mount.ismount(chroot.path):
mount_chroot_image(chroot)
# Mount tmpfs as the chroot's /dev # Mount tmpfs as the chroot's /dev
mount_dev_tmpfs(chroot) mount_dev_tmpfs(chroot)

View file

@ -14,6 +14,7 @@ class ChrootType(enum.Enum):
BUILDROOT = "buildroot" BUILDROOT = "buildroot"
INSTALLER = "installer" INSTALLER = "installer"
NATIVE = "native" NATIVE = "native"
IMAGE = "image"
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@ -65,9 +66,12 @@ class Chroot:
raise ValueError(f"The native suffix can't have a name but got: " raise ValueError(f"The native suffix can't have a name but got: "
f"'{self.__name}'") 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: def __str__(self) -> str:
if len(self.__name) > 0: if len(self.__name) > 0 and self.type != ChrootType.IMAGE:
return f"{self.__type.value}_{self.__name}" return f"{self.__type.value}_{self.__name}"
else: else:
return self.__type.value return self.__type.value

View file

@ -63,11 +63,15 @@ def _parse_flavor(device: str, autoinstall=True):
def _parse_suffix(args: PmbArgs) -> Chroot: def _parse_suffix(args: PmbArgs) -> Chroot:
deviceinfo = pmb.parse.deviceinfo()
if args.image:
rootfs = Chroot.native() / f"home/pmos/rootfs/{deviceinfo.codename}.img"
return Chroot(ChrootType.IMAGE, str(rootfs))
if "rootfs" in args and args.rootfs: if "rootfs" in args and args.rootfs:
return Chroot(ChrootType.ROOTFS, get_context().config.device) return Chroot(ChrootType.ROOTFS, get_context().config.device)
elif args.buildroot: elif args.buildroot:
if args.buildroot == "device": if args.buildroot == "device":
return Chroot.buildroot(pmb.parse.deviceinfo().arch) return Chroot.buildroot(deviceinfo.arch)
else: else:
return Chroot.buildroot(Arch.from_str(args.buildroot)) return Chroot.buildroot(Arch.from_str(args.buildroot))
elif args.suffix: elif args.suffix:
@ -168,12 +172,15 @@ def chroot(args: PmbArgs):
chroot = _parse_suffix(args) chroot = _parse_suffix(args)
user = args.user user = args.user
if (user and chroot != Chroot.native() and if (user and chroot != Chroot.native() and
not chroot.type == ChrootType.BUILDROOT): chroot.type not in [ChrootType.BUILDROOT, ChrootType.IMAGE]):
raise RuntimeError("--user is only supported for native or" raise RuntimeError("--user is only supported for native or"
" buildroot_* chroots.") " buildroot_* chroots.")
if args.xauth and chroot != Chroot.native(): if args.xauth and chroot != Chroot.native():
raise RuntimeError("--xauth is only supported for native chroot.") raise RuntimeError("--xauth is only supported for native chroot.")
if chroot.type == ChrootType.IMAGE:
pmb.chroot.mount(chroot)
# apk: check minimum version, install packages # apk: check minimum version, install packages
pmb.chroot.apk.check_min_version(chroot) pmb.chroot.apk.check_min_version(chroot)
if args.add: if args.add:

View file

@ -1,6 +1,7 @@
# Copyright 2023 Oliver Smith # Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from typing import Optional from typing import Optional
from pmb.core.config import Config
from pmb.helpers import logging from pmb.helpers import logging
import os import os
from pathlib import Path from pathlib import Path

View file

@ -46,14 +46,8 @@ def mount(img_path: Path):
losetup_cmd += ["-b", str(int(sector_size))] losetup_cmd += ["-b", str(int(sector_size))]
pmb.chroot.root(losetup_cmd, check=False) pmb.chroot.root(losetup_cmd, check=False)
try:
device_by_back_file(img_path)
return
except RuntimeError:
pass
# Failure: raise exception return device_by_back_file(img_path)
raise RuntimeError(f"Failed to mount loop device: {img_path}")
def device_by_back_file(back_file: Path) -> Path: def device_by_back_file(back_file: Path) -> Path:
@ -100,5 +94,6 @@ def detach_all():
for loopdevice in losetup["loopdevices"]: for loopdevice in losetup["loopdevices"]:
print(loopdevice["back-file"]) print(loopdevice["back-file"])
if Path(loopdevice["back-file"]).is_relative_to(work): if Path(loopdevice["back-file"]).is_relative_to(work):
pmb.chroot.root(["losetup", "-d", loopdevice["name"]]) pmb.helpers.run.root(["kpartx", "-d", loopdevice["name"]], check=False)
pmb.helpers.run.root(["losetup", "-d", loopdevice["name"]])
return return

View file

@ -812,6 +812,8 @@ def get_parser():
" 'stdout', 'interactive', 'tui' (default)," " 'stdout', 'interactive', 'tui' (default),"
" 'background'. Details: pmb/helpers/run_core.py", " 'background'. Details: pmb/helpers/run_core.py",
default="tui") default="tui")
chroot.add_argument("--image", help="Mount the rootfs image and treat"
" it like a normal chroot.", action="store_true")
chroot.add_argument("command", default=["sh", "-i"], help="command" chroot.add_argument("command", default=["sh", "-i"], help="command"
" to execute inside the chroot. default: sh", " to execute inside the chroot. default: sh",
nargs='*') nargs='*')

View file

@ -20,6 +20,7 @@ import pmb.chroot.other
import pmb.chroot.initfs import pmb.chroot.initfs
import pmb.config import pmb.config
import pmb.config.pmaports import pmb.config.pmaports
import pmb.install.losetup
from pmb.types import PathString, PmbArgs from pmb.types import PathString, PmbArgs
import pmb.helpers.run import pmb.helpers.run
import pmb.parse.cpuinfo import pmb.parse.cpuinfo
@ -345,6 +346,10 @@ def run(args: PmbArgs):
"and select the 'qemu' vendor.") "and select the 'qemu' vendor.")
arch = pmb.parse.deviceinfo().arch arch = pmb.parse.deviceinfo().arch
# Make sure the rootfs image isn't mounted
pmb.mount.umount_all(Chroot(ChrootType.IMAGE, "").path)
pmb.install.losetup.detach_all()
img_path = system_image(device) img_path = system_image(device)
img_path_2nd = None img_path_2nd = None
if args.second_storage: if args.second_storage:

View file

@ -78,6 +78,7 @@ class PmbArgs(Namespace):
host: str host: str
host_qemu: str host_qemu: str
image_size: str image_size: str
image: bool
install_base: str install_base: str
install_blockdev: str install_blockdev: str
install_cgpt: str install_cgpt: str