#!/usr/bin/env bash

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Files are relative to this script directory.
SELF="${BASH_SOURCE[0]}"
cd "$( dirname "${BASH_SOURCE[0]}" )"
COMPOSE_FILE=./infrastructure/docker/build/docker-compose.yml
COMPOSE_FILE_OPT=./infrastructure/docker/build/docker-compose-opt.yml

# Check for dependencies
if ! which docker >/dev/null 2>&1; then
	echo "Error: docker is required for a docker build." >&2
	exit 1
fi

# If the user defined COMPOSE, use that as the path for docker-compose.
if [ ! -z "$COMPOSE" ]; then
	COMPOSECMD=( "$COMPOSE" )
else
	COMPOSECMD=()
fi

# Check to see if docker-compose is already installed and use it directly, if possible.
if [ ${#COMPOSECMD[@]} -eq 0 ]; then
	if which docker-compose >/dev/null 2>&1; then
		COMPOSECMD=( docker-compose )
	fi
fi

# If it's unavailable, download the image and run docker-compose inside a container.
# This is considerably slower, but allows for building on hosts without docker-compose.
if [ ${#COMPOSECMD[@]} -eq 0 ]; then
	# Pin the version of docker-compose.
	IMAGE="docker/compose:1.11.2"

	# We need to either mount the docker socket or export the docker host into the container.
	# This allows the container to manage "sibling" containers via docker.
	if [ -z "$DOCKER_HOST" ]; then
			DOCKER_HOST="/var/run/docker.sock"
	fi

	if [ -S "$DOCKER_HOST" ]; then
			DOCKER_ADDR=(-v "$DOCKER_HOST:$DOCKER_HOST" -e DOCKER_HOST)
	else
			DOCKER_ADDR=(-e DOCKER_HOST -e DOCKER_TLS_VERIFY -e DOCKER_CERT_PATH)
	fi

	# We mount the current directory (the base of the repository) into the same location
	# inside the container. There are many places for which this won't work, but "/" is
	# a major one.
	#
	# You're going to want to avoid keeping your repository in a folder named "/usr/bin",
	# "/home", "/var" or any other standard paths that will be needed by the docker container.
	#
	# This is very unlikely to cause trouble for anyone in practice.
	if [ "$(pwd)" == "/" ]; then
		echo "Error: Cannot compile directly at filesystem root." >&2
		exit 1
	fi

	# Mount the working directory, and the home directory. Mounting $HOME provides container
	# access to config files that are kept there.
	VOLUMES=(-v "$(pwd):$(pwd)" -v "$HOME:$HOME" -v "$HOME:/root")

	# Prepull the image, to avoid spitting out pull progress during other commands.
	if ! docker inspect $IMAGE >/dev/null 2>&1; then
		docker pull $IMAGE >/dev/null 2>&1
	fi

	# COMPOSECMD is kept as an array to significantly simplify handling paths that contain
	# spaces and other special characters.
	COMPOSECMD=(docker run --rm "${DOCKER_ADDR[@]}" $COMPOSE_OPTIONS "${VOLUMES[@]}" -w "$(pwd)" $IMAGE)
fi

# Parse command line arguments
verbose=0
debug=0
quiet=0
all=0
build_images=0
NO_LOG_FILES=0
NO_SOURCE=0
SIMPLE=0
list=0
failure=0
print_help=0
while getopts h78abdf:lopqvsSL opt; do
	case $opt in
		\?)
			failure=1;
			;;
		7)
			BASE_IMAGE=centos
			RHEL_VERSION=7
			;;
		8)
			BASE_IMAGE=rockylinux
			RHEL_VERSION=8
			;;
		a)
			all=1
			;;
		b)
			build_images=1
			;;
		d)
			echo '-d is set! Disabling all compiler optimizations for debugging...';
			# If DEBUG_BUILD is true, then Golang binaries will remain unoptimized and
			# RPM packaging will not strip binaries.
			RUN_OPTIONS+=(-e 'DEBUG_BUILD=true');
			debug=1
			;;
		f)
			COMPOSE_FILE="$OPTARG"
			;;
		h)
			print_help=1;
			;;
		L)
			NO_LOG_FILES=1
			;;
		l)
			list=1;
			;;
		o)
			COMPOSE_FILE="$COMPOSE_FILE_OPT";
			;;
		p)
			build_images=0
			;;
		q)
			exec >/dev/null 2>&1
			quiet=1
			;;
		v)
			verbose=1
			;;
		s)
			SIMPLE=1
			;;
		S)
			NO_SOURCE=1
			;;
	esac
done

