#!/bin/bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2023 SUSE LLC
set -e
shopt -s nullglob

# set to --keep-title to use altenate screen. Better for debugging but causes flicker
dialog_altenate_screen=
dialog_backtitle="Systemd-boot"
interactive=
verbose=
nl=$'\n'
shimdir="/usr/share/efi/$(uname -m)"
sdboot_vendor="systemd"
sdboot_dst="/EFI/$sdboot_vendor"
arg_esp_path="$SYSTEMD_ESP_PATH"
arg_entry_token=
arg_arch=
arg_all_entries=
arg_no_variables=
arg_no_reuse_initrd=
arg_no_random_seed=
# for x in vmlinuz image vmlinux linux bzImage uImage Image zImage; do
image=

# State file for transactional systems
state_file="/var/lib/misc/transactional-update.state"

update_predictions=

rollback=()

tmpdir=$(mktemp -d -t sdboot.XXXXXX)
cleanup()
{
	local i
	for i in "${rollback[@]}"; do
		if [ -e "$i.bak" ]; then
			log_info "restoring $i"
			mv "$i.bak" "$i"
		else
			log_info "removing $i"
			rm -f "$i"
		fi
	done
	rm -rf "$tmpdir"
}
trap cleanup EXIT

entryfile="$tmpdir/entries.json"
snapperfile="$tmpdir/snapper.json"
title_sort_file="$tmpdir/title_sort.txt"
tmpfile="$tmpdir/tmp"

helpandquit()
{
	cat <<-EOF
		Usage: $0 [OPTIONS] [COMMAND]
		OPTIONS:
		  --esp-path		Manually specify path to ESP
		  --arch		Manually set architecture
		  --entry-token		Override entry token
		  --image		Specify Linux kernel file name
		  --no-variables	Do not update UEFI variables
		  --no-reuse-initrd	Always regenerate initrd
		  -v, --verbose		More verbose output
		  -h, --help		This screen

		COMMAND:
		add-kernel VERSION [SNAPSHOT]
			   Create boot entry for specified kernel

		add-all-kernels [SNAPSHOT]
			   Create boot entries for all kernels in SNAPSHOT

		remove-kernel VERSION [SNAPSHOT]
			   Remove boot entry for specified kernel

		remove-all-kernels [SNAPSHOT]
			   Remove boot entries for all kernels in SNAPSHOT

		list-kernels [SNAPSHOT]
			   List all kernels related to SNAPSHOT

		list-entries [SNAPSHOT]
			   List all entries related to SNAPSHOT

		list-snapshots
			   List all snapshots

		set-default-snapshot [SNAPSHOT]
			   Make SNAPSHOT the default for next boot.
			   Also install all kernels if needed

		is-bootable [SNAPSHOT]
			   Check whether SNAPSHOT has any kernels registered, ie
			   is potentially bootable

		install    Install systemd-boot and shim into ESP
		needs-update
			   Check whether the bootloader in ESP needs updating
		update
			    Update the bootloader if it's old
		force-update
			    Update the bootloader in any case
		update-predictions
			    Update TPM2 predictions

		UI commands:
		kernels    Open kernel menu
		snapshots  Open snapshots menu
		entries    Open entry menu

	EOF
	exit 0
}

log_info()
{
	[ "${verbose:-0}" -gt 0 ] || return 0
	echo "$@"
}

d(){
	local retval=0
	# Bash makes it a bit annoying to read the output of a different FD into a variable, it
	# only supports reading stdout by itself. So redirect 3 to stdout and 1 to the real stdout.
	exec {stdoutfd}>&1
	result="$(dialog $dialog_altenate_screen --backtitle "$dialog_backtitle" --output-fd 3 "$@" 3>&1 1>&${stdoutfd})" || retval=$?
	# Word splitting makes it necessary to use eval here.
	eval "exec ${stdoutfd}>&-"
	return "$retval"
}

err()
{
	if [ "$interactive" = 1 ]; then
		d --title 'Error' --ok-label "Quit" --colors --aspect 60 --msgbox "\Z1Error:\Zn $*" 0 0
	else
		echo "Error: $*" >&2
	fi
	exit 1
}

warn()
{
	if [ "$interactive" = 1 ]; then
		d --title 'Warning' --ok-label "Continue" --colors --aspect 60 --msgbox "\Z1Warning:\Zn $*" 0 0
	else
		echo "Warning: $*" >&2
	fi
}

reset_rollback()
{
	for i in "${rollback[@]}"; do
		[ -e "$i.bak" ] || continue
		log_info "removing $i.bak"
		rm -f "$i.bak"
	done
	rollback=()
}

run_command_live_output()
{
	if [ "$interactive" = 1 ]; then
		"$@" 2>&1 | dialog $dialog_altenate_screen --backtitle "$dialog_backtitle" --title "$1" --aspect 60 --progressbox 0 0
	else
		"$@"
	fi
}

