Skip to content

chore(common): builder script :target and --option support#6986

Merged
mcdurdin merged 8 commits intomasterfrom
chore/common/builder-script-target-and-option-support
Jul 29, 2022
Merged

chore(common): builder script :target and --option support#6986
mcdurdin merged 8 commits intomasterfrom
chore/common/builder-script-target-and-option-support

Conversation

@mcdurdin
Copy link
Copy Markdown
Member

@mcdurdin mcdurdin commented Jul 25, 2022

Note: I suggest reviewing only the first commit, as the second one is just changing over #!/bin/bash to #!/usr/bin/env bash.

Adds support for :target and --option to the builder_() functions in build-utils.sh, along with a consistent way of describing command line parameters in short-hand form, so we can avoid the display_usage getting out of sync with actual command line parameters.

I opted to tweak the existing usage slightly, reversing parameter order for builder_report, so that we could extend builder_report cleanly to support targets.

The new builder_() functions are builder_describe and builder_parse, which should be used in place for builder_init where you want to provide more detailed command-line options (for many scripts, the defaults provided through builder_init may suffice).

builder_describe lets you set a single-line description of the script, along with a list of parameters: actions, :targets and --options. Options may also specify a shorthand form, e.g. --option,-o. Each parameter may optionally include a short description, separated by space(s) from the parameter name.

build-utils.test.sh tests the functionality of the various functions, and provides some examples of using them.

Defaults:

  • If a target is not specified in builder_describe, a standard target of :project will be available.
  • If the user does not pass an action on the command line, then build will be used.
  • If the user does not pass a target on the command line, all targets will have the actions applied.

Note, it is possible to have targets which do not use all actions. These may be still be specified, but can be ignored. For example, test:tools may not be an action:target that you handle in the script. At this time, the builder_() functions do not attempt to check for this.

Example

Demonstrates some of the functionality available:

builder_describe \
  "Builds my little project." \
  "clean        Cleans up any build artifacts for target(s)" \
  "build        Build the target(s)" \
  "test         Run tests on target(s)" \
  ":app         My app" \
  ":tools       Tools for development of my app" \
  "--power,-p   Use powerful mode" \
  "--zoom,-z    Use zoom mode"

builder_parse "$@"

USE_POWER=false
builder_has_option --power && USE_POWER=true

if builder_has_action clean :app; then
  npm run clean
  builder_report success clean
fi

if builder_has_action build :app; then
  npm run build
  builder_report success build
fi

if builder_has_action build :tools; then
  ./tools/build.sh build:tools
  builder_report success build
fi

Sample command lines:

  • ./build.sh -p build:app
  • ./build.sh --zoom clean build:tools

😁

@keymanapp-test-bot skip

Adds support for :target and --option to the builder_() functions in
build-utils.sh, along with a consistent way of describing command line
parameters in short-hand form, so we can avoid the display_usage getting
out of sync with actual command line parameters.

I opted to tweak the existing usage slightly, reversing parameter order
for `builder_report`, so that we could extend `builder_report` cleanly
to support targets.

The new builder_() functions are `builder_describe` and `builder_parse`,
which should be used in place for `builder_init` where you want to
provide more detailed command-line options (for many scripts, the
defaults provided through `builder_init` may suffice).

`builder_describe` lets you set a single-line description of the script,
along with a list of parameters: `actions`, `:targets` and `--options`.
Options may also specify a shorthand form, e.g. `--option,-o`. Each
parameter may optionally include a short description, separated by
space(s) from the parameter name.

build-utils.test.sh tests the functionality of the various functions,
and provides some examples of using them.

Defaults:
* If a target is not specified in `builder_describe`, a standard target
  of `:project` will be available.
* If the user does not pass an action on the command line, then `build`
  will be used.
* If the user does not pass a target on the command line, all targets
  will have the actions applied.

Note, it is possible to have targets which do not use all actions. These
may be still be specified, but can be ignored. For example, `test:tools`
may not be an action:target that you handle in the script. At this time,
the builder_() functions do not attempt to check for this.
@mcdurdin mcdurdin added this to the A16S7 milestone Jul 25, 2022
@keymanapp-test-bot
Copy link
Copy Markdown

keymanapp-test-bot bot commented Jul 25, 2022

@mcdurdin mcdurdin changed the title chore(common): builder script :target and --option support chore(common): builder script :target and --option support Jul 25, 2022
Fixes up scripts (except under /linux) to use `#!/usr/bin/env bash`
instead of `#!/bin/bash` or `#!/bin/sh` so that we don't end up with
the ancient version of bash supplied with macOS.

This became urgent with this PR, because of bash-4.xisms in
build-utils.sh, for example on line 572:

```
if [[ -v _builder_params[$e] ]]; then
```
@mcdurdin mcdurdin force-pushed the chore/common/builder-script-target-and-option-support branch from f5ba9b4 to 767927b Compare July 25, 2022 05:15
# TODO: we should try and rework this to avoid the need to manually wrap

