#!/bin/sh
# shellcheck disable=SC2030,SC2031

set -e

atexit() {
	local _err="$?"

	# Dump contents of generated files to config.log.
	exec 1>>config.log 2>&1
	set -x
	[ -e config.h ] && cat config.h
	[ -e config.mk ] && cat config.mk
	rm -rf "$@" || :
	[ "${_err}" -ne 0 ] && fatal
	exit 0
}

compile() {
	# shellcheck disable=SC2086
	"${CC}" ${CPPFLAGS} -Werror -o /dev/null -x c - "$@"
}

cc_has_option() {
	if echo 'int main(void) { return 0; }' | compile "$@"; then
		echo "$@"
	fi
}

cc_has_sanitizer() {
	local _sanitizer

	_sanitizer="$1"; shift; : "${_sanitizer:?}"
	if echo 'int main(void) { return 0; }' |
	   compile "-fsanitize=${_sanitizer}" "$@"
	then
		echo "${_sanitizer}"
	fi
}

fatal() {
	[ $# -gt 0 ] && echo "fatal: ${*}"
	exec 1>&3 2>&4
	cat config.log
	exit 1
}

headers() {
	local _tmp="${WRKDIR}/headers"

	cat >"${_tmp}"
	[ -s "${_tmp}" ] || return 0

	xargs printf '#include <%s>\n' <"${_tmp}"
}

is_openbsd() {
	case "$(uname -s)" in
	OpenBSD)	return 0;;
	*)		return 1;;
	esac
}

make_has_object_dir() (
	cd "${WRKDIR}"
	# shellcheck disable=SC2016
	printf 'all:\n\t[ ${PWD} = ${.OBJDIR} ]\n' >Makefile
	printf 'all:\n\tfalse\n' >GNUMakefile
	mkdir -p obj
	make >/dev/null 1>&2
)

make_variable() {
	# shellcheck disable=SC2016
	var="$(printf 'all:\n\t@echo ${%s}\n' "$1" | make -sf -)"
	if [ -n "${var}" ]; then
		echo "${var}"
	else
		return 1
	fi
}

# rmdup arg ...
#
# Remove duplicates from the given arguments while preserving the order.
rmdup() {
	echo "$@" |
	xargs printf '%s\n' |
	awk '!x[$1]++' |
	xargs
}

# check_gnu99
#
# Check if the compiler implicitly supports the GNU99 C standard.
check_gnu99() {
	compile <<-EOF
	int main(void) {
		int n = __extension__ ({ int _n = 0; _n + 1; });
		for (int i = 0; i < n; i++)
			return 0;
		return 1;
	}
	EOF
}

# check_gnu_source path
#
# Check if the given header rooted in /usr/include depends on _GNU_SOURCE.
check_gnu_source() {
	local _header
	local _path
	local _tmp="${WRKDIR}/gnu"

	_header="$1"; : "${_header:?}"
	_path="/usr/include/${_header}"
	# shellcheck disable=SC2086
	"${CC}" ${CPPFLAGS} -E "${_path}" >"${_tmp}"
	# shellcheck disable=SC2086
	! "${CC}" ${CPPFLAGS} -E -D_GNU_SOURCE "${_path}" | cmp -s "${_tmp}" -
}

check_machine() {
	compile <<-EOF
	#include <sys/param.h>

	int main(void) {
		static const char m[] = MACHINE;
		return 0;
	}
	EOF
}

check_machine_arch() {
	compile <<-EOF
	#include <sys/param.h>

	int main(void) {
		static const char m[] = MACHINE_ARCH;
		return 0;
	}
	EOF
}

check_pledge() {
	compile <<-EOF
	#include <unistd.h>

	int main(void) {
		return !(pledge("stdio", NULL) == 0);
	}
	EOF
}

check_strtonum() {
	compile <<-EOF
	#include <stdlib.h>

	int main(void) {
		return !(strtonum("1", 1, 2, NULL) != 0);
	}
	EOF
}

check_unveil() {
	compile <<-EOF
	#include <unistd.h>

	int main(void) {
		return !(unveil("/", "r") == 0);
	}
	EOF
}

