build.sh
#!/usr/bin/env bash
set -eo pipefail


function print_help {
  cat << END
build.sh is a facade for docker compose. It runs a set of
optional tasks in the order specified below. This is the same
script used by CI/CD to build, test, and package FQL.

If the '--latest' flag is set, then the script will use 'latest'
as the tag for the docker images. This allows for offline
development. Otherwise, the tag changes will every commit and
docker attmepts a build which will fail.

If the '--image build' flag is set then the script starts off by
running 'docker build' for the 'fql-build' docker image. The tag
is determined by the git tag/hash and the FDB version. This image
is used to run the 'generate' and 'verify' tasks below.

If the '--generate' flag is set then the script checks if the
code generated by 'go generate ./...' is up to date.

If the '--verify' flag is set then the script builds, lints, and
tests the codebase. This task interacts with an FDB docker
container which is automatically started.

If the --docs' flag is set then the documentation HTML will be
generated under the /docs directory.

If the '--image fql' flag is set then the script runs 'docker
build' for the 'fql' docker image. The tag is determined by the
git tag/hash and the version of the FDB library specified in the
'.env' file.

If the '--run' flag is provided then all the args after this flag
are passed to an instance of the 'fql' docker image. Normally
this image expects a cluster file as the first argument but this
script takes care of starting an FDB cluster and providing the
cluster file as the first argument. Note that this is the same
FDB instance used by the 'verify' task.

  ./build.sh --run --write -q '/my/dir("hi")=nil'

After this, the script ends. If any of the requested tasks fail
then the script exits immediately.

Multiple image names can be specified on the '--image' flag by
separating them with commas.

  ./build.sh --image build,fql

When building Docker images, the dependencies of the Dockerfile
are specified in the '.env' file. When this file is changed,
you'll need to rebuild the docker images for the changes to take
effect.
END
}


# fail prints $1 to stderr and exits with code 1.

function fail {
  local RED='\033[0;31m' NO_COLOR='\033[0m'
  echo -e "${RED}ERR! ${1}${NO_COLOR}" >&2
  exit 1
}


# join_array joins the elements of the $2 array into a
# single string, placing $1 between each element.

function join_array {
  local sep="$1" out="$2"
  if shift 2; then
    for arg in "$@"; do
      out="${out}${sep}${arg}"
    done
  fi
  echo "$out"
}


# code_version returns the latest tag for the current
# Git commit. If there are no tags associated with
# the commit then the short hash is returned.

function code_version {
  local tag=""
  if tag="$(git describe --tags)"; then
    echo "$tag"
    return 0
  fi

  git rev-parse --short HEAD
}


# fdb_version returns the version of the FDB
# library specified by the env var FDB_VER.
# If FDB_VER is not defined then the .env
# file is read to obtain the version.

function fdb_version {
  if [[ -n "$FDB_VER" ]]; then
    echo "$FDB_VER"
    return 0
  fi

  local regex='FDB_VER=([^'$'\n'']*)'
  if ! [[ "$(cat .env)" =~ $regex ]]; then
    fail "Couldn't find FDB version in .env file."
  fi
  echo "${BASH_REMATCH[1]}"
}


# Change directory to repo root.

cd "${0%/*}"


# Parse the flags.

if [[ $# -eq 0 ]]; then
  print_help
  echo
  fail "At least one flag must be provided."
fi

while [[ $# -gt 0 ]]; do
  case $1 in
    --generate)
      VERIFY_GENERATION="x"
      shift 1
      ;;

    --verify)
      VERIFY_CODEBASE="x"
      shift 1
      ;;

    --docs)
      GENERATE_DOCS="x"
      shift 1
      ;;

    --latest)
      LATEST="x"
      shift 1
      ;;

    --image)
      for service in $(echo "$2" | tr "," "\n"); do
        case $service in
          build)
            IMAGE_BUILD="x"
            ;;
          fql)
            IMAGE_FQL="x"
            ;;
          *)
            fail "Invalid build target '$service'"
            ;;
        esac
      done
      shift 2
      ;;

    --run)
      shift 1
      RUN_FQL="x"
      FQL_ARGS=("$@")
      shift $#
      ;;

    --help)
      print_help
      exit 0
      ;;

    *)
      fail "Invalid flag '$1'"
  esac
done


# Build variables required by the docker compose command.

BUILD_TASKS=()

if [[ -n "$VERIFY_GENERATION" ]]; then
  BUILD_TASKS+=('./scripts/verify_generation.sh')
fi

if [[ -n "$VERIFY_CODEBASE" ]]; then
  BUILD_TASKS+=('./scripts/setup_database.sh')
  BUILD_TASKS+=("./scripts/verify_codebase.sh")
fi

if [[ -n "$GENERATE_DOCS" ]]; then
  BUILD_TASKS+=('./scripts/generate_docs.sh')
fi

BUILD_COMMAND="$(join_array ' && ' "${BUILD_TASKS[@]}")"
echo "BUILD_COMMAND=${BUILD_COMMAND}"
export BUILD_COMMAND

FQL_COMMAND=${FQL_ARGS[*]}
echo "FQL_COMMAND=${FQL_COMMAND}"
export FQL_COMMAND

DOCKER_TAG="${LATEST:-$(code_version)}_fdb.$(fdb_version)"
echo "DOCKER_TAG=${DOCKER_TAG}"
export DOCKER_TAG


# Run the requested commands.

if [[ -n "$IMAGE_BUILD" ]]; then
  (set -x; docker compose build build)
fi

if [[ -n "$BUILD_COMMAND" ]]; then
  (set -x; docker compose run --rm build /bin/sh -c "$BUILD_COMMAND")
fi

if [[ -n "$IMAGE_FQL" ]]; then
  (set -x; docker compose build fql)
fi

if [[ -n "$RUN_FQL" ]]; then
  (set -x; docker compose run --rm fql 'docker:docker@{fdb}:4500' "${FQL_ARGS[@]}")
fi
