Compare commits

..

No commits in common. "ubuntu/master" and "26.04.19" have entirely different histories.

17 changed files with 522 additions and 354 deletions

49
debian/changelog vendored
View File

@ -1,52 +1,3 @@
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

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 ${PWD}/for-iso.filesystem.squashfs
create_squashfs chroot 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

@ -1397,8 +1397,6 @@ 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,7 +44,6 @@ 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}
@ -55,26 +54,22 @@ create_manifest() {
echo "create_manifest creating file listing."
local target_filelist=${2%.manifest}.filelist
(cd "${chroot_root}" && find -xdev) | sort > "${target_filelist}"
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 skipped"
# 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
fi
echo "create_manifest finished"
}

View File

@ -108,10 +108,10 @@ class AptStateManager:
def in_release_path(self) -> pathlib.Path:
"""Return the path to the InRelease file.
This ignores all but the first path.
Will raise Error if there isn't at least one match.
This assumes exactly one InRelease file matches the pattern.
Will raise ValueError if there are 0 or multiple matches.
"""
[path] = self.apt_root.joinpath("var/lib/apt/lists").glob(
f"*ubuntu.com*_dists_{self.series}_InRelease"
f"*_dists_{self.series}_InRelease"
)
return path

View File

@ -17,7 +17,6 @@ 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."""
@ -25,23 +24,23 @@ def make_boot_configurator_for_arch(
case "amd64":
from .amd64 import AMD64BootConfigurator
return AMD64BootConfigurator(logger, apt_state, workdir, iso_root)
return AMD64BootConfigurator(logger, apt_state, iso_root)
case "arm64":
from .arm64 import ARM64BootConfigurator
return ARM64BootConfigurator(logger, apt_state, workdir, iso_root)
return ARM64BootConfigurator(logger, apt_state, iso_root)
case "ppc64el":
from .ppc64el import PPC64ELBootConfigurator
return PPC64ELBootConfigurator(logger, apt_state, workdir, iso_root)
return PPC64ELBootConfigurator(logger, apt_state, iso_root)
case "riscv64":
from .riscv64 import RISCV64BootConfigurator
return RISCV64BootConfigurator(logger, apt_state, workdir, iso_root)
return RISCV64BootConfigurator(logger, apt_state, iso_root)
case "s390x":
from .s390x import S390XBootConfigurator
return S390XBootConfigurator(logger, apt_state, workdir, iso_root)
return S390XBootConfigurator(logger, apt_state, iso_root)
case _:
raise ValueError(f"Unsupported architecture: {arch}")

View File

@ -3,9 +3,8 @@
import pathlib
import shutil
from .base import default_kernel_params
from .grub import copy_grub_modules
from .uefi import UEFIBootConfigurator
from .base import default_kernel_params
CALAMARES_PROJECTS = ["kubuntu", "lubuntu"]
@ -52,13 +51,16 @@ class AMD64BootConfigurator(UEFIBootConfigurator):
opts.extend(
[
"--grub2-mbr",
self.scratch.joinpath("boot_hybrid.img"),
self.grub_dir.joinpath("usr/lib/grub/i386-pc/boot_hybrid.img"),
]
)
# ## Set up the mkisofs options for UEFI boot.
opts.extend(self.get_uefi_mkisofs_opts())
# ## Add cd-boot-tree to the ISO
opts.append(str(self.boot_tree))
return opts
def extract_files(self) -> None:
@ -69,113 +71,115 @@ class AMD64BootConfigurator(UEFIBootConfigurator):
# 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)
self.download_and_extract_package("grub-pc-bin", self.grub_dir)
grub_boot_dir = self.iso_root.joinpath("boot", "grub", "i386-pc")
grub_boot_dir = self.boot_tree.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")
src_grub_dir = self.grub_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"],
self.copy_grub_modules(
src_grub_dir, grub_boot_dir, ["*.mod", "*.lst", "*.o"]
)
def generate_grub_config(self) -> str:
"""Generate grub.cfg content for AMD64."""
result = self.grub_header()
def generate_grub_config(self) -> None:
"""Generate grub.cfg and loopback.cfg for the boot tree."""
boot_grub_dir = self.boot_tree.joinpath("boot", "grub")
boot_grub_dir.mkdir(parents=True, exist_ok=True)
grub_cfg = boot_grub_dir.joinpath("grub.cfg")
if self.project == "ubuntu-mini-iso":
result += """\
menuentry "Choose an Ubuntu version to install" {
self.write_grub_header(grub_cfg)
with grub_cfg.open("a") as f:
f.write(
"""menuentry "Choose an Ubuntu version to install" {
set gfxpayload=keep
linux /casper/vmlinuz iso-chooser-menu ip=dhcp ---
initrd /casper/initrd
linux /casper/vmlinuz iso-chooser-menu ip=dhcp ---
initrd /casper/initrd
}
"""
return result
)
return
# Generate grub.cfg
kernel_params = default_kernel_params(self.project)
# Write common GRUB header
self.write_grub_header(grub_cfg)
# Main menu entry
result += f"""\
menuentry "Try or Install {self.humanproject}" {{
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinuz {kernel_params}
initrd /casper/initrd
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)" {{
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "{self.humanproject} (safe graphics)" {{
set gfxpayload=keep
linux /casper/vmlinuz nomodeset {kernel_params}
initrd /casper/initrd
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)" {{
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep
linux /casper/vmlinuz {oem_kernel_params}
initrd /casper/initrd
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)" {{
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep
linux /casper/vmlinuz {kernel_params} oem-config/enable=true
initrd /casper/initrd
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" {{
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "{self.humanproject} with the HWE kernel" {{
set gfxpayload=keep
linux /casper/hwe-vmlinuz {kernel_params}
initrd /casper/hwe-initrd
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()
# Create the loopback config, based on the main config
with grub_cfg.open("r") as f:
content = f.read()
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")
# sed: delete from line 1 to menu_color_highlight, delete from
# grub_platform to end and replace '---' with
# 'iso-scan/filename=${iso_path} ---' in lines with 'linux'
lines = content.split("\n")
start_idx = 0
for i, line in enumerate(lines):
if "menu_color_highlight" in line:
@ -198,19 +202,16 @@ fi
for line in loopback_lines
]
return "\n".join(loopback_lines)
loopback_cfg = boot_grub_dir.joinpath("loopback.cfg")
with loopback_cfg.open("w") as f:
f.write("\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)
)
# UEFI Entries (wrapped in grub_platform check for dual BIOS/UEFI support)
with grub_cfg.open("a") as f:
f.write("grub_platform\n")
f.write('if [ "$grub_platform" = "efi" ]; then\n')
self.write_uefi_menu_entries(grub_cfg)
with grub_cfg.open("a") as f:
f.write("fi\n")

View File

@ -26,6 +26,7 @@ class ARM64BootConfigurator(UEFIBootConfigurator):
opts.extend(self.get_uefi_mkisofs_opts())
# ARM64-specific: partition cylinder alignment
opts.extend(["-partition_cyl_align", "all"])
opts.append(self.boot_tree)
return opts
def extract_files(self) -> None:
@ -33,15 +34,19 @@ class ARM64BootConfigurator(UEFIBootConfigurator):
with self.logger.logged("extracting ARM64 boot files"):
self.extract_uefi_files()
def generate_grub_config(self) -> str:
def generate_grub_config(self) -> None:
"""Generate grub.cfg for ARM64."""
kernel_params = default_kernel_params(self.project)
result = self.grub_header()
grub_cfg = self.grub_dir.joinpath("grub.cfg")
# Write common GRUB header
self.write_grub_header(grub_cfg)
# ARM64-specific: Snapdragon workarounds
result += f"""\
set cmdline=
with grub_cfg.open("a") as f:
f.write(
"""set cmdline=
smbios --type 4 --get-string 5 --set proc_version
regexp "Snapdragon.*" "$proc_version"
if [ $? = 0 ]; then
@ -54,14 +59,16 @@ if [ $? = 0 ]; then
fi
menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinuz $cmdline {kernel_params} console=tty0
initrd /casper/initrd
\tset gfxpayload=keep
\tlinux\t/casper/vmlinuz $cmdline {kernel_params} console=tty0
\tinitrd\t/casper/initrd
}}
"""
)
# HWE kernel option if available
result += self.hwe_menu_entry(
self.write_hwe_menu_entry(
grub_cfg,
"vmlinuz",
f"{kernel_params} console=tty0",
extra_params="$cmdline ",
@ -71,6 +78,4 @@ menuentry "Try or Install {self.humanproject}" {{
# 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
self.write_uefi_menu_entries(grub_cfg)

View File

@ -1,6 +1,7 @@
"""Base classes and helper functions for boot configuration."""
import pathlib
import shutil
import subprocess
import tempfile
from abc import ABC, abstractmethod
@ -33,14 +34,17 @@ class BaseBootConfigurator(ABC):
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 create_dirs(self, workdir):
self.scratch = workdir.joinpath("boot-stuff")
self.scratch.mkdir(exist_ok=True)
self.boot_tree = self.scratch.joinpath("cd-boot-tree")
def download_and_extract_package(
self, pkg_name: str, target_dir: pathlib.Path
) -> None:
@ -61,6 +65,14 @@ class BaseBootConfigurator(ABC):
dpkg_proc.stdout.close()
tar_proc.communicate()
def copy_grub_modules(
self, src_dir: pathlib.Path, dest_dir: pathlib.Path, extensions: list[str]
) -> None:
"""Copy GRUB module files matching given extensions from src to dest."""
for ext in extensions:
for file in src_dir.glob(ext):
shutil.copy(file, dest_dir)
@abstractmethod
def extract_files(self) -> None:
"""Download and extract bootloader packages to the boot tree.
@ -83,6 +95,7 @@ class BaseBootConfigurator(ABC):
def make_bootable(
self,
workdir: pathlib.Path,
project: str,
capproject: str,
subarch: str,
@ -93,6 +106,6 @@ class BaseBootConfigurator(ABC):
self.humanproject = capproject.replace("-", " ")
self.subarch = subarch
self.hwe = hwe
self.scratch.mkdir(exist_ok=True)
self.create_dirs(workdir)
with self.logger.logged("configuring boot"):
self.extract_files()

View File

@ -7,31 +7,17 @@ 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")
def copy_grub_common_files_to_boot_tree(
grub_dir: pathlib.Path, boot_tree: pathlib.Path
) -> None:
fonts_dir = boot_tree.joinpath("boot", "grub", "fonts")
fonts_dir.mkdir(parents=True, exist_ok=True)
src = grub_pkg_dir.joinpath("usr", "share", "grub", "unicode.pf2")
src = grub_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).
@ -39,66 +25,79 @@ class GrubBootConfigurator(BaseBootConfigurator):
Subclasses must implement generate_grub_config().
"""
def grub_header(self, include_loadfont: bool = True) -> str:
"""Return common GRUB config header (timeout, colors).
def create_dirs(self, workdir):
super().create_dirs(workdir)
self.grub_dir = self.boot_tree.joinpath("grub")
def setup_grub_common_files(self) -> None:
"""Copy common GRUB files (fonts, etc.) to boot tree."""
copy_grub_common_files_to_boot_tree(self.grub_dir, self.boot_tree)
def write_grub_header(
self, grub_cfg: pathlib.Path, include_loadfont: bool = True
) -> None:
"""Write common GRUB config header (timeout, colors).
Args:
grub_cfg: Path to grub.cfg file
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
with grub_cfg.open("a") as f:
f.write("set timeout=30\n\n")
if include_loadfont:
f.write("loadfont unicode\n\n")
f.write(
"""set menu_color_normal=white/black
set menu_color_highlight=black/light-gray
"""
return result
)
def hwe_menu_entry(
def write_hwe_menu_entry(
self,
grub_cfg: pathlib.Path,
kernel_name: str,
kernel_params: str,
extra_params: str = "",
) -> str:
"""Return HWE kernel menu entry if HWE is enabled.
) -> None:
"""Write HWE kernel menu entry if HWE is enabled.
Args:
grub_cfg: Path to grub.cfg file
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
if self.hwe:
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "{self.humanproject} with the HWE kernel" {{
\tset gfxpayload=keep
\tlinux\t/casper/hwe-{kernel_name} {extra_params}{kernel_params}
\tinitrd\t/casper/hwe-initrd
}}
"""
)
@abstractmethod
def generate_grub_config(self) -> str:
"""Generate grub.cfg content.
def generate_grub_config(self) -> None:
"""Generate grub.cfg configuration file.
Each GRUB-based architecture must implement this to return the
GRUB configuration.
Each GRUB-based architecture must implement this to create its
specific GRUB configuration.
"""
...
def make_bootable(
self,
workdir: pathlib.Path,
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)
super().make_bootable(workdir, 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)
self.generate_grub_config()

View File

@ -3,11 +3,7 @@
import pathlib
import shutil
from .grub import (
copy_grub_common_files,
copy_grub_modules,
GrubBootConfigurator,
)
from .grub import GrubBootConfigurator
from .base import default_kernel_params
@ -16,26 +12,28 @@ class PPC64ELBootConfigurator(GrubBootConfigurator):
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for PPC64EL."""
return []
# Add cd-boot-tree to the ISO
return [self.boot_tree]
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)
self.download_and_extract_package("grub2-common", self.grub_dir)
self.download_and_extract_package("grub-ieee1275-bin", self.grub_dir)
# Add common files for GRUB to tree
copy_grub_common_files(grub_pkg_dir, self.iso_root)
self.setup_grub_common_files()
# Add IEEE1275 ppc boot files
ppc_dir = self.iso_root.joinpath("ppc")
ppc_dir.mkdir()
ppc_dir = self.boot_tree.joinpath("ppc")
ppc_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = grub_pkg_dir.joinpath("usr", "lib", "grub", "powerpc-ieee1275")
grub_boot_dir = self.boot_tree.joinpath("boot", "grub", "powerpc-ieee1275")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = self.grub_dir.joinpath("usr", "lib", "grub", "powerpc-ieee1275")
# Copy bootinfo.txt to ppc directory
shutil.copy(
@ -45,30 +43,33 @@ class PPC64ELBootConfigurator(GrubBootConfigurator):
# 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"),
self.boot_tree.joinpath("boot", "grub", "powerpc.elf"),
)
# Copy GRUB modules
copy_grub_modules(
grub_pkg_dir, self.iso_root, "powerpc-ieee1275", ["*.mod", "*.lst"]
)
self.copy_grub_modules(src_grub_dir, grub_boot_dir, ["*.mod", "*.lst"])
def generate_grub_config(self) -> str:
def generate_grub_config(self) -> None:
"""Generate grub.cfg for PPC64EL."""
kernel_params = default_kernel_params(self.project)
result = self.grub_header()
grub_cfg = self.grub_dir.joinpath("grub.cfg")
# Write common GRUB header
self.write_grub_header(grub_cfg)
# Main menu entry
result += f"""\
menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinux quiet {kernel_params}
initrd /casper/initrd
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "Try or Install {self.humanproject}" {{
\tset gfxpayload=keep
\tlinux\t/casper/vmlinux quiet {kernel_params}
\tinitrd\t/casper/initrd
}}
"""
)
# HWE kernel option if available
result += self.hwe_menu_entry("vmlinux", kernel_params, extra_params="quiet ")
return result
self.write_hwe_menu_entry(
grub_cfg, "vmlinux", kernel_params, extra_params="quiet "
)

View File

@ -3,31 +3,35 @@
import pathlib
import shutil
from .grub import GrubBootConfigurator, copy_grub_common_files, copy_grub_modules
from .grub import GrubBootConfigurator
def copy_unsigned_monolithic_grub(
grub_pkg_dir: pathlib.Path,
efi_suffix: str,
grub_target: str,
iso_root: pathlib.Path,
def copy_unsigned_monolithic_grub_to_boot_tree(
grub_dir: pathlib.Path, efi_suffix: str, grub_target: str, boot_tree: pathlib.Path
) -> None:
efi_boot_dir = iso_root.joinpath("EFI", "boot")
efi_boot_dir = boot_tree.joinpath("EFI", "boot")
efi_boot_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(
grub_pkg_dir.joinpath(
grub_dir.joinpath(
"usr",
"lib",
"grub",
grub_target,
f"{grub_target}-efi",
"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"])
grub_boot_dir = boot_tree.joinpath("boot", "grub", f"{grub_target}-efi")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = grub_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)
class RISCV64BootConfigurator(GrubBootConfigurator):
@ -70,19 +74,16 @@ class RISCV64BootConfigurator(GrubBootConfigurator):
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("grub2-common", self.grub_dir)
self.download_and_extract_package("grub-efi-riscv64-bin", self.grub_dir)
self.download_and_extract_package("grub-efi-riscv64-unsigned", self.grub_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
self.setup_grub_common_files()
copy_unsigned_monolithic_grub_to_boot_tree(
self.grub_dir, "riscv64", "riscv64", self.boot_tree
)
# Extract DTBs to tree
@ -106,14 +107,22 @@ class RISCV64BootConfigurator(GrubBootConfigurator):
)
# Copy DTBs if they exist
dtb_dir = self.iso_root.joinpath("dtb")
dtb_dir = self.boot_tree.joinpath("dtb")
dtb_dir.mkdir(parents=True, exist_ok=True)
firmware_dir = kernel_layer.joinpath("usr", "lib", "firmware")
device_tree_files = list(firmware_dir.glob("*/device-tree/*"))
for dtb_file in firmware_dir.glob("*/device-tree/*"):
if dtb_file.is_file():
shutil.copy(dtb_file, dtb_dir)
if device_tree_files:
for dtb_file in device_tree_files:
if dtb_file.is_file():
shutil.copy(dtb_file, dtb_dir)
# Clean up kernel layer
shutil.rmtree(kernel_layer)
# Copy tree contents to live-media rootfs
self.logger.run(["cp", "-aT", self.boot_tree, self.iso_root], check=True)
# Create ESP image with GRUB and dtbs
efi_img = self.scratch.joinpath("efi.img")
@ -122,34 +131,41 @@ class RISCV64BootConfigurator(GrubBootConfigurator):
)
# Add EFI files to ESP
efi_dir = self.iso_root.joinpath("EFI")
efi_dir = self.boot_tree.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:
def generate_grub_config(self) -> None:
"""Generate grub.cfg for RISC-V64."""
result = self.grub_header(include_loadfont=False)
grub_dir = self.iso_root.joinpath("boot", "grub")
grub_dir.mkdir(parents=True, exist_ok=True)
grub_cfg = grub_dir.joinpath("grub.cfg")
# Write GRUB header (without loadfont for RISC-V)
self.write_grub_header(grub_cfg, 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
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "Try or Install {self.humanproject}" {{
\tset gfxpayload=keep
\tlinux\t/casper/vmlinux efi=debug sysctl.kernel.watchdog_thresh=60 ---
\tinitrd\t/casper/initrd
}}
"""
)
# HWE kernel option if available
result += self.hwe_menu_entry(
self.write_hwe_menu_entry(
grub_cfg,
"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.

View File

@ -4,31 +4,29 @@ import pathlib
import shutil
from ..builder import Logger
from .grub import copy_grub_common_files, GrubBootConfigurator
from .grub import GrubBootConfigurator
def copy_signed_shim_grub(
shim_pkg_dir: pathlib.Path,
grub_pkg_dir: pathlib.Path,
def copy_signed_shim_grub_to_boot_tree(
shim_dir: pathlib.Path,
grub_dir: pathlib.Path,
efi_suffix: str,
grub_target: str,
iso_root: pathlib.Path,
boot_tree: pathlib.Path,
) -> None:
efi_boot_dir = iso_root.joinpath("EFI", "boot")
efi_boot_dir = boot_tree.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"
),
shim_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"),
shim_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(
grub_dir.joinpath(
"usr",
"lib",
"grub",
@ -38,10 +36,10 @@ def copy_signed_shim_grub(
efi_boot_dir.joinpath(f"grub{efi_suffix}.efi"),
)
grub_boot_dir = iso_root.joinpath("boot", "grub", f"{grub_target}-efi")
grub_boot_dir = boot_tree.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")
src_grub_dir = grub_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"):
@ -49,10 +47,10 @@ def copy_signed_shim_grub(
def create_eltorito_esp_image(
logger: Logger, iso_root: pathlib.Path, target_file: pathlib.Path
logger: Logger, boot_tree: pathlib.Path, target_file: pathlib.Path
) -> None:
logger.log("creating El Torito ESP image")
efi_dir = iso_root.joinpath("EFI")
efi_dir = boot_tree.joinpath("EFI")
# Calculate size: du -s --apparent-size --block-size=1024 + 1024
result = logger.run(
@ -86,6 +84,10 @@ class UEFIBootConfigurator(GrubBootConfigurator):
grub_target: str = ""
arch: str = ""
def create_dirs(self, workdir):
super().create_dirs(workdir)
self.shim_dir = self.boot_tree.joinpath("shim")
def get_uefi_grub_packages(self) -> list[str]:
"""Return list of UEFI GRUB packages to download."""
return [
@ -96,42 +98,40 @@ class UEFIBootConfigurator(GrubBootConfigurator):
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)
self.download_and_extract_package("shim-signed", self.shim_dir)
for pkg in self.get_uefi_grub_packages():
self.download_and_extract_package(pkg, grub_pkg_dir)
self.download_and_extract_package(pkg, self.grub_dir)
# Add common files for GRUB to tree
copy_grub_common_files(grub_pkg_dir, self.iso_root)
self.setup_grub_common_files()
# Add EFI GRUB to tree
copy_signed_shim_grub(
shim_pkg_dir,
grub_pkg_dir,
copy_signed_shim_grub_to_boot_tree(
self.shim_dir,
self.grub_dir,
self.efi_suffix,
self.grub_target,
self.iso_root,
self.boot_tree,
)
# 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")
self.logger, self.boot_tree, 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
def write_uefi_menu_entries(self, grub_cfg: pathlib.Path) -> None:
"""Write UEFI firmware menu entries."""
with grub_cfg.open("a") as f:
f.write(
"""menuentry 'Boot from next volume' {
\texit 1
}
menuentry 'UEFI Firmware Settings' {
fwsetup
\tfwsetup
}
"""
)
def get_uefi_mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return common UEFI mkisofs options."""

View File

@ -268,7 +268,7 @@ class ISOBuilder:
target.hardlink_to(src)
kernel_name = "vmlinuz"
if self.arch in ("ppc64el", "riscv64"):
if self.arch == "ppc64el":
kernel_name = "vmlinux"
with self.logger.logged(
@ -278,18 +278,19 @@ class ISOBuilder:
for path in artifact_dir.glob(f"{filename_prefix}*.{ext}"):
newname = 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",
)
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):
@ -297,10 +298,10 @@ class ISOBuilder:
self.arch,
self.logger,
self.apt_state,
self.workdir,
self.iso_root,
)
configurator.make_bootable(
self.workdir,
project,
capproject,
subarch,
@ -340,9 +341,9 @@ class ISOBuilder:
self.arch,
self.logger,
self.apt_state,
self.workdir,
self.iso_root,
)
configurator.create_dirs(self.workdir)
mkisofs_opts = configurator.mkisofs_opts()
cmd: list[str | pathlib.Path] = ["xorriso"]
if self.arch == "riscv64":

View File

@ -2,7 +2,7 @@
# create the system seed for TPM-backed FDE in the live layer of the installer.
set -eu
set -eux
case ${PASS:-} in
*.live)
@ -13,15 +13,8 @@ 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).
@ -133,25 +126,8 @@ 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
#
# 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
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
prepare_args=()

View File

@ -0,0 +1,109 @@
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

@ -0,0 +1,104 @@
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==