if builder_has_action clean || builder_has_action build; then
if builder_has_action clean || builder_has_action build >/dev/null; then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a >/dev/null here intentionally? I'm not clear on why it's needed here but not in many other places.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, because the builder_has_action build was triggering a build status message here that was confusing -- because we have to do the clean even for build, see comments in previous lines... I'm not that happy with this, and a --quiet option may be better, but this will do for now I think.

Copy link
Copy Markdown
Contributor

@jahorton jahorton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example

Demonstrates some of the functionality available:

builder_describe \
  "Builds my little project." \
  "clean        Cleans up any build artifacts for target(s)" \
  "build        Build the target(s)" \
  "test         Run tests on target(s)" \
  ":app         My app" \
  ":tools       Tools for development of my app" \
  "--power,-p   Use powerful mode" \
  "--zoom,-z    Use zoom mode"

builder_parse "$@"

USE_POWER=false
builder_has_option --power && USE_POWER=true

if builder_has_action clean :app; then
  npm run clean
  builder_report success clean
fi

if builder_has_action build :app; then
  npm run build
  builder_report success build
fi

if builder_has_action build :tools; then
  ./tools/build.sh build:tools
  builder_report success build
fi

One major question about all of this that's actually addressable through this example:

Suppose I use this script with a simple build.sh - no parameters whatsoever. Are any of the explicitly-defined actions here run in that case? None of them are set against :project.

@mcdurdin
Copy link
Copy Markdown
Member Author

Suppose I use this script with a simple build.sh - no parameters whatsoever. Are any of the explicitly-defined actions here run in that case? None of them are set against :project.

