## Authors:
##   Ajrat Makhmutov <rauty@altlinux.org>
##
## Copyright (C) 2024-2026  Basealt LLC
##
## This file is part of alterator-kopidel.
##
## alterator-kopidel is free software: you can redistribute it and/or modify it under the terms
## of the GNU General Public License as published by the Free Software Foundation,
## either version 3 of the License, or (at your option) any later version.
##
## alterator-kopidel is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
## without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
## See the GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along with alterator-kopidel.
## If not, see <https://www.gnu.org/licenses/>.

if [ -n "${__included_kopidel_steps-}" ]; then
	return 0
fi
readonly __included_kopidel_steps=1

. "${KOPIDEL_LIBDIR:-/usr/lib/alterator-kopidel}/variables"
. "$KOPIDEL_LIBDIR/percentage-handlers"
. "$KOPIDEL_LIBDIR/boot-image-methods"
. "$KOPIDEL_LIBDIR/check-incoming-parameters"
. "$KOPIDEL_LIBDIR/disk-profile"
. alterator-sh-functions

declare -A STEPS
SUBSTEPS=()

step_error() {
	echo "ERROR: $1" >&2
}

is_cleanup_lazy() (
	[ "${KOPIDEL_LAZY_CLEANUP-}" == 1 ]
)

build_workdir() (
	echo "${in_workdir:-$EXDRIVE_WORKDIR}"
)

append_to_build_log() {
	echo "$1" 2>/dev/null >> "$workdir/build.log" || true
}

run_step() (
	workdir="$(build_workdir)"
	if [ -n "$in_workdir" ]; then
		check_target=fast_check_in_workdir
	elif [ -n "$in_exdrive" ]; then
		check_target=fast_check_in_exdrive
	else
		echo "No input workdir or exdrive" >&2
		return 1
	fi
	mkdir -pv "$workdir"
	check_status=0
	check_output="$("$check_target" 2>&1)" || check_status="$?"
	if [ -n "$check_output" ]; then
		append_to_build_log "$check_output"
	fi
	if [ "$check_status" -ne 0 ]; then
		return 1
	fi

	step="$1"
	append_to_build_log "step start: $step"
	echo "step_progress: 0"

	cd "$workdir" || return 1

	"$step" 2>&1 | tee -a "$workdir/build.log" |
		{ grep -Ex --line-buffered 'step_progress: [0-9]*' || true; }

	if [ "${PIPESTATUS[0]}" -ne 0 ]; then
		return 1
	fi

	echo "step_progress: 100"
	append_to_build_log "step end: $step"
)

SUBSTEPS+=( prepare_workdir::create_initrd_image )
prepare_workdir::create_initrd_image() (
	start="${1:-0}"
	end="${2:-100}"
	pushd image || return 1

	mkdir -pv .disk
	cp -vf "$KOPIDEL_DATADIR/initrd.mk" .disk/
	cp -vf "$KOPIDEL_DATADIR/bootchain" .disk/

	mkdir -pv boot
	rm -vf boot/initrd*.img

	make-initrd \
		--config=.disk/initrd.mk \
		--bootdir=boot/ \
		--kernel="$KERNEL_VERSION" \
		BOOTCHAIN_PATH=.disk \
		KERNEL_CONFIG="$BOOTDIR/config-$KERNEL_VERSION" \
		-v 2>&1 |
		awk -v start="$start" -v end="$end" '{
			print
			if (++n % 15 == 0) {
				p = start + int(n / 15)
				if (p >= end) p = end - 1
				print "step_progress: " p
				fflush()
			}
		}'

	if [ "${PIPESTATUS[0]}" -ne 0 ]; then
		step_error "make-initrd failed to create the initrd image."
		return 1
	fi

	if [ ! -s "boot/initrd-$KERNEL_VERSION.img" ]; then
		step_error "make-initrd created no boot/initrd-$KERNEL_VERSION.img."
		return 1
	fi
)

