Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 106 additions & 17 deletions shell/runfiles/runfiles.bash
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,90 @@ function __runfiles_escape_grep() {
}
export -f __runfiles_escape_grep

# Lexically resolves the "." and ".." segments in the given rlocation path.
# Fails if the path is empty or would escape the runfiles root.
function __runfiles_normalize_rlocation_path() {
local rest="$1"
local normalized=
local segment
while [[ -n "$rest" ]]; do
if [[ "$rest" == */* ]]; then
segment="${rest%%/*}"
rest="${rest#*/}"
else
segment="$rest"
rest=
fi
case "$segment" in
"" | ".")
;;
"..")
if [[ "$normalized" == */* ]]; then
normalized="${normalized%/*}"
elif [[ -n "$normalized" ]]; then
normalized=
else
return 1
fi
;;
*)
normalized="${normalized:+$normalized/}$segment"
;;
esac
done
[[ -n "$normalized" ]] || return 1
echo "$normalized"
}
export -f __runfiles_normalize_rlocation_path

# Resolves the target of a runfiles manifest entry to a path in the local file system.
#
# Bazel stores the target of an unresolved symlink (ctx.actions.declare_symlink) in the manifest
# verbatim, so unlike all other targets it may be a relative path. In a materialized runfiles
# directory the entry is a symlink with that very target, which the file system resolves relative to
# the directory containing the symlink. A relative target thus has to be interpreted as an rlocation
# path relative to the directory of the entry and looked up in the manifest again.
#
# Arguments:
# $1: the rlocation path of the manifest entry
# $2: the target of the manifest entry
# $3: the path to append to the target, if any
# $4: the current lookup depth, used to break out of cycles of unresolved symlinks
# Prints the resolved path if it exists and the empty string otherwise.
function __runfiles_resolve_manifest_target() {
if [[ "$2" =~ $_RLOCATION_ISABS_PATTERN || "$2" == /* ]]; then
local -r resolved="$2$3"
if [[ -e "$resolved" ]]; then
if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
echo >&2 "INFO[runfiles.bash]: rlocation($1$3): found in manifest as ($resolved)"
fi
echo "$resolved"
else
if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
echo >&2 "INFO[runfiles.bash]: rlocation($1$3): found in manifest as ($resolved), but file does not exist"
fi
echo ""
fi
return 0
fi

local entry_dir="${1%/*}"
[[ "$entry_dir" == "$1" ]] && entry_dir=
local target_rlocation_path
target_rlocation_path=$(__runfiles_normalize_rlocation_path "${entry_dir:+$entry_dir/}$2$3") || {
if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
echo >&2 "ERROR[runfiles.bash]: rlocation($1$3): unresolved symlink target ($2) points outside the runfiles tree"
fi
echo ""
return 0
}
if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
echo >&2 "INFO[runfiles.bash]: rlocation($1$3): unresolved symlink target ($2) resolves to ($target_rlocation_path)"
fi
runfiles_rlocation_checked "$target_rlocation_path" "$(($4 + 1))"
}
export -f __runfiles_resolve_manifest_target

# 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
Expand Down Expand Up @@ -374,6 +458,16 @@ 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.
# The optional second argument is the current lookup depth, which only differs from zero while
# following the target of an unresolved symlink.
local -r depth="${2:-0}"
if [[ "$depth" -gt 32 ]]; then
if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
echo >&2 "ERROR[runfiles.bash]: rlocation($1): too many levels of symbolic links"
fi
echo ""
return 0
fi
# The manifest takes precedence over the runfiles directory: whether the directory is populated
# is a property of the execution of the action or test, which is not known at analysis time, so
# the directory may exist but contain the stale contents of a previous execution. If the manifest
Expand Down Expand Up @@ -438,11 +532,16 @@ function runfiles_rlocation_checked() {
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
if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
echo >&2 "INFO[runfiles.bash]: rlocation($1): found in manifest via prefix ($prefix)"
fi
local candidate
# The trailing marker prevents command substitution from stripping a newline that is part
# of the resolved path.
candidate=$(__runfiles_resolve_manifest_target \
"$prefix" "$prefix_result" "${1#"${prefix}"}" "$depth"; echo -n x)
candidate="${candidate%$'\n'x}"
if [[ -n "$candidate" ]]; then
echo "$candidate"
return 0
fi
Expand All @@ -457,7 +556,7 @@ function runfiles_rlocation_checked() {
# 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"
echo >&2 "INFO[runfiles.bash]: rlocation($1): prefix ($prefix) did not resolve, not retrying with a shorter one"
fi
break
done
Expand All @@ -470,17 +569,7 @@ function runfiles_rlocation_checked() {
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
__runfiles_resolve_manifest_target "$1" "$result" "" "$depth"
fi
elif [[ -e "${RUNFILES_DIR:-/dev/null}/$1" ]]; then
if [[ "${RUNFILES_LIB_DEBUG:-}" == 1 ]]; then
Expand Down
106 changes: 106 additions & 0 deletions tests/runfiles/runfiles_test.bash
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,112 @@ EOF
[[ -z "$(rlocation "dir with spaces/nested/file" || echo failed)" ]] || fail
}

# Writes a runfiles layout containing unresolved symlinks with relative targets, both as a manifest
# pointing at the original files and as a materialized runfiles directory.
function write_relative_symlink_target_layout() {
local -r tmpdir="$1"
local -r dir="$tmpdir/foo.runfiles"

mkdir -p "$tmpdir/original/dir/deeply/nested"
echo file > "$tmpdir/original/file"
echo nested_file > "$tmpdir/original/dir/deeply/nested/file"
# Lies next to the runfiles directory and is thus only reachable through a relative target that
# leaves the runfiles tree.
echo outside > "$tmpdir/outside"

cat > "$tmpdir/foo.runfiles_manifest" << EOF
_main/pkg/file $tmpdir/original/file
_main/pkg/dir $tmpdir/original/dir
_main/pkg/link ../pkg/file
_main/pkg/nested/link ../../pkg/link
_main/pkg/dir_link ./dir
_main/pkg/dangling ../pkg/missing
_main/pkg/escaping ../../../outside
_main/pkg/loop_a loop_b
_main/pkg/loop_b loop_a
_main/pkg/link\swith\sspaces ../pkg/file
EOF

mkdir -p "$dir/_main/pkg/nested"
ln -s "$tmpdir/original/file" "$dir/_main/pkg/file"
ln -s "$tmpdir/original/dir" "$dir/_main/pkg/dir"
ln -s ../pkg/file "$dir/_main/pkg/link"
ln -s ../../pkg/link "$dir/_main/pkg/nested/link"
ln -s ./dir "$dir/_main/pkg/dir_link"
ln -s ../pkg/missing "$dir/_main/pkg/dangling"
ln -s ../../../outside "$dir/_main/pkg/escaping"
ln -s loop_b "$dir/_main/pkg/loop_a"
ln -s loop_a "$dir/_main/pkg/loop_b"
ln -s ../pkg/file "$dir/_main/pkg/link with spaces"
}

# Asserts that the given rlocation path resolves to a file with the given contents. Only the
# contents are compared since the path itself necessarily differs between the two lookup modes: a
# materialized runfiles directory resolves to the entry in that directory, not to the file it
# points at.
function assert_rlocation_contents() {
local -r resolved="$(rlocation "$1" || echo failed)"
[[ -f "$resolved" ]] || fail "$1 did not resolve to a file, got: $resolved"
[[ "$(cat "$resolved")" == "$2" ]] || fail "$1 resolved to $resolved with unexpected contents"
}

# Asserts that the given rlocation path does not resolve, which rlocation reports as an empty result
# when it uses the manifest and as a non-zero exit code when it uses the runfiles directory (see the
# FIXME on runfiles_rlocation_checked).
function assert_no_rlocation() {
local -r resolved="$(rlocation "$1" || echo failed)"
[[ -z "$resolved" || "$resolved" == failed ]] || fail "$1 unexpectedly resolved to $resolved"
}

# The lookups whose outcome must not depend on whether the manifest or the runfiles directory backs
# them: resolving a relative target against the manifest has to arrive at the same file that the
# file system arrives at when resolving the corresponding symlink in the runfiles directory.
function assert_relative_symlink_target_lookups() {
assert_rlocation_contents _main/pkg/link file
assert_rlocation_contents _main/pkg/nested/link file
assert_rlocation_contents "_main/pkg/link with spaces" file
# A relative target that resolves to a directory runfile also resolves paths underneath it.
[[ -d "$(rlocation _main/pkg/dir_link || echo failed)" ]] || fail
assert_rlocation_contents _main/pkg/dir_link/deeply/nested/file nested_file
# A target that doesn't resolve to an existing file behaves like a missing runfile.
assert_no_rlocation _main/pkg/dangling
assert_no_rlocation _main/pkg/dir_link/does/not/exist
# A cycle terminates instead of looping forever.
assert_no_rlocation _main/pkg/loop_a
}

function test_manifest_based_relative_symlink_targets() {
local -r tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)"
write_relative_symlink_target_layout "$tmpdir"

export RUNFILES_DIR=
export RUNFILES_MANIFEST_FILE=$tmpdir/foo.runfiles_manifest
source "$runfiles_lib_path"

assert_relative_symlink_target_lookups
# The one lookup the manifest cannot reproduce: a relative target that leaves the runfiles tree
# can only be resolved against a materialized runfiles directory, whose existence the manifest
# does not imply. Bazel does not generate such a runfile.
assert_no_rlocation _main/pkg/escaping
}

function test_directory_based_relative_symlink_targets() {
# MSYS2 may materialize symlinks as copies, which does not preserve relative targets.
if is_windows; then
return 0
fi

local -r tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)"
write_relative_symlink_target_layout "$tmpdir"

export RUNFILES_DIR="$tmpdir/foo.runfiles"
export RUNFILES_MANIFEST_FILE=
source "$runfiles_lib_path"

assert_relative_symlink_target_lookups
assert_rlocation_contents _main/pkg/escaping outside
}

function test_manifest_based_envvars() {
local tmpdir="$(mktemp -d $TEST_TMPDIR/tmp.XXXXXXXX)"
echo "a b" > $tmpdir/foo.runfiles_manifest
Expand Down