run_command_output()
{
	if [ "$interactive" = 1 ]; then
		"$@" > "$tmpfile" 2>&1
		[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
	else
		"$@"
	fi
}

# Given the number of total item pairs, outputs the number of items to display at once
menuheight() {
	local height=$(($1 / 2))
	[ "$height" -le "$dh_menu" ] || height="$dh_menu"
	echo "$height"
}

stty_size() {
	set -- $(stty size 2>/dev/null)
	LINES="$1"
	COLUMNS="$2"
	# stty size can return zero when not ready or
	# its a serial console
	if [ "$COLUMNS" = "0" ] || [ "$LINES" = "0" ]; then
		LINES=24
		COLUMNS=80
	fi

	dh_menu=$((LINES-15))
	dh_text=$((LINES-5))
}

# check whether it's a transactional system
is_transactional()
{
	[ "$(stat -f -c %T /etc)" = "overlayfs" ]
}

subvol_is_ro()
{
	local subvol="${1:?}"
	while read -r line; do
		[ "$line" = "ro=true" ] && return 0
	done < <(btrfs prop get -t s "${subvol#${subvol_prefix}}" ro)
	return 1
}

detect_parent() {
	local subvol="$1"
	parent_uuid="$(btrfs subvol show "${subvol#${subvol_prefix}}" | sed -ne 's/\s*Parent UUID:\s*//p')"
	[ "$parent_uuid" != '-' ] || parent_uuid=
	[ -n "$parent_uuid" ] || return 0
	parent_subvol="$(/sbin/btrfs subvol show -u "$parent_uuid" "${subvol#${subvol_prefix}}" | head -1)"
	parent_snapshot="${parent_subvol#${subvol_prefix}/.snapshots/}"
	if [ "$parent_subvol" = "$parent_snapshot" ]; then
		unset parent_subvol parent_snapshot
	else
		parent_snapshot="${parent_snapshot%/snapshot}"
	fi
}

sedrootflags()
{
	local subvol="${1:?}"
	# - delete BOOT_IMAGE= and initrd=
	# - make sure root= refers to uuid
	# - replace or add rootflags to point at correct subvolume
	sed -e "s/[ \t]\+/ /g;s/\<\(BOOT_IMAGE\|initrd\)=[^ ]* \?//;s/\<root=[^ ]*/root=UUID=$root_uuid/;tr;:r;s,\<rootflags=subvol=[^ ]*,rootflags=subvol=$subvol,;tx; s,\$, rootflags=subvol=$subvol,;:x"
}

remove_kernel()
{
	local snapshot="${1:?}"
	local subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	local kernel_version="$2"
	local id="$entry_token-$kernel_version-$snapshot.conf"
	run_command_output bootctl unlink "$id"

	# This action will require to update the PCR predictions
	update_predictions=1
}

install_with_rollback()
{
	local src="${1:?}"
	local dst="${2:?}"

	if [ -e "$dst" ]; then
		if cmp -s "$src" "$dst"; then
			log_info "$dst unchanged"
			return 0
		fi
		mv "$dst" "$dst.bak" || return "$?"
	fi
	rollback+=("$dst")
	install -m 0644 "$src" "$dst" || return "$?"
	chown root:root "$dst" 2>/dev/null || :
	log_info "installed $dst"
}

update_snapper()
{
    snapper --jsonout --no-dbus list --disable-used-space > "$snapperfile"
}

set_snapper_title_and_sortkey()
{
	snapshot="${1:?}"
	local type date desc important pre_num
	local snapshot_info

	update_snapper

	IFS="|" read -r type date desc important pre_num <<< \
		$(jq -r --arg snapshot "$snapshot" \
		'.["root"][]|select(.number==( $snapshot|tonumber))|[.type,.date,(.description|gsub("\\|";"_")),.userdata.important,."pre-number"//""]|join("|")'\
		< "$snapperfile")

	if [ -z "$desc" ] && [ "$type" = "post" ] && [ -n "$pre_num" ]; then
		read -r desc <<< $(jq -r --arg snapshot "$pre_num" '.["root"][]|select(.number==($snapshot|tonumber))|.description' < "$snapperfile")
	fi

	if [ "$important" = "yes" ]; then important="*"; else important=""; fi
	[ "$type" = "single" ] && type=""
	snapshot_info="$snapshot,$kernel_version,$date${type:+, $type}${desc:+, $desc}"

	# shellcheck disable=SC2154
	title="Snapper: ${important}$title ($snapshot_info)"
	sort_key="snapper-$sort_key"
}

reuse_initrd() {
	local snapshot="${1:?}"
	local subvol="${2:?}"

	[ -z "$arg_no_reuse_initrd" ] || return 1

	local conf="$boot_root/loader/entries/$entry_token-$kernel_version-$snapshot.conf"
	if [ -e "$conf" ]; then
		local k
		local v
		while read -r k v; do
			[ "$k" = 'initrd' ] || continue
			log_info "found existing initrd $v"
			dstinitrd+=("$v")
		done < "$conf"
		[ -z "$dstinitrd" ] || return 0
	fi

	# check if we can reuse the initrd from the parent
	# to avoid expensive regeneration
	detect_parent "$subvol"
	local parent_conf="$boot_root/loader/entries/$entry_token-$kernel_version-$parent_snapshot.conf"
	if [ -n "$parent_subvol" ] && [ -e "$parent_conf" ]; then
		#subvol_is_ro "$parent_subvol" || err "Parent snapshot $parent_snapshot is not read-only, can't reuse initrd"
		local k
		local v
		while read -r k v; do
			[ "$k" = 'initrd' ] || continue
			log_info "found parent initrd $v"
			dstinitrd+=("$v")
		done < "$parent_conf"
		[ -z "$dstinitrd" ] || return 0
	fi

	return 1
}

mount_etc()
{
	local snapshot_dir="$1"

	IFS=',' read -ra fields <<< \
	   $(findmnt --tab-file "${snapshot_dir}/etc/fstab" --noheadings --nofsroot --output OPTIONS /etc | sed 's#/sysroot##g' | sed 's#:/etc,#:'${snapshot_dir}'/etc,#g')

	local lower=""
	local upper=""
	for element in "${fields[@]}"; do
		IFS='=' read -r key value <<< "$element"
		[ "$key" = "lowerdir" ] && lower="$value"
		[ "$key" = "upperdir" ] && upper="$value"
	done

	mount overlay -t overlay -o ro,"lowerdir=${upper}:${lower}" "${snapshot_dir}/etc"
}

umount_etc()
{
	local snapshot_dir="$1"
	umount "${snapshot_dir}/etc"
}

add_version_to_title()
{
	# TW pretty name does not include the version
	[ -n "$os_release_VERSION" ] || title="$title $os_release_VERSION_ID"
}

install_kernel()
{
	local snapshot="${1:?}"
	local subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	local kernel_version="$2"
	local initrd=()
	local dstinitrd=()
	local src="${subvol#${subvol_prefix}}/lib/modules/$kernel_version/$image"
	local initrddir="${subvol#${subvol_prefix}}/usr/lib/initrd"
	test -e "$src" || err "Can't find $src"

	calc_chksum "$src"
	local dst="/$entry_token/$kernel_version/linux-$chksum"

	local initrd="${src%/*}/initrd"

	mkdir -p "$boot_root${dst%/*}"

	if [ -e "$initrd" ]; then
		ln -s "$initrd" "$tmpdir/initrd-0"
	elif [ -d "$initrddir" ] && [ -x "/usr/bin/mkmoduleinitrd" ]; then
		local f i
		i=0
		for f in "$initrddir"/*; do
			ln -s "$f" "$tmpdir/initrd-$i"
			((++i))
		done
		/usr/bin/mkmoduleinitrd "${subvol#"${subvol_prefix}"}" "$kernel_version" "$tmpdir/initrd-$i"
	elif ! reuse_initrd "$snapshot" "$subvol"; then
		local snapshot_dir="/.snapshots/$snapshot/snapshot"
		local dracut_args=()
		if [ "$subvol" != "$root_subvol" ]; then
			dracut_args=('--sysroot' "${snapshot_dir}" '--tmpdir' '/var/tmp' '--add-device' "$root_device")
		fi
		log_info "generating new initrd"

		# In MicroOS we need to be sure to have the same /etc
		# inside the snapshot.  For example, /etc/crypttab can
		# have modifications in the overlay that will be
		# visible once the snapshot is active, but the version
		# in /.snashots is still the unmodified base
		is_transactional && mount_etc "${snapshot_dir}"
		run_command_live_output dracut --quiet --reproducible "${dracut_args[@]}" "$tmpdir/initrd-0" "$kernel_version"
		is_transactional && umount_etc "${snapshot_dir}"
	fi

	local boot_options=
	for i in /etc/kernel/cmdline /usr/lib/kernel/cmdline /proc/cmdline; do
		[ -f "$i" ] || continue
		boot_options="$(sedrootflags "$subvol" < "$i")"
		break
	done

	if [ -z "$dstinitrd" ] && [ -e "$tmpdir/initrd-0" ]; then
		i=0
		while [ -e "$tmpdir/initrd-$i" ]; do
			calc_chksum "$tmpdir/initrd-$i"
			dstinitrd+=("${dst%/*}/initrd-$chksum")
			((++i))
		done
	fi

	title="${os_release_PRETTY_NAME:-Linux $kernel_version}"
	# shellcheck disable=SC2154
	sort_key="$os_release_ID"

	if is_transactional; then
		add_version_to_title
	elif subvol_is_ro "$subvol"; then
		add_version_to_title
		set_snapper_title_and_sortkey "$snapshot"
	fi

	local entry_machine_id=
	[ "$entry_token" = "$machine_id" ] && entry_machine_id="$machine_id"

	cat > "$tmpdir/entry.conf" <<-EOF
	# Boot Loader Specification type#1 entry
	title      $title
	version    $snapshot@$kernel_version${entry_machine_id:+${nl}machine-id $entry_machine_id}${sort_key:+${nl}sort-key   $sort_key}
	options    $boot_options
	linux      $dst
	EOF
	for i in "${dstinitrd[@]}"; do
		echo "initrd     $i" >> "$tmpdir/entry.conf"
	done

	local failed=
	if [ ! -e "$boot_root$dst" ]; then
		install_with_rollback "$src" "$boot_root$dst" || failed=kernel
	else
		log_info "reusing $boot_root$dst"
	fi
	if [ -z "$failed" ] && [ -e "$tmpdir/initrd-0" ]; then
		i=0
		while [ -e "$tmpdir/initrd-$i" ]; do
			if [ ! -e "$boot_root${dstinitrd[$i]}" ]; then
				install_with_rollback "$tmpdir/initrd-$i" "$boot_root${dstinitrd[$i]}" || { failed=initrd; break; }
			fi
			((++i))
		done
	fi
	if [ -z "$failed" ]; then
		loader_entry="$boot_root/loader/entries/$entry_token-$kernel_version-$snapshot.conf"
		install_with_rollback "$tmpdir/entry.conf" "$loader_entry" || failed="bootloader entry"
	fi
	rm -f "$tmpdir/initrd"
	rm -f "$tmpdir/entry.conf"
	[ -z "$failed" ] || err "Failed to install $failed"
	reset_rollback

	# This action will require to update the PCR predictions
	update_predictions=1
}

install_all_kernels()
{
	local snapshot="${1:?}"
	find_kernels "$snapshot"
	for kv in "${!found_kernels[@]}"; do
		log_info "installing $kv"
		install_kernel "${snapshot}" "$kv"
	done

}

remove_all_kernels()
{
	local snapshot="${1:?}"
	find_kernels "$snapshot"
	for kv in "${!found_kernels[@]}"; do
		remove_kernel "${snapshot}" "$kv"
	done

}

entry_filter=("cat")
update_entries()
{
	[ -z "$1" ] || entry_filter=("$@")
	bootctl list --json=short | "${entry_filter[@]}" > "$entryfile"
}

update_entries_for_subvol()
{
	local subvol="$1"
	update_entries jq "[.[]|select(has(\"options\"))|select(.options|match(\"root=UUID=$root_uuid .*rootflags=subvol=$subvol\"))]"
}

update_entries_for_snapshot()
{
	local n="$1"
	update_entries_for_subvol "${subvol_prefix}/.snapshots/$n/snapshot"
}

update_entries_for_this_system()
{
	update_entries jq "[.[]|select(has(\"options\"))|select(.options|match(\"root=UUID=$root_uuid\"))]"
}

list_entries()
{
	if [ ! -s "$entryfile" ]; then
		if [ -n "$1" ]; then
			update_entries_for_snapshot "$1"
		elif [ -n "$arg_all_entries" ]; then
			update_entries
		else
			update_entries_for_this_system
		fi
	fi

	local isdefault isreported type id root conf title
	while read -r isdefault isreported type id root conf title; do
		color=
		if [ "$isdefault" = "true" ]; then
			color="\e[1;4m"
		fi
		if [ "$isreported" = "false" ]; then
			color="$color\e[32m"
		fi
		if [ "$type" = "loader" ]; then
			color="$color\e[33m"
		fi
		local errors=()
		if [ -n "$verbose" ] && [ -n "$conf" ] && [  -e "$conf" ]; then
			local k
			local v
			while read -r k v; do
				if [ "$k" = 'linux' ] || [ "$k" = 'initrd' ] ; then
					if [ ! -e "$root$v" ]; then
						errors+=("$root/$v does not exist")
					fi
				fi
				if [ "$k" = 'options' ]; then
					local snapshot
					read -r snapshot <<<"$(echo "$v" | sed -e "s,.*rootflags=subvol=${subvol_prefix}/.snapshots/\([0-9]\+\)/snapshot.*,\1,")"
					if [ ! -d "/.snapshots/$snapshot/snapshot" ]; then
						errors+=("/.snapshot/$snapshot/snapshot does not exist")
					fi
				fi
			done < "$conf"
		fi
		if [ -n "$errors" ]; then
			echo -e "  \e[31m${errors[*]}\e[m" >&2
		fi
		echo -e "$color$id${verbose:+: $title}\e[m"
	done < <(jq '.[]|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .id, .root, .path, .showTitle]|join(" ")' -r < "$entryfile")
}

show_entries()
{
	local dialogtitle="${1:-Entries}"

	[ -s "$entryfile" ] || update_entries_for_this_system

	while true; do
		local list=()
		local n=0
		local default=
		while read -r isdefault isreported type title; do
			color=
			if [ "$isdefault" = "true" ]; then
				default="$n"
				color="\\Zb\Zu"
			fi
			if [ "$isreported" = "false" ]; then
				color="$color\\Z2"
			fi
			if [ "$type" = "loader" ]; then
				color="$color\\Z5"
			fi
			list+=("$n" "$color$title\\Zn")
			n=$((++n))
		done < <(jq '.[]|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .showTitle]|join(" ")' -r < "$entryfile")
		if [ "${#list}" = 0 ]; then
			d --msgbox "No entries" 0 0
			return 0
		fi
		local d_args=(--no-hot-list --colors --ok-label "Options" --cancel-label "Back")
		[ -n "$arg_all_entries" ] || d_args+=(--extra-button --extra-label "All")
		d "${d_args[@]}" --menu "$dialogtitle" 0 0 "$(menuheight ${#list[@]})" "${list[@]}"  || {
			if [ "$?" = 3 ]; then
				arg_all_entries=1
				update_entries cat
				continue
			fi
			return 0
		}
		n="$result"

		show_entry ".[$n]"
	done
}

show_entry()
{
	local filter="$1"
	local type
	local isreported
	local isdefault
	local new=

	read -r isdefault isreported type title < <(jq "$filter"'|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .showTitle]|join(" ")' -r < "$entryfile")

	[ "$isdefault" = true ] || isdefault=
	[ "$isreported" = true ] || new=1

	if [ -n "$isdefault$new" ]; then
		title="$title ["
		[ -z "$isdefault" ] || title="${title}default"
		[ -z "$new" ] || title="${title}${isdefault:+,}new"
		title="$title]"
	fi

	while true; do
		local list=(show json)
		if [ "$type" = "type1" ]; then
			list+=(cat Raw edit Edit)
		fi
		if [ -z "$isdefault" ]; then
			list+=(set-default "set as default" oneshot "set as one-shot")
			if [ "$type" != "loader" ]; then
				list+=(delete delete)
			fi
		fi
		d --no-tags --menu "Entry #$title" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
		action="$result"

		case "$action" in
			show)
				jq "$filter" < "$entryfile" > "$tmpfile"
				d --textbox "$tmpfile" 0 0
				;;
			cat)
				read -r fn < <(jq -r "$filter|.path" < "$entryfile")
				d --textbox "$fn" 0 0
				;;
			edit)
				read -r fn < <(jq -r "$filter|.path" < "$entryfile")
				${EDITOR:-vim} "$fn"
				update_entries
				;;
			delete)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				bootctl unlink "$id" > "$tmpfile" 2>&1
				[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
				update_entries
				break
				;;
			set-default)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				set_default_entry "$id"
				update_entries
				break
				;;
			oneshot)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				bootctl set-oneshot "$id" > "$tmpfile" 2>&1
				[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
				update_entries
				break
				;;
		esac
	done
}

list_snapshots()
{
	update_snapper 2>"$tmpfile" || err "$(cat "$tmpfile")"

	local n=0
	while read -r n isdefault title; do
		[ "$n" != "0" ] || continue
		local id="$n"
		if [ "$isdefault" = "true" ]; then
			id="\e[1;4m$id\e[m"
		fi
		update_kernels "${subvol_prefix}/.snapshots/$n/snapshot"
		[ "$is_bootable" = 1 ] || id="!$id"
		echo -e "$id $title"
	done < <(jq '.root|.[]|[.number, .default, .description]|join(" ")' -r < "$snapperfile")
}

show_snapper()
{
	if ! update_snapper 2>"$tmpfile"; then
		d --title "Error" --textbox "$tmpfile" 0 0
		exit 1
	fi

	while true; do
		local list=()
		local n=0
		local default=
		while read -r n isdefault title; do
			[ "$n" != "0" ] || continue
			if [ "$isdefault" = "true" ]; then
				default="$n"
				title="\\Zb\Zu$title\\Zn"
			fi
			update_kernels "${subvol_prefix}/.snapshots/$n/snapshot"
			[ "$is_bootable" = 1 ] || title="!$title"
			list+=("$n" "$title")
		done < <(jq '.root|.[]|[.number, .default, .description]|join(" ")' -r < "$snapperfile")
		if [ "${#list}" = 0 ]; then
			d --msgbox "No snapshots" 0 0
			return 0
		fi
		d --no-hot-list --colors --ok-label "Options" --cancel-label "Back" --menu "Snapshots" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		n="$result"

		while true; do
			list=(kernels kernels entries entries show json)
			if [ "$n" != "$default" ]; then
				list+=(delete delete)
			fi
			d --no-tags --menu "Snapshot #$n" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
			action="$result"

			case "$action" in
				show)
					jq ".root|.[]|select(.number==$n)" < "$snapperfile" > "$tmpfile"
					d --textbox "$tmpfile" 0 0
					;;
				entries)
					#read -r MACHINE_ID < /etc/machine-id
					#update_entries jq "[.[]|select(.machineId==\"$MACHINE_ID\")|select(has(\"options\"))|select(.options|match(\"${subvol_prefix}/.snapshots/$n/snapshot\"))]"
					update_entries_for_snapshot "$n"
					show_entries "Entries for Snapshot $n"
					;;
				kernels)
					show_kernels "$n"
					;;
			esac
		done
	done
}

calc_chksum() {
    # shellcheck disable=SC2046
    set -- $(sha1sum "$1")
    chksum="$1"
}

# map with kernel version as key and checksum as value
declare -A found_kernels
find_kernels()
{
	local subvol="${subvol_prefix}/.snapshots/${1:?}/snapshot"
	local fn kv
	found_kernels=()

	for fn in "${subvol#${subvol_prefix}}"/usr/lib/modules/*/"$image"; do
		kv="${fn%/*}"
		kv="${kv##*/}"
		calc_chksum "$fn"
		found_kernels["$kv"]="$chksum"
		log_info "found kernel $kv = $chksum"
	done
}

# map that uses expected path on the ESP for each installed kernel as key. The
# value is the entry id if an entry exists.
declare -A installed_kernels
# map of ESP path to id of kernels that are not in the subvol
declare -A stale_kernels
is_bootable=
update_kernels()
{
	local subvol="${1:?}"
	local path id
	installed_kernels=()
	stale_kernels=()
	is_bootable=
	find_kernels "$subvol"
	for kv in "${!found_kernels[@]}"; do
		installed_kernels["/$entry_token/$kv/linux-${found_kernels[$kv]}"]=''
	done
	update_entries_for_subvol "$subvol"

	# XXX: maybe we should parse the actual path in the entry
	while read -r path id; do
		if [ "${installed_kernels[$path]+none}" = 'none' ]; then
			installed_kernels["$path"]="$id"
			is_bootable=1
		else
			# kernel in ESP that is not installed
			stale_kernels["$path"]="$id"
		fi
	done < <(jq -r '.[]|select(has("linux"))|[.linux,.id]|join(" ")'< "$entryfile")
}

list_kernels()
{
	subvol="${subvol_prefix}/.snapshots/${1:?}/snapshot"
	update_kernels "$subvol"
	local kernelfiles=("${!installed_kernels[@]}")
	for k in "${kernelfiles[@]}"; do
		local id="${installed_kernels[$k]}"
		local kv="${k%/*}"
		kv="${kv##*/}"
		if [ -z "$id" ]; then
			echo -e "\e[33mmissing /lib/modules/$kv/$image\e[m"
		else
			echo "ok /lib/modules/$kv/$image -> $id"
		fi
	done
	kernelfiles=("${!stale_kernels[@]}")
	for k in "${kernelfiles[@]}"; do
		local id="${stale_kernels[$k]}"
		printf "\e[31mstale %s\e[m\n" "$id"
	done
}

is_bootable()
{
	subvol="${subvol_prefix}/.snapshots/${1:?}/snapshot"
	update_kernels "$subvol"

	[ "$is_bootable" = 1 ] || return 1
	return 0
}

show_kernels()
{
	local snapshot="${1:?}"
	subvol="${subvol_prefix}/.snapshots/${1:?}/snapshot"
	while true; do
		update_kernels "$subvol"
		local list=()
		local n=0
		local default=
		local id
		local kernelfiles=("${!installed_kernels[@]}" "${!stale_kernels[@]}")
		local ids=()
		for k in "${kernelfiles[@]}"; do
			if [  "${installed_kernels[$k]+yup}" = yup ]; then
				id="${installed_kernels[$k]}"
				if [ -z "$id" ]; then
					state="missing"
				else
					state="ok"
				fi
			else
				id="${stale_kernels[$k]}"
				state='stale'
			fi
			ids+=("$id")
			s="${k#/*/}"
			list+=("$n" "$(printf "%-10s %s" "$state" "$s")")
			n=$((++n))
		done
		if [ "${#list}" = 0 ]; then
			d --msgbox "No kernels" 0 0
			return 1
		fi
		d --no-tags --no-hot-list --colors --ok-label "Options" --cancel-label "Back" --menu "Kernels associated with $subvol" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		n="$result"

		list=()
		id="${ids[$n]}"
		if [ -z "$id" ]; then
			list+=(install "Install")
		else
			list+=(show "Entry")
		fi
		list+=(entries "Other Entries")

		local kv="${kernelfiles[$n]%/*}"
		kv="${kv##*/}"
		local title="Kernel $kv"
		while true; do
			d --no-tags --no-hot-list --colors --ok-label "Ok" --cancel-label "Back" --menu "$title" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
			action="$result"

			case "$action" in
				entries)
					update_entries jq "[.[]|select(has(\"linux\"))|select(.linux|match(\"${kernelfiles[$n]}\"))]"
					show_entries "Entries for kernel ${kernelfiles[$n]#/*/}"
					;;
				show)
					show_entry ".[]|select(.id|match(\"$id\"))"
					break # might have selected delete so refresh
					;;
				install)
					install_kernel "$snapshot" "$kv"
					break
					;;
			esac
		done
	done
}

