feat!: Only use container images as the only source of truth (#138)

Signed-off-by: Zeglius <33781398+Zeglius@users.noreply.github.com>
This commit is contained in:
Zeglius 2026-05-19 16:48:30 +02:00 committed by GitHub
parent 840217d97b
commit 5c457c3d05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1225 additions and 1257 deletions

View file

@ -1,4 +1,5 @@
name: CI
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow
name: Build Examples CI
on:
workflow_dispatch:
@ -6,68 +7,70 @@ on:
pull_request:
branches:
- main
paths:
- "examples/**"
- ".github/workflows/ci.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build ISO
runs-on: ${{ matrix.platform == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
name: Build ${{ matrix.example }} ${{ matrix.arch }}
permissions:
contents: read
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm64' }}
strategy:
fail-fast: false
matrix:
platform: [amd64, arm64]
image:
# cool images list :)
- ghcr.io/ublue-os/bluefin:lts
example: [bazzite, zirconium]
arch: [amd64]
include:
# big dumdums that dont have aarch64 :P
- platform: amd64
image: ghcr.io/ublue-os/bluefin:stable
- platform: amd64
image: ghcr.io/ublue-os/bluefin-nvidia-open:stable
- platform: amd64
image: ghcr.io/ublue-os/bazzite:stable
container-image: ghcr.io/ublue-os/bazzite-deck:stable
- platform: amd64
image: ghcr.io/ublue-os/aurora:stable
- example: zirconium
arch: arm64
steps:
- name: Maximize Build Space
if: matrix.platform == 'amd64'
uses: ublue-os/remove-unwanted-software@cc0becac701cf642c8f0a6613bbdaf5dc36b259e # v9
if: matrix.arch == 'amd64'
uses: ublue-os/remove-unwanted-software@v9
with:
remove-codeql: true
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@v4
- name: Build Container Image
id: build-container
shell: bash
run: |
IMAGE_NAME="titanoboa-example-${{ matrix.example }}:${{ github.sha }}"
sudo podman build \
--cap-add sys_admin \
--security-opt label=disable \
--volume /var/lib/containers/storage:/usr/lib/containers/storage:ro \
--tag "${IMAGE_NAME}" \
"./examples/${{ matrix.example }}"
echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITHUB_OUTPUT"
- name: Build ISO
id: build-iso
uses: ./
with:
livesys: "true"
flatpaks-list: ./src/flatpaks.example.txt
container-image: ${{ matrix.container-image }}
image-ref: ${{ matrix.image }}
add-polkit: "true"
hook-pre-initramfs: ${{ github.workspace }}/.github/workflows/ci_dummy_hook_preinitramfs.sh
hook-post-rootfs: ${{ github.workspace }}/.github/workflows/ci_dummy_hook_postrootfs.sh
kargs: "NONE"
image-ref: ${{ steps.build-container.outputs.IMAGE_NAME }}
- name: Create Image Name
id: name
shell: bash
env:
IMAGE_NAME: ${{ matrix.container-image || matrix.image }}
run: |
set -eoux pipefail
IMAGE_NAME="${IMAGE_NAME##*/}"
IMAGE_NAME="${IMAGE_NAME/:/-}"
IMAGE_NAME="titanoboa-example-${{ matrix.example }}"
echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITHUB_OUTPUT"
- name: Upload Output Artifacts
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@v4
with:
name: ${{ steps.name.outputs.IMAGE_NAME }}-${{ matrix.platform }}
name: ${{ steps.name.outputs.IMAGE_NAME }}-${{ matrix.arch }}
retention-days: 5
if-no-files-found: error
path: |

View file

@ -1,134 +0,0 @@
#!/usr/bin/env bash
# We need to make our own Profiles. This makes anaconda think we are a Kinoite Install
. /etc/os-release
if [[ "$ID_LIKE" =~ rhel ]]; then
echo 'VARIANT_ID="kinoite"' >>/usr/lib/os-release
else
sed -i "s/^VARIANT_ID=.*/VARIANT_ID=kinoite/" /usr/lib/os-release
fi
sed -i "s/^ID=.*/ID=fedora/" /usr/lib/os-release
# Install Anaconda, Webui if >= F42
if [[ "$ID_LIKE" =~ rhel ]]; then
dnf copr enable -y jreilly1821/anaconda-webui
dnf install -y anaconda-webui anaconda
dnf install -y anaconda-live
HIDE_SPOKE="1"
else
dnf install -y anaconda-live libblockdev-{btrfs,lvm,dm}
if [[ "$(rpm -E %fedora)" -ge 42 ]]; then
# Needed for Anaconda Web UI
mkdir -p /var/lib/rpm-state
dnf install -y anaconda-webui
else
HIDE_SPOKE="1"
fi
fi
if [[ "${HIDE_SPOKE:-}" ]]; then
# Hide Root Spoke
cat <<EOF >>/etc/anaconda/conf.d/anaconda.conf
[User Interface]
hidden_spokes =
PasswordSpoke
EOF
fi
# Get Artwork
git clone https://github.com/ublue-os/packages.git /root/packages
case "${PRETTY_NAME,,}" in
"aurora"*)
mkdir -p /usr/share/anaconda/pixmaps/silverblue
cp -r /root/packages/aurora/fedora-logos/src/anaconda/* /usr/share/anaconda/pixmaps/
cp -r /root/packages/aurora/fedora-logos/src/anaconda/* /usr/share/anaconda/pixmaps/silverblue/
;;
"bazzite"*)
mkdir -p /usr/share/anaconda/pixmaps/silverblue
cp -r /root/packages/bazzite/fedora-logos/* /usr/share/anaconda/pixmaps/
;;
"bluefin"*)
mkdir -p /usr/share/anaconda/pixmaps/silverblue
cp -r /root/packages/bluefin/fedora-logos/src/anaconda/* /usr/share/anaconda/pixmaps/
cp -r /root/packages/bluefin/fedora-logos/src/anaconda/* /usr/share/anaconda/pixmaps/silverblue/
;;
esac
rm -rf /root/packages
# Variables
imageref="$(jq -r '."image-ref"' </usr/share/ublue-os/image-info.json)"
imageref="${imageref##*://}"
imagetag="$(jq -r 'if ."image-branch" then ."image-branch" else ."image-tag" end' </usr/share/ublue-os/image-info.json)"
sbkey='https://github.com/ublue-os/akmods/raw/main/certs/public_key.der'
# Secureboot Key Fetch
mkdir -p /run/install/repo
curl -Lo /run/install/repo/sb_pubkey.der "$sbkey"
# Default Kickstart
cat <<EOF >>/usr/share/anaconda/interactive-defaults.ks
ostreecontainer --url=$imageref:$imagetag --transport=containers-storage --no-signature-verification
%include /usr/share/anaconda/post-scripts/install-configure-upgrade.ks
%include /usr/share/anaconda/post-scripts/disable-fedora-flatpak.ks
%include /usr/share/anaconda/post-scripts/install-flatpaks.ks
%include /usr/share/anaconda/post-scripts/secureboot-enroll-key.ks
EOF
# Signed Images
cat <<EOF >>/usr/share/anaconda/post-scripts/install-configure-upgrade.ks
%post --erroronfail
bootc switch --mutate-in-place --enforce-container-sigpolicy --transport registry $imageref:$imagetag
%end
EOF
# Enroll Secureboot Key
cat <<EOF >>/usr/share/anaconda/post-scripts/secureboot-enroll-key.ks
%post --erroronfail --nochroot
set -oue pipefail
readonly ENROLLMENT_PASSWORD="universalblue"
readonly SECUREBOOT_KEY="/run/install/repo/sb_pubkey.der"
if [[ ! -d "/sys/firmware/efi" ]]; then
echo "EFI mode not detected. Skipping key enrollment."
exit 0
fi
if [[ ! -f "\$SECUREBOOT_KEY" ]]; then
echo "Secure boot key not provided: \$SECUREBOOT_KEY"
exit 0
fi
SYS_ID="\$(cat /sys/devices/virtual/dmi/id/product_name)"
if [[ ":Jupiter:Galileo:" =~ ":\$SYS_ID:" ]]; then
echo "Steam Deck hardware detected. Skipping key enrollment."
exit 0
fi
mokutil --timeout -1 || :
echo -e "\$ENROLLMENT_PASSWORD\n\$ENROLLMENT_PASSWORD" | mokutil --import "\$SECUREBOOT_KEY" || :
%end
EOF
# Install Flatpaks
cat <<'EOF' >>/usr/share/anaconda/post-scripts/install-flatpaks.ks
%post --erroronfail --nochroot
deployment="$(ostree rev-parse --repo=/mnt/sysimage/ostree/repo ostree/0/1/0)"
target="/mnt/sysimage/ostree/deploy/default/deploy/$deployment.0/var/lib/"
mkdir -p "$target"
rsync -aAXUHKP /var/lib/flatpak "$target"
%end
EOF
# Disable Fedora Flatpak Repo
cat <<EOF >>/usr/share/anaconda/post-scripts/disable-fedora-flatpak.ks
%post --erroronfail
systemctl disable flatpak-add-fedora-repos.service
%end
EOF
# Set Anaconda Payload to use flathub
cat <<EOF >>/etc/anaconda/conf.d/anaconda.conf
[Payload]
flatpak_remote = flathub https://dl.flathub.org/repo/
EOF

View file

@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
echo "This is a hook being ran right before building the initramfs."

42
.github/workflows/publish-container.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Publish Titanoboa Container
on:
push:
branches:
- main
jobs:
build-and-push:
name: Build and Publish Container
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

22
Containerfile Normal file
View file

@ -0,0 +1,22 @@
FROM quay.io/fedora/fedora:latest
ENV TITANOBOA_INSIDE_CONTAINER="true"
RUN dnf install -yq \
bash \
dosfstools \
e2fsprogs \
mtools \
squashfs-tools \
util-linux-core \
xorriso \
yq \
;
RUN mkdir -p /rootfs
COPY ./main.sh /app/bin/main.sh
COPY ./build_iso.sh /app/bin/build_iso.sh
VOLUME [ "/output", "/usr/lib/containers/storage" ]
CMD ["/app/bin/main.sh"]

554
Justfile
View file

@ -1,554 +0,0 @@
set unstable := true
PODMAN := which("podman") || require("podman-remote")
workdir := env("TITANOBOA_WORKDIR", "work")
isoroot := env("TITANOBOA_ISO_ROOT", "work/iso-root")
rootfs := workdir/"rootfs"
default_image := "ghcr.io/ublue-os/bluefin:lts"
arch := arch()
### BUILDER CONFIGURATION ###
# Distribution to use for the builder container (for tools and dependencies)
# Supported values: fedora, centos, almalinux
# Set via TITANOBOA_BUILDER_DISTRO environment variable (default: fedora)
builder_distro := env("TITANOBOA_BUILDER_DISTRO", "fedora")
##############################
### HOOKS SCRIPT PATHS ###
# Path to scripts used as hooks in between steps, used in 'hook-*' recipes.
# Must follow the naming convention HOOK_<recipe name without 'hook_' prefix>
# Hook used for custom operations done in the rootfs before it is squashed.
HOOK_post_rootfs := env("HOOK_post_rootfs", "")
# Hook used for custom operations done before the initramfs is generated.
HOOK_pre_initramfs := env("HOOK_pre_initramfs", "")
##########################
### UTILS ###
_ci_grouping := '''
if [[ -n "${CI:-}" ]]; then
echo "::group::${BASH_SOURCE[0]##*/} step"
trap 'echo ::endgroup::' EXIT
fi
'''
[private]
just := just_executable() + " -f " + source_file()
[private]
git_root := source_dir()
[private]
builder_image := if builder_distro == "fedora" { "quay.io/fedora/fedora:latest" } else if builder_distro == "centos" { "ghcr.io/hanthor/centos-anaconda-builder:main" } else if builder_distro == "almalinux-kitten" { "quay.io/almalinuxorg/almalinux:10-kitten" } else if builder_distro == "almalinux" { "quay.io/almalinuxorg/almalinux:10" } else { error("Unsupported builder distribution: " + builder_distro + ". Supported: fedora, centos, almalinux") }
[private]
chroot_function := '
function chroot(){
local command="$1"
shift
local args="$*"
' + PODMAN + ' run --rm -it \
--privileged \
--security-opt label=type:unconfined_t \
$args \
--tmpfs /tmp:rw \
--tmpfs /run:rw \
--volume ' + git_root + ':/app \
--rootfs ' + git_root/rootfs + ' \
/usr/bin/bash -c "$command"
}'
[private]
builder_function := '
function builder(){
local command="$1"
shift
local args="$*"
' + PODMAN + ' run --rm -it \
--privileged \
--security-opt label=disable \
--volume ' + git_root + ':/app \
' + builder_image + ' \
/usr/bin/bash -c "$command" $args
}'
[private]
compress_dependencies := '''
function compress_dependencies(){
local MISSING=()
local DEPS=(
mksquashfs
mkfs.erofs
)
for dep in "${DEPS[@]}"; do
if ! command -v $dep >/dev/null; then
MISSING+=($dep)
fi
done
echo "${#MISSING[@]}"
}
'''
[private]
iso_dependencies := '
function iso_dependencies(){
local MISSING=()
local RPMS=(
dosfstools
grub2
grub2-efi
grub2-tools
grub2-tools-extra
shim
xorriso
)
if [[ "' + arch + '" == "x86_64" ]]; then
RPMS+=(
grub2-efi-x64
grub2-efi-x64-cdboot
grub2-efi-x64-modules
)
elif [[ "' + arch + '" == "aarch64" ]]; then
RPMS+=(grub2-efi-aa64-modules)
fi
if ! command -v rpm >/dev/null; then
echo "1"
return
fi
for rpm in "${RPMS[@]}"; do
if ! rpm -q $rpm >/dev/null; then
MISSING+=($rpm)
fi
done
echo "${#MISSING[@]}"
}'
#############
# Default
@default:
{{ just }} --list
# Create Directories
init-work:
@echo "{{ style('command') }}Creating Work Directories...{{ NORMAL }}" >&2
mkdir -p {{ workdir }}
mkdir -p {{ isoroot }}
mkdir -p {{ rootfs }}
# Extract rootfs
rootfs image=default_image:
#!/usr/bin/env bash
{{ _ci_grouping }}
set -xeuo pipefail
# Pull and Extract Filesystem
{{ PODMAN }} pull {{ image }} # Pull newer image
ctr="$({{ PODMAN }} create --rm {{ image }} /usr/bin/bash)" && trap "{{ PODMAN }} rm $ctr" EXIT
{{ PODMAN }} export $ctr | tar --xattrs-include='*' -p -xf - -C {{ rootfs }}
# Make /var/tmp be a tmpfs by symlinking to /tmp,
# in order to make bootc work at runtime.
rm -rf {{ rootfs }}/var/tmp
ln -sr {{ rootfs }}/tmp {{ rootfs }}/var/tmp
# Generate initramfs with live modules
initramfs:
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ chroot_function }}
set -euo pipefail
CMD='set -xeuo pipefail
dnf install -y dracut-live
INSTALLED_KERNEL=$(rpm -q kernel-core --queryformat "%{evr}.%{arch}" | tail -n 1)
mkdir -p $(realpath /root)
export DRACUT_NO_XATTR=1
dracut --zstd --reproducible --no-hostonly --kver "$INSTALLED_KERNEL" --add "dmsquash-live dmsquash-live-autooverlay" --force /app/{{ workdir }}/initramfs.img |& grep -v -e "Operation not supported"'
chroot "$CMD"
# Embed the container
rootfs-include-container container_image=default_image image=default_image:
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ chroot_function }}
set -euo pipefail
CMD="set -xeuo pipefail
mkdir -p /var/lib/containers/storage
podman pull {{ container_image || image }}
dnf install -y fuse-overlayfs"
chroot "$CMD"
# Install Flatpaks into the live system
rootfs-include-flatpaks FLATPAKS_FILE="src/flatpaks.example.txt":
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ if FLATPAKS_FILE =~ '(^$|^(?i)\bnone\b$)' { 'exit 0' } else if path_exists(FLATPAKS_FILE) == 'false' { error('Flatpak file inaccessible: ' + FLATPAKS_FILE) } else { '' } }}
{{ chroot_function }}
CMD='set -xeuo pipefail
mkdir -p /var/lib/flatpak
dnf install -y flatpak
# Get Flatpaks
flatpak remote-add --if-not-exists flathub "https://dl.flathub.org/repo/flathub.flatpakrepo"
grep -v "#.*" /flatpak-list/$(basename {{ FLATPAKS_FILE }}) | sort --reverse | xargs "-i{}" -d "\n" sh -c "flatpak remote-info --arch={{ arch }} --system flathub {} &>/dev/null && flatpak install --noninteractive -y {}" || true'
set -euo pipefail
chroot "$CMD" --volume "$(realpath "$(dirname {{ FLATPAKS_FILE }})")":/flatpak-list
# Install polkit rules
rootfs-include-polkit polkit="1":
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ if polkit == "0" { 'exit 0' } else { '' } }}
set -euo pipefail
install -D -m 0644 {{ git_root }}/src/polkit-1/rules.d/*.rules -t {{ rootfs }}/etc/polkit-1/rules.d
# Install Livesys Scripts
rootfs-install-livesys-scripts livesys="1":
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ if livesys == "0" { 'exit 0' } else { '' } }}
{{ chroot_function }}
set -euo pipefail
CMD='set -xeuo pipefail
dnf="$({ which dnf5 || which dnf; } 2>/dev/null)"
$dnf install -y livesys-scripts
# Determine desktop environment. Must match one of /usr/libexec/livesys/sessions.d/livesys-{desktop_env}
desktop_env=""
_session_file="$(find /usr/share/wayland-sessions/ /usr/share/xsessions \
-maxdepth 1 -type f -not -name '*gamescope*.desktop' -and -name '*.desktop' -printf '%P' -quit)"
case $_session_file in
budgie*) desktop_env=budgie ;;
cosmic*) desktop_env=cosmic ;;
gnome*) desktop_env=gnome ;;
plasma*) desktop_env=kde ;;
sway*) desktop_env=sway ;;
xfce*) desktop_env=xfce ;;
*) echo "\
{{ style('error') }}ERROR[rootfs-install-livesys-scripts]{{ NORMAL }}\
: No Livesys Environment Found"; exit 1 ;;
esac && unset -v _session_file
sed -i "s/^livesys_session=.*/livesys_session=${desktop_env}/" /etc/sysconfig/livesys
# Enable services
systemctl enable livesys.service livesys-late.service
# Set default time zone to prevent oddities with KDE clock
echo "C /var/lib/livesys/livesys-session-extra 0755 root root - /usr/share/factory/var/lib/livesys/livesys-session-extra" > \
/usr/lib/tmpfiles.d/livesys-session-extra.conf'
chroot "$CMD"
install -D -m 0644 {{ git_root }}/src/livesys-session-extra {{ rootfs }}/usr/share/factory/var/lib/livesys/livesys-session-extra
# Hook used for custom operations done in the rootfs before it is squashed.
# Meant to be used in a GH action.
hook-post-rootfs hook=HOOK_post_rootfs:
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ if hook == '' { 'exit 0' } else { '' } }}
{{ chroot_function }}
set -euo pipefail
chroot "$(cat '{{ hook }}')"
# Hook used for custom operations done before the initramfs is generated.
# Meant to be used in a GH action.
hook-pre-initramfs hook=HOOK_pre_initramfs:
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ if hook == '' { 'exit 0' } else { '' } }}
{{ chroot_function }}
set -euo pipefail
chroot "$(cat '{{ hook }}')"
# Remove the sysroot tree
rootfs-clean-sysroot:
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ chroot_function }}
set -euo pipefail
CMD='set -xeuo pipefail
if [[ -d /app ]]; then
rm -rf /sysroot /ostree
dnf autoremove -y
dnf clean all -y
fi'
chroot "$CMD"
# Fix SELinux Permissions
rootfs-selinux-fix image=default_image:
#!/usr/bin/env bash
{{ _ci_grouping }}
set -euo pipefail
CMD='set -xeuo pipefail
cd /app/{{ rootfs }}
setfiles -F -r . /etc/selinux/targeted/contexts/files/file_contexts .
chcon --user=system_u --recursive .'
{{ PODMAN }} run --rm -it \
--volume {{ git_root }}:/app \
--workdir "/app" \
--security-opt label=disable \
--privileged \
{{ image }} \
/usr/bin/bash -c "$CMD"
rmdir {{ rootfs }}/app || true
# Compress rootfs into a compressed image
squash fs_type="squashfs":
#!/usr/bin/env bash
{{ _ci_grouping }}
CMD='{{ if fs_type == "squashfs" { "mksquashfs $0 $1/squashfs.img -all-root -noappend" } else if fs_type == "erofs" { "mkfs.erofs -d0 --quiet --all-root -zlz4hc,6 -Eall-fragments,fragdedupe=inode -C1048576 $1/squashfs.img $0" } else { error(style('error') + "ERROR[squash]" + NORMAL + ": Invalid Compression") } }}'
{{ compress_dependencies }}
{{ builder_function }}
set -euo pipefail
BUILDER="$(compress_dependencies)"
if ! (( BUILDER )); then
bash -c "$CMD" "$(realpath {{ rootfs }})" "$(realpath {{ workdir }})"
else
CMD="dnf install -y {{ if fs_type == 'squashfs' { 'squashfs-tools' } else if fs_type == 'erofs' { 'erofs-utils' } else { '' } }} ; $CMD"
builder "$CMD" "/app/{{ rootfs }}" "/app/{{ workdir }}"
fi
# Expand grub templace, according to the image os-release.
process-grub-template $extra_kargs="NONE":
#!/usr/bin/env bash
{{ _ci_grouping }}
set -xeuo pipefail
kargs=()
IFS=',' read -r -a kargs <<< "$extra_kargs"
if [[ "$extra_kargs" == "NONE" ]]; then
kargs=()
fi
OS_RELEASE="{{ rootfs }}/usr/lib/os-release"
TMPL="src/grub.cfg.tmpl"
DEST="{{ isoroot }}/boot/grub/grub.cfg"
# TODO figure out a better mechanism
PRETTY_NAME="$(source "$OS_RELEASE" >/dev/null && echo "${PRETTY_NAME/ (*)}")"
sed \
-e "s|@PRETTY_NAME@|${PRETTY_NAME}|g" \
-e "s|@EXTRA_KARGS@|${kargs[*]}|g" \
"$TMPL" >"$DEST"
# Prep the environment for the ISO
iso-organize extra_kargs: && (process-grub-template extra_kargs)
#!/usr/bin/env bash
{{ _ci_grouping }}
set -xeuo pipefail
mkdir -p {{ isoroot }}/boot/grub {{ isoroot }}/LiveOS
cp {{ rootfs }}/lib/modules/*/vmlinuz {{ isoroot }}/boot
cp {{ workdir }}/initramfs.img {{ isoroot }}/boot
# Hardcoded on the dmsquash-live source code unless specified otherwise via kargs
# https://github.com/dracut-ng/dracut-ng/blob/0ffc61e536d1193cb837917d6a283dd6094cb06d/modules.d/90dmsquash-live/dmsquash-live-root.sh#L23
{{ if env('CI', '') == '' { 'cp' } else { 'mv' } }} {{ workdir }}/squashfs.img {{ isoroot }}/LiveOS/squashfs.img
# Build the ISO from the compressed image
iso:
#!/usr/bin/env bash
{{ _ci_grouping }}
{{ if env('CI', '') != '' { "echo '" + style('warning') + "In CI - Deleting: " + rootfs + "...' " + NORMAL +"; rm -rf " + rootfs } else { '' } }}
{{ iso_dependencies }}
BUILDER="$(iso_dependencies)"
CMD='set -xeuo pipefail
ISOROOT="$0"
WORKDIR="$1"
mkdir -p $ISOROOT/EFI/BOOT
# ARCH_SHORT needs to be uppercase
ARCH_SHORT="$(echo {{ arch }} | sed 's/x86_64/x64/g' | sed 's/aarch64/aa64/g')"
ARCH_32="$(echo {{ arch }} | sed 's/x86_64/ia32/g' | sed 's/aarch64/arm/g')"
if [[ "$(rpm -E %centos)" -ge 10 ]]; then
cp -avf /boot/efi/EFI/centos/. $ISOROOT/EFI/BOOT
elif [[ "$(rpm -E %fedora)" -ge 41 ]]; then
cp -avf /boot/efi/EFI/fedora/. $ISOROOT/EFI/BOOT
fi
cp -avf $ISOROOT/boot/grub/grub.cfg $ISOROOT/EFI/BOOT/BOOT.conf
cp -avf $ISOROOT/boot/grub/grub.cfg $ISOROOT/EFI/BOOT/grub.cfg
cp -avf /boot/grub*/fonts/unicode.pf2 $ISOROOT/EFI/BOOT/fonts
cp -avf $ISOROOT/EFI/BOOT/shim${ARCH_SHORT}.efi "$ISOROOT/EFI/BOOT/BOOT${ARCH_SHORT^^}.efi"
cp -avf $ISOROOT/EFI/BOOT/shim.efi "$ISOROOT/EFI/BOOT/BOOT${ARCH_32}.efi"
ARCH_GRUB="$(echo {{ arch }} | sed 's/x86_64/i386-pc/g' | sed 's/aarch64/arm64-efi/g')"
ARCH_OUT="$(echo {{ arch }} | sed 's/x86_64/i386-pc-eltorito/g' | sed 's/aarch64/arm64-efi/g')"
ARCH_MODULES="$(echo {{ arch }} | sed 's/x86_64/biosdisk/g' | sed 's/aarch64/efi_gop/g')"
grub2-mkimage -O $ARCH_OUT -d /usr/lib/grub/$ARCH_GRUB -o $ISOROOT/boot/eltorito.img -p /boot/grub iso9660 $ARCH_MODULES
grub2-mkrescue -o $ISOROOT/../efiboot.img
EFI_BOOT_MOUNT=$(mktemp -d)
mount $ISOROOT/../efiboot.img $EFI_BOOT_MOUNT
cp -r $EFI_BOOT_MOUNT/boot/grub $ISOROOT/boot/
umount $EFI_BOOT_MOUNT
rm -rf $EFI_BOOT_MOUNT
# https://github.com/FyraLabs/katsu/blob/1e26ecf74164c90bc24299a66f8495eb2aef4845/src/builder.rs#L145
EFI_BOOT_PART=$(mktemp -d)
fallocate $WORKDIR/efiboot.img -l 25M
mkfs.msdos -v -n EFI $WORKDIR/efiboot.img
mount $WORKDIR/efiboot.img $EFI_BOOT_PART
mkdir -p $EFI_BOOT_PART/EFI/BOOT
cp -dRvf $ISOROOT/EFI/BOOT/. $EFI_BOOT_PART/EFI/BOOT
umount $EFI_BOOT_PART
ARCH_SPECIFIC=()
if [ "{{ arch }}" == "x86_64" ] ; then
ARCH_SPECIFIC=("--grub2-mbr" "/usr/lib/grub/i386-pc/boot_hybrid.img")
fi
xorrisofs \
-R \
-V titanoboa_boot \
-partition_offset 16 \
-appended_part_as_gpt \
-append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B \
$ISOROOT/../efiboot.img \
-iso_mbr_part_type EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 \
-c boot.cat --boot-catalog-hide \
-b boot/eltorito.img \
-no-emul-boot \
-boot-load-size 4 \
-boot-info-table \
--grub2-boot-info \
-eltorito-alt-boot \
-e \
--interval:appended_partition_2:all:: \
-no-emul-boot \
-vvvvv \
-iso-level 3 \
-o /app/output.iso \
"${ARCH_SPECIFIC[@]}" \
$ISOROOT'
set -euo pipefail
if ! (( BUILDER )); then
bash -c "$CMD" "$(realpath {{ isoroot }})" "$(realpath {{ workdir }})"
else
{{ if `systemd-detect-virt -c || true` != 'none' { "echo '" + style('error') + "ERROR[iso]" + NORMAL + ": Cannot run in nested containers'; exit 1" } else { '' } }}
{{ builder_function }}
CMD="dnf install -y grub2 grub2-efi grub2-tools grub2-tools-extra xorriso shim dosfstools {{ if arch == "x86_64" { 'grub2-efi-x64-modules grub2-efi-x64-cdboot grub2-efi-x64' } else if arch == "aarch64" { 'grub2-efi-aa64-modules' } else { '' } }}; $CMD"
builder "$CMD" "/app/{{ isoroot }}" "/app/{{ workdir }}"
fi
# TODO update this recipe parameters. Make it actually usable
[no-exit-message]
[doc('Build a live-iso')]
@build image=default_image livesys="1" flatpaks_file="src/flatpaks.example.txt" compression="squashfs" extra_kargs="NONE" container_image=image polkit="1": \
checkroot \
(show-config image livesys flatpaks_file compression extra_kargs container_image polkit) \
clean \
init-work \
(rootfs image) \
(hook-pre-initramfs HOOK_pre_initramfs) \
initramfs \
(rootfs-include-flatpaks flatpaks_file) \
(rootfs-include-polkit polkit) \
(rootfs-install-livesys-scripts livesys) \
(rootfs-include-container container_image image) \
(hook-post-rootfs HOOK_post_rootfs) \
rootfs-clean-sysroot \
(rootfs-selinux-fix image) \
(ci-delete-image image) \
(squash compression) \
(iso-organize extra_kargs) \
iso
mv ./output.iso {{ justfile_dir() }} &>/dev/null
@show-config image livesys flatpaks_file compression extra_kargs container_image polkit:
echo "Using the following configuration:"
echo "{{ style('warning') }}################################################################################{{ NORMAL }}"
echo "PODMAN := {{ PODMAN }}"
echo "workdir := {{ workdir }}"
echo "isoroot := {{ isoroot }}"
echo "rootfs := {{ rootfs }}"
echo "builder_distro := {{ builder_distro }}"
echo "builder_image := {{ builder_image }}"
echo "HOOK_post_rootfs := {{ if HOOK_post_rootfs =~ '(^$|^(?i)\bnone\b$)' { '' } else { canonicalize(HOOK_post_rootfs) } }}"
echo "HOOK_pre_initramfs := {{ if HOOK_pre_initramfs =~ '(^$|^(?i)\bnone\b$)' { '' } else { canonicalize(HOOK_pre_initramfs) } }}"
echo "image := {{ image }}"
echo "livesys := {{ livesys }}"
echo "flatpaks_file := {{ if flatpaks_file =~ '(^$|^(?i)\bnone\b$)' { '' } else { canonicalize(flatpaks_file) } }}"
echo "compression := {{ compression }}"
echo "extra_kargs := {{ extra_kargs }}"
echo "container_image := {{ container_image || image }}"
echo "polkit := {{ polkit }}"
echo "CI := {{ env('CI', '') }}"
echo "ARCH := {{ arch }}"
echo "{{ style('warning') }}################################################################################{{ NORMAL }}"
sleep 1
[no-exit-message]
@checkroot:
if [ `id -u` -gt 0 ]; then echo '{{ style("error") }}ERROR[build]{{ NORMAL }}: Must be root to build ISO' >&2 && exit 1; fi
@clean:
echo "{{ style('command') }}cleaning {{ absolute_path(workdir) }}...{{ NORMAL }}" >&2
rm -rf {{ absolute_path(workdir) }}
[private]
delete-image image:
#!/usr/bin/env bash
set -xeuo pipefail
{{ PODMAN }} rmi --force "{{ image }}" || :
[private]
ci-delete-image image:
#!/usr/bin/env bash
set -xeuo pipefail
if [[ -n "${CI:-}" ]]; then
{{ PODMAN }} rmi --force {{ image }} || :
fi
# Run VM with qemu
vm ISO_FILE *ARGS:
#!/usr/bin/env bash
qemu="qemu-system-{{ arch }}"
if [[ ! $(type -P "$qemu") ]]; then
qemu="flatpak run --command=$qemu org.virt_manager.virt-manager"
fi
$qemu \
-enable-kvm \
-M q35 \
-cpu host \
-smp $(( $(nproc) / 2 > 0 ? $(nproc) / 2 : 1 )) \
-m 4G \
-net nic,model=virtio \
-net user,hostfwd=tcp::2222-:22 \
-display gtk,show-cursor=on \
-boot d \
-cdrom {{ ISO_FILE }} {{ ARGS }}
# Run VM with a container and web vnc
container-run-vm ISO_FILE:
#!/usr/bin/env bash
set -xeuo pipefail
# Determine an available port to use
port=8006
while grep -q :${port} <<< $(ss -tunalp); do
port=$(( port + 1 ))
done
echo "Using Port: ${port}"
echo "Connect to http://localhost:${port}"
# Ram Size
mem_free=$(awk '/MemAvailable/ { printf "%.0f\n", $2/1024/1024 - 1 }' /proc/meminfo)
ram_size=$(( mem_free > 64 ? mem_free / 2 : (mem_free > 8 ? 8 : (mem_free < 3 ? 3 : mem_free)) ))
# Set up the arguments for running the VM
run_args=()
run_args+=(--rm --privileged)
run_args+=(--pull=newer)
run_args+=(--publish "127.0.0.1:${port}:8006")
run_args+=(--env "CPU_CORES=$(( $(nproc) / 2 > 0 ? $(nproc) / 2 : 1 ))")
run_args+=(--env "RAM_SIZE=${ram_size}G")
run_args+=(--env "DISK_SIZE=64G")
run_args+=(--env "TPM=Y")
run_args+=(--env "GPU=Y")
run_args+=(--env "BOOT_MODE=windows_secure")
run_args+=(--device=/dev/kvm)
run_args+=(--volume "{{ canonicalize(ISO_FILE) }}":"/boot.iso")
run_args+=(ghcr.io/qemus/qemu)
# Run the VM and open the browser to connect
{{ PODMAN }} run "${run_args[@]}" &
xdg-open http://localhost:${port}
# Print the absolute of the files relative to the project dir.
[private]
whereis +FILE_PATHS:
@realpath -e {{ FILE_PATHS }}

420
README.md
View file

@ -1,6 +1,6 @@
# Titanoboa (Beta)
# Titanoboa
A [bootc](https://github.com/bootc-dev/bootc) installer designed to install an image as quickly as possible. Handles a live user session and then hands off to Anaconda or Readymade for installation.
A [bootc](https://github.com/bootc-dev/bootc) installer designed to install an image as quickly as possible. This project enables the creation of bootable ISOs directly from bootc container images, where all customizations are embedded within the container image itself.
## Mission
@ -8,41 +8,78 @@ This is an experiment to see how far we can get building our own ISOs. The objec
- Generate a LiveCD so users can try out an image before committing
- Install the image and flatpaks to a selected disk with minimal user-input
- Basically be an MVP for `bootc install`
- Basically be an MVP for `bootc install`
## Why?
Waiting for existing installers to move to cloud native is untenable, let's see if we can remove that external dependency forever. 😈
## Components
- LiveCD
---
## End-User Documentation
This guide explains how to consume Titanoboa to create a live ISO image of your custom bootc container image. The [@ublue-os/bluefin](https://github.com/ublue-os/bluefin) repository is the canonical example of this integration.
This guide explains how to consume Titanoboa to create a live ISO image of your custom bootc container image.
### Table of Contents
- [Prerequisites](#prerequisites)
- [Container Image Specification](#container-image-specification)
- [GitHub Actions Integration](#github-actions-integration)
- [Action Inputs Reference](#action-inputs-reference)
- [Configuration Files](#configuration-files)
- [Local Usage with Just](#local-usage-with-just)
- [Environment Variables](#environment-variables)
- [Complete Example Workflow](#complete-example-workflow)
- [Local ISO Generation](#local-iso-generation)
- [Testing Your ISO](#testing-your-iso)
### Prerequisites
Before using Titanoboa, ensure you have:
1. **A bootc-compatible container image** hosted in a container registry (e.g., GitHub Container Registry, Docker Hub, Quay.io)
2. **GitHub Actions** (for automated builds) or **Just** + **Podman** (for local builds)
3. **Root/sudo access** when building locally
4. **Sufficient disk space** (at least 20-30 GB free for building ISOs)
1. **A bootc-compatible container image** hosted in a container registry (e.g., GitHub Container Registry, Docker Hub, Quay.io) or locally stored in podman's container storage. This image _must_ adhere to the [Container Image Specification](#container-image-specification).
2. **GitHub Actions** (for automated builds).
### Container Image Specification
Your `bootc` container image must adhere to a specific contract for Titanoboa to successfully build an ISO. All customizations for the ISO (bootloader entries, kernel arguments, Flatpaks, etc.) are expected to be embedded within your container image.
This contract is also known as the [Container-native ISO contract v0.1.0 spec](https://github.com/ondrejbudai/bootc-isos/blob/3b3a185e4a57947f57baf53d2be5aee469274f98/README.md#container-native-iso-contract-v010).
#### `iso.yaml`
Your container image must contain a file at `/usr/lib/bootc-image-builder/iso.yaml` in YAML format. This file configures the ISO's label and GRUB2 boot entries.
The `iso.yaml` file supports the following top-level keys:
- `label` (string): The label for the ISO.
- `grub2` (object): GRUB2 configuration, supporting these keys:
- `default` (integer): The default menu item (0-indexed).
- `timeout` (integer): Default timeout in seconds.
- `entries` (array of objects): GRUB2 menu entries. Each entry must have:
- `name` (string): Name of the entry.
- `linux` (string): Path to the kernel + kernel arguments (e.g., `/images/pxeboot/vmlinuz quiet rhgb`).
- `initrd` (string): Path to the initramfs (e.g., `/images/pxeboot/initrd.img`).
**Example `iso.yaml` (inside your container image):**
```yaml
label: MyCustomImage-ISO
grub2:
default: 0
timeout: 10
entries:
- name: "My Custom Image Live"
linux: "/images/pxeboot/vmlinuz quiet rhgb root=live:CDLABEL=MyCustomImage-ISO enforcing=0 rd.live.image"
initrd: "/images/pxeboot/initrd.img"
- name: "My Custom Image Live (Basic Graphics)"
linux: "/images/pxeboot/vmlinuz quiet rhgb root=live:CDLABEL=MyCustomImage-ISO enforcing=0 rd.live.image nomodeset"
initrd: "/images/pxeboot/initrd.img"
```
#### Required Files
In addition to `iso.yaml`, your container image is expected to contain:
- **Kernel**: In `/usr/lib/modules/*/vmlinuz`
- **Initramfs**: Next to the kernel, named `initramfs.img`
- **UEFI EFI binaries**: In `/boot/efi/EFI/$VENDOR` (e.g., `shimx64.efi`, `mmx64.efi`, `gcdx64.efi`)
- **GRUB2 modules**: In `/usr/lib/grub/i386-pc`
### GitHub Actions Integration
@ -59,9 +96,9 @@ Add Titanoboa as a step in your GitHub Actions workflow:
image-ref: ghcr.io/your-org/your-image:latest
```
#### Real-World Example from Bluefin
#### Real-World Example (from `ublue-os/bluefin`)
This example shows how [@ublue-os/bluefin](https://github.com/ublue-os/bluefin) consumes Titanoboa:
This example shows how an image like `ublue-os/bluefin` consumes Titanoboa, assuming the `bluefin-dx:gts` image already contains its full ISO configuration.
```yaml
- name: Build ISO
@ -69,341 +106,66 @@ This example shows how [@ublue-os/bluefin](https://github.com/ublue-os/bluefin)
uses: ublue-os/titanoboa@main
with:
image-ref: ghcr.io/ublue-os/bluefin-dx:gts
flatpaks-list: ${{ github.workspace }}/flatpaks/system-flatpaks.list
hook-post-rootfs: ${{ github.workspace }}/iso_files/configure_iso_anaconda.sh
kargs: ""
builder-distro: fedora
- name: Rename and Checksum ISO
run: |
mkdir -p output
mv ${{ steps.build.outputs.iso-dest }} output/my-custom-image.iso
(cd output && sha256sum my-custom-image.iso | tee my-custom-image.iso-CHECKSUM)
- name: Upload ISO
uses: actions/upload-artifact@v4
with:
name: custom-iso
path: output/
```
### Action Inputs Reference
### Local ISO Generation
All inputs for the Titanoboa action:
For local development and testing, you can build ISOs directly from a local container image that adheres to the [Container Image Specification](#container-image-specification).
| Input | Required | Default | Description |
|-------|----------|---------|-------------|
| `image-ref` | **Yes** | - | Reference to the bootc container image (e.g., `ghcr.io/ublue-os/bluefin:lts`) |
| `livesys` | No | `true` | Install livesys helpers on the rootfs for live environment features |
| `compression` | No | `squashfs` | Compression type: `squashfs` (smaller, slower) or `erofs` (faster) |
| `hook-post-rootfs` | No | `""` | Path to a script to run in the rootfs before it's squashed (see [Hook Scripts](#hook-scripts)) |
| `hook-pre-initramfs` | No | `""` | Path to a script to run before building the initramfs (e.g., to swap kernels) |
| `iso-dest` | No | `${{ github.workspace }}/output.iso` | Where the ISO will be placed |
| `flatpaks-list` | No | `none` | Path to a newline-separated list of Flatpak apps to install in the ISO |
| `container-image` | No | `""` | Container image to install on target system (can differ from the ISO rootfs image) |
| `add-polkit` | No | `true` | Add default polkit rules for the container |
| `kargs` | No | `""` | Comma-separated kernel arguments for the live ISO |
| `builder-distro` | No | `fedora` | Builder distribution: `fedora`, `centos`, or `almalinux` |
#### Action Outputs
| Output | Description |
|--------|-------------|
| `iso-dest` | Absolute path where the ISO was placed |
### Configuration Files
#### Flatpaks List
A text file containing Flatpak application IDs to be preinstalled in your ISO, one per line. Comments are supported with `#`.
**Example** (`flatpaks/system-flatpaks.list`):
```text
# Web browsers
app/org.mozilla.firefox
app/org.mozilla.Thunderbird
# GNOME apps
app/org.gnome.Calculator
app/org.gnome.Calendar
app/org.gnome.TextEditor
app/org.gnome.Loupe
# Themes
runtime/org.gtk.Gtk3theme.adw-gtk3
runtime/org.gtk.Gtk3theme.adw-gtk3-dark
# Development tools
app/com.github.tchx84.Flatseal
app/io.github.flattool.Warehouse
```
**Usage:**
```yaml
with:
flatpaks-list: ${{ github.workspace }}/flatpaks/system-flatpaks.list
```
#### Hook Scripts
Hook scripts allow you to customize the ISO build process at different stages.
##### `hook-post-rootfs`
A bash script that runs **inside the rootfs** before it's compressed. Use this to:
- Install additional packages
- Configure Anaconda
- Customize the live environment
- Set up default desktop settings
- Disable unnecessary services
**Example** (`iso_files/configure_iso.sh`):
```bash
#!/usr/bin/env bash
set -eoux pipefail
# Remove packages to save space
dnf remove -y google-noto-fonts-all ublue-brew || true
# Configure GNOME defaults for live session
tee /usr/share/glib-2.0/schemas/zz2-org.gnome.shell.gschema.override <<EOF
[org.gnome.shell]
favorite-apps = ['anaconda.desktop', 'org.mozilla.firefox.desktop', 'org.gnome.Nautilus.desktop']
EOF
glib-compile-schemas /usr/share/glib-2.0/schemas
# Disable services not needed in live environment
systemctl disable rpm-ostree-countme.service
systemctl disable tailscaled.service
systemctl disable bootloader-update.service
# Install Anaconda for installation
dnf install -y anaconda-live libblockdev-btrfs
```
**Usage:**
```yaml
with:
hook-post-rootfs: ${{ github.workspace }}/iso_files/configure_iso.sh
```
##### `hook-pre-initramfs`
A bash script that runs **before generating the initramfs**. Use this to:
- Replace the kernel
- Add custom kernel modules
- Modify dracut configuration
**Example:**
```bash
#!/usr/bin/env bash
set -eoux pipefail
# Install a different kernel
dnf swap -y kernel kernel-lts
```
**Usage:**
```yaml
with:
hook-pre-initramfs: ${{ github.workspace }}/iso_files/pre_initramfs.sh
```
### Local Usage with Just
For local development and testing, you can build ISOs using the `just` command directly.
#### Basic Build Command
First, build your container image:
```bash
sudo just build ghcr.io/ublue-os/bluefin:lts
sudo podman build --cap-add sys_admin --security-opt label=disable --squash -t your-local-image-name .
```
This creates `output.iso` in the current directory.
#### Advanced Build Options
The `just build` command accepts multiple parameters:
Then, run the `main.sh` script, providing your locally built image:
```bash
just build <image> [livesys] [flatpaks_file] [compression] [extra_kargs] [container_image] [polkit]
sudo TITANOBOA_CTR_IMAGE="your-local-image-name" ./main.sh
```
**Parameters:**
- `image`: Container image reference (required)
- `livesys`: Enable livesys scripts (`1` or `0`, default: `1`)
- `flatpaks_file`: Path to flatpaks list (default: `src/flatpaks.example.txt`)
- `compression`: Compression type (`squashfs` or `erofs`, default: `squashfs`)
- `extra_kargs`: Comma-separated kernel arguments (default: `NONE`)
- `container_image`: Container to install vs. the ISO rootfs image (default: same as `image`)
- `polkit`: Add polkit rules (`1` or `0`, default: `1`)
**Examples:**
You can also use the titanoboa container directly to build your ISOs:
```bash
# Build with custom flatpaks list
sudo just build ghcr.io/ublue-os/bluefin:lts 1 ./my-flatpaks.txt
# Build with erofs compression
sudo just build ghcr.io/ublue-os/bluefin:lts 1 src/flatpaks.example.txt erofs
# Build without livesys scripts
sudo just build ghcr.io/ublue-os/bluefin:lts 0
# Build with custom kernel arguments
sudo just build ghcr.io/ublue-os/bluefin:lts 1 src/flatpaks.example.txt squashfs "rd.live.overlay.size=8192"
sudo podman run --rm -it \
--security-opt label=disable \
-v ./output:/output \
-v /var/lib/containers/storage:/usr/lib/containers/storage:ro \
--mount type=image,source="your-local-image-name",dst=/rootfs \
ghcr.io/ublue-os/titanoboa:latest
```
### Environment Variables
Titanoboa can be customized using environment variables:
| Variable | Default | Description |
|----------|---------|-------------|
| `TITANOBOA_WORKDIR` | `work` | Working directory for build artifacts |
| `TITANOBOA_ISO_ROOT` | `work/iso-root` | ISO root directory |
| `TITANOBOA_BUILDER_DISTRO` | `fedora` | Builder container distribution (`fedora`, `centos`, `almalinux`) |
| `HOOK_post_rootfs` | `""` | Path to post-rootfs hook script |
| `HOOK_pre_initramfs` | `""` | Path to pre-initramfs hook script |
**Example:**
```bash
# Use CentOS Stream 10 builder
TITANOBOA_BUILDER_DISTRO=centos sudo just build ghcr.io/ublue-os/bluefin:lts
# Use custom working directory
TITANOBOA_WORKDIR=/mnt/scratch sudo just build ghcr.io/your-org/your-image:latest
# Use hook scripts
HOOK_post_rootfs=/path/to/script.sh sudo just build ghcr.io/your-org/your-image:latest
```
### Complete Example Workflow
Here's a complete GitHub Actions workflow that builds ISOs for your custom image:
```yaml
name: Build Custom ISO
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
jobs:
build-iso:
name: Build ISO
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Maximize build space
uses: ublue-os/remove-unwanted-software@v9
- name: Checkout repository
uses: actions/checkout@v4
- name: Build ISO
id: build
uses: ublue-os/titanoboa@main
with:
image-ref: ghcr.io/your-org/your-custom-image:latest
flatpaks-list: ${{ github.workspace }}/config/flatpaks.list
hook-post-rootfs: ${{ github.workspace }}/config/configure-iso.sh
compression: squashfs
builder-distro: fedora
- name: Rename ISO
run: |
mkdir -p output
mv ${{ steps.build.outputs.iso-dest }} output/custom-image-$(date +%Y%m%d).iso
(cd output && sha256sum *.iso | tee CHECKSUMS)
- name: Upload ISO
uses: actions/upload-artifact@v4
with:
name: custom-iso
path: output/
```
The generated ISO will be placed in the `output/` directory by default.
### Testing Your ISO
After building your ISO, test it using the built-in VM command:
After building your ISO via the GitHub Action, you can download the artifact and test it in a virtual machine locally. For local testing during development, you can adapt the `main.sh` script to build the ISO from a local bootc image.
Example of how to run the VM locally (if you have QEMU installed):
```bash
# Test in a VM
just vm ./output.iso
# Or use the container-based VM with web VNC
just container-run-vm ./output.iso
# Example: Assuming you downloaded your ISO to ./output/my-custom-image.iso
qemu-system-x86_64 \
-enable-kvm \
-M q35 \
-cpu host \
-smp 2 \
-m 4G \
-net nic,model=virtio \
-net user,hostfwd=tcp::2222-:22 \
-display gtk,show-cursor=on \
-boot d \
-cdrom ./output/my-custom-image.iso
```
The `just vm` command launches a QEMU virtual machine with:
- TPM 2.0 support
- UEFI Secure Boot
- Automatic memory allocation
- GPU passthrough (when available)
The `container-run-vm` command:
- Runs QEMU in a container
- Provides web-based VNC access (auto-opens in browser)
- Useful for headless systems or CI environments
### Builder Distribution Support
By default, Titanoboa uses Fedora containers for building tools and dependencies. You can specify different builder distributions:
- **fedora** (default): Uses `quay.io/fedora/fedora:latest` - Best for Fedora-based images
- **centos**: Uses `ghcr.io/hanthor/centos-anaconda-builder:main` - For CentOS Stream 10 / LTS images
- **almalinux**: Uses `quay.io/almalinux/almalinux:10` - For AlmaLinux-based images
**When to use different builders:**
- Use `centos` when building LTS images based on CentOS Stream
- Use `fedora` for standard Fedora-based images (default)
- Use `almalinux` for AlmaLinux-based custom images
**Examples:**
```bash
# GitHub Actions
with:
builder-distro: centos
# Local with just
TITANOBOA_BUILDER_DISTRO=centos sudo just build ghcr.io/ublue-os/bluefin:lts
```
### Tips and Best Practices
1. **Minimize ISO size**: Remove unnecessary packages in your `hook-post-rootfs` script to keep the ISO smaller
2. **Test incrementally**: Build and test ISOs frequently during development
3. **Use proper compression**: Use `squashfs` for smaller ISOs (better for distribution), `erofs` for faster boot times
4. **Flatpak selection**: Only include essential Flatpaks in your ISO; users can install more after installation
5. **Builder matching**: Use the builder distribution that matches your base image for best compatibility
6. **Clean builds**: Run `just clean` between builds to ensure a fresh start
7. **Hook scripts**: Make your hook scripts idempotent and include error handling
8. **Version pinning**: Pin to specific Titanoboa releases in production workflows (e.g., `ublue-os/titanoboa@v1.0.0`)
### Troubleshooting
**Build fails with "out of space":**
- Free up disk space or use a larger volume
- Remove unnecessary files in your `hook-post-rootfs` script
- Consider using `erofs` compression instead of `squashfs`
**ISO doesn't boot:**
- Verify your base container image is bootc-compatible
- Check that the image exists and is accessible
- Ensure UEFI Secure Boot settings are correct for your hardware
**Flatpaks don't install:**
- Verify Flatpak IDs are correct (use `flatpak search <name>` to find IDs)
- Check network connectivity during build
- Review build logs for specific Flatpak installation errors
**Hook script fails:**
- Ensure scripts have executable permissions: `chmod +x script.sh`
- Use `set -eoux pipefail` at the start of scripts for better error reporting
- Test scripts in a container before using them in builds
## Contributor Metrics
![Alt](https://repobeats.axiom.co/api/embed/ab79f8a8b6ba6111cc7123cbbb8762864c76699f.svg "Repobeats analytics image")

View file

@ -4,49 +4,12 @@ description: >-
The resulting iso will be placed at `output.iso`.
inputs:
image-ref:
description: Reference to the bootc container image.
description: Reference to the bootc container image to build the ISO from.
required: true
livesys:
description: Install livesys helpers on the rootfs for the ISO
required: false
default: "true"
compression:
description: Which type of compression will the ISO will use (erofs (default), or squashfs (smaller sizes, slower read))
required: false
default: squashfs
hook-post-rootfs:
description: >-
Path to a script ran in the rootfs of the container image before
being squashed in `squashfs.img`.
default: ""
hook-pre-initramfs:
description: >-
Path to a script to be ran before building the initramfs (per example, to swap the kernel).
default: ""
iso-dest:
description: Where the iso will be placed
required: false
default: ${{ github.workspace }}/output.iso
flatpaks-list:
description: Path to a file with a newline separated flatpak apps list to be installed in the rootfs.
required: false
default: none
container-image:
description: Container image that will be installed onto the target system (can be different from rootfs)
required: false
default: ""
add-polkit:
description: Add default polkit rules for the container
required: false
default: "true"
kargs:
description: Kernel arguments to be supplied for the live ISO, comma separated
required: false
default: ""
builder-distro:
description: Distribution to use for the builder container (for tools and dependencies)
required: false
default: fedora
outputs:
iso-dest:
description: Where the iso was be placed
@ -55,73 +18,35 @@ outputs:
runs:
using: composite
steps:
- name: Install Just
id: install-just
uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
- name: Generate iso
id: generate-iso
env:
WORKSPACE: ${{ github.workspace }}
DESTINATION_PATH: ${{ inputs.iso-dest }}
COMPRESSION: ${{ inputs.compression }}
IMAGE_REF: ${{ inputs.image-ref }}
LIVESYS: ${{ inputs.livesys }}
ACTION_PATH: ${{ github.action_path }}
FLATPAKS_LIST: ${{ inputs.flatpaks-list }}
ADD_POLKIT: ${{ inputs.add-polkit }}
HOOK_POST_ROOTFS: ${{ inputs.hook-post-rootfs }}
HOOK_PRE_INITRAMFS: ${{ inputs.hook-pre-initramfs }}
CONTAINER_IMAGE: ${{ inputs.container-image }}
EXTRA_KARGS: ${{ inputs.kargs }}
TITANOBOA_BUILDER_DISTRO: ${{ inputs.builder-distro }}
TITANOBOA_CTR_IMAGE: ${{ inputs.image-ref }}
shell: bash
run: |
set -euxo pipefail
cd "${ACTION_PATH}"
cd "$GITHUB_ACTION_PATH" || exit 1
just=$(which just)
USE_LIVESYS=$(echo "${LIVESYS}" | sed -e 's/true/1/g' -e 's/false/0/g')
ADD_POLKIT=$(echo "${ADD_POLKIT}" | sed -e 's/true/1/g' -e 's/false/0/g')
sudo \
PATH="$PATH" \
CI="$CI" \
HOOK_post_rootfs="${HOOK_POST_ROOTFS}" \
HOOK_pre_initramfs="${HOOK_PRE_INITRAMFS}" \
TITANOBOA_BUILDER_DISTRO="${TITANOBOA_BUILDER_DISTRO}" \
$just build \
"${IMAGE_REF}" "${USE_LIVESYS}" "${FLATPAKS_LIST}" "${COMPRESSION}" "${EXTRA_KARGS}" "${CONTAINER_IMAGE}" "${ADD_POLKIT}"
# Build ISO
ISO_FILENAME="$(env TITANOBOA_CTR_IMAGE="$TITANOBOA_CTR_IMAGE" "$GITHUB_ACTION_PATH"/main.sh)"
# Fix iso file permisions
sudo chown $(id -u):$(id -g) ./output.iso
sudo chown $(id -u):$(id -g) "$ISO_FILENAME"
# Move iso to iso-dest
dest="$(realpath --relative-base="${WORKSPACE}" "${DESTINATION_PATH}")"
dest="$(realpath $dest)"
if [[ "$(realpath ./output.iso)" != "${dest}" ]]; then
mkdir -p "$(dirname "$dest")"
mv ./output.iso "$dest"
fi
dest="$(realpath --relative-base="${GITHUB_WORKSPACE}" "${DESTINATION_PATH}")"
dest="$(realpath -m $dest)"
mkdir -p "$(dirname "$dest")"
mv "$ISO_FILENAME" "$dest"
echo "iso_dest=$dest" >> "$GITHUB_OUTPUT"
- name: Cleanup
id: titanoboa-cleanup
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: |
cd "${ACTION_PATH}"
# Cleanup
sudo PATH="$PATH" $(which just) clean
- name: Check the iso is there
shell: bash
env:
DEST_ISO: ${{ steps.generate-iso.outputs.iso_dest }}
run: |
[[ -f "${DEST_ISO}" ]] || {
echo "::error:Iso file does not exist at $dest"
echo "::error:Iso file does not exist at ${DEST_ISO}"
exit 1
}

94
build_iso.sh Executable file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env -S bash -exo pipefail
{ export PS4='+( ${BASH_SOURCE}:${LINENO} ): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'; } 2>/dev/null
dnf install -y squashfs-tools xorriso yq mtools dosfstools
mkdir -p \
/work \
/work/iso-root \
/work/iso-root/boot/grub2 \
/work/iso-root/images/pxeboot \
/work/iso-root/LiveOS
cd /work || exit 1
# Create the squashfs image of the container image
mksquashfs /rootfs /work/iso-root/LiveOS/squashfs.img -all-root -noappend -e sysroot -e ostree -comp zstd -Xcompression-level 19
iso_config_file=/rootfs/usr/lib/bootc-image-builder/iso.yaml
if [[ ! -f $iso_config_file ]]; then
echo >&2 "ERROR: Missing /usr/lib/bootc-image-builder/iso.yaml file"
exit 1
fi
iso_label=$(yq '.label' <$iso_config_file)
# Copy initrd and kernel
cp -av /rootfs/usr/lib/modules/*/initramfs.img /work/iso-root/images/pxeboot/initrd.img
cp -av /rootfs/usr/lib/modules/*/vmlinuz /work/iso-root/images/pxeboot/vmlinuz
# Copy GRUB modules
for grub_arch in i386-pc arm64-efi; do
[ -f "/rootfs/usr/lib/grub/$grub_arch" ] || continue
echo >&2 "Found $grub_arch files, copying to /work/iso-root/boot/grub2/$grub_arch ..."
cp -avT /rootfs/usr/lib/grub/$grub_arch /work/iso-root/boot/grub2/$grub_arch
done
# Copy efi dir
cp -avT /rootfs/boot/efi/EFI /work/EFI
# Generate grub.cfg
{ grub_cfg="$(</dev/stdin)"; } <<EOF
set timeout=$(yq '.grub2.timeout // 10' <$iso_config_file)
set default="$(yq '.grub2.default // 0' <$iso_config_file)"
set menu_auto_hide=false
function load_video {
insmod all_video
}
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod chain
search --no-floppy --set=root -l '$iso_label'
EOF
for i in $(yq '.grub2.entries | keys | .[]' <"$iso_config_file"); do
entry_name=$(yq ".grub2.entries[$i].name" <"$iso_config_file")
entry_linux=$(yq ".grub2.entries[$i].linux" <"$iso_config_file")
entry_initrd=$(yq ".grub2.entries[$i].initrd" <"$iso_config_file")
{ grub_cfg+=$'\n'"$(</dev/stdin)"; } <<EOF
menuentry '$entry_name' {
linux $entry_linux
initrd $entry_initrd
}
EOF
done
for dir in /work/EFI/* /work/iso-root/boot/grub2; do
echo "$grub_cfg" >"$dir/grub.cfg"
done
# For some reason, fedora also copies EFI into /boot/EFI (?), probably because of hardcoded prefix in grub/shim
cp -avT /work/EFI /work/iso-root/EFI
# Generate uefi.img
pushd /work || exit 1
truncate -s 100M /work/uefi.img
mkfs.fat -F32 /work/uefi.img
mcopy -v -i /work/uefi.img -s /work/EFI ::
xorriso -as mkisofs \
-R \
-V "$iso_label" \
-partition_offset 16 \
-appended_part_as_gpt \
-append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B ./uefi.img \
-iso_mbr_part_type EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 \
-e --interval:appended_partition_2:all:: \
-no-emul-boot \
-iso-level 3 \
-o "/output/$iso_label.iso" \
iso-root

7
examples/README.md Normal file
View file

@ -0,0 +1,7 @@
Examples of container images prepared to be embeded into an ISO.
## References:
- [bootc-isos](https://github.com/ondrejbudai/bootc-isos/tree/3b3a185e4a57947f57baf53d2be5aee469274f98).
- [Container-native ISO contract v0.1.0](https://github.com/ondrejbudai/bootc-isos/blob/3b3a185e4a57947f57baf53d2be5aee469274f98/README.md#container-native-iso-contract-v010)
- [FOSDEM 2026](https://fosdem.org/2026/schedule/event/8MUFCA-building_isos_from_oci_containers/)

View file

@ -0,0 +1,7 @@
# run with --cap-add sys_admin --security-opt label=disable --squash
FROM ghcr.io/ublue-os/bazzite:latest
COPY ./src /src
RUN --mount=type=cache,target=/var/tmp/libdnf5,id=libdnf5 \
/src/build.sh
RUN rm -fr /src

85
examples/bazzite/src/build.sh Executable file
View file

@ -0,0 +1,85 @@
#!/usr/bin/bash
set -exo pipefail
{ export PS4='+( ${BASH_SOURCE}:${LINENO} ): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'; } 2>/dev/null
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Create the directory that /root is symlinked to
mkdir -p "$(realpath /root)"
# bwrap tries to write /proc/sys/user/max_user_namespaces which is mounted as ro
# so we need to remount it as rw
mount -o remount,rw /proc/sys
# Install flatpaks
curl --retry 3 -Lo /etc/flatpak/remotes.d/flathub.flatpakrepo https://dl.flathub.org/repo/flathub.flatpakrepo
xargs -r flatpak install -y --noninteractive <"$SCRIPT_DIR/flatpaks"
# Pull the container image to be installed
podman pull "ghcr.io/ublue-os/bazzite:stable"
# Run the preinitramfs hook
"$SCRIPT_DIR/titanoboa_hook_preinitramfs.sh"
# Install dracut-live and regenerate the initramfs
dnf install -y dracut-live
kernel=$(kernel-install list --json pretty | jq -r '.[] | select(.has_kernel == true) | .version')
DRACUT_NO_XATTR=1 dracut -v --force --zstd --reproducible --no-hostonly \
--add "dmsquash-live dmsquash-live-autooverlay" \
"/usr/lib/modules/${kernel}/initramfs.img" "${kernel}"
# Install livesys-scripts and configure them
dnf install -y livesys-scripts
sed -i "s/^livesys_session=.*/livesys_session=kde/" /etc/sysconfig/livesys
systemctl enable livesys.service livesys-late.service
# Run the postrootfs hook
"$SCRIPT_DIR/titanoboa_hook_postrootfs.sh"
# image-builder needs gcdx64.efi
dnf install -y grub2-efi-x64-cdboot
# image-builder expects the EFI directory to be in /boot/efi
mkdir -p /boot/efi
cp -av /usr/lib/efi/*/*/EFI /boot/efi/
# Remove fallback efi
cp -v /boot/efi/EFI/fedora/grubx64.efi /boot/efi/EFI/BOOT/fbx64.efi # NOTE: remove this line if breaks bootloader
# needed for image-builder's buildroot
dnf install -y xorriso isomd5sum
# Set the timezone to UTC
rm -f /etc/localtime
systemd-firstboot --timezone UTC
# / in a booted live ISO is an overlayfs with upperdir pointed somewhere under /run
# This means that /var/tmp is also technically under /run.
# /run is of course a tmpfs, but set with quite a small size.
# ostree needs quite a lot of space on /var/tmp for temporary files so /run is not enough.
# Mount a larger tmpfs to /var/tmp at boot time to avoid this issue.
rm -rf /var/tmp || :
mkdir -p /var/tmp
cat >/etc/systemd/system/var-tmp.mount <<'EOF'
[Unit]
Description=Larger tmpfs for /var/tmp on live system
[Mount]
What=tmpfs
Where=/var/tmp
Type=tmpfs
Options=size=50%%,nr_inodes=1m,x-systemd.graceful-option=usrquota
[Install]
WantedBy=local-fs.target
EOF
systemctl enable var-tmp.mount
# Copy in the iso config for image-builder
mkdir -p /usr/lib/bootc-image-builder
cp /src/iso.yaml /usr/lib/bootc-image-builder/iso.yaml
# Clean up dnf cache to save space
# dnf clean all

View file

@ -0,0 +1,17 @@
app/org.mozilla.firefox/x86_64/stable
app/org.kde.gwenview/x86_64/stable
app/org.kde.okular/x86_64/stable
app/org.kde.kcalc/x86_64/stable
app/org.kde.haruna/x86_64/stable
app/org.kde.filelight/x86_64/stable
app/com.ranfdev.DistroShelf/x86_64/stable
app/com.github.tchx84.Flatseal/x86_64/stable
app/com.github.Matoking.protontricks/x86_64/stable
app/io.github.flattool.Warehouse/x86_64/stable
app/com.vysp3r.ProtonPlus/x86_64/stable
runtime/org.freedesktop.Platform.VulkanLayer.MangoHud/x86_64/23.08
runtime/org.freedesktop.Platform.VulkanLayer.vkBasalt/x86_64/23.08
runtime/org.freedesktop.Platform.VulkanLayer.OBSVkCapture/x86_64/23.08
runtime/com.obsproject.Studio.Plugin.OBSVkCapture/x86_64/stable
runtime/com.obsproject.Studio.Plugin.Gstreamer/x86_64/stable
runtime/com.obsproject.Studio.Plugin.GStreamerVaapi/x86_64/stable

View file

@ -0,0 +1,7 @@
label: "Bazzite-Live"
grub2:
timeout: 10
entries:
- name: "Bazzite Live ISO"
linux: "/images/pxeboot/vmlinuz quiet rhgb root=live:CDLABEL=Bazzite-Live enforcing=0 rd.live.image"
initrd: "/images/pxeboot/initrd.img"

View file

@ -0,0 +1,592 @@
#!/usr/bin/env bash
#
# This is just an example. Is not meant to be used verbatim on production.
set -exo pipefail
source /etc/os-release
# Remove all versionlocks, in order to avoid dependency issues
dnf -qy versionlock clear
# Install Anaconda
dnf install -qy --enable-repo=fedora-cisco-openh264 --allowerasing firefox anaconda-live libblockdev-{btrfs,lvm,dm}
mkdir -p /var/lib/rpm-state # Needed for Anaconda Web UI
# Utilities for displaying a dialog prompting users to review secure boot documentation
dnf install -qy --setopt=install_weak_deps=0 qrencode yad
# Variables
## TODO: Make this configurable!
imageref="ghcr.io/ublue-os/bazzite"
imagetag="latest"
sbkey='https://github.com/ublue-os/akmods/raw/main/certs/public_key.der'
SECUREBOOT_KEY="/usr/share/ublue-os/sb_pubkey.der"
SECUREBOOT_DOC_URL="https://docs.bazzite.gg/sb"
SECUREBOOT_DOC_URL_QR="/usr/share/ublue-os/secure_boot_qr.png"
# Bazzite anaconda profile
: ${VARIANT_ID:?}
cat >/etc/anaconda/profile.d/bazzite.conf <<EOF
# Anaconda configuration file for bazzite
[Profile]
# Define the profile.
profile_id = bazzite
[Profile Detection]
# Match os-release values
os_id = bazzite
[Network]
default_on_boot = FIRST_WIRED_WITH_LINK
[Bootloader]
efi_dir = fedora
menu_auto_hide = True
[Storage]
default_scheme = BTRFS
btrfs_compression = zstd:1
default_partitioning =
/ (min 1 GiB, max 70 GiB)
/home (min 500 MiB, free 50 GiB)
/var (btrfs)
[User Interface]
custom_stylesheet = /usr/share/anaconda/pixmaps/fedora.css
hidden_spokes =
NetworkSpoke
PasswordSpoke
hidden_webui_pages =
root-password
network
[Localization]
use_geolocation = False
EOF
echo "Bazzite release $VERSION_ID ($VERSION_CODENAME)" >/etc/system-release
# Get Artwork
git clone --depth 1 --quiet https://github.com/ublue-os/bazzite.git /root/packages
case "${PRETTY_NAME,,}" in
"bazzite"*)
mkdir -p /usr/share/anaconda/pixmaps/silverblue
cp -r /root/packages/installer/branding/* /usr/share/anaconda/pixmaps/
;;
esac
# Installer icon
_icon=/root/packages/installer/branding/bazzite-installer.svg
_icon_symbol=/root/packages/installer/branding/bazzite-installer-symbolic.svg
if [[ -f $_icon ]]; then
for f in \
/usr/share/icons/hicolor/48x48/apps/org.fedoraproject.AnacondaInstaller.svg \
/usr/share/icons/hicolor/scalable/apps/org.fedoraproject.AnacondaInstaller.svg; do
cp "$_icon" "$f"
done
cp "$_icon_symbol" /usr/share/icons/hicolor/symbolic/apps/org.fedoraproject.AnacondaInstaller-symbolic.svg
fi
unset -v _icon
unset -v _icon_symbol
rm -rf /root/packages
# Secureboot Key Fetch
mkdir -p /usr/share/ublue-os
curl -Lo /usr/share/ublue-os/sb_pubkey.der "$sbkey"
# Default Kickstart
cat <<EOF >>/usr/share/anaconda/interactive-defaults.ks
# Create log directory
%pre
mkdir -p /tmp/anacoda_custom_logs
%end
# Check if there is a bitlocker partition and ask the user to disable it
%pre --erroronfail --log=/tmp/anacoda_custom_logs/detect_bitlocker.log
DOCS_QR=/tmp/detect_bitlocker_qr.png
IS_BITLOCKER=\$(lsblk -o FSTYPE --json | jq '.blockdevices | map(select(.fstype == "BitLocker")) | . != []')
{ WARNING_MSG="\$(</dev/stdin)"; } << 'WARNINGEOF'
<span size="x-large">Windows Bitlocker partition detected</span>
It might interrupt the installation process.
In such case, please, do <b>one</b> of the following:
a) Disconnect its storage drive.
b) Disable Bitlocker in Windows.
c) Delete it in GNOME Disks.
Do you wish to continue?
WARNINGEOF
if [[ \$IS_BITLOCKER =~ true ]]; then
qrencode -o \$DOCS_QR "https://www.wikihow.com/Turn-Off-BitLocker"
_EXITLOCK=1
_RETCODE=0
while [[ \$_EXITLOCK -ne 0 ]]; do
run0 --user=liveuser yad \
--on-top \
--timeout=10 \
--image=\$DOCS_QR \
--text="\$WARNING_MSG" \
--button="Yes, I'm aware, continue":0 --button="Cancel installation":10
_RETCODE=\$?
case \$_RETCODE in
0) _EXITLOCK=0; ;;
10) _EXITLOCK=0; pkill liveinst; pkill firefox; exit 0 ;;
esac
done
fi
%end
# Remove the efi dir, must match efi_dir from the profile config
%pre-install --erroronfail
rm -rf /mnt/sysroot/boot/efi/EFI/fedora
%end
# Relabel the boot partition for the
%pre-install --erroronfail --log=/tmp/anacoda_custom_logs/repartitioning.log
set -x
xboot_dev=\$(findmnt -o SOURCE --nofsroot --noheadings -f --target /mnt/sysroot/boot)
if [[ -z \$xboot_dev ]]; then
echo "ERROR: xboot_dev not found"
exit 1
fi
e2label "\$xboot_dev" "bazzite_xboot"
%end
# Open a dialog with the installation logs
%onerror
run0 --user=liveuser yad \
--timeout=0 \
--text-info \
--no-buttons \
--width=600 \
--height=400 \
--text="An error occurred during installation. Please report this issue to the developers." \
< /tmp/anaconda.log
%end
$(
if [[ $imageref == *-deck* ]]; then
cat <<EOCAT
# Set default user
user --name=bazzite --password=bazzite --plaintext --groups=wheel
EOCAT
fi
)
ostreecontainer --url=$imageref:$imagetag --transport=containers-storage --no-signature-verification
%include /usr/share/anaconda/post-scripts/install-configure-upgrade.ks
%include /usr/share/anaconda/post-scripts/disable-fedora-flatpak.ks
%include /usr/share/anaconda/post-scripts/install-flatpaks.ks
%include /usr/share/anaconda/post-scripts/secureboot-enroll-key.ks
%include /usr/share/anaconda/post-scripts/secureboot-docs.ks
EOF
# Signed Images
cat <<EOF >>/usr/share/anaconda/post-scripts/install-configure-upgrade.ks
%post --erroronfail --log=/tmp/anacoda_custom_logs/bootc-switch.log
# bootc switch --mutate-in-place --enforce-container-sigpolicy --transport registry $imageref:$imagetag
# DELETEME: This is a nasty hack. Remove whenever http://github.com/bootc-dev/bootc/commit/f7b41cc1ebfc823e9de848b55773faddc59ecf88 makes it into a release
sed -i 's|container-image-reference=.*|container-image-reference=ostree-image-signed:docker://$imageref:$imagetag|' /ostree/deploy/default/deploy/*.origin
%end
EOF
# Enroll Secureboot Key
cat <<EOF >>/usr/share/anaconda/post-scripts/secureboot-enroll-key.ks
%post --erroronfail --nochroot --log=/tmp/anacoda_custom_logs/secureboot-enroll-key.log
set -oue pipefail
readonly ENROLLMENT_PASSWORD="universalblue"
readonly SECUREBOOT_KEY="$SECUREBOOT_KEY"
if [[ ! -d "/sys/firmware/efi" ]]; then
echo "EFI mode not detected. Skipping key enrollment."
exit 0
fi
if [[ ! -f "\$SECUREBOOT_KEY" ]]; then
echo "Secure boot key not provided: \$SECUREBOOT_KEY"
exit 0
fi
SYS_ID="\$(cat /sys/devices/virtual/dmi/id/product_name)"
if [[ ":Jupiter:Galileo:" =~ ":\$SYS_ID:" ]]; then
echo "Steam Deck hardware detected. Skipping key enrollment."
exit 0
fi
mokutil --timeout -1 || :
echo -e "\$ENROLLMENT_PASSWORD\n\$ENROLLMENT_PASSWORD" | mokutil --import "\$SECUREBOOT_KEY" || :
%end
EOF
cat <<EOF >>/usr/share/anaconda/post-scripts/secureboot-docs.ks
%post --nochroot --log=/tmp/anacoda_custom_logs/secureboot-docs.log
SECUREBOOT_KEY="$SECUREBOOT_KEY"
SECUREBOOT_DOC_URL="$SECUREBOOT_DOC_URL"
SECUREBOOT_DOC_URL_QR="$SECUREBOOT_DOC_URL_QR"
LC_ALL=C mokutil -t "\$SECUREBOOT_KEY" | grep -q "is already in the enrollment request" && \
run0 --user=liveuser yad --timeout=0 --on-top --button=Ok:0 --image="\$SECUREBOOT_DOC_URL_QR" --text="<b>Secure Boot Key added:</b>\nPlease check the documentation to finish enrolling the key\n\$SECUREBOOT_DOC_URL"
%end
EOF
qrencode -o "$SECUREBOOT_DOC_URL_QR" "$SECUREBOOT_DOC_URL"
# Install Flatpaks
cat <<'EOF' >>/usr/share/anaconda/post-scripts/install-flatpaks.ks
%post --erroronfail --nochroot --log=/tmp/anacoda_custom_logs/install-flatpaks.log
deployment="$(ostree rev-parse --repo=/mnt/sysimage/ostree/repo ostree/0/1/0)"
target="/mnt/sysimage/ostree/deploy/default/deploy/$deployment.0/var/lib/"
mkdir -p "$target"
rsync -aAXUHKP --filter='-x security.selinux' /var/lib/flatpak "$target"
%end
EOF
# Disable Fedora Flatpak Repo
cat <<EOF >>/usr/share/anaconda/post-scripts/disable-fedora-flatpak.ks
%post --erroronfail --log=/tmp/anacoda_custom_logs/disable-fedora-flatpak.log
systemctl disable flatpak-add-fedora-repos.service || :
%end
EOF
# Set Anaconda Payload to use flathub
cat <<EOF >>/etc/anaconda/conf.d/anaconda.conf
[Payload]
flatpak_remote = flathub https://dl.flathub.org/repo/
EOF
# TODO (@Zeglius): Hide grub by default and set timeout to 5 seconds
# # Hide grub by default and set timeout to 5 seconds
# mkdir -p /boot/grub2
# cat >>/boot/grub2/grub.cfg <<'EOF'
# # Setup for liveisos
# set menu_auto_hide=2
# set timeout_style=hidden
# set timeout=5
# EOF
### Livecds runtime tweaks ###
# Disable services
(
set +e
for s in \
rpm-ostree-countme.service \
tailscaled.service \
bazzite-hardware-setup.service \
bootloader-update.service \
brew-upgrade.timer \
brew-update.timer \
brew-setup.service \
rpm-ostreed-automatic.timer \
uupd.timer \
ublue-guest-user.service \
ublue-os-media-automount.service \
ublue-system-setup.service \
check-sb-key.service; do
systemctl disable $s
done
for s in \
ublue-flatpak-manager.service \
podman-auto-update.timer \
ublue-user-setup.service; do
systemctl --global disable $s
done
)
# Add bootloader restoring script
cat >/usr/bin/bootloader_restore.sh <<'SCRIPTEOF'
#!/usr/bin/env -S /usr/bin/pkexec --keep-cwd /usr/bin/bash
set -o pipefail
if [[ $DEBUG -eq 1 ]]; then
set -x
fi
exec > >(tee -a /tmp/bootloader_restore.log) 2>&1
echo >&2 "### START LOG $(date -u) ###"
######################################################
yad() {
command run0 --user="$PKEXEC_UID" -- command yad \
--title="$_APP_NAME" \
--separator=$'\n' \
--timeout=0 \
"$@"
}
info() {
echo >&2 "INFO [${0##*/}:${BASH_LINENO[0]}]: $*"
}
error() {
echo >&2 "ERROR [${0##*/}:${BASH_LINENO[0]}]: $*"
}
die() {
error "$*"
exit 1
}
die_gui() {
yad --title="Error" --text="$(error "$*" 2>&1)" --button="OK:0"
die "$*"
}
######################################################
if [[ $PKEXEC_UID -eq 0 ]]; then
die "You must not execute this script as root."
fi
_APP_NAME="Bazzite Bootloader Restoring Tool"
DRY_RUN=${DRY_RUN:-0}
MNT=/tmp/mnt
trap 'umount --recursive $MNT/boot 2>/dev/null' EXIT
DISK_PATH=$(lsblk -d -n -o NAME,SIZE,MODEL | while read -r name size model; do
echo "$name"
echo "$size"
echo "$model"
done | yad --list --no-buttons \
--text="Double-click the disk where you installed Bazzite:" --width=500 --height=300 \
--column="Device" \
--column="Size" \
--column="Model" \
--print-column=1) || {
info "User cancelled during disk selection"
exit 0
}
: "${DISK_PATH:?}"
DISK_PATH=/dev/${DISK_PATH}
efi_dev=$(systemd-repart --json=short "$DISK_PATH" 2>/dev/null |
jq -r '.[] | select(.type == "esp").node')
[[ -n ${efi_dev} ]] || { die_gui "EFI partition not found"; }
xboot_dev=$(
lsblk -J -p -f -o NAME,LABEL,SIZE,FSTYPE "$DISK_PATH" 2>/dev/null |
jq -r '.blockdevices[0].children[] | select(.fstype == "ext4") | "\(.name)\n\(.label // "")\n\(.size)"' |
yad --list --no-buttons \
--text="Double-click the XBOOT partition:" --width=500 --height=300 \
--column="Device" \
--column="Label" \
--column="Size" \
--print-column=1
) || {
info "User cancelled during XBOOT partition selection"
exit 0
}
[[ -n "${xboot_dev}" ]] || die_gui "You must select an XBOOT partition."
yad --text="This will restore the boot in the device $DISK_PATH, using $xboot_dev as the XBOOT partition. Proceed?" || {
info "User cancelled during restoration confirmation"
exit 0
}
mount --mkdir "$xboot_dev" "$MNT"/boot || die "Failed to mount XBOOT partition"
mount "$efi_dev" "$MNT"/boot/efi || die "Failed to mount EFI partition"
if [[ $DRY_RUN -eq 1 ]]; then
info "Script was executed with ${DRY_RUN@A}, skipping bootloader restoration..."
yad --text="Script was executed with ${DRY_RUN@A}, skipping bootloader restoration...." --button="OK:0"
else
if [[ -f $MNT/boot/bootupd-state.json ]]; then
rm -vf $MNT/boot/bootupd-state.json &&
info "Removed existing bootupd-state.json"
fi
run0 --user="$PKEXEC_UID" -- \
ptyxis --title="$_APP_NAME - Restoring bootloader" -- \
pkexec bash -c "bootupctl backend install \
-vvvv \
--auto \
--write-uuid \
--update-firmware \
--device \"$DISK_PATH\" \"$MNT\"" &&
info "Bootloader restored successfully." &&
yad --text "Bootloader restored successfully." --button="OK:0"
fi
SCRIPTEOF
chmod +x /usr/bin/bootloader_restore.sh
cat >/usr/share/applications/bazzite_bootloader_restoring_tool.desktop <<'EOF'
[Desktop Entry]
Type=Application
Icon=tools-wizard-symbolic
Name=Bazzite Bootloader Restoring Tool BETA
Comment=Restore the bootloader of an installation if has been overriden by Windows
Keywords=bootloader;fix;grub;windows
Categories=System;Utility
Exec=/usr/bin/bootloader_restore.sh
Hidden=false
NoDisplay=false
StartupNotify=true
Terminal=false
EOF
### Desktop-enviroment specific tweaks ###
# Setup script to show dialog popups at login
echo '#!/usr/bin/bash' >/usr/bin/on_gui_login.sh
chmod +x /usr/bin/on_gui_login.sh
mkdir -p /etc/skel/.config/autostart
cat >/etc/skel/.config/autostart/on_gui_login.desktop <<'EOF'
[Desktop Entry]
Exec=/usr/bin/on_gui_login.sh
Icon=application-x-shellscript
Type=Application
EOF
# Warn the user about non functional Nvidia drivers
if [[ $imageref == *-nvidia* ]]; then
cat >>/usr/bin/on_gui_login.sh <<'EOF'
{ yad --title="Warning" --text="$(</dev/stdin)" || true; } <<'WARNINGEOF'
Nvidia drivers might not be functional on live isos.
Please do not use them in benchmarks.
WARNINGEOF
EOF
fi
# Use GSK_RENDERER=gl for nvidia, workaround for GTK apps not opening.
if [[ $imageref == *-nvidia* ]]; then
mkdir -p /etc/environment.d /etc/skel/.config/environment.d
echo "GSK_RENDERER=gl" >>/etc/environment.d/99-nvidia-fix.conf
echo "GSK_RENDERER=gl" >>/etc/skel/.config/environment.d/99-nvidia-fix.conf
fi
# Reenable noveau.
if [[ $imageref == *-nvidia* ]]; then
for pkg in nvidia-gpu-firmware mesa-vulkan-drivers; do
dnf -yq reinstall --allowerasing $pkg ||
dnf -yq install --allowerasing $pkg
done
# Ensure noveau vulkan icds exist
(
shopt -u nullglob
ls /usr/share/vulkan/icd.d/nouveau_icd.*.json >/dev/null
) || {
echo >&2 "::error::No nouveau vulkan icds found at /usr/share/vulkan/icd.d/nouveau_icd.*.json"
exit 1
}
fi
# Determine desktop environment. Must match one of /usr/libexec/livesys/sessions.d/livesys-{desktop_env}
# See https://github.com/ublue-os/titanoboa/blob/6c2e8ba58c7534b502081fe24363d2a60e7edca9/Justfile#L199-L213
desktop_env=""
_session_file="$(find /usr/share/wayland-sessions/ /usr/share/xsessions \
-maxdepth 1 -type f -not -name '*gamescope*.desktop' -and -name '*.desktop' -printf '%P' -quit)"
case $_session_file in
budgie*) desktop_env=budgie ;;
cosmic*) desktop_env=cosmic ;;
gnome*) desktop_env=gnome ;;
plasma*) desktop_env=kde ;;
sway*) desktop_env=sway ;;
xfce*) desktop_env=xfce ;;
esac
# Dont start Steam at login
rm -vf /etc/skel/.config/autostart/steam*.desktop
# Remove packages that shouldnt be used in a live session
dnf -yq remove steam lutris || :
# Warn about limited capabilities of live sessions, and also show buttons to:
# - Install Bazzite
# - Launch Bootloader Restoring tool
# - Close dialog
cat >>/usr/bin/on_gui_login.sh <<'EOF'
_EXITLOCK=1
_RETVAL=0
while [[ $_EXITLOCK -eq 1 ]]; do
yad \
--no-escape \
--on-top \
--timeout-indicator=bottom \
--text-align=center \
--buttons-layout=center \
--title="Welcome" \
--text="\nWelcome to the Live ISO for Bazzite\!\n\nThe Live ISO is designed for installation and troubleshooting.\nBecause of this, it is <b>not capable of playing games.</b>\n\nPlease do not use it for benchmarks as it\ndoes not represent the installed experience.\n" \
--button="Install Bazzite":10 \
--button="Launch Bootloader Restoring tool":20 \
--button="Close dialog":0
_RETVAL=$?
case $_RETVAL in
10)
liveinst & disown $!
_EXITLOCK=0
;;
20)
/usr/bin/bootloader_restore.sh & disown $!
_EXITLOCK=0
;;
0) _EXITLOCK=0 ;;
esac
done
unset -v _EXITLOCK
unset -v _RETVAL
EOF
(
wallpaper_url=https://github.com/ublue-os/bazzite/raw/refs/heads/main/press_kit/art/Convergence_Wallpaper_DX.jxl
wallpaper_file=/usr/share/wallpapers/convergence.jxl
wget -nv -O "$wallpaper_file" "$wallpaper_url"
cp 2>/dev/null "$wallpaper_file" /usr/share/backgrounds/convergence.jxl || :
cp 2>/dev/null "$wallpaper_file" /usr/share/backgrounds/convergence/convergence_morn.jxl || :
rm -f /usr/share/backgrounds/default.xml
)
# Enable on-screen keyboard
if [[ $imageref == *-deck* ]]; then
# Enable keyboard here
if [[ $desktop_env == gnome ]]; then
echo >>/etc/skel/.bash_profile \
"gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true >/dev/null 2>&1 || :"
rm -rf /usr/share/gnome-shell/extensions/block-caribou-36@lxylxy123456.ercli.dev
elif [[ $desktop_env == kde ]]; then
mv /usr/share/ublue-os/backup/com.github.maliit.keyboard.desktop \
/usr/share/applications/com.github.maliit.keyboard.desktop || :
fi
fi
# Tweak the fedora-welcome app (gnome only) with our own text/icons
if [[ $desktop_env == gnome ]]; then
sed -i 's| Fedora| Bazzite|' /usr/share/anaconda/gnome/fedora-welcome || :
cp -f /usr/share/pixmaps/{fedora-logo-sprite,fedora-logo-icon}.png || :
fi
# Let only browser/installer in the task-bar/dock
if [[ $desktop_env == kde ]]; then
sed -i '/<entry name="launchers" type="StringList">/,/<\/entry>/ s/<default>[^<]*<\/default>/<default>preferred:\/\/browser,applications:liveinst.desktop,preferred:\/\/filemanager<\/default>/' \
/usr/share/plasma/plasmoids/org.kde.plasma.taskmanager/contents/config/main.xml || :
elif [[ $desktop_env == gnome ]]; then
cat >/usr/share/glib-2.0/schemas/zz2-org.gnome.shell.gschema.override <<EOF
[org.gnome.shell]
welcome-dialog-last-shown-version='4294967295'
favorite-apps = ['liveinst.desktop', 'org.mozilla.firefox.desktop', 'org.gnome.Nautilus.desktop']
EOF
glib-compile-schemas /usr/share/glib-2.0/schemas
fi
# Disable kde wallet
if [[ $desktop_env == kde ]]; then
mkdir -p /etc/skel/.config
cat >/etc/skel/.config/kwalletrc <<'EOF'
[Wallet]
Enabled=false
EOF
fi
# Install Gparted
dnf -yq install gparted
###############################

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
#
set -exo pipefail
# Swap kernel with vanilla and rebuild initramfs.
#
# This is done because we want the initramfs to use a signed
# kernel for secureboot.
kernel_pkgs=(
kernel
kernel-core
kernel-devel
kernel-devel-matched
kernel-modules
kernel-modules-core
kernel-modules-extra
)
dnf -y versionlock delete "${kernel_pkgs[@]}"
dnf --setopt=protect_running_kernel=False -y remove "${kernel_pkgs[@]}"
(cd /usr/lib/modules && rm -rf -- ./*)
dnf -y --repo fedora,updates --setopt=tsflags=noscripts install kernel kernel-core
kernel=$(find /usr/lib/modules -maxdepth 1 -type d -printf '%P\n' | grep .)
depmod "$kernel"
imageref="$(podman images --format '{{ index .Names 0 }}\n' 'bazzite*' | head -1)"
imageref="${imageref##*://}"
imageref="${imageref%%:*}"
# Include nvidia-gpu-firmware package.
dnf install -yq nvidia-gpu-firmware || :
dnf clean all -yq

View file

@ -0,0 +1,7 @@
# run with --cap-add sys_admin --security-opt label=disable --squash
FROM ghcr.io/zirconium-dev/zirconium:latest
COPY ./src /src
RUN --mount=type=cache,target=/var/tmp/libdnf5,id=libdnf5 \
/src/build.sh
RUN rm -fr /src

85
examples/zirconium/src/build.sh Executable file
View file

@ -0,0 +1,85 @@
#!/usr/bin/bash
set -exo pipefail
{ export PS4='+( ${BASH_SOURCE}:${LINENO} ): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'; } 2>/dev/null
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Create the directory that /root is symlinked to
mkdir -p "$(realpath /root)"
# bwrap tries to write /proc/sys/user/max_user_namespaces which is mounted as ro
# so we need to remount it as rw
mount -o remount,rw /proc/sys
# Install flatpaks
curl --retry 3 -Lo /etc/flatpak/remotes.d/flathub.flatpakrepo https://dl.flathub.org/repo/flathub.flatpakrepo
xargs -r flatpak install -y --noninteractive <"$SCRIPT_DIR/flatpaks"
# Install dracut-live and regenerate the initramfs
dnf install -y dracut-live
kernel=$(kernel-install list --json pretty | jq -r '.[] | select(.has_kernel == true) | .version')
DRACUT_NO_XATTR=1 dracut -v --force --zstd --reproducible --no-hostonly \
--add "dmsquash-live dmsquash-live-autooverlay" \
"/usr/lib/modules/${kernel}/initramfs.img" "${kernel}"
# Install livesys-scripts and configure them
dnf install -y livesys-scripts
# sed -i "s/^livesys_session=.*/livesys_session=kde/" /etc/sysconfig/livesys
systemctl enable livesys.service livesys-late.service
# Run the postrootfs hook
"$SCRIPT_DIR/titanoboa_hook_postrootfs.sh"
# image-builder needs gcdx64.efi
_arch=$(uname -m)
if [[ $_arch == "x86_64" ]]; then
dnf install -y grub2-efi-x64-cdboot
elif [[ $_arch == "aarch64" ]]; then
dnf install -y grub2-efi-aa64-modules
fi
# image-builder expects the EFI directory to be in /boot/efi
mkdir -p /boot/efi
cp -av /usr/lib/efi/*/*/EFI /boot/efi/
# Remove fallback efi
_arch=$(uname -m)
if [[ $_arch == "x86_64" ]]; then
cp -v /boot/efi/EFI/fedora/grubx64.efi /boot/efi/EFI/BOOT/fbx64.efi
elif [[ $_arch == "aarch64" ]]; then
cp -v /boot/efi/EFI/fedora/grubaa64.efi /boot/efi/EFI/BOOT/fbaa64.efi
fi
# Set the timezone to UTC
rm -f /etc/localtime
systemd-firstboot --timezone UTC
# / in a booted live ISO is an overlayfs with upperdir pointed somewhere under /run
# This means that /var/tmp is also technically under /run.
# /run is of course a tmpfs, but set with quite a small size.
# ostree needs quite a lot of space on /var/tmp for temporary files so /run is not enough.
# Mount a larger tmpfs to /var/tmp at boot time to avoid this issue.
mkdir -p /var/tmp
cat >/etc/systemd/system/var-tmp.mount <<'EOF'
[Unit]
Description=Larger tmpfs for /var/tmp on live system
[Mount]
What=tmpfs
Where=/var/tmp
Type=tmpfs
Options=size=50%%,nr_inodes=1m,x-systemd.graceful-option=usrquota
[Install]
WantedBy=local-fs.target
EOF
systemctl enable var-tmp.mount
# Copy in the iso config for image-builder
mkdir -p /usr/lib/bootc-image-builder
cp /src/iso.yaml /usr/lib/bootc-image-builder/iso.yaml
# Clean up dnf cache to save space
# dnf clean all

View file

View file

@ -0,0 +1,10 @@
label: "Zirconium-Live"
grub2:
timeout: 10
entries:
- name: "Zirconium Live ISO"
linux: "/images/pxeboot/vmlinuz quiet rhgb root=live:CDLABEL=Zirconium-Live enforcing=0 rd.live.image"
initrd: "/images/pxeboot/initrd.img"
- name: "Zirconium Live ISO (Safe Graphics Mode)"
linux: "/images/pxeboot/vmlinuz quiet rhgb root=live:CDLABEL=Zirconium-Live enforcing=0 rd.live.image nomodeset"
initrd: "/images/pxeboot/initrd.img"

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
# TODO: Setup anaconda

76
main.sh Executable file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env -S bash -eo pipefail
#
# Project Titanoboa
#
# Description: Create bootable ISOs from bootc container images.
# Container-native ISO contract v0.1.0
# See https://github.com/ondrejbudai/bootc-isos/tree/main
#
# This spec is inspired by the layout of Fedora bootc images and Fedora live ISO images.
#
# - The kernel is expected to be in /usr/lib/module/*/vmlinuz. If there are multiple kernels, the behavior is unspecified. This is to be specified in a future version of this contract. The kernel is put in /images/pxeboot/vmlinuz in the ISO.
# - The initramfs is expected to be next to the kernel with the filename initramfs.img. The initramfs is put in /images/pxeboot/initrd.img.
# - The UEFI vendor is specified by a directory name in /usr/lib/efi/shim/*/EFI/$VENDOR. If there are multiple directories, the behaviour is unspecified. The BOOT directory is always ignored.
# - Shim and grub2 EFI binaries (shimx64.efi, mmx64.efi, gcdx64.efi) are expected to be in /boot/efi/EFI/$VENDOR.
# - GRUB2 modules are expected to be in /usr/lib/grub/i386-pc.
# - Required executables are podman, mksquashfs, xorriso, implantisomd5, grub2-mkimage, and python.
# - The container image is converted to a squashfs archive and put into /LiveOS/squashfs.img in the ISO.
# - Additional configuration can be written into /usr/lib/bootc-image-builder/iso.yaml in YAML format. The file currently supports 2 top-level keys:
# - label (string): Label of the ISO
# - grub2 (object): GRUB2 configuration, supports the following keys:
# - default (string): Default menu item
# - timeout (string): Default timeout (in seconds)
# - entries (array of objects): GRUB2 menu entries with the following keys (all are required):
# - name (string): Name of the entry
# - linux (string): Path to the kernel + kernel arguments (the path is always /images/pxeboot/vmlinuz in this version of this spec)
# - initrd (string): Path to the initramfs (the path is always /images/pxeboot/initrd.img in this version of this spec)
# - The --bootc-installer-payload-ref argument to image-builder can optionally be used to copy a container image from the host's container storage to /var/lib/containers/storage in the squashfs archive.
{
# Image to be injected on the iso as squashfs. Example: localhost/live_env:latest
TITANOBOA_CTR_IMAGE=${1:-${TITANOBOA_CTR_IMAGE}}
# Directory where the ISO will be stored
TITANOBOA_OUTPUT_DIR=${TITANOBOA_OUTPUT_DIR:-./output}
# Whenever Titanoboa is running inside a container
TITANOBOA_INSIDE_CONTAINER=${TITANOBOA_INSIDE_CONTAINER:-false}
SCRIPT_DIR=$(dirname "$0")
mkdir -p "$TITANOBOA_OUTPUT_DIR"
# If we are running inside a container
if [ "$TITANOBOA_INSIDE_CONTAINER" = "true" ]; then
TITANOBOA_OUTPUT_DIR=/output
if ! mountpoint -q "$TITANOBOA_OUTPUT_DIR"; then
echo "Error: output directory must be a volume mountpoint"
exit 1
fi
if ! mountpoint -q /usr/lib/containers/storage; then
echo "Error: /usr/lib/containers/storage must be a volume mountpoint"
exit 1
fi
if ! mountpoint -q /rootfs; then
echo "Error: /rootfs must be a volume mountpoint"
exit 1
fi
/app/bin/build_iso.sh
else
if [ -z "$TITANOBOA_CTR_IMAGE" ]; then
echo "Error: container image in param 1 nor TITANOBOA_CTR_IMAGE environment variable"
exit 1
fi
sudo podman run --rm -i \
--cap-add sys_admin --security-opt label=disable \
-v "$SCRIPT_DIR"/build_iso.sh:/src/build_iso.sh:ro \
--mount type=image,source="$TITANOBOA_CTR_IMAGE",dst=/rootfs \
-v "$TITANOBOA_OUTPUT_DIR":/output \
quay.io/fedora/fedora:latest /src/build_iso.sh
fi
} >&2
realpath "$TITANOBOA_OUTPUT_DIR"/*.iso | head -1

1
src/.gitignore vendored
View file

@ -1 +0,0 @@
grub.cfg

View file

@ -1,33 +0,0 @@
# org.gnome.Platform
org.gtk.Gtk3theme.adw-gtk3
org.gtk.Gtk3theme.adw-gtk3-dark
com.github.rafostar.Clapper
io.gitlab.adhami3310.Impression
com.github.tchx84.Flatseal
io.github.flattool.Ignition
io.github.flattool.Warehouse
org.gnome.baobab
org.gnome.Calculator
org.gnome.Calendar
org.gnome.Characters
org.gnome.clocks
org.gnome.Connections
org.gnome.Contacts
org.gnome.DejaDup
org.gnome.Papers
com.mattjakeman.ExtensionManager
org.gnome.FileRoller
org.gnome.font-viewer
org.gnome.Logs
org.gnome.Loupe
org.gnome.Maps
org.gnome.NautilusPreviewer
org.gnome.World.PikaBackup
org.gnome.TextEditor
org.gnome.Weather
io.missioncenter.MissionCenter
org.mozilla.firefox
# org.mozilla.Thunderbird
page.tesk.Refine
io.github.pwr_solaar.solaar
# io.github.input_leap.input-leap

View file

@ -1,36 +0,0 @@
set default="0"
function load_video {
insmod all_video
}
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod chain
set timeout=10
search --no-floppy --set=root -l 'titanoboa_boot'
menuentry '@PRETTY_NAME@ Live ISO' --class fedora --class gnu-linux --class gnu --class os {
linux ($root)/boot/vmlinuz quiet rhgb root=live:CDLABEL=titanoboa_boot enforcing=0 rd.live.image @EXTRA_KARGS@
initrd ($root)/boot/initramfs.img
}
menuentry '@PRETTY_NAME@ Live ISO (Basic Graphics Mode)' --class fedora --class gnu-linux --class gnu --class os {
linux ($root)/boot/vmlinuz quiet rhgb root=live:CDLABEL=titanoboa_boot enforcing=0 rd.live.image nomodeset @EXTRA_KARGS@
initrd ($root)/boot/initramfs.img
}
# menuentry 'Try @PRETTY_NAME@ (RAM Only)' --class fedora --class gnu-linux --class gnu --class os {
# linux ($root)/boot/vmlinuz quiet rhgb root=live:CDLABEL=titanoboa_boot enforcing=0 rd.live.ram rd.live.image @EXTRA_KARGS@
# initrd ($root)/boot/initramfs.img
# }
# submenu 'Troubleshooting -->' {
# menuentry 'Install @PRETTY_NAME@ in basic graphics mode' --class fedora --class gnu-linux --class gnu --class os {
# linux ($root)/boot/vmlinuz quiet rhgb root=live:CDLABEL=titanoboa_boot enforcing=0 rd.live.image nomodeset
# initrd ($root)/boot/initramfs.img
# }
# }

View file

@ -1,13 +0,0 @@
#!/bin/bash
. /etc/os-release
usermod -c "$NAME User" liveuser
hostnamectl hostname "$ID"
timedatectl set-timezone UTC
ln -s /usr/share/zoneinfo/UTC /etc/localtime
if [ -d /var/lib/livesys/livesys-session-extra.d ]; then
find /var/lib/livesys/livesys-session-extra.d -type f -exec /bin/bash {} \;
fi

View file

@ -1,10 +0,0 @@
polkit.addRule(function(action,subject) {
if ( (action.id == "org.freedesktop.policykit.exec") &&
(action.lookup("program") == "/usr/bin/bootc") &&
(subject.isInGroup("wheel") ) ) {
return polkit.Result.YES;
}
return polkit.Result.NOT_HANDLED;
}
);

View file

@ -1,10 +0,0 @@
polkit.addRule(function(action,subject) {
if ( (action.id == "org.freedesktop.policykit.exec") &&
(action.lookup("program") == "/usr/sbin/liveinst") &&
(subject.isInGroup("wheel") ) ) {
return polkit.Result.YES;
}
return polkit.Result.NOT_HANDLED;
}
);

View file

@ -1,12 +0,0 @@
polkit.addRule(function(action,subject) {
if ( (action.id == "org.freedesktop.policykit.exec") &&
(action.lookup("program") == "/usr/bin/podman") &&
(subject.isInGroup("wheel") ) ) {
return polkit.Result.YES;
}
return polkit.Result.NOT_HANDLED;
}
);