Compare commits

...

41 Commits

Author SHA1 Message Date
michael.hudson@canonical.com
dad4a04751
releasing package livecd-rootfs version 26.04.23 2026-03-02 10:52:01 +13:00
michael.hudson@canonical.com
9861d393aa
changelog for Tobias' change 2026-02-27 14:40:30 +13:00
Tobias Heider
0b30131aae Properly handle non -generic kernel and initramfs flavour
Determine used flavour by looking at the suffix of kernel.
hwe is currently the only one that needs to have a prefix
added so we can hardcode that.
2026-02-26 02:08:26 +01:00
michael.hudson@canonical.com
301cf9622c
releasing package livecd-rootfs version 26.04.22 2026-02-26 10:57:27 +13:00
michael.hudson@canonical.com
a3c8532764
changelog for tobhe's changes 2026-02-26 10:32:54 +13:00
michael.hudson@canonical.com
92e680cc2c
Merge remote-tracking branch 'tobhe/extra_ppas' into ubuntu/master 2026-02-26 10:31:11 +13:00
Tobias Heider
b1c61d8bf1 Fix extra_ppas
ignore launchpad repos in in_release_path because it
can only handle a single repo.
2026-02-25 22:22:51 +01:00
michael.hudson@canonical.com
9c4ce17909
Merge branch 'more-iso-fixes' into ubuntu/master 2026-02-26 10:15:59 +13:00
michael.hudson@canonical.com
b1ca2ae69b
changelog 2026-02-26 10:13:24 +13:00
michael.hudson@canonical.com
31a0c2716c
place kernel at /casper/vmlinux on riscv64
For no very good reason this is what debian-cd does, and what the grub
config isobuilder generates still expects. It probably makes sense to
switch to 'vmlinuz' like every other arch apart from ppc64el does but
for now I want to maintain compatibility with the old ISOs.
2026-02-26 10:13:23 +13:00
michael.hudson@canonical.com
a66fbc54b7
make sure the for-iso squashfs gets created in the right place for kubuntu 2026-02-26 10:13:21 +13:00
Olivier Gayot
9819eae23c ubuntu: pull model from Launchpad canonical-model git repo
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2026-02-25 10:34:19 +01:00
Didier Roche
b135edf10c
releasing package livecd-rootfs version 26.04.21 2026-02-25 08:40:18 +01:00
Didier Roche
112153325c
Update changelog 2026-02-25 08:39:57 +01:00
Didier Roche
39ab3ea113
Update signed ubuntu classic model to ship new nvidia drivers 2026-02-25 08:38:03 +01:00
michael.hudson@canonical.com
a6ac43fcda
releasing package livecd-rootfs version 26.04.20 2026-02-20 12:46:15 +13:00
michael.hudson@canonical.com
92c29ecbf2
Merge branch 'fix-boot-paths' into ubuntu/master 2026-02-20 12:44:49 +13:00
michael.hudson@canonical.com
7fdf181234
fix changelog merge 2026-02-20 12:44:02 +13:00
michael.hudson@canonical.com
49a0415299
Merge branch 'no-tpm-arm64' into ubuntu/master 2026-02-20 12:43:14 +13:00
michael.hudson@canonical.com
96f41a4cf5
add changelog entry for optional sbom change 2026-02-20 12:43:06 +13:00
michael.hudson@canonical.com
2579dc30cb
Merge branch 'CPC-8952-make-sbom-optional' into ubuntu/master 2026-02-20 12:42:04 +13:00
michael.hudson@canonical.com
77db814700
fix version number 2026-02-20 12:40:03 +13:00
Michael Raymond
a1c8d7f3bd add changelog entry 2026-02-19 16:34:04 -05:00
Allen Abraham
f734d8cb8c feat(ubuntu-cpc): make SBOM generation optional in create_manifest function
There are case in CPC built images where we don't want to create an SBOM.
Add an argument in create_manifest which defaults to creating an SBOM, but can also skip generating an SBOM
2026-02-19 15:30:28 -05:00
Michael Raymond
7e3c74afac feat: Use same keyring for all releases
A change in 2024 [0] was made to debootstrap in which the keyring is now
switched from ubuntu-archive-keyring.gpg to
ubuntu-archive-removed-keys.gpg after a given release goes EOL. This
means that the Release signature cannot be verified after EOL since the
Release is signed with the ubuntu-archive-keyring.gpg. It is expected
that we can continue to build any release even after the suite is
closed.
This change adds a debootstrap configuration to override this behavior
and ensure all of our images are verified against the main archive key.

Refs: [0] https://git.launchpad.net/ubuntu/+source/debootstrap/commit/?id=4f8b3405097b9f655938528ae7105ec534eb7d1b
2026-02-19 14:48:18 -05:00
michael.hudson@canonical.com
7c4588191c
e030-ubuntu-live-system-seed.binary: do not run if there is no layer to install the system, in particular on arm64. 2026-02-19 19:57:16 +13:00
michael.hudson@canonical.com
42df11d4cc
add changelog entry 2026-02-19 15:06:08 +13:00
michael.hudson@canonical.com
6db44c1ea6
make a small change to copy_unsigned_monolithic_grub 2026-02-19 15:06:04 +13:00
michael.hudson@canonical.com
081981e650
simplify scratch directory handling a bit 2026-02-19 15:06:02 +13:00
michael.hudson@canonical.com
a2a166d93d
Standardize whitespace in GRUB menu entry stanzas
Use consistent formatting across all architectures: 4-space indent,
two spaces after "linux", one space after "initrd". Also fix an extra
blank line before "fi" in amd64's UEFI section caused by f-string
interpolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:06:00 +13:00
michael.hudson@canonical.com
a6466ab0a3
Make generate_grub_config return strings instead of writing files
Separate config generation from file I/O by having generate_grub_config()
and its helpers return strings. The base class make_bootable() now handles
writing grub.cfg.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:05:53 +13:00
michael.hudson@canonical.com
a5cffa8414
place boot-related files directly into the ISO root
The debian-cd scripts did this game of placing boot-related files in a
separate directory that was then passed to xorriso to include on the
ISO. Stop doing that and just put the files directly into the ISO root
that is already passed to xorriso.
2026-02-19 15:05:47 +13:00
michael.hudson@canonical.com
ff3addb2f8
Extract packages to scratch dirs instead of boot tree subdirs
Package contents were being extracted into subdirectories of the boot
tree (grub_dir, shim_dir), which meant the boot tree contained both
the final boot files and the raw package extractions. Extract packages
into scratch directories instead, copying only the needed files into
the boot tree. This also removes the grub_dir/shim_dir instance
variables and the create_dirs overrides, and moves copy_grub_modules
to a standalone function in grub.py.
2026-02-19 13:17:35 +13:00
michael.hudson@canonical.com
460037fb4d
releasing package livecd-rootfs version 26.04.19 2026-02-17 11:17:10 +13:00
michael.hudson@canonical.com
b70abe0d1f
add changelog entry 2026-02-17 10:52:43 +13:00
michael.hudson@canonical.com
166e83f5d8
Move ubuntu-mini-iso to use isobuilder instead of debian-cd
Set MAKE_ISO=yes so ubuntu-mini-iso uses the standard isobuilder
flow in auto/build. The binary hook is simplified to just creating
kernel/initrd artifacts; isobuilder handles .disk metadata, boot
configuration, and ISO creation.

The mini-iso's custom grub.cfg (single iso-chooser-menu entry) is
generated by a project-specific path in AMD64BootConfigurator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:47:18 +13:00
michael.hudson@canonical.com
5c6a3fba53
move the calamares-using projects into a global with a meaningful name 2026-02-17 10:47:17 +13:00
michael.hudson@canonical.com
edc2fa0fc5
Use match statement with local imports in boot configurator factory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:47:15 +13:00
michael.hudson@canonical.com
516d8b8913
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>
2026-02-17 10:47:14 +13:00
michael.hudson@canonical.com
edf0acbeac
Add Python boot configuration package
Add architecture-specific boot configurators that translate the
debian-cd boot shell scripts (boot-amd64, boot-arm64, boot-ppc64el,
boot-riscv64, boot-s390x) into Python.

The package uses a class hierarchy:
- BaseBootConfigurator: abstract base with common functionality
- GrubBootConfigurator: shared GRUB config generation
- UEFIBootConfigurator: UEFI-specific shim/ESP handling
- Architecture classes: AMD64, ARM64, PPC64EL, RISCV64, S390X

