#!/bin/bash

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

## This script is executed on the host with the purpose of setting up
## required volume directories and executing the docker run command with any given arguments.

## TODO:
## amd64 images build under a Debian 12 Docker image end up with no BIOS bootloader due to lsblk malfunctioning #348
## https://github.com/grml/grml-debootstrap/issues/348
## related:
## lsblk fails to report partition type UUIDs within a privileged Debian 12 container #50304
## https://github.com/moby/moby/issues/50304

set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

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

HOST_USER="$(id -u)"
HOST_GROUP="$(id -g)"
DOCKER_USER="user"
COMMAND=""
DOCKER_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"
SOURCE_VOLUME="$( dirname -- "${DOCKER_DIR}" )"
BINARY_VOLUME="${HOME}/binary_mnt"
CACHER_VOLUME="${HOME}/approx_cache_mnt"
## TODO: Since this takes over the container's ~/.local/share, it's going to
## end up with a lot of non-key-related junk in it most likely. Maybe split
## this into two volumes, one for Sequoia-PGP's private keys and one for public
## keys?
KEY_VOLUME="${HOME}/.key_mnt"
GNUPG_VOLUME="${HOME}/.gnupg_mnt"
image_ref="$("${MYDIR}"/derivative-maker-docker-image-ref)"
image_arch="${image_ref##*:}"
[ -v CI ] || CI=""

if [ "${CI:-}" = "true" ]; then
  sudo_options_maybe+=("--non-interactive")
fi
[ -v sudo_options_maybe ] || sudo_options_maybe=()

sudo "${sudo_options_maybe[@]}" test -d /usr


print_usage() {
  cat -- << EOF
  Usage: $0 [ options ] -- COMMAND [ ARGS ]

  The command to run in the container is explicit, after '--'.

  Options:
    --binary-mount DIR : binary artifact directory
    --cacher-mount DIR : package cache directory
    --key-mount DIR    : Sequoia-PGP keystore directory
    --gnupg-mount DIR  : GnuPG keystore directory
    -h|--help          : this help
EOF
}

usage_error() {
  print_usage >&2
  printf '%s\n' "ERROR: $*" >&2
  exit 1
}

## Reject a mount source Docker would misinterpret: a non-absolute path is
## treated as a named volume (silently not the host directory), and a ':' splits
## the '-v' spec. Require an absolute, colon-free path.
validate_mount_path() {
  if [[ "${1}" != /* ]]; then
    usage_error "mount path must be an absolute path: $1"
  elif [[ "${1}" = *:* ]]; then
    usage_error "mount path must not contain a colon: $1"
  fi
}

volume_prepare() {
  declare -a -- VOLUMES

  ## CACHER_VOLUME is owned by the in-container 'approx' user; its uid:gid is
  ## pinned to 101:102 at image build (derivative-maker-docker-setup) so this
  ## literal stays correct. Kept in sync with ci/approx-cache-sidecar.
  VOLUMES=(
    "${CACHER_VOLUME}" "101:102" "770"
    "${BINARY_VOLUME}" "${HOST_USER}:${HOST_GROUP}" "770"
    "${KEY_VOLUME}" "${HOST_USER}:${HOST_GROUP}" "700"
    "${GNUPG_VOLUME}" "${HOST_USER}:${HOST_GROUP}" "700"
  )

  while (( ${#VOLUMES[@]} > 0 )); do
    ## Only create-and-own a directory we are making fresh so we don't break
    ## things if the user makes a typo.
    if [ ! -d "${VOLUMES[0]}" ]; then
      mkdir --parents -- "${VOLUMES[0]}"
      sudo "${sudo_options_maybe[@]}" -- chown --recursive -- "${VOLUMES[1]}" "${VOLUMES[0]}"
      sudo "${sudo_options_maybe[@]}" -- chmod --recursive -- "${VOLUMES[2]}" "${VOLUMES[0]}"
    fi

    VOLUMES=("${VOLUMES[@]:3}")
  done
}

## This lets us tell two images built from different pacakge snapshots from
## each other.
DM_SNAPSHOT_LABEL='org.kicksecure.derivative-maker.frozen-snapshot'

frozen_snapshot_timestamp() {
  local pin_file timestamp
  pin_file="${SOURCE_VOLUME}/build_sources/frozen-snapshot-timestamp"
  [ -f "${pin_file}" ] || return 1
  timestamp="$(< "${pin_file}")"
  timestamp="${timestamp//[[:space:]]/}"

  if [[ "${timestamp}" =~ ^(0|[1-9][0-9]*)$ ]]; then
    date --utc --date="@${timestamp}" '+%Y%m%dT%H%M%SZ'
    return 0
  fi
  return 1
}

build_docker_image() {
  ## Hoist the subshell out of the command arguments so 'set -o errexit'
  ## catches a failing 'id' instead of silently passing an empty --build-arg.
  local dm_uid existing_image frozen_snapshot image_snapshot
  dm_uid="$(id -u)"
  frozen_snapshot="$(frozen_snapshot_timestamp)"
  existing_image="$(sudo "${sudo_options_maybe[@]}" -- docker images --quiet -- "${image_ref}" 2>/dev/null)" || true
  if [ -n "${existing_image}" ]; then
    ## Check if the image is up-to-date.
    image_snapshot="$(sudo "${sudo_options_maybe[@]}" -- docker image inspect \
      --format "{{index .Config.Labels \"${DM_SNAPSHOT_LABEL}\"}}" \
      -- "${image_ref}" 2>/dev/null)" || image_snapshot=""
    if [ ! "${image_snapshot}" = "${frozen_snapshot}" ]; then
      printf '%s\n' "${0##*/}: rebuilding the docker image: pinned to '${image_snapshot:-none}', sources say '${frozen_snapshot}'" >&2
      existing_image=""
    fi
  fi
  if [ -z "${existing_image}" ]; then
    sudo \
    "${sudo_options_maybe[@]}" \
    -- \
      docker \
        build \
        --build-arg \
        DM_UID="${dm_uid}" \
        --label \
        "${DM_SNAPSHOT_LABEL}=${frozen_snapshot}" \
        --tag \
        "${image_ref}" \
        --file \
        "${DOCKER_DIR}/Dockerfile" \
        "${SOURCE_VOLUME}"
  fi
}

saw_dashdash="false"

while true; do
  case "${1:-}" in
    --binary-mount)
      [ "$#" -ge 2 ] || usage_error "--binary-mount requires a value."
      validate_mount_path "${2}"
      BINARY_VOLUME="${2}"
      shift 2
      ;;
    --cacher-mount)
      [ "$#" -ge 2 ] || usage_error "--cacher-mount requires a value."
      validate_mount_path "${2}"
      CACHER_VOLUME="${2}"
      shift 2
      ;;
    --key-mount)
      [ "$#" -ge 2 ] || usage_error "--key-mount requires a value."
      validate_mount_path "${2}"
      KEY_VOLUME="${2}"
      shift 2
      ;;
    --gnupg-mount)
      [ "$#" -ge 2 ] || usage_error "--gnupg-mount requires a value."
      validate_mount_path "${2}"
      GNUPG_VOLUME="${2}"
      shift 2
      ;;
    -h|--help)
      print_usage
      exit 0
      ;;
    --)
      saw_dashdash="true"
      shift
      break
      ;;
    -*)
      usage_error "unknown option: $1"
      ;;
    *)
      break
      ;;
  esac
