#!/usr/bin/env bash
#  SPDX-License-Identifier: BSD-3-Clause
#  Copyright (C) 2019 Intel Corporation
#  All rights reserved.
#
shopt -s extglob
git_repo_abi="https://github.com/spdk/spdk-abi.git"

function get_spdk_abi() {
	local dest=$1
	mkdir -p $dest
	if [[ -d $SPDK_ABI_DIR ]]; then
		echo "spdk-abi found at $SPDK_ABI_DIR"
		cp -r "$SPDK_ABI_DIR"/* "$dest/"
	else
		# In case that someone run test manually and did not set existing
		# spdk-abi directory via SPDK_ABI_DIR
		echo "spdk-abi has not been found at $SPDK_ABI_DIR, cloning"
		git clone $git_repo_abi "$dest"
	fi
}

function get_release_branch() {
	tag=$(git describe --tags --abbrev=0 --exclude=LTS --exclude=*-pre)
	branch="${tag:0:6}.x"
	echo "$branch"
}

if [ "$(uname -s)" = "FreeBSD" ]; then
	echo "Not testing for shared object dependencies on FreeBSD."
	exit 0
fi

rootdir=$(readlink -f $(dirname $0)/../..)

if [[ ! -f $1 ]]; then
	echo "ERROR: SPDK test configuration not specified"
	exit 1
fi

source $1
source "$rootdir/test/common/autotest_common.sh"

libdir="$rootdir/build/lib"
libdeps_file="$rootdir/mk/spdk.lib_deps.mk"
suppression_file="$HOME/abigail_suppressions.ini"

function check_header_filenames() {
	local dups_found=0

	xtrace_disable

	include_headers=$(git ls-files -- include/spdk include/spdk_internal | xargs -n 1 basename)
	dups=
	for file in $include_headers; do
		if [[ $(git ls-files "lib/**/$file" "module/**/$file" --error-unmatch 2> /dev/null) ]]; then
			dups+=" $file"
			dups_found=1
		fi
	done

	xtrace_restore

	if ((dups_found == 1)); then
		echo "Private header file(s) found with same name as public header file."
		echo "This is not allowed since it can confuse abidiff when determining if"
		echo "data structure changes impact ABI."
		echo $dups
		return 1
	fi
}