A factory function make_boot_configurator_for_arch() creates the
appropriate configurator for each architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:47:13 +13:00
michael.hudson@canonical.com
6a6b00d68b
Add download_direct method to AptStateManager
Extract a download_direct() method from download() to enable downloading
packages to an arbitrary directory with an arbitrary specification string.

This will be used by the boot configuration code to download bootloader
packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:47:08 +13:00
19 changed files with 1396 additions and 390 deletions

56
debian/changelog vendored
View File

@ -1,3 +1,59 @@
livecd-rootfs (26.04.23) resolute; urgency=medium
[ Tobias Heider ]
* Fix ISO builds when KERNEL_FLAVOUR != generic.
-- Michael Hudson-Doyle <michael.hudson@ubuntu.com> Mon, 02 Mar 2026 10:51:47 +1300
livecd-rootfs (26.04.22) resolute; urgency=medium
[ Oliver Gayot ]
* Pull the model from Launchpad's lp:canonical-models
repo, instead of having it uploaded as part of livecd-rootfs. This
indirection makes it possible to update the models without requiring a new
upload of livecd-rootfs every time.
[ Michael Hudson-Doyle ]
* Fix two more problems with livefs-built ISOs:
- Generate the for-iso squashfs in the right place for Kubuntu.
- Fix confusion about the kernel path on the ISO on riscv64.
[ Tobias Heider ]
* Fix pool generation when using extra_ppas.
-- Michael Hudson-Doyle <michael.hudson@ubuntu.com> Thu, 26 Feb 2026 10:56:42 +1300
livecd-rootfs (26.04.21) resolute; urgency=medium
[ Dan Bungert ]
* Update new signed models to ship latest nvidia drivers for ubuntu hybrid.
-- Didier Roche-Tolomelli <didrocks@ubuntu.com> Wed, 25 Feb 2026 08:38:32 +0100
livecd-rootfs (26.04.20) resolute; urgency=medium
[ Michael Raymond ]
* Bug-fix: Only use main archive keyring when building with debootstrap
so EOL release signatures can be verified after EOL.
[ Allen Abraham ]
* Make SBOM generation optional in create_manifest function.
[ Michael Hudson-Doyle ]
* 030-ubuntu-live-system-seed.binary: do not run if there is no layer to
install the system, in particular on arm64.
* Fix some path confusion in the new isobuilder.boot package and refactor
grub config generation to be more string based.
-- Michael Hudson-Doyle <michael.hudson@ubuntu.com> Fri, 20 Feb 2026 12:45:41 +1300
livecd-rootfs (26.04.19) resolute; urgency=medium
* Translate the debian-cd tools/boot/$series/boot-$arch scripts to Python
and use that to make ISOs bootable rather than cloning debian-cd.
-- Michael Hudson-Doyle <michael.hudson@ubuntu.com> Tue, 17 Feb 2026 11:16:43 +1300
livecd-rootfs (26.04.18) resolute; urgency=medium
[ Michael Hudson-Doyle ]

View File

@ -576,7 +576,7 @@ if [ "${MAKE_ISO}" = "yes" ]; then
# create a for-iso.filesystem.squashfs that does.
if [ -z "$PASSES" ]; then
isobuild generate-sources --mountpoint=/cdrom > chroot/etc/apt/sources.list.d/cdrom.sources
create_squashfs chroot for-iso.filesystem.squashfs
create_squashfs chroot ${PWD}/for-iso.filesystem.squashfs
fi
# Link kernel and initrd files. The ${thing#${PREFIX}} expansion strips
# the PREFIX, so "livecd.ubuntu-server.kernel-generic" becomes

View File

@ -1166,6 +1166,7 @@ case $PROJECT in
OPTS="${OPTS:+$OPTS }--linux-packages=none --initramfs=none"
KERNEL_FLAVOURS=none
BINARY_REMOVE_LINUX=false
MAKE_ISO=yes
add_package install mini-iso-tools linux-generic
case $ARCH in
@ -1396,6 +1397,8 @@ if [ -n "$PASSES" ] && [ -z "$LIVE_PASSES" ]; then
"Either set \$LIVE_PASSES or add a pass ending with '.live'."
fi
echo "DEBOOTSTRAP_OPTIONS=\"--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg\"" >> config/bootstrap
echo "LB_CHROOT_HOOKS=\"$CHROOT_HOOKS\"" >> config/chroot
echo "SUBPROJECT=\"${SUBPROJECT:-}\"" >> config/chroot
echo "LB_DISTRIBUTION=\"$SUITE\"" >> config/chroot

View File

