mirror of
https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git
synced 2025-07-19 18:45:09 +03:00
Refactor the install code to stop using loop devices and instead create and manipulate a disk image directly. Both ext4 and vfat have mechanisms for formatting and populating partitions at an offset inside an image, other filesystems likely do as well but so far have not been implemented or tested. With this "pmbootstrap install" works for standard EFI disk images (e.g. QEMU, X64 or trailblazer) entirely rootless. Since the creation of the disk images happens in the same user namespace as everything else, the resulting disk images have correct ownership and permissions even though from the host perspective they are all subuids. This gets image building working properly *for the default case*. We can now build disk images! In particular, we can build disk images with a 4k sector size even on a host with a 512 byte sector size (or block size in the filesystem). This is surprisingly hard for some reason since not all libfdisk tools have the right flags. Thankfully sfdisk does. In addition, we now generate UUIDs ourselves, to break the loop between generating fstab and running mkfs (since we also populate the disk image /with/ mkfs, we need to already know the UUID when we run it...). Signed-off-by: Casey Connolly <kcxt@postmarketos.org>
297 lines
11 KiB
Python
297 lines
11 KiB
Python
# Copyright 2023 Oliver Smith
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
import copy
|
|
from pathlib import Path
|
|
from pmb.core.context import get_context
|
|
from pmb.core.arch import Arch
|
|
from pmb.helpers import logging
|
|
import inspect
|
|
import os
|
|
import pmb.config
|
|
import pmb.helpers.other
|
|
import pmb.helpers.devices
|
|
from pmb.meta import Cache
|
|
|
|
|
|
# FIXME: It feels weird to handle this at parse time.
|
|
# we should instead have the Deviceinfo object store
|
|
# the attributes for all kernels and require the user
|
|
# to specify which one they're using.
|
|
# Basically: treat Deviceinfo as a standalone type that
|
|
# doesn't need to traverse pmaports.
|
|
def _parse_kernel_suffix(info: dict[str, str], device: str, kernel: str | None) -> dict[str, str]:
|
|
"""
|
|
Remove the kernel suffix (as selected in 'pmbootstrap init') from
|
|
deviceinfo variables. Related:
|
|
https://wiki.postmarketos.org/wiki/Device_specific_package#Multiple_kernels
|
|
|
|
:param info: deviceinfo dict, e.g.:
|
|
{"a": "first",
|
|
"b_mainline": "second",
|
|
"b_downstream": "third"}
|
|
:param device: which device info belongs to
|
|
:param kernel: which kernel suffix to remove (e.g. "mainline")
|
|
:returns: info, but with the configured kernel suffix removed, e.g:
|
|
{"a": "first",
|
|
"b": "second",
|
|
"b_downstream": "third"}
|
|
"""
|
|
# We don't support parsing the kernel variants in tests yet, since this code
|
|
# depends on pmaports being available and calls into a whole lot of other code.
|
|
if os.environ.get("PYTEST_CURRENT_TEST", "").startswith("pmb/parse/test_deviceinfo.py"):
|
|
# If you hit this, you're probably trying to add a test for kernel variants.
|
|
# You'll need to figure out how to mock the APKBUILD parsing below.
|
|
assert kernel is None
|
|
return info
|
|
# Do nothing if the configured kernel isn't available in the kernel (e.g.
|
|
# after switching from device with multiple kernels to device with only one
|
|
# kernel)
|
|
kernels = pmb.parse._apkbuild.kernels(device)
|
|
if not kernels or kernel not in kernels:
|
|
logging.verbose(f"parse_kernel_suffix: {kernel} not in {kernels}")
|
|
return info
|
|
|
|
ret = copy.copy(info)
|
|
|
|
suffix_kernel = kernel.replace("-", "_")
|
|
for key in inspect.get_annotations(Deviceinfo).keys():
|
|
key_kernel = f"{key}_{suffix_kernel}"
|
|
if key_kernel not in ret:
|
|
continue
|
|
|
|
# Move ret[key_kernel] to ret[key]
|
|
logging.verbose(f"parse_kernel_suffix: {key_kernel} => {key}")
|
|
ret[key] = ret[key_kernel]
|
|
del ret[key_kernel]
|
|
|
|
return ret
|
|
|
|
|
|
@Cache("device", "kernel")
|
|
def deviceinfo(device: str | None = None, kernel: str | None = None) -> "Deviceinfo":
|
|
"""
|
|
:param device: defaults to args.device
|
|
:param kernel: defaults to args.kernel
|
|
"""
|
|
context = get_context()
|
|
if not device:
|
|
device = context.config.device
|
|
if not kernel:
|
|
kernel = context.config.kernel
|
|
|
|
path = pmb.helpers.devices.find_path(device, "deviceinfo")
|
|
if not path:
|
|
raise RuntimeError(
|
|
"Device '" + device + "' not found. Run 'pmbootstrap init' to"
|
|
" start a new device port or to choose another device. It may have"
|
|
" been renamed, see <https://postmarketos.org/renamed>"
|
|
)
|
|
|
|
return Deviceinfo(path, kernel)
|
|
|
|
|
|
class Deviceinfo:
|
|
"""Variables from deviceinfo. Reference: <https://postmarketos.org/deviceinfo>
|
|
Many of these are unused in pmbootstrap, and still more that are described
|
|
on the wiki are missing. Eventually this class and associated code should
|
|
be moved to a separate library and become the authoritative source of truth
|
|
for the deviceinfo format."""
|
|
|
|
path: Path
|
|
# general
|
|
format_version: str
|
|
name: str
|
|
manufacturer: str
|
|
codename: str
|
|
year: str
|
|
dtb: str = ""
|
|
arch: Arch
|
|
|
|
# device
|
|
chassis: str
|
|
keyboard: str | None = "" # deprecated
|
|
external_storage: str | None = ""
|
|
drm: bool | None = False
|
|
dev_touchscreen: str | None = ""
|
|
dev_touchscreen_calibration: str | None = ""
|
|
append_dtb: str | None = ""
|
|
|
|
# bootloader
|
|
flash_method: str = ""
|
|
boot_filesystem: str | None = ""
|
|
create_initfs_extra: bool | None = False
|
|
|
|
# flash
|
|
flash_heimdall_partition_kernel: str | None = ""
|
|
flash_heimdall_partition_initfs: str | None = ""
|
|
flash_heimdall_partition_rootfs: str | None = ""
|
|
flash_heimdall_partition_system: str | None = "" # deprecated
|
|
flash_heimdall_partition_vbmeta: str | None = ""
|
|
flash_heimdall_partition_dtbo: str | None = ""
|
|
flash_fastboot_partition_kernel: str | None = ""
|
|
flash_fastboot_partition_rootfs: str | None = ""
|
|
flash_fastboot_partition_system: str | None = "" # deprecated
|
|
flash_fastboot_partition_vbmeta: str | None = ""
|
|
flash_fastboot_partition_dtbo: str | None = ""
|
|
flash_rk_partition_kernel: str | None = ""
|
|
flash_rk_partition_rootfs: str | None = ""
|
|
flash_rk_partition_system: str | None = "" # deprecated
|
|
flash_mtkclient_partition_kernel: str | None = ""
|
|
flash_mtkclient_partition_rootfs: str | None = ""
|
|
flash_mtkclient_partition_vbmeta: str | None = ""
|
|
flash_mtkclient_partition_dtbo: str | None = ""
|
|
generate_legacy_uboot_initfs: str | None = ""
|
|
kernel_cmdline: str | None = ""
|
|
generate_bootimg: str | None = ""
|
|
header_version: str | None = ""
|
|
bootimg_qcdt: str | None = ""
|
|
bootimg_qcdt_type: str | None = ""
|
|
bootimg_mtk_mkimage: str | None = "" # deprecated
|
|
bootimg_mtk_label_kernel: str | None = ""
|
|
bootimg_mtk_label_ramdisk: str | None = ""
|
|
bootimg_dtb_second: str | None = ""
|
|
bootimg_custom_args: str | None = ""
|
|
flash_offset_base: str | None = ""
|
|
flash_offset_dtb: str | None = ""
|
|
flash_offset_kernel: str | None = ""
|
|
flash_offset_ramdisk: str | None = ""
|
|
flash_offset_second: str | None = ""
|
|
flash_offset_tags: str | None = ""
|
|
flash_pagesize: str | None = ""
|
|
flash_fastboot_max_size: str | None = ""
|
|
flash_sparse: str | None = ""
|
|
flash_sparse_samsung_format: str | None = ""
|
|
rootfs_image_sector_size: int | None = 512
|
|
sd_embed_firmware: str | None = ""
|
|
sd_embed_firmware_step_size: str | None = ""
|
|
partition_blacklist: str | None = ""
|
|
boot_part_start: int
|
|
partition_type: str | None = ""
|
|
root_filesystem: str | None = ""
|
|
flash_kernel_on_update: str | None = ""
|
|
cgpt_kpart: str | None = ""
|
|
cgpt_kpart_start: str | None = ""
|
|
cgpt_kpart_size: str | None = ""
|
|
|
|
# weston
|
|
weston_pixman_type: str | None = ""
|
|
|
|
# keymaps
|
|
keymaps: str | None = ""
|
|
|
|
@staticmethod
|
|
def __validate(info: dict[str, str], path: Path) -> None:
|
|
# Resolve path for more readable error messages
|
|
path = path.resolve()
|
|
|
|
# Legacy errors
|
|
if "flash_methods" in info:
|
|
raise RuntimeError(
|
|
"deviceinfo_flash_methods has been renamed to"
|
|
" deviceinfo_flash_method. Please adjust your"
|
|
f" deviceinfo file: {path}"
|
|
)
|
|
if "external_disk" in info or "external_disk_install" in info:
|
|
raise RuntimeError(
|
|
"Instead of deviceinfo_external_disk and"
|
|
" deviceinfo_external_disk_install, please use the"
|
|
" new variable deviceinfo_external_storage in your"
|
|
f" deviceinfo file: {path}"
|
|
)
|
|
if "msm_refresher" in info:
|
|
raise RuntimeError(
|
|
"It is enough to specify 'msm-fb-refresher' in the"
|
|
" depends of your device's package now. Please"
|
|
" delete the deviceinfo_msm_refresher line in: "
|
|
f"{path}"
|
|
)
|
|
if "flash_fastboot_vendor_id" in info:
|
|
raise RuntimeError(
|
|
"Fastboot doesn't allow specifying the vendor ID"
|
|
" anymore (#1830). Try removing the"
|
|
" 'deviceinfo_flash_fastboot_vendor_id' line in: "
|
|
f"{path} (if you are sure that you need this, then"
|
|
" we can probably bring it back to fastboot, just"
|
|
" let us know in the postmarketOS issues!)"
|
|
)
|
|
if "nonfree" in info:
|
|
raise RuntimeError(f"deviceinfo_nonfree is unused. Please delete it in: {path}")
|
|
if "dev_keyboard" in info:
|
|
raise RuntimeError(f"deviceinfo_dev_keyboard is unused. Please delete it in: {path}")
|
|
if "date" in info:
|
|
raise RuntimeError(
|
|
"deviceinfo_date was replaced by deviceinfo_year. "
|
|
f"Set it to the release year in: {path}"
|
|
)
|
|
|
|
# "codename" is required
|
|
codename = os.path.basename(os.path.dirname(path))[7:]
|
|
if "codename" not in info or info["codename"] != codename:
|
|
raise RuntimeError(f"Please add 'deviceinfo_codename=\"{codename}\"' to: {path}")
|
|
|
|
# "chassis" is required
|
|
chassis_types = pmb.config.deviceinfo_chassis_types
|
|
if "chassis" not in info or not info["chassis"]:
|
|
logging.info(
|
|
"NOTE: the most commonly used chassis types in"
|
|
" postmarketOS are 'handset' (for phones) and 'tablet'."
|
|
)
|
|
raise RuntimeError(f"Please add 'deviceinfo_chassis' to: {path}")
|
|
|
|
# "arch" is required
|
|
if "arch" not in info or not info["arch"]:
|
|
raise RuntimeError(f"Please add 'deviceinfo_arch' to: {path}")
|
|
|
|
arch = Arch.from_str(info["arch"])
|
|
if not arch.is_native() and arch not in Arch.supported():
|
|
raise ValueError(
|
|
f"Arch '{arch}' is not available in"
|
|
" postmarketOS. If you would like to add it, see:"
|
|
" <https://postmarketos.org/newarch>"
|
|
)
|
|
|
|
# "chassis" validation
|
|
chassis_type = info["chassis"]
|
|
if chassis_type not in chassis_types:
|
|
raise RuntimeError(
|
|
f"Unknown chassis type '{chassis_type}', should"
|
|
f" be one of {', '.join(chassis_types)}. Fix this"
|
|
f" and try again: {path}"
|
|
)
|
|
|
|
def __init__(self, path: Path, kernel: str | None = None):
|
|
ret = {}
|
|
with open(path) as handle:
|
|
for line in handle:
|
|
if not line.startswith("deviceinfo_"):
|
|
continue
|
|
if "=" not in line:
|
|
raise SyntaxError(f"{path}: No '=' found:\n\t{line}")
|
|
split = line.split("=", 1)
|
|
key = split[0][len("deviceinfo_") :]
|
|
value = split[1].replace('"', "").replace("\n", "")
|
|
ret[key] = value
|
|
|
|
ret = _parse_kernel_suffix(ret, ret["codename"], kernel)
|
|
Deviceinfo.__validate(ret, path)
|
|
|
|
for key, value in ret.items():
|
|
# FIXME: something to turn on and fix in the future
|
|
# if key not in Deviceinfo.__annotations__.keys():
|
|
# logging.warning(f"deviceinfo: {key} is not a known attribute")
|
|
match key:
|
|
case "arch":
|
|
setattr(self, key, Arch.from_str(value))
|
|
case "gpu_accelerated": # deprecated
|
|
setattr(self, "drm", value)
|
|
case "boot_part_start" | "rootfs_image_sector_size":
|
|
setattr(self, key, int(value))
|
|
case _:
|
|
setattr(self, key, value)
|
|
|
|
if not self.flash_method:
|
|
self.flash_method = "none"
|
|
|
|
# Default boot partition start offset
|
|
if not hasattr(self, "boot_part_start"):
|
|
setattr(self, "boot_part_start", 2048)
|