#!/bin/sh
#
# this script tries to do some quality checks
# automatically. It counts compiler warnings,
# builds in both debug/release mode, test multiple
# compilers etc.
#
# To get the most out of it, install as many variants of gcc and clang
# as you can. If g++ is found, it will look for g++-* in the same directory.
# If clang++ is found, it will look for clang++-* in the same directory.
# This means you need to have either system wide installs, or your PATH is
# setup in such a way that g++/clang++ points to the same location as the compilers
# you want to test.
#
# If clang is available, builds with address and undefined sanitizer will be made.
#
# If clang is available and possible to use with libc++, it will be built. On Ubuntu,
# install libc++abi-dev and libc++-dev
#
# If valgrind is available, it will be added as one thing to test.
#
# If dpkg-buildflags is available, a test build will be added with the flags
# coming from that tool.
#
# A build with debug iterators (https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html)
# is made.
#
# All compiles are checked to be warning free, all unit tests should pass.
#
# LICENSE: GPLv2 or later, at your option.
# by Paul Dreik 20181014

set -e

export LANG=

rootdir="$(dirname "$0")"
me="$(basename "$0")"

#flags to configure, for assert.
ASSERT=

###############################################################################
start_from_scratch() {
  cd "$rootdir"
  if [ -e Makefile ]; then
    make distclean >/dev/null 2>&1
  fi

}
###############################################################################
#argument 1 is the compiler
#argument 2 is the c++ standard
#argument 3 (optional) is appended to CXXFLAGS
compile_and_test_standard() {
  start_from_scratch
  # shellcheck disable=SC3037
  /bin/echo -n "$me: using $(basename "$1") with standard $2"
  if [ -n "$3" ]; then
    echo " (with additional CXXFLAGS $3)"
  else
    echo ""
  fi

  if ! ./bootstrap.sh >bootstrap.log 2>&1; then
    echo "$me: failed bootstrap - see bootstrap.log"
    exit 1
  fi
  if ! ./configure $ASSERT --enable-warnings CXX="$1" CXXFLAGS="-std=$2 $3" >configure.log 2>&1; then
    echo "$me: failed configure - see configure.log"
    exit 1
  fi
  #make sure it compiles
  if [ ! -x /usr/bin/time ]; then
    echo "$me: please install /usr/bin/time (apt install time)"
    exit 1
  fi
  if ! /usr/bin/time --format=%e --output=time.log make >make.log 2>&1; then
    echo "$me: failed make"
    exit 1
  fi
  if [ -n "$MEASURE_COMPILE_TIME" ]; then
    echo "$me: compile with $(basename "$1") '$2' took $(cat time.log) seconds"
  fi
  #check for warnings
  if grep -q "warning" make.log; then
    # store as an artifact instead of erroring out
    name="$(cat ./*.log | sha256sum | head -c 12)"
    cp make.log "make_${name}.log"
    echo "$me: found compile warning - see make.log, also stored as make_${name}.log"
  fi
  #run the tests
  if ! make check >makecheck.log 2>&1; then
    echo "$me: failed make check - see makecheck.log"
    exit 1
  fi
}
###############################################################################
#argument 1 is the compiler
compile_and_test() {
  #this is the test program to compile, so we know the compiler and standard lib
  #works. clang 4 with c++2a does not.
  echo "#include <iostream>" >x.cpp
  #does the compiler understand c++17? That is mandatory.
  if ! $1 -c x.cpp -std=c++17 >/dev/null 2>&1; then
    echo "$me: this compiler '$1' does not understand c++17"
    return 0
  fi

  #loop over all standard flags>=17 and try those which work.
  #use the code words.
  for std in 1z 2a 2b; do
    if ! $1 -c x.cpp -std=c++$std >/dev/null 2>&1; then
      echo "$me: compiler does not understand c++$std, skipping this combination."
    else
      # debug build
      ASSERT=--enable-assert
      compile_and_test_standard "$1" "c++$std" "-Og"

      # release build
      ASSERT=--disable-assert
      #compile_and_test_standard $1 c++$std "-O2"
      compile_and_test_standard "$1" "c++$std" "-O3"
      #compile_and_test_standard $1 c++$std "-Os"
    fi
  done

  rm x.cpp
}
###############################################################################
# finds the latest clang on the form clang++-<ver> and if none found, checks for
# clang++. first found is assigned to variable latestclang
get_latest_clang() {
  for ver in $(seq 30 -1 10); do
    candidate="clang++-$ver"
    if which "$candidate" >/dev/null 2>&1; then
      latestclang="$candidate"
      return
    fi
  done
  if which clang++ >/dev/null 2>&1; then
    latestclang=clang++
    return
  fi
  latestclang=
}
###############################################################################
run_with_sanitizer() {
  echo "$me: running with sanitizer (options $1)"
  get_latest_clang
  if [ -z "$latestclang" ]; then
    echo "$me: could not find any clang compiler (on the form clang++-ver)"
    return 0
  fi

  start_from_scratch
  ./bootstrap.sh >bootstrap.log
  ./configure "$ASSERT" "CXX=$latestclang" CXXFLAGS="-std=c++17 $1" >configure.log
  make >make.log 2>&1
  export UBSAN_OPTIONS="halt_on_error=true exitcode=1"
  export ASAN_OPTIONS="halt_on_error=true exitcode=1"
  make check >make-check.log 2>&1
  unset UBSAN_OPTIONS
  unset ASAN_OPTIONS
}
###############################################################################
#This tries to mimic how the debian package is built
run_with_debian_buildflags() {
  echo "$me: running with buildflags from debian dpkg-buildflags"
  if ! which dpkg-buildflags >/dev/null; then
    echo "$me: dpkg-buildflags not found - skipping"
    return 0
  fi
  start_from_scratch
  ./bootstrap.sh >bootstrap.log
  eval "$(DEB_BUILD_MAINT_OPTIONS="hardening=+all qa=+all,-canary reproducible=+all" dpkg-buildflags --export=sh)"
  ./configure >configure.log
  make >make.log 2>&1
  #check for warnings
  if grep -q "warning" make.log; then
    # store as an artifact instead of erroring out
    name="$(cat ./*.log | sha256sum | head -c 12)"
    cp make.log "make_${name}.log"
    echo "$me: found compile warnings - see make.log, also stored as make_${name}.log"

  fi
  make check >make-check.log 2>&1

  #restore the build environment
  for flag in $(dpkg-buildflags | cut -f1 -d=); do
    unset "$flag"
  done
}
###############################################################################
run_with_libcpp() {
  # use the latest clang and see if it works
  echo "#include <iostream>
   int main() { std::cout<<\"libc++ works!\"<<std::endl;}" >x.cpp
  get_latest_clang
  if [ -n "$latestclang" ]; then
    if ! "$latestclang" -std=c++17 -stdlib=libc++ -lc++abi x.cpp >/dev/null 2>&1 || [ ! -x ./a.out ] || ! ./a.out; then
      echo "$me: debug: $latestclang could not compile with libc++ - perhaps uninstalled."
      return
    fi
    compile_and_test_standard "$latestclang" c++17 "-stdlib=libc++ -D_LIBCPP_DEBUG=1"
    return
  fi
  # we will get here if no clang could be found. that is not an error,
  # having clang and libc++ installed is optional
  echo "$me: no working clang with libc++ found, skipping."
}
###############################################################################

verify_packaging() {
  #make sure the packaging works as intended.
  echo "$me: trying to make a tar ball for release and building it..."
  log="$(pwd)/packagetest.log"
  ./bootstrap.sh >"$log"
  ./configure >>"$log"

  touch dummy
  make dist >>"$log"
  TARGZ=$(find "$(pwd)" -newer dummy -name "rdfind*gz" -type f | head -n1)
  rm dummy
  temp="$(mktemp -d)"
  cp "$TARGZ" "$temp"
  cd "$temp"
  tar xzf "$(basename "$TARGZ")" >>"$log"
  cd "$(basename "$TARGZ" .tar.gz)"
  {
    ./configure --prefix="$temp"
    make
    make check
    make install
    "$temp/bin/rdfind" --version
  } >>"$log"
  #coming here means all went fine, go back to the source dir.
  cd "$(dirname "$TARGZ")"
  rm -rf "$temp"
}

###############################################################################
verify_self_contained_headers() {
  # shellcheck disable=SC3037
  /bin/echo -n "$me: verify that all header files are self contained..."
  if [ ! -e configure ]; then
    ./bootstrap.sh >bootstrap.log 2>&1
  fi
  if [ ! -e config.h ]; then
    ./configure >configure.log 2>&1
  fi
  for header in *.hh; do
    cp "$header" tmp.cc
    if ! g++ -std=c++17 -I. -c tmp.cc -o /dev/null >header.log 2>&1; then
      echo "$me: found a header which is not self contained: $header."
      echo "$me: see header.log for details"
      exit 1
    fi
    rm tmp.cc
  done
  echo "OK!"
}

###############################################################################
build_32bit() {
  #compiling to 32 bit, on amd64.
  #apt install libc6-i386 gcc-multilib g++-multilib
  #
  if [ "$(uname -m)" != x86_64 ]; then
    echo "$me: not on x64, won't cross compile with -m32"
    return
  fi
  echo "$me: trying to compile in 32 bit mode with -m32..."
  configureflags="--build=i686-pc-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32"
  here="$(pwd)"
  nettleinstall=$here/nettle32bit
  if [ -d "$nettleinstall" ]; then
    echo "$me: local nettle already seems to be installed"
  else
    mkdir "$nettleinstall"
    cd "$nettleinstall"
    nettleversion=3.10.1
    echo "$me: downloading nettle from gnu.org..."
    wget --quiet https://ftp.gnu.org/gnu/nettle/nettle-$nettleversion.tar.gz
    echo "b0fcdd7fc0cdea6e80dcf1dd85ba794af0d5b4a57e26397eee3bc193272d9132  nettle-$nettleversion.tar.gz" >checksum
    sha256sum --strict --quiet -c checksum
    tar xzf nettle-$nettleversion.tar.gz
    cd nettle-$nettleversion
    echo "$me: trying to configure nettle"
    # shellcheck disable=SC2086
    ./configure $configureflags --prefix="$nettleinstall" >"$here/nettle.configure.log" 2>&1
    make install >"$here/nettle.install.log" 2>&1
    echo "$me: local nettle install went ok"
    cd "$here"
  fi
  ./bootstrap.sh >bootstrap.log 2>&1
  echo "$me: attempting configure with 32 bit flags... (see configure.log if it fails)"
  ./configure --build=i686-pc-linux-gnu CFLAGS=-m32 CXXFLAGS="-m32 -I$nettleinstall/include" LDFLAGS="-m32 -L$nettleinstall/lib" >configure.log 2>&1
  echo "$me: building with 32 bit flags... (check make.log if it fails)"
  make >make.log 2>&1
  echo "$me: make check with 32 bit flags... (check make-check.log if it fails)"
  LD_LIBRARY_PATH=$nettleinstall/lib make check >make-check.log 2>&1
  echo "$me: 32 bit tests went fine!"
}
###############################################################################

#this is pretty quick so start with it.
verify_self_contained_headers

#keep track of which compilers have already been tested
echo "" >inodes_for_tested_compilers.txt

#try all variants of g++
if which g++ >/dev/null; then
  for COMPILER in "$(which g++)"*; do
    if echo "$COMPILER" | grep libc; then
      continue
    fi
    inode=$(stat --dereference --format=%i "$COMPILER")
    if [ -f inodes_for_tested_compilers.txt ] && grep -q "^$inode\$" inodes_for_tested_compilers.txt >/dev/null; then
      echo "$me: skipping this compiler $COMPILER - already tested"
    else
      #echo trying gcc $GCC:$($GCC --version|head -n1)
      echo "$inode" >>inodes_for_tested_compilers.txt
      compile_and_test "$COMPILER"
    fi
  done
fi

#try all variants of clang
get_latest_clang
if which "$latestclang" >/dev/null; then
  for COMPILER in "$(dirname "$(which "$latestclang")")"/clang++*; do
    if echo "$COMPILER" | grep libc; then
      continue
    fi
    inode=$(stat --dereference --format=%i "$COMPILER")
    if [ -f inodes_for_tested_compilers.txt ] && grep -q "^$inode\$" inodes_for_tested_compilers.txt >/dev/null; then
      echo "$me: skipping this compiler $COMPILER - already tested"
    else
      #echo trying gcc $GCC:$($GCC --version|head -n1)
      echo "$inode" >>inodes_for_tested_compilers.txt
      compile_and_test "$COMPILER"
    fi
  done
fi

rm inodes_for_tested_compilers.txt

#run unit tests with sanitizers enabled
ASSERT="--enable-assert"
run_with_sanitizer "-fsanitize=undefined -O3"
run_with_sanitizer "-fsanitize=address -O0"

#build and test with all flags from debian, if available. this increases
#the likelihood rdfind will build when creating a deb package.
ASSERT=""
run_with_debian_buildflags

#make a test build with debug iterators
ASSERT="--enable-assert"
compile_and_test_standard g++ c++17 "-D_GLIBCXX_DEBUG"

#test run with clang/libc++
ASSERT="--enable-assert"
run_with_libcpp
ASSERT="--disable-assert"
run_with_libcpp

#test build with running through valgrind
if which valgrind >/dev/null; then
  echo "$me: running unit tests through valgrind"
  ASSERT="--disable-assert"
  compile_and_test_standard g++ c++17 "-O3"
  VALGRIND=valgrind make check >make-check.log
fi

#make sure it is possible to build a tar ball,
#unpack it, build and execute tests, then finally
#installing and running the program.
verify_packaging

#try to compile to 32 bit (downloads nettle and builds it in 32 bit mode)
build_32bit

echo "$me: congratulations, all tests that were possible to run passed!"