@ -44,6 +44,7 @@ create_manifest() {
local base_default_sbom_name="ubuntu-cloud-image-$(grep "VERSION_ID" $chroot_root/etc/os-release | cut --delimiter "=" --field 2 | tr -d '"')-${ARCH}-$(date +%Y%m%dT%H:%M:%S)"
local sbom_file_name=${3:-"${base_default_sbom_name}.spdx"}
local sbom_document_name=${4:-"${base_default_sbom_name}"}
local should_include_sbom=${5:-"true"}
local sbom_log=${sbom_document_name}.log
echo "create_manifest chroot_root: ${chroot_root}"
dpkg-query --show --admindir="${chroot_root}/var/lib/dpkg" > ${target_file}
@ -54,22 +55,26 @@ create_manifest() {
echo "create_manifest creating file listing."
local target_filelist=${2%.manifest}.filelist
(cd "${chroot_root}" && find -xdev) | sort > "${target_filelist}"
# only creating sboms for CPC project at this time
if [[ ! $(which cpc-sbom) ]]; then
# ensure the tool is installed
sudo snap install --classic --edge cpc-sbom
fi
# generate the SBOM
cpc-sbom --rootdir ${chroot_root} --ignore-copyright-parsing-errors --ignore-copyright-file-not-found-errors --document-name ${sbom_document_name} >"${sbom_file_name}" 2>"${sbom_log}"
SBOM_GENERATION_EXIT_CODE=$?
if [[ ${SBOM_GENERATION_EXIT_CODE} != "0" ]]; then
# check for failure and print log
echo "ERROR: SBOM generation failed. See ${sbom_log}"
cat "$sbom_log"
exit 1
if [ "$should_include_sbom" = "true" ]; then
# only creating sboms for CPC project at this time
if [[ ! $(which cpc-sbom) ]]; then
# ensure the tool is installed
sudo snap install --classic --edge cpc-sbom
fi
# generate the SBOM
cpc-sbom --rootdir ${chroot_root} --ignore-copyright-parsing-errors --ignore-copyright-file-not-found-errors --document-name ${sbom_document_name} >"${sbom_file_name}" 2>"${sbom_log}"
SBOM_GENERATION_EXIT_CODE=$?
if [[ ${SBOM_GENERATION_EXIT_CODE} != "0" ]]; then
# check for failure and print log
echo "ERROR: SBOM generation failed. See ${sbom_log}"
cat "$sbom_log"
exit 1
else
echo "SBOM generation succeeded. see ${sbom_log} for details"
fi
else
echo "SBOM generation succeeded. see ${sbom_log} for details"
fi
echo "SBOM generation skipped"
fi
fi
echo "create_manifest finished"
}

View File

@ -82,7 +82,7 @@ class AptStateManager:
if params:
yield PackageInfo(**params)
def download(self, rootdir: pathlib.Path, pkg_info: PackageInfo):
def download(self, rootdir: pathlib.Path, pkg_info: PackageInfo) -> None:
"""Download the package specified by `pkg_info` under `rootdir`.
The package is saved to the same path under `rootdir` as it is
@ -90,9 +90,17 @@ class AptStateManager:
"""
target_dir = rootdir.joinpath(pkg_info.filename).parent
target_dir.mkdir(parents=True, exist_ok=True)
self.download_direct(pkg_info.spec, target_dir)
def download_direct(self, spec: str, target: pathlib.Path) -> None:
"""Download the package specified by spec to target directory.
The package is downloaded using apt-get download and saved directly
in the target directory.
"""
self.logger.run(
["apt-get", "download", pkg_info.spec],
cwd=target_dir,
["apt-get", "download", spec],
cwd=target,
check=True,
env=self._apt_env(),
)
@ -100,10 +108,10 @@ class AptStateManager:
def in_release_path(self) -> pathlib.Path:
"""Return the path to the InRelease file.
This assumes exactly one InRelease file matches the pattern.
Will raise ValueError if there are 0 or multiple matches.
This ignores all but the first path.
Will raise Error if there isn't at least one match.
"""
[path] = self.apt_root.joinpath("var/lib/apt/lists").glob(
f"*_dists_{self.series}_InRelease"
f"*ubuntu.com*_dists_{self.series}_InRelease"
)
return path

View File

@ -0,0 +1,49 @@
"""Boot configuration package for ISO builder.
This package contains architecture-specific boot configurators for building
bootable ISOs for different architectures.
"""
import pathlib
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..apt_state import AptStateManager
from ..builder import Logger
from .base import BaseBootConfigurator
def make_boot_configurator_for_arch(
arch: str,
logger: "Logger",
apt_state: "AptStateManager",
workdir: pathlib.Path,
iso_root: pathlib.Path,
) -> "BaseBootConfigurator":
"""Factory function to create boot configurator for a specific architecture."""
match arch:
case "amd64":
from .amd64 import AMD64BootConfigurator
return AMD64BootConfigurator(logger, apt_state, workdir, iso_root)
case "arm64":
from .arm64 import ARM64BootConfigurator
return ARM64BootConfigurator(logger, apt_state, workdir, iso_root)
case "ppc64el":
from .ppc64el import PPC64ELBootConfigurator
return PPC64ELBootConfigurator(logger, apt_state, workdir, iso_root)
case "riscv64":
from .riscv64 import RISCV64BootConfigurator
return RISCV64BootConfigurator(logger, apt_state, workdir, iso_root)
case "s390x":
from .s390x import S390XBootConfigurator
return S390XBootConfigurator(logger, apt_state, workdir, iso_root)
case _:
raise ValueError(f"Unsupported architecture: {arch}")
__all__ = ["make_boot_configurator_for_arch"]

View File

@ -0,0 +1,216 @@
"""AMD64/x86_64 architecture boot configuration."""
import pathlib
import shutil
from .base import default_kernel_params
from .grub import copy_grub_modules
from .uefi import UEFIBootConfigurator
CALAMARES_PROJECTS = ["kubuntu", "lubuntu"]
class AMD64BootConfigurator(UEFIBootConfigurator):
"""Boot setup for AMD64/x86_64 architecture."""
efi_suffix = "x64"
grub_target = "x86_64"
arch = "amd64"
def mkisofs_opts(self) -> list[str | pathlib.Path]:
# Boring mkisofs options that should be set somewhere architecture independent.
opts: list[str | pathlib.Path] = ["-J", "-joliet-long", "-l"]
# Generalities on booting
#
# There is a 2x2 matrix of boot modes we care about: legacy or UEFI
# boot modes and having the installer be on a cdrom or a disk. Booting
# from cdrom uses the el torito standard and booting from disk expects
# a MBR or GPT partition table.
#
# https://wiki.osdev.org/El-Torito has a lot more background on this.
# ## Set up the mkisofs options for legacy boot.
# Set the el torito boot image "name", i.e. the path on the ISO
# containing the bootloader for legacy-cdrom boot.
opts.extend(["-b", "boot/grub/i386-pc/eltorito.img"])
# Back in the day, el torito booting worked by emulating a floppy
# drive. This hasn't been a useful way of operating for a long time.
opts.append("-no-emul-boot")
# Misc options to make the legacy-cdrom boot work.
opts.extend(["-boot-load-size", "4", "-boot-info-table", "--grub2-boot-info"])
# The bootloader to write to the MBR for legacy-disk boot.
#
# We use the grub stage1 bootloader, boot_hybrid.img, which then jumps
# to the eltorito image based on the information xorriso provides it
# via the --grub2-boot-info option.
opts.extend(
[
"--grub2-mbr",
self.scratch.joinpath("boot_hybrid.img"),
]
)
# ## Set up the mkisofs options for UEFI boot.
opts.extend(self.get_uefi_mkisofs_opts())
return opts
def extract_files(self) -> None:
with self.logger.logged("extracting AMD64 boot files"):
# Extract UEFI files (common with ARM64)
self.extract_uefi_files()
# AMD64-specific: Add BIOS/legacy boot files
with self.logger.logged("adding BIOS/legacy boot files"):
grub_pc_pkg_dir = self.scratch.joinpath("grub-pc-pkg")
self.download_and_extract_package("grub-pc-bin", grub_pc_pkg_dir)
grub_boot_dir = self.iso_root.joinpath("boot", "grub", "i386-pc")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = grub_pc_pkg_dir.joinpath("usr", "lib", "grub", "i386-pc")
shutil.copy(src_grub_dir.joinpath("eltorito.img"), grub_boot_dir)
shutil.copy(src_grub_dir.joinpath("boot_hybrid.img"), self.scratch)
copy_grub_modules(
grub_pc_pkg_dir,
self.iso_root,
"i386-pc",
["*.mod", "*.lst", "*.o"],
)
def generate_grub_config(self) -> str:
"""Generate grub.cfg content for AMD64."""
result = self.grub_header()
if self.project == "ubuntu-mini-iso":
result += """\
menuentry "Choose an Ubuntu version to install" {
set gfxpayload=keep
linux /casper/vmlinuz iso-chooser-menu ip=dhcp ---
initrd /casper/initrd
}
"""
return result
kernel_params = default_kernel_params(self.project)
# Main menu entry
result += f"""\
menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinuz {kernel_params}
initrd /casper/initrd
}}
"""
# All but server get safe-graphics mode
if self.project != "ubuntu-server":
result += f"""\
menuentry "{self.humanproject} (safe graphics)" {{
set gfxpayload=keep
linux /casper/vmlinuz nomodeset {kernel_params}
initrd /casper/initrd
}}
"""
# ubiquity based projects get OEM mode
if "maybe-ubiquity" in kernel_params:
oem_kernel_params = kernel_params.replace(
"maybe-ubiquity", "only-ubiquity oem-config/enable=true"
)
result += f"""\
menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep
linux /casper/vmlinuz {oem_kernel_params}
initrd /casper/initrd
}}
"""
# Calamares-based projects get OEM mode
if self.project in CALAMARES_PROJECTS:
result += f"""\
menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep
linux /casper/vmlinuz {kernel_params} oem-config/enable=true
initrd /casper/initrd
}}
"""
# Currently only server is built with HWE, hence no safe-graphics/OEM
if self.hwe:
result += f"""\
menuentry "{self.humanproject} with the HWE kernel" {{
set gfxpayload=keep
linux /casper/hwe-vmlinuz {kernel_params}
initrd /casper/hwe-initrd
}}
"""
# UEFI Entries (wrapped in grub_platform check for dual BIOS/UEFI support)
uefi_menu_entries = self.uefi_menu_entries()
result += f"""\
grub_platform
if [ "$grub_platform" = "efi" ]; then
{uefi_menu_entries}\
fi
"""
return result
@staticmethod
def generate_loopback_config(grub_content: str) -> str:
"""Derive loopback.cfg from grub.cfg content.
Strips the header (up to menu_color_highlight) and the UEFI
trailer (from grub_platform to end), and adds iso-scan/filename
to linux lines.
"""
lines = grub_content.split("\n")
start_idx = 0
for i, line in enumerate(lines):
if "menu_color_highlight" in line:
start_idx = i + 1
break
end_idx = len(lines)
for i, line in enumerate(lines):
if "grub_platform" in line:
end_idx = i
break
loopback_lines = lines[start_idx:end_idx]
loopback_lines = [
(
line.replace("---", "iso-scan/filename=${iso_path} ---")
if "linux" in line
else line
)
for line in loopback_lines
]
return "\n".join(loopback_lines)
def make_bootable(
self,
project: str,
capproject: str,
subarch: str,
hwe: bool,
) -> None:
"""Make the ISO bootable, including generating loopback.cfg."""
super().make_bootable(project, capproject, subarch, hwe)
grub_cfg = self.iso_root.joinpath("boot", "grub", "grub.cfg")
grub_content = grub_cfg.read_text()
self.iso_root.joinpath("boot", "grub", "loopback.cfg").write_text(
self.generate_loopback_config(grub_content)
)

View File

@ -0,0 +1,76 @@
"""ARM 64-bit architecture boot configuration."""
import pathlib
from .uefi import UEFIBootConfigurator
from .base import default_kernel_params
class ARM64BootConfigurator(UEFIBootConfigurator):
"""Boot setup for ARM 64-bit architecture."""
efi_suffix = "aa64"
grub_target = "arm64"
arch = "arm64"
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for ARM64."""
opts: list[str | pathlib.Path] = [
"-J",
"-joliet-long",
"-l",
"-c",
"boot/boot.cat",
]
# Add common UEFI options
opts.extend(self.get_uefi_mkisofs_opts())
# ARM64-specific: partition cylinder alignment
opts.extend(["-partition_cyl_align", "all"])
return opts
def extract_files(self) -> None:
"""Download and extract bootloader packages for ARM64."""
with self.logger.logged("extracting ARM64 boot files"):
self.extract_uefi_files()
def generate_grub_config(self) -> str:
"""Generate grub.cfg for ARM64."""
kernel_params = default_kernel_params(self.project)
result = self.grub_header()
# ARM64-specific: Snapdragon workarounds
result += f"""\
set cmdline=
smbios --type 4 --get-string 5 --set proc_version
regexp "Snapdragon.*" "$proc_version"
if [ $? = 0 ]; then
# Work around Snapdragon X firmware bug. cutmem is not allowed in lockdown mode.
if [ $lockdown != "y" ]; then
cutmem 0x8800000000 0x8fffffffff
fi
# arm64.nopauth works around 8cx Gen 3 firmware bug
cmdline="clk_ignore_unused pd_ignore_unused arm64.nopauth"
fi
menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinuz $cmdline {kernel_params} console=tty0
initrd /casper/initrd
}}
"""
# HWE kernel option if available
result += self.hwe_menu_entry(
"vmlinuz",
f"{kernel_params} console=tty0",
extra_params="$cmdline ",
)
# Note: ARM64 HWE also includes $dtb in the original shell script,
# but it's not actually set anywhere in the grub.cfg, so we omit it here
# UEFI Entries (ARM64 is UEFI-only, no grub_platform check needed)
result += self.uefi_menu_entries()
return result

View File

@ -0,0 +1,98 @@
"""Base classes and helper functions for boot configuration."""
import pathlib
import subprocess
import tempfile
from abc import ABC, abstractmethod
from ..builder import Logger
from ..apt_state import AptStateManager
def default_kernel_params(project: str) -> str:
if project == "ubuntukylin":
return (
"file=/cdrom/preseed/ubuntu.seed locale=zh_CN "
"keyboard-configuration/layoutcode?=cn quiet splash --- "
)
if project == "ubuntu-server":
return " --- "
else:
return " --- quiet splash"
class BaseBootConfigurator(ABC):
"""Abstract base class for architecture-specific boot configurators.
Subclasses must implement:
- extract_files(): Download and extract bootloader packages
- mkisofs_opts(): Return mkisofs command-line options
"""
def __init__(
self,
logger: Logger,
apt_state: AptStateManager,
workdir: pathlib.Path,
iso_root: pathlib.Path,
) -> None:
self.logger = logger
self.apt_state = apt_state
self.scratch = workdir.joinpath("boot-stuff")
self.iso_root = iso_root
def download_and_extract_package(
self, pkg_name: str, target_dir: pathlib.Path
) -> None:
"""Download a Debian package and extract its contents to target directory."""
self.logger.log(f"downloading and extracting {pkg_name}")
target_dir.mkdir(exist_ok=True, parents=True)
with tempfile.TemporaryDirectory() as tdir_str:
tdir = pathlib.Path(tdir_str)
self.apt_state.download_direct(pkg_name, tdir)
[deb] = tdir.glob("*.deb")
dpkg_proc = subprocess.Popen(
["dpkg-deb", "--fsys-tarfile", deb], stdout=subprocess.PIPE
)
tar_proc = subprocess.Popen(
["tar", "xf", "-", "-C", target_dir], stdin=dpkg_proc.stdout
)
assert dpkg_proc.stdout is not None
dpkg_proc.stdout.close()
tar_proc.communicate()
@abstractmethod
def extract_files(self) -> None:
"""Download and extract bootloader packages to the boot tree.
Each architecture must implement this to set up its specific bootloader files.
"""
...
@abstractmethod
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs command-line options for this architecture.
Returns:
List of command-line options to pass to mkisofs/xorriso.
"""
...
def post_process_iso(self, iso_path: pathlib.Path) -> None:
"""Post-process the ISO image after xorriso creates it."""
def make_bootable(
self,
project: str,
capproject: str,
subarch: str,
hwe: bool,
) -> None:
"""Make the ISO bootable by extracting bootloader files."""
self.project = project
self.humanproject = capproject.replace("-", " ")
self.subarch = subarch
self.hwe = hwe
self.scratch.mkdir(exist_ok=True)
with self.logger.logged("configuring boot"):
self.extract_files()

