#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose
export LC_ALL=C

## style-ok: no-safe-rm -- this is the sanity/bootstrap step that installs safe-rm.
## style-ok: allow-echo -- the only 'echo' occurrences are inside strings.

true "INFO: Currently running script: ${BASH_SOURCE[0]} $*"

MYDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

cd "${MYDIR}"
cd ..
cd help-steps

source pre
source variables

cd "${MYDIR}"
cd ..

# shellcheck source=../packages/kicksecure/helper-scripts/usr/libexec/helper-scripts/has.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/has.bsh

test-wc() {
   if ! printf '%s\n' "" | wc -l >/dev/null ; then
      error "\
command 'wc' test failed! Do not ignore this!

'wc' can core dump. Example:
zsh: illegal hardware instruction (core dumped) wc -l
https://github.com/rspamd/rspamd/issues/5137"
   fi
}

check-redistributable-builds-requirements() {
   local folder_list folder_item

   ## '~/.gnupg' is required as at least an empty folder for the Qubes
   ## 'split-gpg-2' bug. Auto-create unconditionally; this is independent
   ## of the redistributable folder check below.
   mkdir --parents -- "${HOME}/.gnupg"

   ## Skip the rest for non-redistributable dev builds: '$dist_build_redistributable=false'
   ## means 'help-steps/signing_key' auto-generates keys (see
   ## 'build-steps.d/1200_prepare-build-machine'), so empty folders are fine.
   if ! [ "${dist_build_redistributable:-}" = "true" ]; then
      return 0
   fi
   ## CI: signing keys are auto-generated by 'signing-key-create'
   ## (called from 'build-steps.d/1200_prepare-build-machine'
   ## 'signing_key()'); '~/.gnupg' is auto-created above. The
   ## redistributable folder check would otherwise fail before
   ## 'signing-key-create' has a chance to create '~/.signify',
   ## hence the early return here. Release signing keys are always
   ## auto-generated per build.
   if [ "${CI:-}" = "true" ]; then
      return 0
   fi
   ## '--dry-run true' is documented (see 'help-steps/parse-cmd') as
   ## "useful for debugging dm-prepare-release". The folders are not
   ## checked here in dry-run mode because each is owned by another
   ## piece of the pipeline:
   ## - '~/.signify' is created by 'help-steps/signing-key-create'
   ##   (called from 'build-steps.d/1200_prepare-build-machine'
   ##   'signing_key()', which auto-generates a keypair when
   ##   '$build_dry_run=true').
   ## - '~/buildconfig.d' is optional: 'help-steps/variables'
   ##   'dist_build_source_config_dir()' silently skips it if missing.
   ## - '~/.ssh' is unused in dry-run mode because '$rsync_cmd' is
   ##   mocked in 'help-steps/variables' when '$build_dry_run=true'.
   if [ "${build_dry_run:-}" = "true" ]; then
      return 0
   fi

   ## Real redistributable build (operator on their workstation; not CI,
   ## not dry-run). The folders must contain real content (release
   ## signify keypair, real upload ssh keys, optional operator-provided
   ## buildconfig overrides). Their absence is a configuration error.
   ## Folder '$HOME/.local/share/sequoia/keystore' is optional because
   ## it does not exist if using Qubes 'split-gpg-2'.
   folder_list=(
      "${HOME}/buildconfig.d"
      "${HOME}/.ssh"
      "${HOME}/.signify"
   )
   for folder_item in "${folder_list[@]}"; do
      if [ ! -d "${folder_item}" ]; then
         error "dist_build_redistributable=true, missing required folder: ${folder_item}"
      fi
   done
}

check-git-folder() {
   ## A few places in the source code use "git clean" or "git describe".
   if ! test -e "${source_code_folder_dist}/.git" ; then
      error "${source_code_folder_dist}/.git does not exist."
   fi
}