Good question.

  • The target :project is only defined if there are no other targets defined in builder_describe.

  • If you do not specify a target in your command line call, then all targets will be included.

  • The default action is currently build (ref line 535 of build-utils.sh). (Perhaps the default action should be --help, or all actions?). Now that I look at this, this is probably not right -- it applies build to all targets, not just the ones (if any) specified on the command line...

    # TODO: not sure if this is appropriate or if we should error?
    if (( ! ${#_builder_chosen_action_targets[@]} )); then
    for e in "${_builder_targets[@]}"; do
    _builder_chosen_action_targets+=("build$e")
    done
    fi

builder_init was unnecessarily and inconsistently duplicating
builder_describe and builder_parse functionality. Removing it -- all
scripts should use builder_describe and builder_parse instead, which
gives us more flexibility and consistency into the future.
@jahorton
Copy link
Copy Markdown
Contributor

jahorton commented Jul 27, 2022

  • The default action is currently build (ref line 535 of build-utils.sh). (Perhaps the default action should be --help, or all actions?).

Why not...

_builder_default_action="build"

builder_default_action() {
  _builder_default_action="$1"
}

# ...
  local option_default

# [line 512ish:  block replacement]
  # TODO: not sure if this is appropriate or if we should error? 
  if (( ! ${#_builder_chosen_action_targets[@]} )); then 
    _builder_item_in_array "$_builder_default_action" "${_builder_actions[@]}" || fail "Default action not available"

    # To facilitate `builder_default_action --help` in a dependent script.
    option_default=_builder_item_in_array "$_builder_default_action" "${_builder_options[@]}" && 1 || 0

    if (( option_default )); then
      _builder_chosen_options+=("$_builder_default_action")
    else
      #_builder_chosen_targets is to-be-defined, in line with other concerns of the quoted comment.
      for e in "${_builder_chosen_targets[@]}"; do 
        _builder_chosen_action_targets+=("$_builder_default_action$e") 
      done 
    fi
  fi 

Or you could go crazier if you wanted and make _builder_default_action an array, allow setting multiple actions, and looping over them all in that block. That's... probably overkill though.

@jahorton
Copy link
Copy Markdown
Contributor

jahorton commented Jul 27, 2022

Re this part:

Now that I look at this, this is probably not right -- it applies build to all targets, not just the ones (if any) specified on the command line...

# ... [line 458 or so]

  local _builder_chosen_targets=()

# ... [line 487]

    elif (( has_target )); then
      for e in "${_builder_actions[@]}"; do
        _builder_chosen_action_targets+=("$e:$target")
        _builder_chosen_targets+=($target)  # the only altered (well, added) line in this block
      done

#...

Would those changes work, in combination with my prior suggestion?

Default action is 'build' unless overridden in builder_describe.
Default target is all targets when no target is specified in command-
line.
@mcdurdin
Copy link
Copy Markdown
Member Author

I've gone a slightly different route for defaults:

  • Default action is 'build' unless overridden in builder_describe, by using a '+' suffix on the default action, e.g. "test+ Test this project".
  • Default target is all targets when no target is specified in command-line.

This seems to me to be the defaults that will be most useful:

  • build.sh clean will clean all targets, as generally we want to apply to all targets.
  • build.sh :app will be just build.sh build:app as I think generally running all actions is not what we want.

We could go more complex but I think we get marginal additional value. We want this to be easy to author but also easy to remember how to use -- so build.sh configure, build.sh build and build.sh test are likely to be our most common invocations.

_builder_chosen_action_targets+=("$e:$target")
done
# apply the default action to the selected target
_builder_chosen_action_targets+=("$_builder_default_action$target")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would this handle a case like this?

Script use:

> ./build.sh clean :engine

Script definition:

builder_describe \
  "Tests the build-utils.sh builder functions. This is merely an example." \
  "clean        Cleans up any build artifacts" \
  "build+       Do some building" \
  "test         Does some test stuff" \
  ":app" \
  ":engine      Thomas, y'know" \
  "--power,-p   Use powerful mode" \
  "--zoom,-z    Use zoom mode"

In my mind, such a call says "only run the clean action, and only for the :engine target."

Alternatively, I intuitively believe I could use:

./build.sh clean build :engine

to both clean and build the :engine target, and that target alone.

I bring this up because, by my reading of this part of the code and what precedes it, what we actually get for the first example use is:

# From clean -
clean:app
clean:engine

# From the loose :engine - 
build:engine

which is completely different. Assuming I'm interpreting the code correctly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So looks like our internal intuition engines are colliding here.

You are right, currently, this:

> ./build.sh clean :engine

does generate this, yes:

# From clean -
clean:app
clean:engine

# From the loose :engine - 
build:engine

I am open to swapping the inference model to what you've suggested. Pros/cons both ways. Current model leaves each parameter interpreted in isolation which is easy to figure out the action:target pairs. Your model is probably a bit more intuitive but perhaps sticks just a little for more complex invocations, e.g. something like clean:app build:engine test -- which targets would apply to test here; or am I just being difficult? 🤣)

Copy link
Copy Markdown
Contributor

@jahorton jahorton Jul 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. something like clean:app build:engine test -- which targets would apply to test here; or am I just being difficult? 🤣)

:project, as there's no free-standing target specified otherwise. Admittedly, that example is a bit of a fun parse either way you look at it.

Something like clean:app effectively scopes those commands together, while free-standing actions and free-standing targets have "general" scope. If that makes sense?


I guess that leads to another niche call structure:

> ./build.sh clean:engine :app

would yield

clean:engine
build:app # due to freestanding :app target with no specified, general action

Whereas leaving off the :app drops the build:app part; there'd be no need for a default action because there's no unpaired (or, un-actioned?) target. When no target is specified in any fashion, well, :project exists; if no action is specified either, :project is thus unpaired, so the default action gets paired to it.

Though, I admit, this last example set isn't exactly the clearest scenario.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I feel like either model can lead to confusion, and your suggestion entangles the parameters, whereas in my current model, each parameter can be deduced in isolation, which I think makes it easier to figure out.

So given this, I think I will stick with the existing model:

  • ./build.sh clean :engine is gonna do clean:* and build:engine.
  • ./build.sh clean:engine is gonna do clean:engine.
  • ./build.sh clean build :engine will give us clean:* build:* (the final :engine parameter is effectively irrelevant).
  • ./build.sh will give us build:* (default action is build, default target is *)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as this is explicitly documented in ./build_utils.sh.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this sufficient? Noting that this is made available with --help in any build script, which is more visible than as a comment in build-utils.sh. If this doesn't provide what you are looking for, can you make a suggestion?

echo "* If action is not specified, default action is '$_builder_default_action'"
echo "* If :target is not specified, will apply action to all targets"
echo "* If no actions or :targets are specified, '$_builder_default_action' action will run on all targets"

Copy link
Copy Markdown
Contributor

@jahorton jahorton Jul 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not believe that it is. Remember, that was in place when I asked the questions that led to this discussion. If I made the same mistake, and was this confused about it, I'd be shocked if no one else would have the same interpretation. I'll think on a way to address the concern.

Copy link
Copy Markdown
Contributor

@jahorton jahorton Jul 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First draft:

echo "* Specify `action:target` to run a specific `action` against a specific `:target`."
echo "* If `action` is specified without a `target` suffix, it will be applied to all `:target`s."
echo "* If `:target` is specified without an `action` prefix, `$_builder_default_action:target` will be inferred."
echo "* If no `action`, `:target`, or `action:target` entries are specified, `$_builder_default_action` will run on all `:target`s."

@mcdurdin mcdurdin merged commit 6304d52 into master Jul 29, 2022
@mcdurdin mcdurdin deleted the chore/common/builder-script-target-and-option-support branch July 29, 2022 01:54
@keyman-server
Copy link
Copy Markdown
Collaborator

Changes in this pull request will be available for download in Keyman version 16.0.41-alpha

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants