Skip to content

Build Images

Build Images #420

Workflow file for this run

name: Build Images
# Builds the Swift Linux A/B UEFI images in the plain buildroot-swift:latest
# container. Each job seeds its Buildroot output tree from the rolling
# toolchain-latest release that build-toolchain.yml publishes (br-seed), so
# the cross-toolchain, Swift and the mesa host tools are reused and only the
# image profile's extra packages - sway, GRUB, RAUC, steam, ... - compile.
# The trees persist on the runner at /mnt/br/profiles/<profile>/<arch>,
# bind-mounted to /mnt/br/output/<arch> in the container because that is the
# prefix Buildroot baked into the packaged toolchain - see docs/build.md.
# The Swift SDKs are built by a separate workflow (build-swift-sdk.yml): they
# share nothing with the images but the container, and splitting them means a
# broken SDK and a broken image are separate red marks, each re-runnable
# without paying for the other.
#
# Runs on the self-hosted x86 runner (see docs/build.md), which is a single
# machine: the jobs below execute one at a time rather than side by side on
# GitHub's pool. The ccache lives on that machine's own disk at
# /mnt/br/ccache/<arch> - the same directory the toolchain and SDK workflows
# use - so it survives between runs and there is nothing to upload or restore.
#
# A full run builds four Buildroot trees (two images, two 32-bit companions)
# and takes hours, so it is not worth spending on every push: it runs once a
# night, and on demand from the Actions tab (or "gh workflow run
# build-images.yml") when a change needs checking before then.
on:
schedule:
# 07:00 UTC - after the working day in the Americas, so the nightly
# result is waiting in the morning.
- cron: '0 7 * * *'
workflow_dispatch:
inputs:
frontend:
description: >
Frontend the image boots into (sdk/defconfig/frontend/), or "none"
for the frontend-independent baseline.
required: false
default: minimal
type: choice
options: [minimal, emulationstation, gmenu2x, gnome, phosh, xfce, none]
arch:
description: >
Architecture to build, for the generic UEFI boards. "both" is the
nightly's pair. Ignored when a device is given, since a board is
one architecture already.
required: false
default: both
type: choice
options: [both, x86_64, arm64]
device:
description: >
Board to build for (a directory under sdk/board/, e.g.
chromebook-scarlet), instead of the generic UEFI image for the
architecture. Only that board's architecture is built. Leave empty
for the usual x86_64 + arm64 pair.
required: false
default: ''
type: string
rebuild:
description: >
Space-separated packages to dirclean before building, for when a
persistent tree holds a copy built the wrong way. Needed after
adding a patch to a package the tree already extracted: Buildroot
applies patches once, at extract time, so a new one is otherwise
ignored. build-images.sh watches the patch directories itself and
re-extracts on a change it saw happen, which leaves only the first
sighting - a package it had never recorded before - to name here.
required: false
default: ''
type: string
reseed:
description: >
Unpack the toolchain release over the tree even when its stamp says
it is current. For when the tree has lost something only the release
carries: a seeded tree holds most packages stamped built with no
source, so one dircleaned out of it cannot be rebuilt in place -
swift-foundation compiles host-swift's checkout, and neither is ever
built in an image tree. Wipes the tree, so everything above the
toolchain is rebuilt.
required: false
default: false
type: boolean
clean:
description: >
Empty the persistent output tree before configuring, rebuilding
everything from source. For when the tree itself is suspect rather
than one package in it - a machine that dies mid-build can leave a
package stamped complete with no source and its installed files
missing, which Buildroot reads as done and skips. That surfaces one
failure at a time, in whatever reaches for a file that was never
written. Costs a full rebuild, so prefer "rebuild" when the culprit
can be named.
required: false
default: false
type: boolean
# One runner, so a nightly still running when the next fires would otherwise
# queue behind itself. Supersede instead.
concurrency:
# Keyed the same way the tree is (see the --volume below): a frontend other
# than minimal, and a device, each get their own /mnt/br/profiles/image
# directory. Grouping on workflow and ref alone ignored that and made every
# image run supersede every other one regardless of what it was building -
# the nightly cancelled a gnome build four and a half hours in, having
# gained nothing, because the two share no tree and the runner serialises
# them anyway. Two nightlies still collide, which is what this is for.
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.device || inputs.arch || 'both' }}-${{ inputs.frontend || 'minimal' }}
# Back to cancel-in-progress now this is self-hosted again. It was turned
# off while these jobs ran on GitHub's pool, where the argument held: the
# runners are ephemeral, so two runs of the same ref never touch the same
# tree, and cancelling one threw away hours that had to be recompiled from
# nothing.
#
# Neither half of that is true here. The trees live on this machine at
# /mnt/br/profiles/image/<arch>[-<frontend>] and every run that shares a key
# uses the same one, so overlapping runs interleave on shared state. And
# superseding costs far less: the winner inherits the tree the loser was
# part way through, so the work is picked up rather than discarded.
cancel-in-progress: true
env:
# Which frontend the image boots into. build-images.sh passes this to
# generate-config.sh as --frontend, which is emitted after the board so it
# beats the emulationstation the image profile would otherwise include.
#
# A scheduled run has no inputs, so it takes the fallback rather than an
# empty string - without it the schedule would silently build a different
# image from the one a dispatch builds.
FRONTEND: ${{ inputs.frontend || 'minimal' }}
# The caches are on the runner's disk, not in the 10 GB repo-wide
# actions/cache budget, so they are sized for hit rate.
CCACHE_MAX_SIZE: 20G
jobs:
# Which architectures this run builds. Normally both; with a device, only the
# one that board is, read from its own boardinfo rather than from a list kept
# here - sdk/board has 116 boards and a second copy of that mapping would be
# wrong the first time one is added. Runs on the hosted runners: it reads two
# files and the self-hosted box is the scarce resource.
resolve:
name: Resolve build targets
runs-on: ubuntu-latest
outputs:
arches: ${{ steps.pick.outputs.arches }}
lib32: ${{ steps.pick.outputs.lib32 }}
steps:
- uses: actions/checkout@v4
- id: pick
env:
DEVICE: ${{ inputs.device }}
ARCH: ${{ inputs.arch }}
run: |
set -eu
if [ -z "${DEVICE:-}" ]; then
case "${ARCH:-both}" in
x86_64) echo 'arches=["x86_64"]' >> "$GITHUB_OUTPUT"
echo 'lib32=["i386"]' >> "$GITHUB_OUTPUT"
echo "building x86_64 only" ;;
arm64) echo 'arches=["arm64"]' >> "$GITHUB_OUTPUT"
echo 'lib32=["armv7"]' >> "$GITHUB_OUTPUT"
echo "building arm64 only" ;;
*) echo 'arches=["x86_64","arm64"]' >> "$GITHUB_OUTPUT"
echo 'lib32=["i386","armv7"]' >> "$GITHUB_OUTPUT"
echo "building the usual pair" ;;
esac
exit 0
fi
# The generic UEFI boards are not devices - see build-images.sh for
# what passing one as --device actually produces.
case "$DEVICE" in
x86_64|arm64|common)
echo "'$DEVICE' is an architecture, not a device: use the arch input" >&2
exit 1 ;;
esac
board="sdk/board/$DEVICE"
if [ ! -f "$board/board.config" ]; then
echo "unknown device '$DEVICE': no $board/board.config" >&2
echo "available: $(cd sdk/board && ls -d */board.config 2>/dev/null \
| sed 's|/board.config||' | grep -vE '^(x86_64|arm64|common)$' \
| tr '\n' ' ')" >&2
exit 1
fi
arch=$(sed -n 's/^ARCH=//p' "$board/boardinfo" | head -1)
[ -n "$arch" ] || { echo "no ARCH= in $board/boardinfo" >&2; exit 1; }
case "$arch" in
x86_64) companion=i386 ;;
arm64) companion=armv7 ;;
*) echo "device '$DEVICE' is $arch, which has no image profile" >&2; exit 1 ;;
esac
printf 'arches=["%s"]\n' "$arch" >> "$GITHUB_OUTPUT"
printf 'lib32=["%s"]\n' "$companion" >> "$GITHUB_OUTPUT"
echo "building $DEVICE ($arch, companion $companion)"
image:
name: Image (${{ inputs.device || matrix.arch }})
strategy:
fail-fast: false
matrix:
arch: ${{ fromJSON(needs.resolve.outputs.arches) }}
runs-on: [self-hosted, Linux, X64]
needs: [resolve, lib32]
# "needs" is kept for ordering - the companion must finish building before
# an image reads it - but the image does not require the lib32 job to have
# *succeeded*. What it consumes is the tree on the shared disk, not that
# job's conclusion, and the two have come apart three times: the companion
# built and packed cleanly, then the runner process itself died during the
# artifact upload (SIGBUS in node, with an I/O error out of runsvc.sh), so
# continue-on-error on that step could not save the job. Each time a
# perfectly good companion sat on disk while both images were skipped.
#
# "Locate the 32-bit companion" below is the real gate: it checks the tree
# exists and carries a 32-bit loader, and fails the image if it does not.
# That check is what correctness rests on, so a dead upload step no longer
# costs the run.
if: ${{ !cancelled() && needs.resolve.result == 'success' }}
# Generous, not calibrated - the six-hour hosted-runner ceiling does not
# apply here. It exists so a wedged build eventually releases the runner
# rather than blocking every other workflow forever, which matters more
# here than on the pool: this is one machine and the jobs queue behind
# each other.
#
# For scale: on hosted runners arm64 took 4h32m and x86_64 5h18m from a
# cold tree. This runner keeps its trees, so a repeat run compiles only
# what changed.
timeout-minutes: 1440
# Plain base image; the toolchain comes from br-seed. /mnt is
# bind-mounted so the tree and ccache outlive the container, and the
# per-profile host dir is mounted at the path the packaged toolchain was
# built at.
container:
image: docker.io/colemancda/buildroot-swift:latest
options: >-
--volume /mnt:/mnt
--volume /mnt/br/profiles/image/${{ inputs.device || matrix.arch }}${{ (inputs.frontend != '' && inputs.frontend != 'minimal') && format('-{0}', inputs.frontend) || '' }}:/mnt/br/output/${{ matrix.arch }}
--security-opt label=disable
env:
CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }}
steps:
- name: Checkout swift-linux
uses: actions/checkout@v4
# Seed before br-setup: any defconfig has to land on the seeded tree,
# not be buried under the unpack.
- name: Seed from the toolchain release
uses: ./.github/actions/br-seed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
arch: ${{ matrix.arch }}
dest-dir: /mnt/br/output/${{ matrix.arch }}
force: ${{ inputs.reseed && '1' || '' }}
# configure: 'false' - build-images.sh owns this tree's defconfig (it
# merges the lib32 and FRONTEND fragments), so br-setup only does the
# prerequisites, disk guard and submodule checkout here.
- name: Prepare the tree
uses: ./.github/actions/br-setup
with:
output-dir: /mnt/br/output/${{ matrix.arch }}
arch: ${{ matrix.arch }}
configure: 'false'
ccache-dir: /mnt/br/ccache/${{ matrix.arch }}
ccache-max-size: ${{ env.CCACHE_MAX_SIZE }}
# Back to a real guard now this runs on the self-hosted box: the
# 20G was sized for a hosted runner's ~70G /mnt, and on a machine
# that keeps four output trees, the shared host-tool tree, dl/ and
# the ccaches it is low enough to let a build start and then hit
# ENOSPC hours in. Same figure the toolchain jobs use.
min-free-gb: '60'
# The seed ships host packages as stamps without sources, and the Swift
# packages read host-swift's build directory rather than only what it
# installed into host/. See the action for why that combination fails.
- name: Fetch the shared host-swift
uses: ./.github/actions/host-swift
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
build-dir: /mnt/br/output/${{ matrix.arch }}/build
# The companion is already on this machine. The lib32 job builds it into
# /mnt/br/profiles/lib32/<arch>, /mnt is bind-mounted into this
# container, so the image reads that tree where it lies.
#
# It used to travel as an Actions artifact - tar it up, upload, download,
# verify, untar - which is a 300 MB round trip through GitHub between two
# jobs on the same disk. The transfer is the only part of that which has
# ever gone wrong: once a truncated download (a good 225224255-byte
# artifact arriving short, so tar stopped three seconds into the build),
# once "BlobNotFound" on the upload. Both times a companion that had
# built perfectly was thrown away and the image never started. The
# artifact is still published for anyone who wants to fetch one; it is
# just no longer on the path between these two jobs.
#
# Reading it live is safe: this job "needs: lib32", so every companion
# has finished before any image starts, and post-build-lib32.sh only
# copies out of the tree.
- name: Locate the 32-bit companion
env:
# i386 goes with the x86_64 image, armv7 with arm64.
LIB32_ARCH: ${{ matrix.arch == 'x86_64' && 'i386' || 'armv7' }}
run: |
set -eu
root="/mnt/br/profiles/lib32/$LIB32_ARCH/target"
[ -d "$root" ] || {
echo "no $LIB32_ARCH companion tree at $root" >&2
echo "the lib32 job builds it - see that job's log" >&2
exit 1
}
# Without a 32-bit loader the merge produces libraries nothing can
# run, and post-build-lib32.sh only warns. An empty or half-built
# tree is worth stopping for here rather than shipping an image whose
# /usr/lib32 is decorative.
ls "$root"/lib/ld-*.so.* >/dev/null 2>&1 || {
echo "$root has no 32-bit loader in lib/ - tree is incomplete" >&2
exit 1
}
echo "companion: $root ($(du -sh "$root" | cut -f1))"
- name: Build image
run: |
set -eu
ccache -d "$CCACHE_DIR" --max-size="$CCACHE_MAX_SIZE" \
--set-config=compression=true --set-config=compiler_check=content || true
# Everything comes from the repo's submodules; the seeded tree at
# /mnt/br/output/<arch> supplies the toolchain and swift so only the
# image profile's delta compiles.
export BUILDROOT="$GITHUB_WORKSPACE/buildroot"
export BR2_EXTERNAL_SWIFT="$GITHUB_WORKSPACE/swift"
export BR2_EXTERNAL_PORTS="$GITHUB_WORKSPACE/ports"
export OUTPUT_BASE=/mnt/br/output
export IMAGE_OUTPUT_DIR="/mnt/br/output/${{ matrix.arch }}"
# Must match the seeded tree's BR2_DL_DIR, or its download stamps
# point at files that are not there.
export DL_DIR=/mnt/br/dl
# Merge the companion built by the lib32 job into /usr/lib32 rather
# than building one here (its toolchain lives in another tree).
export LIB32_ROOT=/mnt/br/profiles/lib32/${{ matrix.arch == 'x86_64' && 'i386' || 'armv7' }}/target
export CCACHE=1
export DEVICE="${{ inputs.device }}"
export REBUILD_PKGS="${{ inputs.rebuild }}"
export CLEAN_TREE="${{ inputs.clean && '1' || '' }}"
"$GITHUB_WORKSPACE/build-images.sh" ${{ matrix.arch }}
# Also on cancellation, not just failure. build-images.sh sends the build
# output to a file and this step is the only thing that surfaces it, so
# when the job was cancelled - by GitHub's six-hour ceiling, or by hand -
# "if: failure()" skipped it and the whole build left no trace at all.
- name: Dump build log on failure
if: ${{ failure() || cancelled() }}
run: |
log="/mnt/br/output/${{ matrix.arch }}-image.log"
echo "===== tail of $log ====="
tail -n 200 "$log" || echo "(no log found)"
- name: Show image
run: ls -la /mnt/br/output/${{ matrix.arch }}/images/ || true
- name: Upload disk image
uses: actions/upload-artifact@v4
with:
name: swift-linux-${{ inputs.device || matrix.arch }}-disk
# swift-linux-<arch>.img rather than the board's own name, because
# every board's post-image leaves this symlink pointing at whatever
# it built - disk.img on the UEFI boards, chromebook.img on the
# Chromebooks - and the upload resolves it. One glob, one file, no
# list of names to keep current. Globbing *.img instead would match
# the symlink and its target and upload the image twice.
path: /mnt/br/output/${{ matrix.arch }}/images/swift-linux-*.img
if-no-files-found: error
- name: ccache stats
if: always()
run: |
ccache -d "$CCACHE_DIR" -s 2>/dev/null || true
df -h /mnt
# The container runs as root; the workspace belongs to the runner user on
# the host and, unlike a hosted VM, survives the job. Without this the
# next run's actions/checkout git-cleans as the runner user and hits EPERM
# on the root-owned ports tree this job created.
- name: Return the workspace to the runner user
if: always()
run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true
# The 32-bit companion userland that gets merged into each image as
# /usr/lib32. It is built from its own architecture's seeded toolchain in
# its own tree; the image job then merges the target tree. i386 pairs with
# the x86_64 image, armv7 with arm64.
lib32:
name: lib32 (${{ matrix.arch }})
needs: resolve
strategy:
fail-fast: false
matrix:
arch: ${{ fromJSON(needs.resolve.outputs.lib32) }}
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 1440
# Same mount arrangement as the image job, one profile over.
container:
image: docker.io/colemancda/buildroot-swift:latest
options: >-
--volume /mnt:/mnt
--volume /mnt/br/profiles/lib32/${{ matrix.arch }}:/mnt/br/output/${{ matrix.arch }}
--security-opt label=disable
env:
CCACHE_DIR: /mnt/br/ccache/${{ matrix.arch }}
steps:
- name: Checkout swift-linux
uses: actions/checkout@v4
- name: Seed from the toolchain release
uses: ./.github/actions/br-seed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
arch: ${{ matrix.arch }}
dest-dir: /mnt/br/output/${{ matrix.arch }}
force: ${{ inputs.reseed && '1' || '' }}
- name: Prepare and configure the tree
uses: ./.github/actions/br-setup
with:
output-dir: /mnt/br/output/${{ matrix.arch }}
arch: ${{ matrix.arch }}
profile: lib32
ccache-dir: /mnt/br/ccache/${{ matrix.arch }}
ccache-max-size: ${{ env.CCACHE_MAX_SIZE }}
# Back to a real guard now this runs on the self-hosted box: the
# 20G was sized for a hosted runner's ~70G /mnt, and on a machine
# that keeps four output trees, the shared host-tool tree, dl/ and
# the ccaches it is low enough to let a build start and then hit
# ENOSPC hours in. Same figure the toolchain jobs use.
min-free-gb: '60'
# Same reason as the image job: the companion builds the Swift runtime
# for its architecture, so it needs the real host-swift tree.
- name: Fetch the shared host-swift
uses: ./.github/actions/host-swift
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
build-dir: /mnt/br/output/${{ matrix.arch }}/build
# "rebuild" reaches this tree too. It used to be the image build's alone,
# because build-images.sh is what reads REBUILD_PKGS and this job calls
# make directly - which left the companion with no repair short of
# reseeding it, and a companion is exactly where a wedged package hides:
# the image job reads this tree off the disk rather than rebuilding it.
#
# The names are filtered against the tree before use, because the two
# trees are configured differently and one "rebuild" list serves both.
# "linux" is the case that matters: the image profile has a kernel and
# the lib32 profile does not, so an unfiltered dirclean here dies on
# "No rule to make target 'linux-dirclean'" and takes the companion
# with it. A name this tree does not know is not an error - it is a
# name meant for the other one.
- name: Build the 32-bit companion
env:
REBUILD_PKGS: ${{ inputs.rebuild }}
run: |
set -eu
for pkg in ${REBUILD_PKGS:-}; do
if make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \
BR2_DL_DIR=/mnt/br/dl -n "$pkg-dirclean" >/dev/null 2>&1; then
echo "forcing rebuild of $pkg"
make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \
BR2_DL_DIR=/mnt/br/dl "$pkg-dirclean" >/dev/null
else
echo "$pkg is not in this tree's configuration, skipping"
fi
done
FORCE_UNSAFE_CONFIGURE=1 make -C "$BR_B" O="$BR_O" BR2_EXTERNAL="$BR_EXT" \
BR2_DL_DIR=/mnt/br/dl BR2_CCACHE_DIR="$CCACHE_DIR" -j"$BR_JOBS"
# Kept for anyone who wants a companion tree without access to this
# machine. The image job no longer reads it - it takes $BR_O/target
# directly off the shared disk - so neither of these steps is allowed to
# fail the job: "BlobNotFound" out of GitHub's artifact storage once
# failed this job minutes after the companion had built cleanly, and
# because the image job needs this one, that skipped both images and lost
# the run. A convenience copy should not be able to do that.
- name: Pack the companion target tree
continue-on-error: true
run: tar czf "$GITHUB_WORKSPACE/lib32-${{ matrix.arch }}.tar.gz" -C "$BR_O" target
- name: Upload companion target tree
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: lib32-${{ matrix.arch }}
path: lib32-${{ matrix.arch }}.tar.gz
if-no-files-found: error
- name: ccache stats
if: always()
run: |
ccache -d "$CCACHE_DIR" -s 2>/dev/null || true
df -h /mnt
# See the image job.
- name: Return the workspace to the runner user
if: always()
run: chown -R "$(stat -c '%u:%g' "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE" || true
# Fast machinery check without the Swift toolchain: proves the image config
# resolves and generate-config/build-images plumbing is intact on a plain
# runner. Does not build (that needs the container); config-only.
#
# Runs directly on the self-hosted machine rather than in a container, so it
# needs nothing but bash, sed and awk - which is all generate-config.sh uses.
config-check:
name: Config check (${{ inputs.device || matrix.arch }})
needs: resolve
strategy:
matrix:
arch: ${{ fromJSON(needs.resolve.outputs.arches) }}
# On the same runner as the rest, though it needs nothing from it. Left on
# the hosted pool it would be the one job that can still fail for reasons
# unrelated to the tree, and since nothing depends on it, that failure
# would only ever show up as a red run beside two good images.
#
# The cost is that it can queue behind an image build rather than
# answering in seconds, which blunts it as a fast gate.
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Generate image defconfig
env:
DEVICE: ${{ inputs.device }}
run: |
set -eu
# Same --frontend the build job uses, or this checks a defconfig
# nothing builds. "none" is the absence of a fragment, not a file,
# and generate-config.sh takes it as such.
# And the same board, so a device run checks the defconfig it is
# about to build rather than the generic UEFI one for the arch.
./generate-config.sh --arch ${{ matrix.arch }} --profile image \
${DEVICE:+--device "$DEVICE"} \
--frontend "$FRONTEND" -o /tmp/image.defconfig
echo "----- generated defconfig ($FRONTEND) -----"
grep -E '^BR2_(aarch64|x86_64|PACKAGE_(SWAYWM|FOOT|EMULATIONSTATION|STEAM|BOX64|MESA3D_GALLIUM_DRIVER_VIRGL)|TARGET_GRUB2_.*_EFI|INIT_BUSYBOX)' /tmp/image.defconfig || true
# minimal unwinds the game frontend and the translation layers. It
# does that by negating what earlier fragments turned on, so both
# lines are in the file and only the order decides the outcome:
# kconfig takes the last assignment, and "# X is not set" is how a
# defconfig spells n. Testing for the absence of "X=y" would pass
# on a file where the negation had been emitted first and lost.
#
# Checked here rather than by reading the resolved .config because
# this job has no submodules and so no kconfig to run - the
# ordering is the property that decides the result anyway.
last_state() {
grep -nE "^(# )?BR2_PACKAGE_$1(=y| is not set)\$" /tmp/image.defconfig \
| tail -1 | sed 's/^[0-9]*://'
}
if [ "$FRONTEND" = minimal ]; then
rc=0
# Hours of build apiece, and a different image if they came back.
for sym in EMULATIONSTATION RETROARCH BOX64 BOX86 WINE QEMU PORTMASTER; do
s=$(last_state "$sym")
case "$s" in
''|'# BR2_PACKAGE_'*' is not set') ;;
*) echo "minimal did not unwind BR2_PACKAGE_$sym (last: $s)" >&2; rc=1 ;;
esac
done
# Without these there is no session to boot into at all.
for sym in SWAYWM FOOT DEJAVU; do
[ "$(last_state "$sym")" = "BR2_PACKAGE_$sym=y" ] || {
echo "minimal is missing BR2_PACKAGE_$sym - no session" >&2; rc=1; }
done
[ "$rc" = 0 ] || exit 1
echo "minimal: sway session present, game frontend and translation layers unwound"
fi