View File

@ -0,0 +1,104 @@
"""GRUB boot configuration for multiple architectures."""
import pathlib
import shutil
from abc import abstractmethod
from .base import BaseBootConfigurator
def copy_grub_common_files(grub_pkg_dir: pathlib.Path, iso_root: pathlib.Path) -> None:
fonts_dir = iso_root.joinpath("boot", "grub", "fonts")
fonts_dir.mkdir(parents=True, exist_ok=True)
src = grub_pkg_dir.joinpath("usr", "share", "grub", "unicode.pf2")
dst = fonts_dir.joinpath("unicode.pf2")
shutil.copy(src, dst)
def copy_grub_modules(
grub_pkg_dir: pathlib.Path,
iso_root: pathlib.Path,
grub_target: str,
patterns: list[str],
) -> None:
"""Copy GRUB module files matching given patterns from src to dest."""
src_dir = grub_pkg_dir.joinpath("usr", "lib", "grub", grub_target)
dest_dir = iso_root.joinpath("boot", "grub", grub_target)
dest_dir.mkdir(parents=True, exist_ok=True)
for pat in patterns:
for file in src_dir.glob(pat):
shutil.copy(file, dest_dir)
class GrubBootConfigurator(BaseBootConfigurator):
"""Base class for architectures that use GRUB (all except S390X).
Common GRUB functionality shared across AMD64, ARM64, PPC64EL, and RISC-V64.
Subclasses must implement generate_grub_config().
"""
def grub_header(self, include_loadfont: bool = True) -> str:
"""Return common GRUB config header (timeout, colors).
Args:
include_loadfont: Whether to include 'loadfont unicode'
(not needed for RISC-V)
"""
result = "set timeout=30\n\n"
if include_loadfont:
result += "loadfont unicode\n\n"
result += """\
set menu_color_normal=white/black
set menu_color_highlight=black/light-gray
"""
return result
def hwe_menu_entry(
self,
kernel_name: str,
kernel_params: str,
extra_params: str = "",
) -> str:
"""Return HWE kernel menu entry if HWE is enabled.
Args:
kernel_name: Kernel binary name (vmlinuz or vmlinux)
kernel_params: Kernel parameters to append
extra_params: Additional parameters (e.g., console=tty0, $cmdline)
"""
if not self.hwe:
return ""
return f"""\
menuentry "{self.humanproject} with the HWE kernel" {{
set gfxpayload=keep
linux /casper/hwe-{kernel_name} {extra_params}{kernel_params}
initrd /casper/hwe-initrd
}}
"""
@abstractmethod
def generate_grub_config(self) -> str:
"""Generate grub.cfg content.
Each GRUB-based architecture must implement this to return the
GRUB configuration.
"""
...
def make_bootable(
self,
project: str,
capproject: str,
subarch: str,
hwe: bool,
) -> None:
"""Make the ISO bootable by extracting files and generating GRUB config."""
super().make_bootable(project, capproject, subarch, hwe)
with self.logger.logged("generating grub config"):
content = self.generate_grub_config()
grub_dir = self.iso_root.joinpath("boot", "grub")
grub_dir.mkdir(parents=True, exist_ok=True)
grub_dir.joinpath("grub.cfg").write_text(content)

View File

