# Copyright 1999-2023 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

# @ECLASS: llvm.eclass
# @MAINTAINER:
# Michał Górny <mgorny@gentoo.org>
# @AUTHOR:
# Michał Górny <mgorny@gentoo.org>
# @SUPPORTED_EAPIS: 7 8
# @BLURB: Utility functions to build against slotted LLVM
# @DESCRIPTION:
# The llvm.eclass provides utility functions that can be used to build
# against specific version of slotted LLVM (with fallback to :0 for old
# versions).
#
# This eclass does not generate dependency strings. You need to write
# a proper dependency string yourself to guarantee that appropriate
# version of LLVM is installed.
#
# Example use for a package supporting LLVM 9 to 11:
# @CODE
# inherit cmake llvm
#
# RDEPEND="
#	<sys-devel/llvm-11:=
#	|| (
#		sys-devel/llvm:9
#		sys-devel/llvm:10
#		sys-devel/llvm:11
#	)
# "
# DEPEND=${RDEPEND}
#
# LLVM_MAX_SLOT=11
#
# # only if you need to define one explicitly
# pkg_setup() {
#	llvm_pkg_setup
#	do-something-else
# }
# @CODE
#
# Example for a package needing LLVM+clang w/ a specific target:
# @CODE
# inherit cmake llvm
#
# # note: do not use := on both clang and llvm, it can match different
# # slots then. clang pulls llvm in, so we can skip the latter.
# RDEPEND="
#	>=sys-devel/clang-9:=[llvm_targets_AMDGPU(+)]
# "
# DEPEND=${RDEPEND}
#
# llvm_check_deps() {
#	has_version -d "sys-devel/clang:${LLVM_SLOT}[llvm_targets_AMDGPU(+)]"
# }
# @CODE

case ${EAPI} in
	7|8) ;;
	*) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;;
esac

if [[ ! ${_LLVM_ECLASS} ]]; then
_LLVM_ECLASS=1

# make sure that the versions installing straight into /usr/bin
# are uninstalled
DEPEND="!!sys-devel/llvm:0"

# @ECLASS_VARIABLE: LLVM_MAX_SLOT
# @DEFAULT_UNSET
# @DESCRIPTION:
# Highest LLVM slot supported by the package. Needs to be set before
# llvm_pkg_setup is called. If unset, no upper bound is assumed.

# @ECLASS_VARIABLE: _LLVM_KNOWN_SLOTS
# @INTERNAL
# @DESCRIPTION:
# Correct values of LLVM slots, newest first.
declare -g -r _LLVM_KNOWN_SLOTS=( {17..8} )

# @FUNCTION: get_llvm_slot
# @USAGE: [-b|-d] [<max_slot>]
# @DESCRIPTION:
# Find the newest LLVM install that is acceptable for the package,
# and print its major version number (i.e. slot).
#
# If -b is specified, the checks are performed relative to BROOT,
# and BROOT-path is returned.  This is appropriate when your package
# calls llvm-config executable.
#
# If -d is specified, the checks are performed relative to ESYSROOT,
# and ESYSROOT-path is returned.  This is appropriate when your package
# uses CMake find_package(LLVM).  -d is the default.
#
# If <max_slot> is specified, then only LLVM versions that are not newer
# than <max_slot> will be considered. Otherwise, all LLVM versions would
# be considered acceptable. The function does not support specifying
# minimal supported version -- the developer must ensure that a version
# new enough is installed via providing appropriate dependencies.
#
# If llvm_check_deps() function is defined within the ebuild, it will
# be called to verify whether a particular slot is accepable. Within
# the function scope, LLVM_SLOT will be defined to the SLOT value
# (0, 4, 5...). The function should return a true status if the slot
# is acceptable, false otherwise. If llvm_check_deps() is not defined,
# the function defaults to checking whether sys-devel/llvm:${LLVM_SLOT}
# is installed.
get_llvm_slot() {
	debug-print-function ${FUNCNAME} "${@}"

	local hv_switch=-d
	while [[ ${1} == -* ]]; do
		case ${1} in
			-b|-d) hv_switch=${1};;
			*) break;;
		esac
		shift
	done

	local max_slot=${1}
	local slot
	for slot in "${_LLVM_KNOWN_SLOTS[@]}"; do
		# skip higher slots
		if [[ -n ${max_slot} ]]; then
			if [[ ${max_slot} == ${slot} ]]; then
				max_slot=
			else
				continue
			fi
		fi

		if declare -f llvm_check_deps >/dev/null; then
			local LLVM_SLOT=${slot}
			llvm_check_deps || continue
		else
			# check if LLVM package is installed
			has_version ${hv_switch} "sys-devel/llvm:${slot}" || continue
		fi

		echo "${slot}"
		return
	done

	# max_slot should have been unset in the iteration
	if [[ -n ${max_slot} ]]; then
		die "${FUNCNAME}: invalid max_slot=${max_slot}"
	fi

	die "No LLVM slot${1:+ <= ${1}} satisfying the package's dependencies found installed!"
}

