'{"id":"foo*.conf"}')
}
+cleanup_link_auto() {
+ rm -rf /run/systemd/uki /etc/systemd/uki
+ if [[ -n "${LINK_WORKDIR:-}" ]]; then
+ rm -rf "$LINK_WORKDIR"
+ unset LINK_WORKDIR
+ fi
+ restore_esp
+}
+
+testcase_bootctl_link_auto() {
+ if ! command -v ukify >/dev/null; then
+ echo "ukify not found, skipping."
+ return 0
+ fi
+
+ backup_esp
+ LINK_WORKDIR="$(mktemp --directory /tmp/test-bootctl-link-auto.XXXXXXXXXX)"
+ trap cleanup_link_auto RETURN ERR
+
+ # Ensure loader/entries directory is present
+ bootctl install --make-entry-directory=yes
+
+ local ESP
+ ESP="$(bootctl --print-esp-path)"
+
+ cat >"$LINK_WORKDIR/os-release" <<'EOF'
+ID=testos
+NAME="Test OS"
+PRETTY_NAME="Test OS"
+EOF
+ echo "fake-kernel" >"$LINK_WORKDIR/vmlinuz"
+ echo "fake-initrd" >"$LINK_WORKDIR/initrd"
+
+ # Two distinct UKIs, so we can tell which one was picked up.
+ ukify build \
+ --linux "$LINK_WORKDIR/vmlinuz" \
+ --initrd "$LINK_WORKDIR/initrd" \
+ --os-release "@$LINK_WORKDIR/os-release" \
+ --uname "1.2.3-testkernel" \
+ --cmdline "quiet uki=a" \
+ --output "$LINK_WORKDIR/uki_a.efi"
+ ukify build \
+ --linux "$LINK_WORKDIR/vmlinuz" \
+ --initrd "$LINK_WORKDIR/initrd" \
+ --os-release "@$LINK_WORKDIR/os-release" \
+ --uname "1.2.3-testkernel" \
+ --cmdline "quiet uki=b" \
+ --output "$LINK_WORKDIR/uki_b.efi"
+
+ local TOKEN="systemdtest"
+ local BOOTCTL=(bootctl "--entry-token=literal:$TOKEN")
+ local ENTRY="$ESP/loader/entries/${TOKEN}-commit_1.conf"
+
+ # --- Test 1: link-auto picks up kernel.efi + extras.d/ from /run/systemd/uki/ ---
+ rm -rf /run/systemd/uki
+ mkdir -p /run/systemd/uki/extras.d
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi
+ echo "sysext-data" >/run/systemd/uki/extras.d/hello.sysext.raw
+ echo "confext-data" >/run/systemd/uki/extras.d/hello.confext.raw
+ echo "cred-data" >/run/systemd/uki/extras.d/hello.cred
+
+ "${BOOTCTL[@]}" link-auto
+
+ test -f "$ENTRY"
+ test -f "$ESP/$TOKEN/kernel.efi"
+ cmp "$LINK_WORKDIR/uki_a.efi" "$ESP/$TOKEN/kernel.efi"
+ test -f "$ESP/$TOKEN/hello.sysext.raw"
+ test -f "$ESP/$TOKEN/hello.confext.raw"
+ test -f "$ESP/$TOKEN/hello.cred"
+ grep "^uki /${TOKEN}/kernel.efi\$" "$ENTRY" >/dev/null
+ grep "^extra /${TOKEN}/hello.sysext.raw\$" "$ENTRY" >/dev/null
+ grep "^extra /${TOKEN}/hello.confext.raw\$" "$ENTRY" >/dev/null
+ grep "^extra /${TOKEN}/hello.cred\$" "$ENTRY" >/dev/null
+
+ "${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
+ test ! -e "$ENTRY"
+ test ! -e "$ESP/$TOKEN/kernel.efi"
+
+ # --- Test 2: versioned kernel.efi.v/ and extras .v/ are resolved via vpick ---
+ rm -rf /run/systemd/uki
+ mkdir -p /run/systemd/uki/kernel.efi.v /run/systemd/uki/extras.d/hello.sysext.raw.v
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi.v/kernel_1.0.efi
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi.v/kernel_2.0.efi
+ echo "sysext-1" >/run/systemd/uki/extras.d/hello.sysext.raw.v/hello_1.0.sysext.raw
+ echo "sysext-2" >/run/systemd/uki/extras.d/hello.sysext.raw.v/hello_2.0.sysext.raw
+
+ "${BOOTCTL[@]}" link-auto
+
+ test -f "$ENTRY"
+ # vpick must select the newest version
+ test -f "$ESP/$TOKEN/kernel_2.0.efi"
+ test ! -e "$ESP/$TOKEN/kernel_1.0.efi"
+ test -f "$ESP/$TOKEN/hello_2.0.sysext.raw"
+ test ! -e "$ESP/$TOKEN/hello_1.0.sysext.raw"
+ grep "^uki /${TOKEN}/kernel_2.0.efi\$" "$ENTRY" >/dev/null
+ grep "^extra /${TOKEN}/hello_2.0.sysext.raw\$" "$ENTRY" >/dev/null
+
+ "${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
+
+ # --- Test 3: priority — /etc/systemd/uki/ wins over /run/systemd/uki/ ---
+ rm -rf /run/systemd/uki /etc/systemd/uki
+ mkdir -p /run/systemd/uki/extras.d /etc/systemd/uki/extras.d
+ cp "$LINK_WORKDIR/uki_b.efi" /run/systemd/uki/kernel.efi
+ cp "$LINK_WORKDIR/uki_a.efi" /etc/systemd/uki/kernel.efi
+ echo "run-cred" >/run/systemd/uki/extras.d/hello.cred
+ echo "etc-cred" >/etc/systemd/uki/extras.d/hello.cred
+
+ "${BOOTCTL[@]}" link-auto
+
+ test -f "$ESP/$TOKEN/kernel.efi"
+ # The /etc copy (uki_a) must win over the /run copy (uki_b)
+ cmp "$LINK_WORKDIR/uki_a.efi" "$ESP/$TOKEN/kernel.efi"
+ cmp <(echo "etc-cred") "$ESP/$TOKEN/hello.cred"
+
+ "${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
+ rm -rf /etc/systemd/uki
+
+ # --- Test 4: with nothing staged, link-auto is a successful no-op ---
+ rm -rf /run/systemd/uki
+ "${BOOTCTL[@]}" link-auto
+ # No entries referencing our token should remain.
+ local leftover
+ leftover="$(find "$ESP/loader/entries/" -name "*$TOKEN*" -print -quit 2>/dev/null)"
+ test -z "$leftover"
+
+ # === Varlink coverage: io.systemd.BootControl.LinkAuto ===
+ local BOOTCTL_BIN vreply vid vtoken
+ BOOTCTL_BIN="$(type -p bootctl)"
+
+ # --- Test 5: LinkAuto discovers kernel.efi + extras ---
+ rm -rf /run/systemd/uki
+ mkdir -p /run/systemd/uki/extras.d
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi
+ echo "cred-data" >/run/systemd/uki/extras.d/hello.cred
+
+ vreply="$(varlinkctl call --json=short "$BOOTCTL_BIN" io.systemd.BootControl.LinkAuto '{}')"
+ vid="$(echo "$vreply" | jq -r '.ids[0]')"
+ test -n "$vid"
+ test "$vid" != "null"
+ vtoken="${vid%%-commit_*}"
+ test -n "$vtoken"
+
+ test -f "$ESP/loader/entries/$vid"
+ test -f "$ESP/$vtoken/kernel.efi"
+ test -f "$ESP/$vtoken/hello.cred"
+ grep "^uki /$vtoken/kernel.efi\$" "$ESP/loader/entries/$vid" >/dev/null
+
+ varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink "{\"id\":\"$vid\"}"
+ test ! -e "$ESP/loader/entries/$vid"
+ test ! -e "$ESP/$vtoken/kernel.efi"
+ test ! -e "$ESP/$vtoken/hello.cred"
+
+ # --- Test 6: LinkAuto with nothing staged returns an empty id list ---
+ rm -rf /run/systemd/uki
+ vreply="$(varlinkctl call --json=short "$BOOTCTL_BIN" io.systemd.BootControl.LinkAuto '{}')"
+ assert_eq "$(echo "$vreply" | jq -r '.ids | length')" "0"
+}
+
run_testcases