spiegel-keyman/resources/builder.inc.sh
Marc Durdin 5e55ceb903 fix(common): ignore deps when formatting help
Fixes #7512.

Dependencies were being added to list of parameters, which caused the
help formatting to be overly spaced out. Given deps are not parameters,
we should never have been adding them anyway.
2022-10-24 15:26:16 +11:00

1252 lines
36 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# This script contains utilities for builder_script calls
#
# * builder_ functions and variables are defined here.
# * REPO_ROOT defines the top level of this repository
# * THIS_SCRIPT_PATH defines the full path of the running script
# * THIS_SCRIPT_NAME defines the basename of the running script
# * THIS_SCRIPT_IDENTIFIER defines the repo-relative path of the running script
# * _builder_ functions and variables are internal use only for builder.inc.sh, and
# subject to change at any time. Do not use them in other scripts.
# * Note: the running script is the top-level script that includes either
# builder.inc.sh directly, or, just in the Keyman repo, via build-utils.sh.
#
# _builder_init is called internally at the bottom of this file after we have
# all function declarations in place.
function _builder_init() {
_builder_findRepoRoot
_builder_setBuildScriptIdentifiers
if [[ -n "$TERM" ]] && [[ "$TERM" != "dumb" ]] && [[ "$TERM" != "unknown" ]]; then
builder_use_color true
else
builder_use_color false
fi
}
function _builder_findRepoRoot() {
# See https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself
# None of the answers are 100% correct for cross-platform
# On macOS, requires coreutils (`brew install coreutils`)
local SCRIPT=$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")
REPO_ROOT=$(dirname $(dirname "$SCRIPT"))
readonly REPO_ROOT
}
# Used to build script-related build variables useful for referencing the calling script
# and for prefixing `builder_finish_action` outputs in order to more clearly identify the calling
# script.
#
# Assumes that `THIS_SCRIPT` has been set, typically like this:
#
# ```bash
# ## START STANDARD BUILD SCRIPT INCLUDE
# # adjust relative paths as necessary
# THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")"
# . "$(dirname "$THIS_SCRIPT")/resources/builder.inc.sh"
# ## END STANDARD BUILD SCRIPT INCLUDE
# ```
#
function _builder_setBuildScriptIdentifiers() {
if [ ! -z ${THIS_SCRIPT+x} ]; then
THIS_SCRIPT_PATH="$(dirname "$THIS_SCRIPT")"
readonly THIS_SCRIPT_PATH
THIS_SCRIPT_NAME="$(basename "$THIS_SCRIPT")"
readonly THIS_SCRIPT_NAME
# Leaves only the part of the path based upon REPO_ROOT.
THIS_SCRIPT_IDENTIFIER=${THIS_SCRIPT_PATH#"$REPO_ROOT/"}
readonly THIS_SCRIPT_IDENTIFIER
else
echo "Warning: THIS_SCRIPT not defined; builder.inc.sh has not been sourced with standard script include."
fi
}
################################################################################
# Standard build script functions for managing command line, actions and targets
################################################################################
# The following allows coloring of warning and error lines, but only works if there's a
# terminal attached, so not on the build machine.
# Overrides default colorization of logging; can be used in command-line with
# --color or --no-color, or overridden as necessary on a per-script basis.
#
# Parameters
# 1: use_color true or false
builder_use_color() {
if $1; then
COLOR_RED=$(tput setaf 1)
COLOR_GREEN=$(tput setaf 2)
COLOR_YELLOW=$(tput setaf 3)
COLOR_BLUE=$(tput setaf 4)
COLOR_PURPLE=$(tput setaf 5)
COLOR_TEAL=$(tput setaf 6)
COLOR_WHITE=$(tput setaf 7)
COLOR_GREY=$(tput setaf 8)
COLOR_RESET=$(tput sgr0)
# e.g. VSCode https://code.visualstudio.com/updates/v1_69#_setmark-sequence-support
HEADING_SETMARK='\x1b]1337;SetMark\x07'
# Used by `builder_display_usage` when marking special terms (actions, targets, options)
# in the plain-text description area.
BUILDER_TERM_START="$COLOR_BLUE"
BUILDER_TERM_END="$COLOR_RESET"
else
COLOR_RED=
COLOR_GREEN=
COLOR_YELLOW=
COLOR_BLUE=
COLOR_PURPLE=
COLOR_TEAL=
COLOR_WHITE=
COLOR_GREY=
COLOR_RESET=
HEADING_SETMARK=
BUILDER_TERM_START="<"
BUILDER_TERM_END=">"
fi
}
function builder_die() {
echo
echo "${COLOR_RED}$*"
echo
exit 1
}
####################################################################################
#
# builder_ functions for standard build script parameter and process management
#
####################################################################################
#
# builder_ names are reserved.
# _builder_ names are internal use and subject to change
#
_builder_debug=false
#
# builder_extra_params: string containing all parameters after '--'
#
builder_extra_params=()
# returns 0 if first parameter is in the array passed as second parameter
#
# Usage:
# if _builder_item_in_array "item" "${array[@]}"; then ...; fi
# Parameters:
# 1: item item to search for in array
# 2: array bash array, e.g. array=(one two three)
_builder_item_in_array() {
local e match="$1"
shift
[[ -z "$match" ]] && return 1
for e; do [[ "$e" == $match ]] && return 0; done
return 1
}
#
# Returns `0` if first parameter is in the array passed as second parameter,
# where the array may contain globs.
#
# ### Parameters
#
# * 1: `item` item to search for in array
# * 2: `array` bash array, e.g. `array=(one two three)`
#
# ### Example
#
# ```bash
# array=(foo bar it*)
# if _builder_item_in_glob_array "item" "${array[@]}"; then ...; fi
# ```
#
_builder_item_in_glob_array() {
local e match="$1"
shift
[[ -z "$match" ]] && return 1
for e; do [[ "$match" == $e ]] && return 0; done
return 1
}
_builder_item_is_target() {
local item="$1"
[[ $item =~ ^: ]] && return 1
return 0
}
function _builder_warn_if_incomplete() {
if [ -n "${_builder_current_action}" ]; then
local scope="[$THIS_SCRIPT_IDENTIFIER] "
echo "${COLOR_YELLOW}## ${scope}Warning - $_builder_current_action never reported success or failure${COLOR_RESET}"
# exit 1 # If we wanted this scenario to result in a forced build-script fail.
fi
# Since we've already warned about this once, we'll clear the variable to prevent repetitions.
_builder_current_action=
}
# Used by a `trap` statement later to facilitate auto-reporting failures on error detection
# without obscuring failure exit/error codes.
_builder_failure_trap() {
local trappedExitCode=$?
local action target
_builder_cleanup_deps
# Since 'exit' is also trapped, we can also handle end-of-script incomplete actions.
if [[ $trappedExitCode == 0 ]]; then
# While there weren't errors, were there any actions that never reported success or failure?
_builder_warn_if_incomplete
return
fi
# If we've reached this point, we're here because an error occurred.
# Iterate across currently-active actions and report their failures.
if [ -n "${_builder_current_action}" ]; then
action="${_builder_current_action}"
if [[ $action =~ : ]]; then
IFS=: read -r action target <<< $action
target=:$target
else
target=:project
fi
builder_finish_action failure $action$target
# Make 100% sure that the exit code chains fully.
# Without this, nested scripts have failed to chain errors from npm calls past the script
# that directly executed the failed npm command.
exit $trappedExitCode
fi
}
#
# Removes temporary `_builder_deps_built` file when top-level build script
# finishes.
#
_builder_cleanup_deps() {
if ! builder_is_dep_build && [[ ! -z ${_builder_deps_built+x} ]]; then
if $_builder_debug; then
echo "[DEBUG] Dependencies that were built:"
cat "$_builder_deps_built"
fi
rm -f "$_builder_deps_built"
_builder_deps_built=
fi
}
#
# Builds the standardized `action:target` string for the specified action-target
# pairing and also returns 0 if the user has asked to perform it on the command
# line. Otherwise, returns 0 and sets an empty string in place of the matched
# pair.
#
# The string will be set as `_builder_matched_action`, which is for
# builder.inc.sh internal use, used by `builder_start_action`.
#
# ### Usage
#
# ```bash
# if build_has_action action[:target]; then ...; fi
# ````
#
# Parameters:
# 1: action[:target] name of action:target
# Example:
#
# ```bash
# if builder_has_action build:app; then ...
# ```
#
builder_has_action() {
local action="$1" target
if [[ $action =~ : ]]; then
IFS=: read -r action target <<< $action
target=:$target
else
target=':*'
fi
if _builder_item_in_array "$action$target" "${_builder_chosen_action_targets[@]}"; then
# To avoid WET re-processing of the $action$target string set
_builder_matched_action="$action$target"
if [[ $target == ':*' ]]; then
_builder_matched_action_name="$action"
else
_builder_matched_action_name="$action$target"
fi
return 0
else
_builder_matched_action=
return 1
fi
}
#
# Returns `0` if the user has asked to perform action on target on the command
# line, and then starts the action. Should be paired with
# `builder_finish_action`.
#
# ### Usage
#
# ```bash
# if builder_start_action action[:target]; then ...; fi
# ```
#
# ### Parameters
#
# * 1: `action[:target]` name of action, and optionally also target, if
# target excluded starts for all defined targets
#
# ### Example
#
# ```bash
# if builder_start_action build:app; then ...
# ```
#
builder_start_action() {
local scope="[$THIS_SCRIPT_IDENTIFIER] "
if builder_has_action $1; then
# In a dependency quick build (the default), determine whether we actually
# need to run this step. Uses data passed to builder_describe_outputs to
# verify whether a target output is present.
if builder_is_dep_build &&
! builder_is_full_dep_build &&
[[ ! -z ${_builder_dep_path[$_builder_matched_action]+x} ]] &&
[[ -e "$KEYMAN_ROOT/${_builder_dep_path[$_builder_matched_action]}" ]]; then
echo "$scope skipping $_builder_matched_action_name, up-to-date"
return 1
fi
echo "${COLOR_BLUE}## $scope$_builder_matched_action_name starting...${COLOR_RESET}"
if [ -n "${_builder_current_action}" ]; then
_builder_warn_if_incomplete
fi
_builder_current_action="$_builder_matched_action"
# Build dependencies as required
_builder_do_build_deps "$_builder_matched_action"
return 0
else
return 1
fi
}
#
# Returns 0 if the user has --option on the command line
#
# Usage:
# if build_has_option option; then ...; fi
# Parameters:
# 1: option name of option, i.e. --option
# Example:
# if build_has_option --debug; then
#
builder_has_option() {
local option="$1"
if _builder_item_in_array "$option" "${_builder_chosen_options[@]}"; then
return 0
fi
return 1
}
_builder_trim() {
local var="$*"
# remove leading whitespace characters
var="${var#"${var%%[![:space:]]*}"}"
# remove trailing whitespace characters
var="${var%"${var##*[![:space:]]}"}"
printf '%s' "$var"
}
#
# Expands an in-repo-relative path to a repo-relative path. A path starting with
# `/` is expected to be relative to repo root, not filesystem root. Otherwise,
# it's relative to current script path, not current working directory. The
# returned path will not have a prefix `/`, and will be relative to
# `$KEYMAN_ROOT`. Assumes realpath is installed (brew coreutils on macOS).
#
_builder_expand_relative_path() {
local path="$1"
if [[ "$path" =~ ^/ ]]; then
echo "${path:1}"
else
realpath --canonicalize-missing --relative-to="$KEYMAN_ROOT" "$THIS_SCRIPT_PATH/$path"
fi
}
#
# Expands an `[action][:target]` string, replacing missing values with `*`,
# for example:
#
# * `build` --> `build:*`
# * `build:app` --> `build:app`
# * `:app` --> `*:app`
#
# Supports multiple action:targets in the string
#
_builder_expand_action_target() {
local input="$1" target= action=
if [[ "$input" =~ : ]]; then
action=$(echo "$input" | cut -d: -f 1 -)
target=$(echo "$input" | cut -d: -f 2 -)
else
action=$input
fi
if [[ -z "$action" ]]; then
action='*'
fi
if [[ -z "$target" ]]; then
target='*'
fi
echo "$action:$target"
}
_builder_expand_action_targets() {
local input=($1) e output=()
for e in "${input[@]}"; do
e=`_builder_expand_action_target "$e"`
output+=($e)
done
if [[ ${#output[@]} == 0 ]]; then
echo "*:*"
else
echo "${output[@]}"
fi
}
#
# Describes a build script, defines available parameters and their meanings. Use
# together with `builder_parse` to process input parameters.
#
# ### Usage
#
# ```bash
# builder_describe description param_desc...
# ```
#
# ### Parameters
#
# * `description` A short description of what the script does.
# * `param_desc` Space separated name and description of parameter, e.g.
# `"build Builds the target"`
# This parameter may be repeated to describe all parameters.
#
# There are four types of parameters that may be specified:
#
# * **Option:** `"--option[,-o][=var] [One line description]"`
#
# All options must have a longhand form with two prefix hyphens,
# e.g. `--option`. The `,-o` shorthand form is optional. When testing if
# the option is set with `builder_has_option`, always use the longhand
# form.
#
# if `=var` is specified, then the next parameter will be a variable stored in
# `$var` for that option. e.g. `--option=opt` means `$opt` will have the value
# `"foo"` when the script is called for `--option foo`.
#
# * **Action**: `"action [One line description]"`
#
# Actions must be a single word, lower case. To specify an action as the
# default, append a `+` to the action name, e.g. `"test+ Test the project"`.
# If there is no default specified, then it will be `build`.
#
# * **Target:** `":target [One line description]"`
#
# A target always starts with colon, e.g. `:project`.
#
# * **Dependency:** "@/path/to/dependency [action][:target] ..."
#
# A dependency always starts with `@`. The path to the dependency will be
# relative to the build script folder, or to the root of the repository, if
# the path starts with `/`, not to the root of the file system. It is an error
# to specify a dependency outside the repo root.
#
# Relative paths will be expanded to full paths, again, relative to the root
# of the repository.
#
# Dependencies may be limited to specific `action:target`. If not specified,
# dependencies will be built for all actions on all targets. Either `action`
# or `:target` may be omitted, and multiple actions and targets may be
# specified, space separated.
#
builder_describe() {
_builder_description="$1"
_builder_actions=()
_builder_targets=()
_builder_options=()
_builder_deps=() # array of all dependencies for this script
_builder_default_action=build
declare -A -g _builder_params
declare -A -g _builder_options_short
declare -A -g _builder_options_var
declare -A -g _builder_dep_path # array of output files for action:target pairs
declare -A -g _builder_dep_related_actions # array of action:targets associated with a given dependency
declare -A -g _builder_internal_dep # array of internal action:targets dependency relationships
shift
# describe each target, action, and option possibility
while [[ $# -gt 0 ]]; do
local key="$1"
local value="$(echo "$key" | cut -d" " -f 1 -)"
local description=
if [[ $key =~ [[:space:]] ]]; then
description="$(_builder_trim "$(echo "$key" | cut -d" " -f 2- -)")"
fi
if [[ $value =~ ^: ]]; then
# Parameter is a target
_builder_targets+=($value)
elif [[ $value =~ ^@ ]]; then
# Parameter is a dependency
local dependency="${value:1}"
dependency="`_builder_expand_relative_path "$dependency"`"
_builder_deps+=($dependency)
_builder_dep_related_actions[$dependency]="`_builder_expand_action_targets "$description"`"
# We don't want to add deps to params, so shift+continue
shift
continue
elif [[ $value =~ ^-- ]]; then
# Parameter is an option
# Look for a shorthand version of the option
local option_var=
if [[ $value =~ = ]]; then
option_var="$(echo "$value" | cut -d= -f 2 -)"
value="$(echo "$value" | cut -d= -f 1 -)"
fi
if [[ $value =~ , ]]; then
local option_long="$(echo "$value" | cut -d, -f 1 -)"
local option_short="$(echo "$value" | cut -d, -f 2 -)"
_builder_options+=($option_long)
_builder_options_short[$option_short]="$option_long"
if [[ ! -z "$option_var" ]]; then
_builder_options_var[$option_long]="$option_var"
fi
value="$option_long, $option_short"
else
_builder_options+=($value)
if [[ ! -z "$option_var" ]]; then
_builder_options_var[$value]="$option_var"
fi
fi
if [[ ! -z $option_var ]]; then
value="$value $option_var"
fi
else
# Parameter is an action
if [[ $value =~ \+$ ]]; then
# If the action name has a '+' suffix then it is the default action
value=${value//+}
_builder_default_action=$value
fi
_builder_actions+=($value)
fi
if [[ -z "${description}" ]]; then
description=$(_builder_get_default_description "$value")
fi
_builder_params[${value}]="$description"
shift
done
# We'll always add a :project if no target is specified
if (( ! ${#_builder_targets[@]} )); then
_builder_targets+=(:project)
_builder_params[\:project]=$(_builder_get_default_description ":project")
fi
}
#
# Defines an output file or folder expected to be present after successful
# completion of an action for a target. Used to skip actions for dependency
# builds. If `:target` is not provided, assumes `:project`.
#
# Relative paths are relative to script folder; absolute paths are relative
# to repository root, not filesystem root.
#
# ### Usage
#
# ```bash
# builder_describe_outputs action:target filename [...]
# ```
#
# ### Parameters
#
# * 1: `action[:target]` action and/or target associated with file
# * 2: `filename` name of file or folder to check
# * 3+: ... repeat previous arguments for additional outputs
#
# ### Example
#
# ```bash
# builder_describe_outputs \
# configure /node_modules \
# build build/index.js
# ```
#
function builder_describe_outputs() {
while [[ $# -gt 0 ]]; do
local key="$1" path="$2" action target
path="`_builder_expand_relative_path "$path"`"
if [[ $key =~ : ]]; then
action="$(echo "$key" | cut -d: -f 1 -)"
target=":$(echo "$key" | cut -d: -f 2 -)"
else
# Add dependency expected output file for all targets, as well as a
# wildcard target match
action="$key"
for target in "${_builder_targets[@]}"; do
_builder_dep_path[$action$target]="$path"
done
target=':*'
fi
_builder_dep_path[$action$target]="$path"
shift 2
done
_builder_define_default_internal_dependencies
}
_builder_get_default_description() {
local description=
local value="$1"
# default descriptions for common build actions, targets, and options
case "$value" in
clean) description="remove build/ folder and build artifacts" ;;
configure) description="install dependencies, e.g. npm" ;;
build) description="build target(s)" ;;
test) description="run automated tests" ;;
:project) description="this project" ;;
:app) description="main app" ;;
:engine) description="engine module" ;;
:module) description="this module" ;;
:tools) description="build tools for this project" ;;
--debug) description="debug build" ;;
esac
echo "$description"
}
_builder_parameter_error() {
local program="$1"
local type="$2"
local param="$3"
echo "$COLOR_RED$program: invalid $type: $param$COLOR_RESET"
echo
builder_display_usage
exit 64
}
# Pre-initializes the color setting based on the options specified to a
# a build.sh script, parsing the command line to do so. This is only
# needed if said script wishes to use this script's defined colors while
# respecting the options provided by the script's caller.
#
# Usage:
# builder_check_color "$@"
# Parameters
# 1: $@ all command-line arguments (as with builder_parse)
builder_check_color() {
# Process command-line arguments
while [[ $# -gt 0 ]] ; do
local key="$1"
case "$key" in
--color)
builder_use_color true
;;
--no-color)
builder_use_color false
;;
esac
shift # past the processed argument
done
}
#
# For every build action:target in _builder_chosen_action_targets, add
# its full internal dependency tree
#
_builder_add_chosen_action_target_dependencies() {
local action_target e i=0 new_actions=()
# Iterate through every action specified on command line; we use this loop
# style so that any new actions added here will also be iteratively checked
while (( $i < ${#_builder_chosen_action_targets[@]} )); do
action_target=${_builder_chosen_action_targets[$i]}
# If we have an internal dependency for the chosen action:target pair, add
# it to the list, but only if there is a defined output and that output is
# missing
if [[ ! -z ${_builder_internal_dep[$action_target]+x} ]]; then
local dep_output=${_builder_internal_dep[$action_target]}
if [[ ! -z ${_builder_dep_path[$dep_output]+x} ]] &&
[[ ! -e "$KEYMAN_ROOT/${_builder_dep_path[$dep_output]}" ]]; then
if ! _builder_item_in_array "$dep_output" "${_builder_chosen_action_targets[@]}"; then
_builder_chosen_action_targets+=($dep_output)
new_actions+=($dep_output)
fi
fi
fi
i=$((i + 1))
done
if [[ ${#new_actions[@]} -gt 0 ]]; then
echo "Automatically running following required actions with missing outputs:"
for e in "${new_actions[@]}"; do
echo "* $e"
done
fi
}
#
# If we have described outputs, then we will setup our
# default internal dependency chain:
#
# configure <- build <- (test,install,publish)
#
_builder_define_default_internal_dependencies() {
for target in "${_builder_targets[@]}"; do
_builder_define_default_internal_deps_for_target "$target"
done
_builder_define_default_internal_deps_for_target ':*'
}
_builder_define_default_internal_deps_for_target() {
local target=$1
_builder_define_default_internal_dep "$target" configure build
_builder_define_default_internal_dep "$target" build test
_builder_define_default_internal_dep "$target" build install
}
_builder_define_default_internal_dep() {
local target=$1 dep=$2 action=$3
if _builder_item_in_array $dep "${_builder_actions[@]}" &&
_builder_item_in_array $action "${_builder_actions[@]}"; then
_builder_internal_dep[$action$target]=$dep$target
fi
}
# Initializes a build.sh script, parses command line. Will abort the script if
# invalid parameters are passed in. Use together with builder_describe which
# sets up the possible command line parameters
#
# Usage:
# builder_parse "$@"
# Parameters
# 1: $@ command-line arguments
builder_parse() {
_builder_build_deps=--deps
builder_verbose=
builder_debug=
builder_extra_params=()
_builder_chosen_action_targets=()
_builder_chosen_options=()
_builder_current_action=
# Process command-line arguments
while [[ $# -gt 0 ]] ; do
local key="$1"
local action=
local target=
local e has_action has_target has_option longhand_option
if [[ $key == "--" ]]; then
shift
builder_extra_params=("$@")
break
fi
if [[ $key =~ : ]]; then
IFS=: read -r action target <<< $key
target=:$target
else
action="$key"
target=
fi
_builder_item_in_array "$action" "${_builder_actions[@]}" && has_action=1 || has_action=0
_builder_item_in_array "$target" "${_builder_targets[@]}" && has_target=1 || has_target=0
# Expand short -o to --option in options lookup
if [[ ! -z ${_builder_options_short[$key]+x} ]]; then
key=${_builder_options_short[$key]}
fi
_builder_item_in_array "$key" "${_builder_options[@]}" && has_option=1 || has_option=0
if (( has_action )) && (( has_target )); then
# apply the selected action and selected target
_builder_chosen_action_targets+=("$key")
elif (( has_action )); then
# apply the selected action to all targets
if [[ ! -z $target ]]; then
# A target was specified but is not valid
_builder_parameter_error "$0" target "$target"
fi
for e in "${_builder_targets[@]}"; do
_builder_chosen_action_targets+=("$action$e")
done
elif (( has_target )); then
# apply the default action to the selected target
if [[ ! -z $action ]]; then
# An action was specified but is not valid
_builder_parameter_error "$0" action "$action"
fi
_builder_chosen_action_targets+=("$_builder_default_action$target")
elif (( has_option )); then
_builder_chosen_options+=("$key")
if [[ ! -z ${_builder_options_var[$key]+x} ]]; then
shift
if [[ $# -eq 0 ]]; then
_builder_parameter_error "$0" parameter "$key"
fi
# Set the variable associated with this option to the next parameter value
# A little bit of hoop jumping here to avoid issues with cygwin paths being
# corrupted too early in the game
local varname=${_builder_options_var[$key]}
declare -g $varname="$1"
fi
else
case "$key" in
--help|-h)
builder_display_usage
exit 0
;;
--color)
builder_use_color true
;;
--no-color)
builder_use_color false
;;
--verbose|-v)
_builder_chosen_options+=(--verbose)
builder_verbose=--verbose
;;
--debug|-d)
_builder_chosen_options+=(--debug)
builder_debug=--debug
;;
--deps|--no-deps|--force-deps)
_builder_build_deps=$key
;;
--builder-dep-parent)
# internal use parameter for dependency builds - identifier of parent script
shift
builder_dep_parent="$1"
;;
--builder-deps-built)
# internal use parameter for dependency builds - path to dependency tracking file
shift
_builder_deps_built="$1"
;;
--builder-report-dependencies)
# internal reporting function, ignores all other parameters
_builder_report_dependencies
;;
*)
_builder_parameter_error "$0" parameter "$key"
esac
fi
shift # past the processed argument
done
if (( ! ${#_builder_chosen_action_targets[@]} )); then
for e in "${_builder_targets[@]}"; do
_builder_chosen_action_targets+=("$_builder_default_action$e")
done
fi
_builder_add_chosen_action_target_dependencies
if $_builder_debug; then
echo "[DEBUG] Selected actions and targets:"
for e in "${_builder_chosen_action_targets[@]}"; do
echo "* $e"
done
echo
echo "[DEBUG] Selected options:"
for e in "${_builder_chosen_options[@]}"; do
echo "* $e"
done
fi
if builder_is_dep_build; then
echo "[$THIS_SCRIPT_IDENTIFIER] dependency build, started by $builder_dep_parent"
if [[ -z ${_builder_deps_built+x} ]]; then
echo "FATAL ERROR: Expected --builder-deps-built parameter"
exit 1
fi
else
# This is a top-level invocation, not a dependency build, so we want to
# track which dependencies have been built, so they don't get built multiple
# times.
_builder_deps_built=`mktemp`
fi
# Now that we've successfully parsed options adhering to the _builder spec, we may activate our
# action_failure and action_hanging traps. (We don't want them active on scripts not yet using
# said script.)
#
# Note: if an error occurs within a script's function in a `set -e` script, it becomes an exit
# instead for the function's caller. So, we need both `err` and `exit` here.
# See https://medium.com/@dirk.avery/the-bash-trap-trap-ce6083f36700.
trap _builder_failure_trap err exit
}
_builder_pad() {
local count=$1
local text1=$2
local text2=$3
local fmt="%-${count}s%s\n"
printf $fmt "$text1" "$text2"
}
builder_display_usage() {
local e program description
# Minimum padding is 12 characters, increase this if necessary
# if you add other, longer, global options (like --verbose, --debug)
local width=12
for e in "${!_builder_params[@]}"; do
if (( ${#e} > $width )); then
width=${#e}
fi
done
width=$((width + 6))
program="$(basename "$0")"
if [[ ! -z ${_builder_description+x} ]]; then
echo "Summary:"
echo " $_builder_description"
echo
fi
echo "Script Identifier:"
echo " $THIS_SCRIPT_IDENTIFIER"
echo
echo "Usage:"
echo " $program [options...] [action][:target]..."
echo
echo "Actions: "
for e in "${_builder_actions[@]}"; do
if [[ -v _builder_params[$e] ]]; then
description="${_builder_params[$e]}"
else
description=$(_builder_get_default_description "$e")
fi
_builder_pad $width " $e" "$description"
done
echo
echo "Targets: "
for e in "${_builder_targets[@]}"; do
if [[ -v _builder_params[$e] ]]; then
description="${_builder_params[$e]}"
else
description=$(_builder_get_default_description "$e")
fi
_builder_pad $width " $e" "$description"
done
echo
echo "Options: "
for e in "${!_builder_params[@]}"; do
if [[ $e =~ ^-- ]]; then
_builder_pad $width " $e" "${_builder_params[$e]}"
fi
done
_builder_pad $width " --verbose, -v" "Verbose logging"
_builder_pad $width " --debug, -d" "Debug build"
_builder_pad $width " --color" "Force colorized output"
_builder_pad $width " --no-color" "Never use colorized output"
if builder_has_dependencies; then
_builder_pad $width " --deps" "Build dependencies if required (default)"
_builder_pad $width " --no-deps" "Skip build of dependencies"
_builder_pad $width " --force-deps" "Reconfigure and rebuild all dependencies"
fi
_builder_pad $width " --help, -h" "Show this help"
echo
echo "Dependencies: "
if builder_has_dependencies; then
for d in "${_builder_deps[@]}"; do
echo " $d"
done
else
echo " This module has no dependencies"
fi
# Defined in `builder_use_color`; this assumes that said func has been called.
local c1=$BUILDER_TERM_START
local c0=$BUILDER_TERM_END
echo
echo "* Specify ${c1}action:target${c0} to run a specific ${c1}action${c0} against a specific ${c1}:target${c0}."
echo "* If ${c1}action${c0} is specified without a ${c1}target${c0} suffix, it will be applied to all ${c1}:target${c0}s."
echo "* If ${c1}:target${c0} is specified without an ${c1}action${c0} prefix, ${c1}$_builder_default_action:target${c0} will be inferred."
echo "* If no ${c1}action${c0}, ${c1}:target${c0}, or ${c1}action:target${c0} entries are specified, ${c1}$_builder_default_action${c0} will run on all ${c1}:target${c0}s."
echo
}
builder_finish_action() {
local result="$1"
local action="$2" target action_name
if [[ $action =~ : ]]; then
IFS=: read -r action target <<< $action
target=":$target"
else
target=':*'
fi
if [[ "$target" == ":*" ]]; then
action_name="$action"
else
action_name="$action$target"
fi
local scope="[$THIS_SCRIPT_IDENTIFIER] "
if [[ "$action$target" == "${_builder_current_action}" ]]; then
if [[ $result == success ]]; then
echo "${COLOR_GREEN}## $scope$action_name completed successfully${COLOR_RESET}"
elif [[ $result == failure ]]; then
echo "${COLOR_RED}## $scope$action_name failed${COLOR_RESET}"
else
echo "${COLOR_RED}## $scope$action_name failed with message: $result${COLOR_RESET}"
fi
# Remove $action$target from the array; it is no longer a current action
_builder_current_action=
else
echo "${COLOR_YELLOW}## Warning: reporting result of $action_name but the action was never started!${COLOR_RESET}"
fi
}
#
# Returns `0` if the dependency should be built for the given action:target
#
_builder_should_build_dep() {
local action_target="$1"
local dep="$2"
local related_actions=(${_builder_dep_related_actions[$dep]})
# echo "bdra: ${_builder_dep_related_actions[@]}"
# echo "target: $action_target"
# echo "dep: $2"
# echo "ra: ${related_actions[@]}"
if ! _builder_item_in_glob_array "$action_target" "${related_actions[@]}"; then
return 1
fi
return 0
}
#
# Configure and build all dependencies
# Later, may restrict by either action or target
#
_builder_do_build_deps() {
local action_target="$1"
if [[ $_builder_build_deps == --no-deps ]]; then
# we've been asked to skip dependencies
return 0
fi
for dep in "${_builder_deps[@]}"; do
# Don't attempt to build dependencies that don't match the current
# action:target (wildcards supported for matches here)
if ! _builder_should_build_dep "$action_target" "$dep"; then
echo "[$THIS_SCRIPT_IDENTIFIER] Skipping dependency build $dep for $_builder_matched_action_name"
continue
fi
# Only configure and build the dependency once per invocation
if builder_has_module_been_built "$dep"; then
continue
fi
# TODO: add --debug as a standard builder parameter
builder_set_module_has_been_built "$dep"
"$KEYMAN_ROOT/$dep/build.sh" configure build \
$builder_verbose \
$builder_debug \
$_builder_build_deps \
--builder-deps-built "$_builder_deps_built" \
--builder-dep-parent "$THIS_SCRIPT_IDENTIFIER"
done
}
#
# returns `0` if we are in a dependency doing a build.
#
builder_is_dep_build() {
if [[ ! -z ${builder_dep_parent+x} ]]; then
return 0
fi
return 1
}
#
# returns `0` if we should attempt to do quick builds in a dependency build, for
# example skipping `tsc -b` where a parent may also do it; corresponds to the
# `--deps` parameter (which is the default).
#
builder_is_quick_dep_build() {
if builder_is_dep_build && [[ $_builder_build_deps == --deps ]]; then
return 0
fi
return 1
}
#
# returns `0` if we should do a full configure and build in a dependency build;
# corresponds to the `--force-deps`` parameter.
#
builder_is_full_dep_build() {
if builder_is_dep_build && [[ $_builder_build_deps == --force-deps ]]; then
return 0
fi
return 1
}
#
# returns `0` if the current build script has at least one dependency.
#
builder_has_dependencies() {
if [[ ${#_builder_deps[@]} -eq 0 ]]; then
return 1
fi
return 0
}
#
# Tests if a dependency module has been built already in the current script
# invocation; if not running in a builder context, always returns `1` (i.e.
# "false").
#
# ### Usage
#
# ```bash
# builder_has_module_been_built dependency-name
# ```
#
# ### Parameters
#
# * 1: `dependency-name` the `$SCRIPT_IDENTIFIER` of the dependency
# (repo-relative path without leading `/`); or for
# external dependencies, a path-like starting with
# `/external/`.
#
# ### Examples
#
# ```bash
# if builder_has_module_been_built common/web/keyman-version; then ...
# if builder_has_module_been_built /external/npm-ci; then ...
# ```
#
builder_has_module_been_built() {
local module="$1"
if [[ -z ${_builder_deps_built+x} ]]; then
# not in a builder context, so we assume a build is needed
return 1
fi
if [[ -f $_builder_deps_built ]] && grep -qx "$module" $_builder_deps_built; then
# dependency history file contains the dependency module
return 0
fi
return 1
}
#
# Updates the dependency module build state for the current script invocation;
# if not running in a builder context, a no-op.
#
# ### Usage
#
# ```bash
# builder_set_module_has_been_built dependency-name
# ```
#
# ### Parameters
#
# * 1: `dependency-name` the `$SCRIPT_IDENTIFIER` of the dependency
# (repo-relative path without leading `/`); or for
# external dependencies, a path-like starting with
# `/external/`.
#
# ### Examples
#
# ```bash
# builder_set_module_has_been_built common/web/keyman-version
# builder_set_module_has_been_built /external/npm-ci
# ```
#
builder_set_module_has_been_built() {
local module="$1"
if [[ ! -z ${_builder_deps_built+x} ]]; then
echo "$module" >> $_builder_deps_built
fi
}
#
# returns `0` if we should be verbose in output
#
builder_verbose() {
if [[ $builder_verbose == --verbose ]]; then
return 0
fi
return 1
}
#
# returns `0` if we are doing a debug build
#
builder_debug() {
if [[ $builder_debug == --debug ]]; then
return 0
fi
return 1
}
#
# Reports on all described dependencies, then exits
# used by builder-controls.sh
#
_builder_report_dependencies() {
echo "${_builder_deps[@]}"
exit 0
}
#
# Initialize builder once all functions are declared
#
_builder_init