blob: e4d756c08d4aa9acfa90669ae6e7561fc123a0f7 [file] [log] [blame]
#!/bin/bash
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# See usage() for description.
script="$0"
script_dir="$(dirname "$script")"
# The project_root must cover all inputs, prebuilt tools, and build outputs.
# This should point to $FUCHSIA_DIR for the Fuchsia project.
# ../../ because this script lives in build/rbe.
# The value is an absolute path.
project_root="$(readlink -f "$script_dir"/../..)"
function usage() {
cat <<EOF
This wrapper script helps dispatch a remote Rust compilation action by inferring
the necessary inputs for uploading based on the command-line.
$script [options] -- rustc-command...
Options:
--help|-h: print this help and exit
--local: disable remote execution and run the original command locally.
--verbose|-v: print debug information, including details about uploads.
--dry-run: print remote execution command without executing (remote only).
--project-root: location of source tree which also encompasses outputs
and prebuilt tools, forwarded to --exec-root in the reclient tools.
[default: inferred based on location of this script]
--source FILE: the Rust source root for the crate being built
[default: inferred as the first .rs file in the rustc command]
--depfile FILE: the dependency file that the command is expected to write.
This file lists all source files needed for this crate.
[default: this is inferred from --emit=dep-info=FILE]
If the rust-command contains --remote-inputs=..., those will be interpreted
as extra --inputs to upload, and removed prior local and remote execution.
The option argument is a comma-separated list of files, relative to
\$project_root.
Detected parameters:
project_root: $project_root
EOF
}
local_only=0
dry_run=0
verbose=0
# Extract script options before --
for opt
do
# handle --option arg
if test -n "$prev_opt"
then
eval "$prev_opt"=\$opt
prev_opt=
shift
continue
fi
# Extract optarg from --opt=optarg
case "$opt" in
*=?*) optarg=$(expr "X$opt" : '[^=]*=\(.*\)') ;;
*=) optarg= ;;
esac
case "$opt" in
--help|-h) usage ; exit ;;
--dry-run) dry_run=1 ;;
--local) local_only=1 ;;
--verbose|-v) verbose=1 ;;
--project-root=*) project_root="$optarg" ;;
--project-root) prev_opt=project_root ;;
--source=*) top_source="$optarg" ;;
--source) prev_opt=top_source ;;
--depfile=*) depfile="$optarg" ;;
--depfile) prev_opt=depfile ;;
# stop option processing
--) shift; break ;;
*) echo "Unknown option: $opt"; usage; exit 1 ;;
esac
shift
done
test -z "$prev_out" || { echo "Option is missing argument to set $prev_opt." ; exit 1;}
# Modify original command to extract dep-info only (fast).
# Start with `env` in case command starts with environment variables.
dep_only_command=(env)
# Infer the source_root.
first_source=
# C toolchain linker
linker=()
link_arg_files=()
link_sysroot=()
# input files referenced in environment variables
envvar_files=()
rust_lld=()
debug_var() {
# With --verbose, prints variable values to stdout.
# $1 is name of variable to display.
# The rest are array values.
test "$verbose" = 0 || {
if test "$#" -le 2
then
echo "$1: $2"
else
echo "$1:"
shift
for f in "$@"
do echo " $f"
done
fi
}
}
# Examine the rustc compile command
comma_remote_inputs=
# Compute a rustc command suitable for local and remote execution.
# Prefix command with `env` in case it starts with local environment variables.
rustc_command=(env)
prev_opt=
for opt in "$@"
do
# Copy most command tokens.
dep_only_token="$opt"
# handle --option arg
if test -n "$prev_opt"
then
eval "$prev_opt"=\$opt
case "$prev_opt" in
comma_remote_inputs) ;; # Remove this optarg.
# Copy all others.
*) dep_only_command+=( "$dep_only_token" )
rustc_command+=( "$opt" )
;;
esac
prev_opt=
shift
continue
fi
# Extract optarg from --opt=optarg
case "$opt" in
*=?*) optarg=$(expr "X$opt" : '[^=]*=\(.*\)') ;;
*=) optarg= ;;
esac
# Reject absolute paths, for the sake of build artifact portability,
# and remote-action cache hit benefits.
case "$opt" in
*"$project_root"*)
cat <<EOF
Absolute paths are not remote-portable. Found:
$opt
Please rewrite the command without absolute paths.
EOF
exit 1
;;
esac
case "$opt" in
# This is the (likely prebuilt) rustc binary.
*/rustc) rustc="$opt" ;;
# --remote-inputs signals to the remote action wrapper,
# and not the actual rustc command.
--remote-inputs=*)
comma_remote_inputs="$optarg"
# Remove this from the actual command to be executed.
shift
continue
;;
--remote-inputs) prev_opt=comma_remote_inputs
# Remove this from the actual command to be executed.
shift
continue
;;
# -o path/to/output.rlib
-o) prev_opt=output ;;
# Rewrite this token to only generate dependency information (locally),
# and do no other compilation/linking.
# Write to a renamed depfile because the remote command will also
# produce the originally named depfile.
--emit=*)
test -n "$depfile" || {
# Parse and split --emit=...,...
IFS=, read -ra emit_args <<< "$optarg"
for emit_arg in "${emit_args[@]}"
do
# Extract VALUE from dep-info=VALUE
case "$emit_arg" in
*=?*) emit_value=$(expr "X$emit_arg" : '[^=]*=\(.*\)') ;;
*=) emit_value= ;;
esac
case "$emit_arg" in
dep-info=*) depfile="$emit_value" ;;
esac
done
}
dep_only_token="--emit=dep-info=$depfile.nolink"
# Tell rustc to report all transitive *library* dependencies,
# not just the sources, because these all need to be uploaded.
# This includes (prebuilt) system libraries as well.
# TODO(fxb/78292): this -Z flag is not known to be stable yet.
dep_only_command+=( "-Zbinary-dep-depinfo" )
;;
# --crate-type cdylib needs rust-lld (hard-coding this is a hack)
cdylib)
_rust_lld_rel="$(dirname "$rustc")"/../lib/rustlib/x86_64-unknown-linux-gnu/bin/rust-lld
rust_lld=("$(realpath --relative-to="$project_root" "$_rust_lld_rel")")
;;
# Detect custom linker, preserve symlinks
-Clinker=*)
linker=("$(realpath -s --relative-to="$project_root" "$optarg")")
debug_var "[from -Clinker]" "${linker[@]}"
;;
# sysroot is a directory with system libraries
-Clink-arg=--sysroot=*)
sysroot="$(expr "X$optarg" : '[^=]*=\(.*\)')"
sysroot_relative="$(realpath --relative-to="$project_root" "$sysroot")"
debug_var "[from -Clink-arg=--sysroot]" "$sysroot_relative"
link_sysroot=("$sysroot_relative")
;;
# Link arguments that reference .o or .a files need to be uploaded.
-Clink-arg=*.o | -Clink-arg=*.a | -Clink-arg=*.so | -Clink-arg=*.so.debug)
link_arg="$(realpath --relative-to="$project_root" "$optarg")"
debug_var "[from -Clink-arg]" "$link_arg"
link_arg_files+=("$link_arg")
;;
# This flag informs the linker where to search for libraries.
-Lnative=* )
if test -d "$optarg"
then
# Rather than grab the entire directory (whose contents are not stable
# due to temporary files being written during a build), list specific
# shared objects and archives. Observed temporary files include
# .o.tmp files.
#
# Caveat: if the same dir is home to more than one archive produced by
# parallel build actions, the partial directory listing may not be
# stable throughout the entire build, and be subject to race
# conditions.
#
# Some of these directories contain both .a and .o files.
# It is not yet clear whether the .o files are needed when there is a
# .a archive.
# There are also directories that contain only .o files.
# It is safe to over-specify inputs, so for now, we grab them all.
#
# || : to ignore exit code of ls.
objs=($(ls "$optarg"/*.{so,a,o} 2> /dev/null)) || :
objs_rel=($(echo "${objs[@]}" | grep . | xargs -n 1 realpath --relative-to="$project_root"))
debug_var "[from -Lnative (dir:$optarg)]" "${objs_rel[@]}"
link_arg_files+=("${objs_rel[@]}")
else
link_arg="$(realpath --relative-to="$project_root" "$optarg")"
debug_var "[from -Lnative (file:$optarg)]" "$link_arg"
link_arg_files+=("$link_arg")
fi
;;
--*=* ) ;; # forward
# Forward other environment variables (or similar looking).
*=*) ;;
# Capture the first named source file as the source-root.
*.rs) test -n "$first_source" || first_source="$opt" ;;
*.a | *.o | *.so | *.so.debug)
link_arg_files+=("$(realpath --relative-to="$project_root" "$opt")")
;;
# Preserve all other tokens.
*) ;;
esac
# Copy tokens to craft a local command for dep-info.
dep_only_command+=( "$dep_only_token" )
# Copy tokens to craft a command for local and remote execution.
rustc_command+=("$opt")
shift
done
test -z "$prev_out" || { echo "Option is missing argument to set $prev_opt." ; exit 1;}
# Copy the original command.
# Prefix with env, in case command starts with VAR=VALUE ...
if test "$local_only" = 1
then
# Run original command and exit (no remote execution).
"${rustc_command[@]}"
exit "$?"
fi
# Otherwise, prepare for remote execution.
# Specify the rustc binary to be uploaded.
rustc_relative="$(realpath --relative-to="$project_root" "$rustc")"
# Collect extra inputs to upload for remote execution.
extra_inputs=()
IFS=, read -ra extra_inputs <<< "$comma_remote_inputs"
# TODO(fangism): if possible, determine these shlibs statically to avoid `ldd`-ing.
# TODO(fangism): for host-independence, use llvm-otool and `llvm-readelf -d`,
# which requires uploading more tools.
function nonsystem_shlibs() {
# $1 is a binary
ldd "$1" | grep "=>" | cut -d\ -f3 | \
grep -v -e '^/lib' -e '^/usr/lib' | \
xargs -n 1 realpath --relative-to="$project_root"
}
function depfile_inputs_by_line() {
# From a depfile arrange deps one per line.
# This looks at the phony deps "FILE:".
grep ":$" "$1" | cut -d: -f1
}
# The rustc binary might be linked against shared libraries.
# Exclude system libraries in /usr/lib and /lib.
# convert to paths relative to $project_root for rewrapper.
mapfile -t rustc_shlibs < <(nonsystem_shlibs "$rustc")
# At this time, the linker we pass is known to be statically linked itself
# and doesn't need to be accompanied by any shlibs.
# If --source was not specified, infer it from the command-line.
# This source file is likely to appear as the first input in the depfile.
test -n "$top_source" || top_source="$first_source"
top_source="$(realpath --relative-to="$project_root" "$top_source")"
# Locally generate a depfile only and read it as list of files to upload.
# These inputs appear relative to the build/output directory, but need to be
# relative to the $project_root for rewrapper.
"${dep_only_command[@]}"
mapfile -t depfile_inputs < <(depfile_inputs_by_line "$depfile.nolink" | \
xargs -n 1 realpath --relative-to="$project_root")
# Done with temporary depfile, remove it.
rm -f "$depfile.nolink"
# Inputs to upload include (all relative to $project_root):
# * rust tool(s) [$rustc_relative]
# * rust tool shared libraries [$rustc_shlibs]
# * direct source files [$top_source]
# * indirect source files [$depfile.nolink]
# * direct dependent libraries [$depfile.nolink]
# * transitive dependent libraries [$depfile.nolink]
# * objects and libraries used as linker arguments [$link_arg_files]
# * system rust libraries [$depfile.nolink]
# * clang toolchain binaries for codegen and linking
# For example: -Clinker=.../lld
# * additional data dependencies [$extra_inputs]
# Need more than the bin/ directory, but its parent dir which contains tool
# libraries, and system libraries needed for linking.
# This is expected to cover the custom linker referenced by -Clinker=.
tools_dir=()
test "${#linker[@]}" = 0 || {
tools_dir=("$(dirname "$(dirname "${linker[0]}")")")
}
remote_inputs=(
"$rustc_relative"
"${rust_lld[@]}"
"${rustc_shlibs[@]}"
"$top_source"
"${depfile_inputs[@]}"
"${envvar_files[@]}"
"${tools_dir[@]}"
"${link_arg_files[@]}"
"${link_sysroot[@]}"
"${extra_inputs[@]}"
)
remote_inputs_joined="$(IFS=, ; echo "${remote_inputs[*]}")"
# Outputs include the declared output file and a depfile.
outputs=("$(realpath --relative-to="$project_root" "$output")")
test -z "$depfile" || outputs+=("$(realpath --relative-to="$project_root" "$depfile")")
# Removing outputs these avoids any unintended reuse of them.
rm -f "${outputs[@]}"
outputs_joined="$(IFS=, ; echo "${outputs[*]}")"
dump_vars() {
debug_var "outputs" "${outputs[@]}"
debug_var "rustc binary" "$rustc_relative"
debug_var "rustc shlibs" "${rustc_shlibs[@]}"
debug_var "rust lld" "${rust_lld[@]}"
debug_var "source root" "$top_source"
debug_var "linker" "${linker[@]}"
debug_var "link args" "${link_arg_files[@]}"
debug_var "link sysroot" "${link_sysroot[@]}"
debug_var "env var files" "${envvar_files[@]}"
debug_var "depfile" "$depfile"
debug_var "[$script: dep-info]" "${dep_only_command[@]}"
debug_var "depfile inputs" "${depfile_inputs[@]}"
debug_var "tools dir" "${tools_dir[@]}"
debug_var "extra inputs" "${extra_inputs[@]}"
}
dump_vars
# Assemble the remote execution command.
# During development, if you need to test a pre-release at top-of-tree,
# symlink the bazel-built binaries into a single directory, e.g.:
# --bindir=$HOME/re-client/install-bin
# and pass options available in the new version, e.g.:
# --preserve_symlink
remote_rustc_command=("$script_dir"/fuchsia-rbe-action.sh \
--exec_root="$project_root" \
--inputs="$remote_inputs_joined" \
--output_files="$outputs_joined" -- \
"${rustc_command[@]}")
# Execute the remote command.
if test "$dry_run" = 1
then
echo "[$script: skipped]:" "${remote_rustc_command[@]}"
dump_vars
else
# Execute the rust command remotely.
debug_var "[$script: remote]" "${remote_rustc_command[@]}"
"${remote_rustc_command[@]}"
status="$?"
# Scan remote-generated depfile for absolute paths, and reject them.
abs_deps=()
if test -f "$depfile"
then
# TEMPORARY WORKAROUND until upstream fix lands:
# https://github.com/pest-parser/pest/pull/522
# Rewrite the depfile if it contains any absolute paths from the remote
# build; paths should be relative to the root_build_dir.
sed -i -e 's|/b/f/w/out/[^/]*/||g' "$depfile"
mapfile -t remote_depfile_inputs < <(depfile_inputs_by_line "$depfile")
for f in "${remote_depfile_inputs[@]}"
do
case "$f" in
/*) abs_deps+=("$f") ;;
esac
done
test "${#abs_deps[@]}" = 0 || status=1
# error message below
fi
test "$status" = 0 || {
# On any failure, dump debug info, even if it is not related to RBE.
verbose=1
cat <<EOF
======== Remote Rust build action FAILED ========
This could either be a failure with the original command, or something
wrong with the remote version of the command.
Try the command locally first, without RBE, and make sure that works.
Add 'disable_rbe = true' to the problematic Rust target in GN,
or 'fx set' without '--rbe' to disable globally.
Once it passes locally, re-enable RBE.
If the remote version still fails, file a bug, and CC the Fuchsia Build Team
with the following info below:
EOF
# Identify which target failed by its command, useful in parallel build.
debug_var "[$script: FAIL]" "${remote_rustc_command[@]}"
dump_vars
# Reject absolute paths in depfiles.
if test "${#abs_deps[@]}" -ge 1
then
debug_var "Forbidden absolute paths in remote-generated depfile" "${abs_deps[@]}"
fi
echo
tmpdir="${RBE_proxy_log_dir:-/tmp}"
reproxy_errors="$tmpdir"/reproxy.ERROR
echo "The last lines of $reproxy_errors might explain a remote failure:"
if test -r "$reproxy_errors" ; then tail "$reproxy_errors" ; fi
# Attempt to diagnose common symptoms.
if grep -q "Fail to dial" "$reproxy_errors"
then
cat <<EOF
"Fail to dial" could indicate that reproxy is not running.
Did you run with 'fx build'?
If not, you may need to wrap your build command with:
$project_root/build/rbe/fuchsia-reproxy-wrap.sh -- YOUR-COMMAND
'Proxy started successfully.' indicates that reproxy is running.
EOF
fi
if grep -q "Error connecting to remote execution client: rpc error: code = PermissionDenied" "$reproxy_errors"
then
cat <<EOF
You might not have permssion to access the RBE instance.
Contact fuchsia-build-team@google.com for support.
EOF
fi
}
exit "$status"
fi