check-operating-system-version() {
   local codename

   if ! test -r /etc/os-release ; then
      error "file /etc/os-release does not exist!

Is package base-files installed? It is required for this check.
sudo apt install base-files

Are you building on Debian? Only building on Debian is supported."
   fi

   codename=$(cat -- /etc/os-release)
   codename=$(printf '%s\n' "${codename}" | grep VERSION_CODENAME)
   codename=$(printf '%s\n' "${codename}" | cut -d= -f2)

   ## TODO: Debian forky
   #if [ "forky" = "$codename" ]; then
   #   true "INFO: legacy codename OK."
   #   return 0
   #fi

   if [ "${dist_build_apt_stable_release:-}" = "${codename}" ]; then
      true "INFO: up-to-date codename OK."
      return 0
   fi

   if [ "${build_unsupported_os:-}" = "true" ]; then
      ## '--unsupported-os true' opt-out for developer / AI / CI smoke
      ## testing on non-Debian hosts. The mismatch is logged loudly so
      ## it is still obvious in build logs, but does not abort.
      printf '%s\n' "${yellow:-}${bold:-}WARN: detected codename '${codename}' != expected '${dist_build_apt_stable_release}'; \
continuing because --unsupported-os true is set. Resulting image is NOT a release-quality build.${reset:-}" >&2
      return 0
   fi

   error "\
Wrong operating system!

You are attempting to build on an unsupported operating system or version.

Either your version is older or newer than expected. See build documentation
to learn which operating system is suggested.

detected operating system codename: '${codename}'
expected operating system codename: '${dist_build_apt_stable_release}'

To bypass this check (NOT for release builds; dev / AI / CI smoke only):
  --unsupported-os true"

   true
}

check-sudo() {
   local sudo_test_output
   sudo_test_output="$(${SUDO_TO_ROOT} test -d /usr 2>&1)"
   if [ "${sudo_test_output:-}" = "" ]; then
      true "INFO: sudo_test_output is empty as expected, ok."
      return 0
   fi
   error "\
sudo_test_output not empty! In other words, the output of command

sudo dpkg test -d /usr

is expected to be empty but was non-empty.

The user must fix this issue before proceeding. These issue is most likely not caused by derivative-issue. This is most likely a general system configuration issue."
   true
}

check-dpkg() {
   ## dpkg --audit does not return anything, if everything is fine.
   ## Therefore we see if dpkg has to say something, and if yes, the system is
   ## broken and we abort.
   dpkg_audit_output="$(${SUDO_TO_ROOT} dpkg --audit 2>&1)" || true
   if [ "${dpkg_audit_output:-}" = "" ]; then
      true "INFO: dpkg_audit_output is empty as expected, ok."
      return 0
   fi
   error "\
dpkg_audit_output not empty! In other words, the output of command

sudo dpkg --audit

is expected to be empty but was non-empty. Meaning that dpkg found a system configuration issue.

modified quote of the dpkg man page:

> Performs database sanity and consistency checks for [...] all packages. For example, searches for packages that have been installed only partially on your system or that have missing, wrong or obsolete control data or files. dpkg will suggest what to do with them to get them fixed.

The user must fix this issue before proceeding. These issue is most likely not caused by derivative-issue. This is most likely a general system configuration issue."
   true
}

check-hostname() {
   ## mmdebstrap requires file '/etc/hostname' to exist.
   if ! test -f /etc/hostname ; then
      local local_hostname
      if ! local_hostname="$(hostname)" ; then
         error "${bold}${red}ERROR ${BASH_SOURCE[0]}: Running command 'hostname' failed. See above.${reset}"
      fi
      if [ "${local_hostname:-}" = "" ]; then
         error "${bold}${red}ERROR ${BASH_SOURCE[0]}: Running command 'hostname' returned empty output. See above.${reset}"
      fi
      true "${bold}${cyan}INFO: File /etc/hostname did not exist. Writing ${local_hostname} to /etc/hostname, ok.${reset}"
      printf '%s\n' "${local_hostname}" | ${SUDO_TO_ROOT} tee -- /etc/hostname >/dev/null
   fi
   if ! test -r /etc/hostname ; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: /etc/hostname unreadable on the build system!${reset}"
   fi
   local build_system_hostname
   if ! build_system_hostname=$(cat -- /etc/hostname) ; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: /etc/hostname reading failed the build system!${reset}"
   fi
   if [ "${build_system_hostname:-}" = "" ]; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: /etc/hostname file is empty on build system!${reset}"
   fi
   true "INFO: /etc/hostname check ok."
}

check-mailname() {
   ## Silence noisy pbuilder warning:
   ## W: No local /etc/mailname to copy, relying on /var/cache/pbuilder/cow.cow_amd64/cow.950654/etc/mailname to be correct
   if ! test -f /etc/mailname ; then
      true "INFO: File /etc/mailname did not exist. Creating empty one...."
      ${SUDO_TO_ROOT} touch /etc/mailname
   fi
}

check-source-folder-permissions() {
   ## Debugging.
   true "${bold}${cyan}INFO: PWD: ${PWD} ${reset}"

   ## Checking if derivative-maker source folder has been obtained (git cloned) as user or root.

   stat_output_owner="$(stat -c %U "${BASH_SOURCE[0]}")"
   stat_output_group="$(stat -c %G "${BASH_SOURCE[0]}")"

   if [ "${stat_output_owner:-}" = "root" ]; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: Is owned by root user! Instructions say you should get derivative-maker source code as user, not root! \
Please delete derivative-maker source code folder and get it again as user, not root!${reset}"
   fi

   if [ "${stat_output_group:-}" = "root" ]; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: Is owned by root group! Instructions say you should get derivative-maker source code as user, not root! \
Please delete derivative-maker source code folder and get it again as user, not root!${reset}"
   fi

   test -x "${debsign_wrapper}"
   test -x "${sq_git_wrapper}"

   true
}

check-sufficient-disk() {
   ## Fail early when the build output folder lacks the disk space a full build
   ## needs.
   local build_folder avail_kib avail_gib minimum_gib
   minimum_gib="${dist_build_min_free_gib}"
   build_folder="${binary_build_folder_dist:-}"
   if [ -z "${build_folder}" ] || [ ! -d "${build_folder}" ]; then
      build_folder="${HOME}"
   fi
   avail_kib="$(df --output=avail -k -- "${build_folder}" 2>/dev/null | tail -n 1 | tr -d '[:space:]')"
   if [ -z "${avail_kib}" ]; then
      true "${bold}${cyan}INFO: ${BASH_SOURCE[0]}: could not read free disk for '${build_folder}'; skipping the disk check.${reset}"
      return 0
   fi
   avail_gib=$(( avail_kib / 1024 / 1024 ))
   true "${bold}${cyan}INFO: ${BASH_SOURCE[0]}: ${avail_gib} GiB disk space free in '${build_folder}' (minimum ${minimum_gib} GiB).${reset}"
   if [ "${avail_gib}" -lt "${minimum_gib}" ]; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: only ${avail_gib} GiB free in the build output folder \
'${build_folder}', but at least ${minimum_gib} GiB needed.${reset}"
   fi
}

check-stray-loop-devices() {
   true "INFO: Checking for stray loop devices..."

   local losetup_output
   losetup_output=$(${SUDO_TO_ROOT} losetup --all)

   if [ "${losetup_output:-}" = "" ]; then
      true "INFO: Output of losetup_output is empty. No stray loop devices, OK."
      return 0
   fi

   ## TODO: ignore /var/swapfile loop device
   ## losetup_output:
   ## /dev/loop0: [64769]:2097454 (/var/swapfile)

   true "INFO: Stray loop devices detected!

losetup_output: '${losetup_output}'

This has been detected by the following command... To reproduce this manually, run...

sudo losetup --all ; echo $?

expected result:
0

In other words, this build script as currently implemented expects that there are no open loop devices.
'sudo losetup --all' should not have any output and exit with exit code 0.

If there is a legitimate loop device, this message can be ignored.

Potential causes:
- A previously broken or aborted build might result in a stray loop device.

Recommendation:
- Reboot. Often a reboot is required to get rid of the stray loop device."

   true
}

check-umask-in-sync() {
   ## Ensure our umask is the same permissive value that security-misc's
   ## sudoers config would set.
   ##
   ## Don't check for a umask pin by parsing help-steps/variables. Parsing like
   ## this would be unreliable, and our code shouldn't (and can't) internally
   ## distrust itself.

   true "INFO: Checking that the build umask matches security-misc..."

   local sudoers_file security_misc_umask current_umask sanity_line

   sudoers_file="${source_code_folder_dist}/packages/kicksecure/security-misc/etc/sudoers.d/security-misc#security-misc-shared"
   variables_file="${source_code_folder_dist}/help-steps/variables"

   if [ ! -f "${sudoers_file}" ]; then
      ## This is a fatal error; if the submodule isn't checked out, the build
      ## would fail later.
      error "security-misc sudoers drop-in absent! Submodule not checked out?"
      return 0
   fi

   security_misc_umask=""
   while IFS= read -r sanity_line; do
      case "${sanity_line}" in
         Defaults*umask=*)
            security_misc_umask="${sanity_line#*umask=}"
            security_misc_umask="${security_misc_umask%%[^0-7]*}"
            break
            ;;
      esac
   done < "${sudoers_file}"

   if [ -z "${security_misc_umask}" ]; then
      true "INFO: no 'Defaults umask=' found in security-misc; nothing to compare. OK."
      return 0
   fi
   ## Trim off leading zeros. This value will be compared with the output of
   ## the umask command, and the number of leading zeros it prints can vary.
   [[ "${security_misc_umask}" =~ ([^0]*)$ ]]
   security_misc_umask="${BASH_REMATCH[1]}"
   ## Intentionally do not check for a non-empty security_misc_umask here.
   ## 0000 is a technically valid umask value, which would make this empty.

   current_umask="$(umask)"
   ## Also trim leading zeros from the umask command's output.
   if [ -z "${current_umask}" ]; then
      error "Cannot get current umask!"
      return 0
   fi
   [[ "${current_umask}" =~ ([^0]*)$ ]]
   current_umask="${BASH_REMATCH[1]}"

   if [ ! "${current_umask}" = "${security_misc_umask}" ]; then
      error "Current umask is different from security-misc! Current umask is '${current_umask}', expected umask is '${security_misc_umask}'."
      return 0
   fi

   true "INFO: current umask ${current_umask} matches security-misc, OK."
}

check-stale-nbd() {
   true "INFO: Checking for stale nbd devices..."

   local nbd_mounts

   if [ ! -e /dev/nbd0 ]; then
      true "INFO: no nbd devices present, OK."
      return 0
   fi

   nbd_mounts="$(${SUDO_TO_ROOT} cat /proc/mounts | grep -- '^/dev/nbd' || true)"

   if [ -z "${nbd_mounts}" ]; then
      true "INFO: no stale nbd mounts, OK."
      return 0
   fi

   error "Stale nbd mount(s) detected! Mounts: '${nbd_mounts}'. Use dm-nbd-cleanup to release them."
}

check-stray-mounts() {
   true "INFO: Checking for mounts..."

   local chroot_folder_base_name proc_mounts_grep_result

   if [ "${CHROOT_FOLDER:-}" = "" ]; then
      true "INFO: CHROOT_FOLDER variable is empty (probably --target root). Skip test for stray mounts, OK."
      return 0
   fi

   chroot_folder_base_name="$(basename "${CHROOT_FOLDER}")"

   proc_mounts_grep_result=$(${SUDO_TO_ROOT} cat /proc/mounts | grep -i -- "${chroot_folder_base_name}") || true

   if [ "${proc_mounts_grep_result:-}" = "" ]; then
      true "INFO: Output of proc_mounts_grep_result is empty. No stray mounts, OK."
      return 0
   fi

   error "Stray mounts detected!

proc_mounts_grep_result: '${proc_mounts_grep_result}'

This has been detected by the following command... To reproduce this manually, run...

sudo cat /proc/mounts | grep '${CHROOT_FOLDER}' ; echo $?

expected result:
1

Potential causes:
- A previously broken or aborted build might result in a stray mount.

Recommendation:
- Reboot. Often a reboot is required to get rid of the stray mount."

   true
}

