#!/bin/bash
#
# Copyright (c) 2016-2024 The Brenwill Workshop Ltd.
#
# fetchDependencies - Retrieves the correct versions of all dependencies
#
# macOS usage: ./fetchDependencies [--macos] [--ios] [--iossim] [--maccat] [--tvos] [--tvossim]
#                                  [--visionos] [--visionossim] [--all] [--none]
#                                  [-v] [--debug] [--build-spirv-tools]
#                                  [--v-headers-root path] [--spirv-cross-root path] [--glslang-root path]
#
#      --macos
#              Build the external libraries for the macOS platform.
#
#      --ios
#              Build the external libraries for the iOS platform.
#
#      --iossim
#              Build the external libraries for the iOS Simulator platform.
#
#      --maccat
#              Build the external libraries for the Mac Catalyst platform.
#
#      --tvos
#              Build the external libraries for the tvOS platform.
#
#      --tvossim
#              Build the external libraries for the tvOS Simulator platform.
#
#      --visionos
#              Build the external libraries for the visionOS platform.
#
#      --visionossim
#              Build the external libraries for the visionOS Simulator platform.
#
#      --all
#              Equivalent to specifying [--macos --ios --iossim --maccat --tvos --tvossim].
#              Results in one XCFramework for each external library, each containing
#              binaries for all supported platforms.
#              Currently excludes --visionos and --visionossim targets because those
#              require Xcode 15+ and will abort a multi-platform build.
#
#      --none
#              Don't build the external libraries for any platform (this is the default).
#
#      Multiple platform options may be specified. At least one platform option must be specified.
#
#      --asan
#              Build the external libraries against Address Sanitizer, which may be useful when debugging
#              and tracing calls into those libraries.
#
#      --tsan
#              Build the external libraries against Thread Sanitizer, which may be useful when debugging
#              and tracing calls into those libraries.
#
#      --ubsan
#              Build the external libraries against Undefined Behavior Sanitizer, which may be useful when debugging
#              and tracing calls into those libraries.
#
#      --debug
#              Build the external libraries in Debug mode, which may be useful when debugging
#              and tracing calls into those libraries.
#
#      --parallel-build
#              Build the external libraries in parallel using background processes.
#
#      --no-parallel-build
#              Build the external libraries serially instead of in parallel. This is the default.
#
#      --keep-cache
#              Do not remove the External/build/Intermediates cache directory after building.
#              Removing the Intermediates directory returns significant disk space after the
#              build, and is the default behaviour. Use this option if you intend to run this
#              script repeatedly to incrementally build one platform at a time.
#
#      --glslang-root path
#              "path" specifies a directory path to a KhronosGroup/glslang repository.
#              This repository does need to be built and the build directory must be in the
#              specified directory. It should be built the same way this script builds it.
#
#      --build-spirv-tools
#              Build the full spirv-tools distribution. Normally this is not needed, because
#              MoltenVK includes a template of pre-generated SPIRV-Tools header files, which
#              is all that is needed. Avoiding the spirv-tools build saves significant time
#              during the running of this script, and is necessary during CI because Travis CI
#              cannot support the required use of Python3 by the spirv-tools build. This flag
#              is used by the packagePregenSpirvToolsHeaders script which regenerates the
#              spirv-tools header files and repackages the Templates/spirv-tools/build.zip
#              file when the spirv-tools library version is upgraded.
#
#      --spirv-cross-root path
#              "path" specifies a directory path to a KhronosGroup/SPIRV-Cross repository.
#              This repository does not have to be built.
#
#      -v      verbose output
#
#      --v-headers-root path
#              "path" specifies a directory path to a KhronosGroup/Vulkan-Headers repository.
#              This repository does not have to be built.
#

set -e

# ----------------- Functions -------------------

BLD_NONE=""
BLD_IOS=""
BLD_IOS_SIM=""
BLD_MAC_CAT=""
BLD_TVOS=""
BLD_TVOS_SIM=""
BLD_VISIONOS=""
BLD_VISIONOS_SIM=""
BLD_MACOS=""
BLD_SPECIFIED=""
XC_CONFIG="Release"
XC_BUILD_VERBOSITY="-quiet"
XC_USE_BCKGND=""
XC_USE_ASAN="NO"
XC_USE_TSAN="NO"
XC_USE_UBSAN="NO"
V_HEADERS_ROOT=""
SPIRV_CROSS_ROOT=""
GLSLANG_ROOT=""
BLD_SPV_TLS=""
export KEEP_CACHE=""

