#!/bin/sh
#
# POSIX shell script to detect system properties required by Oils.  Distributed
# with the source tarball.
#
# For usage, run:
#
#   ./configure --help
#
# External utilities used: cc
# Optional dependency: GNU readline
#
# TODO:
# - Should be able to run this from another directory.
# - Other settings: LTO, PGO?  Consider moving prefix, LTO, PGO to build and
#   install steps.

TMP="$(mktemp -d || echo "${TMPDIR:-/tmp}")"  # note: mktemp consults $TMPDIR first too
readonly TMP  # POSIX sh supports 'readonly'

log() {
  echo "$0: $@" 1>&2
}

info() {
  echo "$0 INFO: $@" 1>&2
}

die() {
  echo "$0 ERROR: $@" 1>&2
  exit 1
}

show_help() {
  cat <<'EOF'
Detect system features before a build of oils-for-unix.

Usage:
  ./configure FLAG*
  ./configure --help

Flags:

  --cxx-for-configure CXX [default 'cc']
    Use this compiler to detect system features

Installation dirs:

  --prefix PREFIX [default '/usr/local']
    Prefix for the bin/ directory 

  --datarootdir DATAROOTDIR [default $PREFIX/share]
    Prefix for data files, including man page 

Dependencies:

  --with-readline          Fail unless readline is available
  --without-readline       Don't compile with readline, even if it's available.
                           The shell won't have any interactive features.
  --readline DIR           An alternative readline installation to link against
  --without-libc-features  Assume no FNM_EXTMATCH, GLOB_PERIOD, *pwent()
  --with-systemtap-sdt     Fail unless systemtap-sdt is available
  --without-systemtap-sdt  Don't compile with systemtap-sdt, even if it's available

EOF
}

# This script roughly follows the GNU Standards
# https://www.gnu.org/prep/standards/html_node/Configuration.html
# https://www.gnu.org/prep/standards/html_node/Directory-Variables.html
#
# Default installation is /usr/local/bin/oils-for-unix, but this can be changed with
# --prefix.
#
# While this script only uses a handful of the standard directory variables
# listed on the above documents, it accepts most of them in the --arg=value
# form as noops. This helps automated build-systems passing preconfigured
# sets of arguments to configure oils.

init_flags() {
  ### Used in tests

  # Quirk: we use a C compiler by default, which seems consistent.  We are not
  # detecting anything about C++.
  # This is because OLD oils-ref tarball only depends on C.  In contrast,
  # oils-for-unix depends on C++.
  FLAG_cxx_for_configure=cc

  FLAG_prefix='/usr/local'
  FLAG_datarootdir=''  # default initialized after processing flags
  FLAG_with_readline=''  # Fail if it's not available.
  FLAG_without_readline=''  # Don't even check if it's available
  FLAG_readline=''
  FLAG_without_systemtap_sdt=''  # Don't even check if it's available
  FLAG_without_libc_features=''
}

init_flags  # Set GLOBALS


# These variables are set by detect_readline and used by echo_cpp and
# echo_shell_vars
detected_deps=''

have_readline=''
readline_dir=''

have_systemtap_sdt=''

# libc
have_fnm_extmatch=''
have_glob_period=''
have_pwent=''

