#!/bin/sh

# Build portable binary release tarballs for Linux/x64 and Linux/arm64.

set -e
set -u

export PATH='/usr/local/bin:/usr/bin:/bin'

myself=${0##*/}

info()
{
	echo "$myself: $*"
}

error()
{
	echo >&2 "$myself: $*"
}

usage()
{
	echo >&2 "Usage: $myself [<target> ...]"
	exit 2
}

if ! [ -e 'rebar.config' ] || ! [ -e "tools/$myself" ]
then
	error "Please call this script from the repository's root directory."
	exit 2
fi

rel_name='eturnal'
rel_vsn=$(tools/get-version)
rebar_vsn='3.24.0'
crosstool_vsn='1.27.0'
zlib_vsn='1.3.1'
yaml_vsn='0.2.5'
ssl_vsn='3.5.0'
otp_vsn='27.3.3'
root_dir="${BUILD_DIR:-$HOME/build}"
bootstrap_dir="$root_dir/bootstrap"
ct_prefix_dir="$root_dir/x-tools"
build_dir="$root_dir/$rel_name"
crosstool_dir="crosstool-ng-$crosstool_vsn"
zlib_dir="zlib-$zlib_vsn"
yaml_dir="yaml-$yaml_vsn"
ssl_dir="openssl-$ssl_vsn"
otp_dir="otp_src_$otp_vsn"
crosstool_tar="$crosstool_dir.tar.xz"
zlib_tar="$zlib_dir.tar.gz"
yaml_tar="$yaml_dir.tar.gz"
ssl_tar="$ssl_dir.tar.gz"
otp_tar="$otp_dir.tar.gz"
rel_tar="$rel_name-$rel_vsn.tar.gz"
ct_jobs=$(nproc)
src_dir="$root_dir/src"
platform=$(gcc -dumpmachine | sed 's/^\(.*\)-\(.*\)-\(.*\)-\(.*\)$/\1-\3-\4/')
erl_compiler_options='[no_debug_info, deterministic]'
build_start=$(date '+%F %T')
have_current_deps='false'
dep_vsns_file="$build_dir/.dep_vsns"
dep_vsns=''
deps='
    crosstool
    zlib
    yaml
    ssl
    otp'
supported_targets='
    x86_64-linux-gnu
    aarch64-linux-gnu
    x86_64-linux-musl
    aarch64-linux-musl'

umask 022

#' Try to find a browser for checking dependency versions.
have_browser()
{
	for browser in 'lynx' 'links' 'elinks'
	do
		$browser -dump 'https://eturnal.net/' >'/dev/null' && return 0
	done
	return 1
}
#.

#' Check whether the given dependency version is up-to-date.
check_vsn()
{
	local name="$1"
	local our_vsn="$2"
	local src_url="$3"
	local reg_exp="$4"
	local cur_vsn=$($browser -dump "$src_url" |
	                sed -n "s/.*$reg_exp.*/\\1/p" |
	                head -1)

	if [ "$our_vsn" = "$cur_vsn" ]
	then
		return 0
	else
		error "Current $name version is: $cur_vsn"
		error "But our $name version is: $our_vsn"
		error "Update $0 or set CHECK_DEPS=false"
		exit 1
	fi
}
#.

#' Check whether our dependency versions are up-to-date.
check_configured_dep_vsns()
{
	check_vsn 'OpenSSL' "$ssl_vsn" \
	          'https://www.openssl.org/source/' \
	          'openssl-\(3\.[1-9]\.[0-9.]*\)\.tar\.gz'
	check_vsn 'LibYAML' "$yaml_vsn" \
	          'https://pyyaml.org/wiki/LibYAML' \
	          'yaml-\([0-9][0-9.]*\)\.tar\.gz'
	check_vsn 'zlib' "$zlib_vsn" \
	          'https://zlib.net/' \
	          'zlib-\([1-9][0-9.]*\)\.tar\.gz'
	check_vsn 'Rebar3' "$rebar_vsn" \
	          'https://github.com/erlang/rebar3/tags' \
	          'tags\/\(3\.[0-9][0-9.]*\)\.tar\.gz'
}
#.

#' Check whether existing dependencies are up-to-date.
check_built_dep_vsns()
{
	for dep in $deps
	do
		eval dep_vsns=\"\$dep_vsns\$${dep}_vsn\"
	done

	dep_vsns="$dep_vsns-$targets"

	if [ -e "$dep_vsns_file" ]
	then
		if [ "$dep_vsns" = "$(cat "$dep_vsns_file")" ]
		then have_current_deps='true'
		fi
		rm "$dep_vsns_file"
	fi
}
#.

#' Save built dependency versions.
save_built_dep_vsns()
{
	echo "$dep_vsns" >"$dep_vsns_file"
}
#.

#' Create common part of Crosstool-NG configuration file.
create_common_config()
{
	local file="$1"

	cat >"$file" <<-'EOF'
		CT_CONFIG_VERSION="4"
		CT_DOWNLOAD_AGENT_CURL=y
		CT_OMIT_TARGET_VENDOR=y
		CT_CC_LANG_CXX=y
		CT_ARCH_64=y
		CT_KERNEL_LINUX=y
		CT_LINUX_V_3_16=y
		CT_LOG_PROGRESS_BAR=n
	EOF
}
#.

#' Create Crosstool-NG configuration file for glibc.
create_glibc_config()
{
	local file="$1"

	create_common_config "$file"

	cat >>"$file" <<-'EOF'
		CT_GLIBC_V_2_19=y
	EOF
}
#.

#' Create Crosstool-NG configuration file for musl.
create_musl_config()
{
	local file="$1"

	create_common_config "$file"

	cat >>"$file" <<-'EOF'
		CT_EXPERIMENTAL=y
		CT_LIBC_MUSL=y
	EOF
}
#.

#' Create Crosstool-NG configuration file for x64.
create_x64_config()
{
	local file="$1"
	local libc="$2"

	create_${libc}_config "$file"

	cat >>"$file" <<-'EOF'
		CT_ARCH_X86=y
	EOF
}
#.

#' Create Crosstool-NG configuration file for arm64.
create_arm64_config()
{
	local file="$1"
	local libc="$2"

	create_${libc}_config "$file"

	cat >>"$file" <<-'EOF'
		CT_ARCH_ARM=y
	EOF
}
#.

#' Return our name for the given platform.
arch_name()
{
	local target="$1"

	case $target in
	x86_64*)
		printf 'x64'
		;;
	aarch64*)
		printf 'arm64'
		;;
	*)
		error "Unsupported target platform: $target"
		exit 1
		;;
	esac
}
#.

