daml/BAZEL-bash.md

149 lines
6.6 KiB
Markdown
Raw Normal View History

2019-07-19 18:31:45 +03:00
# Working with Bash in Bazel
Bazel has two out-of-the-box rules, `sh_test` and `genrule`, that can be used
for one-shot custom build targets. If there is some reusable logic behind your
target, consider building a new rule in Skylark instead.
## Platform
There are a number of limitations in the way Bazel will set up the environment
for these Bash scripts when running them, and they differ based on the OS. The
instructions given here will hopefully help you get to a point where your rules
run on all platforms.
## Dependencies
Bazel promises hermeticity, but Bash scripts have access to whatever they want.
As far as I can tell they are not run in any kind of chroot or container (at
least on macOS), although they are run with a restricted PATH (on Linux) and in
a specially set-up folder.
As such, in order to get all of your dependencies available, you need to
specify them in the relevant attributes:
- `srcs` for (source) files required during build; note that in the case of
`sh_test`, this must be a single-element list that contains just the script
you intend to run.
- `data` for files required at run time (`sh_test` only).
- `deps` for results of other Bazel rules required at run time.
- `tools` for executables (`genrule` only).
## `genrule`: substitutions
The code of a `genrule` directive is the `cmd` attribute, which is a multiline
string embedding a Bash script. Within that script, you need to use `$$` to
produce a single `$` in the Bash script itself, as `$()` will be expanded by
Bazel's own string interpolation. You can use the `location` and `locations`
functions to get paths for Bazel build results. You should note that the paths
these return is relative to the starting `$PWD` of the script, so if your Bash
script `cd`s somwhere, you have to take that into account.
You can include local files (files present in your workdir that are not the
result of another Bazel rule) as part of your `srcs` by using the `glob`
function, as in:
```
srcs = glob([
"package.json",
"syntaxes/*",
]) + [
"//some:rule",
],
```
The script will start running at the root of your workspace. While local files
specified in `srcs` are specified using paths relative to the package root in
the `glob` directive (i.e. relative to the containing directory of the
`BUILD.bazel` file); they are added to the run time directory of the script
using their package-qualified path. If the above snippet came from
`my/package/BUILD.bazel`, the `package.json` file would be at
`my/package/package.json`.
Note that tools and data dependencies will also be given as relative (to the
script's starting directory) paths through the `location` and `locations`
functions.
## Conditionals
Bazel supports simple conditionals in its rules, though I have not found a way
to use them in string substitutions. As some rules have different names under
Windows, this creates a bit of a problem. Aliases provide a way around that;
for example:
```
alias(
name = "yarn",
actual = "@nodejs//:bin/yarn.cmd" if is_windows else "@nodejs//:bin/yarn",
)
```
would let you use, in the `cmd` entry of a script:
```
$$DIR/$(location :yarn)
```
to invoke the correct `yarn` executable regardless of platform.
## Links and speed
Bazel relies a lot on symbolic links to provide what should appear like a new,
isolated environment to each build rule without having to pay the cost of
copying files over each time. However, this means that
1. Everything path-related is different on Windows, because symlinks there are
not well-supported, and
1. While Bazel can claim some nice speed for creating the environment, every
read your rule does from a Bazel-provided file takes the hit of going
through the symlink indirection.
That second point can be very costly when you have to read many small files
(e.g. node dependencies).
## `sh_test`: "runfiles"
Whereas `genrule` takes an inline Bash script as its `cmd` attribute, the
`sh_test` script will run a Bash script defined in a separate file and
specified as the single entry in the `srcs` attribute. This adds the
complication that `location` substitution is not available.
A partial workaround for that is to pass the relevant paths to the script as
the `args` attribute of the `sh_rule`; those will be available as traditional
arguments in Bash (`$@`, `$#`, `$1`, etc.).
However, in order for the test script to work under Windows (where symlinks are
not well supported), another level of indirection is introduced by Bazel: the
paths returned by the `location` function cannot be used directly by the script
given to `sh_test`, and must further be translated to "real", absolute paths
through the use of an `rlocation` Bash function.
To make this function available to your Bash script, the `sh_test` rule must
include an additional dependency:
```
deps = ["@bazel_tools//tools/bash/runfiles"],
```
Then, anything your script needs access to, including executables, needs to be
passed in as arguments (using `location` in the `BUILD.bazel` file). In the
script itself, before using any of the arguments, you have to paste [the
following snippet from the Bazel runfiles]():
```
# Copy-pasted from the Bazel Bash runfiles library v2.
set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v2 ---
```
After that, in your Bash script, you can construct the absolute paths by taking
the values Bazel gave you and expanding them like (assuming the first argument
specified in the `BUILD.bazel` file was a `$(location ...)` call) :
```
MY_FIRST_ARG="$(rlocation "$TEST_WORKSPACE/$1")"
```
The `TEST_WORKSPACE` variable is set by Bazel to the current WORKSPACE name.
Note that you should use this name even for dependencies from external
namespaces.
Note that you should use the above method to access _all_ of your dependencies,
_including_ "local" files from the same package as the `sh_test`. Trying to
access those files directly by their expected path, as you can do in
`genrule`s, will not work with `sh_test` on Windows. You may have to wrap your
local files in a `filegroup` to have a valid target for the `$(location ...)`
call you should pass as an `args` to the `sh_test`.