diff --git a/live-build/isobuilder/builder.py b/live-build/isobuilder/builder.py index 8ca6522a..3973f871 100644 --- a/live-build/isobuilder/builder.py +++ b/live-build/isobuilder/builder.py @@ -1,6 +1,5 @@ import contextlib import json -import os import pathlib import shlex import shutil @@ -8,6 +7,7 @@ import subprocess import sys from isobuilder.apt_state import AptStateManager +from isobuilder.boot import make_boot_configurator_for_arch from isobuilder.gpg_key import EphemeralGPGKey from isobuilder.pool_builder import PoolBuilder @@ -114,27 +114,34 @@ class ISOBuilder: self.workdir = workdir self.logger = Logger() self.iso_root = workdir.joinpath("iso-root") - self._series = self._arch = self._gpg_key = self._apt_state = None + self._config: dict | None = None + self._gpg_key = self._apt_state = None # UTILITY STUFF def _read_config(self): with self.workdir.joinpath("config.json").open() as fp: - data = json.load(fp) - self._series = data["series"] - self._arch = data["arch"] + self._config = json.load(fp) + + @property + def config(self): + if self._config is None: + self._read_config() + return self._config + + def save_config(self): + with self.workdir.joinpath("config.json").open("w") as fp: + json.dump(self._config, fp) @property def arch(self): - if self._arch is None: - self._read_config() - return self._arch + return self.config["arch"] @property def series(self): - if self._series is None: + if self._config is None: self._read_config() - return self._series + return self._config["series"] @property def gpg_key(self): @@ -162,8 +169,8 @@ class ISOBuilder: dot_disk.mkdir() self.logger.log("saving config") - with self.workdir.joinpath("config.json").open("w") as fp: - json.dump({"arch": arch, "series": series}, fp) + self._config = {"arch": arch, "series": series} + self.save_config() self.logger.log("populating .disk") dot_disk.joinpath("base_installable").touch() @@ -219,9 +226,8 @@ class ISOBuilder: # can verify it's booting from the right media. with self.logger.logged("extracting casper uuids"): casper_dir = self.iso_root.joinpath("casper") - prefix = "filesystem.initrd-" dot_disk = self.iso_root.joinpath(".disk") - for initrd in casper_dir.glob(f"{prefix}*"): + for initrd in casper_dir.glob("*initrd"): initrddir = self.workdir.joinpath("initrd") with self.logger.logged( f"unpacking {initrd.name} ...", done_msg="... done" @@ -231,9 +237,7 @@ class ISOBuilder: # - Platforms with early firmware: subdirs like "main/" or "early/" # containing conf/uuid.conf # - Other platforms: conf/uuid.conf directly in the root - # Try to find uuid.conf in both locations. The [uuid_conf] = confs - # unpacking asserts exactly one match; multiple matches would - # indicate an unexpected initrd structure. + # Try to find uuid.conf in both locations. confs = list(initrddir.glob("*/conf/uuid.conf")) if confs: [uuid_conf] = confs @@ -242,33 +246,31 @@ class ISOBuilder: else: raise Exception("uuid.conf not found") self.logger.log(f"found {uuid_conf.relative_to(initrddir)}") - uuid_conf.rename( - dot_disk.joinpath("casper-uuid-" + initrd.name[len(prefix) :]) - ) + if initrd.name == "initrd": + suffix = "generic" + elif initrd.name == "hwe-initrd": + suffix = "generic-hwe" + else: + raise Exception(f"unexpected initrd name {initrd.name}") + uuid_conf.rename(dot_disk.joinpath(f"casper-uuid-{suffix}")) shutil.rmtree(initrddir) def add_live_filesystem(self, artifact_prefix: pathlib.Path): - # Link build artifacts into the ISO's casper directory. We use hardlinks - # (not copies) for filesystem efficiency - they reference the same inode. - # - # Artifacts come from the layered build with names like "for-iso.base.squashfs" - # and need to be renamed for casper. The prefix is stripped, so: - # for-iso.base.squashfs -> base.squashfs - # for-iso.kernel-generic -> filesystem.kernel-generic - # - # Kernel and initrd get the extra "filesystem." prefix because debian-cd - # expects names like filesystem.kernel-* and filesystem.initrd-*. casper_dir = self.iso_root.joinpath("casper") artifact_dir = artifact_prefix.parent filename_prefix = artifact_prefix.name - def link(src, target_name): + def link(src: pathlib.Path, target_name: str): target = casper_dir.joinpath(target_name) self.logger.log( f"creating link from $ISOROOT/casper/{target_name} to $src/{src.name}" ) target.hardlink_to(src) + kernel_name = "vmlinuz" + if self.arch == "ppc64el": + kernel_name = "vmlinux" + with self.logger.logged( f"linking artifacts from {casper_dir} to {artifact_dir}" ): @@ -276,64 +278,42 @@ class ISOBuilder: for path in artifact_dir.glob(f"{filename_prefix}*.{ext}"): newname = path.name[len(filename_prefix) :] link(path, newname) - for item in "kernel", "initrd": - for path in artifact_dir.glob(f"{filename_prefix}{item}-*"): - newname = "filesystem." + path.name[len(filename_prefix) :] - link(path, newname) + for suffix, prefix in ( + ("-generic", ""), + ("-generic-hwe", "hwe-"), + ): + if artifact_dir.joinpath(f"{filename_prefix}kernel{suffix}").exists(): + link( + artifact_dir.joinpath(f"{filename_prefix}kernel{suffix}"), + f"{prefix}{kernel_name}", + ) + link( + artifact_dir.joinpath(f"{filename_prefix}initrd{suffix}"), + f"{prefix}initrd", + ) self._extract_casper_uuids() def make_bootable(self, project: str, capproject: str, subarch: str): - # debian-cd is Ubuntu's CD/ISO image build system. It contains - # architecture and series-specific boot configuration scripts that set up - # GRUB, syslinux, EFI boot, etc. The tools/boot/$series/boot-$arch script - # knows how to make an ISO bootable for each architecture. - # - # TODO: The boot configuration logic should eventually be ported directly - # into isobuilder to avoid this external dependency and git clone. - debian_cd_dir = self.workdir.joinpath("debian-cd") - with self.logger.logged("cloning debian-cd"): - self.logger.run( - [ - "git", - "clone", - "--depth=1", - "https://git.launchpad.net/~ubuntu-cdimage/debian-cd/+git/ubuntu", - debian_cd_dir, - ], - ) - # Override apt-selection to use our ISO's apt configuration instead of - # debian-cd's default. This ensures the boot scripts get packages from - # the correct repository when installing boot packages. - apt_selection = debian_cd_dir.joinpath("tools/apt-selection") - with self.logger.logged("overwriting apt-selection"): - apt_selection.write_text( - "#!/bin/sh\n" f"APT_CONFIG={self.apt_state.apt_conf_path} apt-get $@\n" - ) - env = dict( - os.environ, - BASEDIR=str(debian_cd_dir), - DIST=self.series, - PROJECT=project, - CAPPROJECT=capproject, - SUBARCH=subarch, + configurator = make_boot_configurator_for_arch( + self.arch, + self.logger, + self.apt_state, + self.iso_root, + ) + configurator.make_bootable( + self.workdir, + project, + capproject, + subarch, + self.iso_root.joinpath("casper/hwe-initrd").exists(), ) - tool_name = f"tools/boot/{self.series}/boot-{self.arch}" - with self.logger.logged(f"running {tool_name} ...", done_msg="... done"): - self.logger.run( - [ - debian_cd_dir.joinpath(tool_name), - "1", - self.iso_root, - ], - env=env, - ) def checksum(self): # Generate md5sum.txt for ISO integrity verification. # - Symlinks are excluded because their targets are already checksummed # - Files are sorted for deterministic, reproducible output across builds # - Paths use "./" prefix and we run md5sum from iso_root so the output - # matches what casper-md5check expects. + # matches what users get when they verify with "md5sum -c" from the ISO all_files = [] for dirpath, dirnames, filenames in self.iso_root.walk(): filepaths = [dirpath.joinpath(filename) for filename in filenames] @@ -351,14 +331,20 @@ class ISOBuilder: ) def make_iso(self, dest: pathlib.Path, volid: str | None): - # 1.mkisofs_opts is generated by debian-cd's make_bootable step. The "1" - # refers to "pass 1" of the build (a legacy naming convention). It contains - # architecture-specific xorriso options for boot sectors, EFI images, etc. - mkisofs_opts = shlex.split(self.workdir.joinpath("1.mkisofs_opts").read_text()) - self.checksum() # xorriso with "-as mkisofs" runs in mkisofs compatibility mode. # -r enables Rock Ridge extensions for Unix metadata (permissions, symlinks). # -iso-level 3 (amd64 only) allows files >4GB which some amd64 ISOs need. + # mkisofs_opts comes from the boot configurator and contains architecture- + # specific options for boot sectors, EFI images, etc. + self.checksum() + configurator = make_boot_configurator_for_arch( + self.arch, + self.logger, + self.apt_state, + self.iso_root, + ) + configurator.create_dirs(self.workdir) + mkisofs_opts = configurator.mkisofs_opts() cmd: list[str | pathlib.Path] = ["xorriso"] if self.arch == "riscv64": # For $reasons, xorriso is not run in mkisofs mode on riscv64 only. @@ -380,7 +366,4 @@ class ISOBuilder: cmd.extend(mkisofs_opts + [self.iso_root, "-o", dest]) with self.logger.logged("running xorriso"): self.logger.run(cmd, cwd=self.workdir, check=True, limit_length=False) - if self.arch == "riscv64": - debian_cd_dir = self.workdir.joinpath("debian-cd") - add_riscv_gpt = debian_cd_dir.joinpath("tools/add_riscv_gpt") - self.logger.run([add_riscv_gpt, dest], cwd=self.workdir) + configurator.post_process_iso(dest)