#' Return our name for the given libc.
libc_name()
{
	local target="$1"

	case $target in
	*gnu)
		printf 'glibc'
		;;
	*musl)
		printf 'musl'
		;;
	*)
		error "Unsupported target platform: $target"
		exit 1
		;;
	esac
}
#.

#' Add native Erlang/OTP "bin" directory to PATH (for bootstrapping and Rebar3).
add_otp_path()
{
	local prefix="$1"
	local mode="$2"

	if [ "$mode" = 'native' ]
	then
		native_otp_bin="$prefix/bin"
	elif [ -n "${INSTALL_DIR_FOR_OTP+x}" ]
	then
		# Let GitHub runners build for non-native systems:
		# https://github.com/erlef/setup-beam#environment-variables
		native_otp_bin="$INSTALL_DIR_FOR_OTP/bin"
	fi
	export PATH="$native_otp_bin:$PATH"
}
#.

#' Return Rebar3 profile depending on compilation mode.
rebar3_profile()
{
	local mode="$1"

	if [ "$mode" = 'native' ]
	then printf 'prod'
	else printf 'prod_cross'
	fi
}
#.

#' Build toochain for a given target.
build_toolchain()
{
	local prefix="$1"
	local target="$2"
	local libc=$(libc_name "$target")
	local arch=$(arch_name "$target")

	if [ -d "$prefix" ]
	then
		info "Using existing toolchain in $prefix ..."
	else
		if ! [ -x "$bootstrap_dir/bin/ct-ng" ]
		then
			info "Extracting Crosstool-NG $crosstool_vsn ..."
			cd "$src_dir"
			tar -xJf "$crosstool_tar"
			cd "$OLDPWD"

			info "Building Crosstool-NG $crosstool_vsn ..."
			cd "$src_dir/$crosstool_dir"
			./configure --prefix="$bootstrap_dir"
			make V=0
			make install
			cd "$OLDPWD"
		fi

		info "Building toolchain for $arch-$libc ..."
		cd "$root_dir"
		create_${arch}_config 'defconfig' "$libc"
		ct-ng defconfig
		ct-ng build CT_PREFIX="$ct_prefix_dir" CT_JOBS="$ct_jobs"
		rm -rf 'defconfig' '.config'* '.build' 'build.log'
		cd "$OLDPWD"
	fi
}
#.

#' Build target dependencies.
build_deps()
{
	local prefix="$1"
	local target="$2"
	local mode="$3"
	local arch="$(arch_name "$target")"
	local libc=$(libc_name "$target")
	local profile="$(rebar3_profile "$mode")"
	local rel_dir="$PWD/_build/$profile/rel/$rel_name"
	local target_src_dir="$prefix/src"
	local saved_path="$PATH"

	if [ "$mode" = 'cross' ]
	then configure="./configure --host=$target --build=$platform"
	else configure='./configure'
	fi

	mkdir "$prefix"

	info 'Extracting dependencies ...'
	mkdir "$target_src_dir"
	cd "$target_src_dir"
	tar -xzf "$src_dir/$zlib_tar"
	tar -xzf "$src_dir/$yaml_tar"
	tar -xzf "$src_dir/$ssl_tar"
	tar -xzf "$src_dir/$otp_tar"
	cd "$OLDPWD"

	info "Building zlib $zlib_vsn for $arch-$libc ..."
	cd "$target_src_dir/$zlib_dir"
	CFLAGS="$CFLAGS -O3 -fPIC" ./configure --prefix="$prefix" --static
	make
	make install
	cd "$OLDPWD"

	info "Building LibYAML $yaml_vsn for $arch-$libc ..."
	cd "$target_src_dir/$yaml_dir"
	$configure --prefix="$prefix" --disable-shared CFLAGS="$CFLAGS -fPIC"
	make
	make install
	cd "$OLDPWD"

	info "Building OpenSSL $ssl_vsn for $arch-$libc ..."
	cd "$target_src_dir/$ssl_dir"
	CFLAGS="$CFLAGS -O3 -fPIC" \
	    ./Configure no-shared no-module no-ui-console \
	    --prefix="$prefix" \
	    --openssldir="$prefix" \
	    --libdir='lib' \
	    "linux-${target%-linux-*}"
	make build_libs
	make install_dev
	cd "$OLDPWD"

	info "Building Erlang/OTP $otp_vsn for $arch-$libc ..."
	if [ "$mode" = 'cross' ]
	then
		add_otp_path "$prefix" "$mode" # For bootstrapping/Rebar3.
		export erl_xcomp_sysroot="$prefix"
	fi
	cd "$target_src_dir/$otp_dir"
	# The additional CFLAGS/LIBS below are required by --enable-static-nifs.
	$configure \
	    --prefix="$prefix" \
	    --with-ssl="$prefix" \
	    --without-termcap \
	    --without-javac \
	    --disable-dynamic-ssl-lib \
	    --enable-static-nifs \
	    --enable-deterministic-build \
	    CFLAGS="$CFLAGS -Wl,-L$prefix/lib" \
	    LIBS='-lcrypto'
	make
	make install
	if [ "$mode" = 'native' ]
	then add_otp_path "$prefix" "$mode" # For bootstrapping/Rebar3.
	else unset erl_xcomp_sysroot
	fi
	cd "$OLDPWD"

	export PATH="$saved_path"
}
#.

