blob: d3d929f3295dfb4791904d9b6fd891480595367c [file] [log] [blame]
#!/bin/bash
# Copyright 2017 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.
function get_host_tools_dir {
local -r build_dir="$(fx-build-dir-if-present && echo "${FUCHSIA_BUILD_DIR}")"
local -r host_tools="${build_dir}/host-tools"
if [[ -d "${build_dir}" && -d "${host_tools}" ]]; then
echo "${host_tools}"
fi
}
function get_exec_from_metadata {
local -r metadata_file=$1
export PREBUILT_3P_DIR FUCHSIA_DIR HOST_PLATFORM
export HOST_TOOLS_DIR="$(get_host_tools_dir)"
awk -F ' *= *' -f - "${metadata_file}" <<'EOF'
/^#### +EXECUTABLE */ {
gsub(/\${PREBUILT_3P_DIR}/, ENVIRON["PREBUILT_3P_DIR"], $2);
gsub(/\${FUCHSIA_DIR}/, ENVIRON["FUCHSIA_DIR"], $2);
gsub(/\${HOST_TOOLS_DIR}/, ENVIRON["HOST_TOOLS_DIR"], $2);
gsub(/\${HOST_PLATFORM}/, ENVIRON["HOST_PLATFORM"], $2);
print $2;
}
EOF
}
function _relative {
cmd="$1"
if [[ "${cmd}" == *"${fuchsia_dir}"* ]]; then
echo "//${cmd#${fuchsia_dir}/}"
else
echo "${cmd}"
fi
}
function find_executable {
local cmd_name="$1"
local cmd_path="$(commands "${cmd_name}")"
cmd_path="${cmd_path%.fx}"
local fx_file_path="${cmd_path}.fx"
if [[ ! -f "${fx_file_path}" ]]; then
# no metadata, so let's try to find the file
if [[ -z "${cmd_path}" ]]; then
# no file in regular script directories, look in host_tools
cmd_path="$(find_host_tools "${cmd_name}")"
fi
echo "${cmd_path}"
if [[ ! -x "${cmd_path}" ]]; then
return 1
fi
else
# there's metadata
local from_metadata="$(get_exec_from_metadata "${fx_file_path}")"
if [[ -n "${from_metadata}" ]]; then
# EXECUTABLE is defined in the metadata, use it
echo "${from_metadata}"
if [[ -f "${cmd_path}" && ! "${cmd_path}" -ef "${from_metadata}" ]]; then
fx-error "Invalid ${fx_file_path}: if both ${basename_exec} and EXECUTABLE metadata exist, they must point to the same file"
return 1
fi
else
# no EXECUTABLE in the metadata, so return the cmd_path itself
echo "${cmd_path}"
if [[ ! -x "${cmd_path}" ]]; then
return 1
fi
fi
fi
}
function find_execs_and_metadata {
local cmd_name=$1
shift
if [[ -z "${cmd_name}" ]]; then
cmd_name="*"
fi
local dirs=()
for d in "$@"; do
if [[ -d "${d}" ]]; then
dirs+=( "${d}" )
fi
done
if [[ ${#dirs[@]} -eq 0 ]]; then
return 0
fi
# run find assuming "-executable" is supported
cmds="$( find "${dirs[@]}" -maxdepth 1 -type f \
\( -executable -name "${cmd_name}" \) -o \
\( -name "${cmd_name}.fx" \) 2>/dev/null )"
if [[ $? -ne 0 ]]; then
# assume that the error was caused because versions of find older than 4.3.0
# don't support -executable. Run with -perm +100 instead, which is not
# supported in versions of find newer than 4.5.12, so it can't be used always.
cmds="$( find "${dirs[@]}" -maxdepth 1 -type f \
\( -perm +100 -name "${cmd_name}" \) -o \
\( -name "${cmd_name}.fx" \) 2>/dev/null )"
if [[ $? -ne 0 ]]; then
{
echo "ERROR: 'find' failed unexpectedly, please execute fx with '-x' and report a bug."
echo "At least one of the commands below was expected to work:"
echo 'find ' "${dirs[@]}" '-maxdepth 1 -type f \( -executable -name' "\"${cmd_name}\"" '\) -o \( -name ' "\"${cmd_name}.fx\"" '\)'
echo 'find ' "${dirs[@]}" '-maxdepth 1 -type f \( -perm +100 -name' "\"${cmd_name}\"" '\) -o \( -name ' "\"${cmd_name}.fx\"" '\)'
} >&2
exit 1
fi
fi
sort -u <<< "${cmds}"
}
function find_host_tools {
local cmd_name=$1
local -r host_tools="$(get_host_tools_dir)"
if [[ -z "${host_tools}" ]]; then
return
fi
# get a list of non-host-tools commands and metadata files, separated by
# semi-colon.
cmds="$(commands | tr '\n' ';')"
binaries=()
# do not list a host tool if there's a subcommand script or an .fx metadata
# file with the same name.
for binary in $(find_execs_and_metadata "${cmd_name}" "${host_tools}"); do
name="${binary##*/}" # remove path, equivalent to basename but faster
# only return the binary if no {binary} or {binary}.fx as other commands
if [[ "${cmds}" != */${name}.fx\;* && "${cmds}" != */${name}\;* ]]; then
binaries+=( "${binary}" )
fi
done
echo "${binaries[@]}"
}
function commands {
local cmd_name=$1
local dirs
# handle "fx help vendor VENDOR COMMAND"
if [[ "${cmd_name}" == "vendor" && $# -eq 3 ]]; then
vendor=$2
cmd_name=$3
dirs=("${fuchsia_dir}"/vendor/${vendor}/scripts/devshell)
else
dirs=("${fuchsia_dir}"/tools/devshell{,/contrib} "${fuchsia_dir}"/vendor/*/scripts/devshell)
fi
find_execs_and_metadata "${cmd_name}" "${dirs[@]}"
}
function help_options {
cat <<END
fx help flags: fx help [OPTION]
--no-contrib Hide contrib commands (see //tools/devshell/README.md)
--deprecated Do not hide deprecated commands
END
}
function help_global_options {
cat <<END
Global fx options: fx [OPTION] ${cmd} ...
--dir=BUILD_DIR Path to the build directory to use when running COMMAND.
-d=DEVICE[:SSH_PORT] Target a specific device.
DEVICE may be a Fuchsia device name to be resolved using
device-finder or an IP address.
An IPv4 address must be specified directly, while an IPv6
need to be surrounded by brackets.
SSH_PORT, if specified, will be used for all commands
that rely on SSH to connect to the device instead of the
default SSH port (22).
Note: See "fx help set-device" for more help and to set
the default device for a BUILD_DIR.
-i Iterative mode. Repeat the command whenever a file is
modified under your Fuchsia directory, not including
out/.
-x Print commands and their arguments as they are executed.
-xx Print extra logging of the fx tool itself (implies -x)
END
}
function help_list_commands {
local hide_contrib=0
local show_deprecated=0
while [[ $# -ne 0 ]]; do
if [[ "$1" == "--deprecated" ]]; then
show_deprecated=1
elif [[ "$1" == "--no-contrib" ]]; then
hide_contrib=1
fi
shift
done
# list all subcommands with summaries, grouped by categories
commands | xargs awk \
-v hide_contrib=${hide_contrib} \
-v show_deprecated=${show_deprecated} \
-f "${fuchsia_dir}/scripts/fx-help.awk"
# list host tools build artifacts without corresponding metadata
host_tools="$(find_host_tools)"
if [[ -n "${host_tools}" ]]; then
echo "Host binaries produced by the build with no metadata (more info at //tools/devshell/README.md):"
for i in ${host_tools}; do
echo -n " "
basename $i
done
fi
help_options
help_global_options
}
function help_command {
local cmd="$@"
local cmd_path="$(commands ${cmd} | head -1)"
if [[ -z "${cmd_path}" ]]; then
local cmd_path="$(find_host_tools ${cmd} | head -1)"
if [[ -z "${cmd_path}" ]]; then
echo "Command ${cmd} not found. Try \`fx help\`"
else
echo "'$(_relative "${cmd_path}")' is a host tool and no metadata" \
"was found. Try running \`fx ${cmd} -h\`"
fi
elif [[ $(file -b --mime "${cmd_path}" | cut -d / -f 1) == "text" ]]; then
fx-print-command-help "${cmd_path}"
help_global_options
else
echo "No help found. Try \`fx ${cmd} -h\`"
fi
}
function usage {
cat <<END
usage: fx [--dir BUILD_DIR] [-d DEVICE_NAME] [-i] [-x] COMMAND [...]
Run Fuchsia development commands. Must be run either from a directory
that is contained in a Platform Source Tree or with the FUCHSIA_DIR
environment variable set to the root of a Platform Source Tree.
host shell extensions: (requires "source scripts/fx-env.sh")
fx-update-path add useful tools to the PATH
fx-set-prompt display the current configuration in the shell prompt
END
help_list_commands "$@"
}
fuchsia_dir="${FUCHSIA_DIR}"
if [[ -z "${fuchsia_dir}" ]]; then
# We walk the parent directories looking for .jiri_root rather than using
# BASH_SOURCE so that we find the fuchsia_dir enclosing the current working
# directory instead of the one containing this file in case the user has
# multiple source trees and is picking up this file from another one.
fuchsia_dir="$(pwd)"
while [[ ! -d "${fuchsia_dir}/.jiri_root" ]]; do
fuchsia_dir="$(dirname "${fuchsia_dir}")"
if [[ "${fuchsia_dir}" == "/" ]]; then
echo "Cannot find Platform Source Tree containing $(pwd)"
exit 1
fi
done
fi
declare -r vars_sh="${fuchsia_dir}/tools/devshell/lib/vars.sh"
source "${vars_sh}" || exit $?
declare -r metrics_sh="${fuchsia_dir}/tools/devshell/lib/metrics.sh"
source "${metrics_sh}" || exit $?
while [[ $# -ne 0 ]]; do
case $1 in
--config=*|--dir=*|-d=*)
# Turn --switch=value into --switch value.
arg="$1"
shift
set -- "${arg%%=*}" "${arg#*=}" "$@"
continue
;;
--config)
fx-warn "DEPRECATION NOTICE: --config is deprecated, use --dir instead."
sleep 5
if [[ $# -lt 2 ]]; then
usage
fx-error "Missing path to config file for --config argument"
exit 1
fi
shift # Removes --config.
export _FX_BUILD_DIR="$(source "$1"; echo "${FUCHSIA_BUILD_DIR}")"
;;
--dir)
if [[ $# -lt 2 ]]; then
usage
fx-error "Missing path to build directory for --dir argument"
exit 1
fi
shift # Removes --dir.
export _FX_BUILD_DIR="$1"
if [[ "$_FX_BUILD_DIR" == //* ]]; then
_FX_BUILD_DIR="${fuchsia_dir}/${_FX_BUILD_DIR#//}"
fi
;;
-d)
if [[ $# -lt 2 ]]; then
usage
fx-error "Missing device name for -d argument"
exit 1
fi
shift # removes -d
if ! is-valid-device "$1"; then
fx-error "Invalid device: $1. See valid values in 'fx help set-device'"
exit 1
fi
export FUCHSIA_DEVICE_NAME="$1"
;;
-i)
declare iterative=1
;;
-x)
export FUCHSIA_DEVSHELL_VERBOSITY=1
;;
-xx)
set -x
;;
--)
shift
break
;;
help|--help)
if [[ $# -lt 2 || "$2" =~ ^\-\-.* ]]; then
shift
usage "$@"
else
shift
help_command "$@"
fi
exit 0
;;
-*)
usage
fx-error "Unknown global argument $1"
exit 1
;;
*)
break
;;
esac
shift
done
if [[ $# -lt 1 ]]; then
usage
fx-error "Missing command name"
exit 1
fi
command_name="$1"
command_path="$(find_executable ${command_name})"
if [[ $? -ne 0 || ! -x "${command_path}" ]]; then
if [[ -n "${command_path}" ]]; then
_path_message=" in the expected location $(_relative "${command_path}")"
fi
fx-error "Cannot find executable for ${command_name}${_path_message}."\
"If this is a tool produced by the build, make sure your"\
"\`fx set\` config produces it in the $(_relative "$(get_host_tools_dir)") directory."
exit 1
fi
declare -r cmd_and_args="$@"
shift # Removes the command name.
if [[ "${command_name}" != "vendor" || $# -lt 2 ]]; then
metric_name="${command_name}"
else
metric_name="vendor/$1/$2"
fi
track-command-execution "${metric_name}" "$@" &
declare -r start_time="$SECONDS"
"${command_path}" "$@"
declare -r retval=$?
declare -r end_time="$SECONDS"
declare -r ellapsed_time=$(( 1000 * (end_time - start_time) )) # milliseconds
if [ -z "${iterative}" ]; then
track-command-finished "${ellapsed_time}" "${retval}" "${command_name}" "$@" &
exit ${retval}
elif which inotifywait >/dev/null; then
monitor_source_changes() {
# Watch everything except out/ and files/directories beginning with "."
# such as lock files, swap files, .git, etc'.
inotifywait -qrme modify \
--exclude "/(\.|lock|compile_commands.json)" \
"${fuchsia_dir}" \
@"${fuchsia_dir}"/out \
@"${fuchsia_dir}"/zircon/public
}
elif which apt-get >/dev/null; then
echo "Missing inotifywait"
echo "Try: sudo apt-get install inotify-tools"
exit 1
elif which fswatch >/dev/null; then
monitor_source_changes() {
fswatch --one-per-batch --event=Updated \
-e "${fuchsia_dir}"/out/ \
-e "${fuchsia_dir}"/zircon/public/ \
-e "/\." \
-e "/lock" \
-e "/compile_commands.json" \
.
}
else
echo "Missing fswatch"
echo "Try: brew install fswatch"
exit 1
fi
monitor_and_run() {
local -r event_pipe="$1"
local -r display_name="$2"
shift 2
# Explicitly bind $event_pipe to a numbered FD so read behaves consistently
# on Linux and Mac shells ("read <$event_pipe" closes $event_pipe after the
# first read on Mac bash).
exec 3<"${event_pipe}"
while read -u 3; do
if [[ "$(uname -s)" != "Darwin" ]]; then
# Drain all subsequent events in a batch.
# Otherwise when multiple files are changes at once we'd run multiple
# times.
read -u 3 -d "" -t .01
fi
# Allow at most one fx -i invocation per Fuchsia dir at a time.
# Otherwise multiple concurrent fx -i invocations can trigger each other
# and cause a storm.
echo "---------------------------------- fx -i ${display_name} ---------------------------------------"
"$@"
echo "--- Done!"
done
}
monitor_and_run <(monitor_source_changes) "${cmd_and_args}" "${command_path}" "$@"