while (( "$#" )); do
  case "$1" in
       --ios)
         BLD_IOS="Y"
         shift 1
         ;;
       --iossim)
         BLD_IOS_SIM="Y"
         shift 1
         ;;
       --maccat)
         BLD_MAC_CAT="Y"
         shift 1
         ;;
       --tvos)
         BLD_TVOS="Y"
         shift 1
         ;;
       --tvossim)
         BLD_TVOS_SIM="Y"
         shift 1
         ;;
       --visionos)
         BLD_VISIONOS="Y"
         shift 1
         ;;
       --visionossim)
         BLD_VISIONOS_SIM="Y"
         shift 1
         ;;
       --macos)
         BLD_MACOS="Y"
         shift 1
         ;;
       --all)
         BLD_MACOS="Y"
         BLD_IOS="Y"
         BLD_IOS_SIM="Y"
         BLD_MAC_CAT="Y"
         BLD_TVOS="Y"
         BLD_TVOS_SIM="Y"
#         BLD_VISIONOS="Y"              # Requires Xcode 15+
#         BLD_VISIONOS_SIM="Y"          # Requires Xcode 15+
         shift 1
         ;;
       --none)
         BLD_NONE="Y"
         shift 1
         ;;
       --debug)
         XC_CONFIG="Debug"
         shift 1
         ;;
       --asan)
         XC_USE_ASAN="YES"
         shift 1
         ;;
       --tsan)
         XC_USE_TSAN="YES"
         shift 1
         ;;
       --ubsan)
         XC_USE_UBSAN="YES"
         shift 1
         ;;
       --parallel-build)
         XC_USE_BCKGND="Y"
         shift 1
         ;;
       --no-parallel-build)
         XC_USE_BCKGND=""
         shift 1
         ;;
       --keep-cache)
         KEEP_CACHE="Y"
         shift 1
         ;;
       -v)
         XC_BUILD_VERBOSITY=""
         shift 1
         ;;
       --build-spirv-tools)
         BLD_SPV_TLS="Y"
         shift 1
         ;;
       --skip-spirv-tools-build)	#deprecated
         BLD_SPV_TLS=""
         shift 1
         ;;
       --v-headers-root)
         V_HEADERS_ROOT=$2
         shift 2
         ;;
       --spirv-cross-root)
         SPIRV_CROSS_ROOT=$2
         shift 2
         ;;
       --glslang-root)
         GLSLANG_ROOT=$2
         shift 2
         ;;
       -*|--*=)
         echo "Error: Unsupported option $1" >&2
         exit 1
         ;;
  esac
done

# Update a repository. If it exists, fetch it; if not, clone it.
# $1 repo name
# $2 repo url
# $3 repo revision (commit SHA)
function update_repo() {
	echo "$1 repo: $2"
	echo "$1 revision: $3"

	if [ -d $1 -a -d $1/.git ]; then
		cd $1
		git cat-file -e $3 || git fetch --all
		git checkout --force $3
		cd -  > /dev/null
	else
		rm -rf $1
		git clone $2 $1
		cd $1
		git checkout $3
		cd -  > /dev/null
	fi
}

# Build a repository
# $1 repo name
function build_repo() {
	echo "Building $1"

	mkdir -p $1/build
	cd $1/build
	if type ninja >/dev/null 2>&1 ; then
		cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=install ..
		ninja
	else
		cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=install ..
		make -j $(sysctl -n hw.activecpu)
	fi

	cd -  > /dev/null
}


# ----------------- Main -------------------

EXT_DIR=External
EXT_REV_DIR=../ExternalRevisions

echo
echo ========== Retrieving MoltenVK dependencies into ${EXT_DIR} ==========

mkdir -p ${EXT_DIR}
cd ${EXT_DIR}


# ----------------- Cereal -------------------

echo
echo ========== Cereal ==========
echo

REPO_NAME=cereal
REPO_URL="https://github.com/USCiLab/${REPO_NAME}.git"
REPO_REV=$(cat "${EXT_REV_DIR}/${REPO_NAME}_repo_revision")

update_repo ${REPO_NAME} ${REPO_URL} ${REPO_REV}


# ----------------- Vulkan-Headers -------------------

echo
echo ========== Vulkan-Headers ==========
echo

# When MoltenVK is built by something that already has
# a copy of this repo, use it by creating a symlink.

