#!/bin/bash
set -eE -o pipefail

if ((DEBUG)); then
  set -x
fi

BUILDDIRECTORY=/var/lib/repro

BOOTSTRAPMIRROR=https://mirror.archlinux.no/iso/latest
readonly bootstrap_img=archlinux-bootstrap-"$(date +%Y.%m)".01-"$(uname -m)".tar.gz
CONFIGDIR='/etc/archlinux-repro'

# HOSTMIRROR=$(curl -s 'https://www.archlinux.org/mirrorlist/?protocol=https' | awk '/^#Server/ {print $3; exit}')
## Hardcoded until further notice
HOSTMIRROR="http://mirror.neuf.no/archlinux/\$repo/os/\$arch"

IMGDIRECTORY=$(mktemp -dt XXXXXXXXXX.arch_img)
trap "{ rm -r $IMGDIRECTORY; }" EXIT

DIFFOSCOPE="diffoscope"

# Turn on/off check in repro
NOCHECK=${NOCHECK:-0}

# Desc: Escalates privileges
orig_argv=("$0" "$@")
src_owner=${SUDO_USER:-$USER}
function check_root() {
    local keepenv=$1
    (( EUID == 0 )) && return
    if type -P sudo >/dev/null; then
        exec sudo --preserve-env=$keepenv -- "${orig_argv[@]}"
    else
        exec su root -c "$(printf ' %q' "${orig_argv[@]}")"
    fi
}

# Use a private gpg keyring
function gpg() {
  command gpg --homedir="$BUILDDIRECTORY/_gnupg" "$@"
}

function init_gnupg() {
    [ ! -d "$BUILDDIRECTORY/_gnupg" ] && mkdir -p "$BUILDDIRECTORY/_gnupg"

    # ensure signing key is available
    # We try WKD first, then fallback to keyservers.
    # This works on debian./
    gpg --keyserver=p80.pool.sks-keyservers.net --auto-key-locate wkd,keyserver --locate-keys pierre@archlinux.de
}

# Desc: Sets the appropriate colors for output
function colorize() {
    # prefer terminal safe colored and bold text when tput is supported
    if tput setaf 0 &>/dev/null; then
        ALL_OFF="$(tput sgr0)"
        BOLD="$(tput bold)"
        BLUE="${BOLD}$(tput setaf 4)"
        GREEN="${BOLD}$(tput setaf 2)"
        RED="${BOLD}$(tput setaf 1)"
        YELLOW="${BOLD}$(tput setaf 3)"
    else
        ALL_OFF="\e[0m"
        BOLD="\e[1m"
        BLUE="${BOLD}\e[34m"
        GREEN="${BOLD}\e[32m"
        RED="${BOLD}\e[31m"
        YELLOW="${BOLD}\e[33m"
    fi
    readonly ALL_OFF BOLD BLUE GREEN RED YELLOW
}
colorize

# Desc: Message format
function msg() {
    local mesg=$1; shift
    # shellcheck disable=SC2059
    printf "${GREEN}==>${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2
}

# Desc: Sub-message format
function msg2() {
    local mesg=$1; shift
    # shellcheck disable=SC2059
    printf "${BLUE}  ->${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2
}

# Desc: Warning format
function warning() {
    local mesg=$1; shift
    # shellcheck disable=SC2059
    printf "${YELLOW}==> $(gettext "WARNING:")${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2
}

# Desc: Error format
function error() {
    local mesg=$1; shift
    # shellcheck disable=SC2059
    printf "${RED}==> $(gettext "ERROR:")${ALL_OFF}${BOLD} ${mesg}${ALL_OFF}\n" "$@" >&2
}

##
#  usage : lock( $fd, $file, $message, [ $message_arguments... ] )
##
lock() {
    # Only reopen the FD if it wasn't handed to us
    if ! [[ "/dev/fd/$1" -ef "$2" ]]; then
        mkdir -p -- "$(dirname -- "$2")"
        eval "exec $1>"'"$2"'
    fi

    flock -n "$1"
}

##
#  usage : slock( $fd, $file, $message, [ $message_arguments... ] )
##
slock() {
    # Only reopen the FD if it wasn't handed to us
    if ! [[ "/dev/fd/$1" -ef "$2" ]]; then
        mkdir -p -- "$(dirname -- "$2")"
        eval "exec $1>"'"$2"'
    fi

    if ! flock -sn "$1"; then
        flock -s "$1"
    fi
}

##
#  usage : lock_close( $fd )
##
lock_close() {
    local fd=$1
    # https://github.com/koalaman/shellcheck/issues/862
    # shellcheck disable=2034
    exec {fd}>&-
}

# Desc: Executes an command inside a given nspawn container
# 1: Container name
# 2: Command to execute
function exec_nspawn(){
    local container=$1
    systemd-nspawn -q \
      --as-pid2 \
      --register=no \
      --pipe \
      -E "PATH=/usr/local/sbin:/usr/local/bin:/usr/bin" \
      -D "$BUILDDIRECTORY/$container" "${@:2}"
}

# Desc: Removes the root container
function cleanup_root_volume(){
    warning "Removing root container..."
    rm -rf "$BUILDDIRECTORY/root"
}

# Desc: Removes a given snapshot
# 1: Snapshot name
function remove_snapshot (){
    local build=$1
    msg2 "Delete snapshot for $build..."
    umount "$BUILDDIRECTORY/$build" || true
    rm -rf "${BUILDDIRECTORY:?}/${build}"
    rm -rf "${BUILDDIRECTORY:?}/${build}_upperdir"
    rm -rf "${BUILDDIRECTORY:?}/${build}_workdir"
    trap - ERR INT
}

# Desc: Creates a snapshot of the root container
# 1: name of container
function create_snapshot (){
    local build="$1"

    trap "{ remove_snapshot \"$build\" ; exit 1; }" ERR INT

    msg2 "Create snapshot for $build..."
    mkdir -p "$BUILDDIRECTORY/"{"${build}","${build}_upperdir","${build}_workdir"}
    # shellcheck disable=SC2140
    mount -t overlay overlay \
        -o lowerdir="$BUILDDIRECTORY/root",upperdir="$BUILDDIRECTORY/${build}_upperdir",workdir="$BUILDDIRECTORY/${build}_workdir" \
        "$BUILDDIRECTORY/${build}"
    touch "$BUILDDIRECTORY/$build"
}

# Desc: Build a package inside a container
# 1: Container name
# 2: Container buildpath
function build_package(){
    local build=$1
    local builddir=${2:-"/startdir"}
    local cmds=""
    if ((NOCHECK)); then
      cmds="--nocheck"
    fi
    exec_nspawn "$build" \
bash <<-__END__
set -e
install -d -o builduser -g builduser /pkgdest
install -d -o builduser -g builduser /srcpkgdest
install -d -o builduser -g builduser /build
__END__
    exec_nspawn "$build" /usr/bin/setsid -f -c -w sudo -iu builduser bash -c ". /etc/profile; cd /startdir; SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH makepkg -sc --noconfirm --skippgpcheck $cmds"
    mkdir -p "./build"
    for pkgfile in "$BUILDDIRECTORY/$build"/pkgdest/*; do
        mv "$pkgfile" "./build/"
    done
    chown -R "$src_owner" "./build"
}

# Desc: Sets up a container with the correct files
function init_chroot(){
    set -e

    [ ! -d "$BUILDDIRECTORY" ] && mkdir -p "$BUILDDIRECTORY"

    # Prepare root chroot
    if [ ! -d "$BUILDDIRECTORY"/root ]; then
        lock 9 "$BUILDDIRECTORY"/root.lock
        msg "Preparing chroot"
        trap '{ cleanup_root_volume; exit 1; }' ERR
        trap '{ cleanup_root_volume; trap - INT; kill -INT $$; }' INT

        msg2 "Extracting image into container..."
        mkdir -p "$BUILDDIRECTORY/root"
        tar xvf "$IMGDIRECTORY/$bootstrap_img" -C "$BUILDDIRECTORY/root" --strip-components=1 > /dev/null

        printf 'Server = %s\n' "$HOSTMIRROR" > "$BUILDDIRECTORY"/root/etc/pacman.d/mirrorlist
        printf '%s.UTF-8 UTF-8\n' en_US de_DE > "$BUILDDIRECTORY"/root/etc/locale.gen
        printf 'LANG=en_US.UTF-8\n' > "$BUILDDIRECTORY"/root/etc/locale.conf

        systemd-machine-id-setup --root="$BUILDDIRECTORY"/root
        msg2 "Setting up keyring, this might take a while..."
        exec_nspawn root pacman-key --init &> /dev/null
        exec_nspawn root pacman-key --populate archlinux &> /dev/null

        msg2 "Updating and installing base & base-devel"
        exec_nspawn root pacman -Syu base-devel --noconfirm
        exec_nspawn root pacman -R arch-install-scripts --noconfirm
        exec_nspawn root locale-gen

        printf 'builduser ALL = NOPASSWD: /usr/bin/pacman\n' > "$BUILDDIRECTORY"/root/etc/sudoers.d/builduser-pacman
        exec_nspawn root useradd -m -G wheel -s /bin/bash -d /build builduser
        echo "keyserver-options auto-key-retrieve" | install -Dm644 /dev/stdin "$BUILDDIRECTORY/root"/build/.gnupg/gpg.conf
        exec_nspawn root chown -R builduser /build/.gnupg
        exec_nspawn root chmod 700 /build/.gnupg
        lock_close 9
    else

      if lock 9 "$BUILDDIRECTORY"/root.lock; then
          printf 'Server = %s\n' "$HOSTMIRROR" > "$BUILDDIRECTORY"/root/etc/pacman.d/mirrorlist
          exec_nspawn root pacman -Syu --noconfirm
          lock_close 9
      else
          msg "Couldn't acquire lock on root chroot, didn't update."
      fi
    fi
    trap - ERR INT
}

# Desc: Reproduces a package
# 1: Location of package
function cmd_check(){
    local pkg="${1}"
    local cachedir="cache"

    if [[ ! -f "${pkg}" ]]; then
      error "no package file given"
      exit 1
    fi

    trap - ERR INT


    declare -A buildinfo
    while IFS=$'=' read -r key value; do
        [[ "${key}" = [#!]* ]] || [[ "${key}" = "" ]] || buildinfo["${key}"]="${value}"
    done <<< "$(buildinfo -ff "${pkg}")"
    packager="${buildinfo[packager]}"
    builddir="${buildinfo[builddir]}"
    _pkgver="${buildinfo[pkgver]}"
    pkgrel=${_pkgver##*-}
    pkgver=${_pkgver%-*}
    pkgbase=${buildinfo[pkgbase]}
    options=${buildinfo[options]}
    buildenv=${buildinfo[buildenv]}
    format=${buildinfo[format]}

    pkgbuild_sha256sum="${buildinfo[pkgbuild_sha256sum]}"
    SOURCE_DATE_EPOCH="${buildinfo[builddate]}"


    local build="${pkgbase}_$$"

    if [[ ${format} -ne 1 ]]; then
      error "unsupported BUILDINFO format or no format definition found, aborting rebuild"
      exit 1
    fi

    msg2 "Preparing packages"
    mkdir -p "${cachedir}"
    mapfile -t packages < <(buildinfo -d "${cachedir}" "${pkg}")
    msg2 "Finished preparing packages"

    msg "Starting build..."
    slock 9 "$BUILDDIRECTORY"/root.lock
    create_snapshot "$build" 0

    # Father I have sinned
    exec_nspawn "$build" --bind="$(readlink -e ${cachedir}):/var/cache/pacman/pkg" \
    bash <<-__END__
shopt -s globstar
install -d -o builduser -g builduser /startdir
pacman -S asp devtools --noconfirm
cp /usr/share/devtools/makepkg-x86_64.conf /etc/makepkg.conf
asp checkout $pkgbase
cd $pkgbase
for rev in \$(git rev-list --all -- repos/); do
    pkgbuild_checksum=\$(git show \$rev:trunk/PKGBUILD | sha256sum -b)
    pkgbuild_checksum=\${pkgbuild_checksum%% *}
    if [ \$pkgbuild_checksum = $pkgbuild_sha256sum ]; then
        git checkout \$rev
        mv ./trunk/* /startdir
        pacman -Rs asp devtools --noconfirm
        exit 0
    fi
done
exit 1
__END__

    # Setup environment
    {
        printf 'MAKEFLAGS="%s"\n' "${MAKEFLAGS:--j$(nproc)}"
        printf 'PKGDEST=/pkgdest\n'
        printf 'SRCPKGDEST=/srcpkgdest\n'
        printf 'BUILDDIR=%s\n' "${builddir}"
        printf 'PACKAGER=%s\n' "${packager@Q}"
        printf 'OPTIONS=(%s)\n' "${options}"
        printf 'BUILDENV=(%s)\n' "${buildenv}"
        printf 'COMPRESSZST=(zstd -c -T0 --ultra -20 -)\n'
        printf 'PKGEXT=".pkg.tar%s"\n' "${pkg##*tar}"
     } >> "$BUILDDIRECTORY/$build/etc/makepkg.conf"
    # We do the signature checking with pacman -Udd
    sed -i "s/LocalFileSigLevel.*//g" "$BUILDDIRECTORY/$build/etc/pacman.conf"


    msg "Installing packages"
    # shellcheck disable=SC2086
    exec_nspawn "$build" --bind="$(readlink -e ${cachedir}):/cache" bash -c \
        'yes y | pacman -Udd --needed --overwrite "*" -- "$@"' -bash "${packages[@]}"

    read -r -a buildinfo_packages <<< "$(buildinfo -f installed "${pkg}")"
    uninstall=$(comm -13 \
        <(printf '%s\n' "${buildinfo_packages[@]}" | rev | cut -d- -f4- | rev | sort) \
        <(exec_nspawn "$build" --bind="$(readlink -e ${cachedir}):/cache" pacman -Qq | sort))

    if [ -n "$uninstall" ]; then
        exec_nspawn "$build" pacman -Rdd --noconfirm -- $uninstall

        # We might encounter issues where packages has been renamed, and we overwrote the files
        # This causes pacman to remove .so-names beliving it belongs to the package we removed
        # This fixes any messed up packages as reported by -Qqk
        # Note: ++ is removed because DEBUG gets injected into stderr
        fixup=$(exec_nspawn "$build" pacman -Qqk 2>&1 | grep -v "^++" | awk '{print $1}' | uniq) || true
        if [ -n "$fixup" ]; then
          fixup_packages=$(echo $fixup| tr ' ' '\0' | xargs -0 -n1 -I{} bash -c "printf -- '%s\n' ${packages[*]} | grep {}")
          exec_nspawn "$build" --bind="$(readlink -e ${cachedir}):/cache" bash -c \
              'yes y | pacman -Udd -- "$@"' -bash ${fixup_packages}
        fi
    fi

    build_package "$build" "$builddir"
    remove_snapshot "$build"
    chown -R "$src_owner" "${cachedir}"
    lock_close 9

    msg "Comparing hashes..."
    if diff -q -- "$pkg" ./build/"$(basename "$pkg")" > /dev/null ; then
      msg "Package is reproducible!"
      exit 0
    else
      error "Package is not reproducible"
      if ((run_diffoscope)); then
          PYTHONIOENCODING=utf-8 $DIFFOSCOPE "$pkg" ./build/"$(basename "$pkg")" || true
      fi
      exit 1
    fi
}