parse_flags() {
  while true; do
    case "$1" in
      '')
        break
        ;;
      --help)
        show_help
        exit 0
        ;;

      --cxx-for-configure=*)
        FLAG_cxx_for_configure="${1#*=}"
        ;;
      --cxx-for-configure)
        if test $# -eq 1; then
          die "--cxx-for-configure requires an argument"
        fi
        shift
        FLAG_cxx_for_configure=$1
        ;;

      --with-readline)
        FLAG_with_readline=1
        ;;

      --without-readline)
        FLAG_without_readline=1
        ;;

      --readline=*)
        FLAG_readline="${1#*=}"
        ;;
      --readline)
        if test $# -eq 1; then
          die "--readline requires an argument"
        fi
        shift
        FLAG_readline=$1
        ;;

      --without-systemtap-sdt)
        FLAG_without_systemtap_sdt=1
        ;;

      --without-libc-features)
        FLAG_without_libc_features=1
        ;;

      # TODO: Maybe prefix only needs to be part of the install step?  I'm not
      # sure if we need it for building anything.
      --prefix=*)
        FLAG_prefix="${1#*=}"
        ;;
      --prefix)
        if test $# -eq 1; then
          die "--prefix requires an argument"
        fi
        shift
        FLAG_prefix=$1
        ;;

      # Following autoconf's spelling of --mandir
      --datarootdir=*)
        FLAG_datarootdir="${1#*=}"
        ;;
      --datarootdir)
        if test $# -eq 1; then
          die "--datarootdir requires an argument"
        fi
        shift
        FLAG_datarootdir=$1
        ;;

      --with-*|--enable-*)
        info "Argument '$1' not used by this configure script"
        ;;

      --build=*|--host=*)
        info "Argument '$1' not used by this configure script"
        ;;

      --exec-prefix=*|--bindir=*|--sbindir=*|--libexecdir=*|--sysconfdir=*)
        info "Argument '$1' not used by this configure script"
        ;;
      --sharedstatedir=*|--localstatedir=*|--runstatedir=*)
        info "Argument '$1' not used by this configure script"
        ;;
      --libdir=*|--includedir=*|--oldincludedir=*)
        info "Argument '$1' not used by this configure script"
        ;;
      --datadir=*|--infodir=*|--localedir=*|--mandir=*|--docdir=*)
        info "Argument '$1' not used by this configure script"
        ;;
      --htmldir=*|--dvidir=*|--pdfdir=*|--psdir=*)
        info "Argument '$1' not used by this configure script"
        ;;

      *)
        die "Invalid argument '$1'"
        ;;
    esac
    shift
  done

  # If not set, fallback to --prefix
  FLAG_datarootdir=${FLAG_datarootdir:-$FLAG_prefix/share}
}

# No output file, no logging, no stderr.
# TODO: Maybe send stdout/stderr to config.log?
cc_quiet() {
  "$FLAG_cxx_for_configure" "$@" -o /dev/null >/dev/null 2>&1
}

cc_or_die() {
  # Used to detect
  local log_path=$TMP/cc_or_die.log 
  if ! "$FLAG_cxx_for_configure" "$@" >"$log_path" 2>&1; then
    log "Error running 'cc $@':"
    cat "$log_path"
    die "Fatal compile error running feature test"
  fi
}

# Check if a given program compiles
cc_statement() {
  local pp_var="$1"
  local prog="$2"
  local includes="$3"

  cat >$TMP/cc_statement.c <<EOF
$includes
int main() {
  $prog
}
EOF
  # Return exit code of compiler
  if cc_quiet $TMP/cc_statement.c; then
    echo "#define $pp_var 1"
    return 0
  else
    return 1
  fi
}

# Check if a given library is installed via compilation
cc_header_file() {
  local pp_var="$1"
  local c_lib="$2"

  cc_statement "$pp_var" 'return 0;' "#include <$c_lib>"
}

detect_readline() {
  detected_deps=1  # for assertions in echo_shell_vars and echo_cpp

  # User disabled readline
  if test -n "$FLAG_without_readline"; then
    # have_readline remains false
    return
  fi

  # User requested specific location
  if test -n "$FLAG_readline"; then
    if cc_quiet build/detect-readline.c \
      -L "$FLAG_readline/lib" \
      -I "$FLAG_readline/include" \
      -l readline; then

      readline_dir="$FLAG_readline"
      have_readline=1
    fi
    return
  fi

  # Detect in default location
  if cc_quiet build/detect-readline.c -l readline; then
    have_readline=1
    return
  fi

  # User requested that it be found
  if test "$FLAG_with_readline" = 1 && test "$have_readline" != 1; then
    die 'readline was not detected on the system (--with-readline passed).'
  fi
}

detect_systemtap_sdt() {
  detected_deps=1  # for assertions in echo_shell_vars and echo_cpp

  if test -n "$FLAG_without_systemtap_sdt"; then
    return
  fi

  if cc_quiet build/detect-systemtap-sdt.c; then
    have_systemtap_sdt=1
    return
  fi
}

detect_libc() {
  if test -n "$FLAG_without_libc_features"; then
    return
  fi

  # Check if non-POSIX FNM_EXTMATCH is available
  if cc_quiet build/detect-fnm-extmatch.c; then
    have_fnm_extmatch=1
  fi

  # Check if non-POSIX GLOB_PERIOD is available
  if cc_quiet build/detect-glob-period.c; then
    have_glob_period=1
  fi

  # Check if pwent is callable. E.g. bionic libc (Android) doesn't have it
  if cc_quiet build/detect-pwent.c; then
    have_pwent=1
  fi
}