prepare_workdir::installed_deps_closure() (
	declare -A seen=()
	queue=("$@")
	while [ "${#queue[@]}" -gt 0 ]; do
		pkg="${queue[0]}"
		queue=("${queue[@]:1}")
		[ -n "${seen[$pkg]:-}" ] && continue
		seen["$pkg"]=1
		while read -r dep; do
			[ -n "$dep" ] && [ -z "${seen[$dep]:-}" ] && queue+=( "$dep" )
		done < <(rpm -q --requires "$pkg" 2>/dev/null |
			sed 's/[[:space:]].*//' |
			grep -vE '^(rpmlib|rtld|config|executable)\(' |
			grep -v '^$' |
			xargs -r rpm -q --whatprovides --qf '%{NAME}\n' 2>/dev/null |
			grep -v 'no package')
	done
	if [ "${#seen[@]}" -gt 0 ]; then
		printf '%s\n' "${!seen[@]}"
	fi
)

prepare_workdir::path_through_real_dirs() (
	if [ -L "$1" ]; then
		echo "$(realpath "$(dirname "$1")")/$(basename "$1")"
	else
		realpath "$1"
	fi
)

SUBSTEPS+=( prepare_workdir::create_squashfs_files_list )
prepare_workdir::create_squashfs_files_list() (
	readonly SQUASHFS_PACKAGES_LOG="$workdir/squashfs-packages.log"

	cp -fv "$KOPIDEL_DATADIR/squashfs-files-default.txt" squashfs-files.txt
	rm -fv "$SQUASHFS_PACKAGES_LOG"

	mapfile -t squashfs_top_packages < <(
		rpm -qa --qf "%{NAME}\n" |
			grep -E -x -f "$KOPIDEL_DATADIR/squashfs-packages-default.txt")
	mapfile -t squashfs_packages < <(
		prepare_workdir::installed_deps_closure "${squashfs_top_packages[@]}" |
			sort -u)
	if [ "${#squashfs_packages[@]}" -eq 0 ]; then
		echo "WARNING: the dependency closure is empty, using the top-level packages only." \
			>> "$SQUASHFS_PACKAGES_LOG"
		squashfs_packages=( "${squashfs_top_packages[@]}" )
	fi
	echo "The altinst packages: ${#squashfs_top_packages[@]} top-level, ${#squashfs_packages[@]} with the dependencies." \
		>> "$SQUASHFS_PACKAGES_LOG"

	for squashfs_package in "${squashfs_packages[@]}"; do
		if [ -z "$(rpm -q "$squashfs_package" 2>>"$SQUASHFS_PACKAGES_LOG")" ]; then
			continue
		fi

		echo "Add the $squashfs_package to the altinst image" >> "$SQUASHFS_PACKAGES_LOG"
		rpm -ql "$squashfs_package" | while read -r squashfs_file; do
			prepare_workdir::path_through_real_dirs "$squashfs_file" >> squashfs-files.txt
		done
	done

	find "$(realpath -m "$MODULES_DIR/$KERNEL_VERSION")" >> squashfs-files.txt
)