sdboot_version()
{
	local fn="$1"
	if [ -z "$1" ]; then
		if [ -e "$shimdir/shim.efi" ]; then
			fn="$boot_root$sdboot_dst/grub.efi"
		else
			local sdboot
			sdboot="$(find_sdboot)"
			fn="$boot_root$sdboot_dst/${sdboot##*/}"
		fi
	fi
	[ -e "$fn" ] || return 1
	read -r _ _ _ v _ < <(grep -ao '#### LoaderInfo: systemd-boot [^#]\+ ####' "$fn")
	[ -n "$v" ] || return 1
	echo "$v"
}

is_installed()
{
	sdboot_version > /dev/null && [ -e "$boot_root/$sdboot_dst/installed_by_sdbootutil" ]
}

find_sdboot()
{
	local prefix="/.snapshots/${1-$root_snapshot}/snapshot"
	# XXX: this is a hack in case we need to inject a signed
	# systemd-boot from a separate package
	local sdboot="$prefix/usr/lib/systemd-boot/systemd-boot$firmware_arch.efi"
	[ -e "$sdboot" ] || sdboot="$prefix/usr/lib/systemd/boot/efi/systemd-boot$firmware_arch.efi"
	[ -e "$sdboot" ] || err "missing $sdboot"
	echo "$sdboot"
}

sdboot_needs_update()
{
	local snapshot="${1-$root_snapshot}"
	local prefix="/.snapshots/${snapshot}/snapshot"
	local v nv
	v="$(sdboot_version)"
	[ -n "$v" ] || return 1
	log_info "deployed version $v"
	nv="$(sdboot_version "$(find_sdboot "$snapshot")")"
	[ -n "$v" ] || return 1
	log_info "system version $nv"
	systemd-analyze compare-versions "$v" lt "$nv" 2>/dev/null || return 1
	log_info "systemd-boot needs to be updated"
	return 0
}

install_sdboot()
{
	local snapshot="${1:-$root_snapshot}"
	local prefix="/.snapshots/${root_snapshot}/snapshot"
	local sdboot blkpart drive partno

	sdboot=$(find_sdboot "$1")

	mkdir -p "$boot_root/loader/entries"

	mountpoint -q "$boot_root" || err "$boot_root is not a valid mountpoint"
	blkpart="$(findmnt -nvo SOURCE "$boot_root")"
	[ -L "/sys/class/block/${blkpart##*/}" ] || err "$blkpart is not a partition"
	drive="$(readlink -f "/sys/class/block/${blkpart##*/}")"
	drive="${drive%/*}"
	drive="/dev/${drive##*/}"
	read -r partno < "/sys/class/block/${blkpart##*/}"/partition

	if [ -e "$prefix$shimdir/shim.efi" ]; then
		log_info "Installing systemd-boot with shim into $boot_root"
		entry="$sdboot_dst/shim.efi"
		for i in MokManager shim; do
			install -D "$prefix$shimdir/$i.efi" "$boot_root$sdboot_dst/$i.efi"
		done
		install -D "$sdboot" "$boot_root$sdboot_dst/grub.efi"

		# boot entry point
		for i in MokManager fallback; do
			install -D "$prefix$shimdir/$i.efi" "$boot_root/EFI/BOOT/$i.efi"
		done
		install -D "$prefix$shimdir/shim.efi" "$boot_root/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	else
		log_info "Installing systemd-boot into $boot_root"
		entry="$sdboot_dst/${sdboot##*/}"
		install -D "$sdboot" "$boot_root$entry"
		install -D "$sdboot" "$boot_root/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	fi
	# this is for shim to create the entry if missing
	echo "${entry##*/},openSUSE Boot Manager" | iconv -f ascii -t ucs2 > "$boot_root/$sdboot_dst/boot.csv"

	mkdir -p "$boot_root/$entry_token"
	echo "$entry_token" > "$boot_root/$sdboot_dst/installed_by_sdbootutil"
	mkdir -p "/etc/kernel"
	[ -s /etc/kernel/entry-token ] || echo "$entry_token" > /etc/kernel/entry-token
	update_random_seed

	[ -s "$boot_root/loader/entries.srel" ] || echo type1 > "$boot_root/loader/entries.srel"

    	[ -e "$boot_root/loader/loader.conf" ] || echo -e "#timeout 3\n#console-mode keep\n" > "$boot_root/loader/loader.conf"

	# Create boot menu entry if it does not exist
	[ -n "$arg_no_variables" ] || efibootmgr | grep -q 'Boot.*openSUSE Boot Manager' || efibootmgr -q --create --disk "$drive" --part "$partno" --label "openSUSE Boot Manager" --loader "$entry" || true

	# This action will require to update the PCR predictions
	update_predictions=1
}

settle_entry_token()
{
	case "$arg_entry_token" in
		"") [ -n "$entry_token" ] || entry_token="$machine_id" ;;
		auto)
			if [ -s '/etc/kernel/entry-token' ]; then
				read -r entry_token < '/etc/kernel/entry-token'
			else
				entry_token="$machine_id"
				# bootctl has more here in case the machine id
				# is random falls back to trying IMAGE_ID and
				# ID, only them machine id. We assume there is
				# a valid machine-id
			fi
			;;
		machine-id) entry_token="$machine_id" ;;
		os-id)
			# shellcheck disable=SC2154
			entry_token="$os_release_ID"
			[ -n "$entry_token" ] || err "Missing ID"
			;;
		os-image)
			# shellcheck disable=SC2154
			entry_token="$os_release_IMAGE_ID"
			[ -n "$entry_token" ] || err "Missing IMAGE_ID"
			;;
		*) entry_token="$arg_entry_token" ;;
	esac
	return 0
}

hex_to_binary()
{
	local s="$1"
	local i
	for ((i=0;i<${#s};i+=2)); do eval echo -n "\$'\x${s:$i:2}'"; done
}

update_random_seed()
{
	[ -z "$arg_no_random_seed" ] || return 0
	local s _p
	read -r s _p < <({ dd if=/dev/urandom bs=32 count=1 status=none; [ -e "$boot_root/loader/random-seed" ] && dd if="$boot_root/loader/random-seed" bs=32 count=1 status=none; } | sha256sum)
	[ "${#s}" = 64 ] || { warn "Invalid random seed"; return 0; }
	hex_to_binary "$s" > "$boot_root/loader/random-seed.new"
	mv "$boot_root/loader/random-seed.new" "$boot_root/loader/random-seed"
}

install_sdboot_interactive()
{
	local v
	v="$(sdboot_version)"
	if [ -n "$v" ]; then
		if sdboot_needs_update; then
			local nv
			nv="$(sdboot_version "$(find_sdboot)")"
			d --aspect 60 --yesno "Update systemd-boot from $v to $nv?" 0 0 || return 0
		else
			d --aspect 60 --yesno "systemd-boot already at current version $v. Install again?" 0 0 || return 0
		fi
	else
		d --aspect 60 --yesno "Are you sure you want to install systemd-boot into $boot_root?\n
			This will overwrite any existing bootloaders" 0 0 || return 0
	fi
	install_sdboot
	d --aspect 60 --msgbox "Installed into $boot_root" 0 0
}

set_default_entry()
{
	local id="${1:?}"
	log_info "setting default entry ${id}"
	if ! bootctl set-default "$id" > "$tmpfile" 2>&1; then
		if grep -q "Failed to update EFI variable .*LoaderEntryDefault.* Read-only file system" "$tmpfile"; then
			if grep -q "^default " "$boot_root/loader/loader.conf"; then
				sed -i -e "s/^default .*/default $id/" "$boot_root/loader/loader.conf"
			else
				echo "default $id" >> "$boot_root/loader/loader.conf"
			fi
		else
			err "$(cat "$tmpfile")"
		fi
	fi
}

set_default_snapshot()
{
	local num="${1:?}"
	local configs
	update_entries_for_snapshot "$num"
	mapfile configs < <(jq '.[]|[.id]|join(" ")' -r < "$entryfile")
	configs=("${configs[@]%$nl}")
	if [ -z "${configs[0]}" ]; then
		log_info "snapshot $num has no configs, trying to create them..."
		install_all_kernels "$num"
		update_entries_for_snapshot "$num"
		mapfile configs < <(jq '.[]|[.id]|join(" ")' -r < "$entryfile")
		configs=("${configs[@]%$nl}")
		if [ -z "${configs[0]}" ]; then
			err "snapshot $num has no kernels"
		fi
	fi
	set_default_entry "${configs[0]}"
}

# TODO: Maybe share this code outside
have_pcrlock() {
	[ -e /usr/lib/systemd/systemd-pcrlock ]
}

pcrlock() {
	SYSTEMD_LOG_LEVEL="${SYSTEMD_LOG_LEVEL:-warning}" /usr/lib/systemd/systemd-pcrlock "$@"
}

# TODO: Maybe share this code outside
is_pcr_oracle() {
	[ -e /etc/systemd/tpm2-pcr-public-key.pem ] && \
	    [ -e /etc/systemd/tpm2-pcr-private-key.pem ] && \
	    [ -e /usr/bin/pcr-oracle ]
}

# TODO: Maybe share this code outside
select_entries_for_prediction()
{
	# Get the numbers for the last three snapshots
	[ -s "$snapperfile" ] || update_snapper

	# Select the default and the active snapshots
	declare -A snapshots
	local id
	while read -r id; do
	    snapshots[$id]=1
	done < <(jq -r '.root[]|select(.active==true or .default==true)|.number' "$snapperfile")

	if is_transactional && [ -e "${state_file}" ]; then
	    . "${state_file}"
	    for id in $LAST_WORKING_SNAPSHOTS; do
		snapshots[$id]=1
	    done
	fi

	log_info "Creating predictions for snapshots: ${!snapshots[*]}"
	local re
	if [ "${#snapshots[@]}" = 1 ]; then
	    re="${!snapshots[*]}"
	else
	    IFS='|' eval re='"(:?${!snapshots[*]})"'
	fi

	update_entries_for_snapshot "$re"
}

# TODO: Maybe share this code outside
pcrlock_cmdline_initrd() {
	local cmdline="$1"
	local initrd="$2"
	local suffix="$3"

	# 710-kernel-cmdline-initrd-entry.pcrlock.d is not part of the
	# pcrlock standards
	echo "$cmdline" > "$tmpdir/cmdline"
	pcrlock \
		lock-kernel-cmdline \
		--pcrlock="$tmpdir/cmdline.pcrlock" \
		"$tmpdir/cmdline"
	pcrlock \
		lock-kernel-initrd \
		--pcrlock="$tmpdir/initrd.pcrlock" \
		"${boot_root}/$initrd"
	jq --slurp '{"records": [.[].records[0]]}' \
	   "$tmpdir/cmdline.pcrlock" \
	   "$tmpdir/initrd.pcrlock" \
	   > "/var/lib/pcrlock.d/710-kernel-cmdline-initrd-entry.pcrlock.d/cmdline-initrd-$suffix.pcrlock"
	rm "$tmpdir/cmdline"
	rm "$tmpdir/cmdline.pcrlock"
	rm "$tmpdir/initrd.pcrlock"

	# 710-kernel-cmdline-boot-loader.pcrlock.d is not part of the
	# pcrlock standards
	echo -ne "$cmdline\0" > "$tmpdir/cmdline"
	iconv -t UTF-16LE -o "$tmpdir/cmdline.utf16" "$tmpdir/cmdline"
	pcrlock \
		lock-raw \
		--pcr=12 \
		--pcrlock="/var/lib/pcrlock.d/710-kernel-cmdline-boot-loader.pcrlock.d/cmdline-$suffix.pcrlock" \
		"$tmpdir/cmdline.utf16"
	rm "$tmpdir/cmdline.utf16"
}

# TODO: Maybe share this code outside
generate_tpm2_predictions_pcrlock()
{
	local pcrs="$1"

	# Select the affected entries
	select_entries_for_prediction

	# Remove all the generated measurements.  This will keep the
	# predictions at minimum and decrease the combinations
	rm -fr /var/lib/pcrlock.d/*

	pcrlock lock-firmware-code
	pcrlock lock-firmware-config
	# If secure boot is disabled, this can fail.  There is patch
	# for the policy generation, and for the authority is planned
	/usr/lib/systemd/systemd-pcrlock lock-secureboot-policy || true
	/usr/lib/systemd/systemd-pcrlock lock-secureboot-authority || true
	# uses / by default
	pcrlock lock-gpt

	# 630-shim-efi-application is not part of the pcrlock standards
	# TODO: move to shim-pcrlock.rpm
	pcrlock \
	    lock-pe \
	    --pcrlock=/var/lib/pcrlock.d/630-shim-efi-application.pcrlock.d/generated.pcrlock \
	    "${boot_root}${sdboot_dst}/shim.efi"

	# 640-boot-loader-efi-application is not part of the pcrlock
	# standards
	# This is measuing the systemd-boot EFI binary (named grub.efi)
	# TODO: move to systemd-boot-pcrlock.rpm
	pcrlock \
	    lock-pe \
	    --pcrlock=/var/lib/pcrlock.d/640-boot-loader-efi-application.pcrlock.d/generated.pcrlock \
	    "${boot_root}${sdboot_dst}/grub.efi"

	if [ -e "$boot_root/loader/loader.conf" ]; then
		pcrlock \
			lock-raw /boot/efi/loader/loader.conf \
			--pcr=5 \
			--pcrlock=/var/lib/pcrlock.d/641-sdboot-loader-conf.pcrlock
	fi

	# 650-kernel-efi-application.pcrlock is not part of the
	# pcrlock standards
	# TODO: move to kernel-TYPE-pcrlock.rpm
	local n=0
	while read -r i; do
		n=$((n+1))
		pcrlock \
		    lock-pe \
		    --pcrlock=/var/lib/pcrlock.d/650-kernel-efi-application.pcrlock.d/linux-"$n".pcrlock \
		    "${boot_root}/$i"
	done < <(jq --raw-output 'map(.linux) | unique | .[]' "$entryfile")

	# Join the cmdline and the initrd in a single component
	mkdir -p /var/lib/pcrlock.d/710-kernel-cmdline-initrd-entry.pcrlock.d
	n=0
	while read -r cmdline; do
		read -r initrd
		n=$((n+1))
		pcrlock_cmdline_initrd "initrd=$cmdline" "$initrd" "$n"
	done < <(jq --raw-output '.[] | ([(.initrd[0] | sub("/"; "\\"; "g")), .options] | join(" ")), .initrd[0]' "$entryfile")

	# Generate variation for 710-kernel-cmdline-initrd-entry
	# component that contains the current cmdline and the current
	# initrd, even if this will never be used again.  This is
	# required because disk-encryption-tool generates a new initrd
	# during the first boot, making the event log impossible to
	# align for systemd-pcrlock
	if [ "$SDB_ADD_INITIAL_CMDLINE" = "1" ]; then
		read -r cmdline < /proc/cmdline
		local initrd="${cmdline#*initrd=}"; initrd="${initrd%% *}"; initrd="${initrd//\\//}"
		pcrlock_cmdline_initrd "$cmdline" "$initrd" "0"
	fi

	pcrlock --pcr="$pcrs" make-policy

	# Publish the assets in the ESP, so can be imported by
	# dracut-pcr-signature
	[ -e /var/lib/systemd/pcrlock.json ] && cp /var/lib/systemd/pcrlock.json "${boot_root}${sdboot_dst}"
}

get_pcrs() {
	local pcrs
	local jq_pcr='.tokens[]|select(.type == "systemd-tpm2")|."tpm2_pubkey_pcrs"|join(",")'
	# We can have multiple devices, each one of them with
	# different PCRs
	while read -r dev; do
		pcrs=$(cryptsetup luksDump --dump-json-metadata "$dev" | jq -r "$jq_pcr")
		[ -z "$pcrs" ] || echo "$pcrs"
	done <<<$(blkid -t TYPE=crypto_LUKS -o device)
}

# TODO: Maybe share this code outside
generate_tpm2_predictions_pcr_oracle()
{
	local entry
	local all_pcrs

	# Select the affected entries
	select_entries_for_prediction

	all_pcrs=$(get_pcrs)
	if [ -z "$all_pcrs" ]; then
		warn "PCR Oracle configured but not enrolled in any LUKS header"
		return 0
	fi

	rm -f /etc/systemd/tpm2-pcr-signature.json

	# We make as many predictions as |all_pcrs| * |entries| to
	# cover all the combinations.  pcr-oracle is smart to include
	# the entry only one time, so we will not have duplications.
	# This is a step for multi device configurations.
	declare -a entries
	mapfile -t entries < <(jq -r '.[]|.id' "$entryfile")
	if [ -z "${entries[0]}" ]; then
		err "No bootloader entries found"
	fi
	for pcrs in $all_pcrs; do
		for entry in "${entries[@]}"; do
			log_info "Generate prediction for $entry with PCRs $pcrs"
			if ! pcr-oracle \
				--private-key /etc/systemd/tpm2-pcr-private-key.pem \
				--from eventlog \
				--output /etc/systemd/tpm2-pcr-signature.json \
				--target-platform=systemd \
				--boot-entry "${entry}" \
				sign "$pcrs"; then
				err "Failed to install TPM predictions for ${entry}"
			fi
		done
	done

	# Publish the assets in the ESP, so can be imported by
	# dracut-pcr-signature
	cp /etc/systemd/tpm2-pcr-public-key.pem "${boot_root}${sdboot_dst}"
	[ -e /etc/systemd/tpm2-pcr-signature.json ] && cp /etc/systemd/tpm2-pcr-signature.json "${boot_root}${sdboot_dst}"
}


# TODO: Maybe share this code outside
generate_tpm2_predictions()
{
	[ -e /etc/crypttab ] || return 0
	grep -q "tpm2-device" /etc/crypttab || return 0

	if is_pcr_oracle; then
		generate_tpm2_predictions_pcr_oracle
	elif have_pcrlock; then
		[ -e /etc/sysconfig/fde-tools ] || return 0
		. /etc/sysconfig/fde-tools

		generate_tpm2_predictions_pcrlock "${FDE_SEAL_PCR_LIST}"
	fi
}

main_menu()
{
	while true; do
		list=(kernels Kernels snapper Snapshots sd-boot Entries install "Install/Update")
		d --no-tags --cancel-label "Quit"  --menu "Main Menu" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		action="$result"

		case "$action" in
			snapper) show_snapper ;;
			sd-boot) update_entries cat; show_entries ;;
			kernels) show_kernels "$root_snapshot";;
			install) install_sdboot_interactive ;;
		esac
	done
}

####### main #######

getopttmp=$(getopt -o hc:v --long help,flicker,verbose,esp-path:,entry-token:,arch:,image:,no-variables,no-reuse-initrd,no-random-seed,all -n "${0##*/}" -- "$@")
eval set -- "$getopttmp"

while true ; do
        case "$1" in
                -h|--help) helpandquit ;;
		--flicker) dialog_altenate_screen=--keep-tite; shift ;;
		-v|--verbose) verbose=$((++verbose)); shift ;;
		--esp-path) arg_esp_path="$2"; shift 2 ;;
		--arch) arg_arch="$2"; shift 2 ;;
		--entry-token) arg_entry_token="$2"; shift 2 ;;
		--image) image="$2"; shift 2 ;;
		--no-variables) arg_no_variables=1; shift ;;
		--no-reuse-initrd) arg_no_reuse_initrd=1; shift ;;
		--no-random-seed) arg_no_random_seed=1; shift ;;
		--all) arg_all_entries=1; shift ;;
                --) shift ; break ;;
                *) echo "Internal error!" ; exit 1 ;;
        esac
done

case "$1" in
	install|needs-update|update|force-update|add-kernel|remove-kernel|set-default-snapshot|add-all-kernels|remove-all-kernels|is-installed|list-snapshots|list-entries|list-kernels|is-bootable|update-predictions) ;;
	kernels|snapshots|entries|"") stty_size; interactive=1 ;;
	*) err "unknown command $1" ;;
esac

for i in /etc/os-release /usr/lib/os-release; do
	[ -f "$i" ] || continue
	eval $(sed -ne '/^[A-Z_]\+=/s/^/os_release_/p' < "$i")
	break
done

[ -n "$arg_esp_path" ] && export SYSTEMD_ESP_PATH="$arg_esp_path"

# XXX: bootctl should have json output for that too
eval "$(bootctl 2>/dev/null | sed -ne 's/Firmware Arch: *\(\w\+\)/firmware_arch="\1"/p;s/ *token: *\(\w\+\)/entry_token="\1"/p;s, *\$BOOT: *\([^ ]\+\).*,boot_root="\1",p')"
read -r root_uuid root_device < <(findmnt / -v -r -n -o UUID,SOURCE)
root_subvol=$(btrfs subvol show / 2>/dev/null|head -1)
subvol_prefix="${root_subvol%/.snapshots/*}"
[ -s /etc/machine-id ] && read -r machine_id < /etc/machine-id

if [ -n "$arg_esp_path" ] && [ "$boot_root" != "$arg_esp_path" ]; then
	err "mismatch of esp path"
fi
[ -n "$arg_arch" ] && firmware_arch="$arg_arch"
settle_entry_token

[ -n "$boot_root" ] || err "No ESP detected. Legacy system?"
[ -n "$root_uuid" ] || err "Can't determine root UUID"
[ -n "$root_subvol" ] || err "Can't determine root subvolume"
[ -n "$root_device" ] || err "Can't determine root device"
[ -n "$entry_token" ] || err "No entry token. sd-boot not installed?"
[ -n "$firmware_arch" ] || err "Can't determine firmware arch"
case "$firmware_arch" in
	x64) image=vmlinuz ;;
	aa64) image=Image ;;
	*) err "Unsupported architecture $firmware_arch" ;;
esac

root_snapshot="${root_subvol#${subvol_prefix}/.snapshots/}"
root_snapshot="${root_snapshot%/snapshot}"

if [ "$1" = "install" ]; then
	install_sdboot "${2:-$root_snapshot}"
elif [ "$1" = "needs-update" ]; then
	sdboot_needs_update "${2:-$root_snapshot}"
elif [ "$1" = "update" ]; then
	if sdboot_needs_update "${2:-$root_snapshot}"; then install_sdboot "${2:-$root_snapshot}"; else :; fi
elif [ "$1" = "force-update" ]; then
	if is_installed; then install_sdboot "${2:-$root_snapshot}"; else :; fi
elif [ "$1" = "add-kernel" ]; then
	install_kernel "${3:-$root_snapshot}" "$2"
elif [ "$1" = "add-all-kernels" ]; then
	install_all_kernels "${2:-$root_snapshot}"
elif [ "$1" = "remove-kernel" ]; then
	remove_kernel "${3:-$root_snapshot}" "$2"
elif [ "$1" = "remove-all-kernels" ]; then
	remove_all_kernels "${2:-$root_snapshot}"
elif [ "$1" = "set-default-snapshot" ]; then
	set_default_snapshot "${2:-$root_snapshot}"
elif [ "$1" = "is-installed" ]; then
	if is_installed; then
		log_info "systemd-boot was installed using sdbootutil"
		exit 0
	else
		log_info "not installed using this tool"
		exit 1
	fi
elif [ "$1" = "list-kernels" ]; then
	list_kernels "${2:-$root_snapshot}"
elif [ "$1" = "list-entries" ]; then
	list_entries "${2:-}"
elif [ "$1" = "list-snapshots" ]; then
	list_snapshots
elif [ "$1" = "is-bootable" ]; then
	is_bootable "${2:-$root_snapshot}"
elif [ "$1" = "update-predictions" ]; then
	update_predictions=1
elif [ "$1" = "kernels" ]; then
	show_kernels "${2:-$root_snapshot}"
elif [ "$1" = "snapshots" ]; then
	show_snapper
elif [ "$1" = "entries" ]; then
	show_entries
else
	main_menu
fi

[ -z "$update_predictions" ] || generate_tpm2_predictions