# @FUNCTION: get_llvm_prefix
# @USAGE: [-b|-d] [<max_slot>]
# @DESCRIPTION:
# Find the newest LLVM install that is acceptable for the package,
# and print an absolute path to it.
#
# The options and behavior is the same as for get_llvm_slot.
get_llvm_prefix() {
	debug-print-function ${FUNCNAME} "${@}"

	local prefix=${ESYSROOT}
	[[ ${1} == -b ]] && prefix=${BROOT}

	echo "${prefix}/usr/lib/llvm/$(get_llvm_slot "${@}")"
}

# @FUNCTION: llvm_fix_clang_version
# @USAGE: <variable-name>...
# @DESCRIPTION:
# Fix the clang compiler name in specified variables to include
# the major version, to prevent PATH alterations from forcing an older
# clang version being used.
llvm_fix_clang_version() {
	debug-print-function ${FUNCNAME} "${@}"

	local shopt_save=$(shopt -p -o noglob)
	set -f
	local var
	for var; do
		local split=( ${!var} )
		case ${split[0]} in
			*clang|*clang++|*clang-cpp)
				local version=()
				read -r -a version < <("${split[0]}" --version)
				local major=${version[-1]%%.*}
				if [[ -n ${major//[0-9]} ]]; then
					die "${var}=${!var} produced invalid --version: ${version[*]}"
				fi

				split[0]+=-${major}
				if ! type -P "${split[0]}" &>/dev/null; then
					die "${split[0]} does not seem to exist"
				fi
				declare -g "${var}=${split[*]}"
				;;
		esac
	done
	${shopt_save}
}

# @FUNCTION: llvm_fix_tool_path
# @USAGE: <variable-name>...
# @DESCRIPTION:
# Fix the LLVM tools referenced in the specified variables to their
# current location, to prevent PATH alterations from forcing older
# versions being used.
llvm_fix_tool_path() {
	debug-print-function ${FUNCNAME} "${@}"

	local shopt_save=$(shopt -p -o noglob)
	set -f
	local var
	for var; do
		local split=( ${!var} )
		local path=$(type -P ${split[0]} 2>/dev/null)
		# if it resides in one of the LLVM prefixes, it's an LLVM tool!
		if [[ ${path} == "${BROOT}/usr/lib/llvm"* ]]; then
			split[0]=${path}
			declare -g "${var}=${split[*]}"
		fi
	done
	${shopt_save}
}

# @FUNCTION: llvm_pkg_setup
# @DESCRIPTION:
# Prepend the appropriate executable directory for the newest
# acceptable LLVM slot to the PATH. For path determination logic,
# please see the get_llvm_prefix documentation.
#
# The highest acceptable LLVM slot can be set in LLVM_MAX_SLOT variable.
# If it is unset or empty, any slot is acceptable.
#
# The PATH manipulation is only done for source builds. The function
# is a no-op when installing a binary package.
#
# If any other behavior is desired, the contents of the function
# should be inlined into the ebuild and modified as necessary.
llvm_pkg_setup() {
	debug-print-function ${FUNCNAME} "${@}"

	if [[ ${MERGE_TYPE} != binary ]]; then
		LLVM_SLOT=$(get_llvm_slot "${LLVM_MAX_SLOT}")

		llvm_fix_clang_version CC CPP CXX
		# keep in sync with profiles/features/llvm/make.defaults!
		llvm_fix_tool_path ADDR2LINE AR AS LD NM OBJCOPY OBJDUMP RANLIB
		llvm_fix_tool_path READELF STRINGS STRIP

		local prefix=${ESYSROOT}
		local llvm_path=${prefix}/usr/lib/llvm/${LLVM_SLOT}/bin
		local IFS=:
		local split_path=( ${PATH} )
		local new_path=()
		local x added=

		# prepend new path before first LLVM version found
		for x in "${split_path[@]}"; do
			if [[ ${x} == */usr/lib/llvm/*/bin ]]; then
				if [[ ${x} != ${llvm_path} ]]; then
					new_path+=( "${llvm_path}" )
				elif [[ ${added} && ${x} == ${llvm_path} ]]; then
					# deduplicate
					continue
				fi
				added=1
			fi
			new_path+=( "${x}" )
		done
		# ...or to the end of PATH
		[[ ${added} ]] || new_path+=( "${llvm_path}" )

		export PATH=${new_path[*]}
	fi
}

fi

EXPORT_FUNCTIONS pkg_setup