SUBSTEPS+=( prepare_workdir::create_squashfs_altinst )
prepare_workdir::create_squashfs_altinst() (
	start="${1:-0}"
	end="${2:-100}"
	(( rsync_end = start + (end - start) * 2 / 7 ))
	pushd image || return 1

	if [ ! -r "$workdir/squashfs-files.txt" ]; then
		step_error 'No readable file squashfs-files.txt in workdir.'
		return 1
	fi

	rm   -rf squashfs-root
	mkdir -v squashfs-root
	rm -vf "$workdir/rsync-squashfs-root.log"
	expected_lines="$(rsync -avAXHW --dry-run --exclude="$workdir" \
		--files-from="$workdir/squashfs-files.txt" \
		/ "squashfs-root/" 2>&1 | wc -l)"
	rsync -avAXHW \
		--exclude="$workdir" \
		--files-from="$workdir/squashfs-files.txt" \
		/ "squashfs-root/" 2>&1 \
		| expected_percentage_handler "$expected_lines" "$start" "$rsync_end" \
		"$workdir/rsync-squashfs-root.log"

	pushd squashfs-root || return 1
		rm -v usr/sbin/init
		if [ -f usr/sbin/install2-init ]; then
			mv -v usr/sbin/install2-init sbin/init
		else
			mv -v usr/libexec/install2/install2-init sbin/init
		fi

		mkdir -pv sys mnt tmp proc
	popd || return 1

	chroot squashfs-root /usr/bin/bash -c "alternatives-validate; alternatives-update"

	rm -fv altinst
	mksquashfs squashfs-root altinst -no-recovery -comp xz -b 262144 -percentage | default_percentage_handler "$rsync_end" "$end"
	if [ "${PIPESTATUS[0]}" -ne 0 ]; then
		step_error "mksquashfs failed to create altinst."
		return 1
	fi

	if is_cleanup_lazy; then
		rm -rf "$workdir/squashfs-root"
		mv squashfs-root "$workdir/"
	else
		rm -rf squashfs-root
	fi
)

STEPS[prepare_workdir]="$(_ "Preparing the working directory")"
prepare_workdir() (
	mkdir -pv image/.disk
	rpm -qi alterator-kopidel > image/.disk/alterator-kopidel.txt

	mkdir -pv image/boot
	rm -vf image/boot/vmlinuz-*
	cp -v "$BOOTDIR/vmlinuz-$KERNEL_VERSION" image/boot/

	mkdir -pv image/Metadata
	cp -vf "$KOPIDEL_DATADIR/autoinstall.scm" image/Metadata/autoinstall.scm

	echo "step_progress: 3"
	prepare_workdir::create_initrd_image 3 20       || return 1
	echo "step_progress: 20"
	prepare_workdir::create_squashfs_files_list     || return 1
	echo "step_progress: 30"
	prepare_workdir::create_squashfs_altinst 30 100 || return 1
	echo "step_progress: 100"
)

readonly TAR_SOME_FILES_DIFFER=1

STEPS[create_copied_fs]="$(_ "Copying the file system")"
create_copied_fs() (
	"$KOPIDEL_LIBEXECDIR/update-ignored-files.sh" "$in_ignored_files" || return 1

	mkdir -pv image/Metadata
	rm -rf copied-fs
	rm -vf image/Metadata/copied-fs.tar*
	rm -vf "$workdir/copied-fs-files.log"

	expected_lines="$(rsync -avAXHW --dry-run \
		--exclude="$workdir" \
		--exclude="$WORKDIR_NAME" \
		--exclude-from="$IGNORED_FILES" \
		/ "copied-fs/" 2>/dev/null | wc -l)"

	archive=image/Metadata/copied-fs.tar
	compress_args=()
	if test_bool "$in_compress_copied"; then
		archive+=.xz
		compress_args=( --use-compress-program='xz -T0' )
	fi

	tar cvf "$archive" --absolute-names \
		"${compress_args[@]}" \
		--exclude="$workdir" \
		--exclude="**/$WORKDIR_NAME" \
		--exclude-from="$IGNORED_FILES" \
		-C / / | expected_percentage_handler "$expected_lines" 0 99 \
		"$workdir/copied-fs-files.log"

	tar_status="${PIPESTATUS[0]}"
	if [ "$tar_status" -gt "$TAR_SOME_FILES_DIFFER" ]; then
		step_error "create_copied_fs: tar failed with $tar_status, see above."
		return 1
	fi
)

