#!/bin/bash

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

## Intentional word-splitting of cowbuilder command arguments (e.g. the
## optional COWBUILDER_PREFIX); quoting them would break the call.
# shellcheck disable=SC2086

## example usage:

#make_cross_build_platform_list="armel armhf amd64 arm64" ./build-steps.d/*_cowbuilder-setup --allow-untagged true --allow-uncommitted true --flavor source --target root

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

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 ..

if [ "${dist_build_skip_cowbuilder_setup:-}" = "true" ]; then
   true "${bold}${cyan}INFO: run with '--skip-cowbuilder-setup' switch, skipping ${BASH_SOURCE[0]}.${reset}"
   exit 0
fi

source_date_epoch_test() {
   if [ "${SOURCE_DATE_EPOCH:-}" = "" ]; then
      error "SOURCE_DATE_EPOCH is still unset!"
   fi
   true "INFO: SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH}"
}

cowbuilder_setup() {
   if cowbuilder_setup_do ; then
      true "${FUNCNAME[0]}: success"
   else
      true "${FUNCNAME[0]}: try again"
      cowbuilder_setup_do
   fi
}

cowbuilder_setup_do() {
   true "${bold}${green}INFO${reset}: Setting up cowbuilder..."

   dist_build_sources_list_primary_contents="$(cat -- "${dist_build_sources_list_primary}")"
   export dist_build_sources_list_primary_contents

   if [ "${dist_build_unsafe_io:-}" = "true" ]; then
      eatmydata_maybe_install="eatmydata"
   fi

   if [ "${dist_build_unsafe_io:-}" = "true" ]; then
      if [ "${DOCKER:-}" = "true" ]; then
         ## https://forums.whonix.org/t/docker-container-that-builds-whonix-images/17494/21
         true "INFO: Skip re-mounting '${cowbuilder_cache_dir}' as tmpfs because DOCKER=true, ok."
      else
         ## Create the mount point first: a relocated (non-default)
         ## '$cowbuilder_cache_dir' may not exist yet, and 'mount' fails on a
         ## missing target. Harmless for the default '/var/cache/pbuilder'
         ## (already created by the 'pbuilder' package).
         ${SUDO_TO_ROOT} mkdir --parents -- "${cowbuilder_cache_dir}"
         if ${SUDO_TO_ROOT} mount | grep -- "${cowbuilder_cache_dir}" | grep -- tmpfs ; then
            true "INFO: '${cowbuilder_cache_dir}' already tmpfs."
         else
            ${SUDO_TO_ROOT} mount -t tmpfs -o size=50% none "${cowbuilder_cache_dir}"
         fi
      fi
   fi

   ## Debugging.
   ${SUDO_TO_ROOT} cat -- "${dist_build_pbuilder_config_file}"

   make_cross_build_platform_list_including_host_architecture+=" ${make_cross_build_platform_list} "

   ## VirtualBox on Linux ships an amd64 build only; there is no VirtualBox for
   ## Linux/arm64 (ARM64 VirtualBox exists solely on macOS). So an '--arch arm64
   ## --target virtualbox' build cannot install VirtualBox into an arm64 chroot
   ## and must build an additional amd64 chroot to produce the VirtualBox image.
   ## On an arm64 host that amd64 chroot is emulated (qemu-user); see the
   ## foreign-architecture handling in the per-architecture loop below.
   if [ "${dist_build_virtualbox:-}" = "true" ]; then
      if [ "${dist_build_target_arch:-}" = "arm64" ]; then
         true "${cyan}INFO: '--target virtualbox' and '--arch arm64', therefore also building a AMD64 chroot, because at time of writing VirtualBox is available only on ARM64 macOS but not for ARM64 Linux.${reset}"
         make_cross_build_platform_list_including_host_architecture+=" amd64 "
      fi
   fi

   true "INFO: Also adding host_architecture to variable make_cross_build_platform_list_including_host_architecture, so local dependencies can be built."
   make_cross_build_platform_list_including_host_architecture+=" ${host_architecture} "

   true "INFO: Removing potential duplicates in variable make_cross_build_platform_list_including_host_architecture."
   make_cross_build_platform_list_including_host_architecture=$(printf "%s\n" "${make_cross_build_platform_list_including_host_architecture}" | tr ' ' '\n' | sort | uniq | tr '\n' ' ')

   for dist_build_multiarch_package_item in ${make_cross_build_platform_list_including_host_architecture} ; do
      true "dist_build_multiarch_package_item: ${dist_build_multiarch_package_item}"
      export dist_build_multiarch_package_item

      ## Foreign-architecture chroot: qemu-user emulation and setuid.
      ##
      ## When the chroot architecture differs from the build host architecture
      ## (for example the amd64 VirtualBox chroot on an arm64 host, see above),
      ## its binaries run under qemu-user emulation via binfmt_misc. setuid
      ## binaries such as 'sudo' only gain root under emulation when the qemu
      ## handler carries the 'C' (credentials) flag; without it the kernel
      ## derives credentials from the unprivileged qemu interpreter rather than
      ## the setuid target, and 'sudo' fails later, deep inside
      ## dist-installer-cli's get_su_cmd, with a misleading 'sudo: effective uid
      ## is not 0 ... nosuid' error.
      ##
      ## This affects any cross-architecture build (CI and developer hosts
      ## alike): the build installs 'qemu-user-binfmt', whose systemd-binfmt
      ## registration ships flags 'OPF' (no credentials). Rather than abort and
      ## ask the operator to reconfigure binfmt_misc by hand, add the flag
      ## automatically via help-steps/binfmt-credential-setup (an /etc/binfmt.d
      ## override reusing the vendor spec, re-applied with systemd-binfmt). Runs
      ## after the qemu-user-binfmt install in 1100_sanity-tests and before this
      ## chroot is bootstrapped.
      if [ "${dist_build_multiarch_package_item}" != "${host_architecture}" ]; then
         ## '|| true' so an unmapped architecture (function returns non-zero,
         ## empty output) does not trip errexit; handled by the check below.
         qemu_emulation_machine="$(dist_build_arch_to_qemu_machine "${dist_build_multiarch_package_item}")" || true
         if [ -n "${qemu_emulation_machine}" ]; then
            true "${cyan}INFO: Foreign-architecture chroot '${dist_build_multiarch_package_item}' on host '${host_architecture}' runs under qemu-user emulation; ensuring the qemu binfmt handler carries the 'C' (credentials) flag so setuid (sudo) works inside it.${reset}"
            ${SUDO_TO_ROOT} "${dist_source_help_steps_folder}/binfmt-credential-setup" "qemu-${qemu_emulation_machine}"
         else
            true "${cyan}INFO: No qemu machine mapping for architecture '${dist_build_multiarch_package_item}'; skipping binfmt credentials setup.${reset}"
         fi
      fi

      ## Implemented in help-steps/variables.
      ## sets:
      ## cow_folder
      ## base_folder
      set_cowbuilder_folders

      ## The cowbuilder base needs a 'dev'- and 'suid'-capable filesystem, like a normal
      ## (e.g. CI) build host. Qubes mounts '/home' 'nosuid,nodev', and under Docker the
      ## base lives on the bind-mounted build volume there.
      ##
      ## ${cowbuilder_cache_dir} is relocated to
      ## ${binary_build_folder_dist}/pbuilder when --reuse-cowbuilder-base is
      ## passed, and therefore it may not exist yet. Create it if needed.
      ${SUDO_TO_ROOT} mkdir --parents -- "${cowbuilder_cache_dir}"
      local cowbuilder_base_mount_options
      cowbuilder_base_mount_options="$(${SUDO_TO_ROOT} findmnt --noheadings --output OPTIONS --target "${cowbuilder_cache_dir}")"
      case ",${cowbuilder_base_mount_options}," in
         *,nodev,*|*,nosuid,*)
            true "${cyan}INFO: cowbuilder base filesystem '${cowbuilder_cache_dir}' is mounted 'nodev' and/or 'nosuid'; clearing so the chroot has working device nodes and setuid.${reset}"
            ${SUDO_TO_ROOT} mount --bind -- "${cowbuilder_cache_dir}" "${cowbuilder_cache_dir}"
            ${SUDO_TO_ROOT} mount -o remount,bind,dev,suid "${cowbuilder_cache_dir}"
            ;;
      esac

      ## If --reuse-cowbuilder-base was passed, reuse an existing cowbuilder
      ## chroot if it exists and is usable.
      local cowbuilder_base_reusable
      cowbuilder_base_reusable=false
      if [ "${dist_build_reuse_cowbuilder_base:-}" = "true" ] && [ -d "${base_folder}" ]; then
         if ${SUDO_TO_ROOT} test -c "${base_folder}/dev/ptmx"; then
            true "${cyan}INFO: --reuse-cowbuilder-base: reusing existing cowbuilder base '${base_folder}'.${reset}"
            cowbuilder_base_reusable=true
         else
            true "${cyan}INFO: --reuse-cowbuilder-base: existing base '${base_folder}' is broken (no '/dev/ptmx' character device); rebuilding.${reset}"
         fi
      fi

      if [ "${cowbuilder_base_reusable}" = "false" ]; then
         if [ "${dist_build_reuse_cowbuilder_base:-}" = "true" ] && [ ! -d "${base_folder}" ]; then
            true "${cyan}INFO: --reuse-cowbuilder-base requested but no base at '${base_folder}'; building a fresh one.${reset}"
         fi
         if [ -d "${base_folder}" ]; then
            ${SUDO_TO_ROOT} "${dist_source_help_steps_folder}/mount-cleanup" -- "${base_folder}"
            ## unmount-tree exits non-zero while anything is still mounted
            ## under the tree, so the delete cannot follow a leftover bind
            ## mount out of it. --one-file-system is not enough to protect us
            ## from a bind mount linking to important files.
            ${SUDO_TO_ROOT} "${dist_source_help_steps_folder}/unmount-tree" -- "${base_folder}"
            ${SUDO_TO_ROOT} safe-rm --one-file-system --recursive --force -- "${base_folder}"
         fi

         local cowbuilder_exit_code
         cowbuilder_exit_code=0

         ## '--mirror "$dist_build_apt_sources_mirror"' should be only cosmetic,
         ## because of mmdebstrap wrapper and hooks.
         ##
         ## When '--unsafe-io true' is enabled, help-steps/variables sets a
         ## global LD_PRELOAD=.../libeatmydata.so. That library is only
         ## installed into the chroot late in the bootstrap process, so we
         ## need to disable the preload during the bootstrap.
         ##
         ## Duplicate --extrapackages to prevent removal by pbuilder.
         ${SUDO_TO_ROOT} \
            ${COWBUILDER_PREFIX:-} \
               ${UNSHARE_PID_NS:-} \
               env --unset=LD_PRELOAD \
               cowbuilder \
                  --architecture "${dist_build_multiarch_package_item}" \
                  --configfile "${dist_build_pbuilder_config_file}" \
                  --create \
                  --basepath "${base_folder}" \
                  --buildplace "${cow_folder}" \
                  --distribution "${dist_build_apt_stable_release}" \
                  --mirror "${dist_build_apt_sources_mirror}" \
                  --debootstrap "${dist_source_help_steps_folder}/mmdebstrap" \
                  --hookdir "${dist_source_help_steps_folder}/pbuilder-hooks" \
                  --extrapackages "sudo devscripts debhelper strip-nondeterminism fakeroot apt-transport-tor eatmydata aptitude cowdancer fasttrack-archive-keyring adduser sq qemu-utils safe-rm" \
                  || { cowbuilder_exit_code="$?" ; true; };

         if [ -d "${base_folder}" ]; then
            ${SUDO_TO_ROOT} "${dist_source_help_steps_folder}/mount-cleanup" -- "${base_folder}"
         fi

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

      true "INFO: Disable needless initramfs builds inside cowbuilder chroot (not supposed to get booted)."
      ${SUDO_TO_ROOT} mkdir --parents -- "${base_folder}/etc/dracut.conf.d"
      ${SUDO_TO_ROOT} -- cp --verbose -- "${source_dracut_disable_config_snippet}" "${base_folder}/etc/dracut.conf.d"

      ## help-steps/pbuilder-hooks/G10sources_list_restore.bsh should have restored it already.
      ## `pbuilder` component `/usr/lib/pbuilder/pbuilder-createbuildenv`
      ## unfortunately runs function `installaptlines` after `${DEBOOTSTRAP}`,
      ## which uses a different sources list. This pbuilder hook script restores
      ## derivative-maker build APT sources list.
      ##
      ## Out-commented. No longer necessary in Trixie as we now use deb822
      ## format sources files and delete sources.list.
      #true "INFO: Sanity test. Security critical. Comparing derivative-maker build sources list with chroot sources list."
      #$SUDO_TO_ROOT diff -- "$dist_build_sources_list_primary" "$base_folder/etc/apt/sources.list.d/derivative.sources"
      #$SUDO_TO_ROOT cp -- "$dist_build_sources_list_primary" "$base_folder/etc/apt/sources.list.d/derivative.sources"

      ${SUDO_TO_ROOT} mkdir --parents -- "${base_folder}/home/${user_name}"
      ${SUDO_TO_ROOT} cp -- "${dist_build_pbuilder_config_file}" "${base_folder}/home/${user_name}/pbuilder_config_file"
      ## retry_run helper so the in-chroot pbuilder-chroot-script-* can wrap
      ## their apt calls too (they cannot source help-steps/pre).
      ${SUDO_TO_ROOT} cp -- "${dist_source_help_steps_folder}/retry-run" "${base_folder}/home/${user_name}/retry-run"

      ## virtualbox-installer / dist-installer-cli
      dist_installer_cli_file_name="$(basename -- "${binary_image_installer_dist_source}")"
      ## example dist_installer_cli_file_name:
      ## dist-installer-cli-standalone

      ## copy dist-installer-cli-standalone into chroot home folder
      ## Might not be required for all platforms but depending on '"$dist_build_virtualbox" = "true"'
      ## might cause issues with reproducibility. Has no effect if not used.
      ## Copy unconditionally for simplicity.
      ${SUDO_TO_ROOT} cp -- "${binary_image_installer_dist_source}" "${base_folder}/usr/bin/${dist_installer_cli_file_name}"
      ${SUDO_TO_ROOT} chmod o+rx -- "${base_folder}/usr/bin/${dist_installer_cli_file_name}"
      ${SUDO_TO_ROOT} chown --recursive -- "${user_name}:${user_name}" "${base_folder}/home/${user_name}"

      cowbuilder_script_list=()
      cowbuilder_script_list+=("${dist_build_pbuilder_main_chroot_script}")

      if [ "${dist_build_virtualbox:-}" = "true" ]; then
         ## Install VirtualBox inside the chroot using 'dist-installer-cli'.
         ## Only install into the amd64 chroot:
         ## - VirtualBox binaries are available for amd64 only (no Linux arm64
         ##   build exists at time of writing), so installing into any
         ##   non-amd64 chroot would fail.
         ## - When '--target virtualbox --arch arm64' (for Mac M1/M2 .ova
         ##   output), an amd64 chroot is explicitly added above; the
         ##   arm64 .vdi is wrapped into an .ova using amd64 'VBoxManage'.
         if [ "${dist_build_multiarch_package_item}" = "amd64" ]; then
            cowbuilder_script_list+=("${dist_build_pbuilder_virtualbox_chroot_script}")
         else
            true "INFO: Skipping VirtualBox install in '${dist_build_multiarch_package_item}' chroot; VirtualBox is installed into the amd64 chroot only."
         fi
      fi

      for cowbuilder_execute_script in "${cowbuilder_script_list[@]}"; do
         cowbuilder_exit_code=0
         ${SUDO_TO_ROOT} \
            ${COWBUILDER_PREFIX:-} \
               ${UNSHARE_PID_NS:-} \
               cowbuilder \
                  --architecture "${dist_build_multiarch_package_item}" \
                  --configfile "${dist_build_pbuilder_config_file}" \
                  --execute \
                  --basepath "${base_folder}" \
                  --buildplace "${cow_folder}" \
                  --save-after-login \
                  -- \
                  "${cowbuilder_execute_script}" \
                  || { cowbuilder_exit_code="$?" ; true; };

         if [ -d "${base_folder}" ]; then
            ${SUDO_TO_ROOT} "${dist_source_help_steps_folder}/mount-cleanup" -- "${base_folder}"
         fi

         if [ ! "${cowbuilder_exit_code:-}" = "0" ]; then
            break
         fi
      done

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

   true "${bold}${green}INFO${reset}: cowbuilder setup done."
}

main() {
   source_date_epoch_test "$@"

   ## Not possible because we need *_local-dependencies build step.
   #if [ "${build_dry_run:-}" = "true" ]; then
      #true "${bold}${cyan}INFO: dry-run, skipping ${BASH_SOURCE[0]}. ${reset}"
      #return 0
   #fi

   cowbuilder_setup "$@"
}

main "$@"
