#!/usr/bin/env bash

# This script builds a single Go binary of Coder with the given parameters.
#
# Usage: ./build_go.sh [--version 1.2.3-devel+abcdef] [--os linux] [--arch amd64] [--output path/to/output] [--slim] [--agpl] [--boringcrypto] [--dylib]
#
# Defaults to linux:amd64 with slim disabled, but can be controlled with GOOS,
# GOARCH and CODER_SLIM_BUILD=1. If no version is specified, defaults to the
# version from ./version.sh.
#
# GOARM can be controlled by suffixing any arm architecture (i.e. arm or arm64)
# with "vX" (e.g. "v7", "v8").
#
# Unless overridden via --output, the built binary will be dropped in
# "$repo_root/build/coder_$version_$os_$arch" (with a ".exe" suffix for windows
# builds) and the absolute path to the binary will be printed to stdout on
# completion.
#
# If the --sign-darwin parameter is specified and the OS is darwin, the output
# binary will be signed using ./sign_darwin.sh. Read that file for more details
# on the requirements.
#
# If the --sign-gpg parameter is specified, the output binary will be signed using ./sign_with_gpg.sh.
# Read that file for more details on the requirements.
#
# If the --agpl parameter is specified, builds only the AGPL-licensed code (no
# Coder enterprise features).
#
# If the --boringcrypto parameter is specified, builds use boringcrypto instead of
# the standard go crypto libraries.
#
# If the --dylib parameter is specified, the Coder Desktop `.dylib` is built
# instead of the standard binary. This is only supported on macOS arm64 & amd64.

set -euo pipefail
# shellcheck source=scripts/lib.sh
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"

version=""
os="${GOOS:-linux}"
arch="${GOARCH:-amd64}"
output_path=""
slim="${CODER_SLIM_BUILD:-0}"
agpl="${CODER_BUILD_AGPL:-0}"
sign_darwin="${CODER_SIGN_DARWIN:-0}"
sign_windows="${CODER_SIGN_WINDOWS:-0}"
sign_gpg="${CODER_SIGN_GPG:-0}"
boringcrypto=${CODER_BUILD_BORINGCRYPTO:-0}
dylib=0
windows_resources="${CODER_WINDOWS_RESOURCES:-0}"
debug=0
develop_in_coder="${DEVELOP_IN_CODER:-0}"

bin_ident="com.coder.cli"

args="$(getopt -o "" -l version:,os:,arch:,output:,slim,agpl,sign-darwin,sign-windows,boringcrypto,dylib,windows-resources,debug -- "$@")"
eval set -- "$args"
while true; do
	case "$1" in
	--version)
		version="$2"
		shift 2
		;;
	--os)
		os="$2"
		shift 2
		;;
	--arch)
		arch="$2"
		shift 2
		;;
	--output)
		mkdir -p "$(dirname "$2")"
		output_path="$(realpath "$2")"
		shift 2
		;;
	--slim)
		slim=1
		shift
		;;
	--agpl)
		agpl=1
		shift
		;;
	--sign-darwin)
		sign_darwin=1
		shift
		;;
	--sign-windows)
		sign_windows=1
		shift
		;;
	--sign-gpg)
		sign_gpg=1
		shift
		;;
	--boringcrypto)
		boringcrypto=1
		shift
		;;
	--dylib)
		dylib=1
		shift
		;;
	--windows-resources)
		windows_resources=1
		shift
		;;
	--debug)
		debug=1
		shift
		;;
	--)
		shift
		break
		;;
	*)
		error "Unrecognized option: $1"
		;;
	esac
done

cdroot

# Remove the "v" prefix.
version="${version#v}"
if [[ "$version" == "" ]]; then
	version="$(execrelative ./version.sh)"
fi

# Check dependencies
dependencies go
if [[ "$sign_darwin" == 1 ]]; then
	dependencies rcodesign
	requiredenvs AC_CERTIFICATE_FILE AC_CERTIFICATE_PASSWORD_FILE