STEPS[create_disk_partition_info]="$(_ "Generating disk partition information")"
create_disk_partition_info() (
	vm_profile=image/Metadata/vm-profile.scm

	mkdir -pv image/.disk image/Metadata
	lsblk --raw -b -o 'FSTYPE,SIZE,UUID,TYPE,FSUSED' > image/.disk/lsblk
	cp -vf "$FSTAB" image/.disk/fstab

	rm -vf "$vm_profile"
	cp -v "$KOPIDEL_DATADIR/vm-profile-start.scm" "$vm_profile"

	lsblk -n --raw -b -o 'FSTYPE,SIZE,UUID,TYPE,FSUSED' | sort -k4,4r -k2,2n | while read -r line; do
		create_disk_partition_info::profile_entry "$line" >> "$vm_profile" || true
	done

	echo '               )))' >> "$vm_profile"

	echo "--- lsblk --raw -b ---"
	cat image/.disk/lsblk
	echo "--- blkid ---"
	blkid 2>/dev/null || true
	echo "--- vm-profile.scm ---"
	cat "$vm_profile"
	echo "--- end create_disk_partition_info ---"
)

STEPS[create_install_scripts]="$(_ "Creating installation scripts")"
create_install_scripts() (
	rm -vf image/Metadata/install-scripts.tar
	rm -rf install-scripts

	mkdir -pv \
		install-scripts/initinstall.d \
		install-scripts/postinstall.d \
		install-scripts/preinstall.d

	clear_scripts=( postinstall.d/00-remove-installer-pkgs.sh )
	clear_scripts+=( preinstall.d/00-cp-diskinfo.sh )
	clear_scripts+=( preinstall.d/05-copy-fstab )
	clear_scripts+=( preinstall.d/20-sysconfig.sh )

	install -v -m 755 "$KOPIDEL_DATADIR/90-dmsetup-remove_all.sh" install-scripts/initinstall.d/

	if test_bool "$in_oem_mode"; then
		install -v -m 755 "$KOPIDEL_DATADIR/91-alterator-setup"        install-scripts/postinstall.d/
		install -v -m 755 "$KOPIDEL_DATADIR/92-oem-remove-human-users" install-scripts/postinstall.d/
	fi

	install -v -m 755 "$KOPIDEL_DATADIR/51-kopidel-grub.sh" install-scripts/postinstall.d/
	if test_bool "$in_grub_efi_removable"; then
		sed -i "s/@GRUB_DEVICE_EFI@/efiremovable/" install-scripts/postinstall.d/51-kopidel-grub.sh
	else
		sed -i "s/@GRUB_DEVICE_EFI@/efi/" install-scripts/postinstall.d/51-kopidel-grub.sh
	fi

	install -v -m 755 "$KOPIDEL_DATADIR/00-preinstall.sh" install-scripts/postinstall.d/

	pushd install-scripts || return 1
	echo '#!/bin/sh' | tee "${clear_scripts[@]}" > /dev/null
	chmod 755 "${clear_scripts[@]}"
	popd || return 1

	mkdir -pv image/Metadata
	tar cf image/Metadata/install-scripts.tar -C install-scripts .

	if ! is_cleanup_lazy; then
		rm -rf install-scripts
	fi
)

razlivochniy_loop_dev() (
	losetup -j razlivochniy.img | head -n 1 | cut -d ':' -f 1
)

razlivochniy_dev() (
	if [ -n "$in_exdrive" ]; then
		echo "$in_exdrive"
	else
		razlivochniy_loop_dev
	fi
)

attach_razlivochniy() (
	dev="$(razlivochniy_dev)"
	if [ -z "$dev" ]; then
		dev="$(losetup --show -Pf razlivochniy.img)"
	fi
	echo "$dev"
)

razlivochniy_part() (
	lsblk --paths -n --raw -b -o 'NAME,PARTLABEL' "$1" | sed -n "s/ $2\$//p" | head -n 1
)

SUBSTEPS+=( create_razlivochniy_mountpoint::mount_razlivochniy )
create_razlivochniy_mountpoint::mount_razlivochniy() (
	dev="$(attach_razlivochniy)"
	root_pdev="$(razlivochniy_part "$dev" ROOT)"
	efi_pdev="$(razlivochniy_part "$dev" ESP)"

	mkdir -pv image
	if ! mount "$root_pdev" image; then
		step_error "create_razlivochniy_mountpoint: failed to mount the root partition $root_pdev of $dev."
		return 1
	fi
	if [ -n "$efi_pdev" ]; then
		mkdir -pv image/boot/efi
		if ! mount "$efi_pdev" image/boot/efi; then
			step_error "create_razlivochniy_mountpoint: failed to mount the ESP $efi_pdev of $dev."
			return 1
		fi
	fi
)

