diff --git a/.changeset/rules_task-rude-doors-sneeze.md b/.changeset/rules_task-rude-doors-sneeze.md new file mode 100644 index 000000000..2dbf857b2 --- /dev/null +++ b/.changeset/rules_task-rude-doors-sneeze.md @@ -0,0 +1,5 @@ +--- +"rules_task": patch +--- + +docs: Add docs for rules_task diff --git a/rules/rules_task/README.md b/rules/rules_task/README.md new file mode 100644 index 000000000..4ef2223a5 --- /dev/null +++ b/rules/rules_task/README.md @@ -0,0 +1,281 @@ +# rules_task + +`rules_task` is a Bazel ruleset for creating and running programs called tasks. It aims to be a simpler alternative than writing `sh_binary` targets and look similar to scripts you would write for CI/CD systems like GitHub and GitLab. It achieves this by using a basic rule definition and by exposing AST nodes for more complex tasks. + +# Installation + +Follow instructions from the release you wish to use: [rules_task releases](https://github.com/vgijssel/setup/releases?q=%22name+%3D+%5C%22rules_task%5C%22%22&expanded=true). + +# Getting Started + +1. Load the `task` rule in a `BUILD.bazel` file + +```bazel +load("@rules_task//task:def.bzl", "task") +``` + +2. Create a task with the `task` rule + +```bazel +task( + name = "hello", + cmds = [ + "echo Hello, world", + ] +) +``` + +3. Run the task with `bazel run` + +```bash +bazel run :hello +``` + +# Use cases + +### Multiple commands in a single target + +```bazel +task( + name = "multiple_commands", + cmds = [ + "echo hello", + "echo world", + ] +) +``` + +### Command with environment variables + +```bazel +task( + name = "env", + cmds = [ + "echo $MY_ENV_VAR", + ], + env = { + "MY_ENV_VAR": "Hello, world", + } +) +``` + +Note that these environment variables are persisted inside the task binary, as opposed to the args / env arguments of `py_binary` and `sh_binary` rules. This means if this task target is called by another target, the environments will still be present. + +### Runnning a command at exit + +```bazel +task( + name = "defer", + cmds = [ + "echo 1", + "echo 2", + { "defer": "echo 3" }, + "echo 4", + "exit 1", + "echo 5", + ], +) +``` + +This will print + +```bash +1 +2 +4 +3 # note this is executed after 4 +``` + +### Passing cli arguments + +A special environment variable `$CLI_ARGS` is available to all tasks, which contains the arguments passed to the task. + +```bazel +task( + name = "cli_args", + cmds = [ + "echo $CLI_ARGS", + ], +) +``` + +```bash +bazel run :cli_args -- "Hello, world" +``` + +### Setting a current working directory + +This will set the current working directory to the root of the Bazel workspace. + +```bazel +task( + name = "cwd", + cmds = [ + "pwd", + ], + cwd = "$BUILD_WORKSPACE_DIRECTORY", +) +``` + +### Using executable targets + +You can use the `cmd.executable` AST node to reference other **executable** targets. This does some magic behind the scenes to make sure the target is tracked as a runfile dependency and the absolute path is resolved. This makes it easy to use in combination with a changed working directory. No need to add `$(location ...)` or `$(execpath ...)` statements or to explicitly add the target as a dependency in the `data` attribute. + +```bazel +task( + name = "executable", + cmds = [ + "$my_executable", + ], + env = { + "tool": cmd.executable("my_executable"), + } +) +``` + +### Referencing outputs of other targets + +You can use the `cmd.file` and `cmd.files` AST nodes to reference other targets. This does some magic behind the scenes to make sure the target is tracked as a runfile dependency and the absolute path is resolved. + +```bazel +task( + name = "file", + cmds = [ + "echo $my_file", + "cat $my_file_group", + ], + env = { + "file": cmd.file("my_file"), + "files": cmd.files(":my_file_group"), + } +) +``` + +### More examples + +For more examples, see the [tests](tests/BUILD.bazel). + +# AST nodes + +Using the AST nodes directly allows for more advanced use cases. The current AST visitor implementation does not allow for nested nodes of the same type because Starlark does not support recursion. This means that you cannot use `cmd.defer` inside a `cmd.defer` or `cmd.file` inside a `cmd.file`. This is a limitation of the current implementation and may be resolved in the future. + +Load the `cmd` rule in a `BUILD.bazel` file + +```bazel +load("@rules_task//task:def.bzl", "cmd") +``` + +### `cmd.root` + +Is used by the `task` rule to define the root of the AST. It is not meant to be used directly currently, due to the recursion limitation of the current visitor. + +### `cmd.env` + +This allows you to set environment variables using a dict in any place of the AST. + +```bazel +task( + name = "env_ast", + cmds = [ + "echo $MY_ENV_VAR", + cmd.env({ + "MY_ENV_VAR": "bar", + }), + "echo $MY_ENV_VAR", + ], + env = { + "MY_ENV_VAR": "foo", + } +) +``` + +This will print + +```bash +foo +bar +``` + +### `cmd.defer` + +Previously example `{ "defer": "echo 3" }` is syntactic sugar for `cmd.defer("echo 3")`. + +### `cmd.shell` + +The `cmd.shell` is the default node for each of the arguments of the `cmd.root` node. For example + +```bazel +task( + name = "hello", + cmds = [ + "echo Hello, world", + ] +) +``` + +Can also be written as + +```bazel +task( + name = "shell", + cmds = [ + cmd.shell("echo Hello, world"), + ] +) +``` + +or + +```bazel +task( + name = "shell_args", + cmds = [ + cmd.shell("echo", "Hello", "world"), + ] +) +``` + +which is useful for passing arguments to a command. This also allows passing a `cmd.executable` without relying on `cmd.env` + +```bazel +task( + name = "shell_and_executable", + cmds = [ + cmd.shell(cmd.executable(":my_executable"), "some", "args"), + ] +) +``` + +### `cmd.file` + +See [Referencing outputs of other targets](#referencing-outputs-of-other-targets) + +### `cmd.files` + +See [Referencing outputs of other targets](#referencing-outputs-of-other-targets) + +### `cmd.executable` + +See [Using executable targets](#using-executable-targets) + +### `cmd.string` + +This is used for most of the leaf nodes, at the end of the AST. For example + +```bazel +cmd.shell("echo", "hello", world) +``` + +can also be written as + +```bazel +cmd.shell(cmd.string("echo"), cmd.string("hello"), cmd.string("world")) +``` + +but is arguably less readable. + +# Inspiration/Alternatives + +- [task](https://github.com/go-task/task) - rules_task is heavily inspired by the task tool, taking some of the best ideals like deferred execution. +- [multirun](https://github.com/atlassian/bazel-tools/blob/master/multirun/README.md) - Initial inspiration for rules_task, making it easy to run multiple commands directly from a Bazel file +- [rules_multirun](https://github.com/keith/rules_multirun) - Modern and maintained version of the Atlassian multirun tool.