#!/bin/sh
#
# Type `build -h` for help and see https://github.com/romkatv/gitstatus
# for full documentation.

set -ue

if [ -n "${ZSH_VERSION:-}" ]; then
  emulate sh -o err_exit -o no_unset
fi

export LC_ALL=C

if [ -z "${ZSH_VERSION-}" ] && command -v zsh >/dev/null 2>&1; then
  case "${BASH_VERSION-}" in
    [0-3].*) exec zsh "$0" "$@";;
  esac
fi

usage="$(command cat <<\END
Usage: build [-m ARCH] [-c CPU] [-d CMD] [-i IMAGE] [-s] [-w]

Options:

  -m ARCH   `uname -m` from the target machine; defaults to `uname -m`
            from the local machine
  -c CPU    generate machine instructions for CPU of this type; this
            value gets passed as `-march` (or `-mcpu` for ppc64le) to gcc;
            inferred from ARCH if not set explicitly
  -d CMD    build in a Docker container and use CMD as the `docker`
            command; e.g., `-d docker` or `-d podman`
  -i IMAGE  build in this Docker image; inferred from ARCH if not set
            explicitly
  -s        install whatever software is necessary for build to
            succeed; on some operating systems this option is not
            supported; on others it can have partial effect
  -w        automatically download tarballs for dependencies if they
            do not already exist in ./deps; dependencies are described
            in ./build.info
END
)"

build="$(command cat <<\END
outdir="$(command pwd)"

if command -v mktemp >/dev/null 2>&1; then
  workdir="$(command mktemp -d "${TMPDIR:-/tmp}"/gitstatus-build.XXXXXXXXXX)"
else
  workdir="${TMPDIR:-/tmp}/gitstatus-build.tmp.$$"
  command mkdir -- "$workdir"
fi

cd -- "$workdir"
workdir="$(command pwd)"

narg() { echo $#; }

if [ "$(narg $workdir)" != 1 -o -z "${workdir##*:*}" -o -z "${workdir##*=*}" ]; then
  >&2 echo "[error] cannot build in this directory: $workdir"
  exit 1
fi

appname=gitstatusd
libgit2_tmp="$outdir"/deps/"$appname".libgit2.tmp

cleanup() {
  trap - INT QUIT TERM ILL PIPE
  cd /
  if ! command rm -rf -- "$workdir" "$outdir"/usrbin/"$appname".tmp "$libgit2_tmp"; then
    command sleep 5
    command rm -rf -- "$workdir" "$outdir"/usrbin/"$appname".tmp "$libgit2_tmp"
  fi
}
trap cleanup INT QUIT TERM ILL PIPE

if [ -n "$gitstatus_install_tools" ]; then
  case "$gitstatus_kernel" in
    linux)
      if command -v apk >/dev/null 2>&1; then
        command apk update
        command apk add binutils cmake gcc g++ git make musl-dev perl-utils
      elif command -v apt-get >/dev/null 2>&1; then
        apt-get update
        apt-get install -y binutils cmake gcc g++ make wget
      else
        >&2 echo "[error] -s is not supported on this system"
        exit 1
      fi
    ;;
    freebsd)
      command pkg install -y cmake gmake binutils git perl5 wget
    ;;
    openbsd)
      command pkg_add install cmake gmake gcc git wget
    ;;
    netbsd)
      command pkgin -y install cmake gmake binutils git
    ;;
    darwin)
      if ! command -v make >/dev/null 2>&1 || ! command -v gcc >/dev/null 2>&1; then
        >&2 echo "[error] please run 'xcode-select --install' and retry"
        exit 1
      fi
      if command -v port >/dev/null 2>&1; then
        sudo port -N install libiconv cmake wget
      elif command -v brew >/dev/null 2>&1; then
        for formula in libiconv cmake git wget; do
          if command brew ls --version "$formula" &>/dev/null; then
            command brew upgrade "$formula"
          else
            command brew install "$formula"
          fi
        done
      else
        >&2 echo "[error] please install MacPorts or Homebrew and retry"
        exit 1
      fi
    ;;
    msys*|mingw*)
      command pacman -Syu --noconfirm
      command pacman -S --needed --noconfirm binutils cmake gcc git make perl
    ;;
    *)
      >&2 echo "[internal error] unhandled kernel: $gitstatus_kernel"
      exit 1
    ;;
  esac
fi

cpus="$(command getconf _NPROCESSORS_ONLN 2>/dev/null)" ||
  cpus="$(command sysctl -n hw.ncpu 2>/dev/null)" ||
  cpus=8

case "$gitstatus_cpu" in
  powerpc64le) archflag="-mcpu";;
  *)           archflag="-march";;
esac

cflags="$archflag=$gitstatus_cpu -fno-plt -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fpie"
ldflags=
static_pie=

if [ -z "${CC-}" ]; then
  case "$gitstatus_kernel" in
    freebsd) export CC=clang;;
    *)       export CC=cc;;
  esac
fi

printf 'int main() {}\n' >"$workdir"/cc-test.c
if 2>/dev/null "$CC"   \
     -ffile-prefix-map=x=y   \
     -Werror                 \
     -c "$workdir"/cc-test.c \
     -o "$workdir"/cc-test.o; then
  cflags="$cflags -ffile-prefix-map=$workdir/="
fi

command rm -f -- "$workdir"/cc-test "$workdir"/cc-test.o
if 2>/dev/null "$CC"                    \
     -fstack-clash-protection -fcf-protection \
     -Werror                                  \
     -c "$workdir"/cc-test.c                  \
     -o "$workdir"/cc-test.o; then
  cflags="$cflags -fstack-clash-protection -fcf-protection"
fi

command rm -f -- "$workdir"/cc-test "$workdir"/cc-test.o
if 2>/dev/null "$CC"                             \
     -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now \
     -Werror                                           \
     "$workdir"/cc-test.c                              \
     -o "$workdir"/cc-test; then
  ldflags="$ldflags -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now"
fi

command rm -f -- "$workdir"/cc-test "$workdir"/cc-test.o
if 2>/dev/null "$CC" \
     -fpie -static-pie     \
     -Werror               \
     "$workdir"/cc-test.c  \
     -o "$workdir"/cc-test; then
  static_pie='-static-pie'
fi

if [ "$gitstatus_cpu" = x86-64 ]; then
  cflags="$cflags -mtune=generic"
fi

libgit2_cmake_flags=
libgit2_cflags="${CFLAGS-} $cflags -O3 -DNDEBUG"

gitstatus_cxx=g++
gitstatus_cxxflags="${CXXFLAGS-} $cflags -I${workdir}/libgit2/include -DGITSTATUS_ZERO_NSEC -D_GNU_SOURCE -D_GLIBCXX_ASSERTIONS"
gitstatus_ldflags="${LDFLAGS-} $ldflags -L${workdir}/libgit2/build"
gitstatus_ldlibs=
gitstatus_make=make

case "$gitstatus_kernel" in
  linux)
    gitstatus_ldflags="$gitstatus_ldflags ${static_pie:--static}"
    libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=ON"
  ;;
  freebsd)
    gitstatus_cxx=clang++
    gitstatus_make=gmake
    gitstatus_ldflags="$gitstatus_ldflags ${static_pie:--static}"
    libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=ON"
  ;;
  openbsd)
    gitstatus_cxx=eg++
    gitstatus_make=gmake
    gitstatus_ldflags="$gitstatus_ldflags ${static_pie:--static}"
    libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=ON"
  ;;
  netbsd)
    gitstatus_make=gmake
    gitstatus_ldflags="$gitstatus_ldflags ${static_pie:--static}"
    libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=ON"
  ;;
  darwin)
    command mkdir -- "$workdir"/lib
    if [ -e /opt/local/lib/libiconv.a ]; then
      command ln -s -- /opt/local/lib/libiconv.a "$workdir"/lib
      libgit2_cflags="$libgit2_cflags -I/opt/local/include"
      gitstatus_cxxflags="$gitstatus_cxxflags -I/opt/local/include"
    else
      brew_prefix="$(command brew --prefix)"
      command ln -s -- "$brew_prefix"/opt/libiconv/lib/libiconv.a "$workdir"/lib
      libgit2_cflags="$libgit2_cflags -I"$brew_prefix"/opt/libiconv/include"
      gitstatus_cxxflags="$gitstatus_cxxflags -I"$brew_prefix"/opt/libiconv/include"
    fi
    libgit2_cmake_flags="$libgit2_cmake_flags -DUSE_ICONV=ON"
    gitstatus_ldlibs="$gitstatus_ldlibs -liconv"
    gitstatus_ldflags="$gitstatus_ldflags -L${workdir}/lib"
    libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=OFF"
  ;;
  msys*|mingw*)
    gitstatus_ldflags="$gitstatus_ldflags ${static_pie:--static}"
    libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=ON"
  ;;
  cygwin*)
    gitstatus_ldflags="$gitstatus_ldflags ${static_pie:--static}"
    libgit2_cmake_flags="$libgit2_cmake_flags -DENABLE_REPRODUCIBLE_BUILDS=ON"
  ;;
  *)
    >&2 echo "[internal error] unhandled kernel: $gitstatus_kernel"
    exit 1
  ;;
esac

for cmd in cat cmake git ld ln mkdir rm strip tar "$gitstatus_make"; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    if [ -n "$gitstatus_install_tools" ]; then
      >&2 echo "[internal error] $cmd not found"
      exit 1
    else
      >&2 echo "[error] command not found: $cmd"
      exit 1
    fi
  fi
done

. "$outdir"/build.info
if [ -z "${libgit2_version:-}" ]; then
  >&2 echo "[internal error] libgit2_version not set"
  exit 1
fi
if [ -z "${libgit2_sha256:-}" ]; then
  >&2 echo "[internal error] libgit2_sha256 not set"
  exit 1
fi
libgit2_tarball="$outdir"/deps/libgit2-"$libgit2_version".tar.gz
if [ ! -e "$libgit2_tarball" ]; then
  if [ -n "$gitstatus_download_deps" ]; then
    if ! command -v wget >/dev/null 2>&1; then
      if [ -n "$gitstatus_install_tools" ]; then
        >&2 echo "[internal error] wget not found"
        exit 1
      else
        >&2 echo "[error] command not found: wget"
        exit 1
      fi
    fi
    libgit2_url=https://github.com/romkatv/libgit2/archive/"$libgit2_version".tar.gz
    if ! >"$libgit2_tmp" command wget --no-config -qO- -- "$libgit2_url" &&
       ! >"$libgit2_tmp" command wget             -qO- -- "$libgit2_url"; then
      set -x
      >&2 command which wget
      >&2 command ls -lAd -- "$(command which wget)"
      >&2 command ls -lAd -- "$outdir"
      >&2 command ls -lA -- "$outdir"
      >&2 command ls -lAd -- "$outdir"/deps
      >&2 command ls -lA -- "$outdir"/deps
      set +x
      exit 1
    fi
    command mv -f -- "$libgit2_tmp" "$libgit2_tarball"
  else
    >&2 echo "[error] file not found: deps/libgit2-"$libgit2_version".tar.gz"
    exit 1
  fi
fi

libgit2_actual_sha256=
if command -v shasum >/dev/null 2>/dev/null; then
  libgit2_actual_sha256="$(command shasum -b -a 256 -- "$libgit2_tarball")"
  libgit2_actual_sha256="${libgit2_actual_sha256%% *}"
elif command -v sha256sum >/dev/null 2>/dev/null; then
  libgit2_actual_sha256="$(command sha256sum -b -- "$libgit2_tarball")"
  libgit2_actual_sha256="${libgit2_actual_sha256%% *}"
elif command -v sha256 >/dev/null 2>/dev/null; then
  libgit2_actual_sha256="$(command sha256 -- "$libgit2_tarball" </dev/null)"
  # Ignore sha256 output if it's from hashalot. It's incompatible.
  if [ ${#libgit2_actual_sha256} -lt 64 ]; then
    libgit2_actual_sha256=
  else
    libgit2_actual_sha256="${libgit2_actual_sha256##* }"
  fi
fi

if [ -z "$libgit2_actual_sha256" ]; then
  >&2 echo "[error] command not found: shasum or sha256sum"
  exit 1
fi

if [ "$libgit2_actual_sha256" != "$libgit2_sha256" ]; then
  >&2 echo "[error] sha256 mismatch"
  >&2 echo ""
  >&2 echo "  file    : deps/libgit2-$libgit2_version.tar.gz"
  >&2 echo "  expected: $libgit2_sha256"
  >&2 echo "  actual  : $libgit2_actual_sha256"
  exit 1
fi

cd -- "$workdir"
command tar -xzf "$libgit2_tarball"
command mv -- libgit2-"$libgit2_version" libgit2
command mkdir libgit2/build
cd libgit2/build

CFLAGS="$libgit2_cflags" command cmake \
  -DCMAKE_BUILD_TYPE=None              \
  -DZERO_NSEC=ON                       \
  -DTHREADSAFE=ON                      \
  -DUSE_BUNDLED_ZLIB=ON                \
  -DREGEX_BACKEND=builtin              \
  -DUSE_HTTP_PARSER=builtin            \
  -DUSE_SSH=OFF                        \
  -DUSE_HTTPS=OFF                      \
  -DBUILD_CLAR=OFF                     \
  -DUSE_GSSAPI=OFF                     \
  -DUSE_NTLMCLIENT=OFF                 \
  -DBUILD_SHARED_LIBS=OFF              \
  $libgit2_cmake_flags                 \
  ..
command make -j "$cpus" VERBOSE=1

APPNAME="$appname".tmp           \
  OBJDIR="$workdir"/gitstatus    \
  CXX="${CXX:-$gitstatus_cxx}"   \
  CXXFLAGS="$gitstatus_cxxflags" \
  LDFLAGS="$gitstatus_ldflags"   \
  LDLIBS="$gitstatus_ldlibs"     \
  command "$gitstatus_make" -C "$outdir" -j "$cpus"

app="$outdir"/usrbin/"$appname"

command strip "$app".tmp

command mkdir -- "$workdir"/repo
printf '[init]\n  defaultBranch = master\n' >"$workdir"/.gitconfig
(
  cd -- "$workdir"/repo
  GIT_CONFIG_NOSYSTEM=1 HOME="$workdir" command git init
  GIT_CONFIG_NOSYSTEM=1 HOME="$workdir" command git config user.name "Your Name"
  GIT_CONFIG_NOSYSTEM=1 HOME="$workdir" command git config user.email "you@example.com"
  GIT_CONFIG_NOSYSTEM=1 HOME="$workdir" command git commit \
    --allow-empty --allow-empty-message --no-gpg-sign -m ''
)

resp="$(printf "hello\037$workdir/repo\036" | "$app".tmp)"
case "$resp" in
  hello*1*/repo*master*);;
  *)
    >&2 echo 'error: invalid gitstatusd response for a git repo'
    exit 1
  ;;
esac

resp="$(printf 'hello\037\036' | "$app".tmp)"
case "$resp" in
  hello*0*);;
  *)
    >&2 echo 'error: invalid gitstatusd response for a non-repo'
    exit 1
  ;;
esac

command mv -f -- "$app".tmp "$app"

cleanup

command cat >&2 <<-END
	-------------------------------------------------
	SUCCESS: created usrbin/$appname
	END
END
)"

docker_image=
docker_cmd=

gitstatus_arch=
gitstatus_cpu=
gitstatus_install_tools=
gitstatus_download_deps=

while getopts ':m:c:i:d:swh' opt "$@"; do
  case "$opt" in
    h)
      printf '%s\n' "$usage"
      exit
    ;;
    m)
      if [ -n "$gitstatus_arch" ]; then
        >&2 echo "[error] duplicate option: -$opt"
        exit 1
      fi
      if [ -z "$OPTARG" ]; then
        >&2 echo "[error] incorrect value of -$opt: $OPTARG"
        exit 1
      fi
      gitstatus_arch="$OPTARG"
    ;;
    c)
      if [ -n "$gitstatus_cpu" ]; then
        >&2 echo "[error] duplicate option: -$opt"
        exit 1
      fi
      if [ -z "$OPTARG" ]; then
        >&2 echo "[error] incorrect value of -$opt: $OPTARG"
        exit 1
      fi
      gitstatus_cpu="$OPTARG"
    ;;
    i)
      if [ -n "$docker_image" ]; then
        >&2 echo "[error] duplicate option: -$opt"
        exit 1
      fi
      if [ -z "$OPTARG" ]; then
        >&2 echo "[error] incorrect value of -$opt: $OPTARG"
        exit 1
      fi
      docker_image="$OPTARG"
    ;;
    d)
      if [ -n "$docker_cmd" ]; then
        >&2 echo "[error] duplicate option: -$opt"
        exit 1
      fi
      if [ -z "$OPTARG" ]; then
        >&2 echo "[error] incorrect value of -$opt: $OPTARG"
        exit 1
      fi
      docker_cmd="$OPTARG"
    ;;
    s)
      if [ -n "$gitstatus_install_tools" ]; then
        >&2 echo "[error] duplicate option: -$opt"
        exit 1
      fi
      gitstatus_install_tools=1
    ;;
    w)
      if [ -n "$gitstatus_download_deps" ]; then
        >&2 echo "[error] duplicate option: -$opt"
        exit 1
      fi
      gitstatus_download_deps=1
    ;;
    \?) >&2 echo "[error] invalid option: -$OPTARG"           ; exit 1;;
    :)  >&2 echo "[error] missing required argument: -$OPTARG"; exit 1;;
    *)  >&2 echo "[internal error] unhandled option: -$opt"   ; exit 1;;
  esac
done

if [ "$OPTIND" -le $# ]; then
  >&2 echo "[error] unexpected positional argument"
  exit 1
fi

if [ -n "$docker_image" -a -z "$docker_cmd" ]; then
  >&2 echo "[error] cannot use -i without -d"
  exit 1
fi

if [ -z "$gitstatus_arch" ]; then
  gitstatus_arch="$(uname -m)"
  gitstatus_arch="$(printf '%s' "$gitstatus_arch" | tr '[A-Z]' '[a-z]')"
fi

if [ -z "$gitstatus_cpu" ]; then
  case "$gitstatus_arch" in
    armel)          gitstatus_cpu=armv5;;
    armv6l|armhf)   gitstatus_cpu=armv6;;
    armv7l)         gitstatus_cpu=armv7;;
    arm64|aarch64)  gitstatus_cpu=armv8-a;;
    ppc64le)        gitstatus_cpu=powerpc64le;;
    riscv64)        gitstatus_cpu=rv64imafdc;;
    x86_64|amd64)   gitstatus_cpu=x86-64;;
    x86)            gitstatus_cpu=i586;;
    s390x)          gitstatus_cpu=z900;;
    i386|i586|i686) gitstatus_cpu="$gitstatus_arch";;
    *)
      >&2 echo '[error] unable to infer target CPU architecture'
      >&2 echo 'Please specify explicitly with `-c CPU`.'
      exit 1
    ;;
  esac
fi

gitstatus_kernel="$(uname -s)"
gitstatus_kernel="$(printf '%s' "$gitstatus_kernel" | tr '[A-Z]' '[a-z]')"

case "$gitstatus_kernel" in
  linux)
    if [ -n "$docker_cmd" ]; then
      if [ -z "${docker_cmd##*/*}" ]; then
        if [ ! -x "$docker_cmd" ]; then
          >&2 echo "[error] not an executable file: $docker_cmd"
          exit 1
        fi
      else
        if ! command -v "$docker_cmd" >/dev/null 2>&1; then
          >&2 echo "[error] command not found: $docker_cmd"
          exit 1
        fi
      fi
      if [ -z "$docker_image" ]; then
        case "$gitstatus_arch" in
          x86_64)             docker_image=alpine:3.11.6;;
          x86|i386|i586|i686) docker_image=i386/alpine:3.11.6;;
          armv6l|armhf)       docker_image=arm32v6/alpine:3.11.6;;
          armv7l)             docker_image=arm32v7/alpine:3.11.6;;
          aarch64)            docker_image=arm64v8/alpine:3.11.6;;
          ppc64le)            docker_image=ppc64le/alpine:3.11.6;;
          s390x)              docker_image=s390x/alpine:3.11.6;;
          *)
            >&2 echo '[error] unable to infer docker image'
            >&2 echo 'Please specify explicitly with `-i IMAGE`.'
            exit 1
          ;;
        esac
      fi
    fi
  ;;
  freebsd|openbsd|netbsd|darwin)
    if [ -n "$docker_cmd" ]; then
      >&2 echo "[error] docker (-d) is not supported on $gitstatus_kernel"
      exit 1
    fi
  ;;
  msys_nt-*|mingw32_nt-*|mingw64_nt-*|cygwin_nt-*)
    if ! printf '%s' "$gitstatus_kernel" | grep -Eqx '[^-]+-[0-9]+\.[0-9]+(-.*)?'; then
      >&2 echo '[error] unsupported kernel, sorry!'
      exit 1
    fi
    gitstatus_kernel="$(printf '%s' "$gitstatus_kernel" | sed 's/^\([^-]*-[0-9]*\.[0-9]*\).*/\1/')"
    if [ -n "$docker_cmd" ]; then
      >&2 echo '[error] docker (-d) is not supported on windows'
      exit 1
    fi
    if [ -n "$gitstatus_install_tools" -a -z "${gitstatus_kernel##cygwin_nt-*}" ]; then
      >&2 echo '[error] -s is not supported on cygwin'
      exit 1
    fi
  ;;
  *)
    >&2 echo '[error] unsupported kernel, sorry!'
    exit 1
  ;;
esac

dir="$(dirname -- "$0")"
cd -- "$dir"
dir="$(pwd)"

>&2 echo "Building gitstatusd..."
>&2 echo ""
>&2 echo "  kernel := $gitstatus_kernel"
>&2 echo "  arch := $gitstatus_arch"
>&2 echo "  cpu := $gitstatus_cpu"
[ -z "$docker_cmd" ] || >&2 echo "  docker command := $docker_cmd"
[ -z "$docker_image"  ] || >&2 echo "  docker image := $docker_image"
if [ -n "$gitstatus_install_tools" ]; then
  >&2 echo "  install tools := yes"
else
  >&2 echo "  install tools := no"
fi
if [ -n "$gitstatus_download_deps" ]; then
  >&2 echo "  download deps := yes"
else
  >&2 echo "  download deps := no"
fi

if [ -n "$docker_cmd" ]; then
  "$docker_cmd" run                                       \
    -e docker_cmd="$docker_cmd"                           \
    -e docker_image="$docker_image"                       \
    -e gitstatus_kernel="$gitstatus_kernel"               \
    -e gitstatus_arch="$gitstatus_arch"                   \
    -e gitstatus_cpu="$gitstatus_cpu"                     \
    -e gitstatus_install_tools="$gitstatus_install_tools" \
    -e gitstatus_download_deps="$gitstatus_download_deps" \
    -v "$dir":/out                                        \
    -w /out                                               \
    --rm                                                  \
    -- "$docker_image" /bin/sh -uexc "$build"
else
  eval "$build"
fi