mount-test() {
   local test_img="" test_dir="" part_dev="" part_name kpartx_out loop_assoc mt_loop

   _mt_cleanup() {
      if [ -n "${test_dir}" ]; then
         ${SUDO_TO_ROOT} umount -- "${test_dir}" 2>/dev/null || true
         ${SUDO_TO_ROOT} rmdir -- "${test_dir}" 2>/dev/null || true
         test_dir=""
      fi
      if [ -n "${test_img}" ]; then
         ${SUDO_TO_ROOT} kpartx -d -s -v -- "${test_img}" 2>/dev/null || true
         ## kpartx is supposed to remove the loop device too, but try manually
         ## removing too it just in case. NOTE: 'losetup --detach' takes the loop
         ## device as a plain operand and does NOT honor a '--' separator (it would
         ## parse '--' AS the device: "losetup: /dev/--: detach failed").
         while read -r mt_loop; do
            [ -n "${mt_loop}" ] || continue
            ${SUDO_TO_ROOT} losetup --detach "${mt_loop}" 2>/dev/null || true
         done < <(${SUDO_TO_ROOT} losetup --associated "${test_img}" --noheadings --output NAME 2>/dev/null)
         ${SUDO_TO_ROOT} safe-rm --force -- "${test_img}" 2>/dev/null || true
         test_img=""
      fi
   }

   true "INFO: device-mapper / loop-partition self-test..."

   ## device-mapper reachable? 1100 runs BEFORE the build sets up the dm stack,
   ## and in some build containers (the CI docker image) dm is not reachable at
   ## this stage even though loop is (dmsetup version fails). A reachable driver
   ## is a PRECONDITION for the self-test, not something to fail on here, so SKIP
   ## (not error) when absent -- else every CI build dies at 1100. A merely-WEDGED
   ## device still has a reachable driver, so the stale-mapping scan + self-test
   ## below still run where they matter.
   if ! ${SUDO_TO_ROOT} dmsetup version >/dev/null 2>&1; then
      true "${bold}${cyan}INFO: ${BASH_SOURCE[0]}: device-mapper not reachable at this \
stage (set up later in the build); skipping dm/loop self-test.${reset}"
      return 0
   fi

   ## mke2fs (e2fsprogs) is a PRECONDITION for the filesystem-creation part below,
   ## but is not shipped in every build container at this stage -- the build's real
   ## mkfs runs later inside grml's chroot, which has it. Skip (not fail) when it is
   ## absent, mirroring the dm-reachable skip above, so a container without
   ## e2fsprogs does not fail 1100 on a tool it legitimately lacks here.
   if ! ${SUDO_TO_ROOT} sh -c 'command -v mke2fs' >/dev/null 2>&1; then
      true "${bold}${cyan}INFO: ${BASH_SOURCE[0]}: mke2fs (e2fsprogs) not available at \
this stage; skipping dm/loop self-test.${reset}"
      return 0
   fi

   ## Check for stale loop partition device-mapper devices.
   local stale_dm
   stale_dm="$(${SUDO_TO_ROOT} dmsetup ls 2>/dev/null | grep -E '^loop[0-9]+p[0-9]+' || true)"
   if [ -n "${stale_dm}" ]; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: stale device-mapper loop partition mapping(s) present:
${stale_dm}
Recommendation: reboot the build host to clear them, then retry.${reset}"
   fi

   ## Create a minimal, partitioned test image.
   test_img="$(${SUDO_TO_ROOT} mktemp)"
   if ! ${SUDO_TO_ROOT} truncate --size=32M -- "${test_img}"; then
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: could not size the test image.${reset}"
   fi
   if ! ${SUDO_TO_ROOT} parted --script -- "${test_img}" mklabel msdos mkpart primary ext2 1MiB 100% >/dev/null 2>&1; then
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: could not partition the test image.${reset}"
   fi

   ## Attach the image's partition via device-mapper.
   if ! kpartx_out="$(${SUDO_TO_ROOT} kpartx -a -s -v -- "${test_img}" 2>&1)" ; then
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: kpartx -a failed on the test image. \
Output: ${kpartx_out}${reset}"
   fi
   part_name="$(printf '%s\n' "${kpartx_out}" | grep -o -E 'loop[0-9]+p[0-9]+' | head -n 1 || true)"
   if [ -z "${part_name}" ]; then
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: kpartx -a mapped no partition. Output: ${kpartx_out}${reset}"
   fi
   part_dev="/dev/mapper/${part_name}"
   if [ ! -b "${part_dev}" ]; then
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: partition node '${part_dev}' missing after kpartx -a.${reset}"
   fi

   ## Create an ext4 filesystem on the test image; surface mke2fs's OWN error on
   ## failure instead of swallowing it, so the cause is diagnosable rather than a
   ## bare "mke2fs failed".
   local mke2fs_out=""
   if ! mke2fs_out="$(${SUDO_TO_ROOT} mke2fs -F -q -- "${part_dev}" 2>&1)"; then
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: mke2fs failed on a freshly-mapped partition (mke2fs: ${mke2fs_out}). Recommendation: reboot the build host, then retry.${reset}"
   fi

   ## Mount and unmount the filesystem.
   test_dir="$(${SUDO_TO_ROOT} mktemp --directory)"
   if ! ${SUDO_TO_ROOT} mount -- "${part_dev}" "${test_dir}" 2>/dev/null; then
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: mounting the test partition failed.${reset}"
   fi
   ${SUDO_TO_ROOT} umount -- "${test_dir}" 2>/dev/null || true
   ${SUDO_TO_ROOT} rmdir -- "${test_dir}" 2>/dev/null || true
   test_dir=""

   ## Detach the test image and verify teardown. loop_teardown_verify
   ## (help-steps/misc-helpers.bsh) tolerates the benign Qubes/Xen/docker case
   ## where 'kpartx -d' leaves the backing loop attached (an explicit
   ## 'losetup --detach' plus a bounded re-poll releases it), but still fails on a
   ## genuinely stuck device.
   ${SUDO_TO_ROOT} kpartx -d -s -v -- "${test_img}" 2>/dev/null || true
   if ! loop_teardown_verify "${test_img}" ; then
      loop_assoc="$(${SUDO_TO_ROOT} losetup --associated "${test_img}" --noheadings --output NAME 2>/dev/null || true)"
      _mt_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: device-mapper/loop teardown did not release cleanly, loop device '${loop_assoc}' still attached. Recommendation: reboot the build host, then retry.${reset}"
   fi

   _mt_cleanup
   true "${bold}${cyan}INFO: device-mapper / loop partition self-test OK.${reset}"
}

check-build-primitives() {
   ## Fail fast on a broken build /dev mount.
   local loop_test_file="" loop_dev="" loop_rc mount_test_dir=""

   ## Idempotent cleanup function.
   _cbp_cleanup() {
      if [ -n "${mount_test_dir}" ]; then
         ${SUDO_TO_ROOT} umount -- "${mount_test_dir}" 2>/dev/null || true
         ${SUDO_TO_ROOT} rmdir -- "${mount_test_dir}" 2>/dev/null || true
         mount_test_dir=""
      fi
      if [ -n "${loop_dev}" ]; then
         ${SUDO_TO_ROOT} losetup --detach "${loop_dev}" 2>/dev/null || true
         loop_dev=""
      fi
      if [ -n "${loop_test_file}" ]; then
         ${SUDO_TO_ROOT} rm --force -- "${loop_test_file}" 2>/dev/null || true
         loop_test_file=""
      fi
   }

   ## /dev/null must be a writable character device.
   if [ ! -c /dev/null ]; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: /dev/null is not a character device on \
the build host. Fix the build environment's /dev.${reset}"
   fi
   if ! printf '%s\n' "test" > /dev/null 2>&1; then
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: writing to /dev/null failed. \
Fix the build environment's /dev.${reset}"
   fi

   ## A newly-created loop device's /dev node must actually appear.
   ##
   ## Create the backing file (and the mount dir below) as root via mktemp: security-misc
   ## sets fs.protected_regular=2, which blocks even root from opening a file it does not
   ## own for writing inside the sticky /tmp.
   loop_test_file="$(${SUDO_TO_ROOT} mktemp)"
   if ! ${SUDO_TO_ROOT} truncate --size=10M -- "${loop_test_file}"; then
      _cbp_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: could not size the loop backing file (truncate) \
on the build host.${reset}"
   fi
   loop_rc=0
   loop_dev="$(${SUDO_TO_ROOT} losetup --find --show -- "${loop_test_file}" 2>/dev/null)" || loop_rc="$?"
   if [ "${loop_rc}" != "0" ] || [ -z "${loop_dev:-}" ]; then
      _cbp_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: could not allocate a loop device \
(losetup --find --show). In a container the build needs a host /dev + privileges \
('docker run --privileged --volume /dev:/dev').${reset}"
   fi
   if [ ! -b "${loop_dev}" ]; then
      _cbp_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: loop device '${loop_dev}' was allocated \
but its /dev node is missing (container /dev isolated?). In a container the build needs a \
host /dev + privileges ('docker run --privileged --volume /dev:/dev').${reset}"
   fi
   _cbp_cleanup

   ## mount must work.
   mount_test_dir="$(${SUDO_TO_ROOT} mktemp --directory)"
   if ! ${SUDO_TO_ROOT} mount --types tmpfs --options size=1M -- tmpfs "${mount_test_dir}" 2>/dev/null; then
      _cbp_cleanup
      error "${bold}${red}ERROR ${BASH_SOURCE[0]}: a tmpfs mount failed, the build's \
bind/loop mounts will fail. In a container the build needs --privileged (or the appropriate \
mount capabilities).${reset}"
   fi
   _cbp_cleanup

   true "${bold}${cyan}INFO: build-primitives check OK.${reset}"
}

check-copy-vms-into-raw() {
   if [ "${dist_build_source_run:-}" = "true" ]; then
      return 0
   fi

   if [ "${dist_build_type_short:-}" = "kicksecure" ]; then
      true "INFO: kicksecure does not copy VM images into the build, ok."
      return 0
   fi

   if [ ! "${dist_build_iso:-}" = "true" ]; then
      return 0
   fi

   ## TODO: VirtualBox support

   local help_text
   help_text="A much later build step would try to copy these into the raw image and fail. Therefore we test it already here and fail early.

##########
If you want to do a debug build, perhaps create empty files?

qemu-img create -f qcow2 ${copy_vms_into_raw_file_one} 1M
qemu-img create -f qcow2 ${copy_vms_into_raw_file_two} 1M
##########
If you want to do a debug build, manually set which files you like to copy into the raw image using build configuration variables?

copy_vms_into_raw_file_one=${binary_build_folder_dist}/Whonix-Gateway.qcow2 copy_vms_into_raw_file_two=${binary_build_folder_dist}/Whonix-Workstation.qcow2
##########"

   if [ ! -f "${copy_vms_into_raw_file_one}" ]; then
      error "Whonix VMs need to be build first before Whonix host can be build.

copy_vms_into_raw_file_one '${copy_vms_into_raw_file_one}' does not exist!

${help_text}"
   fi
   if [ ! -f "${copy_vms_into_raw_file_two}" ]; then
      error "Whonix VMs need to be build first before Whonix host can be build

copy_vms_into_raw_file_two '${copy_vms_into_raw_file_two}' does not exist!

${help_text}"
   fi

   true
}