fi
if [[ "$sign_windows" == 1 ]]; then
	dependencies java
	requiredenvs JSIGN_PATH EV_KEYSTORE EV_KEY EV_CERTIFICATE_PATH EV_TSA_URL GCLOUD_ACCESS_TOKEN
fi
if [[ "$windows_resources" == 1 ]]; then
	dependencies go-winres
fi

ldflags=(
	-X "'github.com/coder/coder/v2/buildinfo.tag=$version'"
)
# Disable deubgger information if not building a binary for debuggers.
if [[ "$debug" == 0 ]]; then
	ldflags+=(-s -w)
fi

if [[ "$develop_in_coder" == 1 ]]; then
	echo "INFO : Overriding codersdk.SessionTokenCookie as we are developing inside a Coder workspace."
	ldflags+=(
		-X "'github.com/coder/coder/v2/codersdk.SessionTokenCookie=dev_coder_session_token'"
	)
fi

# We use ts_omit_aws here because on Linux it prevents Tailscale from importing
# github.com/aws/aws-sdk-go-v2/aws, which adds 7 MB to the binary.
TS_EXTRA_SMALL="ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube"
if [[ "$slim" == 1 || "$dylib" == 1 ]]; then
	build_args+=(-tags "slim,$TS_EXTRA_SMALL")
else
	build_args+=(-tags "embed,$TS_EXTRA_SMALL")
fi
if [[ "$agpl" == 1 ]]; then
	# We don't use a tag to control AGPL because we don't want code to depend on
	# a flag to control AGPL vs. enterprise behavior.
	ldflags+=(-X "'github.com/coder/coder/v2/buildinfo.agpl=true'")