# Desc: Fetches a bootstrap image and verifies the signature
function get_bootstrap_img() {
    if [ ! -e "$IMGDIRECTORY/$bootstrap_img" ]; then
        msg "Downloading bootstrap image..."
        ( cd "$IMGDIRECTORY" && curl -f --remote-name-all "$BOOTSTRAPMIRROR/$bootstrap_img"{,.sig} )
        if ! gpg --verify "$IMGDIRECTORY/$bootstrap_img.sig" "$IMGDIRECTORY/$bootstrap_img"; then
            error "Can't verify image"
            exit 1
        fi
    fi
}

# Desc: Prints the help section
function print_help() {
cat <<__END__
Usage:
  repro [options]

General Options:
 -h                           Print this help message
 -d                           Run diffoscope if packages are not reproducible
__END__
}

hash buildinfo 2>/dev/null || { error "Require buildinfo in path! Aborting..."; exit 1; }

# Default options
run_diffoscope=0

repro_conf=$CONFIGDIR/repro.conf
if [[ -r $repro_conf ]]; then
    # shellcheck source=/dev/null
    source "$repro_conf"
fi

xdg_repro_dir="${XDG_CONFIG_HOME:-$HOME/.config}/archlinux-repro"
if [[ -r "$xdg_repro_dir/repro.conf" ]]; then
    # shellcheck source=/dev/null
    source "$xdg_repro_dir/repro.conf"
elif [[ -r "$HOME/.repro.conf" ]]; then
    # shellcheck source=/dev/null
    source "$HOME/.repro.conf"
fi


while getopts :hdn arg; do
    case $arg in
        h) print_help; exit 0;;
        d) run_diffoscope=1;;
        n) NOCHECK=1;;
        *) ;;
    esac
done

# Save command args (such as path to .pkg.tar.xz file)
shift $((OPTIND-1))

check_root NOCHECK,MAKEFLAGS,DEBUG
init_gnupg
test -d "$BUILDDIRECTORY"/root || get_bootstrap_img
init_chroot
cmd_check "$@"