echo_shell_vars() {
  if test "$detected_deps" != 1; then
    die 'called echo_shell_vars before detecting readline.'
  fi

  # Present a consistent interface to build/ninja-rules-cpp.sh
  if test "$have_readline" = 1; then
    echo 'HAVE_READLINE=1'
    echo "READLINE_DIR=$readline_dir"
  else
    echo 'HAVE_READLINE='
    echo 'READLINE_DIR='
  fi
  echo

  echo "PREFIX=$FLAG_prefix"
  echo "DATAROOTDIR=$FLAG_datarootdir"
  echo

  if cc_quiet build/detect-cc.c -Wl,--gc-sections; then
    echo 'STRIP_FLAGS=--gc-sections'
  elif cc_quiet build/detect-cc.c -Wl,-dead_strip; then
    echo 'STRIP_FLAGS=-dead_strip'
  fi
}

# c.m4 AC_LANG_INT_SAVE
cc_print_expr() {
  local c_expr="$1"
  cat >$TMP/print_expr.c <<EOF
#include <stdio.h>
#include <sys/types.h>  /* size_t, pid_t */

int main() {
  printf("%lu", $c_expr);
}
EOF
  cc_or_die -o $TMP/print_expr $TMP/print_expr.c
  $TMP/print_expr > $TMP/print_expr.out
}

# Shell note:
# - local is not POSIX, but most shells have it.
# C note:
# - autoconf uses ac_fn_compute_int (in sh) aka AC_COMPUTE_INT (in m4).
#   - it uses different tests when cross compiling.
#   - cross-compiling does binary search?
#   - other one does AC_LANG_INT_SAVE
#     - generates a C program that outputs to conftest.val!
#     - well why not use exit code?
# - QEMU configure doesn't do any tests

# Hm, don't bother with cross compiling case for now.

# Check if the size of a type is greater than a certain integer.
check_sizeof() {
  local pp_var="$1"
  local c_type="$2"
  local min_bytes="$3"

  cc_print_expr "sizeof($c_type)"

  local actual_bytes
  actual_bytes=$(cat $TMP/print_expr.out)

  if test -n "$min_bytes" && test "$actual_bytes" -lt "$min_bytes"; then
    die "sizeof($c_type) should be at least $min_bytes; got $actual_bytes"
  fi

  # Echo to stdout!
  echo "#define $pp_var $actual_bytes"
}

echo_libc() {
  # Exported by pyext/libc.c
  if test "$have_fnm_extmatch" = 1; then
    echo '#define HAVE_FNM_EXTMATCH 1'
  else
    echo '#define HAVE_FNM_EXTMATCH 0'
  fi

  if test "$have_glob_period" = 1; then
    echo '#define HAVE_GLOB_PERIOD 1'
  else
    echo '#define HAVE_GLOB_PERIOD 0'
    echo '#define GLOB_PERIOD 0  /* define bit flag to have no effect */'
  fi

  # Used by cpp/core.cc
  if test "$have_pwent" = 1; then
    echo '#define HAVE_PWENT 1'
  else
    echo '/* #undef HAVE_PWENT */'
  fi
}

# Note: this is only for the OLD oils-ref tarball
detect_c_language() {
  echo_libc
  echo

  # This is the equivalent of AC_CHECK_SIZEOF(int, 4)
  check_sizeof SIZEOF_INT 'int' 4
  check_sizeof SIZEOF_LONG 'long' 4
  check_sizeof SIZEOF_VOID_P 'void *' 4
  check_sizeof SIZEOF_SHORT 'short' 2
  check_sizeof SIZEOF_FLOAT 'float' 4
  check_sizeof SIZEOF_DOUBLE 'double' 8

  check_sizeof SIZEOF_SIZE_T 'size_t' 4

  # NOTE: This might only be relevant for large file support, which we don't
  # have.
  check_sizeof SIZEOF_FPOS_T 'fpos_t' 4
  check_sizeof SIZEOF_PID_T 'pid_t' 4

  check_sizeof SIZEOF_OFF_T 'off_t' ''
  # autoconf checks if we have time.h, but the check isn't used.  We just
  # assume it's there.
  check_sizeof SIZEOF_TIME_T 'time_t' ''

  if cc_statement HAVE_LONG_LONG 'long long x; x = (long long)0;'
  then
    check_sizeof SIZEOF_LONG_LONG 'long long' 8
  fi
  if cc_statement HAVE_LONG_DOUBLE 'long double x; x = (long double)0;'
  then
    check_sizeof SIZEOF_LONG_DOUBLE 'long double' 8
  fi

  if cc_statement HAVE_C99_BOOL '_Bool x; x = (_Bool)0;'
  then
    # NOTE: this is mainly used in ctypes.h, which we might not need.
    check_sizeof SIZEOF__BOOL '_Bool' 1
  fi
  # NOTE: Python also has a check for C99 uintptr_t.  Just assume we don't
  # have it?

  #if cc_statement HAVE_C99_BOOL 'wchar_t x; x = (wchar_t)0;'
  #then
  #  check_sizeof SIZEOF_WCHAR_T 'wchar_t' 4
  #fi

  # TODO: Detect header and size.
  echo '#define HAVE_WCHAR_H 1'
  echo '#define SIZEOF_WCHAR_T 4'

  cat >$TMP/detect_va_list.c <<EOF
#include <stdarg.h>  /* C89 */
int main() {
  va_list list1, list2;
  list1 = list2;
}
EOF
  if cc_quiet $TMP/detect_va_list.c; then
    echo ''  # not an array
  else
    echo '#define VA_LIST_IS_ARRAY 1'
  fi

  # TODO: are these feature checks really necessary, or can we
  # strip these out of posixmodule.c entirely?
  cc_header_file HAVE_PTY_H 'pty.h'
  cc_header_file HAVE_LIBUTIL_H 'libutil.h'
  cc_header_file HAVE_UTIL_H 'util.h'

  # TODO: are these feature checks really necessary?
  cc_statement HAVE_STAT_TV_NSEC \
    'struct stat st; st.st_mtim.tv_nsec = 1; return 0;' \
    '#include <sys/stat.h>'
  cc_statement HAVE_STAT_TV_NSEC2 \
    'struct stat st; st.st_mtimespec.tv_nsec = 1; return 0;' \
    '#include <sys/stat.h>'
}

echo_cpp() {
  if test "$detected_deps" != 1; then
    die 'called echo_cpp before detecting readline.'
  fi
  # Dev builds can use non-portable clock_gettime()
  if test -n "$_OIL_DEV"; then
    echo '#define GC_TIMING 1'
    log 'Turned on -D GC_TIMING because $_OIL_DEV is set'
  fi

  # Important: HAVE_READLINE is only a SHELL variable, not a preprocessor
  # variable.  This is so _build/oils.sh --without-readline works, not just
  # ./configure --without-readline.

  # Used by mycpp/probes.h
  if test "$have_systemtap_sdt" = 1; then
    echo '#define HAVE_SYSTEMTAP_SDT 1'
  else
    echo '/* #undef HAVE_SYSTEMTAP_SDT */'
  fi

  echo

  # Shared with detect_c_language
  echo_libc
}

# Another way of working: set detected-config.mk ?
# And set the default target as oil_readline, oil_no_readline, oil_lto,
# oil_pgo, etc.?
main() {
  parse_flags "$@"  # sets FLAG_*

  mkdir -p _build

  if ! cc_quiet build/detect-cc.c; then
    die "Couldn't compile a basic C program (cc not installed?)"
  fi

  # Sets globals $have_readline and $readline_dir
  detect_readline

  detect_libc

  detect_systemtap_sdt

  # Generate configuration for oils-for-unix
  local cpp_out=_build/detected-cpp-config.h
  echo_cpp > $cpp_out
  log "Wrote $cpp_out"

  # Legacy OVM build: shell build actions will 'source
  # _build/detected-config.sh'.  And then adjust flags to compiler (-D, -l,
  # etc.)
  local sh_out=_build/detected-config.sh

  echo_shell_vars > $sh_out
  log "Wrote $sh_out"

  local c_out=_build/detected-config.h

  # Fast mode
  if test -n "$_OIL_DEV"; then
    # Do only this subset
    echo_libc > $c_out
    log "Wrote $c_out"
    return
  fi

  detect_c_language > $c_out
  log "Wrote $c_out"
}

if test -z "$_OIL_CONFIGURE_TEST"; then
  main "$@"
fi
