mirror of
https://git.launchpad.net/livecd-rootfs
synced 2026-02-17 15:33:32 +00:00
Use Python boot package instead of debian-cd shell scripts
Replace the debian-cd git clone and shell script invocation in ISOBuilder with the new Python boot configurators. Key changes to builder.py: - make_bootable() creates a boot configurator and calls its make_bootable() method instead of cloning debian-cd - make_iso() gets mkisofs_opts directly from the configurator instead of reading a serialized file - add_live_filesystem() links kernel/initrd with names expected by the boot configurators (vmlinuz/initrd, hwe-vmlinuz/hwe-initrd) - _extract_casper_uuids() updated for the new initrd naming scheme - Refactor config storage to use a single _config dict - Add limit_length parameter to Logger for long xorriso commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
edf0acbeac
commit
516d8b8913
@ -1,6 +1,5 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
@ -8,6 +7,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from isobuilder.apt_state import AptStateManager
|
from isobuilder.apt_state import AptStateManager
|
||||||
|
from isobuilder.boot import make_boot_configurator_for_arch
|
||||||
from isobuilder.gpg_key import EphemeralGPGKey
|
from isobuilder.gpg_key import EphemeralGPGKey
|
||||||
from isobuilder.pool_builder import PoolBuilder
|
from isobuilder.pool_builder import PoolBuilder
|
||||||
|
|
||||||
@ -114,27 +114,34 @@ class ISOBuilder:
|
|||||||
self.workdir = workdir
|
self.workdir = workdir
|
||||||
self.logger = Logger()
|
self.logger = Logger()
|
||||||
self.iso_root = workdir.joinpath("iso-root")
|
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
|
# UTILITY STUFF
|
||||||
|
|
||||||
def _read_config(self):
|
def _read_config(self):
|
||||||
with self.workdir.joinpath("config.json").open() as fp:
|
with self.workdir.joinpath("config.json").open() as fp:
|
||||||
data = json.load(fp)
|
self._config = json.load(fp)
|
||||||
self._series = data["series"]
|
|
||||||
self._arch = data["arch"]
|
@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
|
@property
|
||||||
def arch(self):
|
def arch(self):
|
||||||
if self._arch is None:
|
return self.config["arch"]
|
||||||
self._read_config()
|
|
||||||
return self._arch
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def series(self):
|
def series(self):
|
||||||
if self._series is None:
|
if self._config is None:
|
||||||
self._read_config()
|
self._read_config()
|
||||||
return self._series
|
return self._config["series"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gpg_key(self):
|
def gpg_key(self):
|
||||||
@ -162,8 +169,8 @@ class ISOBuilder:
|
|||||||
dot_disk.mkdir()
|
dot_disk.mkdir()
|
||||||
|
|
||||||
self.logger.log("saving config")
|
self.logger.log("saving config")
|
||||||
with self.workdir.joinpath("config.json").open("w") as fp:
|
self._config = {"arch": arch, "series": series}
|
||||||
json.dump({"arch": arch, "series": series}, fp)
|
self.save_config()
|
||||||
|
|
||||||
self.logger.log("populating .disk")
|
self.logger.log("populating .disk")
|
||||||
dot_disk.joinpath("base_installable").touch()
|
dot_disk.joinpath("base_installable").touch()
|
||||||
@ -219,9 +226,8 @@ class ISOBuilder:
|
|||||||
# can verify it's booting from the right media.
|
# can verify it's booting from the right media.
|
||||||
with self.logger.logged("extracting casper uuids"):
|
with self.logger.logged("extracting casper uuids"):
|
||||||
casper_dir = self.iso_root.joinpath("casper")
|
casper_dir = self.iso_root.joinpath("casper")
|
||||||
prefix = "filesystem.initrd-"
|
|
||||||
dot_disk = self.iso_root.joinpath(".disk")
|
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")
|
initrddir = self.workdir.joinpath("initrd")
|
||||||
with self.logger.logged(
|
with self.logger.logged(
|
||||||
f"unpacking {initrd.name} ...", done_msg="... done"
|
f"unpacking {initrd.name} ...", done_msg="... done"
|
||||||
@ -231,9 +237,7 @@ class ISOBuilder:
|
|||||||
# - Platforms with early firmware: subdirs like "main/" or "early/"
|
# - Platforms with early firmware: subdirs like "main/" or "early/"
|
||||||
# containing conf/uuid.conf
|
# containing conf/uuid.conf
|
||||||
# - Other platforms: conf/uuid.conf directly in the root
|
# - Other platforms: conf/uuid.conf directly in the root
|
||||||
# Try to find uuid.conf in both locations. The [uuid_conf] = confs
|
# Try to find uuid.conf in both locations.
|
||||||
# unpacking asserts exactly one match; multiple matches would
|
|
||||||
# indicate an unexpected initrd structure.
|
|
||||||
confs = list(initrddir.glob("*/conf/uuid.conf"))
|
confs = list(initrddir.glob("*/conf/uuid.conf"))
|
||||||
if confs:
|
if confs:
|
||||||
[uuid_conf] = confs
|
[uuid_conf] = confs
|
||||||
@ -242,33 +246,31 @@ class ISOBuilder:
|
|||||||
else:
|
else:
|
||||||
raise Exception("uuid.conf not found")
|
raise Exception("uuid.conf not found")
|
||||||
self.logger.log(f"found {uuid_conf.relative_to(initrddir)}")
|
self.logger.log(f"found {uuid_conf.relative_to(initrddir)}")
|
||||||
uuid_conf.rename(
|
if initrd.name == "initrd":
|
||||||
dot_disk.joinpath("casper-uuid-" + initrd.name[len(prefix) :])
|
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)
|
shutil.rmtree(initrddir)
|
||||||
|
|
||||||
def add_live_filesystem(self, artifact_prefix: pathlib.Path):
|
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")
|
casper_dir = self.iso_root.joinpath("casper")
|
||||||
artifact_dir = artifact_prefix.parent
|
artifact_dir = artifact_prefix.parent
|
||||||
filename_prefix = artifact_prefix.name
|
filename_prefix = artifact_prefix.name
|
||||||
|
|
||||||
def link(src, target_name):
|
def link(src: pathlib.Path, target_name: str):
|
||||||
target = casper_dir.joinpath(target_name)
|
target = casper_dir.joinpath(target_name)
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
f"creating link from $ISOROOT/casper/{target_name} to $src/{src.name}"
|
f"creating link from $ISOROOT/casper/{target_name} to $src/{src.name}"
|
||||||
)
|
)
|
||||||
target.hardlink_to(src)
|
target.hardlink_to(src)
|
||||||
|
|
||||||
|
kernel_name = "vmlinuz"
|
||||||
|
if self.arch == "ppc64el":
|
||||||
|
kernel_name = "vmlinux"
|
||||||
|
|
||||||
with self.logger.logged(
|
with self.logger.logged(
|
||||||
f"linking artifacts from {casper_dir} to {artifact_dir}"
|
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}"):
|
for path in artifact_dir.glob(f"{filename_prefix}*.{ext}"):
|
||||||
newname = path.name[len(filename_prefix) :]
|
newname = path.name[len(filename_prefix) :]
|
||||||
link(path, newname)
|
link(path, newname)
|
||||||
for item in "kernel", "initrd":
|
for suffix, prefix in (
|
||||||
for path in artifact_dir.glob(f"{filename_prefix}{item}-*"):
|
("-generic", ""),
|
||||||
newname = "filesystem." + path.name[len(filename_prefix) :]
|
("-generic-hwe", "hwe-"),
|
||||||
link(path, newname)
|
):
|
||||||
|
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()
|
self._extract_casper_uuids()
|
||||||
|
|
||||||
def make_bootable(self, project: str, capproject: str, subarch: str):
|
def make_bootable(self, project: str, capproject: str, subarch: str):
|
||||||
# debian-cd is Ubuntu's CD/ISO image build system. It contains
|
configurator = make_boot_configurator_for_arch(
|
||||||
# architecture and series-specific boot configuration scripts that set up
|
self.arch,
|
||||||
# GRUB, syslinux, EFI boot, etc. The tools/boot/$series/boot-$arch script
|
self.logger,
|
||||||
# knows how to make an ISO bootable for each architecture.
|
self.apt_state,
|
||||||
#
|
self.iso_root,
|
||||||
# TODO: The boot configuration logic should eventually be ported directly
|
)
|
||||||
# into isobuilder to avoid this external dependency and git clone.
|
configurator.make_bootable(
|
||||||
debian_cd_dir = self.workdir.joinpath("debian-cd")
|
self.workdir,
|
||||||
with self.logger.logged("cloning debian-cd"):
|
project,
|
||||||
self.logger.run(
|
capproject,
|
||||||
[
|
subarch,
|
||||||
"git",
|
self.iso_root.joinpath("casper/hwe-initrd").exists(),
|
||||||
"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,
|
|
||||||
)
|
)
|
||||||
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):
|
def checksum(self):
|
||||||
# Generate md5sum.txt for ISO integrity verification.
|
# Generate md5sum.txt for ISO integrity verification.
|
||||||
# - Symlinks are excluded because their targets are already checksummed
|
# - Symlinks are excluded because their targets are already checksummed
|
||||||
# - Files are sorted for deterministic, reproducible output across builds
|
# - Files are sorted for deterministic, reproducible output across builds
|
||||||
# - Paths use "./" prefix and we run md5sum from iso_root so the output
|
# - 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 = []
|
all_files = []
|
||||||
for dirpath, dirnames, filenames in self.iso_root.walk():
|
for dirpath, dirnames, filenames in self.iso_root.walk():
|
||||||
filepaths = [dirpath.joinpath(filename) for filename in filenames]
|
filepaths = [dirpath.joinpath(filename) for filename in filenames]
|
||||||
@ -351,14 +331,20 @@ class ISOBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def make_iso(self, dest: pathlib.Path, volid: str | None):
|
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.
|
# xorriso with "-as mkisofs" runs in mkisofs compatibility mode.
|
||||||
# -r enables Rock Ridge extensions for Unix metadata (permissions, symlinks).
|
# -r enables Rock Ridge extensions for Unix metadata (permissions, symlinks).
|
||||||
# -iso-level 3 (amd64 only) allows files >4GB which some amd64 ISOs need.
|
# -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"]
|
cmd: list[str | pathlib.Path] = ["xorriso"]
|
||||||
if self.arch == "riscv64":
|
if self.arch == "riscv64":
|
||||||
# For $reasons, xorriso is not run in mkisofs mode on riscv64 only.
|
# 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])
|
cmd.extend(mkisofs_opts + [self.iso_root, "-o", dest])
|
||||||
with self.logger.logged("running xorriso"):
|
with self.logger.logged("running xorriso"):
|
||||||
self.logger.run(cmd, cwd=self.workdir, check=True, limit_length=False)
|
self.logger.run(cmd, cwd=self.workdir, check=True, limit_length=False)
|
||||||
if self.arch == "riscv64":
|
configurator.post_process_iso(dest)
|
||||||
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)
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user