install_required_packages() {
   ## XXX: Ideally this would happen in a different script without
   ##       "sanity-tests" in its name.

   ## Using the build system's already exiting APT sources list.
   ## As configured by the local system administrator.
   ##
   ## Not using the following command line options:
   #-o Dir::Etc::sourcelist="$dist_build_sources_list_primary" \
   #-o Dir::Etc::sourceparts="-" \
   ## Because dist_build_sources_list_primary requires approx.

   ${SUDO_TO_ROOT} \
      apt-get \
         "${DIST_APTGETOPT_WITHOUT_APT_CACHE[@]}" \
         update

   # shellcheck disable=SC2086
   ${SUDO_TO_ROOT} \
      apt-get \
         "${DIST_APTGETOPT_WITHOUT_APT_CACHE[@]}" \
         ${apt_unattended_opts} \
         --no-install-recommends \
         --yes \
         install \
         ${required_packages_list}
}

check_required_packages_installed() {
   required_packages_list="git time curl approx lsb-release fakeroot fasttrack-archive-keyring safe-rm gpg-sq kmod qemu-utils kpartx parted"
   local required_package_item

   # shellcheck disable=SC2086
   for required_package_item in ${required_packages_list} ; do
      ## 'dpkg-query' does not exit non-zero if package is absent on the system.
      if [ "$(dpkg-query --show --showformat='${Version}' "${required_package_item}")" = "" ] ; then
         true "INFO: Required package '${required_package_item}' missing."
         install_required_packages
         break
      fi
   done
}

main() {
   test-wc "$@"
   check-redistributable-builds-requirements "$@"
   check-git-folder "$@"
   check-operating-system-version "$@"
   check-sudo "$@"
   check-dpkg "$@"
   check-hostname "$@"
   check-mailname "$@"
   check-source-folder-permissions "$@"
   check-sufficient-disk "$@"
   check-stray-loop-devices "$@"
   check-umask-in-sync "$@"
   check-stale-nbd "$@"
   check-stray-mounts "$@"
   ## Install the required packages (check_required_packages_installed installs any
   ## missing -- incl. kpartx, parted, approx) BEFORE the checks below that USE them:
   ## mount-test invokes kpartx, and 1200's approx_proxy_setup needs approx. Running
   ## it AFTER mount-test (as before) failed a fresh machine with "kpartx: command
   ## not found" before this install ever ran.
   check_required_packages_installed "$@"
   mount-test "$@"
   check-build-primitives "$@"
   check-copy-vms-into-raw "$@"

   ## --sign-and-tag true: on a PREPARED build machine, create the signing key
   ## (idempotent) and sign+tag HEAD + submodules HERE, immediately before the verify
   ## below, so no separate signing-key-create / sign-and-tag pre-step is needed. This
   ## is the only spot the order holds: the sign must precede the verify it satisfies,
   ## and the git tree is already validated (check-git-folder ran above).
   ## PRECONDITION (identical to this step's own verify): the signing tools are already
   ## installed -- git_sanity_test needs sq/sq-git/sqop, and signing-key-create also
   ## needs signify-openbsd + sponge, all from prepare-build-machine (step 1200, which
   ## runs AFTER this step). So a never-prepared host must run prepare-build-machine
   ## first; this does NOT fix the pre-existing 1100-before-1200 dep ordering (which
   ## the verify already depends on too). No-op on an already-signed HEAD; both helpers
   ## are env-driven (dist_build_sign_and_tag) and argless like CI; sign-and-tag
   ## self-refuses a redistributable build. Gated on the dangerous --sign-and-tag flag.
   if [ "${dist_build_sign_and_tag}" = "true" ]; then
      "${dist_source_help_steps_folder}/signing-key-create"
      "${dist_source_help_steps_folder}/sign-and-tag"
   fi

   "${dist_source_help_steps_folder}/git_sanity_test" --mode all --context "main repo"

   true
}

main "$@"
