#!/bin/bash

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

## style-ok: no-strict
## Sourced-only function library (make-helper.bsh sources it); it must not set
## strict-mode, which would leak 'errexit'/'nounset'/'pipefail' into the
## sourcing shell.

#set -x

## Code duplication. Copied from
## helper-scripts//usr/libexec/helper-scripts/has.sh.
##
## Because has.sh cannot be sources here (the build host may not have
## helper-scripts installed).
has() {
  local _cmd _name

  for _name in "$@"; do
    _cmd="$(command -v "${_name}")" 2>/dev/null || return 1
    [ -x "${_cmd}" ] || return 1
  done
}

## Code duplication. Copied from
## helper-scripts/usr/libexec/helper-scripts/check_runtime.bsh.
##
## Because check_runtime.bsh cannot be sourced here, see above.
was_executed() {
  local caller_bash_source_zero="${1}"
  if [[ "${caller_bash_source_zero}" == "${0}" ]] ||
    [[ "${caller_bash_source_zero}" == "$(command -v "${0}")" ]]; then
    return 0
  fi
  return 1
}
if [ "${GENMKFILE_DEBUG:-}" = "1" ]; then
  set -x
fi

make_output_info() {
  printf "%s\n" "${0}: ${cyan:-}[${make_source_package_name:-${PWD##*/}}]${reset:-} ${green}INFO:${reset} ${*}"
}

make_output_warn() {
  printf "%s\n" "${0}: ${cyan:-}[${make_source_package_name:-${PWD##*/}}]${reset:-} ${red}WARNING:${reset} ${*}" >&2
}

make_output_cached() {
  printf "%s\n" "${0}: ${cyan:-}[${make_source_package_name:-${PWD##*/}}]${reset:-} ${green}CACHED:${reset} ${*}"
}

make_output_debug() {
  printf "%s\n" "${0}: ${cyan:-}[${make_source_package_name:-${PWD##*/}}]${reset:-} ${green}DEBUG:${reset} ${*}"
}

make_log_and_run() {
  printf "%s\n" "${0}: ${cyan:-}[${make_source_package_name:-${PWD##*/}}]${reset:-} ${green}running:${reset} ${*}"
  "${@}"
}

make_output_error() {
  local trace
  source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/trace.bsh" || true
  ## override in case 'function_trace' unavailable.
  trace="$(function_trace)" || true
  printf "%s\n" "${0}: ${cyan:-}[${make_source_package_name:-${PWD##*/}}]${reset:-} ${red}${bold}ERROR:${reset} ${*}" >&2
  printf "%s\n" "${trace}" >&2
}

die() {
  make_output_error "${@}"
  exit 1
}

make_require() {
  local cmd
  for cmd in "${@}"; do
    if ! has "${cmd}"; then
      make_output_error "required command '${cmd}' not found in PATH.
If '${cmd}' is listed in debian/control, try:
  sudo genmkfile deb-build-dep"
      exit 1
    fi
  done
}

## Validate DEBEMAIL / DEBFULLNAME. The matching tools (debsign,
## debchange, sq, sq-git, git, bc, ...) are 'make_require'd at each
## individual call site rather than lumped together here, so failing a
## target only blocks on the tools that target actually needs.
deb_variables_check() {
  if [ -z "${DEBEMAIL:-}" ]; then
    ## Otherwise e-mail address in debian/changelog would default to
    ## user@host.localdomain and then lintian would complain and exit with
    ## failure code.
    die 1 "environment variable DEBEMAIL may not be empty! Both, DEBEMAIL and DEBFULLNAME need to be set!"
  fi
  if [ -z "${DEBFULLNAME:-}" ]; then
    die 1 "environment variable DEBFULLNAME may not be empty! Both, DEBEMAIL and DEBFULLNAME need to be set!"
  fi

  ## TODO: DEBSIGN_KEYID ?
}

make_init() {
  local pwd dirname_pwd parent_folder

  ## For manual invocation.
  ## Should be set by makefile already.
  [ -n "${GENMKFILE_PATH:-}" ] || GENMKFILE_PATH="/usr/share/genmkfile"
  export GENMKFILE_PATH

  pwd="$PWD"
  ## Example pwd:
  ## ~/derivative-maker/packages/kicksecure/genmkfile
  dirname_pwd="$(dirname -- "$pwd")"
  ## Example dirname_pwd:
  ## ~/derivative-maker/packages/kicksecure
  parent_folder="$(basename -- "$dirname_pwd")"
  ## Example parent_folder:
  ## kicksecure
  derivative_repository_name="$parent_folder"
  ## derivative_repository_name is read by dm-reprepro-wrapper
  export derivative_repository_name

  ## No longer in use.
  #pushd "$GENMKFILE_ROOT_DIR" >/dev/null

  genmkfile_temp_dir="$(mktemp --directory)"
}

make_deinit() {
  make_require safe-rm
  safe-rm --recursive --force -- "$genmkfile_temp_dir"
}

make_parse_cmd() {
  ## Thanks to:
  ## http://mywiki.wooledge.org/BashFAQ/035

  local matched="false" arg target_fn

  while [ $# -gt 0 ]; do
    arg="$1"
    case "$arg" in
    --)
      true "$0: end-of-options"
      shift
      break
      ;;
    -*)
      die 1 "unknown option: $arg"
      ;;
    esac

    ## Targets map to 'make_<target with dashes -> underscores>'.
    ## If no such function exists, fall through to the trailing
    ## block which either defaults to make_all or tries to call
    ## the argument as a function name directly.
    target_fn="make_${arg//-/_}"
    if ! declare -F "$target_fn" >/dev/null 2>&1; then
      true "$0: positional parameter: $*"
      break
    fi

    matched="true"
    if [ "$arg" = "all" ]; then
      ## 'all' uniquely forwards the remaining CLI args to
      ## make_all, which passes them through to
      ## 'make -f Makefile_orig all "$@"'. This is what lets
      ## 'genmkfile all VAR=value' propagate VAR=value to the
      ## underlying 'make' invocation.
      shift
      make_function_run "$target_fn" "$@"
      ## Args were forwarded; consume the rest so the loop
      ## terminates.
      set --
    else
      make_function_run "$target_fn"
      shift
    fi
  done

  ## If there are input files (for example) that follow the options, they
  ## will remain in the "$@" positional parameters.

  if [ $# -eq 0 ] && [ "$matched" != "true" ]; then
    make_function_run make_all
    return 0
  fi

  if ! [ "$matched" = "true" ]; then
    make_function_run "$@"
  fi
}

make_dependencies_filter_helper() {
  local temp to_remove
  ## Read a Debian dependency expression on stdin, write a clean
  ## space-separated package name list to stdout. Strips:
  ##   - commas (separator)
  ##   - version constraints like '(>= 1.0)'
  ##   - architecture qualifiers like '[linux-any]' / '[!linux-any]'
  ##   - debhelper substitution markers (${shlibs:Depends} etc.)
  ##   - '|' alternative-dep markers
  temp="$(sed -e 's/,/ /g' -e 's/([^)]*)//g' -e 's/\[[^]]*\]//g')"
  ## These are debhelper substitution markers, literal in debian/control.
  # shellcheck disable=SC2016
  for to_remove in '${shlibs:Depends}' '${misc:Depends}' '${python:Depends}' '${python3:Depends}' '|'; do
    temp="${temp//"$to_remove"/""}"
  done
  printf "%s\n" "$temp"
}

## Parse Build-Depends and Depends from debian/control. Split out from
## make_get_variables so 'genmkfile deb-build-dep' (and friends) can run on a
## fresh system without build-essential, then install it from
## debian/control. dpkg-parsechangelog (which pulls in build-essential) is
## therefore only required for other targets.
make_get_dependencies() {
  ## sets: make_debian_control_file_relative_path
  ## sets: make_debian_control_file_absolute_path
  ## sets: dpkg_print_architecture
  # shellcheck source=SCRIPTDIR/shared.bsh
  source "$GENMKFILE_PATH/shared.bsh"
  make_shared_function

  ## Special repos opted in via make_use_git_describe_for_version
  ## have no debian/control; nothing to parse.
  if [ -z "${make_debian_control_file_absolute_path:-}" ]; then
    make_deb_build_dependencies=""
    make_deb_runtime_dependencies=""
    return 0
  fi

  ## Bootstrap dctrl-tools when missing. grep-dctrl (from dctrl-tools)
  ## is required to PARSE debian/control, so it cannot itself be
  ## listed in Build-Depends and self-bootstrapped through the
  ## normal deb-build-dep flow. On fresh CI images this leaves a
  ## chicken-and-egg: 'genmkfile deb-build-dep' fails immediately
  ## with "grep-dctrl not found". When running as root with apt-get
  ## available, install it transparently. Non-root invocations fall
  ## through to make_require below for the existing error path.
  if ! has grep-dctrl && [ "${EUID:-$(id -u)}" -eq 0 ] && has apt-get; then
    apt-get update --error-on=any
    apt-get install --yes --no-install-recommends dctrl-tools
  fi

  make_require grep-dctrl

  local raw_build_depends raw_runtime_depends
  raw_build_depends="$(grep-dctrl --show-field=Build-Depends --no-field-names '' "$make_debian_control_file_absolute_path")"
  make_deb_build_dependencies="$(make_dependencies_filter_helper <<<"$raw_build_depends")"

  raw_runtime_depends="$(grep-dctrl --show-field=Depends --no-field-names '' "$make_debian_control_file_absolute_path")"
  make_deb_runtime_dependencies="$(make_dependencies_filter_helper <<<"$raw_runtime_depends")"
}

make_get_variables() {
  ## Initialize the array if the caller did not set it (nounset-safe).
  [ -n "${make_folder_list_for_un_and_install+x}" ] || make_folder_list_for_un_and_install=()
  if [ "${#make_folder_list_for_un_and_install[@]}" -eq 0 ]; then
    make_folder_list_for_un_and_install=(bin boot dev etc home lib opt sbin srv sys usr var)
  fi

  local my_user_id
  my_user_id="$(id -u)"
  if [ "$my_user_id" = "0" ]; then
    running_as_root_maybe="Running under account '${under}root${eunder}'. (uid '$my_user_id')"
  else
    running_as_root_maybe="Running under account '${under}user${eunder}'. (uid '$my_user_id')"
  fi

  ## Deterministic.
  LC_ALL=C.UTF-8
  TZ=UTC
  export LC_ALL TZ

  ## XXX: maybe better set dynamically but would that be deterministic?
  [ -n "${XZ_OPT:-}" ] || XZ_OPT="--threads=8"
  export XZ_OPT

  ## Skip dpkg-parsechangelog.
  ## make_get_dependencies short-circuits on empty
  ## make_debian_control_file_absolute_path.
  if [ "${make_use_git_describe_for_version:-}" = "true" ]; then
    ## make_get_dependencies runs make_shared_function, which sets
    ## make_debian_control_file_absolute_path (empty when there is no
    ## debian/control). The control-present check below branches on it.
    make_function_run make_get_dependencies

    ## Derive the git tag version from the nearest signed git tag (created
    ## as commit_<sha> by make_git_tag_sign) rather than from
    ## debian/changelog, so `genmkfile git-verify` / `git-tag` work for
    ## repos that tag by commit. This is independent of any deb version
    ## and is preserved even when the repo is also built as a .deb (see
    ## the control-present fall-through below).
    if ! make_git_tag_version_number="$(git describe --abbrev=0 2>/dev/null)"; then
      die 1 "make_use_git_describe_for_version=true but no git tag reachable from HEAD; sign one with dm-quickgittagsign first."
    fi
    [ -n "${make_git_tag_version_number}" ] \
      || die 1 "make_use_git_describe_for_version=true but 'git describe --abbrev=0' returned empty."
    make_git_tag_version_number="${make_git_tag_version_number}${make_git_tag_sign_appendix:-}"
    true "make_git_tag_version_number (from git describe): $make_git_tag_version_number"

    ## A repo with no debian/control has no deb to build. Blank the deb
    ## details variables and short-circuit. This is the original special-repo
    ## path (Whonix-Installer, Whonix-Starter,
    ## qubes-template-{kicksecure,whonix}, ...).
    if [ -z "${make_debian_control_file_absolute_path:-}" ]; then
      make_changelog_version=""
      make_pkg_revision=""
      make_pkg_version=""
      make_epoch=""
      make_pkg_version_with_revision=""

      ## Downstream make_git_tag_shared reads make_source_package_name
      ## under `set -o nounset` (just to check whether it equals
      ## "derivative-maker"). The regular branch sets it from
      ## `dpkg-parsechangelog --show-field Source`; for special repos
      ## there's no changelog, so use the repo's directory basename
      ## ("qubes-template-kicksecure", "Whonix-Installer", ...).
      make_source_package_name="$(basename -- "$PWD")"
      make_package_list=("$make_source_package_name")
      return 0
    fi

    ## Otherwise this repo opted into git-describe versioning but DOES
    ## ship debian/control (e.g. live-build) and is still built as a .deb
    ## via genmkfile. Fall through to the normal path below so the
    ## changelog version and the tarball / .dsc variables get set; the
    ## git-describe tag version computed above is kept (the changelog
    ## version assignment below is skipped while this flag is set).
  fi

  local temp
  if ! has dpkg-parsechangelog; then
    die 1 "dpkg-parsechangelog not found (provided by 'dpkg-dev'). Run: sudo genmkfile deb-build-dep"
  fi

  make_function_run make_get_dependencies

  make_debian_changelog_file_relative_path="debian/changelog"
  make_debian_changelog_file_absolute_path="$(realpath -- "$make_debian_changelog_file_relative_path")"
  test -f "$make_debian_changelog_file_absolute_path"

  ## Example make_changelog_version values:
  ##   3:0.1-1
  ##   0.1-1
  ##   1:20240810
  make_changelog_version="$(dpkg-parsechangelog --show-field Version)"

  if [[ "$make_changelog_version" == *-* ]]; then
    make_pkg_revision="-${make_changelog_version#*-}"
    temp="${make_changelog_version%-*}"
  else
    make_pkg_revision=""
    temp="$make_changelog_version"
  fi

  ## Parse version (strip epoch if present).
  make_pkg_version="${temp#*:}"

  ## Parse epoch (if present).
  if [[ "$make_changelog_version" == *:* ]]; then
    make_epoch="${temp%:*}"
  else
    make_epoch=""
  fi

  true "make_changelog_version: $make_changelog_version"
  true "make_pkg_revision: $make_pkg_revision"
  true "make_pkg_version: $make_pkg_version"
  true "make_epoch: $make_epoch"

  if [ -z "$make_pkg_version" ]; then
    die 1 "variable make_pkg_version is empty."
  fi

  make_pkg_version_with_revision="${make_pkg_version}${make_pkg_revision}"

  if [ "${make_use_git_describe_for_version:-}" = "true" ]; then
    ## Tag version already derived from git describe above (with the
    ## appendix applied there). Keep it; do not override with the
    ## changelog version. The tag scheme for these repos is commit_<sha>,
    ## so git-verify must compare against that, not the changelog version.
    true "make_git_tag_version_number (git describe, kept): $make_git_tag_version_number"
  else
    if [ "${make_pkg_use_pkg_version_without_revision_for_git_version:-}" = "true" ]; then
      ## used for derivative-maker
      make_git_tag_version_number="$make_pkg_version"
    else
      ## default for all packages
      make_git_tag_version_number="$make_pkg_version_with_revision"
    fi
    ## make_git_tag_sign_appendix gets set by dm-bump-derivative-maker-version
    make_git_tag_version_number="${make_git_tag_version_number}${make_git_tag_sign_appendix:-}"
  fi

  [ -n "${host_architecture:-}" ] || host_architecture="$dpkg_print_architecture"
  [ -n "${target_architecture:-}" ] || target_architecture="$dpkg_print_architecture"

  local package
  make_source_package_name="$(dpkg-parsechangelog --show-field Source)"
  ## Source package name may or may not match binary package name.
  make_package_list=("$make_source_package_name")
  make_package_debs_files_list=()

  make_debian_non_package_specific_install_file_relative_path="debian/install"
  make_debian_install_file_relative_path="debian/${make_source_package_name}.install"

  ## Walk every binary-package stanza in debian/control via grep-dctrl
  ## and build:
  ##   make_package_list           - source name + every binary package name
  ##   make_package_debs_files_list - the .deb path each binary will produce
  local package_name_architecture binary_package_architecture line
  package=""
  binary_package_architecture=""
  while IFS= read -r line; do
    case "$line" in
    "Package: "*) package="${line#Package: }" ;;
    "Architecture: "*) binary_package_architecture="${line#Architecture: }" ;;
    "")
      [ -n "$package" ] || continue
      true "package: $package"
      true "make_pkg_version: $make_pkg_version"
      true "make_pkg_revision: $make_pkg_revision"
      true "binary_package_architecture: $binary_package_architecture"
      ## Some packages, like tirdad-dkms, have an architecture of
      ## 'linux-amd64' or similar, but the resulting deb file name
      ## only includes 'amd64'.
      if [[ "$binary_package_architecture" =~ ^linux-(.*) ]]; then
        package_name_architecture="${BASH_REMATCH[1]}"
      else
        package_name_architecture="$binary_package_architecture"
      fi
      if [ "$package_name_architecture" = "any" ]; then
        package_name_architecture="$target_architecture"
      fi
      ## Example architecture independent "all" package:
      ## anon-apt-sources-list_3.4-1_all.deb
      temp="${DISTDIR}/${package}_${make_pkg_version}${make_pkg_revision}_${package_name_architecture}.deb"
      make_package_debs_files_list+=("$temp")
      if [ "$package" != "$make_source_package_name" ]; then
        make_package_list+=("$package")
      fi
      package=""
      binary_package_architecture=""
      ;;
    esac
  done < <(grep-dctrl --show-field=Package,Architecture '' "$make_debian_control_file_absolute_path")

  true "make_package_list: ${make_package_list[*]}"
  true "make_package_debs_files_list: ${make_package_debs_files_list[*]}"

  make_main_dsc_file_name="${make_source_package_name}_${make_pkg_version}${make_pkg_revision}.dsc"

  ## Changes file contains target_architecture such as 'amd64', not 'all'.
  ## Example changes file for architecture independent package:
  ## anon-apt-sources-list_3.4-1_amd64.changes
  make_main_dsc_file="${DISTDIR}/$make_main_dsc_file_name"
  make_main_changes_file="${DISTDIR}/${make_source_package_name}_${make_pkg_version}${make_pkg_revision}_${target_architecture}.changes"

  make_debian_tarball_file_name="${make_source_package_name}_${make_pkg_version}${make_pkg_revision}.debian.tar.xz"
  make_debian_tarball="${DISTDIR}/$make_debian_tarball_file_name"
  if [ "${make_debdist_tolower:-}" != "false" ]; then
    make_debian_tarball_file_name="${make_debian_tarball_file_name,,}"
    ## Lowercase only the file name (the package name must be lowercase),
    ## not DISTDIR: lowercasing the whole path breaks an uppercase DISTDIR.
    make_debian_tarball="${DISTDIR}/$make_debian_tarball_file_name"
  fi
  make_debian_tarball_absolute_path="$(realpath -- "$make_debian_tarball")"

  make_upstream_tarball_relative_path_file_name="${make_source_package_name}_${make_pkg_version}.orig.tar.xz"
  make_upstream_tarball_relative_path="${DISTDIR}/$make_upstream_tarball_relative_path_file_name"
  if [ "${make_upstream_tarball_relative_path_tolower:-}" != "false" ]; then
    make_upstream_tarball_relative_path_file_name="${make_upstream_tarball_relative_path_file_name,,}"
    ## Lowercase only the file name, not DISTDIR (see make_debian_tarball above).
    make_upstream_tarball_relative_path="${DISTDIR}/$make_upstream_tarball_relative_path_file_name"
  fi
  make_upstream_tarball_real_path="$(realpath -- "$make_upstream_tarball_relative_path")"

  ## Exposed for consumption by override hook files.
  # shellcheck disable=SC2034
  make_upstream_tarball_source_folder_name="${make_source_package_name}-${make_pkg_version}"
}

make_get_destdir() {
  if [ -z "${DESTDIR:-}" ]; then
    DESTDIR="/"
  fi
  if type -t make_hook_at_the_end_of_get_destdir; then
    make_hook_at_the_end_of_get_destdir
  fi
}

make_get_distdir() {
  if [ -z "${DISTDIR:-}" ]; then
    DISTDIR=".."
  fi

  if [ "${make_use_cowbuilder:-}" = "true" ]; then
    if [ -z "${make_cowbuilder_dist_folder:-}" ]; then
      die 1 "make_use_cowbuilder=$make_use_cowbuilder, so environment variable make_cowbuilder_dist_folder needs to be set! For example:
make_use_cowbuilder=$make_use_cowbuilder make_cowbuilder_dist_folder=~/genmkfile-packages-result"
    fi
    DISTDIR="$make_cowbuilder_dist_folder"

    if ! test -d "$make_cowbuilder_dist_folder"; then
      die 1 "make_cowbuilder_dist_folder $make_cowbuilder_dist_folder does not exist! Run:
mkdir -p -- $make_cowbuilder_dist_folder"
    fi
  fi

  if ! test -d "$DISTDIR"; then
    die 1 "DISTDIR '$DISTDIR' does not exist!"
  fi
  if ! test -w "$DISTDIR"; then
    die 1 "DISTDIR '$DISTDIR' is not writeable!"
  fi
}