umount_mounted_parts() (
	for part in "$1"*; do
		if findmnt -rn -S "$part" > /dev/null 2>&1; then
			umount "$part" || return 1
		fi
	done
)

run_parted() (
	if ! parted "$@"; then
		step_error "create_razlivochniy_mountpoint: parted $* failed."
		return 1
	fi
)

create_razlivochniy_mountpoint::take_exdrive() (
	if ! umount_mounted_parts "$in_exdrive"; then
		step_error "create_razlivochniy_mountpoint: $in_exdrive is in use and its partitions cannot be unmounted."
		findmnt --raw -n -o SOURCE,TARGET | grep "^$in_exdrive" >&2 || true
		return 1
	fi
)

SUBSTEPS+=( create_razlivochniy_mountpoint::create_img )
create_razlivochniy_mountpoint::create_img() (
	rm -fv razlivochniy.img razlivochniy.iso
	if test_bool "$in_compress_copied"; then
		if [ ! -d ready-to-use-image ]; then
			echo "Incorrect using create_razlivochniy_mountpoint step with compressing:" >&2
			echo "razlivochniy.img should be created after creating image compressing copied fs" >&2
			echo "with ready to use installer files. This is necessary to" >&2
			echo "create the minimal size razlivochniy.img." >&2
			return 1
		fi
		img_size="$(razlivochniy_img_size_mb "$(du -sm ready-to-use-image | cut -f 1)")"
	else
		"$KOPIDEL_LIBEXECDIR/update-ignored-files.sh" "$in_ignored_files" || return 1
		img_size="$(razlivochniy_img_size_for_copied_fs_mb "$("$KOPIDEL_LIBEXECDIR/required-space.sh")")"
	fi

	dd if=/dev/zero of=razlivochniy.img bs=1M count="$img_size" &
	dd_pid="$!"
	dd_percentage_handler 0 90 "$dd_pid" razlivochniy.img "$(( img_size * BYTES_IN_MB ))"
	if ! wait "$dd_pid"; then
		step_error "create_razlivochniy_mountpoint: dd failed to create razlivochniy.img of $img_size MiB."
		return 1
	fi
)

create_razlivochniy_mountpoint::partition() (
	pdev="$1"
	run_parted "$pdev" --script mklabel gpt || return 1
	start_mb=1
	part_num=1

	if is_img_legacy_bootable; then
		run_parted "$pdev" --script mkpart non-fs "${start_mb}MiB" 2MiB || return 1
		run_parted "$pdev" --script name "$part_num" "BIOS" || return 1
		run_parted "$pdev" --script set "$part_num" bios_grub on || return 1
		(( start_mb += 1 ))
		(( part_num += 1 ))
	fi

	if is_img_efi_bootable; then
		run_parted "$pdev" --script mkpart primary fat32 "${start_mb}MiB" "$(( ESP_SIZE_MB + start_mb ))MiB" || return 1
		run_parted "$pdev" --script name "$part_num" "ESP" || return 1
		run_parted "$pdev" --script set "$part_num" esp on || return 1
		(( start_mb += ESP_SIZE_MB ))
		(( part_num += 1 ))
	fi

	run_parted "$pdev" --script mkpart primary ext4 "${start_mb}MiB" 100% || return 1
	run_parted "$pdev" --script name "$part_num" "ROOT" || return 1
)

