#!/bin/bash

## Copyright (C) 2023 - 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

true "$0: START"

run_cmd() {
  [ -n "${TESTING_MODE:-}" ] || TESTING_MODE=0
  if [ "${TESTING_MODE}" -eq 1 ]; then
    set +x
    printf '%s\n' "$*"
  else
    "$@"
  fi
}

true "INFO: 0: $0"

## Most of the arguments supported here are the ones parse-cmd handles, but we
## need to handle '--dry-run' and '--remote-derivative-packages' here for our
## own use, and need to parse and strip out '--show-steps', which is specific
## to this script.
forwarded_args=()
[ -n "${build_dry_run:-}" ] || build_dry_run='false'
while [ "$#" -ge 1 ]; do
  case "${1}" in
    --dry-run)
      case "${2:-}" in
        true|false)
          build_dry_run="${2}"
          ;;
        '')
          error '--dry-run requires an argument!'
          ;;
        *)
          error "--dry-run only accepts 'true' or 'false' as arguments!"
          ;;
      esac
      forwarded_args+=( "${1}" "${2}" )
      shift 2
      ;;
    --remote-derivative-packages)
      case "${2:-}" in
        true|false)
          remote_derivative_packages="${2}"
          ;;
        '')
          error '--remote-derivative-packages requires an argument!'
          ;;
        *)
          error "--remote-derivative-packages only accepts 'true' or 'false' as arguments!"
          ;;
      esac
      forwarded_args+=( "${1}" "${2}" )
      shift 2
      ;;
    --show-steps)
      export TESTING_MODE=1
      shift
      ;;
    *)
      forwarded_args+=( "${1}" )
      shift
      ;;
  esac
done
set -- "${forwarded_args[@]}"

if [ "${CI:-}" = "true" ]; then
  true "INFO: Skipping test if folder ~/.ssh exists on CI."
elif [ "${build_dry_run:-}" = "true" ]; then
  ## '--dry-run true' is documented (see 'help-steps/parse-cmd') as
  ## "useful for debugging dm-prepare-release"; rsync is mocked
  ## (see 'help-steps/variables' '$rsync_cmd' default for dry-run),
  ## so '~/.ssh' is not used for real uploads and its absence is not
  ## an error. Coordinated with
  ## 'build-steps.d/1100_sanity-tests'
  ## 'check-redistributable-builds-requirements()'.
  true "INFO: Skipping test if folder ~/.ssh exists in dry-run mode."
else
  run_cmd test -d ~/.ssh
fi

## architecture choice
## - If environment variable 'dist_build_target_arch' is set, honor it.
## - On CI, build ARM64 by default since it is the most recent architecture port.
## - Official builds default to Intel/AMD64 at the time of writing.
build_args=()
if ! [ "${dist_build_target_arch:-}" = "" ]; then
  architecture="${dist_build_target_arch}"
elif [ "${CI:-}" = "true" ]; then
  architecture=arm64
  ## Build the Kicksecure/Whonix derivative packages from source rather than
  ## pulling prebuilt ones from the remote repository: a CI (stage) build must be
  ## reproducible, and a reproducible image must not embed opaque prebuilt
  ## packages. Trade-off: slower (compiles all derivative packages).
  build_args+=(--remote-derivative-packages false)
  export dist_build_upgrade_build_machine='true'
else
  architecture="amd64"
fi
build_args+=(--arch "${architecture}")

## Ordered as follows for the following reasons:
## - 1) Build '--flavor kicksecure-lxqt' because building ISO, which is newer,
##      therefore more prone to build issues. Error out quicker is better.
## - 2) Build '--flavor whonix-gateway-lxqt' because it is required to be able to
##      run dm-prepare-release due to unified ova files.
## - 3) Build '--flavor whonix-workstation-lxqt' because it is the most
##      complex, because of downloading Tor Browser as well as when used with
##      '--target windows', because:
## - *_prepare-build-machine downloads VirtualBox-*-Win.exe
## - dm-prepare-release builds Whonix Windows Installer
## - 4) Build CLI flavors because these are the least likely to have build issues.
##
## NOTE: Fewer flavors are build on CI. If 'CI=true', 'help-steps/parse-cmd'
##       will inject 'build_dry_run="true"' (equivalent of '--dry-run true').
## 'flavors_list' may arrive as a space-separated SCALAR (an env var, e.g. from
## CI selecting a subset such as 'whonix-gateway-lxqt whonix-workstation-lxqt').
## Split it into the array the loops below iterate; a scalar iterated as
## "${flavors_list[@]}" would otherwise be ONE element and pass the whole string
## as a single '--flavor', which parse-cmd rejects. Unset -> the full default set.
if [ -n "${flavors_list:-}" ]; then
  read -r -a flavors_list <<< "${flavors_list}"
else
  flavors_list=(
    kicksecure-lxqt
    kicksecure-cli
    whonix-gateway-lxqt
    whonix-workstation-lxqt
    whonix-gateway-cli
    whonix-workstation-cli
  )
fi

flavor_built() {
  printf '%s\n' "${flavors_list[@]}" \
    | grep --fixed-strings --line-regexp -- "$1" >/dev/null 2>&1
}

