mirror-ublue-os-titanoboa/Justfile
2025-11-09 19:23:06 +00:00

554 lines
20 KiB
Makefile

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 }}