done

## The command to run in the container is explicit and must follow '--'.
if [ "${saw_dashdash}" != "true" ] || [ "$#" -lt 1 ]; then
  usage_error "no command given: pass -- COMMAND [ARGS] (see --help)."
fi
COMMAND="${1}"
shift

## The default values for the various volumes can contain characters that make
## Docker misbehave, so recheck all the volume strings.
for volume_path in "${SOURCE_VOLUME}" "${BINARY_VOLUME}" "${CACHER_VOLUME}" "${KEY_VOLUME}" "${GNUPG_VOLUME}"; do
  validate_mount_path "${volume_path}"
done

## The container intentionally does NOT run derivative-update to fetch/merge
## git submodules. It builds the source tree exactly as bind-mounted from the
## host (SOURCE_VOLUME); keeping that checkout current is the host's
## responsibility.

build_docker_image

volume_prepare

## We can't load kernel modules from within a container, so load them now. nbd
## is needed by dm-reproducible-compare-artifacts.
sudo "${sudo_options_maybe[@]}" -- modprobe -a loop dm_mod
sudo "${sudo_options_maybe[@]}" -- modprobe nbd max_part=16 || true

docker_run_opts=()

## In CI environments, stdin is /dev/null and
## there is no controlling terminal.  'docker run --interactive --tty'
## requires a real TTY on stdin; without one Docker prints
## "the input device is not a TTY" and exits non-zero.
if [ "${CI:-}" = "true" ]; then
  ## Keep --tty: it allocates a pty so the systemd entrypoint service streams
  ## build output to docker stdout (live CI logs). Drop --interactive: it
  ## needs a real host TTY. --env CI=true tells the container it is headless.
  docker_run_opts+=( --tty --env 'CI=true' )
else
  docker_run_opts+=( --interactive --tty )
fi

sudo \
  "${sudo_options_maybe[@]}" \
  -- \
    docker \
      run \
      --name "derivative-maker-docker-${image_arch}-$$" \
      "${docker_run_opts[@]}" \
      --rm \
      --pull=never \
      --privileged \
      --volume /dev:/dev \
      --env 'flavor_meta_packages_to_install=' \
      --env 'install_package_list=' \
      --env 'DERIVATIVE_APT_REPOSITORY_OPTS=' \
      --volume "${SOURCE_VOLUME}:/home/${DOCKER_USER}/derivative-maker" \
      --volume "${BINARY_VOLUME}:/home/${DOCKER_USER}/derivative-binary" \
      --volume "${CACHER_VOLUME}:/var/cache/approx-derivative-maker" \
      --volume "${KEY_VOLUME}:/home/${DOCKER_USER}/.local/share" \
      --volume "${GNUPG_VOLUME}:/home/${DOCKER_USER}/.gnupg" \
      "${image_ref}" \
        sudo \
          --non-interactive \
          --preserve-env \
          -u "${DOCKER_USER}" \
          user_name="${DOCKER_USER}" \
          -- \
            "/usr/bin/derivative-maker-docker-start" "${COMMAND}" "${@}"