@ -0,0 +1,74 @@
"""PowerPC 64-bit Little Endian architecture boot configuration."""
import pathlib
import shutil
from .grub import (
copy_grub_common_files,
copy_grub_modules,
GrubBootConfigurator,
)
from .base import default_kernel_params
class PPC64ELBootConfigurator(GrubBootConfigurator):
"""Boot setup for PowerPC 64-bit Little Endian architecture."""
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for PPC64EL."""
return []
def extract_files(self) -> None:
"""Download and extract bootloader packages for PPC64EL."""
self.logger.log("extracting PPC64EL boot files")
grub_pkg_dir = self.scratch.joinpath("grub-pkg")
# Download and extract bootloader packages
self.download_and_extract_package("grub2-common", grub_pkg_dir)
self.download_and_extract_package("grub-ieee1275-bin", grub_pkg_dir)
# Add common files for GRUB to tree
copy_grub_common_files(grub_pkg_dir, self.iso_root)
# Add IEEE1275 ppc boot files
ppc_dir = self.iso_root.joinpath("ppc")
ppc_dir.mkdir()
src_grub_dir = grub_pkg_dir.joinpath("usr", "lib", "grub", "powerpc-ieee1275")
# Copy bootinfo.txt to ppc directory
shutil.copy(
src_grub_dir.joinpath("bootinfo.txt"), ppc_dir.joinpath("bootinfo.txt")
)
# Copy eltorito.elf to boot/grub as powerpc.elf
shutil.copy(
src_grub_dir.joinpath("eltorito.elf"),
self.iso_root.joinpath("boot", "grub", "powerpc.elf"),
)
# Copy GRUB modules
copy_grub_modules(
grub_pkg_dir, self.iso_root, "powerpc-ieee1275", ["*.mod", "*.lst"]
)
def generate_grub_config(self) -> str:
"""Generate grub.cfg for PPC64EL."""
kernel_params = default_kernel_params(self.project)
result = self.grub_header()
# Main menu entry
result += f"""\
menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinux quiet {kernel_params}
initrd /casper/initrd
}}
"""
# HWE kernel option if available
result += self.hwe_menu_entry("vmlinux", kernel_params, extra_params="quiet ")
return result

View File

@ -0,0 +1,207 @@
"""RISC-V 64-bit architecture boot configuration."""
import pathlib
import shutil
from .grub import GrubBootConfigurator, copy_grub_common_files, copy_grub_modules
def copy_unsigned_monolithic_grub(
grub_pkg_dir: pathlib.Path,
efi_suffix: str,
grub_target: str,
iso_root: pathlib.Path,
) -> None:
efi_boot_dir = iso_root.joinpath("EFI", "boot")
efi_boot_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(
grub_pkg_dir.joinpath(
"usr",
"lib",
"grub",
grub_target,
"monolithic",
f"gcd{efi_suffix}.efi",
),
efi_boot_dir.joinpath(f"boot{efi_suffix}.efi"),
)
copy_grub_modules(grub_pkg_dir, iso_root, grub_target, ["*.mod", "*.lst"])
class RISCV64BootConfigurator(GrubBootConfigurator):
"""Boot setup for RISC-V 64-bit architecture."""
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for RISC-V64."""
efi_img = self.scratch.joinpath("efi.img")
return [
"-joliet",
"on",
"-compliance",
"joliet_long_names",
"--append_partition",
"2",
"0xef",
efi_img,
"-boot_image",
"any",
"partition_offset=10240",
"-boot_image",
"any",
"partition_cyl_align=all",
"-boot_image",
"any",
"efi_path=--interval:appended_partition_2:all::",
"-boot_image",
"any",
"appended_part_as=gpt",
"-boot_image",
"any",
"cat_path=/boot/boot.cat",
"-fs",
"64m",
]
def extract_files(self) -> None:
"""Download and extract bootloader packages for RISC-V64."""
self.logger.log("extracting RISC-V64 boot files")
u_boot_dir = self.scratch.joinpath("u-boot-sifive")
grub_pkg_dir = self.scratch.joinpath("grub-pkg")
# Download and extract bootloader packages
self.download_and_extract_package("grub2-common", grub_pkg_dir)
self.download_and_extract_package("grub-efi-riscv64-bin", grub_pkg_dir)
self.download_and_extract_package("grub-efi-riscv64-unsigned", grub_pkg_dir)
self.download_and_extract_package("u-boot-sifive", u_boot_dir)
# Add GRUB to tree
copy_grub_common_files(grub_pkg_dir, self.iso_root)
copy_unsigned_monolithic_grub(
grub_pkg_dir, "riscv64", "riscv64-efi", self.iso_root
)
# Extract DTBs to tree
self.logger.log("extracting device tree files")
kernel_layer = self.scratch.joinpath("kernel-layer")
squashfs_path = self.iso_root.joinpath(
"casper", "ubuntu-server-minimal.squashfs"
)
# Extract device tree firmware from squashfs
self.logger.run(
[
"unsquashfs",
"-no-xattrs",
"-d",
kernel_layer,
squashfs_path,
"usr/lib/firmware",
],
check=True,
)
# Copy DTBs if they exist
dtb_dir = self.iso_root.joinpath("dtb")
dtb_dir.mkdir(parents=True, exist_ok=True)
firmware_dir = kernel_layer.joinpath("usr", "lib", "firmware")
for dtb_file in firmware_dir.glob("*/device-tree/*"):
if dtb_file.is_file():
shutil.copy(dtb_file, dtb_dir)
# Create ESP image with GRUB and dtbs
efi_img = self.scratch.joinpath("efi.img")
self.logger.run(
["mkfs.msdos", "-n", "ESP", "-C", "-v", efi_img, "32768"], check=True
)
# Add EFI files to ESP
efi_dir = self.iso_root.joinpath("EFI")
self.logger.run(["mcopy", "-s", "-i", efi_img, efi_dir, "::/."], check=True)
# Add DTBs to ESP
self.logger.run(["mcopy", "-s", "-i", efi_img, dtb_dir, "::/."], check=True)
def generate_grub_config(self) -> str:
"""Generate grub.cfg for RISC-V64."""
result = self.grub_header(include_loadfont=False)
# Main menu entry
result += f"""\
menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinux efi=debug sysctl.kernel.watchdog_thresh=60 ---
initrd /casper/initrd
}}
"""
# HWE kernel option if available
result += self.hwe_menu_entry(
"vmlinux",
"---",
extra_params="efi=debug sysctl.kernel.watchdog_thresh=60 ",
)
return result
def post_process_iso(self, iso_path: pathlib.Path) -> None:
"""Add GPT partitions with U-Boot for SiFive Unmatched board.
The SiFive Unmatched board needs a GPT table containing U-Boot in
order to boot. U-Boot does not currently support booting from a CD,
so the GPT table also contains an entry pointing to the ESP so that
U-Boot can find it.
"""
u_boot_dir = self.scratch.joinpath(
"u-boot-sifive", "usr", "lib", "u-boot", "sifive_unmatched"
)
self.logger.run(
[
"sgdisk",
iso_path,
"--set-alignment=2",
"-d",
"1",
"-n",
"1:2082:10273",
"-c",
"1:loader2",
"-t",
"1:2E54B353-1271-4842-806F-E436D6AF6985",
"-n",
"3:10274:12321",
"-c",
"3:loader1",
"-t",
"3:5B193300-FC78-40CD-8002-E86C45580B47",
"-c",
"2:ESP",
"-r=2:3",
],
)
self.logger.run(
[
"dd",
f"if={u_boot_dir / 'u-boot.itb'}",
f"of={iso_path}",
"bs=512",
"seek=2082",
"conv=notrunc",
],
)
self.logger.run(
[
"dd",
f"if={u_boot_dir / 'u-boot-spl.bin'}",
f"of={iso_path}",
"bs=512",
"seek=10274",
"conv=notrunc",
],
)

View File