CMD=$("${COMPOSECMD[@]}" -f $COMPOSE_FILE config --services)
# Weasel must be built first otherwise it will fail from build artifacts/cache in an environment that doesn't have git
# e.g. the environment produced by `./pkg source`.
PROJECTS=$(grep -E "^weasel$" <<<"$CMD"; grep -Ev "^weasel$"<<<"$CMD")
HELP_TEXT="$(cat <<HELP_TEXT
Usage: $SELF [options] [projects]
  -7           Build RPMs targeting CentOS 7
  -8           Build RPMs targeting Rocky Linux 8 (default)
  -a           Build all projects, including optional ones.
  -b           Build builder Docker images before building projects
  -d           Disable compiler optimizations for debugging.
  -f <file>    Use <file> as the docker-compose. Default: $COMPOSE_FILE
  -h           Print help message and exit
  -L           Don$(echo \')t write logs to files - respects output levels on stderr/stdout as set by -q/-v
  -l           List available projects.
  -o           Build from the optional list. Same as -f "$COMPOSE_FILE_OPT"
  -p           Pull builder Docker images, do not build them (default)
  -q           Quiet mode. Supresses output. (default)
  -s           Simple output filenames - e.g. traffic_ops.rpm instead of traffic_ops-6.1.0-11637.ec9ff6a6.el8.x86_64.rpm
  -S           Skip building "source rpms"
  -v           Verbose mode. Lists all build output.

If no projects are listed, all projects will be packaged.
Valid projects:
$(echo "$PROJECTS" | sed "s/^/  - /")
HELP_TEXT
)";

if [[ "$failure" -ne 0 ]]; then
	echo "$HELP_TEXT" >&2;
	exit "$failure";
fi

if [[ "$print_help" -eq 1 ]]; then
	echo "$HELP_TEXT";
	exit 0;
fi

if [[ "$list" -eq 1 ]]; then
	echo "$PROJECTS";
	exit 0;
fi

shift $((OPTIND-1))

# Mark BASE_IMAGE for export
export BASE_IMAGE;
if [[ -n "$BASE_IMAGE" ]]; then
	RUN_OPTIONS+=(-e BASE_IMAGE);
fi
# Mark RHEL_VERSION for export
export RHEL_VERSION;
if [[ -n "$RHEL_VERSION" ]]; then
	RUN_OPTIONS+=(-e RHEL_VERSION);
fi
export SIMPLE;
export NO_SOURCE;
export NO_LOG_FILES;
RUN_OPTIONS+=(-e SIMPLE);
RUN_OPTIONS+=(-e NO_SOURCE);
RUN_OPTIONS+=(-e NO_LOG_FILES);

# If no specific packages are listed, or -a is used, run them all.
if (( ! "$#" || "$all" == 1 )); then
	set -- `$SELF -f "$COMPOSE_FILE" -l`
fi

# Create the dist directory if it does not exist.
mkdir -p dist

# Build each project in turn.
badproj=""
while (( "$#" )); do
	echo Building $1.
	(
		if (( "$verbose" == 0 )); then
			exec >/dev/null 2>&1
		fi

		# Build the project
		if [[ $build_images -eq 1 ]]; then
			"${COMPOSECMD[@]}" -f $COMPOSE_FILE build $1 || exit 1
		else
			"${COMPOSECMD[@]}" -f $COMPOSE_FILE pull $1 || exit 1
		fi
		"${COMPOSECMD[@]}" -f $COMPOSE_FILE run "${RUN_OPTIONS[@]}" --rm $1 || exit 1

		# Check for a chained compose file for this particular project.
		# A chained compose file will be named exactly the same as main docker-compose, with .service added,
		# where <service> is the name of the specific service to be chained. The file may be a symlink to another
		# compose file, in which case the symlink will be followed before it is processed.
		if [ -e "$COMPOSE_FILE.$1" ] ; then
			$SELF -f $(realpath -e "$COMPOSE_FILE.$1") $([ "$verbose" == 0 ] || echo "-v") $([ "$quiet" == 0 ] || echo "-q") $([ "$debug" == 0 ] || echo "-d") $([ "$build" == 0 ] || echo "-b") $("${COMPOSECMD[@]}" -f $(realpath -e "$COMPOSE_FILE.$1") config --services)
			chained_exit=$?
			[ $chained_exit == 0 ] || exit $chained_exit
		fi
	) || {
		# Don't totally bail out, but make note of the failures.
		failure=1
		badproj="$badproj $1"
	}
	shift
done

[ "$all" == 0 ] || $SELF -o $([ "$verbose" == 0 ] || echo "-v") $([ "$quiet" == 0 ] || echo "-q") $([ "$debug" == 0 ] || echo "-d") || failure=1

if [[ ! -z $badproj ]]; then
	echo "Failed to build$badproj."
fi

if [[ -d dist ]]; then
        echo "Results in 'dist':"
        ls -lt dist
else
        echo "dist artifact directory not found."
fi

exit $failure