_fuzz=''
_pedantic=0
_sanitize=''

while [ $# -gt 0 ]; do
	case "$1" in
	--fuzz)		shift; _fuzz="$1"; _sanitize="address";;
	--pedantic)	_pedantic=1;;
	--sanitize)	shift; _sanitize="${1}";;
	*)		;;
	esac
	shift
done

WRKDIR=$(mktemp -dt configure.XXXXXX)
trap 'atexit ${WRKDIR}' EXIT

exec 3>&1 4>&2
exec 1>config.log 2>&1

# At this point, all variables used must be defined.
set -u
# Enable tracing, will end up in config.log.
set -x

HAVE_GNU_SOURCE=0
HAVE_MACHINE=0
HAVE_MACHINE_ARCH=0
HAVE_PLEDGE=0
HAVE_STRTONUM=0
HAVE_UNVEIL=0

# Order is important, must come first if not defined.
DEBUG="$(make_variable DEBUG || :)"

CC=$(make_variable CC || fatal "CC: not defined")
CFLAGS="$(unset CFLAGS DEBUG; make_variable CFLAGS || :) ${CFLAGS:-} ${DEBUG}"
CFLAGS="${CFLAGS} -MD -MP"
CPPFLAGS="$(make_variable CPPFLAGS || :)"
LDFLAGS="$(unset DEBUG; make_variable LDFLAGS || :)"
PREFIX="$(make_variable PREFIX || echo /usr/local)"
BINDIR="$(make_variable BINDIR || echo "${PREFIX}/bin")"
SBINDIR="$(make_variable SBINDIR || echo "${PREFIX}/sbin")"
MANDIR="$(make_variable MANDIR || echo "${PREFIX}/man")"
LIBEXECDIR="${PREFIX}/libexec"
INSTALL="$(make_variable INSTALL || echo install)"
INSTALL_MAN="$(make_variable INSTALL_MAN || echo "${INSTALL}")"

# Following chunks must happen after CC is defined.

if ! check_gnu99; then
	CFLAGS="-std=gnu99 ${CFLAGS}"
fi

if [ "${_pedantic}" -eq 1 ]; then
	while read -r _o; do
		CFLAGS="${CFLAGS} ${_o}"
	done <<-CC-PEDANTIC
	-g
	-O2
	-Wall
	-Werror
	-Wextra
	-Wcast-qual
	-Wconversion
	-Wmissing-prototypes
	-Wpedantic
	-Wshadow
	-Wsign-conversion
	-Wwrite-strings
	CC-PEDANTIC

	while read -r _o; do
		CFLAGS="${CFLAGS} $(cc_has_option "${_o}")"
	done <<-CC-PEDANTIC-OPTIONAL
	-Waddress-of-packed-member
	-Wcovered-switch-default
	-Wextra-semi-stmt
	-Wformat-signedness
	-Wimplicit-fallthrough
	-Wmissing-format-attribute
	-Wmissing-variable-declarations
	-Wtautological-unsigned-enum-zero-compare
	-Wunreachable-code-aggressive
	-Wused-but-marked-unused
	-Wno-gnu-zero-variadic-macro-arguments
	CC-PEDANTIC-OPTIONAL

	while read -r _o; do
		LDFLAGS="${LDFLAGS} $(cc_has_option "${_o}")"
	done <<-LD-PEDANTIC-OPTIONAL
	-Wl,--fatal-warnings
	LD-PEDANTIC-OPTIONAL

	DEBUG="-g ${DEBUG}"
fi