function confirm_abi_deps() {
	local processed_so=0
	local abidiff_output
	local release
	local source_abi_dir="$rootdir/test/make/abi"

	release=$(get_release_branch)

	get_spdk_abi "$source_abi_dir"
	echo "* Running ${FUNCNAME[0]} against the latest (${release%.*}) release" >&2

	if ! hash abidiff; then
		echo "Unable to check ABI compatibility. Please install abidiff."
		return 1
	fi

	cat << EOF > ${suppression_file}
[suppress_type]
	name = spdk_nvme_power_state
[suppress_type]
	name = spdk_nvme_ctrlr_data
[suppress_type]
	name = spdk_nvme_cdata_oacs
[suppress_type]
	name = spdk_nvme_cdata_nvmf_specific
[suppress_type]
	name = spdk_nvme_cmd
[suppress_type]
	name = spdk_bs_opts
[suppress_type]
	name = spdk_app_opts
EOF

	for object in "$libdir"/libspdk_*.so; do
		abidiff_output=0

		so_file=$(basename $object)
		if [ ! -f "$source_abi_dir/$release/$so_file" ]; then
			echo "No corresponding object for $so_file in canonical directory. Skipping."
			continue
		fi

		cmd_args=('abidiff'
			$source_abi_dir/$release/$so_file "$libdir/$so_file"
			'--headers-dir1' $source_abi_dir/$release/include
			'--headers-dir2' $rootdir/include
			'--leaf-changes-only' '--suppressions' $suppression_file)

		if ! output=$("${cmd_args[@]}" --stat); then
			# remove any filtered out variables.
			output=$(sed "s/ [()][^)]*[)]//g" <<< "$output")

			IFS="." read -r _ _ new_so_maj new_so_min < <(readlink "$libdir/$so_file")
			IFS="." read -r _ _ old_so_maj old_so_min < <(readlink "$source_abi_dir/$release/$so_file")

			found_abi_change=false
			so_name_changed=no

			if [[ $output == *"ELF SONAME changed"* ]]; then
				so_name_changed=yes
			fi

			changed_leaf_types=0
			if [[ $output =~ "leaf types summary: "([0-9]+) ]]; then
				changed_leaf_types=${BASH_REMATCH[1]}
			fi

			removed_functions=0 changed_functions=0 added_functions=0
			if [[ $output =~ "functions summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
				removed_functions=${BASH_REMATCH[1]} changed_functions=${BASH_REMATCH[2]} added_functions=${BASH_REMATCH[3]}
			fi

			removed_vars=0 changed_vars=0 added_vars=0
			if [[ $output =~ "variables summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
				removed_vars=${BASH_REMATCH[1]} changed_vars=${BASH_REMATCH[2]} added_vars=${BASH_REMATCH[3]}
			fi

			if ((changed_leaf_types != 0)); then
				if ((new_so_maj == old_so_maj)); then
					abidiff_output=1
					touch $fail_file
					echo "Please update the major SO version for $so_file. A header accessible type has been modified since last release."
				fi
				found_abi_change=true
			fi

			if ((removed_functions != 0)) || ((removed_vars != 0)); then
				if ((new_so_maj == old_so_maj)); then
					abidiff_output=1
					touch $fail_file
					echo "Please update the major SO version for $so_file. API functions or variables have been removed since last release."
				fi
				found_abi_change=true
			fi

			if ((changed_functions != 0)) || ((changed_vars != 0)); then
				if ((new_so_maj == old_so_maj)); then
					abidiff_output=1
					touch $fail_file
					echo "Please update the major SO version for $so_file. API functions or variables have been changed since last release."
				fi
				found_abi_change=true
			fi

			if ((added_functions != 0)) || ((added_vars != 0)); then
				if ((new_so_min == old_so_min && new_so_maj == old_so_maj)) && ! $found_abi_change; then
					abidiff_output=1
					touch $fail_file
					echo "Please update the minor SO version for $so_file. API functions or variables have been added since last release."
				fi
				found_abi_change=true
			fi

			if [[ $so_name_changed == yes ]]; then
				# After 22.01 LTS all SO major versions were intentionally increased. Disable this check until SPDK 22.05 release.
				found_abi_change=true
				if ! $found_abi_change; then
					echo "SO name for $so_file changed without a change to abi. please revert that change."
					touch $fail_file
				fi

				if ((new_so_maj != old_so_maj && new_so_min != 0)); then
					echo "SO major version for $so_file was bumped. Please reset the minor version to 0."
					touch $fail_file
				fi

				if ((new_so_min > old_so_min + 1)); then
					echo "SO minor version for $so_file was incremented more than once. Please revert minor version to $((old_so_min + 1))."
					touch $fail_file
				fi

				if ((new_so_maj > old_so_maj + 1)); then
					echo "SO major version for $so_file was incremented more than once. Please revert major version to $((old_so_maj + 1))."
					touch $fail_file
				fi
			fi

			if ((abidiff_output == 1)); then
				"${cmd_args[@]}" --impacted-interfaces || :
			fi
		fi
		processed_so=$((processed_so + 1))
	done
	rm -f $suppression_file
	echo "Processed $processed_so objects."
	rm -rf "$source_abi_dir"
}

function get_lib_shortname() {
	local lib=${1##*/}
	echo "${lib//@(libspdk_|.so)/}"
}

function import_libs_deps_mk() {
	local var_mk val_mk dep_mk fun_mk
	while read -r var_mk _ val_mk; do
		if [[ $var_mk == "#"* || ! $var_mk =~ (DEPDIRS-|_DEPS|_LIBS) ]]; then
			continue
		fi
		var_mk=${var_mk#*-}
		for dep_mk in $val_mk; do
			fun_mk=${dep_mk//@('$('|')')/}
			if [[ $fun_mk != "$dep_mk" ]]; then
				eval "${fun_mk}() { echo \$$fun_mk ; }"
			# Ignore any event_* dependencies. Those are based on the subsystem configuration and not readelf.
			elif ((IGNORED_LIBS["$dep_mk"] == 1)) || [[ $dep_mk =~ event_ ]]; then
				continue
			fi
			eval "$var_mk=\${$var_mk:+\$$var_mk }$dep_mk"
		done
	done < "$libdeps_file"
}

function confirm_deps() {
	local lib=$1 deplib lib_shortname

	lib_shortname=$(get_lib_shortname "$lib")
	lib_make_deps=(${!lib_shortname})

	symbols=($(readelf -s --wide "$lib" | grep -E "NOTYPE.*GLOBAL.*UND" | awk '{print $8}' | sort -u))
	symbols_regx=$(
		IFS="|"
		echo "(${symbols[*]})"
	)

	if ((${#symbols[@]} > 0)); then
		for deplib in "$libdir/"libspdk_!("$lib_shortname").so; do
			readelf -s --wide "$deplib" | grep -m1 -qE "DEFAULT\s+[0-9]+\s$symbols_regx$" || continue
			found_symbol_lib=$(get_lib_shortname "$deplib")
			# Ignore the env_dpdk readelf dependency. We don't want people explicitly linking against it.
			if [[ $found_symbol_lib != *env_dpdk* ]]; then
				dep_names+=("$found_symbol_lib")
			fi
		done
	fi

	diff=$(echo "${dep_names[@]}" "${lib_make_deps[@]}" | tr ' ' '\n' | sort | uniq -u)
	if [ "$diff" != "" ]; then
		touch $fail_file
		echo "there was a dependency mismatch in the library $lib_shortname"
		echo "The makefile (spdk.lib_deps.mk) lists: '${lib_make_deps[*]}'"
		echo "readelf outputs   : '${dep_names[*]}'"
		echo "---------------------------------------------------------------------"
	fi
}

function confirm_makefile_deps() {
	echo "---------------------------------------------------------------------"
	# Exclude libspdk_env_dpdk.so from the library list. We don't link against this one so that
	# users can define their own environment abstraction. However we do want to still check it
	# for dependencies to avoid printing out a bunch of confusing symbols under the missing
	# symbols section.
	SPDK_LIBS=("$libdir/"libspdk_!(env_dpdk).so)

	declare -A IGNORED_LIBS=()
	if grep -q 'CONFIG_RDMA?=n' $rootdir/mk/config.mk; then
		IGNORED_LIBS["rdma"]=1
	fi

	(
		import_libs_deps_mk
		for lib in "${SPDK_LIBS[@]}"; do confirm_deps "$lib" & done
		wait
	)
}

run_test "check_header_filenames" check_header_filenames

config_params=$(get_config_params)
if [ "$SPDK_TEST_OCF" -eq 1 ]; then
	config_params="$config_params --with-ocf=$rootdir/ocf.a"
fi

if [[ -f $rootdir/mk/spdk.common.mk ]]; then
	$MAKE $MAKEFLAGS clean
fi

./configure $config_params --with-shared
# By setting SPDK_NO_LIB_DEPS=1, we ensure that we won't create any link dependencies.
# Then we can be sure we get a valid accounting of the symbol dependencies we have.
SPDK_NO_LIB_DEPS=1 $MAKE $MAKEFLAGS

xtrace_disable

fail_file=$output_dir/check_so_deps_fail

rm -f $fail_file

run_test "confirm_abi_deps" confirm_abi_deps

run_test "confirm_makefile_deps" confirm_makefile_deps

$MAKE $MAKEFLAGS clean

if [ -f $fail_file ]; then
	rm -f $fail_file
	echo "shared object test failed"
	exit 1
fi

xtrace_restore