make_help() {
  set +x
  printf "%s\n" "\
genmkfile help
   Show this help.

genmkfile all
   Forwarded to 'make -f Makefile_orig all' when a Makefile_orig exists
   in the current directory, otherwise a no-op.

genmkfile test
   Dummy target. Exists only so debhelper can invoke it without
   failing; genmkfile does not run any tests itself.

genmkfile dist
   Create package-version.tar.xz from source files in \$DISTDIR (default \"..\").

genmkfile undist
   Delete package-version.tar.xz from source files in \$DISTDIR (default \"..\").

genmkfile debdist
   Create debian.tar.xz from source files in \$DISTDIR (default \"..\").

genmkfile undebdist
   Delete debian.tar.xz from source files in \$DISTDIR (default \"..\").

genmkfile debdsc
   Create the .dsc file in \$DISTDIR (default \"..\").

genmkfile undebdsc
   Delete the .dsc file in \$DISTDIR (default \"..\").

genmkfile debinstfile
   Create debian/pkg-name.install files.

genmkfile manpages
   Create man pages from the man source folder. Output is stored in auto-generated-man-pages folder.

genmkfile uch
   Store upstream changelog from git log in changelog.upstream.

genmkfile install
   Copy files from the source tree to system-wide directories.

genmkfile installsim
   Simulate copying files from the source tree to system-wide directories.

genmkfile deb-build-dep
   Install build dependencies listed in debian/control under Build-Depends using apt-get.
   Various environment variables are supported.

genmkfile deb-run-dep
   Install runtime dependencies listed in debian/control under Depends using apt-get.
   Various environment variables are supported.

genmkfile deb-all-dep
   Combination of deb-build-dep and deb-run-dep.

genmkfile deb-pkg-build
   Create a deb, which will be stored in the parent folder.

genmkfile deb-install
   Install deb from the parent folder.

genmkfile deb-pkg
   Combination of genmkfile deb-cleanup, dist, debdist, debdsc and deb-pkg-build.
   Builds a .deb (or source package) from scratch.

genmkfile deb-icup
   Combination of genmkfile deb-cleanup, deb-pkg-build, genmkfile deb-install and genmkfile deb-cleanup.
   icup stands for install and cleanup.

genmkfile deb-remove
   apt-get remove make_source_package_name

genmkfile deb-purge
   apt-get purge make_source_package_name

genmkfile deb-clean
   Delete temporary debhelper files.

genmkfile deb-cleanup
   Same as genmkfile deb-clean, and also deletes debuild artifacts from the parent folder.

genmkfile lintian
   Run lintian on the latest .changes file. Normally invoked
   automatically at the end of 'genmkfile deb-pkg-build'; exposed here
   for manual re-runs. Honours \$DEBUILD_LINTIAN_OPTS and
   \$make_lintian_fail_open_versus_closed.

genmkfile clean
   If a Makefile_orig exists, forward to 'make -f Makefile_orig clean',
   otherwise run 'make clean' if that target is defined.

genmkfile dput-ubuntu-ppa
   Upload to Ubuntu PPA. Requires a functional .dput.cf.

genmkfile distclean
   Currently the same as genmkfile clean.

genmkfile checkout
   Fetch from git.

genmkfile installcheck
   Check if source files match installed files.

genmkfile uninstallcheck
   Check if genmkfile uninstall removed all files.

genmkfile uninstall
   Delete all installed files.

genmkfile uninstallsim
   Simulate what genmkfile uninstall would do.

genmkfile deb-chl-bumpup-manual
   Manually bump the upstream version number in debian/changelog.

genmkfile deb-uachl-bumpup-manual
   Combination of genmkfile uch and genmkfile deb-chl-bumpup-manual.

genmkfile deb-chl-bumpup-major
   Major bump of the upstream version number in debian/changelog.

genmkfile deb-uachl-bumpup-major
   Combination of genmkfile uch and genmkfile deb-chl-bumpup-major.

genmkfile deb-uachl-commit-changelog
   git add debian/changelog (and changelog.upstream if present) and
   commit with a standard message.

genmkfile git-tag-show
   Echo the current git tag to stdout.

genmkfile git-tag-sign
   git tag (-s) sign latest pkg_version_with_revision from debian/changelog.
   Only a repository sanity check. Not for security purposes!

genmkfile git-tag-verify
   git tag (-v) verify latest pkg_version_with_revision from debian/changelog.
   Only a repository sanity check. Not for security purposes!

genmkfile git-tag-check
   Check if the current git tag is a signed git tag.
   Only a repository sanity check. Not for security purposes!

genmkfile git-commit-verify
   Check if the current git head is a signed git commit.
   Only a repository sanity check. Not for security purposes!

genmkfile git-verify
   Combination of tag-check and commit-verify.
   Only a repository sanity check. Not for security purposes!

genmkfile git-tag-push
   Atomic push of branches + tip tags to each listed remote, in
   parallel. Default: current branch + tag at HEAD -> 'origin'.
   Every listed branch tip must carry a tag.
   Environment variables:
     make_git_push_branches       space-separated branch list (unset -> current branch)
     make_git_push_remotes        space-separated remote list (unset -> origin)
     make_git_tag_push_cache_dir  cache root for skip-already-pushed (unset -> no cache)
   The caller is responsible for filtering make_git_push_branches /
   make_git_push_remotes to refs that exist locally; genmkfile errors
   out on a missing branch or remote.

genmkfile git-tag-show-latest
   Echo the most recent git tag to stdout.

genmkfile reprepro-add
   Add the main deb package using the reprepro wrapper
   (Environment variable make_reprepro_wrapper defaults to 'reprepro'.)
   to the repository (Environment variable dist_build_apt_codename defaults to 'local'.)

   Example oldstable: dist_build_apt_codename=bookworm-developers genmkfile reprepro-add
   Example stable   : dist_build_apt_codename=trixie-developers genmkfile reprepro-add

genmkfile reprepro-remove
   Delete.

multiple architecture builds examples:

make_use_cowbuilder=true make_cross_build_platform_list='amd64 arm64' genmkfile deb-pkg
make_use_cowbuilder=true make_cross_build_platform_list='amd64' genmkfile deb-pkg"
}

make_all() {
  if test -f "$PWD/Makefile_orig"; then
    make -f "$PWD/Makefile_orig" all "$@"
  fi
}

make_dist() {
  local empty_directories msg

  make_require safe-rm strip-nondeterminism

  #find "." -not -iwholename '*.git*' -exec touch --no-dereference --date="Wed Sep 30 16:30:05 UTC 2015" {} +

  ## No longer using --transform because that does not work with symlinks.
  ## (Important for 'corridor' package.)

  if ! test -d "$DISTDIR"; then
    die 1 "DISTDIR '$DISTDIR' does not exist!"
  fi
  if ! test -w "$DISTDIR"; then
    die 1 "DISTDIR '$DISTDIR' is not writeable!"
  fi

  ## Debugging.
  test -w "$PWD"
  touch -- "$make_upstream_tarball_real_path"
  test -w "$make_upstream_tarball_real_path"
  safe-rm -f -- "$make_upstream_tarball_real_path"

  empty_directories="$(find "." -not -ipath '*.git*' -type d -empty)"

  if [ -n "${empty_directories}" ]; then
    die 1 "Empty directory found! empty_directories:
'${empty_directories}'"
  fi

  make_output_debug "manual reproduction:
cd $PWD"

  ## Excluding '/debian' folder from upstream tarball to avoid lintian warning
  ## 'no-debian-changes'.

  msg="manual reproduction:
find \
      '.' \
      -not -path '*/\.git/*' \
      -not -path '*/\.git' \
      -not -name '.gitignore' \
      -not -path './debian*' \
      -print0 \
         | tar \
            --null \
            --no-recursion \
            --create \
            --owner=0 --group=0 --numeric-owner \
            --mode=go=rX,u+rw,a-s \
            --sort=name \
            --mtime='2015-10-21 00:00Z' \
            --xz \
            --file='$make_upstream_tarball_real_path' \
            -T \
            -
   "

  msg="$(printf "%s\n" "$msg" | sed 's/  */ /g')"
  make_output_debug "$msg"

  find \
    '.' \
    -not -path '*/\.git/*' \
    -not -path '*/\.git' \
    -not -name '.gitignore' \
    -not -path './debian*' \
    -print0 |
    tar \
      --null \
      --no-recursion \
      --create \
      --owner=0 --group=0 --numeric-owner \
      --mode=go=rX,u+rw,a-s \
      --sort=name \
      --mtime='2015-10-21 00:00Z' \
      --xz \
      --file="$make_upstream_tarball_real_path" \
      -T \
      -

  strip-nondeterminism -- "$make_upstream_tarball_real_path"

  test -f "$make_upstream_tarball_real_path"
}

make_debdist() {
  make_require strip-nondeterminism

  #find "." -not -iwholename '*.git*' -exec touch --no-dereference --date="Wed Sep 30 16:30:05 UTC 2015" {} +

  ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=657401
  ## Not using `dpkg-source` since that does not support any other output dir than '../'.

  find \
    debian \
    -print0 |
    tar \
      --null \
      --no-recursion \
      --create \
      --owner=0 --group=0 --numeric-owner \
      --mode=go=rX,u+rw,a-s \
      --sort=name \
      --mtime='2015-10-21 00:00Z' \
      --xz \
      --file="$make_debian_tarball_absolute_path" \
      -T \
      -

  strip-nondeterminism -- "$make_debian_tarball_absolute_path"

  test -f "$make_debian_tarball_absolute_path"
}

make_undist() {
  make_require safe-rm
  safe-rm --force -- "$make_upstream_tarball_relative_path"
}

make_debdsc() {
  make_require dpkg-source
  if ! test -d "$DISTDIR"; then
    die 1 "DISTDIR '$DISTDIR' does not exist!"
  fi
  if ! test -w "$DISTDIR"; then
    die 1 "DISTDIR '$DISTDIR' is not writeable!"
  fi

  pushd -- "$DISTDIR" >/dev/null

  ## Creates make_main_dsc_file.
  make_log_and_run \
    dpkg-source \
    -c"$make_debian_control_file_absolute_path" \
    -l"$make_debian_changelog_file_absolute_path" \
    --format="3.0 (custom)" \
    --target-format="3.0 (quilt)" \
    -b / \
    "$make_upstream_tarball_real_path" \
    "$make_debian_tarball_absolute_path"

  popd >/dev/null

  test -f "$make_main_dsc_file"
}

make_undebdsc() {
  make_require safe-rm
  safe-rm --force -- "$make_main_dsc_file"
}

make_undebdist() {
  make_require safe-rm
  safe-rm --force -- "$make_debian_tarball_absolute_path"
}

make_debinstfile() {
  ## calamares-settings-debian comes with debian/install by upstream default.
  ## If file debian/calamares-settings-debian.install was created, that would result in debian/install
  ## being ignored and in an empty package. Therefore, if debian/install already exists, do not
  ## automatically create a debian/package-name.install file.
  if test -f "$make_debian_non_package_specific_install_file_relative_path"; then
    return 0
  fi

  if test -f "$make_debian_install_file_relative_path"; then
    if grep --quiet -- "nogenmkfile" "$make_debian_install_file_relative_path"; then
      return 0
    fi
  fi

  make_debinstfile_create
}

make_debinstfile_create() {
  local new_file filename_without_dot filename_without_hash \
    cleaned_filename_without_hash pkg_name \
    temp_deb_install_file real_deb_install_file
  local -a find_cmd
  local -A delete_yet

  make_require safe-rm sponge

  find_cmd+=(find)
  find_cmd+=(.)
  find_cmd+=(-not -path '*/\.git/*')
  find_cmd+=(-not -path '*/\.git')
  find_cmd+=(-not -name '.gitignore')
  find_cmd+=(-not -path './debian*')
  find_cmd+=(-iname '*#*')

  if "${find_cmd[@]}" | grep --fixed-strings -- '#' >/dev/null; then
    true "multi-package hash filename found: yes"
    find_cmd+=(-print0)

    "${find_cmd[@]}" | LC_ALL='C' sort -z |
      while IFS= read -r -d '' file_name; do
        filename_without_dot="${file_name#./}"
        filename_without_hash="${file_name%%#*}"
        cleaned_filename_without_hash="${filename_without_hash/#.\//\/}"
        pkg_name="${file_name#*#}"
        temp_deb_install_file="${genmkfile_temp_dir}/${pkg_name}.install"

        if [ -z "${delete_yet[${pkg_name}]:-}" ]; then
          safe-rm --force -- "$temp_deb_install_file"
          delete_yet["${pkg_name}"]=true
          printf "%s\n" "\
#!/usr/bin/dh-exec

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

## This file was generated using 'genmkfile debinstfile'.
" | sponge -a -- "$temp_deb_install_file"
          chmod -- +x "$temp_deb_install_file"
        fi

        printf "%s\n" "${filename_without_dot} => ${cleaned_filename_without_hash}" | sponge -a -- "$temp_deb_install_file"
      done

    for new_file in "$genmkfile_temp_dir"/*.install; do
      real_deb_install_file="debian/${new_file##*/}"
      if test -f "$real_deb_install_file"; then
        if cmp -s -- "$new_file" "$real_deb_install_file"; then
          continue
        fi
      fi
      cp -- "$new_file" "$real_deb_install_file"
    done

  else
    true "multi-package hash filename found: no"
    temp_deb_install_file="${genmkfile_temp_dir}/pkg.install"
    real_deb_install_file="$make_debian_install_file_relative_path"
    printf "%s\n" "\
## Copyright (C) 2020 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## This file was generated using 'genmkfile debinstfile'.
" | sponge -a -- "$temp_deb_install_file"
    local d
    for d in "${make_folder_list_for_un_and_install[@]}"; do
      if test -d "$d"; then
        printf "%s\n" "${d}/*" | sponge -a -- "$temp_deb_install_file"
      fi
    done
    if cmp -s -- "$temp_deb_install_file" "$real_deb_install_file"; then
      return 0
    fi
    cp -- "$temp_deb_install_file" "$real_deb_install_file"
  fi
}

make_test() {
  ## Automatically run by debhelper. To prevent debhelper from failing,
  ## implemented a dummy function here.
  make_output_info "${FUNCNAME[0]} not implemented"
}

make_manpages() {
  shopt -s globstar dotglob nullglob
  local i filename_without_ronn_file_extension base_name
  make_require ronn
  for i in "$PWD/man/"*".ronn"; do
    if test -d "$i"; then
      continue
    fi

    base_name="${i##*/}"
    filename_without_ronn_file_extension="${base_name%.*}"

    mkdir --parents -- "$PWD/auto-generated-man-pages"

    true "${bold}${cyan}${green}INFO:${reset} Creating man page $PWD/auto-generated-man-pages/$filename_without_ronn_file_extension ${reset}"

    ## '--date=2020-01-01' for deterministic man pages.
    ronn \
      --date=2020-01-01 \
      --manual="$make_source_package_name Manual" \
      --organization="$make_source_package_name" \
      -- \
      <"$i" \
      >"$PWD/auto-generated-man-pages/$filename_without_ronn_file_extension"

  done
  shopt -u globstar dotglob nullglob
}

make_uch() {
  if test -f "$PWD/CHANGES"; then
    return 0
  fi
  make_require git sponge
  git log | \
    sed 's/[[:space:]]\+$//' | \
    stcat | \
    sponge -- "$PWD/changelog.upstream"
  test -f "$PWD/changelog.upstream"
  unicode-show "$PWD/changelog.upstream"
}

make_distclean() {
  make_output_info "(internal) genmkfile distclean"
  make_function_run make_clean
}

make_clean() {
  if test -f "$PWD/Makefile_orig"; then
    make -f "$PWD/Makefile_orig" clean
  else
    if make --dry-run clean &>/dev/null; then
      make_output_info "(internal) make clean"
      make clean
    else
      make_output_info "no target 'make clean', ok."
    fi
  fi
}

make_deb_shared_dep() {
  local msg apt_get_exit_code

  make_require sudo apt-get

  msg="sudo apt-get ${DIST_APTGETOPT[*]} --yes --no-install-recommends --allow-downgrades install -- $make_deb_shared_dependencies"
  make_output_info "running: $msg"

  apt_get_exit_code="0"
  ## $make_deb_shared_dependencies is intentionally unquoted: the
  ## extractor produces a space-separated package list and apt-get
  ## expects one argument per package.
  # shellcheck disable=SC2086
  sudo \
    apt-get \
    "${DIST_APTGETOPT[@]}" \
    --yes \
    --no-install-recommends \
    --allow-downgrades \
    install \
    -- \
    $make_deb_shared_dependencies || apt_get_exit_code="$?"

  ## 'return', not 'exit' -- exiting the shell here used to abort
  ## chained targets (e.g. 'genmkfile deb-build-dep debinstfile'
  ## would stop after deb-build-dep even on success) and also skip
  ## make_deinit.
  return "$apt_get_exit_code"
}

make_deb_build_dep() {
  ## 'build-essential' is implicitly assumed by every Debian source
  ## package; lintian emits 'build-depends-on-build-essential' if it
  ## is listed in debian/control Build-Depends. So we cannot put it
  ## there. To save users from having to install it by hand before
  ## running 'genmkfile deb-build-dep' for the first time, inject it
  ## here on top of whatever Build-Depends the extractor pulled from
  ## debian/control.
  ##
  ## make_get_dependencies needs grep-dctrl to parse debian/control,
  ## so any environment that ran us this far must keep it. Hardcoding
  ## here keeps the apt-install path explicit even if a future
  ## debian/control accidentally drops dctrl-tools from Build-Depends.
  local make_deb_shared_dependencies="build-essential dctrl-tools $make_deb_build_dependencies"
  make_function_run make_deb_shared_dep
}

make_deb_run_dep() {
  local make_deb_shared_dependencies="$make_deb_runtime_dependencies"
  make_function_run make_deb_shared_dep
}

make_deb_all_dep() {
  ## See make_deb_build_dep for why build-essential and dctrl-tools
  ## are added here.
  local make_deb_shared_dependencies="build-essential dctrl-tools $make_deb_build_dependencies $make_deb_runtime_dependencies"
  make_function_run make_deb_shared_dep
}

make_deb_pkg_build_shared() {
  local cow_folder base_folder package
  local -a make_cowbuilder_config_file_arg=()
  if [ "${make_use_cowbuilder:-}" = "true" ]; then
    make_require sudo cowbuilder
    if ! test -f "$make_upstream_tarball_real_path"; then
      die 1 "upstream tarball $make_upstream_tarball_real_path does not exist.
Did you run
genmkfile dist && genmkfile debdist && genmkfile debdsc
beforehand?"
    fi

    if ! test -f "$make_debian_tarball_absolute_path"; then
      die 1 "debian tarball $make_debian_tarball_absolute_path does not exist.
Did you run
genmkfile dist && genmkfile debdist && genmkfile debdsc
beforehand?"
    fi

    if ! test -f "$make_main_dsc_file"; then
      die 1 "dsc file $make_main_dsc_file does not exist.
Did you run
genmkfile dist && genmkfile debdist && genmkfile debdsc
beforehand?"
    fi

    true "COWBUILDER_PREFIX: ${COWBUILDER_PREFIX:-}"

    if [ -n "${dist_build_apt_stable_release:-}" ]; then
      [ -n "${make_cowbuilder_distribution:-}" ] || make_cowbuilder_distribution="$dist_build_apt_stable_release"
    elif [ -z "${make_cowbuilder_distribution:-}" ]; then
      make_require lsb_release
      make_cowbuilder_distribution="$(lsb_release --short --codename)"
    fi

    ## derivative-maker sets variable `make_cowbuilder_mirror`.

    if [ -n "${dist_build_apt_sources_mirror:-}" ]; then
      [ -n "${make_cowbuilder_mirror:-}" ] || make_cowbuilder_mirror="$dist_build_apt_sources_mirror"
    else
      ## approx caching proxy support
      if [ "${APPROX_PROXY_ENABLE:-}" = "yes" ]; then
        ## TODO: Use the derivative-maker specific proxy at port 9977, or
        ## the system-wide proxy at port 9999?
        [ -n "${make_cowbuilder_mirror:-}" ] || make_cowbuilder_mirror="http://127.0.0.1:9977/debian"
      fi
      [ -n "${make_cowbuilder_mirror:-}" ] || make_cowbuilder_mirror="https://deb.debian.org/debian"
    fi

    ## Parent folder of the cowbuilder chroots. Honor '$cowbuilder_cache_dir'
    ## if the caller (e.g. derivative-maker) exported it, otherwise default to
    ## the cowbuilder standard location.
    [ -v cowbuilder_cache_dir ] || cowbuilder_cache_dir="/var/cache/pbuilder"
    cow_folder="$cowbuilder_cache_dir/cow.cow_${target_architecture}"
    base_folder="$cowbuilder_cache_dir/base.cow_${target_architecture}"

    if [ -n "${dist_build_pbuilder_config_file:-}" ]; then
      make_cowbuilder_config_file_arg=(--configfile "$dist_build_pbuilder_config_file")
    fi

    ## Using cowbuilder with
    ## --host-arch "$host_architecture" \
    ## --architecture "$dist_build_target_arch" \
    ## can cause the following build error:
    ## The following packages have unmet dependencies:
    ## builddeps:/build/anon-apps-config_3.0-1.dsc:amd64 : Depends: genmkfile:amd64 but it is not installable
    ##
    ## Test command:
    ## dist_build_pbuilder_config_file=/home/user/derivative-binary/pbuilder.conf make_cross_build_platform_list=arm64 make_cowbuilder_dist_folder=~/genmkfile-packages-result make_use_cowbuilder=true genmkfile deb-pkg

    ## Not using '--mirror' results in cowbuilder automatically passing to pbuilder:
    ## --mirror http://you-have-broken-debconf-configuration.example.com/
    ## Not using '--distribution' results in cowbuilder automatically passing to pbuilder:
    ## --distribution sid

    ## W: --override-config is not set; not updating apt.conf Read the manpage for details.
    ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1075960

    ## TODO: REPO_PROXY is no longer used, update this comment.
    ## COWBUILDER_PREFIX could set variables such as http_proxy.
    ## derivative-maker sets:
    ## COWBUILDER_PREFIX+=" env http_proxy=${REPO_PROXY}"
    ## which results in:
    ## sudo env http_proxy=http://127.0.0.1:9977 cowbuilder [...]
    ##
    ## creates $make_main_changes_file
    ##
    ## creates again (overwrites) $make_debian_tarball_file_name
    ##
    ## Forces the inclusion of the original source.
    ## --debbuildopts="-sa" is passed by cowbuilder to pdebuild (which passes
    ## it to debuild?) which passes it to dpkg-genchanges. In other words,
    ## results in 'dpkg-genchanges -sa'.
    ## That is required when make_pkg_revision is higher than 1. In that case
    ## by default the original source tarball would not be included by Debian
    ## default. The practice in Debian is not to upload the original source
    ## tarball if only the Debian package revision version number was
    ## increased since it would already be in the archive. That however seems
    ## incompatible with reprepro, which would show the following error:
    ## ##
    ## Unable to find pool/main/l/lkrg/lkrg_0.9.1.orig.tar.xz needed by lkrg_0.9.1-3.dsc!
    ## Perhaps you forgot to give dpkg-buildpackage the '-sa' option,
    ## or you could try --ignore=missingfile to guess possible files to use.
    ## There have been errors!
    ## ##
    ## Maybe non-ideal. '--debbuildopts="-sa" would only be required' if
    ## package revision number is higher than 1 and maybe only if there never
    ## has been package revision number 1 including upstream source tarball
    ## uploaded beforehand. Good enough.
    ##
    ## --unset=TEMP \
    ## --unset=TEMPDIR \
    ## --unset=TMP \
    ## --unset=TMPDIR \
    ## Setting TMPDIR breaks pbuilder (which gets called by cowbuilder).
    ## Installing libpam-tmpdir (security-feature) results in these variables being set.
    ## In other words:
    ## "`sudo apt install libpam-tmpdir` breaks cowbuilder"
    ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=823651
    ## https://github.com/Kicksecure/security-misc/pull/147
    ## https://forums.whonix.org/t/make-symlink-attacks-and-other-tmp-based-attacks-harder-or-impossible-using-libpam-tmpdir/8488
    ## Installing libpam-tmpdir inside the grml-debootstrap chroot did fix a similar issue for grml-debootstrap / grub.
    ## https://github.com/grml/grml-debootstrap/issues/232
    ## Installing libpam-tmpdir inside the cowbuilder chroot did however not fix this issue.
    ## $COWBUILDER_PREFIX is intentionally unquoted: callers set it
    ## to something like 'env http_proxy=... env foo=bar' that needs
    ## to split into multiple argv slots.
    # shellcheck disable=SC2086
    make_log_and_run \
      sudo \
      ${COWBUILDER_PREFIX:-} \
      cowbuilder \
      --debbuildopts="-sa" \
      --architecture "$target_architecture" \
      "${make_cowbuilder_config_file_arg[@]}" \
      --build "$make_main_dsc_file" \
      --basepath "$base_folder" \
      --buildplace "$cow_folder" \
      --distribution "$make_cowbuilder_distribution" \
      --mirror "$make_cowbuilder_mirror" \
      --extrapackages "$make_deb_build_dependencies" \
      --buildresult "$DISTDIR"

  else
    make_require debuild dpkg-checkbuilddeps

    if ! dpkg-checkbuilddeps; then
      die 1 "dpkg-checkbuilddeps reported missing build dependencies.
To install all dependencies, try running:
sudo genmkfile deb-all-dep"
    fi

    ## Using --no-lintian, because debuild does not exist with a non-zero exit
    ## code, should lintian find an error. Therefore running lintian afterwards.
    ## $make_debuild_maybe_unsigned_opts is a space-separated
    ## string like '-us -uc' that needs to word-split.
    # shellcheck disable=SC2086
    make_log_and_run \
      debuild \
      --no-lintian \
      -sa \
      ${make_debuild_maybe_unsigned_opts:-} \
      "$@"
  fi

  make_output_info "make_use_debsign: '${make_use_debsign:-}' | make_debsign_opts: '${make_debsign_opts:-}' | whoami: '$(whoami)' | DEBEMAIL: '${DEBEMAIL:-}'"
  if [ "${make_use_debsign:-}" = "true" ]; then
    make_require debsign sq
    deb_variables_check

    ## clearsign's $make_main_changes_file
    ## $make_debsign_opts is intentionally unquoted: user-supplied
    ## flags like '-k<keyid> --re-sign' must split into argv slots.
    # shellcheck disable=SC2086
    make_log_and_run debsign ${make_debsign_opts:-} "$make_main_changes_file"

    ## sanity test
    #make_log_and_run gpg --verify "$make_main_changes_file"
    make_log_and_run sq verify --signer-email "$DEBEMAIL" --cleartext "$make_main_changes_file"

    #       ## Required, otherwise:
    #       ## dpkg-source: error: unpack target exists: /tmp/tmp.n1V3KXmMtI
    #       genmkfile_dpkg_source_test_extract_temp_dir="${genmkfile_temp_dir}/temp"
    #       ## Sanity test.
    #       ## Broken under Trixie, disabled. See:
    #       ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1110172
    #       ## TODO: consider re-enabling in Debian forky
    #       make_log_and_run \
    #         dpkg-source \
    #            --require-strong-checksums \
    #            --require-valid-signature \
    #            --extract \
    #            "$make_main_dsc_file" \
    #            "${genmkfile_dpkg_source_test_extract_temp_dir}/"
  fi

  for package in "${make_package_debs_files_list[@]}"; do
    if ! test -f "$package"; then
      die 1 "File '$package' does not exist."
    fi
    make_output_info "You can find your deb file here: ${bold}$package${reset}"
  done

  if [ "${make_use_lintian:-}" = "false" ]; then
    make_output_info "make_use_lintian=false - skipping lintian."
  elif [ "${make_use_lintian:-}" = "true" ]; then
    make_output_info "make_use_lintian=true - running lintian..."
    make_function_run make_lintian
  else
    make_output_info "make_use_lintian='' - Autodetecting if lintian is installed..."
    if has lintian; then
      make_output_info "lintian auto detected, using it..."
      make_function_run make_lintian
    else
      make_output_info "lintian not installed, skipping."
    fi
  fi
}

make_lintian_on_warning() {
  local lintian_exit_code="${1:-0}"
  if [ "${make_lintian_fail_open_versus_closed:-}" = "open" ]; then
    make_output_warn "lintian warning. make_lintian_fail_open_versus_closed=open - failing open."
    return 0
  fi
  make_output_error "lintian warning. make_lintian_fail_open_versus_closed is not set to 'open' - failing closed."
  ## If lintian itself exited 0 but we got here because of warning
  ## output, still exit 1 to signal the failure.
  [ "$lintian_exit_code" = "0" ] && lintian_exit_code=1
  exit "$lintian_exit_code"
}

make_lintian() {
  if ! test -f "$make_main_changes_file"; then
    die 1 "make_main_changes_file '$make_main_changes_file' does not exist."
  fi
  if [ -z "${DEBUILD_LINTIAN_OPTS:-}" ]; then
    ## https://github.com/openwall/lkrg/pull/105
    ## --suppress-tags systemd-service-file-missing-documentation-key
    ##
    ## Building from git. Not from tarball.
    ## https://forums.whonix.org/t/end-to-end-signed-debs-debsign-debsig-and-dpkg-sig/3446
    ## --suppress-tags orig-tarball-missing-upstream-signature
    ##
    ## Removed '--suppress-tags package-supports-alternative-init-but-no-init.d-script' for Trixie, tag no longer exists.
    export DEBUILD_LINTIAN_OPTS="\
         --suppress-tags missing-tests-control \
         --suppress-tags systemd-service-file-missing-documentation-key \
         --suppress-tags orig-tarball-missing-upstream-signature \
         --suppress-tags no-manual-page \
         --quiet \
         --pedantic \
         --info \
         --display-info \
         "
  fi
  local make_lintian_exit_code="0"
  local lintian_output
  make_output_info "running: lintian ${genmkfile_lintian_pre_opts:-} $DEBUILD_LINTIAN_OPTS ${genmkfile_lintian_post_opts:-} \"$make_main_changes_file\""
  ## Capture the exit code in the parent shell. A previous idiom
  ## assigned it inside the $(...) subshell, where the assignment was
  ## thrown away when the subshell exited and the code was never seen.
  ## Lintian option vars are user-supplied multi-flag strings; leave
  ## them unquoted so they word-split into argv.
  # shellcheck disable=SC2086
  lintian_output="$(lintian ${genmkfile_lintian_pre_opts:-} $DEBUILD_LINTIAN_OPTS ${genmkfile_lintian_post_opts:-} "$make_main_changes_file" 2>&1)" || make_lintian_exit_code="$?"
  if [ -n "$lintian_output" ]; then
    if ! test -o xtrace; then
      make_output_info "lintian exit code: $make_lintian_exit_code"
      make_output_info "lintian output:"
      printf "%s\n" "################################################################################"
      printf "%s\n" "$lintian_output"
      printf "%s\n" "################################################################################"
    fi
    make_lintian_on_warning "$make_lintian_exit_code"
  fi
  if [ "$make_lintian_exit_code" != "0" ]; then
    make_lintian_on_warning "$make_lintian_exit_code"
  else
    make_output_info "lintian ok."
  fi
}

make_deb_pkg_build() {
  ## Local so make_deb_pkg_build_shared sees it via dynamic scoping
  ## without polluting the global namespace.
  local make_debuild_maybe_unsigned_opts="-us -uc"
  make_function_run make_deb_pkg_build_shared "$@"
}

make_deb_install() {
  local package
  make_require sudo apt-get
  for package in "${make_package_debs_files_list[@]}"; do
    if ! test -f "$package"; then
      die 1 "File '$package' does not exist. Have you run 'genmkfile deb-pkg' already?"
    fi
  done
  ## '--reinstall' is required otherwise apt-get would refuse to install the package with the following error message:
  ## Note, selecting 'systemcheck' instead of '/home/user/derivative-binary/genmkfile-packages-result/systemcheck_25.1-1_all.deb'
  ## systemcheck is already the newest version (3:25.1-1).
  ## TODO: '--reinstall' does not fully fix this.
  make_log_and_run sudo \
    apt-get \
    "${DIST_APTGETOPT[@]}" \
    --yes \
    --no-install-recommends \
    --allow-downgrades \
    --reinstall \
    install \
    -- \
    "${make_package_debs_files_list[@]}"

  ## make_installcheck diffs the package's files at native FHS paths
  ## (bin/ etc/ usr/ ...) in the source tree against what was installed,
  ## which only fits genmkfile-native packages. A dh-packaged repo (e.g.
  ## live-build: source under share/, functions/, data/, mapped into the
  ## deb by debian/rules) has no such native-FHS files, so make_helper
  ## matches nothing and aborts with "Nothing has been copied". Let such
  ## a repo opt out via make_use_installcheck=false in its overrides.
  if [ "${make_use_installcheck:-}" = "false" ]; then
    make_output_info "make_use_installcheck=false - skipping installcheck."
  else
    make_function_run make_installcheck
  fi
}

make_deb_remove() {
  make_require sudo apt-get
  sudo apt-get --yes remove -- "$make_source_package_name"
}

make_deb_purge() {
  make_require sudo apt-get
  sudo apt-get --yes purge -- "$make_source_package_name"
}

make_dh_clean() {
  ## TODO: Executes arbitrary code from the working directory, is this safe?
  if test -x debian/rules; then
    debian/rules clean
  fi
}

make_deb_clean() {
  make_require safe-rm
  if test -d "debian/tmp"; then
    safe-rm --recursive -- "debian/tmp"
  fi
  if test -d "deb_dist"; then
    safe-rm --recursive -- "deb_dist"
  fi
  if test -d "debian/.debhelper"; then
    safe-rm --recursive -- "debian/.debhelper"
  fi
  if test -f "debian/autoreconf.after"; then
    safe-rm -- "debian/autoreconf.after"
  fi
  if test -f "debian/autoreconf.before"; then
    safe-rm -- "debian/autoreconf.before"
  fi
  local to_delete package
  for package in "${make_package_list[@]}"; do
    if test -d "debian/$package"; then
      safe-rm --recursive -- "debian/$package"
    fi
    for to_delete in \
      "debian/$package.debhelper.log" \
      "debian/$package.substvars" \
      "debian/files" \
      "debian/$package.prerm.debhelper" \
      "debian/$package.preinst.debhelper" \
      "debian/$package.postinst.debhelper" \
      "debian/$package.postrm.debhelper" \
      "debian/$package.dkms.debhelper" \
      "debian/debhelper-build-stamp"; do
      if test -e "$to_delete"; then
        safe-rm -- "$to_delete"
      fi
    done
  done
}

make_deb_cleanup() {
  local to_delete package
  make_require safe-rm
  make_function_run make_undist
  make_function_run make_undebdist
  make_function_run make_undebdsc
  make_function_run make_clean
  make_function_run make_dh_clean
  make_function_run make_deb_clean
  for package in "${make_package_list[@]}"; do
    for to_delete in \
      "${DISTDIR}/${package}_"*-*_*".deb" \
      "${DISTDIR}/${package}-dbgsym_"*-*_*".deb" \
      "${DISTDIR}/${package}_"*-*_*".buildinfo" \
      "${DISTDIR}/${package}_"*-*".debian.tar.xz" \
      "${DISTDIR}/${package}_"*-*".orig.tar.xz" \
      "${DISTDIR}/${package}_"*-*".dsc" \
      "${DISTDIR}/${package}_"*-*_*".build" \
      "${DISTDIR}/${package}_"*-*_*".changes" \
      "${DISTDIR}/${package}_"*-*"_source.build" \
      "${DISTDIR}/${package}_"*-*"_source.changes" \
      "${DISTDIR}/${package}_"*-*"_source.${package}.upload"; do
      if test -e "$to_delete"; then
        safe-rm -- "$to_delete"
      fi
    done
  done
  if test -f "$make_upstream_tarball_relative_path"; then
    safe-rm -- "$make_upstream_tarball_relative_path"
  fi
}

make_dput_ubuntu_ppa() {
  make_require dput
  dput "$make_source_package_name" "${DISTDIR}/${make_source_package_name}_${make_pkg_version}${make_pkg_revision}_source.changes"
}

make_checkout() {
  make_require git
  git fetch
}

in_array() {
  local needle item
  needle="$1"
  shift
  true "INFO: in_array: @: |$*|"
  for item in "$@"; do
    true "INFO: in_array: item: $item"
    if [ "$item" = "$needle" ]; then
      true "INFO: in_array: found"
      return 0
    fi
  done
  true "INFO: in_array: not found"
  return 1
}

array_match() {
  local needle item
  needle="$1"
  shift
  true "INFO: array_match: @: |$*|"
  for item in "$@"; do
    true "item: $item"
    if [[ "$needle" == *"$item"* ]]; then
      true "INFO: array_match: found"
      return 0
    fi
  done
  true "INFO: array_match: not found"
  return 1
}

make_helper() {
  shopt -s globstar dotglob nullglob
  local source_directory source_fso file_without_pwd file_with_destdir
  local still_installed="false"
  local fso_with_destdir_stat
  local genmkfile_rsync_opts folder_permission_skip_list

  make_require safe-rm rsync

  ## Mode flags are set by the wrappers (make_install, make_uninstall,
  ## ...) as bash locals, and dynamic scoping makes them visible here.
  ## Snapshot them into our own locals and default any unset flag to
  ## "false" so nounset keeps catching real logic errors. Using
  ## 'local' here (rather than bare assignment) means make_helper
  ## does not leak globals that could make a subsequent wrapper
  ## invocation see stale state.
  local make_install_="${make_install_:-false}"
  local make_installcheck_="${make_installcheck_:-false}"
  local make_installsim_="${make_installsim_:-false}"
  local make_uninstall_="${make_uninstall_:-false}"
  local make_uninstallcheck_="${make_uninstallcheck_:-false}"
  local make_uninstallsim_="${make_uninstallsim_:-false}"

  if ! test -d "$DESTDIR"; then
    die 1 "DESTDIR '$DESTDIR' does not exist!"
  fi

  if [ "$make_installcheck_" != "true" ]; then
    if ! test -w "$DESTDIR"; then
      die 1 "DESTDIR '$DESTDIR' is not writeable!"
    fi
  fi

  genmkfile_rsync_opts=()

  ## Does not work.
  #genmkfile_rsync_opts+=("--chmod=o+r")
  #genmkfile_rsync_opts+=("--executability")

  genmkfile_rsync_opts+=("--recursive")     ## recurse into directories
  genmkfile_rsync_opts+=("--keep-dirlinks") ## treat symlinked dir on receiver as dir
  genmkfile_rsync_opts+=("--links")         ## copy symlinks as symlinks
  genmkfile_rsync_opts+=("--hard-links")    ## preserve hard links
  genmkfile_rsync_opts+=("--safe-links")    ## ignore symlinks that point outside the tree
  #genmkfile_rsync_opts+=("--progress")
  #genmkfile_rsync_opts+=("--verbose")

  ## Cannot be `local` so hooks can increase it.
  #local genmkfile_file_has_been_copied
  [ -n "${genmkfile_file_has_been_copied:-}" ] || genmkfile_file_has_been_copied=false

  folder_permission_skip_list=()
  folder_permission_skip_list+=("/etc/sudoers.d")
  folder_permission_skip_list+=("/usr/src")

  if [ "$make_installsim_" = "true" ]; then
    if ! test -d "$DESTDIR"; then
      make_output_info "would create DESTDIR '$DESTDIR': mkdir --parents -- '$DESTDIR'"
    else
      make_output_info "No need to run, because folder DESTDIR '$DESTDIR' already exists: mkdir --parents -- '$DESTDIR'"
    fi
  fi

  for source_directory in "${make_folder_list_for_un_and_install[@]}"; do
    if ! test -d "$source_directory"; then
      continue
    fi
    local rsync_done
    rsync_done=""
    for source_fso in "$PWD/$source_directory"/**; do
      genmkfile_file_has_been_copied=true
      file_without_pwd="${source_fso#"$PWD"}"
      ## Issue: starts with double '//'
      #file_with_destdir="${DESTDIR}${file_without_pwd}"
      ## New: single '/'
      file_with_destdir="${DESTDIR%/}/${file_without_pwd#/}"
      if [ "$make_install_" = "true" ]; then
        if ! test -d "$DESTDIR"; then
          mkdir --parents -- "$DESTDIR"
        fi
        if [ "$rsync_done" != "true" ]; then
          ## Not using 'cp' because:
          #cp --recursive lib /
          # > cp: cannot overwrite non-directory '/lib' with directory 'lib'
          rsync "${genmkfile_rsync_opts[@]}" -- "$source_directory" "$DESTDIR"
          rsync_done=true
        fi

        ## TODO: review
        ## rsync (above) installed every source entry into DESTDIR EXCEPT
        ## symlinks that 'rsync --safe-links' drops - those point outside the
        ## copied tree (absolute, or via '..') and are silently NOT installed
        ## (there are no --exclude filters, so safe-links is the only thing that
        ## skips an entry). For a symlink source, decide from the link TARGETS,
        ## not from destination existence/type: on a reinstall/upgrade DESTDIR
        ## may already hold a stale file OR a stale symlink at this path, either
        ## of which would mask a dropped symlink. rsync copies a KEPT symlink
        ## verbatim, so the destination must be a symlink with the identical
        ## target; anything else (absent, a non-symlink, or a differently-aimed
        ## stale symlink) means rsync dropped this one -> fail loud, it is a
        ## packaging bug. (A non-symlink source whose destination is somehow
        ## missing falls through to 'stat' below and aborts under
        ## 'set -o errexit' - also loud.)
#         if [ -L "$source_fso" ]; then
#           local source_fso_link_target destination_link_target
#           source_fso_link_target="$(readlink -- "$source_fso")"
#           destination_link_target="$(readlink -- "$file_with_destdir" 2>/dev/null || true)"
#           if [ "$source_fso_link_target" != "$destination_link_target" ]; then
#             die 1 "refusing to install: symlink '$source_fso' -> '$source_fso_link_target' was not installed at '$file_with_destdir'. A symlink pointing outside the copied tree (absolute, or via '..') is skipped by 'rsync --safe-links' - make it a relative in-tree symlink."
#           fi
#           ## Destination is the copied symlink (identical target). If its in-tree
#           ## target is absent (a dangling symlink) it cannot be chmod'ed
#           ## meaningfully - chmod follows to the missing target - so skip it
#           ## rather than let 'stat' below abort.
#           if [ ! -e "$file_with_destdir" ]; then
#             continue
#           fi
#         fi

        ## Need to check first if it is a file...
        ## ...before checking if it is an executable.
        ## Because folders can also have executable bit.
        fso_with_destdir_stat="$(stat -c "%a" -- "$file_with_destdir")"
        #echo "source_fso: $source_fso"
        if test -d "$source_fso"; then
          if [ "$fso_with_destdir_stat" != "755" ]; then
            if [ "$DESTDIR" = "/" ]; then
              true "calling...: in_array |$file_without_pwd| |${folder_permission_skip_list[*]}|"
              if ! in_array "$file_without_pwd" "${folder_permission_skip_list[@]}"; then
                true "in_array did not match, therefore continuing..."
                make_output_info "chmod change on folder may be required, consider: chmod -- 755 '$file_with_destdir'"
                ## Not safe due to folders such as '/etc/sudoers.d' which have chmod '700' as per Debian default.
                #chmod -- 755 "$file_with_destdir"
              fi
            fi
          fi
        elif test -f "$source_fso"; then
          true "calling...: array_match |$file_without_pwd| |${folder_permission_skip_list[*]}|"
          if ! array_match "$file_without_pwd" "${folder_permission_skip_list[@]}"; then
            true "array_match did not match, therefore continuing..."
            ## CI workaround for tree-mode regressions. Some upstream
            ## push paths (notably the GitHub Contents API used by
            ## github-mcp-server's push_files / create_or_update_file)
            ## set every new tree entry to mode 100644 with no way to
            ## opt out, so an executable script can land in the git
            ## tree at 100644. `test -x "$source_fso"` then returns
            ## false here and the file installs as 0644, breaking any
            ## CI step that invokes it from PATH (rc=126, EACCES).
            ##
            ## Under CI=true (set by GitHub Actions), force +x for
            ## files installed under one of the conventional
            ## executable locations: /usr/bin, /usr/sbin, and the
            ## per-package /usr/libexec/<source-pkg-name>/. False
            ## positives (a sourced lib file gaining an exec bit it
            ## does not need) are functionally harmless: bash
            ## `source` does not consult the bit. Disable with
            ## GENMKFILE_CI_FORCE_EXEC=0.
            genmkfile_force_exec=false
            if [ "${CI:-}" = "true" ] \
              && [ "${GENMKFILE_CI_FORCE_EXEC:-1}" = "1" ]; then
              case "$file_without_pwd" in
                /usr/bin/*|/usr/sbin/*|/usr/libexec/"${make_source_package_name:-__none__}"/*)
                  genmkfile_force_exec=true
                  ;;
              esac
            fi
            if test -x "$source_fso" || [ "$genmkfile_force_exec" = "true" ]; then
              if [ "$fso_with_destdir_stat" != "755" ]; then
                make_output_info "chmod -- 755 '$file_with_destdir' (executable, wrong mode)"
                chmod -- 755 "$file_with_destdir"
              fi
            else
              if [ "$fso_with_destdir_stat" != "644" ]; then
                make_output_info "chmod -- 644 '$file_with_destdir' (non-executable, wrong mode)"
                chmod -- 644 "$file_with_destdir"
              fi
            fi
          fi
        fi
      elif [ "$make_installsim_" = "true" ]; then
        if [ "$rsync_done" != "true" ]; then
          ## use '[*]' to treat as single line and avoid newlines in output
          make_output_info "would do: rsync --dry-run ${genmkfile_rsync_opts[*]} -- \"$source_directory\" \"$DESTDIR\""
          rsync --dry-run "${genmkfile_rsync_opts[@]}" -- "$source_directory" "$DESTDIR"
          rsync_done=true
        fi
        ## 'chmod' cannot be simulated because actually no file gets copied.
      elif [ "$make_uninstallcheck_" = "true" ]; then
        if test -f "$file_with_destdir"; then
          make_output_error "file '$file_with_destdir' still installed."
          still_installed="true"
        fi
      elif [ "$make_installcheck_" = "true" ]; then
        if test -f "$source_fso"; then
          ## make_deb_install runs make_installcheck but configuration files may have changed on the system.
          ##
          ## TODO: How do we handle files in sudoers.d? (See also 'folder_permission_skip_list'.)
          ##       Run make_installcheck as root?
          ## diff: /etc/sudoers.d/pwfeedback: Permission denied
          if [ "/$source_directory" = "/etc" ]; then
            diff -- "$source_fso" "$file_with_destdir" || make_output_info "'$file_with_destdir' changed in /etc"
          elif [ "/$source_directory" = "/usr/local/etc" ]; then
            diff -- "$source_fso" "$file_with_destdir" || make_output_info "'$file_with_destdir' changed in /usr/local/etc"
          else
            diff -- "$source_fso" "$file_with_destdir"
          fi
        fi
      elif [ "$make_uninstallsim_" = "true" ]; then
        if test -f "$file_with_destdir"; then
          make_output_info "would do: safe-rm --verbose -- \"$file_with_destdir\""
        fi
      elif [ "$make_uninstall_" = "true" ]; then
        if test -f "$file_with_destdir"; then
          safe-rm --verbose -- "$file_with_destdir"
        fi
      else
        die 1 "error 2 in ${FUNCNAME[0]}!"
      fi
    done
  done
  shopt -u globstar dotglob nullglob
  if [ "$still_installed" = "true" ]; then
    exit 1
  fi
  if [ "$genmkfile_file_has_been_copied" = "false" ]; then
    die 1 "Nothing has been copied."
  fi
}

## Each of the six wrappers below sets its flag as a bash 'local' so it
## is visible to make_helper via dynamic scoping but does not leak to
## the global scope. Previously these were global assignments, which
## caused stale state: 'genmkfile install uninstall' left make_install_
## set to 'true' after the first call, so the second call saw *both*
## flags true, hit the earlier branch in make_helper's elif chain, and
## silently re-ran install instead of uninstalling.
make_install() {
  local make_install_="true"
  make_function_run make_helper
}

make_installcheck() {
  local make_installcheck_="true"
  make_function_run make_helper
}

make_installsim() {
  local make_installsim_="true"
  make_function_run make_helper
}

make_uninstallcheck() {
  local make_uninstallcheck_="true"
  make_function_run make_helper
}

make_uninstall() {
  local make_uninstall_="true"
  make_function_run make_helper
}

make_uninstallsim() {
  local make_uninstallsim_="true"
  make_function_run make_helper
}

make_deb_chl_bumpup_major() {
  local make_bump_type="major"
  make_function_run make_deb_chl_bumpup
}

make_deb_chl_bumpup_manual() {
  local make_bump_type="manual"
  make_function_run make_deb_chl_bumpup
}

make_deb_uachl_commit_changelog() {
  make_require git
  if test -f changelog.upstream; then
    git add -- changelog.upstream
  fi
  git add -- debian/changelog
  git commit -m "bumped changelog version"
}

make_deb_chl_bumpup() {
  make_require debchange bc
  deb_variables_check

  local new_upstream_version new_changelog_version new_revision

  if [ "${make_bump_type:-}" = "major" ]; then
    true "make_pkg_version: $make_pkg_version"

    if [ "${version_numbers_by_upstream:-}" = "true" ]; then
      ## Thanks to:
      ## https://stackoverflow.com/a/4485581
      #new_upstream_version="$(printf "%s\n" "$make_pkg_version" | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')"
      new_upstream_version="$make_pkg_version"
      new_revision="$(bc -l <<<"scale=9 ; $make_pkg_revision + 1" 2>&1)"
    else
      #new_upstream_version_without_epoch="$(bc -l <<< "scale=9 ; $make_pkg_version + 0.1" 2>&1)"
      #new_upstream_version="$(printf '%3.1f\n' "$new_upstream_version_without_epoch")"
      make_require "$GENMKFILE_PATH/version-bump-calculate"
      new_upstream_version="$("$GENMKFILE_PATH/version-bump-calculate" "$make_pkg_version")"
      ## Packages by Kicksecure / Whonix always bump package version. Not Debian package revision.
      new_revision="1"
    fi

    if [ -z "$make_epoch" ]; then
      new_changelog_version="$new_upstream_version-$new_revision"
    else
      new_changelog_version="$make_epoch:$new_upstream_version-$new_revision"
    fi
  elif [ "${make_bump_type:-}" = "manual" ]; then
    make_output_info "Current version number: $make_changelog_version"
    make_output_info "Please enter new version number:"
    read -r new_changelog_version
  else
    die 1 "make_bump_type '${make_bump_type:-}' is neither 'major' nor 'manual'."
  fi

  true "\
Bumping version from / to...
$make_changelog_version
$new_changelog_version"

  if [[ "$new_changelog_version" == *error* ]]; then
    die 1 "Failed!"
  fi

  [ -n "${DEBCHANGE_MSG:-}" ] || DEBCHANGE_MSG="New upstream version (local package)."

  make_log_and_run \
    debchange \
    --newversion "$new_changelog_version" \
    "$DEBCHANGE_MSG"

  make_log_and_run \
    debchange \
    --release \
    ""
}

make_deb_uachl_bumpup_manual() {
  make_function_run make_uch
  make_function_run make_deb_chl_bumpup_manual
}

make_deb_uachl_bumpup_major() {
  make_function_run make_uch
  make_function_run make_deb_chl_bumpup_major
}

make_git_tag_shared() {
  if [ "${make_git_tag_shared_done:-}" = "true" ]; then
    return 0
  fi
  if [ "$make_source_package_name" = "derivative-maker" ]; then
    ## make_git_tag_sign_appendix gets set by dm-bump-derivative-maker-version
    if [ -z "${make_git_tag_sign_appendix:-}" ]; then
      die 1 "package is '$make_source_package_name' but variable make_git_tag_sign_appendix is empty."
    fi
  fi
  [ -n "${make_git_push_remotes:-}" ] || make_git_push_remotes="origin"
  make_require git
  make_git_tag_shared_done="true"
}

make_git_tag_show() {
  make_function_run make_git_tag_shared
  printf "%s\n" "$make_git_tag_version_number"
}

make_git_tag_sign() {
  make_function_run make_git_tag_shared

  ## Special repos opted in via make_use_git_describe_for_version
  ## have no debian/changelog version that is potentially newer than
  ## any existing tag. We want to make a new, non-duplicate tag, so
  ## override make_git_tag_version_number with a "version" based on
  ## HEAD's commit hash and create a tag using that.
  if [ "${make_use_git_describe_for_version:-}" = "true" ]; then
    local head_sha
    head_sha="$(git rev-parse --verify HEAD)"
    make_git_tag_version_number="commit_${head_sha}"
    if git rev-parse --verify --quiet "refs/tags/${make_git_tag_version_number}" >/dev/null; then
      make_output_info "tag '${make_git_tag_version_number}' already exists at HEAD - no-op."
      return 0
    fi
  fi

  ## Refuse to create a tag that collides with an existing branch
  ## name. Tags and branches live in separate namespaces so git
  ## will happily create both, but afterwards every subsequent
  ## operation that takes a bare ref name ('git push origin X',
  ## 'git rev-parse X', ...) becomes ambiguous.
  if git show-ref --verify --quiet -- "refs/heads/$make_git_tag_version_number"; then
    die 1 "A branch named '$make_git_tag_version_number' already exists. Refusing to create a tag with the same name; rename the branch or delete it first."
  fi

  make_log_and_run git tag -s -m "." -- "$make_git_tag_version_number"

  make_function_run make_git_tag_verify
}

## FIXME: This function's name implies that it verifies a *tag*, and
## developer-meta-files uses it as such, but in practice it verifies
## the commit a tag points to. It should probably verify the actual
## tag instead. See corresponding TODO near
## developer-meta-files/usr/libexec/developer-meta-files/git-helpers.bsh
## function 'internal_git_verify_ref'.
make_git_tag_verify() {
  make_function_run make_git_tag_shared
  #make_log_and_run git verify-tag -- "$make_git_tag_version_number"
  ## 'sq-git' verifies a '[COMMIT_RANGE]'. Various contributors not listed
  ## in '$sq_git_policy_file' may have committed.
  ##
  ## NOTE: 'git rev-parse' does not support end-of-options ('--').
  ## Use 'refs/tags/...^{commit}' rather than a bare name so a
  ## same-named branch does not silently win the lookup.
  ## Pass the full tag ref to sq_git_run for the same reason.

  [ -n "${make_git_describe_output:-}" ] || make_git_describe_output="$(git describe --always --abbrev=1000000000)"

  if [ "$make_git_tag_version_number" != "$make_git_describe_output" ]; then
    if [ "${make_skip_git_describe_output_check:-}" != "true" ]; then
      die 1 "Failed! Current git head is not at a signed tag! $running_as_root_maybe make_git_tag_version_number '$make_git_tag_version_number' differs from make_git_describe_output '$make_git_describe_output'"
    fi
  fi

  [ -n "${sq_git_trust_root:-}" ] || sq_git_trust_root="$(git rev-parse --verify "refs/tags/${make_git_tag_version_number}^{commit}")"

  sq_git_run "refs/tags/$make_git_tag_version_number"
}

sq_git_run() {
  local cmd
  make_require sq-git
  ## XXX: hardcoded
  [ -n "${sq_git_trust_root:-}" ] || sq_git_trust_root="HEAD"
  ## XXX: hardcoded
  [ -n "${sq_git_policy_file:-}" ] || sq_git_policy_file="$HOME/derivative-maker/buildconfig.d/openpgp-policy.toml"
  cmd=(sq-git log --policy-file "${sq_git_policy_file}" --trust-root "${sq_git_trust_root}" -- "${1}")
  make_output_info "running: ${cmd[*]}"
  ## Direct call (no make_function_run wrapper) so make_git_commit_verify
  ## can 'if ! sq_git_run "HEAD"' to catch failures and print a
  ## context-specific 'die' message. make_function_run 'exit's on
  ## failure and would skip the if-branch.
  "${cmd[@]}"
}

make_git_tag_check() {
  make_function_run make_git_tag_shared

  ## Sanity test.
  #local make_git_describe_output
  ## dm-bump-derivative-maker-version script overrules this to disable this check.
  [ -n "${make_git_describe_output:-}" ] || make_git_describe_output="$(git describe --always --abbrev=1000000000)"
  if [ "$make_git_tag_version_number" != "$make_git_describe_output" ]; then
    if [ "${make_skip_git_describe_output_check:-}" != "true" ]; then
      die 1 "Failed! Current git head is a untagged git commit! $running_as_root_maybe make_git_tag_version_number $make_git_tag_version_number differs from make_git_describe_output $make_git_describe_output"
    fi
  fi

  make_function_run make_git_tag_verify
  make_output_info "current git head is a signed git tag."
}

make_git_commit_verify() {
  make_function_run make_git_tag_shared
  ## Direct call (no make_function_run wrapper) so this if-branch can
  ## fire; make_function_run 'exit's on failure.
  if ! sq_git_run "HEAD"; then
    die 1 "Failed! Current git head commit is unsigned! $running_as_root_maybe"
  fi
  make_output_info "current git head commit is signed."
}

make_git_verify() {
  make_function_run make_git_commit_verify
  make_function_run make_git_tag_check
  make_output_info "current git head is a signed git commit and a signed git tag."
}

## Push-cache. Opt-in via make_git_tag_push_cache_dir.
##
## Layout:
##   ${make_git_tag_push_cache_dir}/<repo>/<remote>.txt
##   Each line: <remote-url>:<tag>:<full-40-char-commit-sha>
##
## Including the remote URL in each line invalidates on 'git remote
## set-url' (stale entries stay in the file but won't match the new
## URL, so the push proceeds). Missing folders self-heal: fresh
## cache misses, every push proceeds, cache rebuilds.

git_tag_push_cache_path() {
  local remote repo_top repo_name

  remote="$1"

  [ -n "${make_git_tag_push_cache_dir:-}" ] || return 0

  repo_top="$(git rev-parse --show-toplevel)"
  repo_name="$(basename -- "${repo_top}")"

  printf "%s\n" "${make_git_tag_push_cache_dir}/${repo_name}/${remote}.txt"
}

git_tag_push_cache_check() {
  local remote tag sha cache_file remote_url

  remote="$1"
  tag="$2"
  sha="$3"

  cache_file="$(git_tag_push_cache_path "${remote}")"
  [ -n "${cache_file}" ] || return 1
  [ -f "${cache_file}" ] || return 1
  remote_url="$(git remote get-url -- "${remote}")"
  grep --line-regexp --fixed-strings --quiet -- "${remote_url}:${tag}:${sha}" "${cache_file}"
}

git_tag_push_cache_write() {
  local remote tag sha cache_file cache_dir remote_url

  remote="$1"
  tag="$2"
  sha="$3"

  cache_file="$(git_tag_push_cache_path "${remote}")"
  [ -n "${cache_file}" ] || return 0

  cache_dir="$(dirname -- "${cache_file}")"
  mkdir --parents -- "${cache_dir}"
  remote_url="$(git remote get-url -- "${remote}")"
  append-once "${cache_file}" "${remote_url}:${tag}:${sha}"
}

## Backgrounded by make_git_tag_push (one per remote): 'die' kills
## only the sub-shell; parent's 'wait' aggregates per-remote
## failures. Args: <remote> <branch>... -- <tag>...
##
## FIXME: This function's name is wrong, it pushes branches as well as tags.
git_tag_push_to_remote() {
  local remote branch tag tag_sha local_sha tracking_ref arg sep_seen
  local -a branches tags tags_to_push refspecs

  remote="$1"
  shift

  branches=()
  tags=()
  sep_seen='false'
  for arg in "$@"; do
    if [ "${arg}" = '--' ]; then
      sep_seen='true'
      continue
    fi
    if [ "${sep_seen}" = 'true' ]; then
      tags+=( "${arg}" )
    else
      branches+=( "${arg}" )
    fi
  done

  tags_to_push=()
  for tag in "${tags[@]}"; do
    tag_sha="$(git rev-parse --verify "refs/tags/${tag}^{commit}")"
    if git_tag_push_cache_check "${remote}" "${tag}" "${tag_sha}"; then
      make_output_cached "tag '${tag}' (${tag_sha:0:12}) already pushed to '${remote}'"
    else
      tags_to_push+=( "${tag}" )
    fi
  done

  refspecs=()
  for branch in "${branches[@]}"; do
    local_sha="$(git rev-parse --verify "refs/heads/${branch}^{commit}")"
    tracking_ref="refs/remotes/${remote}/${branch}"
    if git show-ref --verify --quiet -- "${tracking_ref}" \
        && [ "$(git rev-parse --verify "${tracking_ref}^{commit}")" = "${local_sha}" ]; then
      make_output_cached "branch '${branch}' (${local_sha:0:12}) already at remote '${remote}' tip"
      continue
    fi
    refspecs+=( "refs/heads/${branch}:refs/heads/${branch}" )
  done
  for tag in "${tags_to_push[@]}"; do
    refspecs+=( "refs/tags/${tag}:refs/tags/${tag}" )
  done

  if [ "${#refspecs[@]}" -eq 0 ]; then
    make_output_cached "nothing to push to '${remote}' (all branches at remote tip, all tags cached)"
    return 0
  fi

  make_log_and_run git push --atomic --porcelain -- "${remote}" "${refspecs[@]}"

  for tag in "${tags_to_push[@]}"; do
    tag_sha="$(git rev-parse --verify "refs/tags/${tag}^{commit}")"
    git_tag_push_cache_write "${remote}" "${tag}" "${tag_sha}"
  done
}

## See 'genmkfile git-tag-push' help text above for the contract
## (env vars, default vs configured mode, strict-error rules).
make_git_tag_push() {
  local branch remote tag tags_at_tip current_branch unique_tags_raw
  local -a branches remotes all_tags unique_tags
  local -A push_label=()

  source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/parallel.bsh"

  make_function_run make_git_tag_shared
  make_require append-once

  if [ -n "${make_git_push_branches:-}" ]; then
    read -r -a branches <<< "${make_git_push_branches}"
    [ "${#branches[@]}" -gt 0 ] \
      || die 1 "make_git_push_branches is set but parses to an empty list (whitespace-only?)."
    for branch in "${branches[@]}"; do
      git show-ref --verify --quiet -- "refs/heads/${branch}" \
        || die 1 "branch '${branch}' (from make_git_push_branches) does not exist in this repo."
    done
  else
    current_branch="$(git rev-parse --abbrev-ref HEAD)"
    [ "${current_branch}" != "HEAD" ] \
      || die 1 "HEAD is detached; cannot determine current branch. Set make_git_push_branches or check out a branch."
    branches=( "${current_branch}" )
  fi

  read -r -a remotes <<< "${make_git_push_remotes}"
  [ "${#remotes[@]}" -gt 0 ] \
    || die 1 "make_git_push_remotes is set but parses to an empty list (whitespace-only?)."
  for remote in "${remotes[@]}"; do
    git remote get-url -- "${remote}" &>/dev/null \
      || die 1 "remote '${remote}' (from make_git_push_remotes) does not exist in this repo."
  done

  all_tags=()
  for branch in "${branches[@]}"; do
    tags_at_tip="$(git tag --points-at "refs/heads/${branch}")"
    [ -n "${tags_at_tip}" ] \
      || die 1 "no tag at tip of branch '${branch}'; create a signed tag at the tip before pushing."
    while IFS= read -r tag; do
      [ -n "${tag}" ] || continue
      all_tags+=( "${tag}" )
    done <<< "${tags_at_tip}"
  done

  unique_tags_raw="$(printf "%s\n" "${all_tags[@]}" | sort --unique)"
  mapfile -t unique_tags <<< "${unique_tags_raw}"

  for remote in "${remotes[@]}"; do
    git_tag_push_to_remote "${remote}" "${branches[@]}" -- "${unique_tags[@]}" &
    push_label[$!]="${remote}"
  done

  if ! parallel_wait push_label; then
    die 1 "atomic push failed: branches=[${branches[*]}] tags=[${unique_tags[*]}] remote(s)=[${parallel_failures[*]}]"
  fi

  make_output_info "atomic push succeeded: branches=[${branches[*]}] tags=[${unique_tags[*]}] remotes=[${remotes[*]}]"
}

make_git_tag_show_latest() {
  make_function_run make_git_tag_shared
  ## Thanks to kilianc - http://stackoverflow.com/a/7979255/2605155
  local latest_tagged_commit make_git_tag_latest
  latest_tagged_commit="$(git rev-list --tags --max-count=1)"
  if [ -z "$latest_tagged_commit" ]; then
    die 1 "no tags exist in this repository."
  fi
  make_git_tag_latest="$(git describe --tags -- "$latest_tagged_commit")"
  printf "%s\n" "$make_git_tag_latest"
}

make_reprepro_shared() {
  local package make_reprepro_codename_list make_reprepro_codename_item changes_file_absolute_path

  ## global dist_build_apt_codename
  ## Can be set by derivative-maker.

  [ -n "${make_reprepro_wrapper:-}" ] || make_reprepro_wrapper="dm-reprepro-wrapper"
  make_require "$make_reprepro_wrapper"

  if [ "${dist_build_apt_codename:-local}" = "local" ]; then
    make_reprepro_codename_list="local"
  else
    make_reprepro_codename_list="local $dist_build_apt_codename"
  fi

  for make_reprepro_codename_item in $make_reprepro_codename_list; do
    ## read by help-steps/variables
    export make_reprepro_codename_item

    ## $make_reprepro_wrapper is intentionally unquoted so that a
    ## wrapper set to e.g. 'dm-reprepro-wrapper --some-flag' splits
    ## correctly across the next four call sites.
    if [ "$make_reprepro_cmd" = "add" ]; then
      changes_file_absolute_path="$(readlink -f -- "$make_main_changes_file")"
      test -f "$changes_file_absolute_path"
      # shellcheck disable=SC2086
      make_log_and_run $make_reprepro_wrapper --ignore=wrongdistribution include "$make_reprepro_codename_item" "$changes_file_absolute_path"
    else
      for package in "${make_package_list[@]}"; do
        # shellcheck disable=SC2086
        make_log_and_run $make_reprepro_wrapper remove "$make_reprepro_codename_item" "$package"
        # shellcheck disable=SC2086
        make_log_and_run $make_reprepro_wrapper removesrc "$make_reprepro_codename_item" "$package"
        ## Avoid the following issue during the next package "include".
        # File "pool/main/a/anon-apt-sources-list/anon-apt-sources-list_6.5-1.dsc" is already registered with different checksums!
        # md5 expected: e6645bc3ef5cbb3769b8be0a008f8b01, got: 3142ef469a2bcab118dd772da437a80b
        # sha1 expected: 154a00a18c2d60d4d10148abab5ce809e85cbe86, got: b6da54d5d481180adf5298cd031e7381bf7253ba
        # sha256 expected: c5312451a92bda546a4d8e4331f835fecf1dbc07ebf99f21d67eab9e5082967d, got: 6a2f2d3aaba452659385a7727c246e45dfbb76af40a98278912a3a1d3c8563c8
        # There have been errors!'
        # shellcheck disable=SC2086
        make_log_and_run $make_reprepro_wrapper deleteunreferenced
        ## Results in:
        # deleting and forgetting pool/main/a/anon-apt-sources-list/anon-apt-sources-list_6.5-1.debian.tar.xz
        # deleting and forgetting pool/main/a/anon-apt-sources-list/anon-apt-sources-list_6.5-1.dsc
        # deleting and forgetting pool/main/a/anon-apt-sources-list/anon-apt-sources-list_6.5-1_all.deb
        # deleting and forgetting pool/main/a/anon-apt-sources-list/anon-apt-sources-list_6.5.orig.tar.xz
      done
    fi
  done
}

make_reprepro_add() {
  local make_reprepro_cmd=add
  make_function_run make_reprepro_shared
}

make_reprepro_remove() {
  local make_reprepro_cmd=remove
  make_function_run make_reprepro_shared
}

make_source_overrides_file() {
  if test -x "./make-helper-overrides.bsh"; then
    bash -n "./make-helper-overrides.bsh"
    source "./make-helper-overrides.bsh"
  fi
  if test -x "./debian/make-helper-overrides.bsh"; then
    bash -n "./debian/make-helper-overrides.bsh"
    source "./debian/make-helper-overrides.bsh"
  fi
}

make_source_overrides_folder() {
  local folder_name i
  for folder_name in ./make-helper-overrides.d ./debian/make-helper-overrides.d; do
    if test -d "$folder_name"; then
      for i in "$folder_name"/*; do
        if test -x "$i"; then
          ## If the last character is a ~, ignore that file,
          ## because it was created by some editor,
          ## which creates backup files.
          if [[ "$i" == *'~' ]]; then
            continue
          fi
          ## Skipping files such as .dpkg-old and .dpkg-dist.
          if [[ "$i" == *'.dpkg-'* ]]; then
            true "skip: $i"
            continue
          fi
          bash -n "$i"
          # shellcheck disable=SC1090
          source "$i"
        fi
      done
    fi
  done
}

make_function_run() {
  local function_name
  function_name="$1"
  shift

  if [ "$function_name" = "make_helper" ]; then
    true "${FUNCNAME[0]}: $function_name"
  else
    make_output_info "${FUNCNAME[0]}: $function_name"
  fi
  if type -t "${function_name}_hook_pre" >/dev/null; then
    "${function_name}_hook_pre" "$@"
  fi
  "$function_name" "$@"
  if type -t "${function_name}_hook_post" >/dev/null; then
    "${function_name}_hook_post" "$@"
  fi
}

if was_executed "${BASH_SOURCE[0]}"; then
  if test -f "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/pre.bsh; then
    ## pre.bsh would `source` the following folders:
    ## /etc/make-helper-one.bsh_pre.d/*.conf
    ## /usr/local/etc/make-helper-one.bsh_pre.d/*.conf
    source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/pre.bsh
  fi

  ## Color variables are set by pre.bsh if available; default to empty
  ## strings so printf calls do not fail under nounset.
  : "${red:=}" "${green:=}" "${bold:=}" "${reset:=}" \
    "${cyan:=}" "${under:=}" "${eunder:=}"

  temp="$*"
  if [ -z "$temp" ]; then
    make_output_info "called without parameters."
  else
    make_output_info "command: $*"
  fi

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

  if [ -n "${DIST_APTGETOPT_SERIALIZED:-}" ]; then
    mapfile -t DIST_APTGETOPT <<<"$DIST_APTGETOPT_SERIALIZED"
  else
    DIST_APTGETOPT=()
  fi

  make_function_run make_init

  make_function_run make_source_overrides_file "$@"
  make_function_run make_source_overrides_folder "$@"

  make_function_run make_get_distdir
  make_function_run make_get_destdir

  ## deb-{build,run,all}-dep only need Build-Depends / Depends parsed
  ## from debian/control. Every other target additionally needs
  ## dpkg-parsechangelog (from dpkg-dev, pulled in by build-essential)
  ## which may be absent on a fresh system -- the user would run
  ## 'genmkfile deb-build-dep' to install it. So call the full setup
  ## only when a target actually needs it.
  __needs_version_info=false
  __needs_dependencies=false
  for __arg in "$@"; do
    case "$__arg" in
    "" | help | --) ;;
    deb-build-dep | deb-run-dep | deb-all-dep)
      __needs_dependencies=true
      ;;
    *)
      __needs_version_info=true
      ;;
    esac
  done
  ## No arguments falls through to make_all in make_parse_cmd; treat it
  ## like any other build target.
  [ $# -eq 0 ] && __needs_version_info=true

  if [ "$__needs_version_info" = "true" ]; then
    make_function_run make_get_variables
  elif [ "$__needs_dependencies" = "true" ]; then
    make_function_run make_get_dependencies
  fi
  unset __arg __needs_version_info __needs_dependencies

  make_function_run make_parse_cmd "$@"
  make_function_run make_deinit

  true "$0: END"
fi