#' Build the actual release.
build_rel()
{
	local prefix="$1"
	local target="$2"
	local mode="$3"
	local arch="$(arch_name "$target")"
	local libc=$(libc_name "$target")
	local profile="$(rebar3_profile "$mode")"
	local rel_dir="$PWD/_build/$profile/rel/$rel_name"
	local target_src_dir="$prefix/src"
	local target_dst_dir="$prefix/$rel_name"
	local target_dst_tar="$rel_name-$rel_vsn-linux-$libc-$arch.tar.gz"
	local saved_path="$PATH"

	export PATH="$ct_prefix_dir/$target/bin:$PATH"
	export CC="$target-gcc"
	export CXX="$target-g++"
	export CPP="$target-cpp"
	export LD="$target-ld"
	export AS="$target-as"
	export AR="$target-ar"
	export NM="$target-nm"
	export RANLIB="$target-ranlib"
	export OBJCOPY="$target-objcopy"
	export STRIP="$target-strip"
	export CPPFLAGS="-I$prefix/include"
	export CFLAGS="-g0 -O2 -pipe -fomit-frame-pointer -static-libgcc $CPPFLAGS"
	export CXXFLAGS="$CFLAGS -static-libstdc++"
	export LDFLAGS="-L$prefix/lib -static-libgcc -static-libstdc++"
	export ERL_COMPILER_OPTIONS='[no_debug_info]'

	if [ $have_current_deps = false ]
	then build_deps "$prefix" "$target" "$mode"
	fi

	add_otp_path "$prefix" "$mode"

	info "Removing Rebar3 cache and old $rel_name builds"
	rm -rf "$HOME/.cache/rebar3" '_build'
	rebar3 clean -a

	info "Building $rel_name $rel_vsn for $arch-$libc ..."
	if [ "$mode" = 'native' ]
	then
		ERL_COMPILER_OPTIONS="$erl_compiler_options" \
		    ERL_DIST_PORT='3470' \
		    rebar3 as "$(rebar3_profile "$mode")" tar
	else
		ln -s "$prefix/lib" # As expected by the 'cross' profile.
		ei_inc="$prefix/lib/erlang/lib/erl_interface-"*'/include'
		ei_lib="$prefix/lib/erlang/lib/erl_interface-"*'/lib'
		ERL_COMPILER_OPTIONS="$erl_compiler_options" \
		    ERL_DIST_PORT='3470' \
		    ERL_EI_INCLUDE_DIR=$(ls -1d $ei_inc) \
		    ERL_EI_LIBDIR=$(ls -1d $ei_lib) \
		    LDLIBS='-lpthread' \
		    rebar3 as "$(rebar3_profile "$mode")" tar
		rm 'lib'
	fi

	info "Editing $rel_name $rel_vsn for $arch-$libc ..."
	mkdir "$target_dst_dir"
	tar -C "$target_dst_dir" -xzf "$rel_dir/$rel_tar"
	find "$target_dst_dir/lib" -type f -name 'otp_test_engine.so' \
	    -delete # Remove shared object file used only in test suite.
	find "$target_dst_dir/lib/crypto-"* "$target_dst_dir/lib/asn1-"* \
	    '(' -name 'asn1rt_nif.so' -o \
	        -name 'crypto.so' -o \
	        -name 'lib' -o \
	        -name 'priv' ')' \
	    -delete # Remove shared object files of statically linked NIFs.
	find "$target_dst_dir/lib" -type f -name '*.so' \
	    -exec "$STRIP" -s '{}' '+'
	find "$target_dst_dir/erts-"*'/bin' -type f -perm '-u+x' \
	    -exec "$STRIP" -s '{}' '+' 2>'/dev/null' || :
	# See https://github.com/erlware/relx/pull/906 re. ["Attr"] argument.
	test "$mode" = 'cross' &&
	    erl -noinput -eval \
	        "{ok, _} = beam_lib:strip_release('$target_dst_dir', [\"Attr\"]), halt()"
	tar -C "$prefix" --owner="$rel_name" --group="$rel_name" -cf - \
	    "$rel_name" | gzip -9 >"$target_dst_tar"
	rm -rf "$target_dst_dir"

	info "Created $target_dst_tar successfully."

	unset CC CXX CPP LD AS AR NM RANLIB OBJCOPY STRIP
	unset CFLAGS CXXFLAGS LDFLAGS ERL_COMPILER_OPTIONS
	export PATH="$saved_path"
}
#.