create_razlivochniy_mountpoint::format() (
	dev="$1"
	partprobe "$dev"
	if ! udevadm wait --timeout=30 --settle "$dev"; then
		echo "WARNING: udev events for $dev didn't settle in 30 seconds" >&2
	fi

	root_pdev="$(razlivochniy_part "$dev" ROOT)"
	efi_pdev="$(razlivochniy_part "$dev" ESP)"

	echo "efi_pdev=$efi_pdev"
	echo "root_pdev=$root_pdev"

	if [ -z "$root_pdev" ]; then
		step_error "create_razlivochniy_mountpoint: the partitioning of $dev left no ROOT partition."
		return 1
	fi

	if ! mkfs.ext4 -m 0 -E nodiscard "$root_pdev"; then
		step_error "create_razlivochniy_mountpoint: mkfs.ext4 failed on $root_pdev."
		return 1
	fi
	if [ -n "$efi_pdev" ]; then
		if ! mkfs.vfat -F32 "$efi_pdev"; then
			step_error "create_razlivochniy_mountpoint: mkfs.vfat failed on $efi_pdev."
			return 1
		fi
	fi
)

STEPS[create_razlivochniy_mountpoint]="$(_ "Creating partitions and mount points")"
create_razlivochniy_mountpoint() (
	umount_razlivochniy || true
	if mountpoint -q image; then
		step_error "create_razlivochniy_mountpoint: $workdir/image stays mounted, its files are not removed."
		return 1
	fi
	rm -rf image
	mkdir -pv image

	if [ -n "$in_exdrive" ]; then
		create_razlivochniy_mountpoint::take_exdrive || return 1
		create_razlivochniy_mountpoint::partition "$in_exdrive" || return 1
	else
		create_razlivochniy_mountpoint::create_img || return 1
		create_razlivochniy_mountpoint::partition razlivochniy.img || return 1
	fi

	create_razlivochniy_mountpoint::format "$(attach_razlivochniy)" || return 1
	create_razlivochniy_mountpoint::mount_razlivochniy || return 1
)

STEPS[save_ready_to_use_image]="$(_ "Moving ready-to-use files into an image")"
save_ready_to_use_image() (
	if ! [[ -f image/altinst &&
		-f image/Metadata/copied-fs.tar.xz &&
		-f image/Metadata/vm-profile.scm ]]; then
		step_error "save_ready_to_use_image: not ready to use image."
		return 1
	fi
	rm -rf ready-to-use-image
	mv image ready-to-use-image
)

STEPS[use_ready_to_use_image]="$(_ "Filling the image with the ready-to-use files")"
use_ready_to_use_image() (
	if [ ! -d ready-to-use-image ]; then
		step_error "use_ready_to_use_image: no ready-to-use-image dir."
		return 1
	fi
	if ! cp -RT ready-to-use-image image; then
		step_error "use_ready_to_use_image: failed to fill the image."
		return 1
	fi
	if ! is_cleanup_lazy; then
		rm -rf ready-to-use-image
	fi
)

STEPS[install_grub]="$(_ "Installing GRUB")"
install_grub() (
	dev="$(razlivochniy_dev)"
	root_pdev="$(razlivochniy_part "$dev" ROOT)"
	root_pdev_uuid="$(blkid -s UUID -o value "$root_pdev")"
	if [ -z "$root_pdev_uuid" ]; then
		step_error "install_grub: the root partition $root_pdev of $dev has no UUID to boot from."
		return 1
	fi

	modules="linux normal iso9660 f2fs ext2 fat exfat ntfs ntfscomp part_gpt search font configfile gfxterm"
	rescue_modules="tar ls minicmd reboot halt cat"
	look_args=( --locales="" --fonts="unicode" )
	efi_dir_args=( --boot-directory="image/boot" --efi-directory="image/boot/efi" --removable )
	efi_module_args=( --install-modules="$modules $rescue_modules" --modules="$modules" )

	if is_img_legacy_bootable; then
		if ! grub-install \
			--target=i386-pc \
			--boot-directory="image/boot" \
			--install-modules="$modules biosdisk $rescue_modules" \
			--modules="$modules biosdisk" \
			"${look_args[@]}" \
			"$dev"; then
			step_error "install_grub: grub-install failed for legacy BIOS."
			return 1
		fi
	fi

	if is_img_efi_bootable; then
		if command -v grub-efi-install > /dev/null 2>&1; then
			if ! grub-efi-install "${efi_dir_args[@]}" -- "${efi_module_args[@]}" "${look_args[@]}"; then
				step_error "install_grub: grub-efi-install failed."
				return 1
			fi
		else
			if ! grub-install \
				--target="$(grub_efi_target)" \
				"${efi_dir_args[@]}" \
				"${efi_module_args[@]}" \
				"${look_args[@]}" \
				"$dev"; then
				step_error "install_grub: grub-install failed for EFI."
				return 1
			fi
		fi
	fi

	pushd image || return 1

	if ! cp -f "$KOPIDEL_DATADIR/grub.cfg" boot/grub/grub.cfg; then
		step_error "install_grub: failed to put the grub.cfg into the image."
		return 1
	fi

	sed -i "s/@KFLAVOUR@/$KERNEL_VERSION/g" boot/grub/grub.cfg
	sed -i "s/@UUID@/$root_pdev_uuid/g" boot/grub/grub.cfg

	menuentry_install="$(_ 'Install a "razlivochniy obraz".')"
	sed -i "s/@MENUENTRY_INSTALL@/$menuentry_install/g" boot/grub/grub.cfg

	popd
)

