|  | #!/bin/bash | 
|  | # Copyright 2019 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. | 
|  |  | 
|  | #### CATEGORY=Other | 
|  | ### Run an `fx` command across multiple build directories. | 
|  | ## Usage: fx multi [add | list | remove | rm | save FILE | use FILE] | 
|  | ##        fx multi [[-p | --parallel] | [-f | --fail]] COMMAND ... | 
|  | ##        fx multi set {PRODUCT.BOARD | SPEC}... [SWITCHES...] | 
|  | ## | 
|  | ## fx multi maintains lists of build directories and runs fx commands | 
|  | ## across multiple builds in sequence. | 
|  | ## | 
|  | ## The first form uses subcommands that maintain the current multi list: | 
|  | ## | 
|  | ##   add                   Adds the current build to the multi list. | 
|  | ##                         e.g. `fx --dir out/foo multi add` | 
|  | ##                         If the directory is already present, rotates it | 
|  | ##                         to the end of the list. | 
|  | ## | 
|  | ##   clear                 Resets the current multi list to empty. | 
|  | ## | 
|  | ##   list                  Displays the current multi list. | 
|  | ##                         Just `fx multi` does this too. | 
|  | ## | 
|  | ##   remove [-f] | rm [-f] Removes the current build from the multi list. | 
|  | ##                         With -f it's not an error if it's not in the list. | 
|  | ## | 
|  | ##   save FILE             Saves the multi list in FILE. | 
|  | ## | 
|  | ##   use FILE              Resets the multi list to the one saved in FILE. | 
|  | ## | 
|  | ## The second form runs any other `fx` subcommand you like, several times. | 
|  | ## For each build in the multi list, it runs `fx --dir <build-dir> COMMAND ...` | 
|  | ## With `--fail` (or `-f`), `fx multi` exits as soon as one COMMAND fails. | 
|  | ## By default, it runs each one in sequence even if the previous one failed. | 
|  | ## At the end it reports which ones failed. | 
|  | ## With `--parallel` (or `-p`), `fx multi` executes COMMAND on all directories | 
|  | ## in parallel. Fail and parallel are mutually exclusive. | 
|  | ## | 
|  | ## The third form resets the multi list to empty and then runs several `fx set` | 
|  | ## commands, adding each new build dir to the multi list (if it succeeded). | 
|  | ## It's like running `fx multi clear` and then a series of: | 
|  | ## | 
|  | ##   fx set PRODUCT.BOARD SWITCHES... && fx multi add | 
|  | ## | 
|  | ## Arguments before SWITCHES... can be explicit PRODUCT.BOARD or can be | 
|  | ## one of a fixed set of SPEC strings.  Run `fx multi set` alone to see | 
|  | ## the set of available SPEC strings.  Each string corresponds to a list | 
|  | ## of PRODUCT.BOARD + FIXED_SWITCHES... combinations.  The FIXED_SWITCHES... | 
|  | ## are prepended to any SWITCHES... on the `fx multi set` command line. | 
|  | ## | 
|  |  | 
|  | source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/lib/vars.sh || exit $? | 
|  |  | 
|  | set -e | 
|  | shopt -s nullglob | 
|  |  | 
|  | # Set an envrionment variable so that child processes can know they were invoked (directly or | 
|  | # indirectly) by fx multi. For example, build metrics need this information. | 
|  | export FUCHSIA_FX_MULTI_RUN=1 | 
|  |  | 
|  | readonly MULTI_LIST_FILE="${FUCHSIA_DIR}/.fx-multi-list" | 
|  | readonly MULTI_SPEC_DIRS=( | 
|  | "${FUCHSIA_DIR}/tools/devshell/lib/multi-specs" | 
|  | "${FUCHSIA_DIR}"/vendor/*/scripts/devshell/lib/multi-specs | 
|  | ) | 
|  |  | 
|  | PARALLEL=false | 
|  | FAIL_FAST=false | 
|  |  | 
|  | MULTI_LIST=() | 
|  |  | 
|  | function for-each-build { | 
|  | local on_failure="$1" | 
|  | shift | 
|  | local status=0 failed=() pids=() dir | 
|  |  | 
|  | for dir in "${MULTI_LIST[@]}"; do | 
|  | if "$PARALLEL"; then | 
|  | _FX_BUILD_DIR="$FUCHSIA_DIR/$dir" "$@" & | 
|  | pids+=($!) | 
|  | else | 
|  | _FX_BUILD_DIR="$FUCHSIA_DIR/$dir" "$@" || { | 
|  | status=$? | 
|  | failed+=("$dir") | 
|  | $FAIL_FAST && break | 
|  | } | 
|  | fi | 
|  | done | 
|  | if "$PARALLEL"; then | 
|  | n=${#MULTI_LIST[@]} | 
|  | i=0 | 
|  | while ((i < n)); do | 
|  | wait "${pids[i]}" || { | 
|  | status=$? | 
|  | failed+=("${MULTI_LIST[i]}") | 
|  | } | 
|  | ((++i)) | 
|  | done | 
|  | fi | 
|  | for dir in "${failed[@]}"; do | 
|  | "$on_failure" "$dir" "$@" | 
|  | done | 
|  | return $status | 
|  | } | 
|  |  | 
|  | function read-multi-list { | 
|  | if [[ $# -eq 0 ]]; then | 
|  | set -- "$MULTI_LIST_FILE" | 
|  | fi | 
|  | MULTI_LIST=() | 
|  | if [[ -r "$1" ]]; then | 
|  | MULTI_LIST=($(<"$1")) | 
|  | else | 
|  | return 1 | 
|  | fi | 
|  | } | 
|  |  | 
|  | function write-multi-list { | 
|  | if [[ $# -eq 0 ]]; then | 
|  | set -- "$MULTI_LIST_FILE" | 
|  | fi | 
|  | local -r tmpfile="$(mktemp)" | 
|  | for-each-build : list-one --quiet >"$tmpfile" | 
|  | mv -f "$tmpfile" "$1" | 
|  | } | 
|  |  | 
|  | function list { | 
|  | if [[ ${#MULTI_LIST[@]} == 0 ]]; then | 
|  | fx-warn "no build directories in current fx multi list; use fx multi add" | 
|  | fi | 
|  | for-each-build : list-one | 
|  | } | 
|  |  | 
|  | function list-one { | 
|  | echo "${_FX_BUILD_DIR#$FUCHSIA_DIR/}" | 
|  | if [[ $# -eq 0 ]]; then | 
|  | if [[ ! -d "$_FX_BUILD_DIR" ]]; then | 
|  | fx-warn "build directory $_FX_BUILD_DIR does not exist" | 
|  | return 1 | 
|  | elif [[ ! -r "$_FX_BUILD_DIR/args.gn" ]]; then | 
|  | fx-warn "build directory $_FX_BUILD_DIR is not configured" | 
|  | return 1 | 
|  | fi | 
|  | fi | 
|  | } | 
|  |  | 
|  | function use { | 
|  | if [[ $# -ne 1 ]]; then | 
|  | fx-command-help | 
|  | return 1 | 
|  | fi | 
|  | read-multi-list "$1" | 
|  | write-multi-list | 
|  | } | 
|  |  | 
|  | function save { | 
|  | if [[ $# -ne 1 ]]; then | 
|  | fx-command-help | 
|  | return 1 | 
|  | fi | 
|  | write-multi-list "$1" | 
|  | } | 
|  |  | 
|  | function clear { | 
|  | MULTI_LIST=() | 
|  | write-multi-list | 
|  | } | 
|  |  | 
|  | function add { | 
|  | if [[ $# -ne 0 ]]; then | 
|  | fx-command-help | 
|  | return 1 | 
|  | fi | 
|  |  | 
|  | remove -f | 
|  | MULTI_LIST+=("${FUCHSIA_BUILD_DIR#$FUCHSIA_DIR/}") | 
|  | write-multi-list | 
|  | } | 
|  |  | 
|  | function remove { | 
|  | local check=true | 
|  | if [[ $# -eq 1 && "$1" == "-f" ]]; then | 
|  | check=false | 
|  | shift | 
|  | fi | 
|  | if [[ $# -ne 0 ]]; then | 
|  | fx-command-help | 
|  | return 1 | 
|  | fi | 
|  |  | 
|  | fx-config-read | 
|  | local new_list=() missing=true dir | 
|  | for dir in "${MULTI_LIST[@]}"; do | 
|  | if [[ "$FUCHSIA_DIR/$dir" == "$FUCHSIA_BUILD_DIR" ]]; then | 
|  | missing=false | 
|  | else | 
|  | new_list+=("$dir") | 
|  | fi | 
|  | done | 
|  |  | 
|  | if $missing && $check; then | 
|  | fx-error "build dir not in current fx multi list" | 
|  | return 1 | 
|  | fi | 
|  |  | 
|  | MULTI_LIST=("${new_list[@]}") | 
|  | write-multi-list | 
|  | } | 
|  |  | 
|  | function execute-one { | 
|  | # Do it in a subshell in case it exits. | 
|  | (fx-command-run "$@") | 
|  | } | 
|  |  | 
|  | function one-failed { | 
|  | local dir="$1" | 
|  | shift 2 | 
|  | fx-error fx --dir "$dir" "$@" | 
|  | } | 
|  |  | 
|  | function find-spec-file { | 
|  | local dir spec_file | 
|  | for dir in "${MULTI_SPEC_DIRS[@]}"; do | 
|  | spec_file="$dir/$1" | 
|  | if [[ -r "$spec_file" ]]; then | 
|  | echo "$spec_file" | 
|  | return 0 | 
|  | fi | 
|  | done | 
|  | fx-error "Unrecognized SPEC string: $1" | 
|  | fx-error 'Run `fx multi set` alone to see available SPEC strings' | 
|  | return 1 | 
|  | } | 
|  |  | 
|  | function multi-set { | 
|  | local builds=() | 
|  | while [[ $# -gt 0 && "$1" != -* ]]; do | 
|  | builds+=("$1") | 
|  | shift | 
|  | done | 
|  |  | 
|  | if [[ ${#builds[@]} == 0 ]]; then | 
|  | fx-error "Missing PRODUCT.BOARD or SPEC goals." | 
|  | fx-error "PRODUCT.BOARD is as for fx set, which see.  SPEC is one of:" | 
|  | local dir spec_file | 
|  | for dir in "${MULTI_SPEC_DIRS[@]}"; do | 
|  | for spec_file in "$dir"/*[!~]; do | 
|  | fx-error | 
|  | fx-error "  ${spec_file#$dir/}  (cf ${spec_file#$FUCHSIA_DIR/})" | 
|  | fx-error "    $(sed -n 's/^## \{0,1\}//p' "$spec_file")" | 
|  | done | 
|  | done | 
|  | return 1 | 
|  | fi | 
|  |  | 
|  | clear | 
|  |  | 
|  | local status=0 failed=() build spec_file set_cmd | 
|  | for build in "${builds[@]}"; do | 
|  | if [[ "$build" == *.* ]]; then | 
|  | spec_file= | 
|  | else | 
|  | spec_file="$(find-spec-file "$build")" | 
|  | exec 3<"$spec_file" || { | 
|  | fx-error "$build is not a known fx multi set SPEC" | 
|  | return 1 | 
|  | } | 
|  | fi | 
|  | while [[ -z "$spec_file" ]] || { | 
|  | read build <&3 && | 
|  | while [[ -z "$build" || "$build" == \#* ]] && read build <&3; do | 
|  | : | 
|  | done | 
|  | }; do | 
|  | # Clear the build directory variable so the checks in `fx-build-dir-if-present` allow us to | 
|  | # change the build directory, otherwise we end up with the first build directory as the | 
|  | # directory for each set invocation. | 
|  | FUCHSIA_BUILD_DIR="" | 
|  | set_cmd=("set" "$build" "${spec_switches[@]}" "$@") | 
|  | if fx-command-run "${set_cmd[@]}" 3<&-; then | 
|  | # FUCHSIA_BUILD_DIR is not updated by fx-* functions after the first build directory so use | 
|  | # .fx-build-dir to update it ourselves. | 
|  | FUCHSIA_BUILD_DIR="$(<"${FUCHSIA_DIR}/.fx-build-dir")" | 
|  | add | 
|  | else | 
|  | status=$? | 
|  | failed+=("fx ${set_cmd[*]}") | 
|  | $FAIL_FAST && break | 
|  | fi | 
|  | [[ -n "$spec_file" ]] || break | 
|  | done | 
|  | done | 
|  | exec 3<&- | 
|  |  | 
|  | for build in "${failed[@]}"; do | 
|  | fx-error "$build" | 
|  | done | 
|  |  | 
|  | return $status | 
|  | } | 
|  |  | 
|  | function main { | 
|  | if [[ $# -eq 0 ]]; then | 
|  | set -- list | 
|  | fi | 
|  |  | 
|  | while [[ "$1" == -* ]]; do | 
|  | case "$1" in | 
|  | -p | --parallel) | 
|  | PARALLEL=true | 
|  | ;; | 
|  | -f | --fail) | 
|  | FAIL_FAST=true | 
|  | ;; | 
|  | --) | 
|  | break | 
|  | ;; | 
|  | *) | 
|  | fx-command-help | 
|  | return 1 | 
|  | ;; | 
|  | esac | 
|  | shift | 
|  | done | 
|  |  | 
|  | if $FAIL_FAST && $PARALLEL; then | 
|  | fx-error "--fail and --parallel are mutually exclusive, pick one." | 
|  | return 1 | 
|  | fi | 
|  |  | 
|  | read-multi-list "$MULTI_LIST_FILE" || : | 
|  |  | 
|  | case "$1" in | 
|  | add | clear | list | remove | save | use) | 
|  | "$@" | 
|  | ;; | 
|  | rm) | 
|  | shift | 
|  | remove "$@" | 
|  | ;; | 
|  | set) | 
|  | shift | 
|  | multi-set "$@" | 
|  | ;; | 
|  | *) | 
|  | for-each-build one-failed execute-one "$@" | 
|  | ;; | 
|  | esac | 
|  | } | 
|  |  | 
|  | main "$@" |