#!/bin/bash
# Should be run from the root dir, or SOURCE_DIR should be set.
set -o errexit
set -o nounset
set -o pipefail

# Default compiler
: ${CC:="gcc"}


# Paths
: ${SOURCE_DIR:="."}
: ${DIST_DIR:="${SOURCE_DIR}/dist"}
: ${BUILD_DIR:="/tmp/build"}

# GPG Configuration
: ${GPG_PASSPHRASE:=""}


# Make those paths absolute, and export them for the Python tests to consume.
export SOURCE_DIR="$(readlink -f "${SOURCE_DIR}")"
export DIST_DIR="$(readlink -f "${DIST_DIR}")"
export BUILD_DIR="$(readlink -f "${BUILD_DIR}")"

# Configuration
: ${FORCE_SUBREAPER:="1"}
export FORCE_SUBREAPER


# Our build platform doesn't have those newer Linux flags, but we want Tini to have subreaper support
# We also use those in our tests
CFLAGS="${CFLAGS-} -DPR_SET_CHILD_SUBREAPER=36 -DPR_GET_CHILD_SUBREAPER=37"
if [[ "${FORCE_SUBREAPER}" -eq 1 ]]; then
  # If FORCE_SUBREAPER is requested, then we set those CFLAGS for the Tini build
  export CFLAGS
fi

echo "CC=${CC}"
echo "CFLAGS=${CFLAGS}"
echo "MINIMAL=${MINIMAL-}"
echo "ARCH_SUFFIX=${ARCH_SUFFIX-}"
echo "ARCH_NATIVE=${ARCH_NATIVE-}"

# Ensure Python output is not buffered (to make tests output clearer)
export PYTHONUNBUFFERED=1

# Set path to prioritize our utils
export REAL_PATH="${PATH}"
export PATH="${SOURCE_DIR}/ci/util:${PATH}"

# Build
CMAKE_ARGS=(-B"${BUILD_DIR}" -H"${SOURCE_DIR}")
if [[ -n "${MINIMAL:-}" ]]; then
  CMAKE_ARGS+=(-DMINIMAL=ON)
fi
cmake "${CMAKE_ARGS[@]}"

pushd "${BUILD_DIR}"
make clean
make
if [[ -n "${ARCH_NATIVE-}" ]]; then
  make package
fi
popd

pkg_version="$(cat "${BUILD_DIR}/VERSION")"

if [[ -n "${ARCH_NATIVE-}" ]]; then
  echo "Built native package (ARCH_NATIVE=${ARCH_NATIVE-})"
  echo "Running smoke and internal tests"

  BIN_TEST_DIR="${BUILD_DIR}/bin-test"
  mkdir -p "$BIN_TEST_DIR"
  export PATH="${BIN_TEST_DIR}:${PATH}"

  # Smoke tests (actual tests need Docker to run; they don't run within the CI environment)
  for tini in "${BUILD_DIR}/tini" "${BUILD_DIR}/tini-static"; do
    echo "Smoke test for ${tini}"
    "$tini" --version

    echo "Testing ${tini} --version"
    "$tini" --version | grep -q "tini version"

    echo "Testing ${tini} without arguments exits with 1"
    ! "$tini" 2>/dev/null

    echo "Testing ${tini} shows help message"
    {
      ! "$tini" 2>&1
    } | grep -q "supervision of a valid init process"

    if [[ -n "${MINIMAL:-}" ]]; then
      echo "Testing $tini with: true"
      "${tini}" true

      echo "Testing $tini with: false"
      if "${tini}" false; then
        exit 1
      fi

      echo "Testing ${tini} does not reference options that don't exist"
      ! {
        ! "$tini" 2>&1
      } | grep -q "more verbose"

      # We try running binaries named after flags (both valid and invalid
      # flags) and test that they run.
      for flag in h s w x; do
        bin="-${flag}"
        echo "Testing $tini can run binary: ${bin}"
        cp "$(which true)" "${BIN_TEST_DIR}/${bin}"
        "$tini" "$bin"
      done

      echo "Testing $tini can run binary --version if args are given"
      cp "$(which true)" "${BIN_TEST_DIR}/--version"
      if "$tini" "--version" --foo | grep -q "tini version"; then
        exit 1
      fi
    else
      echo "Testing ${tini} -h"
      "${tini}" -h

      echo "Testing $tini for license"
      "$tini" -l | diff - "${SOURCE_DIR}/LICENSE"

      echo "Testing $tini with: true"
      "${tini}" -vvv true

      echo "Testing $tini with: false"
      if "${tini}" -vvv false; then
        exit 1
      fi

      echo "Testing ${tini} references options that exist"
      {
        ! "$tini" 2>&1
      } | grep -q "more verbose"

      echo "Testing $tini with: -- true (should succeed)"
      "${tini}" -vvv -- true

      echo "Testing $tini with: -- -- true (should fail)"
      if "${tini}" -vvv -- -- true; then
        exit 1
      fi
    fi

    echo "Testing ${tini} supports TINI_VERBOSITY"
    TINI_VERBOSITY=3 "$tini" true 2>&1 | grep -q 'Received SIGCHLD'

    echo "Testing ${tini} exits with 127 if the command does not exist"
    "$tini" foobar123 && rc="$?" || rc="$?"
    if [[ "$rc" != 127 ]]; then
      echo "Exit code was: ${rc}"
      exit 1
    fi

    echo "Testing ${tini} exits with 126 if the command is not executable"
    "$tini" /etc && rc="$?" || rc="$?"
    if [[ "$rc" != 126 ]]; then
      echo "Exit code was: ${rc}"
      exit 1
    fi

    # Test stdin / stdout are handed over to child
    echo "Testing ${tini} does not break pipes"
    echo "exit 0" | "${tini}" sh
    if [[ ! "$?" -eq "0" ]]; then
      echo "Pipe test failed"
      exit 1
    fi

    echo "Checking hardening on $tini"
    hardening_skip=(--nopie --nostackprotector --nobindnow)
    if [[ "$CC" == "musl-gcc" ]]; then
      # FORTIFY_SOURCE is a glibc thing
      hardening_skip=("${hardening_skip[@]}" --nofortify)
    fi
    hardening-check  "${hardening_skip[@]}" "${tini}"
  done

  # Quick package audit
  if which rpm >/dev/null; then
    echo "Contents for RPM:"
    rpm -qlp "${BUILD_DIR}/tini_${pkg_version}.rpm"
    echo "--"
  fi

  if which dpkg >/dev/null; then
    echo "Contents for DEB:"
    dpkg --contents "${BUILD_DIR}/tini_${pkg_version}.deb"
    echo "--"
  fi

  # Compile test code
  "${CC}" -o "${BUILD_DIR}/sigconf-test" "${SOURCE_DIR}/test/sigconf/sigconf-test.c"

  # Create virtual environment to run tests.
  # Accept system site packages for faster local builds.
  VENV="${BUILD_DIR}/venv"
  virtualenv --system-site-packages "${VENV}"

  # Don't use activate because it does not play nice with nounset
  export PATH="${VENV}/bin:${PATH}"
  export CFLAGS  # We need them to build our test suite, regardless of FORCE_SUBREAPER

  # Install test dependencies
  CC=gcc python3 -m pip install psutil python-prctl bitmap

  # Run tests
  python3 "${SOURCE_DIR}/test/run_inner_tests.py"