build_upload_noninteractive=true
export build_upload_noninteractive

dist_build_redistributable=true
export dist_build_redistributable

if [ "${dist_build_redistributable:-}" = "true" ]; then
  build_args+=(--repo true)
  build_args+=(--tb closed)
  ## Install package 'firmware-nonfreedom' by default.
  ## - Applicable to '--arch' 'amd64' only
  ## - Applicable to for ISO builds only.
  ## See also:
  ## https://www.kicksecure.com/wiki/Dev/nonfree
  build_args+=(--freedom false)
fi

if [ "${remote_derivative_packages:-}" = "true" ] || [ "${CI:-}" = "true" ]; then
  ## Skip upload of debug images, and never upload from CI.
  rsync_cmd="true simulate-only"
  export rsync_cmd
fi

## Several build steps allow us to pass multiple targets at once rather than
## having to run them multiple times with different targets.
##
## Cannot mix,
## * 1) '--target iso', and,
## * 2) "${multi_target_args[@]}"
## because no ISO should be built for Whonix-Gateway, Whonix-Workstation.
## (Maybe in the future for Whonix-Host.)
## The ISO should only be built for Kicksecure-LXQt for now.
##
## The ISO target is always built, regardless of what other targets we are
## (not) building.
multi_target_args=()
case "${architecture:-}" in
  amd64)
    ## VirtualBox ova + qcow2. (amd64 for Windows, Linux, Mac)
    multi_target_args+=(
      "--target" "virtualbox"
      "--target" "qcow2"
    )
  ;;
  arm64)
    ## Only qcow2 for now; VirtualBox arm64 (Mac M1/M2) disabled pending CI.
    multi_target_args+=("--target" "qcow2")
  ;;
  *)
    ## '--target qcow2' might be the most universal.
    multi_target_args+=("--target" "qcow2")
  ;;
esac

## 'dist_build_multi_target_list', when set (even to empty), is the AUTHORITATIVE
## VM-target set for ALL steps -- the shared PREP included -- overriding the arch
## default above. So a qcow2-only build does not prep (and does not apt-install)
## VirtualBox in the cowbuilder chroot: '--target virtualbox' no longer leaks into
## the shared prep, and parse-cmd never sets dist_build_virtualbox. The always-on
## '--target iso' / '--target windows' the prep lines add are unaffected. Unset ->
## the arch default (build every image the arch ships).
if [ -n "${dist_build_multi_target_list+x}" ]; then
  multi_target_args=()
  # shellcheck disable=SC2086
  for multi_target_item in ${dist_build_multi_target_list}; do
    multi_target_args+=("--target" "${multi_target_item}")
  done
fi

############################################################
## Phase 1: per-flavor sanity tests.
############################################################

## '--target windows' is omitted from some of the commands below; when used
## everywhere, it pulls in the VirtualBox Windows installer download and an
## Authenticode verify that fetches CRLs over the network at verify time.
## This breaks offline builds. These files should be manually populated for an
## offline build.