if [ $# -eq 1 ] && [ "$1" = '-h' ]
then
	usage
elif [ $# -eq 0 ]
then
	targets="$supported_targets"
	build_native='true'
else
	build_native='false'
	for target in "$@"
	do
		is_supported='false'
		for supported_target in $supported_targets
		do
			if [ "$target" = "$supported_target" ]
			then
				is_supported='true'
				if [ "$target" = "$platform" ]
				then
					build_native='true'
				fi
				break
			fi
		done
		if [ "$is_supported" = 'false' ]
		then
			error "Target not supported: $target"
			echo >&2
			echo >&2 "Supported targets:"
			echo >&2 "$supported_targets"
			exit 2
		fi
	done
	targets="$*"
fi

if [ "$build_native" = 'false' ] && [ -z "${INSTALL_DIR_FOR_OTP+x}" ]
then
	error 'Native Erlang/OTP installation not found.'
	error "Set INSTALL_DIR_FOR_OTP or add the '$platform' target."
	exit 1
fi

if [ "${CHECK_DEPS:-true}" = 'true' ]
then
	if have_browser
	then
		check_configured_dep_vsns
	else
		error 'Cannot check dependency versions.'
		error 'Install a browser or set CHECK_DEPS=false'
		exit 1
	fi
else
	info "Won't check dependency versions."
fi

if ! mkdir -p "$root_dir"
then
	error 'Set BUILD_DIR to a usable build directory path.'
	exit 1
fi

check_built_dep_vsns

info 'Removing old bootstrap tools ...'
rm -rf "$bootstrap_dir"
mkdir "$bootstrap_dir"

if [ $have_current_deps = true ]
then
	info 'Dependencies are up-to-date ...'
else
	# Keep existing toolchains but rebuild everything else.
	info 'Removing old builds ...'
	rm -rf "$build_dir"
	mkdir "$build_dir"

	info 'Removing old source ...'
	rm -rf "$src_dir"
	mkdir "$src_dir"

	info 'Downloading dependencies ...'
	cd "$src_dir"
	curl -fsSLO "http://crosstool-ng.org/download/crosstool-ng/$crosstool_tar"
	curl -fsSLO "https://zlib.net/fossils/$zlib_tar"
	curl -fsSLO "https://pyyaml.org/download/libyaml/$yaml_tar"
	curl -fsSLO "https://www.openssl.org/source/$ssl_tar"
	curl -fsSLO "https://github.com/erlang/otp/releases/download/OTP-$otp_vsn/$otp_tar"
	cd "$OLDPWD"
fi

info "Downloading Rebar3 $rebar_vsn ..."
install -d "$bootstrap_dir/bin"
cd "$bootstrap_dir/bin"
curl -fsSLO "https://github.com/erlang/rebar3/releases/download/$rebar_vsn/rebar3"
chmod +x 'rebar3'
cd "$OLDPWD"

export PATH="$bootstrap_dir/bin:$PATH" # For Rebar3 and possibly ct-ng.

for target in $targets
do
	prefix="$build_dir/$target"
	toolchain_dir="$ct_prefix_dir/$target"

	if [ "$platform" = "$target" ]
	then mode='native'
	else mode='cross'
	fi
	build_toolchain "$toolchain_dir" "$target"
	build_rel "$prefix" "$target" "$mode"
done

save_built_dep_vsns

info "Build started: $build_start"
info "Build ended: $(date '+%F %T')"

# vim:set foldmarker=#',#. foldmethod=marker:
