# Copyright 2018 The Bazel Authors. All rights reserved.
#
# 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.

# Runfiles lookup library for Bazel-built Bash binaries and tests, version 3.
#
# VERSION HISTORY:
# - version 3: Fixes a bug in the init code on macOS and makes the library aware
#              of Bzlmod repository mappings.
#   Features:
#     - With Bzlmod enabled, rlocation now takes the repository mapping of the
#       Bazel repository containing the calling script into account when
#       looking up runfiles. The new, optional second argument to rlocation can
#       be used to specify the canonical name of the Bazel repository to use
#       instead of this default. The new runfiles_current_repository function
#       can be used to obtain the canonical name of the N-th caller's Bazel
#       repository.
#   Fixed:
#     - Sourcing a shell script that contains the init code from a shell script
#       that itself contains the init code no longer fails on macOS.
#   Compatibility:
#     - The init script and the runfiles library are backwards and forwards
#       compatible with version 2.
# - version 2: Shorter init code.
#   Features:
#     - "set -euo pipefail" only at end of init code.
#       "set -e" breaks the source <path1> || source <path2> || ... scheme on
#       macOS, because it terminates if path1 does not exist.
#     - Not exporting any environment variables in init code.
#       This is now done in runfiles.bash itself.
#   Compatibility:
#     - The v1 init code can load the v2 library, i.e. if you have older source
#       code (still using v1 init) then you can build it with newer Bazel (which
#       contains the v2 library).
#     - The reverse is not true: the v2 init code CANNOT load the v1 library,
#       i.e. if your project (or any of its external dependencies) use v2 init
#       code, then you need a newer Bazel version (which contains the v2
#       library).
# - version 1: Original Bash runfiles library.
#
# ENVIRONMENT:
# - If RUNFILES_LIB_DEBUG=1 is set, the script will print diagnostic messages to
#   stderr.
#
# USAGE:
# 1.  Depend on this runfiles library from your build rule:
#
#       sh_binary(
#           name = "my_binary",
#           ...
#           deps = ["@bazel_tools//tools/bash/runfiles"],
#       )
#
# 2.  Source the runfiles library.
#
#     The runfiles library itself defines rlocation which you would need to look
#     up the library's runtime location, thus we have a chicken-and-egg problem.
#     Insert the following code snippet to the top of your main script:
#
#       # --- begin runfiles.bash initialization v3 ---
#       # Copy-pasted from the Bazel Bash runfiles library v3.
#       set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
#       source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
#         source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
#         source "$0.runfiles/$f" 2>/dev/null || \
#         source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
#         source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
#         { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
#       # --- end runfiles.bash initialization v3 ---
#
#
# 3.  Use rlocation to look up runfile paths.
#
#       cat "$(rlocation my_workspace/path/to/my/data.txt)"
#

if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
  if [[ -f "$0.runfiles_manifest" ]]; then
    export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
  elif [[ -f "$0.runfiles/MANIFEST" ]]; then
    export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
  elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
    export RUNFILES_DIR="$0.runfiles"
  fi
fi

case "$(uname -s | tr [:upper:] [:lower:])" in
msys*|mingw*|cygwin*)
  # matches an absolute Windows path
  export _RLOCATION_ISABS_PATTERN="^[a-zA-Z]:[/\\]"
  ;;
*)
  # matches an absolute Unix path
  export _RLOCATION_ISABS_PATTERN="^/[^/].*"
  ;;
esac

# Does not exit with a non-zero exit code if no match is found.
function __runfiles_maybe_grep() {
  grep "$@" || test $? = 1;
}
export -f __runfiles_maybe_grep