if [ -n "${_sanitize}" ]; then
	(
		if is_openbsd; then
			# Abuse CPPFLAGS honored by compile.
			CPPFLAGS="${CPPFLAGS} -fsanitize-minimal-runtime"
			CPPFLAGS="${CPPFLAGS} -Wl,--no-execute-only"
		fi
		cc_has_sanitizer "${_sanitize}"
		cc_has_sanitizer undefined
		cc_has_sanitizer unsigned-integer-overflow
		{ [ "${_fuzz}" = "llvm" ] && echo fuzzer; } || :
	) >"${WRKDIR}/sanitize"
	if ! cmp -s /dev/null "${WRKDIR}/sanitize"; then
		_sanitize="-fsanitize=$(paste -sd , "${WRKDIR}/sanitize")"
		_sanitize="${_sanitize} $(cc_has_option -fno-sanitize-recover=all)"
		_sanitize="${_sanitize} $(cc_has_option -fno-omit-frame-pointer)"
		if is_openbsd; then
			_sanitize="${_sanitize} -fsanitize-minimal-runtime"
			LDFLAGS="${LDFLAGS} -Wl,--no-execute-only"
		fi
		CFLAGS="${_sanitize} ${CFLAGS}"
		DEBUG="${_sanitize} ${DEBUG}"
	fi
fi

check_gnu_source time.h && HAVE_GNU_SOURCE=1
check_machine && HAVE_MACHINE=1
check_machine_arch && HAVE_MACHINE_ARCH=1
check_pledge && HAVE_PLEDGE=1
check_strtonum && HAVE_STRTONUM=1
check_unveil && HAVE_UNVEIL=1

if make_has_object_dir; then
	mkdir -p obj/{,libks}
fi

# Redirect stdout to config.h.
exec 1>config.h

printf '#ifndef CONFIG_H\n#define CONFIG_H\n'

# Order is important, must be present before any includes.
[ "${HAVE_GNU_SOURCE}" -eq 1 ] && printf '#define _GNU_SOURCE\n'

# Headers needed for function prototypes.
{
	:
} | sort | uniq | headers

[ "${HAVE_MACHINE}" -eq 1 ] || printf '#define MACHINE\t"%s"\n' "$(arch)"
[ "${HAVE_MACHINE_ARCH}" -eq 1 ] || printf '#define MACHINE_ARCH\t"%s"\n' "$(arch)"
[ "${HAVE_PLEDGE}" -eq 1 ] && printf '#define HAVE_PLEDGE\t1\n'
[ "${HAVE_STRTONUM}" -eq 1 ] && printf '#define HAVE_STRTONUM\t1\n'
[ "${HAVE_UNVEIL}" -eq 1 ] && printf '#define HAVE_UNVEIL\t1\n'

if [ -n "${_fuzz}" ]; then
	printf '#define FUZZER_%s\t1\n' \
		"$(echo "${_fuzz}" | tr '[:lower:]' '[:upper:]')"
fi

[ "${HAVE_PLEDGE}" -eq 0 ] &&
	printf 'int pledge(const char *, const char *);\n'
[ "${HAVE_STRTONUM}" -eq 0 ] && \
	printf 'long long strtonum(const char *, long long, long long, const char **);\n'
[ "${HAVE_UNVEIL}" -eq 0 ] &&
	printf 'int unveil(const char *, const char *);\n'

printf '#endif\n'

# Redirect stdout to config.mk.
exec 1>config.mk

# Use echo to normalize whitespace.
# shellcheck disable=SC1083,SC2086,SC2116
cat <<EOF
CC=			$(echo ${CC})
CFLAGS=			$(rmdup ${CFLAGS})
CPPFLAGS=		$(rmdup ${CPPFLAGS} -I\${.CURDIR})
DEBUG=			$(rmdup ${DEBUG})
LDFLAGS=		$(rmdup ${LDFLAGS})

BINDIR?=		$(echo ${BINDIR})
SBINDIR?=		$(echo ${SBINDIR})
MANDIR?=		$(echo ${MANDIR})
LIBEXECDIR?=		$(echo ${LIBEXECDIR})
INSTALL?=		$(echo ${INSTALL})
INSTALL_MAN?=		$(echo ${INSTALL_MAN})
EOF

if make_has_object_dir; then
	cat <<EOF

.SUFFIXES: .c .o
.c.o:
	\${CC} \${CPPFLAGS} \${CFLAGS} \${DEBUG} -c -o \${<:\${.CURDIR}/%.c=%.o} \$<
EOF
else
	cat <<EOF

.SUFFIXES: .c .o
.c.o:
	\${CC} \${CPPFLAGS} \${CFLAGS} \${DEBUG} -c -o \$@ \$<
EOF
fi