for flavor_item in "${flavors_list[@]}"; do
  ## Sanity-test the target(s) actually built for this flavor. A non-empty
  ## multi_target_args (VM image build) is passed as-is -- deliberately WITHOUT an
  ## added '--target iso', so a Whonix-Gateway ISO sanity does not complain the VMs
  ## are not built yet. An EMPTY multi_target_args (dist_build_multi_target_list='',
  ## ISO-only build) would otherwise leave NO --target and parse-cmd rejects it
  ## ("--target must be ..."); handle that per-flavor in the else branch.
  if [ "${#multi_target_args[@]}" -gt 0 ]; then
    run_cmd ./build-steps.d/*_sanity-tests "${build_args[@]}" "${multi_target_args[@]}" --flavor "${flavor_item}" "$@"
  else
    ## Empty multi_target_args = ISO-only build. Only the Kicksecure ISO flavors
    ## (same gate as Phase 3 below) actually get an ISO build here, so sanity-test
    ## '--target iso' for them only. Other flavors build no image in this config;
    ## adding '--target iso' would set dist_build_iso=true and, for Whonix
    ## (dist_build_type_short=whonix), trip check-copy-vms-into-raw ("Whonix VMs
    ## need to be build first"), so skip their no-op sanity call.
    case "${flavor_item:-}" in
      kicksecure-lxqt|kicksecure-ci-tiny-do-not-use)
        run_cmd ./build-steps.d/*_sanity-tests "${build_args[@]}" --target iso --flavor "${flavor_item}" "$@"
        ;;
      *)
        true "INFO: ${flavor_item}: no image built in ISO-only config; skipping its sanity-test."
        ;;
    esac
  fi
done

############################################################
## Phase 2: shared one-time work.
############################################################

## Use both '--target iso' and the VM targets here, because ISO builds need
## additional dependencies.

run_cmd ./build-steps.d/*_prepare-build-machine "${build_args[@]}" --target iso "${multi_target_args[@]}" --flavor source "$@"
run_cmd ./build-steps.d/*_cowbuilder-setup "${build_args[@]}" --target iso --target windows "${multi_target_args[@]}" --flavor source "$@"
run_cmd ./build-steps.d/*_local-dependencies "${build_args[@]}" --target iso --target windows "${multi_target_args[@]}" --flavor source "$@"

## '--flavor source' would lead to variable dist_build_hostname being unset.
## Therefore using '--flavor kicksecure-cli' to create a raw base image.
run_cmd ./build-steps.d/*_create-raw-image "${build_args[@]}" --target iso "${multi_target_args[@]}" --flavor kicksecure-cli --base-image-role create "$@"

## '--target iso' is required to download calamares from backports.
run_cmd ./build-steps.d/*_create-debian-packages "${build_args[@]}" --target iso "${multi_target_args[@]}" --flavor source --skip-published-packages "$@"

############################################################
## Phase 3: per-flavor image builds.
############################################################

## Skip all the one-time steps we just did and ensure we reuse the base image.
reuse_shared_args=(
  --skip-prepare-build-machine
  --skip-cowbuilder-setup
  --skip-local-dependencies
  --skip-published-packages
  --base-image-role consume
)

for flavor_item in "${flavors_list[@]}"; do
  case "${flavor_item:-}" in
    kicksecure-lxqt|kicksecure-ci-tiny-do-not-use)
      ## ISO is built for Kicksecure LXQt and for the
      ## kicksecure-ci-tiny-do-not-use boot-test flavor. Not for Kicksecure
      ## CLI. Not yet for Whonix.
      true "INFO: ISO build flavor."
      run_cmd ./derivative-maker "${build_args[@]}" "${reuse_shared_args[@]}" --target iso --flavor "${flavor_item}" "$@"
      ;;
    *)
      true "INFO: Non-ISO build flavor."
      ;;
  esac

  if [ "${#multi_target_args[@]}" -gt 0 ]; then
    run_cmd ./derivative-maker "${build_args[@]}" "${reuse_shared_args[@]}" "${multi_target_args[@]}" --flavor "${flavor_item}" "$@"
  else
    true "INFO: dist_build_multi_target_list empty: no multi-target VM image build for ${flavor_item}."
  fi
done

if [ "${CI:-}" = "true" ]; then
  ## Get rid of the shared base image.
  find /home/user/derivative-binary -mindepth 2 -maxdepth 2 -name base-image.raw -delete 2>/dev/null || true
fi

############################################################
## Phase 4: source release + image upload.
############################################################

## Same source code for all of the following:
## - desktop: LXQt versus CLI
## - target: Kicksecure versus Whonix.
## - architecture: all
[ -n "${dist_build_source_release_flavor:-}" ] || dist_build_source_release_flavor='kicksecure-lxqt'

if flavor_built "${dist_build_source_release_flavor}"; then
  ## dm-prepare-release is only needed for the source archive, all other images
  ## have already been prepared at this point.
  run_cmd dm-prepare-release --target source --flavor "${dist_build_source_release_flavor}" "$@"
  run_cmd dm-upload-images --target source --flavor "${dist_build_source_release_flavor}" "$@"
else
  true "INFO: ${BASH_SOURCE[0]}: no source release: dist_build_source_release_flavor='${dist_build_source_release_flavor}' is not in flavors_list."
fi
## Route the (flavor-independent) source tarball to the whonix upload location
## too -- but ONLY when it was actually prepared above, i.e. when the
## source-release flavor is part of this build. A reduced build (e.g. the whonix
## pair without the source-release flavor) prepares no source, so uploading it
## would fail on a missing file.
if flavor_built "${dist_build_source_release_flavor}" && flavor_built "whonix-workstation-lxqt"; then
  run_cmd dm-upload-images --target source --flavor "whonix-workstation-lxqt" "$@"
fi

## TODO: Windows
# if [ "${CI:-}" = "true" ]; then
# #if [ "${architecture:-}" = "amd64" ]; then
#   ## Also build the Whonix-Windows-Installer.
#   run_cmd dm-prepare-release "${build_args[@]}" --target windows --flavor whonix-workstation-lxqt
#   run_cmd dm-upload-images "${build_args[@]}" --target windows --flavor whonix-workstation-lxqt
# fi

## Uploading.
for flavor_item in "${flavors_list[@]}"; do
  if printf '%s\n' "${flavor_item}" | grep -- "gateway" &>/dev/null; then
    ## Not needed for gateway due to unified images.
    continue
  fi

  case "${flavor_item:-}" in
    kicksecure-lxqt)
      true "INFO: ISO build flavor."
      run_cmd dm-upload-images "${build_args[@]}" --target iso --flavor "${flavor_item}" "$@"
      ;;
    *)
      true "INFO: Non-ISO build flavor."
      ;;
  esac

  if [ "${#multi_target_args[@]}" -gt 0 ]; then
    run_cmd dm-upload-images "${build_args[@]}" "${multi_target_args[@]}" --flavor "${flavor_item}" "$@"
  else
    true "INFO: dist_build_multi_target_list empty: no multi-target image upload for ${flavor_item}."
  fi
done

true "$0: END"