@ -0,0 +1,206 @@
"""IBM S/390 architecture boot configuration."""
import pathlib
import shutil
import struct
from .base import BaseBootConfigurator
README_dot_boot = """\
About the S/390 installation CD
===============================
It is possible to "boot" the installation system off this CD using
the files provided in the /boot directory.
Although you can boot the installer from this CD, the installation
itself is *not* actually done from the CD. Once the initrd is loaded,
the installer will ask you to configure your network connection and
uses the network-console component to allow you to continue the
installation over SSH. The rest of the installation is done over the
network: all installer components and Debian packages are retrieved
from a mirror.
Instead of SSH, one can also use the ASCII terminal available in HMC.
Exporting full .iso contents (including the hidden .disk directory)
allows one to use the result as a valid mirror for installation.
"""
ubuntu_dot_exec = """\
/* REXX EXEC TO IPL Ubuntu for */
/* z Systems FROM THE VM READER. */
/* */
'CP CLOSE RDR'
'PURGE RDR ALL'
'SPOOL PUNCH * RDR'
'PUNCH KERNEL UBUNTU * (NOHEADER'
'PUNCH PARMFILE UBUNTU * (NOHEADER'
'PUNCH INITRD UBUNTU * (NOHEADER'
'CHANGE RDR ALL KEEP NOHOLD'
'CP IPL 000C CLEAR'
"""
ubuntu_dot_ins = """\
* Ubuntu for IBM Z (default kernel)
kernel.ubuntu 0x00000000
initrd.off 0x0001040c
initrd.siz 0x00010414
parmfile.ubuntu 0x00010480
initrd.ubuntu 0x01000000
"""
def gen_s390_cd_kernel(
kernel: pathlib.Path, initrd: pathlib.Path, cmdline: str, outfile: pathlib.Path
) -> None:
"""Generate a bootable S390X CD kernel image.
This is a Python translation of gen-s390-cd-kernel.pl from debian-cd.
It creates a bootable image for S/390 architecture by combining kernel,
initrd, and boot parameters in a specific format.
"""
# Calculate sizes
initrd_size = initrd.stat().st_size
# The initrd is placed at a fixed offset of 16 MiB
initrd_offset = 0x1000000
# Calculate total boot image size (rounded up to 4K blocks)
boot_size = ((initrd_offset + initrd_size) >> 12) + 1
boot_size = boot_size << 12
# Validate cmdline length (max 896 bytes)
if len(cmdline) >= 896:
raise ValueError(f"Kernel commandline too long ({len(cmdline)} bytes)")
# Create output file and fill with zeros
with outfile.open("wb") as out_fh:
# Fill entire file with zeros
out_fh.write(b"\x00" * boot_size)
# Copy kernel to offset 0
out_fh.seek(0)
with kernel.open("rb") as kernel_fh:
out_fh.write(kernel_fh.read())
# Copy initrd to offset 0x1000000 (16 MiB)
out_fh.seek(initrd_offset)
with initrd.open("rb") as initrd_fh:
out_fh.write(initrd_fh.read())
# Write boot loader control value at offset 4
# This tells the S/390 boot loader where to find the kernel
out_fh.seek(4)
out_fh.write(struct.pack("!I", 0x80010000))
# Write kernel command line at offset 0x10480
out_fh.seek(0x10480)
out_fh.write(cmdline.encode("utf-8"))
# Write initrd parameters
# Initrd offset at 0x1040C
out_fh.seek(0x1040C)
out_fh.write(struct.pack("!I", initrd_offset))
# Initrd size at 0x10414
out_fh.seek(0x10414)
out_fh.write(struct.pack("!I", initrd_size))
class S390XBootConfigurator(BaseBootConfigurator):
"""Boot setup for IBM S/390 architecture."""
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for S390X."""
return [
"-J",
"-no-emul-boot",
"-b",
"boot/ubuntu.ikr",
]
def extract_files(self) -> None:
"""Set up boot files for S390X."""
self.logger.log("extracting S390X boot files")
boot_dir = self.iso_root.joinpath("boot")
boot_dir.mkdir(parents=True, exist_ok=True)
# Copy static .ins & exec scripts, docs from data directory
self.iso_root.joinpath("README.boot").write_text(README_dot_boot)
boot_dir.joinpath("ubuntu.exec").write_text(ubuntu_dot_exec)
boot_dir.joinpath("ubuntu.ins").write_text(ubuntu_dot_ins)
# Move kernel image to the name used in .ins & exec scripts
kernel_src = self.iso_root.joinpath("casper", "vmlinuz")
kernel_dst = boot_dir.joinpath("kernel.ubuntu")
kernel_src.replace(kernel_dst)
# Move initrd to the name used in .ins & exec scripts
initrd_src = self.iso_root.joinpath("casper", "initrd")
initrd_dst = boot_dir.joinpath("initrd.ubuntu")
initrd_src.replace(initrd_dst)
# Compute initrd offset & size, store in files used by .ins & exec scripts
# Offset is always 0x1000000 (16 MiB)
initrd_offset_file = boot_dir.joinpath("initrd.off")
with initrd_offset_file.open("wb") as f:
f.write(struct.pack("!I", 0x1000000))
# Size is the actual size of the initrd
initrd_size = initrd_dst.stat().st_size
initrd_size_file = boot_dir.joinpath("initrd.siz")
with initrd_size_file.open("wb") as f:
f.write(struct.pack("!I", initrd_size))
# Compute cmdline, store in parmfile used by .ins & exec scripts
parmfile = boot_dir.joinpath("parmfile.ubuntu")
with parmfile.open("w") as f:
f.write(" --- ")
# Generate secondary top-level ubuntu.ins file
# This transforms lines not starting with * by prepending "boot/"
ubuntu_ins_src = boot_dir.joinpath("ubuntu.ins")
ubuntu_ins_dst = self.iso_root.joinpath("ubuntu.ins")
if ubuntu_ins_src.exists():
self.logger.run(
["sed", "-e", "s,^[^*],boot/&,g", ubuntu_ins_src],
stdout=ubuntu_ins_dst.open("w"),
check=True,
)
# Generate QEMU-KVM boot image using gen_s390_cd_kernel
cmdline = parmfile.read_text().strip()
ikr_file = boot_dir.joinpath("ubuntu.ikr")
gen_s390_cd_kernel(kernel_dst, initrd_dst, cmdline, ikr_file)
# Extract bootloader signing certificate
installed_pem = pathlib.Path("/usr/lib/s390-tools/stage3.pem")
squashfs_root = self.iso_root.joinpath("squashfs-root")
squashfs_path = self.iso_root.joinpath(
"casper", "ubuntu-server-minimal.squashfs"
)
if squashfs_path.exists():
self.logger.run(
[
"unsquashfs",
"-no-xattrs",
"-i",
"-d",
squashfs_root,
squashfs_path,
installed_pem,
],
check=True,
)
# Move certificate to iso root
cert_src = squashfs_root.joinpath(str(installed_pem).lstrip("/"))
cert_dst = self.iso_root.joinpath("ubuntu.pem")
if cert_src.exists():
cert_src.replace(cert_dst)
# Clean up squashfs extraction
shutil.rmtree(squashfs_root)

View File

@ -0,0 +1,168 @@
"""UEFI boot configuration for AMD64 and ARM64 architectures."""
import pathlib
import shutil
from ..builder import Logger
from .grub import copy_grub_common_files, GrubBootConfigurator
def copy_signed_shim_grub(
shim_pkg_dir: pathlib.Path,
grub_pkg_dir: pathlib.Path,
efi_suffix: str,
grub_target: str,
iso_root: pathlib.Path,
) -> None:
efi_boot_dir = iso_root.joinpath("EFI", "boot")
efi_boot_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(
shim_pkg_dir.joinpath(
"usr", "lib", "shim", f"shim{efi_suffix}.efi.signed.latest"
),
efi_boot_dir.joinpath(f"boot{efi_suffix}.efi"),
)
shutil.copy(
shim_pkg_dir.joinpath("usr", "lib", "shim", f"mm{efi_suffix}.efi"),
efi_boot_dir.joinpath(f"mm{efi_suffix}.efi"),
)
shutil.copy(
grub_pkg_dir.joinpath(
"usr",
"lib",
"grub",
f"{grub_target}-efi-signed",
f"gcd{efi_suffix}.efi.signed",
),
efi_boot_dir.joinpath(f"grub{efi_suffix}.efi"),
)
grub_boot_dir = iso_root.joinpath("boot", "grub", f"{grub_target}-efi")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = grub_pkg_dir.joinpath("usr", "lib", "grub", f"{grub_target}-efi")
for mod_file in src_grub_dir.glob("*.mod"):
shutil.copy(mod_file, grub_boot_dir)
for lst_file in src_grub_dir.glob("*.lst"):
shutil.copy(lst_file, grub_boot_dir)
def create_eltorito_esp_image(
logger: Logger, iso_root: pathlib.Path, target_file: pathlib.Path
) -> None:
logger.log("creating El Torito ESP image")
efi_dir = iso_root.joinpath("EFI")
# Calculate size: du -s --apparent-size --block-size=1024 + 1024
result = logger.run(
["du", "-s", "--apparent-size", "--block-size=1024", efi_dir],
capture_output=True,
text=True,
check=True,
)
size_kb = int(result.stdout.split()[0]) + 1024
# Create filesystem: mkfs.msdos -n ESP -C -v
logger.run(
["mkfs.msdos", "-n", "ESP", "-C", "-v", target_file, str(size_kb)],
check=True,
)
# Copy files: mcopy -s -i target_file EFI ::/.
logger.run(["mcopy", "-s", "-i", target_file, efi_dir, "::/."], check=True)
class UEFIBootConfigurator(GrubBootConfigurator):
"""Base class for UEFI-based architectures (AMD64, ARM64).
Subclasses should set:
- efi_suffix: EFI binary suffix (e.g., "x64", "aa64")
- grub_target: GRUB target name (e.g., "x86_64", "arm64")
"""
# Subclasses must override these
efi_suffix: str = ""
grub_target: str = ""
arch: str = ""
def get_uefi_grub_packages(self) -> list[str]:
"""Return list of UEFI GRUB packages to download."""
return [
"grub2-common",
f"grub-efi-{self.arch}-bin",
f"grub-efi-{self.arch}-signed",
]
def extract_uefi_files(self) -> None:
"""Extract common UEFI files to boot tree."""
shim_pkg_dir = self.scratch.joinpath("shim-pkg")
grub_pkg_dir = self.scratch.joinpath("grub-pkg")
# Download UEFI packages
self.download_and_extract_package("shim-signed", shim_pkg_dir)
for pkg in self.get_uefi_grub_packages():
self.download_and_extract_package(pkg, grub_pkg_dir)
# Add common files for GRUB to tree
copy_grub_common_files(grub_pkg_dir, self.iso_root)
# Add EFI GRUB to tree
copy_signed_shim_grub(
shim_pkg_dir,
grub_pkg_dir,
self.efi_suffix,
self.grub_target,
self.iso_root,
)
# Create ESP image for El-Torito catalog and hybrid boot
create_eltorito_esp_image(
self.logger, self.iso_root, self.scratch.joinpath("cd-boot-efi.img")
)
def uefi_menu_entries(self) -> str:
"""Return UEFI firmware menu entries."""
return """\
menuentry 'Boot from next volume' {
exit 1
}
menuentry 'UEFI Firmware Settings' {
fwsetup
}
"""
def get_uefi_mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return common UEFI mkisofs options."""
# To make our ESP / El-Torito image compliant with MBR/GPT standards,
# we first append it as a partition and then point the El Torito at
# it. See https://lists.debian.org/debian-cd/2019/07/msg00007.html
opts: list[str | pathlib.Path] = [
"-append_partition",
"2",
"0xef",
self.scratch.joinpath("cd-boot-efi.img"),
"-appended_part_as_gpt",
]
# Some BIOSes ignore removable disks with no partitions marked bootable
# in the MBR. Make sure our protective MBR partition is marked bootable.
opts.append("--mbr-force-bootable")
# Start a new entry in the el torito boot catalog
opts.append("-eltorito-alt-boot")
# Specify where the el torito UEFI boot image "name". We use a special
# syntax available in latest xorriso to point at our newly-created
# partition.
opts.extend(["-e", "--interval:appended_partition_2:all::"])
# Whether to emulate a floppy or not is a per-boot-catalog-entry
# thing, so we need to say it again.
opts.append("-no-emul-boot")
# Create a partition table entry that covers the iso9660 filesystem
opts.extend(["-partition_offset", "16"])
return opts