REPO_NAME=Vulkan-Headers

if [ ! "$V_HEADERS_ROOT" = "" ]; then
	rm -rf ${REPO_NAME}
	ln -sfn ${V_HEADERS_ROOT} ${REPO_NAME}
else
	REPO_URL="https://github.com/KhronosGroup/${REPO_NAME}.git"
	REPO_REV=$(cat "${EXT_REV_DIR}/${REPO_NAME}_repo_revision")

	update_repo ${REPO_NAME} ${REPO_URL} ${REPO_REV}
fi


# ----------------- SPIRV-Cross -------------------

echo
echo ========== SPIRV-Cross ==========
echo

# When MoltenVK is built by something that already has
# a copy of this repo, use it by creating a symlink.

REPO_NAME=SPIRV-Cross

if [ ! "$SPIRV_CROSS_ROOT" = "" ]; then
	rm -rf ${REPO_NAME}
	ln -sfn ${SPIRV_CROSS_ROOT} ${REPO_NAME}
else
	REPO_REMOTE=$(git remote | head -n 1)
	REPO_URL=$(git remote get-url "$REPO_REMOTE")
	REPO_URL="$(dirname "$REPO_URL")/${REPO_NAME}.git"
	REPO_REV=$(cat "${EXT_REV_DIR}/${REPO_NAME}_repo_revision")

	update_repo ${REPO_NAME} ${REPO_URL} ${REPO_REV}
fi


# ----------------- glslang -------------------

echo
echo ========== glslang and SPIRV-Tools ==========
echo

# When MoltenVK is built by something that already has
# a copy of this repo, use it by creating a symlink.

REPO_NAME=glslang

if [ ! "$GLSLANG_ROOT" = "" ]; then
	rm -rf ${REPO_NAME}
	ln -sfn ${GLSLANG_ROOT} ${REPO_NAME}
else
	REPO_URL="https://github.com/KhronosGroup/${REPO_NAME}.git"
	REPO_REV=$(cat "${EXT_REV_DIR}/${REPO_NAME}_repo_revision")

	update_repo ${REPO_NAME} ${REPO_URL} ${REPO_REV}

	cd ${REPO_NAME}
	./update_glslang_sources.py
	rm -rf build
	./build_info.py .        \
		-i build_info.h.tmpl  \
		-o build/include/glslang/build_info.h
	cd -  > /dev/null
fi

# Build the embedded spirv-tools, or use option of pre-generated headers
SPV_TLS_DIR="${REPO_NAME}/External/spirv-tools"
if [ "$BLD_SPV_TLS" = "Y" ]; then
	build_repo "${SPV_TLS_DIR}"
else
	unzip -o -q -d "${SPV_TLS_DIR}" ../Templates/spirv-tools/build.zip
	rm -rf "${SPV_TLS_DIR}/__MACOSX"
fi


# ----------------- Vulkan-Tools -------------------

echo
echo ========== Vulkan-Tools ==========
echo

REPO_NAME=Vulkan-Tools
REPO_URL="https://github.com/KhronosGroup/${REPO_NAME}.git"
REPO_REV=$(cat "${EXT_REV_DIR}/${REPO_NAME}_repo_revision")

update_repo ${REPO_NAME} ${REPO_URL} ${REPO_REV}


# ----------------- Volk -------------------

echo
echo ========== Volk ==========
echo

REPO_NAME=Volk
REPO_URL="https://github.com/zeux/${REPO_NAME}.git"
REPO_REV=$(cat "${EXT_REV_DIR}/${REPO_NAME}_repo_revision")

update_repo ${REPO_NAME} ${REPO_URL} ${REPO_REV}


# ----------------- Cleanup -------------------

cd ..


# -------------- Build MoltenVK external library dependencies -----------------

echo
echo ========== Started building dependency libraries at `date +"%r"` ==========
echo Please be patient on first build

function execute_xcodebuild_command () {
	if [ -n "${XCPRETTY}" ]; then
		set -o pipefail && xcodebuild "$@" | tee -a "dependenciesbuild.log" | ${XCPRETTY}
	else
		xcodebuild "$@"
	fi
}

