blob: 9788fa49a28a161096731f73e0505d739642415f [file] [log] [blame]
# 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 v2 ---
# # Copy-pasted from the Bazel Bash runfiles library v2.
# 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 v2 ---
#
#
# 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)
local -r remainder=$(echo "$1" | cut -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)