View File

@ -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 in ("ppc64el", "riscv64"):
kernel_name = "vmlinux"
with self.logger.logged(
f"linking artifacts from {casper_dir} to {artifact_dir}"
):
@ -276,64 +278,41 @@ 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 kernel_path in artifact_dir.glob(f"{filename_prefix}kernel*"):
suffix = kernel_path.name[len(filename_prefix) + len("kernel") :]
prefix = "hwe-" if suffix.endswith("-hwe") else ""
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.workdir,
self.iso_root,
)
configurator.make_bootable(
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 +330,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.workdir,
self.iso_root,
)
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 +365,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)

View File

@ -1,5 +1,9 @@
#!/bin/sh
# Create kernel/initrd artifacts for isobuilder to consume.
# The standard MAKE_ISO flow in auto/build expects files named
# ${PREFIX}.kernel-${flavour} and ${PREFIX}.initrd-${flavour}.
set -eu
case $ARCH in
@ -10,68 +14,7 @@ case $ARCH in
;;
esac
. config/binary
PREFIX="livecd.${PROJECT}"
KERNEL=chroot/boot/vmlinuz
INITRD=chroot/boot/initrd.img
git clone https://git.launchpad.net/~ubuntu-cdimage/debian-cd/+git/ubuntu debian-cd
export BASEDIR=$(readlink -f debian-cd) DIST=$LB_DISTRIBUTION
cat > apt.conf <<EOF
Dir "$(pwd)/chroot";
EOF
case $ARCH in
amd64)
mkdir -p "ubuntu-mini-iso/amd64/tree/casper"
cp "$KERNEL" ubuntu-mini-iso/amd64/tree/casper/filesystem.kernel-generic
cp "$INITRD" ubuntu-mini-iso/amd64/tree/casper/filesystem.initrd-generic
APT_CONFIG_amd64=$(pwd)/apt.conf $BASEDIR/tools/boot/$LB_DISTRIBUTION/boot-amd64 1 $(readlink -f ubuntu-mini-iso/amd64/tree)
# Overwrite the grub.cfg that debian-cd generates by default
cat > ubuntu-mini-iso/amd64/tree/boot/grub/grub.cfg <<EOF
menuentry "Choose an Ubuntu version to install" {
set gfxpayload=keep
linux /casper/vmlinuz iso-chooser-menu ip=dhcp ---
initrd /casper/initrd
}
EOF
rm -f ubuntu-mini-iso/amd64/tree/boot/grub/loopback.cfg ubuntu-mini-iso/amd64/tree/boot/memtest*.bin
;;
esac
mkdir -p ubuntu-mini-iso/$ARCH/tree/.disk
touch ubuntu-mini-iso/$ARCH/tree/.disk/base_installable
tmpdir=$(mktemp -d)
unmkinitramfs $INITRD $tmpdir
if [ -e $tmpdir/*/conf/uuid.conf ]; then
uuid_conf=$tmpdir/*/conf/uuid.conf
elif [ -e "$tmpdir/conf/uuid.conf" ]; then
uuid_conf="$tmpdir/conf/uuid.conf"
else
echo "uuid.conf not found"
exit 1
fi
cp $uuid_conf ubuntu-mini-iso/$ARCH/tree/.disk/casper-uuid-generic
rm -fr $tmpdir
cat > ubuntu-mini-iso/$ARCH/tree/.disk/cd_type <<EOF
full_cd/single
EOF
version=$(distro-info --fullname --series=$LB_DISTRIBUTION \
| sed s'/^Ubuntu/ubuntu-mini-iso/')
cat > ubuntu-mini-iso/$ARCH/tree/.disk/info <<EOF
$version - $ARCH ($BUILDSTAMP)
EOF
dest="${PWD}/livecd.${PROJECT}.iso"
cd ubuntu-mini-iso/$ARCH
xorriso -as mkisofs $(cat 1.mkisofs_opts) tree -o $dest
cd ../..
rm -rf ubuntu-mini-iso
cp chroot/boot/vmlinuz "${PREFIX}.kernel-generic"
cp chroot/boot/initrd.img "${PREFIX}.initrd-generic"

View File

@ -2,7 +2,7 @@
# create the system seed for TPM-backed FDE in the live layer of the installer.
set -eux
set -eu
case ${PASS:-} in
*.live)
@ -13,8 +13,15 @@ case ${PASS:-} in
esac
. config/binary
. config/common
. config/functions
set -x
if ! echo $PASSES | grep --quiet enhanced-secureboot; then
# Only run this hook if there is going to be a layer that installs it...
exit 0
fi
# Naive conversion from YAML to JSON. This is needed because yq is in universe
# (but jq is not).
@ -126,8 +133,25 @@ get_components()
# env SNAPPY_STORE_NO_CDN=1 snap known --remote model series=16 brand-id=canonical model=ubuntu-classic-2410-amd64 > config/classic-model.model
#
dangerous_model=/usr/share/livecd-rootfs/live-build/${PROJECT}/ubuntu-classic-amd64-dangerous.model
stable_model=/usr/share/livecd-rootfs/live-build/${PROJECT}/ubuntu-classic-amd64.model
# We used to have the models included in livecd-rootfs itself, but now we pull
# them from the Launchpad git mirror.
canonical_models_tree=$(mktemp -d)
git clone --depth 1 https://git.launchpad.net/canonical-models -- "${canonical_models_tree}"
cleanup_repo()
{
rm -rf -- "${canonical_models_tree}"
}
trap cleanup_repo EXIT
echo 'Checked out canonical-models revision' "$(git -C "${canonical_models_tree}" rev-parse HEAD)"
model_version=$(release_ver | sed 's/\.//')
dangerous_model="${canonical_models_tree}"/ubuntu-classic-"${model_version}"-amd64-dangerous.model
stable_model="${canonical_models_tree}"/ubuntu-classic-"${model_version}"-amd64.model
prepare_args=()

View File

@ -1,109 +0,0 @@
type: model
authority-id: canonical
series: 16
brand-id: canonical
model: ubuntu-classic-2604-amd64-dangerous
architecture: amd64
base: core24
classic: true
distribution: ubuntu
grade: dangerous
snaps:
-
default-channel: classic-26.04/edge
id: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH
name: pc
type: gadget
-
components:
nvidia-580-uda-ko:
presence: optional
nvidia-580-uda-user:
presence: optional
default-channel: 26.04/beta
id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza
name: pc-kernel
type: kernel
-
default-channel: latest/edge
id: amcUKQILKXHHTlmSa7NMdnXSx02dNeeT
name: core22
type: base
-
default-channel: latest/edge
id: dwTAh7MZZ01zyriOZErqd1JynQLiOGvM
name: core24
type: base
-
default-channel: latest/edge
id: cUqM61hRuZAJYmIS898Ux66VY61gBbZf
name: core26
type: base
-
default-channel: latest/edge
id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4
name: snapd
type: snapd
-
default-channel: latest/edge
id: EISPgh06mRh1vordZY9OZ34QHdd7OrdR
name: bare
type: base
-
default-channel: latest/edge
id: HyhSEBPv3vHsW6uOHkQR384NgI7S6zpj
name: mesa-2404
type: app
-
default-channel: 1/edge
id: EI0D1KHjP8XiwMZKqSjuh6W8zvcowUVP
name: firmware-updater
type: app
-
default-channel: 1/edge
id: FppXWunWzuRT2NUT9CwoBPNJNZBYOCk0
name: desktop-security-center
type: app
-
default-channel: 1/edge
id: aoc5lfC8aUd2VL8VpvynUJJhGXp5K6Dj
name: prompting-client
type: app
-
default-channel: 2/edge
id: gjf3IPXoRiipCu9K0kVu52f0H56fIksg
name: snap-store
type: app
-
default-channel: latest/edge
id: jZLfBRzf1cYlYysIjD2bwSzNtngY0qit
name: gtk-common-themes
type: app
-
default-channel: latest/edge
id: 3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk
name: firefox
type: app
-
default-channel: latest/edge
id: ew7OxpbRTxfK7ImpIygRR85lkxvU7Pzt
name: gnome-46-2404
type: app
-
default-channel: latest/edge
id: IrwRHakqtzhFRHJOOPxKVPU0Kk7Erhcu
name: snapd-desktop-integration
type: app
timestamp: 2025-12-09T12:00:00.0Z
sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn
AcLBXAQAAQoABgUCaUFt7QAKCRDgT5vottzAEhdnD/92LBcQm3iw/kPao4KqGE0OhfXDFd7Z6+Qv
A1Dlzz6Cw0tuj0r5aZH7vJQCx4kC1Eaoi8apg3XhqAyhr74/MsIwMhPPL8qcSNv8ZWruoGwFp/rx
M6NSBKc6hrYqACYfEkBwfq9SgmIDQKFeBVudwswLK2SN58wrDNJjuWz/eJ5hUIIe3ga5ScfzO4Jr
jTWS4kh5lpttCPFX8ouLkMgLUxijQpxFbHoF1trXJndFvavStT0yuC0y5TXzb3wJbbiF/MXZWyjV
/4U+oQLodO77MhaD01kk2y5bZ62YuQ3MPL0fQGypon12GPHeNNcEcYWRZlFv+JkWAduWlnuefj1D
dVWV8dQQmSZGZNiGTsIJxkY9+4B+t/OhosGDc6jEmEZcKNVi9fnl0+awkzK6scNNmupZ8NwJl8ZR
mJSsfaBcH4paYV1x31y4uTELv+OuDWAJ3D0RoCR8H0djTBxRhsF2/JpSJasxVmSbzWHPSeM3f1aO
ChZGwbD6J2SpzsrdogUP/9z6o8YuVnJkOxoBYuXhT1pEYTd93/hE++j3MpOqey/xw8UDbYmq5oJf
uKaYLOMphqDm5hUCZmxQp8gTzDleZGjxYS2fOS4qFUJlvyVwsSoJMXU+6YfA6tgEQ4Dbh6zp6r78
MjEqfWn4lL16xW2Zzr6e8xWwUrM7T3Gp4WTA7/xOeA==

View File

@ -1,104 +0,0 @@
type: model
authority-id: canonical
series: 16
brand-id: canonical
model: ubuntu-classic-2604-amd64
architecture: amd64
base: core24
classic: true
distribution: ubuntu
grade: signed
snaps:
-
default-channel: classic-26.04/stable
id: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH
name: pc
type: gadget
-
components:
nvidia-580-uda-ko:
presence: optional
nvidia-580-uda-user:
presence: optional
default-channel: 26.04/stable
id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza
name: pc-kernel
type: kernel
-
default-channel: latest/stable
id: amcUKQILKXHHTlmSa7NMdnXSx02dNeeT
name: core22
type: base
-
default-channel: latest/stable
id: dwTAh7MZZ01zyriOZErqd1JynQLiOGvM
name: core24
type: base
-
default-channel: latest/stable
id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4
name: snapd
type: snapd
-
default-channel: latest/stable
id: EISPgh06mRh1vordZY9OZ34QHdd7OrdR
name: bare
type: base
-
default-channel: latest/stable/ubuntu-26.04
id: HyhSEBPv3vHsW6uOHkQR384NgI7S6zpj
name: mesa-2404
type: app
-
default-channel: 1/stable/ubuntu-26.04
id: EI0D1KHjP8XiwMZKqSjuh6W8zvcowUVP
name: firmware-updater
type: app
-
default-channel: 1/stable/ubuntu-26.04
id: FppXWunWzuRT2NUT9CwoBPNJNZBYOCk0
name: desktop-security-center
type: app
-
default-channel: 1/stable/ubuntu-26.04
id: aoc5lfC8aUd2VL8VpvynUJJhGXp5K6Dj
name: prompting-client
type: app
-
default-channel: 2/stable/ubuntu-26.04
id: gjf3IPXoRiipCu9K0kVu52f0H56fIksg
name: snap-store
type: app
-
default-channel: latest/stable/ubuntu-26.04
id: jZLfBRzf1cYlYysIjD2bwSzNtngY0qit
name: gtk-common-themes
type: app
-
default-channel: latest/stable/ubuntu-26.04
id: 3wdHCAVyZEmYsCMFDE9qt92UV8rC8Wdk
name: firefox
type: app
-
default-channel: latest/stable/ubuntu-26.04
id: ew7OxpbRTxfK7ImpIygRR85lkxvU7Pzt
name: gnome-46-2404
type: app
-
default-channel: latest/stable/ubuntu-26.04
id: IrwRHakqtzhFRHJOOPxKVPU0Kk7Erhcu
name: snapd-desktop-integration
type: app
timestamp: 2025-12-09T12:00:00.0Z
sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn
AcLBXAQAAQoABgUCaYzP9QAKCRDgT5vottzAEus2D/4jJVutpoPmDrLjNQLn2KNf/f1L2zU8ESSe
VpFjy+9Ff7AxXckALM4eEy/J5mc+UNhHQ/7Thp4XYy2NiH14n9Lv5kVqZCz8udiEfcfLy5gGveio
oXyGX7J5x9sq3YXV1IHS84aqJS0si80TTLCRQXUN8oUZIVRkgFOGIVVneQkn1ppNs87kNgvBT1ow
nwr9fVvZnt5bTprCxs4R5cEUlWTJMN4l96Eh530Q+wqCjFxbTs6FADUYielsFnBDl/Q1M0fozg4F
Ct4gBbvFGWZhp8LXiCbJvTd3PAAV1HYAgtKDKZT0NQp8qaU5DpgTDiUzIjaAJP7feSU5AYDLuVSH
V3zD8sosg1nmPvVtuSi2q5Z+/zd6gmG+vLn5d16whNqELDnX0O9Hxarc/3DD3ANZrrbXlq/PEJNB
Lor5osHLN4utW7CUC5MIEQ5/Z/6cSuav6rQ+bBiAOzQSHRCbhfyCGSMMINX2CE3ePw3moi9gwXeh
vKw1iItEOxywEKbeBNEvddnGsvmzoqf9Jg53/X0yrQQVZTHYFsQlTRk9ggajdZnPjJMTqlAqjXnP
QCsgnprvln0akW4IfEzc+IgoF5eiShJd4IidkBbbdNXRRYlHfmOG7ZvR9upJwe1M73Zfu1nQFEvT
fly59e2Vw8O50ljOVW3jT5fW36z8h1+ttxkKwVsQJg==