fi
cgo=0
if [[ "$dylib" == 1 ]]; then
	if [[ "$os" != "darwin" ]]; then
		error "dylib builds are not supported on $os"
	fi
	cgo=1
	build_args+=("-buildmode=c-shared")
	SDKROOT="$(xcrun --sdk macosx --show-sdk-path)"
	export SDKROOT
	bin_ident="com.coder.Coder-Desktop.VPN.dylib"

	plist_file=$(mktemp)
	trap 'rm -f "$plist_file"' EXIT
	# CFBundleShortVersionString must be in the format /[0-9]+.[0-9]+.[0-9]+/
	# CFBundleVersion can be in any format
	BUNDLE_IDENTIFIER="$bin_ident" VERSION_STRING="$version" SHORT_VERSION_STRING=$(echo "$version" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') \
		execrelative envsubst <"$(realpath ./vpn/dylib/info.plist.tmpl)" >"$plist_file"
	ldflags+=("-extldflags '-sectcreate __TEXT __info_plist $plist_file'")
fi
build_args+=(-ldflags "${ldflags[*]}")

# Disable optimizations if building a binary for debuggers.
if [[ "$debug" == 1 ]]; then
	build_args+=(-gcflags "all=-N -l")
fi

# Compute default output path.
if [[ "$output_path" == "" ]]; then
	mkdir -p "build"
	output_path="build/coder_${version}_${os}_${arch}"
	if [[ "$os" == "windows" ]]; then
		output_path+=".exe"
	fi
	output_path="$(realpath "$output_path")"
fi
build_args+=(-o "$output_path")

# Determine GOARM.
arm_version=""
if [[ "$arch" == "arm" ]]; then
	arm_version="7"
elif [[ "$arch" == "armv"* ]] || [[ "$arch" == "arm64v"* ]]; then
	arm_version="${arch//*v/}"

	# Remove the v* suffix.
	arch="${arch//v*/}"
fi

cmd_path="./enterprise/cmd/coder"
if [[ "$agpl" == 1 ]]; then
	cmd_path="./cmd/coder"
fi
if [[ "$dylib" == 1 ]]; then
	cmd_path="./vpn/dylib/lib.go"
fi

goexp=""
if [[ "$boringcrypto" == 1 ]]; then
	cgo=1
	goexp="boringcrypto"
fi

# On Windows, we use go-winres to embed the resources into the binary.
if [[ "$windows_resources" == 1 ]] && [[ "$os" == "windows" ]]; then
	# Convert the version to a format that Windows understands.
	# Remove any trailing data after a "+" or "-".
	version_windows=$version
	version_windows="${version_windows%+*}"
	version_windows="${version_windows%-*}"
	# If there wasn't any extra data, add a .0 to the version. Otherwise, add
	# a .1 to the version to signify that this is not a release build so it can
	# be distinguished from a release build.
	non_release_build=0
	if [[ "$version_windows" == "$version" ]]; then
		version_windows+=".0"
	else
		version_windows+=".1"
		non_release_build=1
	fi

	if [[ ! "$version_windows" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-1]$ ]]; then
		error "Computed invalid windows version format: $version_windows"
	fi

	# File description changes based on slimness, AGPL status, and architecture.
	file_description="Coder"
	if [[ "$agpl" == 1 ]]; then
		file_description+=" AGPL"
	fi
	if [[ "$slim" == 1 ]]; then
		file_description+=" CLI"
	fi
	if [[ "$non_release_build" == 1 ]]; then
		file_description+=" (development build)"
	fi

	# Because this writes to a file with the OS and arch in the filename, we
	# don't support concurrent builds for the same OS and arch (irregardless of
	# slimness or AGPL status).
	#
	# This is fine since we only embed resources during dogfood and release
	# builds, which use make (which will build all slim targets in parallel,
	# then all non-slim targets in parallel).
	expected_rsrc_file="./buildinfo/resources/resources_windows_${arch}.syso"
	if [[ -f "$expected_rsrc_file" ]]; then
		rm "$expected_rsrc_file"
	fi
	touch "$expected_rsrc_file"

	pushd ./buildinfo/resources
	GOARCH="$arch" go-winres simply \
		--arch "$arch" \
		--out "resources" \
		--product-version "$version_windows" \
		--file-version "$version_windows" \
		--manifest "cli" \
		--file-description "$file_description" \
		--product-name "Coder" \
		--copyright "Copyright $(date +%Y) Coder Technologies Inc." \
		--original-filename "coder.exe" \
		--icon ../../scripts/win-installer/coder.ico
	popd

	if [[ ! -f "$expected_rsrc_file" ]]; then
		error "Failed to generate $expected_rsrc_file"
	fi
fi

set +e
GOEXPERIMENT="$goexp" CGO_ENABLED="$cgo" GOOS="$os" GOARCH="$arch" GOARM="$arm_version" \
	go build \
	"${build_args[@]}" \
	"$cmd_path" 1>&2
exit_code=$?
set -e

# Clean up the resources file if it was generated.
if [[ "$windows_resources" == 1 ]] && [[ "$os" == "windows" ]]; then
	rm "$expected_rsrc_file"
fi

if [[ "$exit_code" != 0 ]]; then
	exit "$exit_code"
fi

# If we did embed resources, verify that they were included.
if [[ "$windows_resources" == 1 ]] && [[ "$os" == "windows" ]]; then
	winres_dir=$(mktemp -d)
	if ! go-winres extract --dir "$winres_dir" "$output_path" 1>&2; then
		rm -rf "$winres_dir"
		error "Compiled binary does not contain embedded resources"
	fi
	# If go-winres didn't return an error, it means it did find embedded
	# resources.
	rm -rf "$winres_dir"
fi

if [[ "$sign_darwin" == 1 ]] && [[ "$os" == "darwin" ]]; then
	execrelative ./sign_darwin.sh "$output_path" "$bin_ident" 1>&2
fi

if [[ "$sign_windows" == 1 ]] && [[ "$os" == "windows" ]]; then
	execrelative ./sign_windows.sh "$output_path" 1>&2
fi

# Platform agnostic signing
if [[ "$sign_gpg" == 1 ]]; then
	execrelative ./sign_with_gpg.sh "$output_path" 1>&2
fi

echo "$output_path"