STEPS[umount_razlivochniy]="$(_ "Unmounting an image")"
umount_razlivochniy() (
	sync
	if [ -n "$in_exdrive" ]; then
		umount_mounted_parts "$in_exdrive"
	else
		if [ ! -f razlivochniy.img ]; then
			echo "No razlivochniy.img for umount" >&2
			return 1
		fi
		loop_dev="$(razlivochniy_loop_dev)"
		if [ -e "$loop_dev" ]; then
			umount_mounted_parts "$loop_dev" || return 1
			losetup -d "$loop_dev"
		fi
	fi
)

build_plan() (
	if [ -n "$in_workdir" ] && test_bool "$in_compress_copied"; then
		printf '%s\n' \
			'prepare_workdir                1' \
			'create_copied_fs               25' \
			'create_disk_partition_info     57' \
			'create_install_scripts         58' \
			'save_ready_to_use_image        59' \
			'create_razlivochniy_mountpoint 60' \
			'use_ready_to_use_image         80' \
			'install_grub                   90' \
			'umount_razlivochniy            99'
	else
		printf '%s\n' \
			'create_razlivochniy_mountpoint 1' \
			'prepare_workdir                20' \
			'create_copied_fs               40' \
			'create_disk_partition_info     80' \
			'create_install_scripts         81' \
			'install_grub                   82' \
			'umount_razlivochniy            95'
	fi
)

is_image_attached() (
	loop_dev="$(losetup -j "$1/razlivochniy.img" | head -n 1 | cut -d ':' -f 1)"
	[ -n "$loop_dev" ]
)

prepare_build_workdir() (
	workdir="$(build_workdir)"
	if [ "$(basename "$workdir")" != "$WORKDIR_NAME" ]; then
		step_error "prepare_build_workdir: the working directory is not named $WORKDIR_NAME and is not removed: $workdir."
		return 1
	fi
	run_step umount_razlivochniy > /dev/null
	if is_image_attached "$workdir"; then
		step_error "prepare_build_workdir: the image of a previous build is still attached and cannot be removed: $workdir/razlivochniy.img."
		return 1
	fi
	if mountpoint -q "$workdir/image"; then
		step_error "prepare_build_workdir: $workdir/image stays mounted, its files are not removed."
		return 1
	fi
	rm -rf --one-file-system "$workdir" && mkdir -p "$workdir"
)

run_build_plan() (
	step_wrapper="$1"
	mapfile -t plan < <(build_plan)
	for entry in "${plan[@]}"; do
		read -r step main_progress <<< "$entry"
		if ! "$step_wrapper" "$step" "$main_progress"; then
			run_step umount_razlivochniy > /dev/null
			return 1
		fi
	done
)