# Build an Xcode scheme for an OS and platform
# 1 - OS
# 2 - Platform
# 3 - Destination (Optional. Defaults to same as platform)
function build_impl() {
	XC_OS=${1}
	XC_PLTFM=${2}
	if [ "${3}" != "" ]; then
		XC_DEST=${3};
	else
		XC_DEST=${XC_PLTFM};
	fi

	XC_SCHEME="${EXT_DEPS}-${XC_OS}"
	XC_LCL_DD_PATH="${XC_DD_PATH}/Intermediates/${XC_PLTFM}"

	echo Building external libraries for platform ${XC_PLTFM} and destination ${XC_DEST}

	execute_xcodebuild_command 									\
		GCC_PREPROCESSOR_DEFINITIONS='NDEBUG $(inherited)'		\
		-project "${XC_PROJ}"									\
		-scheme "${XC_SCHEME}"									\
		-destination "generic/platform=${XC_DEST}"				\
		-configuration "${XC_CONFIG}"							\
		-enableAddressSanitizer "${XC_USE_ASAN}"				\
		-enableThreadSanitizer "${XC_USE_TSAN}"					\
		-enableUndefinedBehaviorSanitizer "${XC_USE_UBSAN}"		\
		-derivedDataPath "${XC_LCL_DD_PATH}"					\
		${XC_BUILD_VERBOSITY}									\
		build

	echo Completed building external libraries for ${XC_PLTFM}
}

# Select whether or not to run the build in parallel.
# 1 - OS
# 2 - platform
# 3 - Destination (Optional. Defaults to same as platform)
function build() {
    BLD_SPECIFIED="Y"
	if [ "$XC_USE_BCKGND" != "" ]; then
		build_impl "${1}" "${2}" "${3}" &
	else
		build_impl "${1}" "${2}" "${3}"
	fi
}

EXT_DEPS=ExternalDependencies
XC_PROJ="${EXT_DEPS}.xcodeproj"
XC_DD_PATH="${EXT_DIR}/build"
export SKIP_PACKAGING="Y"

# Determine if xcpretty is present
XCPRETTY_PATH=$(command -v xcpretty 2> /dev/null || true) # ignore failures

# If xcpretty is present, use it to format xcodebuild output
XCPRETTY=""
if [ -n "$XCPRETTY_PATH" ]; then
	XCPRETTY="xcpretty -c"
fi
if [ "$XC_USE_BCKGND" != "" ]; then
	# For now, avoid using xcpretty if parallel background tasks are being used
	XCPRETTY=""
fi

# Structure build tasks by platform so they can be built in parallel per platform.
# Content for each platform must be built in series to avoid
if [ "$XC_USE_BCKGND" != "" ]; then
	trap "exit" INT TERM ERR
	trap "kill 0" EXIT
fi

if [ "$BLD_MACOS" != "" ]; then
	build "macOS" "macOS"
fi

if [ "$BLD_IOS" != "" ]; then
	build "iOS" "iOS"
fi

if [ "$BLD_IOS_SIM" != "" ]; then
	build "iOS" "iOS Simulator"
fi

if [ "$BLD_MAC_CAT" != "" ]; then
	build "iOS" "Mac Catalyst" "macOS,variant=Mac Catalyst"
fi

if [ "$BLD_TVOS" != "" ]; then
	build "tvOS" "tvOS"
fi

if [ "$BLD_TVOS_SIM" != "" ]; then
	build "tvOS" "tvOS Simulator"
fi

if [ "$BLD_VISIONOS" != "" ]; then
	build "xrOS" "xrOS"
fi

if [ "$BLD_VISIONOS_SIM" != "" ]; then
	build "xrOS" "xrOS Simulator"
fi

# Wait for any background process (if selected) to finish
if [ "$XC_USE_BCKGND" != "" ]; then
	wait
fi

if [ "$BLD_SPECIFIED" != "" ]; then
	# Build XCFrameworks, update latest symlink, remove intermediates, and clean MoltenVK for rebuild
	PROJECT_DIR="."
	CONFIGURATION=${XC_CONFIG}
	SKIP_PACKAGING=""
	. "./Scripts/create_ext_lib_xcframeworks.sh"
	. "./Scripts/package_ext_libs_finish.sh"
else
	if [ "$BLD_NONE" != "" ]; then
		echo Not building any platforms
	else
		echo "WARNING: You did not specify a platform to build."
		echo "To build the external libraries, include one or"
		echo "more of the following platform options:"
		echo "    --macos --ios --iossim --maccat --tvos --tvossim --visionos --visionossim --all"
		echo "See the instructions in the fetchDependencies script for more info."
	fi
fi

echo ========== Finished at `date +"%r"` ==========
exit 0