# Prints to stdout the runtime location of a data-dependency.
# The optional second argument can be used to specify the canonical name of the
# repository whose repository mapping should be used to resolve the repository
# part of the provided path. If not specified, the repository of the caller is
# used.
function rlocation() {
  if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
    echo >&2 "INFO[runfiles.bash]: rlocation($1): start"
  fi
  if [[ "$1" =~ $_RLOCATION_ISABS_PATTERN ]]; then
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "INFO[runfiles.bash]: rlocation($1): absolute path, return"
    fi
    # If the path is absolute, print it as-is.
    echo "$1"
    return 0
  elif [[ "$1" == ../* || "$1" == */.. || "$1" == ./* || "$1" == */./* || "$1" == "*/." || "$1" == *//* ]]; then
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "ERROR[runfiles.bash]: rlocation($1): path is not normalized"
    fi
    return 1
  elif [[ "$1" == \\* ]]; then
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "ERROR[runfiles.bash]: rlocation($1): absolute path without" \
               "drive name"
    fi
    return 1
  fi

  if [[ -f "$RUNFILES_REPO_MAPPING" ]]; then
    local -r target_repo_apparent_name=$(echo "$1" | cut -d / -f 1)
     # Use -s to get an empty remainder if the argument does not contain a slash.
    # The repo mapping should not be applied to single segment paths, which may
    # be root symlinks.
    local -r remainder=$(echo "$1" | cut -s -d / -f 2-)
    if [[ -n "$remainder" ]]; then
      if [[ -z "${2+x}" ]]; then
        local -r source_repo=$(runfiles_current_repository 2)
      else
        local -r source_repo=$2
      fi
      if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
        echo >&2 "INFO[runfiles.bash]: rlocation($1): looking up canonical name for ($target_repo_apparent_name) from ($source_repo) in ($RUNFILES_REPO_MAPPING)"
      fi
      local -r target_repo=$(__runfiles_maybe_grep -m1 "^$source_repo,$target_repo_apparent_name," "$RUNFILES_REPO_MAPPING" | cut -d , -f 3)
      if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
        echo >&2 "INFO[runfiles.bash]: rlocation($1): canonical name of target repo is ($target_repo)"
      fi
      if [[ -n "$target_repo" ]]; then
        local -r rlocation_path="$target_repo/$remainder"
      else
        local -r rlocation_path="$1"
      fi
    else
      local -r rlocation_path="$1"
    fi
  else
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "INFO[runfiles.bash]: rlocation($1): not using repository mapping ($RUNFILES_REPO_MAPPING) since it does not exist"
    fi
    local -r rlocation_path="$1"
  fi

  runfiles_rlocation_checked "$rlocation_path"
}
export -f rlocation

# Exports the environment variables that subprocesses need in order to use
# runfiles.
# If a subprocess is a Bazel-built binary rule that also uses the runfiles
# libraries under @bazel_tools//tools/<lang>/runfiles, then that binary needs
# these envvars in order to initialize its own runfiles library.
function runfiles_export_envvars() {
  if [[ ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" \
        && ! -d "${RUNFILES_DIR:-/dev/null}" ]]; then
    return 1
  fi

  if [[ ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
    if [[ -f "$RUNFILES_DIR/MANIFEST" ]]; then
      export RUNFILES_MANIFEST_FILE="$RUNFILES_DIR/MANIFEST"
    elif [[ -f "${RUNFILES_DIR}_manifest" ]]; then
      export RUNFILES_MANIFEST_FILE="${RUNFILES_DIR}_manifest"
    else
      export RUNFILES_MANIFEST_FILE=
    fi
  elif [[ ! -d "${RUNFILES_DIR:-/dev/null}" ]]; then
    if [[ "$RUNFILES_MANIFEST_FILE" == */MANIFEST \
          && -d "${RUNFILES_MANIFEST_FILE%/MANIFEST}" ]]; then
      export RUNFILES_DIR="${RUNFILES_MANIFEST_FILE%/MANIFEST}"
      export JAVA_RUNFILES="$RUNFILES_DIR"
    elif [[ "$RUNFILES_MANIFEST_FILE" == *_manifest \
          && -d "${RUNFILES_MANIFEST_FILE%_manifest}" ]]; then
      export RUNFILES_DIR="${RUNFILES_MANIFEST_FILE%_manifest}"
      export JAVA_RUNFILES="$RUNFILES_DIR"
    else
      export RUNFILES_DIR=
    fi
  fi
}
export -f runfiles_export_envvars

# Returns the canonical name of the Bazel repository containing the script that
# calls this function.
# The optional argument N, which defaults to 1, can be used to return the
# canonical name of the N-th caller instead.
#
# Note: This function only works correctly with Bzlmod enabled. Without Bzlmod,
# its return value is ignored if passed to rlocation.
function runfiles_current_repository() {
  local -r idx=${1:-1}
  local -r caller_path="${BASH_SOURCE[$idx]}"
  if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
    echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): caller's path is ($caller_path)"
  fi

  local rlocation_path=

  # If the runfiles manifest exists, search for an entry with target the caller's path.
  if [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
    # Escape $caller_path for use in the grep regex below. Also replace \ with / since the manifest
    # uses / as the path separator even on Windows.
    local -r normalized_caller_path="$(echo "$caller_path" | sed 's|\\\\*|/|g')"
    local -r escaped_caller_path="$(echo "$normalized_caller_path" | sed 's/[^-A-Za-z0-9_/]/\\&/g')"
    rlocation_path=$(__runfiles_maybe_grep -m1 "^[^ ]* ${escaped_caller_path}$" "${RUNFILES_MANIFEST_FILE}" | cut -d ' ' -f 1)
    if [[ -z "$rlocation_path" ]]; then
      if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
        echo >&2 "ERROR[runfiles.bash]: runfiles_current_repository($idx): ($normalized_caller_path) is not the target of an entry in the runfiles manifest ($RUNFILES_MANIFEST_FILE)"
      fi
      return 1
    else
      if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
        echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($normalized_caller_path) is the target of ($rlocation_path) in the runfiles manifest"
      fi
    fi
  fi

  # If the runfiles directory exists, check if the caller's path is of the form
  # $RUNFILES_DIR/rlocation_path and if so, set $rlocation_path.
  if [[ -z "$rlocation_path" && -d "${RUNFILES_DIR:-/dev/null}" ]]; then
    local -r normalized_caller_path="$(echo "$caller_path" | sed 's|\\\\*|/|g')"
    local -r normalized_dir="$(echo "${RUNFILES_DIR%[\/]}" | sed 's|\\\\*|/|g')"
    if [[ "$normalized_caller_path" == "$normalized_dir"/* ]]; then
      rlocation_path=${normalized_caller_path:${#normalized_dir}}
      rlocation_path=${rlocation_path:1}
    fi
    if [[ -z "$rlocation_path" ]]; then
      if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
        echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($normalized_caller_path) does not lie under the runfiles directory ($normalized_dir)"
      fi
      # The only shell script that is not executed from the runfiles directory (if it is populated)
      # is the sh_binary entrypoint. Parse its path under the execroot, using the last match to
      # allow for nested execroots (e.g. in Bazel integration tests).
      local -r repository=$(echo "$normalized_caller_path" | __runfiles_maybe_grep -E -o '(^|/)bazel-out/[^/]+/bin/external/[^/]+/' | tail -1 | rev | cut -d / -f 2 | rev)
      if [[ -n "$repository" ]]; then
        if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
          echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($normalized_caller_path) lies in repository ($repository)"
        fi
        echo "$repository"
      else
        if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
          echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($normalized_caller_path) lies in the main repository"
        fi
        echo ""
      fi
      return 0
    else
      if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
        echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($caller_path) has path ($rlocation_path) relative to the runfiles directory ($RUNFILES_DIR)"
      fi
    fi
  fi

  if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
    echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($caller_path) corresponds to rlocation path ($rlocation_path)"
  fi
  # Normalize the rlocation path to be of the form repo/pkg/file.
  rlocation_path=${rlocation_path#_main/external/}
  rlocation_path=${rlocation_path#_main/../}
  local -r repository=$(echo "$rlocation_path" | cut -d / -f 1)
  if [[ "$repository" == _main ]]; then
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($rlocation_path) lies in the main repository"
    fi
    echo ""
  else
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "INFO[runfiles.bash]: runfiles_current_repository($idx): ($rlocation_path) lies in repository ($repository)"
    fi
    echo "$repository"
  fi
}
export -f runfiles_current_repository

function runfiles_rlocation_checked() {
  # FIXME: If the runfiles lookup fails, the exit code of this function is 0 if
  #  and only if the runfiles manifest exists. In particular, the exit code
  #  behavior is not consistent across platforms.
  if [[ -e "${RUNFILES_DIR:-/dev/null}/$1" ]]; then
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "INFO[runfiles.bash]: rlocation($1): found under RUNFILES_DIR ($RUNFILES_DIR), return"
    fi
    echo "${RUNFILES_DIR}/$1"
  elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "INFO[runfiles.bash]: rlocation($1): looking in RUNFILES_MANIFEST_FILE ($RUNFILES_MANIFEST_FILE)"
    fi
    local -r result=$(__runfiles_maybe_grep -m1 "^$1 " "${RUNFILES_MANIFEST_FILE}" | cut -d ' ' -f 2-)
    if [[ -z "$result" ]]; then
      # If path references a runfile that lies under a directory that itself
      # is a runfile, then only the directory is listed in the manifest. Look
      # up all prefixes of path in the manifest and append the relative path
      # from the prefix if there is a match.
      local prefix="$1"
      local prefix_result=
      local new_prefix=
      while true; do
        new_prefix="${prefix%/*}"
        [[ "$new_prefix" == "$prefix" ]] && break
        prefix="$new_prefix"
        prefix_result=$(__runfiles_maybe_grep -m1 "^$prefix " "${RUNFILES_MANIFEST_FILE}" | cut -d ' ' -f 2-)
        [[ -z "$prefix_result" ]] && continue
        local -r candidate="${prefix_result}${1#"${prefix}"}"
        if [[ -e "$candidate" ]]; then
          if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
            echo >&2 "INFO[runfiles.bash]: rlocation($1): found in manifest as ($candidate) via prefix ($prefix)"
          fi
          echo "$candidate"
          return 0
        fi
        # At this point, the manifest lookup of prefix has been successful,
        # but the file at the relative path given by the suffix does not
        # exist. We do not continue the lookup with a shorter prefix for two
        # reasons:
        # 1. Manifests generated by Bazel never contain a path that is a
        #    prefix of another path.
        # 2. Runfiles libraries for other languages do not check for file
        #    existence and would have returned the non-existent path. It seems
        #    better to return no path rather than a potentially different,
        #    non-empty path.
        if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
          echo >&2 "INFO[runfiles.bash]: rlocation($1): found in manifest as ($candidate) via prefix ($prefix), but file does not exist"
        fi
        break
      done
      if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
        echo >&2 "INFO[runfiles.bash]: rlocation($1): not found in manifest"
      fi
      echo ""
    else
      if [[ -e "$result" ]]; then
        if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
          echo >&2 "INFO[runfiles.bash]: rlocation($1): found in manifest as ($result)"
        fi
        echo "$result"
      else
        if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
          echo >&2 "INFO[runfiles.bash]: rlocation($1): found in manifest as ($result), but file does not exist"
        fi
        echo ""
      fi
    fi
  else
    if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
      echo >&2 "ERROR[runfiles.bash]: cannot look up runfile \"$1\" " \
               "(RUNFILES_DIR=\"${RUNFILES_DIR:-}\"," \
               "RUNFILES_MANIFEST_FILE=\"${RUNFILES_MANIFEST_FILE:-}\")"
    fi
    return 1
  fi
}
export -f runfiles_rlocation_checked

export RUNFILES_REPO_MAPPING=$(runfiles_rlocation_checked _repo_mapping 2> /dev/null)
