diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 19cc938..ab97269 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -8,7 +8,7 @@ matrix: - windows bazel: - 6.5.0 - - 7.3.2 + - 7.4.0 tasks: test_module_bzlmod: diff --git a/WORKSPACE b/WORKSPACE index e69de29..b8d89d9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -0,0 +1,7 @@ +workspace(name = "rules_shell") + +load("//shell:repositories.bzl", "rules_shell_dependencies", "rules_shell_toolchains") + +rules_shell_dependencies() + +rules_shell_toolchains() diff --git a/shell/private/root_symlinks.bzl b/shell/private/root_symlinks.bzl new file mode 100644 index 0000000..76e0328 --- /dev/null +++ b/shell/private/root_symlinks.bzl @@ -0,0 +1,54 @@ +# Copyright 2024 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. + +"""Helper rule to preserve the legacy runfiles path for the runfiles lib.""" + +# Requires the private skip_conflict_checking parameter on ctx.runfiles, which +# is only available as of Bazel 7.4.0. We only use it when the native shell +# rules are not available. +ROOT_SYMLINKS_SUPPORTED = not hasattr(native, "sh_binary") + +def _single_file_or_fail(target): + files = target[DefaultInfo].files.to_list() + if len(files) != 1: + fail("Expected exactly one file in {}, got {}".format(target.label, files)) + return files[0] + +def _root_symlinks_impl(ctx): + runfiles = ctx.runfiles( + root_symlinks = { + path: _single_file_or_fail(target) + for target, path in ctx.attr.root_symlinks.items() + }, + # Adding root symlinks from Starlark usually enables conflict checking, + # but that would break backwards compatibility as it affects all + # runfiles, not just the symlinks. + skip_conflict_checking = True, + ) + return [ + DefaultInfo( + files = depset(), + runfiles = runfiles, + ), + ] + +root_symlinks = rule( + implementation = _root_symlinks_impl, + attrs = { + "root_symlinks": attr.label_keyed_string_dict( + allow_files = True, + mandatory = True, + ), + }, +) diff --git a/shell/runfiles/BUILD b/shell/runfiles/BUILD new file mode 100644 index 0000000..fb86e50 --- /dev/null +++ b/shell/runfiles/BUILD @@ -0,0 +1,22 @@ +load("//shell:sh_library.bzl", "sh_library") +load("//shell/private:root_symlinks.bzl", "ROOT_SYMLINKS_SUPPORTED", "root_symlinks") + +alias( + name = "runfiles", + actual = ":runfiles_impl" if ROOT_SYMLINKS_SUPPORTED else "@bazel_tools//tools/bash/runfiles", + visibility = ["//visibility:public"], +) + +sh_library( + name = "runfiles_impl", + data = [":runfiles_at_legacy_location"], + tags = ["manual"], +) + +root_symlinks( + name = "runfiles_at_legacy_location", + root_symlinks = { + "runfiles.bash": "bazel_tools/tools/bash/runfiles/runfiles.bash", + }, + tags = ["manual"], +) diff --git a/shell/runfiles/runfiles.bash b/shell/runfiles/runfiles.bash new file mode 100644 index 0000000..8e1f944 --- /dev/null +++ b/shell/runfiles/runfiles.bash @@ -0,0 +1,468 @@ +# 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 || source || ... 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 +# # shellcheck disable=SC1090 +# 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]:[/\\]" + # Windows paths are case insensitive and Bazel and MSYS2 capitalize differently, so we can't + # assume that all paths are in the same native case. + export _RLOCATION_GREP_CASE_INSENSITIVE_ARGS=-i + ;; +*) + # matches an absolute Unix path + export _RLOCATION_ISABS_PATTERN="^/[^/].*" + export _RLOCATION_GREP_CASE_INSENSITIVE_ARGS= + ;; +esac + +# Does not exit with a non-zero exit code if no match is found and performs a case-insensitive +# search on Windows. +function __runfiles_maybe_grep() { + grep $_RLOCATION_GREP_CASE_INSENSITIVE_ARGS "$@" || 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//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 raw_caller_path="${BASH_SOURCE[$idx]}" + # Make the caller path absolute if needed to handle the case where the script is run directly + # from bazel-bin, with working directory a subdirectory of bazel-bin. + if [[ "$raw_caller_path" =~ $_RLOCATION_ISABS_PATTERN ]]; then + local -r caller_path="$raw_caller_path" + else + local -r caller_path="$(cd $(dirname "$raw_caller_path"); pwd)/$(basename "$raw_caller_path")" + fi + 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/[.[\*^$]/\\&/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 + # The binary may also be run directly from bazel-bin or bazel-out. + local -r repository=$(echo "$normalized_caller_path" | __runfiles_maybe_grep -E -o '(^|/)(bazel-out/[^/]+/bin|bazel-bin)/external/[^/]+/' | tail -1 | awk -F/ '{print $(NF-1)}') + 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) (parsed exec path)" + 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 (parsed exec path)" + fi + echo "" + 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 + normalized_caller_path="$(echo "$caller_path" | sed 's|\\\\*|/|g')" + normalized_dir="$(echo "${RUNFILES_DIR%[\/]}" | sed 's|\\\\*|/|g')" + if [[ -n "${_RLOCATION_GREP_CASE_INSENSITIVE_ARGS}" ]]; then + # When comparing file paths insensitively, also normalize the case of the prefixes. + normalized_caller_path=$(echo "$normalized_caller_path" | tr '[:upper:]' '[:lower:]') + normalized_dir=$(echo "$normalized_dir" | tr '[:upper:]' '[:lower:]') + fi + 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). The binary may also be run + # directly from bazel-bin. + local -r repository=$(echo "$normalized_caller_path" | __runfiles_maybe_grep -E -o '(^|/)(bazel-out/[^/]+/bin|bazel-bin)/external/[^/]+/' | tail -1 | awk -F/ '{print $(NF-1)}') + 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) (parsed exec path)" + 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 (parsed exec path)" + 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 [[ -z "$rlocation_path" ]]; then + if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then + echo >&2 "ERROR[runfiles.bash]: runfiles_current_repository($idx): cannot determine repository for ($caller_path) since neither the runfiles directory (${RUNFILES_DIR:-}) nor the runfiles manifest (${RUNFILES_MANIFEST_FILE:-}) exist" + fi + return 1 + 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 + # If the rlocation path contains a space or newline, it needs to be prefixed + # with a space and spaces, newlines, and backslashes have to be escaped as + # \s, \n, and \b. + if [[ "$1" == *" "* || "$1" == *$'\n'* ]]; then + local search_prefix=" $(echo -n "$1" | sed 's/\\/\\b/g; s/ /\\s/g')" + search_prefix="${search_prefix//$'\n'/\\n}" + local escaped=true + if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then + echo >&2 "INFO[runfiles.bash]: rlocation($1): using escaped search prefix ($search_prefix)" + fi + else + local search_prefix="$1" + local escaped=false + fi + # The extra space below is added because cut counts from 1. + local trim_length=$(echo -n "$search_prefix " | wc -c | tr -d ' ') + # Escape the search prefix for use in the grep regex below *after* + # determining the trim length. + local result=$(__runfiles_maybe_grep -m1 "^$(echo -n "$search_prefix" | sed 's/[.[\*^$]/\\&/g') " "${RUNFILES_MANIFEST_FILE}" | cut -b "${trim_length}-") + 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" + if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then + echo >&2 "INFO[runfiles.bash]: rlocation($1): looking for prefix ($prefix)" + fi + if [[ "$prefix" == *" "* || "$prefix" == *$'\n'* ]]; then + search_prefix=" $(echo -n "$prefix" | sed 's/\\/\\b/g; s/ /\\s/g')" + search_prefix="${search_prefix//$'\n'/\\n}" + escaped=true + if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then + echo >&2 "INFO[runfiles.bash]: rlocation($1): using escaped search prefix ($search_prefix)" + fi + else + search_prefix="$prefix" + escaped=false + fi + # The extra space below is added because cut counts from 1. + trim_length=$(echo -n "$search_prefix " | wc -c) + prefix_result=$(__runfiles_maybe_grep -m1 "$(echo -n "$search_prefix" | sed 's/[.[\*^$]/\\&/g') " "${RUNFILES_MANIFEST_FILE}" | cut -b ${trim_length}-) + if [[ "$escaped" = true ]]; then + prefix_result="${prefix_result//\\n/$'\n'}" + prefix_result="${prefix_result//\\b/\\}" + fi + [[ -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 [[ "$escaped" = true ]]; then + result="${result//\\n/$'\n'}" + result="${result//\\b/\\}" + fi + 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) diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel deleted file mode 100644 index faa7068..0000000 --- a/tests/BUILD.bazel +++ /dev/null @@ -1,7 +0,0 @@ -load("//shell:sh_test.bzl", "sh_test") - -# Placeholder test to get the release action's `bazel test` to pass. -sh_test( - name = "test", - srcs = ["test.sh"], -) diff --git a/tests/bcr/BUILD b/tests/bcr/BUILD index f5f3edd..fb884dd 100644 --- a/tests/bcr/BUILD +++ b/tests/bcr/BUILD @@ -6,7 +6,7 @@ sh_library( name = "lib", srcs = ["lib.sh"], data = ["greeting.txt"], - deps = ["@bazel_tools//tools/bash/runfiles"], + deps = ["@rules_shell//shell/runfiles"], ) sh_binary( @@ -14,7 +14,7 @@ sh_binary( srcs = ["bin.sh"], deps = [ ":lib", - "@bazel_tools//tools/bash/runfiles", + "@rules_shell//shell/runfiles", ], ) @@ -22,5 +22,5 @@ sh_test( name = "test", srcs = ["test.sh"], data = [":bin"], - deps = ["@bazel_tools//tools/bash/runfiles"], + deps = ["@rules_shell//shell/runfiles"], ) diff --git a/tests/runfiles/BUILD b/tests/runfiles/BUILD new file mode 100644 index 0000000..dce2922 --- /dev/null +++ b/tests/runfiles/BUILD @@ -0,0 +1,7 @@ +load("//shell:sh_test.bzl", "sh_test") + +sh_test( + name = "runfiles_test", + srcs = ["runfiles_test.bash"], + deps = ["//shell/runfiles"], +) diff --git a/tests/runfiles/runfiles_test.bash b/tests/runfiles/runfiles_test.bash new file mode 100755 index 0000000..e50925b --- /dev/null +++ b/tests/runfiles/runfiles_test.bash @@ -0,0 +1,457 @@ +#!/bin/bash +# +# 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. +set -euo pipefail + +function _log_base() { + prefix=$1 + shift + echo >&2 "${prefix}[$(basename "${BASH_SOURCE[0]}"):${BASH_LINENO[1]} ($(date "+%H:%M:%S %z"))] $*" +} + +function fail() { + _log_base "FAILED" "$@" + exit 1 +} + +function log_fail() { + # non-fatal version of fail() + _log_base "FAILED" $* +} + +function log_info() { + _log_base "INFO" $* +} + +which uname >&/dev/null || fail "cannot locate GNU coreutils" + +case "$(uname -s | tr [:upper:] [:lower:])" in +msys*|mingw*|cygwin*) + function is_windows() { true; } + ;; +*) + function is_windows() { false; } + ;; +esac + +function find_runfiles_lib() { + # Unset existing definitions of the functions we want to test. + if type rlocation >&/dev/null; then + unset rlocation + unset runfiles_export_envvars + fi + + 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 + if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + echo "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash" + elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \ + "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2- + else + echo >&2 "ERROR: cannot find //shell/runfiles:runfiles.bash" + exit 1 + fi +} + +function test_rlocation_call_requires_no_envvars() { + export RUNFILES_DIR=mock/runfiles + export RUNFILES_MANIFEST_FILE= + export RUNFILES_MANIFEST_ONLY= + source "$runfiles_lib_path" || fail +} + +function test_rlocation_argument_validation() { + export RUNFILES_DIR= + export RUNFILES_MANIFEST_FILE= + export RUNFILES_MANIFEST_ONLY= + source "$runfiles_lib_path" + + # Test invalid inputs to make sure rlocation catches these. + if rlocation "../foo" >&/dev/null; then + fail + fi + if rlocation "foo/.." >&/dev/null; then + fail + fi + if rlocation "foo/../bar" >&/dev/null; then + fail + fi + if rlocation "./foo" >&/dev/null; then + fail + fi + if rlocation "foo/." >&/dev/null; then + fail + fi + if rlocation "foo/./bar" >&/dev/null; then + fail + fi + if rlocation "//foo" >&/dev/null; then + fail + fi + if rlocation "foo//" >&/dev/null; then + fail + fi + if rlocation "foo//bar" >&/dev/null; then + fail + fi + if rlocation "\\foo" >&/dev/null; then + fail + fi +} + +function test_rlocation_abs_path() { + export RUNFILES_DIR= + export RUNFILES_MANIFEST_FILE= + export RUNFILES_MANIFEST_ONLY= + source "$runfiles_lib_path" + + if is_windows; then + [[ "$(rlocation "c:/Foo" || echo failed)" == "c:/Foo" ]] || fail + [[ "$(rlocation "c:\\Foo" || echo failed)" == "c:\\Foo" ]] || fail + else + [[ "$(rlocation "/Foo" || echo failed)" == "/Foo" ]] || fail + fi +} + +function test_init_manifest_based_runfiles() { + local tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)" + cat > $tmpdir/foo.runfiles_manifest << EOF +a/b $tmpdir/c/d +e/f $tmpdir/g h +y $tmpdir/y +c/dir $tmpdir/dir +unresolved $tmpdir/unresolved + h/\si $tmpdir/ j k + h/\s\bi $tmpdir/ j k b + h/\n\bi $tmpdir/ \bnj k \na + dir\swith\sspaces $tmpdir/dir with spaces + space\snewline\nbackslash\b_dir $tmpdir/space newline\nbackslash\ba +EOF + mkdir "${tmpdir}/c" + mkdir "${tmpdir}/y" + mkdir -p "${tmpdir}/dir/deeply/nested" + touch "${tmpdir}/c/d" "${tmpdir}/g h" + touch "${tmpdir}/dir/file" + ln -s /does/not/exist "${tmpdir}/dir/unresolved" + touch "${tmpdir}/dir/deeply/nested/file" + touch "${tmpdir}/dir/deeply/nested/file with spaces" + ln -s /does/not/exist "${tmpdir}/unresolved" + touch "${tmpdir}/ j k" + touch "${tmpdir}/ j k b" + mkdir -p "${tmpdir}/dir with spaces/nested" + touch "${tmpdir}/dir with spaces/nested/file" + if ! is_windows; then + touch "${tmpdir}/ \nj k "$'\n'a + mkdir -p "${tmpdir}/space newline"$'\n'"backslash\a" + touch "${tmpdir}/space newline"$'\n'"backslash\a/f i\le" + fi + + export RUNFILES_DIR= + export RUNFILES_MANIFEST_FILE=$tmpdir/foo.runfiles_manifest + source "$runfiles_lib_path" + + [[ -z "$(rlocation a || echo failed)" ]] || fail + [[ -z "$(rlocation c/d || echo failed)" ]] || fail + [[ "$(rlocation a/b || echo failed)" == "$tmpdir/c/d" ]] || fail + [[ "$(rlocation e/f || echo failed)" == "$tmpdir/g h" ]] || fail + [[ "$(rlocation y || echo failed)" == "$tmpdir/y" ]] || fail + [[ -z "$(rlocation c || echo failed)" ]] || fail + [[ -z "$(rlocation c/di || echo failed)" ]] || fail + [[ "$(rlocation c/dir || echo failed)" == "$tmpdir/dir" ]] || fail + [[ "$(rlocation c/dir/file || echo failed)" == "$tmpdir/dir/file" ]] || fail + [[ -z "$(rlocation c/dir/unresolved || echo failed)" ]] || fail + [[ "$(rlocation c/dir/deeply/nested/file || echo failed)" == "$tmpdir/dir/deeply/nested/file" ]] || fail + [[ "$(rlocation "c/dir/deeply/nested/file with spaces" || echo failed)" == "$tmpdir/dir/deeply/nested/file with spaces" ]] || fail + [[ -z "$(rlocation unresolved || echo failed)" ]] || fail + [[ "$(rlocation "h/ i" || echo failed)" == "$tmpdir/ j k" ]] || fail + [[ "$(rlocation "h/ \i" || echo failed)" == "$tmpdir/ j k b" ]] || fail + [[ "$(rlocation "dir with spaces" || echo failed)" == "$tmpdir/dir with spaces" ]] || fail + [[ "$(rlocation "dir with spaces/nested/file" || echo failed)" == "$tmpdir/dir with spaces/nested/file" ]] || fail + if ! is_windows; then + [[ "$(rlocation $'h/\n\\i' || echo failed)" == "$tmpdir/ \nj k "$'\n'a ]] || fail + [[ "$(rlocation "space newline"$'\n'"backslash\_dir/f i\le" || echo failed)" == "${tmpdir}/space newline"$'\n'"backslash\a/f i\le" ]] || fail + fi + + rm -r "$tmpdir/c/d" "$tmpdir/g h" "$tmpdir/y" "$tmpdir/dir" "$tmpdir/unresolved" "$tmpdir/ j k" "$tmpdir/dir with spaces" + if ! is_windows; then + rm -r "$tmpdir/ \nj k "$'\n'a "${tmpdir}/space newline"$'\n'"backslash\a" + [[ -z "$(rlocation $'h/\n\\i' || echo failed)" ]] || fail + [[ -z "$(rlocation "space newline"$'\n'"backslash\_dir/f i\le" || echo failed)" ]] || fail + fi + [[ -z "$(rlocation a/b || echo failed)" ]] || fail + [[ -z "$(rlocation e/f || echo failed)" ]] || fail + [[ -z "$(rlocation y || echo failed)" ]] || fail + [[ -z "$(rlocation c/dir || echo failed)" ]] || fail + [[ -z "$(rlocation c/dir/file || echo failed)" ]] || fail + [[ -z "$(rlocation c/dir/deeply/nested/file || echo failed)" ]] || fail + [[ -z "$(rlocation "h/ i" || echo failed)" ]] || fail + [[ -z "$(rlocation "dir with spaces" || echo failed)" ]] || fail + [[ -z "$(rlocation "dir with spaces/nested/file" || echo failed)" ]] || fail +} + +function test_manifest_based_envvars() { + local tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)" + echo "a b" > $tmpdir/foo.runfiles_manifest + + export RUNFILES_DIR= + export RUNFILES_MANIFEST_FILE=$tmpdir/foo.runfiles_manifest + mkdir -p $tmpdir/foo.runfiles + source "$runfiles_lib_path" + + runfiles_export_envvars + [[ "${RUNFILES_DIR:-}" == "$tmpdir/foo.runfiles" ]] || fail + [[ "${RUNFILES_MANIFEST_FILE:-}" == "$tmpdir/foo.runfiles_manifest" ]] || fail +} + +function test_init_directory_based_runfiles() { + local tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)" + + export RUNFILES_DIR=${tmpdir}/mock/runfiles + export RUNFILES_MANIFEST_FILE= + source "$runfiles_lib_path" + + mkdir -p "$RUNFILES_DIR/a" + touch "$RUNFILES_DIR/a/b" "$RUNFILES_DIR/c d" + [[ "$(rlocation a || echo failed)" == "$RUNFILES_DIR/a" ]] || fail + [[ "$(rlocation c/d || echo failed)" == failed ]] || fail + [[ "$(rlocation a/b || echo failed)" == "$RUNFILES_DIR/a/b" ]] || fail + [[ "$(rlocation "c d" || echo failed)" == "$RUNFILES_DIR/c d" ]] || fail + [[ "$(rlocation "c" || echo failed)" == failed ]] || fail + rm -r "$RUNFILES_DIR/a" "$RUNFILES_DIR/c d" + [[ "$(rlocation a || echo failed)" == failed ]] || fail + [[ "$(rlocation a/b || echo failed)" == failed ]] || fail + [[ "$(rlocation "c d" || echo failed)" == failed ]] || fail +} + +function test_directory_based_runfiles_with_repo_mapping_from_main() { + local tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)" + + export RUNFILES_DIR=${tmpdir}/mock/runfiles + mkdir -p "$RUNFILES_DIR" + cat > "$RUNFILES_DIR/_repo_mapping" < "$RUNFILES_DIR/_repo_mapping" < "$tmpdir/foo.repo_mapping" < "$RUNFILES_MANIFEST_FILE" << EOF +_repo_mapping $tmpdir/foo.repo_mapping +config.json $tmpdir/config.json +protobuf+3.19.2/foo/runfile $tmpdir/protobuf+3.19.2/foo/runfile +_main/bar/runfile $tmpdir/_main/bar/runfile +protobuf+3.19.2/bar/dir $tmpdir/protobuf+3.19.2/bar/dir +EOF + source "$runfiles_lib_path" + + mkdir -p "$tmpdir/_main/bar" + touch "$tmpdir/_main/bar/runfile" + mkdir -p "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted" + touch "$tmpdir/protobuf+3.19.2/bar/dir/file" + touch "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" + mkdir -p "$tmpdir/protobuf+3.19.2/foo" + touch "$tmpdir/protobuf+3.19.2/foo/runfile" + touch "$tmpdir/config.json" + + [[ "$(rlocation "my_module/bar/runfile" "" || echo failed)" == "$tmpdir/_main/bar/runfile" ]] || fail + [[ "$(rlocation "my_workspace/bar/runfile" "" || echo failed)" == "$tmpdir/_main/bar/runfile" ]] || fail + [[ "$(rlocation "my_protobuf/foo/runfile" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/foo/runfile" ]] || fail + [[ "$(rlocation "my_protobuf/bar/dir" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir" ]] || fail + [[ "$(rlocation "my_protobuf/bar/dir/file" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/file" ]] || fail + [[ "$(rlocation "my_protobuf/bar/dir/de eply/nes ted/fi+le" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" ]] || fail + + [[ -z "$(rlocation "protobuf/foo/runfile" "" || echo failed)" ]] || fail + [[ -z "$(rlocation "protobuf/bar/dir/dir/de eply/nes ted/fi+le" "" || echo failed)" ]] || fail + + [[ "$(rlocation "_main/bar/runfile" "" || echo failed)" == "$tmpdir/_main/bar/runfile" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/foo/runfile" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/foo/runfile" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/bar/dir" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/bar/dir/file" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/file" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" "" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" ]] || fail + + [[ "$(rlocation "config.json" "" || echo failed)" == "$tmpdir/config.json" ]] || fail +} + +function test_manifest_based_runfiles_with_repo_mapping_from_other_repo() { + local tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)" + + cat > "$tmpdir/foo.repo_mapping" < "$RUNFILES_MANIFEST_FILE" << EOF +_repo_mapping $tmpdir/foo.repo_mapping +config.json $tmpdir/config.json +protobuf+3.19.2/foo/runfile $tmpdir/protobuf+3.19.2/foo/runfile +_main/bar/runfile $tmpdir/_main/bar/runfile +protobuf+3.19.2/bar/dir $tmpdir/protobuf+3.19.2/bar/dir +EOF + source "$runfiles_lib_path" + + mkdir -p "$tmpdir/_main/bar" + touch "$tmpdir/_main/bar/runfile" + mkdir -p "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted" + touch "$tmpdir/protobuf+3.19.2/bar/dir/file" + touch "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" + mkdir -p "$tmpdir/protobuf+3.19.2/foo" + touch "$tmpdir/protobuf+3.19.2/foo/runfile" + touch "$tmpdir/config.json" + + [[ "$(rlocation "protobuf/foo/runfile" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/foo/runfile" ]] || fail + [[ "$(rlocation "protobuf/bar/dir" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir" ]] || fail + [[ "$(rlocation "protobuf/bar/dir/file" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/file" ]] || fail + [[ "$(rlocation "protobuf/bar/dir/de eply/nes ted/fi+le" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" ]] || fail + + [[ -z "$(rlocation "my_module/bar/runfile" "protobuf+3.19.2" || echo failed)" ]] || fail + [[ -z "$(rlocation "my_protobuf/bar/dir/de eply/nes ted/fi+le" "protobuf+3.19.2" || echo failed)" ]] || fail + + [[ "$(rlocation "_main/bar/runfile" "protobuf+3.19.2" || echo failed)" == "$tmpdir/_main/bar/runfile" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/foo/runfile" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/foo/runfile" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/bar/dir" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/bar/dir/file" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/file" ]] || fail + [[ "$(rlocation "protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" "protobuf+3.19.2" || echo failed)" == "$tmpdir/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le" ]] || fail + + [[ "$(rlocation "config.json" "protobuf+3.19.2" || echo failed)" == "$tmpdir/config.json" ]] || fail +} + +function test_directory_based_envvars() { + export RUNFILES_DIR=mock/runfiles + export RUNFILES_MANIFEST_FILE= + source "$runfiles_lib_path" + + runfiles_export_envvars + [[ "${RUNFILES_DIR:-}" == "mock/runfiles" ]] || fail + [[ -z "${RUNFILES_MANIFEST_FILE:-}" ]] || fail +} + +function main() { + local -r manifest_file="${RUNFILES_MANIFEST_FILE:-}" + local -r dir="${RUNFILES_DIR:-}" + local -r runfiles_lib_path=$(find_runfiles_lib) + + local -r tests=$(declare -F | grep " -f test" | awk '{print $3}') + local failure=0 + for t in $tests; do + export RUNFILES_MANIFEST_FILE="$manifest_file" + export RUNFILES_DIR="$dir" + if ! ($t); then + failure=1 + fi + done + return $failure +} + +main diff --git a/tests/test.sh b/tests/test.sh deleted file mode 100755 index f1f641a..0000000 --- a/tests/test.sh +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env bash