else
  echo "Not a native build, skipping smoke and internal tests"
fi

# Now, copy over files to DIST_DIR, with appropriate names depending on the
# architecture.
# Handle the DEB / RPM
mkdir -p "${DIST_DIR}"

DIST_TINIS=()

SUFFIX=""
if [[ -n "$ARCH_SUFFIX" ]]; then
  SUFFIX="-${ARCH_SUFFIX}"
elif [[ -z "$ARCH_NATIVE" ]]; then
  echo "Refusing to publish a non-native build without suffix!"
  exit 1
fi

for build_tini in tini tini-static; do
  dist_tini="${build_tini}${SUFFIX}"
  cp "${BUILD_DIR}/${build_tini}" "${DIST_DIR}/${dist_tini}"
  DIST_TINIS+=("$dist_tini")
done

if [[ -n "${ARCH_NATIVE-}" ]]; then
  for pkg_format in deb rpm; do
    build_tini="tini_${pkg_version}.${pkg_format}"
    dist_tini="tini_${pkg_version}${SUFFIX}.${pkg_format}"
    cp "${BUILD_DIR}/${build_tini}" "${DIST_DIR}/${dist_tini}"
    DIST_TINIS+=("$dist_tini")
  done
fi

echo "Tinis: ${DIST_TINIS[*]}"

pushd "$DIST_DIR"

for tini in "${DIST_TINIS[@]}"; do
  echo "${tini}:"

  for sum in sha1sum sha256sum; do
    "$sum" "$tini" | tee "${tini}.${sum}"
  done

  file "$tini"
  echo "--"
done

# If a signing key and passphrase are made available, then use it to sign the
# binaries
if [[ -n "$GPG_PASSPHRASE" ]] && [[ -f "${SOURCE_DIR}/sign.key" ]]; then
  echo "Signing tinis"
  GPG_SIGN_HOMEDIR="${BUILD_DIR}/gpg-sign"
  GPG_VERIFY_HOMEDIR="${BUILD_DIR}/gpg-verify"
  PGP_KEY_FINGERPRINT="595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7"
  PGP_KEYSERVER="ha.pool.sks-keyservers.net"

  mkdir "${GPG_SIGN_HOMEDIR}" "${GPG_VERIFY_HOMEDIR}"
  chmod 700 "${GPG_SIGN_HOMEDIR}" "${GPG_VERIFY_HOMEDIR}"

  gpg --homedir "${GPG_SIGN_HOMEDIR}" --import "${SOURCE_DIR}/sign.key"
  gpg --homedir "${GPG_VERIFY_HOMEDIR}" --keyserver "$PGP_KEYSERVER" --recv-keys "$PGP_KEY_FINGERPRINT"

  for tini in "${DIST_TINIS[@]}"; do
    echo "${GPG_PASSPHRASE}" | gpg --homedir "${GPG_SIGN_HOMEDIR}" --passphrase-fd 0 --armor --detach-sign "${tini}"
    gpg --homedir "${GPG_VERIFY_HOMEDIR}" --verify "${tini}.asc"
  done
fi

popd
