michael.hudson@canonical.com 51624c1b44
Place ISO artifacts directly into the ISO tree
For MAKE_ISO=yes builds, squashfs, kernel, initrd, manifests, and sizes
are now placed directly into config/iso-dir/iso-root/casper/ during the
build rather than creating livecd.* intermediates that get linked as
for-iso.* files and then copied into casper/ by isobuild.

This stops publishing the intermediate livecd.* artifacts so that only
livecd.*.iso and livecd.*.netboot.tar.gz are published for ISO builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 20:30:10 +13:00

216 lines
6.2 KiB
Python
Executable File

#!/usr/bin/python3
# Building an ISO requires knowing:
#
# * The architecture and series we are building for
# * The address of the mirror to pull packages from the pool from and the
# components of that mirror to use
# * The list of packages to include in the pool
# * Where the squashfs files that contain the rootfs and other metadata layers
# are
# * Where to put the final ISO
# * All the bits of information that end up in .disk/info on the ISO and in the
# "volume ID" for the ISO
#
# It's not completely trivial to come up with a nice feeling interface between
# livecd-rootfs and this tool. There are about 13 parameters that are needed to
# build the ISO and having a tool take 13 arguments seems a bit overwhelming. In
# addition some steps need to run before the layers are made into squashfs files
# and some after. It felt nicer to have a tool with a few subcommands (7, in the
# end) and taking arguments relevant to each step:
#
# $ isobuild --work-dir "" init --disk-id "" --series "" --arch ""
#
# Set up the work-dir for later steps. Create the skeleton file layout of the
# ISO, populate .disk/info etc, create the gpg key referred to above. Store
# series and arch somewhere that later steps can refer to.
#
# $ isobuild --work-dir "" setup-apt --chroot ""
#
# Set up aptfor use by later steps, using the configuration from the passed
# chroot.
#
# $ isobuild --work-dir "" generate-pool --package-list-file ""
#
# Create the pool from the passed germinate output file.
#
# $ isobuild --work-dir "" generate-sources --mountpoint ""
#
# Generate an apt deb822 source for the pool, assuming it is mounted at the
# passed mountpoint, and output it on stdout.
#
# $ isobuild --work-dir "" extract-casper-uuids
#
# Extract casper UUID files from the initrds in the casper directory.
#
# $ isobuild --work-dir "" make-bootable --project "" --capitalized-project ""
# --subarch ""
#
# Set up the bootloader etc so that the ISO can boot (for this clones debian-cd
# and run the tools/boot/$series-$arch script but those should be folded into
# isobuild fairly promptly IMO).
#
# $ isobuild --work-dir "" make-iso --vol-id "" --dest ""
#
# Generate the checksum file and run xorriso to build the final ISO.
import pathlib
import shlex
import click
from isobuilder.builder import ISOBuilder
@click.group()
@click.option(
"--workdir",
type=click.Path(file_okay=False, resolve_path=True, path_type=pathlib.Path),
required=True,
help="working directory",
)
@click.pass_context
def main(ctxt, workdir):
ctxt.obj = ISOBuilder(workdir)
cwd = pathlib.Path().cwd()
if workdir.is_relative_to(cwd):
workdir = workdir.relative_to(cwd)
ctxt.obj.logger.log(f"isobuild starting, workdir: {workdir}")
def subcommand(f):
"""Decorator that converts a function into a Click subcommand with logging.
This decorator:
1. Converts function name from snake_case to kebab-case for the CLI
2. Wraps the function to log the subcommand name and all parameters
3. Registers it as a Click command under the main command group
4. Extracts the ISOBuilder instance from the context and passes it as first arg
"""
name = f.__name__.replace("_", "-")
def wrapped(ctxt, **kw):
# Build a log message showing the subcommand and all its parameters.
# We use ctxt.params (Click's resolved parameters) rather than **kw
# because ctxt.params includes path resolution and type conversion.
# Paths are converted to relative form to keep logs readable and avoid
# exposing full filesystem paths in build artifacts.
msg = f"subcommand {name}"
cwd = pathlib.Path().cwd()
for k, v in sorted(ctxt.params.items()):
if isinstance(v, pathlib.Path):
if v.is_relative_to(cwd):
v = v.relative_to(cwd)
v = shlex.quote(str(v))
msg += f" {k}={v}"
with ctxt.obj.logger.logged(msg):
f(ctxt.obj, **kw)
return main.command(name=name)(click.pass_context(wrapped))
@click.option(
"--disk-info",
type=str,
required=True,
help="contents of .disk/info",
)
@click.option(
"--series",
type=str,
required=True,
help="series being built",
)
@click.option(
"--arch",
type=str,
required=True,
help="architecture being built",
)
@subcommand
def init(builder, disk_info, series, arch):
builder.init(disk_info, series, arch)
@click.option(
"--chroot",
type=click.Path(
file_okay=False, resolve_path=True, path_type=pathlib.Path, exists=True
),
required=True,
)
@subcommand
def setup_apt(builder, chroot: pathlib.Path):
builder.setup_apt(chroot)
@click.pass_obj
@click.option(
"--package-list-file",
type=click.Path(
dir_okay=False, exists=True, resolve_path=True, path_type=pathlib.Path
),
required=True,
)
@subcommand
def generate_pool(builder, package_list_file: pathlib.Path):
builder.generate_pool(package_list_file)
@click.option(
"--mountpoint",
type=str,
required=True,
)
@subcommand
def generate_sources(builder, mountpoint: str):
builder.generate_sources(mountpoint)
@subcommand
def extract_casper_uuids(builder):
builder.extract_casper_uuids()
@click.option(
"--project",
type=str,
required=True,
)
@click.option("--capproject", type=str, required=True)
@click.option(
"--subarch",
type=str,
default="",
)
@subcommand
def make_bootable(builder, project: str, capproject: str | None, subarch: str):
# capproject is the "capitalized project name" used in GRUB menu entries,
# e.g. "Ubuntu" or "Kubuntu". It should come from gen-iso-ids (which uses
# project_to_capproject_map for proper formatting like "Ubuntu-MATE"), but
# we provide a simple .capitalize() fallback for cases where the caller
# doesn't have the pre-computed value.
if capproject is None:
capproject = project.capitalize()
builder.make_bootable(project, capproject, subarch)
@click.option(
"--dest",
type=click.Path(dir_okay=False, resolve_path=True, path_type=pathlib.Path),
required=True,
)
@click.option(
"--volid",
type=str,
default=None,
)
@subcommand
def make_iso(builder, dest: pathlib.Path, volid: str | None):
builder.make_iso(dest, volid)
if __name__ == "__main__":
main()