feat: basic stamping support

This commit is contained in:
Alex Eagle 2022-07-14 22:32:39 -07:00 committed by Greg Magolan
parent 6f52c77a51
commit de081fb72e
9 changed files with 401 additions and 0 deletions

View File

@ -93,4 +93,9 @@ stardoc_with_diff_test(
bzl_library_target = "//lib:host_repo",
)
stardoc_with_diff_test(
name = "stamping",
bzl_library_target = "//lib:stamping",
)
update_docs()

112
docs/stamping.md generated Executable file
View File

@ -0,0 +1,112 @@
<!-- Generated with Stardoc: http://skydoc.bazel.build -->
Version Stamping
Bazel is generally only a build tool, and is unaware of your version control system.
However, when publishing releases, you may want to embed version information in the resulting distribution.
Bazel supports this with the concept of a "Workspace status" which is evaluated before each build.
See [the Bazel workspace status docs](https://docs.bazel.build/versions/master/user-manual.html#workspace_status)
To stamp a build, you pass the `--stamp` argument to Bazel.
> Note: https://github.com/bazelbuild/bazel/issues/14341 proposes that Bazel enforce this by
> only giving constant values to rule implementations when stamping isn't enabled.
Stamping is typically performed on a later action in the graph, like on a linking or packaging rule (`pkg_*`).
This means that a changed status variable only causes that action, not re-compilation and thus does not cause cascading re-builds.
Bazel provides a couple of statuses by default, such as `BUILD_EMBED_LABEL` which is the value of the `--embed_label`
argument, as well as `BUILD_HOST` and `BUILD_USER`. You can supply more with the workspace status script, see below.
Some rules accept an attribute that uses the status variables.
They will usually say something like "subject to stamp variable replacements".
## Stamping with a Workspace status script
To define additional statuses, pass the `--workspace_status_command` argument to `bazel`.
The value of this flag is a path to a script that prints space-separated key/value pairs, one per line, such as
```bash
#!/usr/bin/env bash
echo STABLE_GIT_COMMIT $(git rev-parse HEAD)
```
> For a more full-featured script, take a look at the [bazel_stamp_vars in Angular]
Make sure you set the executable bit, eg. `chmod 755 tools/bazel_stamp_vars.sh`.
> **NOTE** keys that start with `STABLE_` will cause a re-build when they change.
> Other keys will NOT cause a re-build, so stale values can appear in your app.
> Non-stable (volatile) keys should typically be things like timestamps that always vary between builds.
You might like to encode your setup using an entry in `.bazelrc` such as:
```sh
# This tells Bazel how to interact with the version control system
# Enable this with --config=release
build:release --stamp --workspace_status_command=./tools/bazel_stamp_vars.sh
```
[bazel_stamp_vars in Angular]: https://github.com/angular/angular/blob/master/tools/bazel_stamp_vars.sh
## Writing a custom rule which reads stamp variables
First, load the helpers:
```starlark
load("@aspect_bazel_lib//lib:stamping.bzl", "STAMP_ATTRS", "maybe_stamp")
```
In your rule implementation, call the `maybe_stamp` function.
If it returns `None` then this build doesn't have stamping enabled.
Otherwise you can use the returned struct to access two files.
The stable_status file contains the keys which were prefixed with `STABLE_`, see above.
The volatile_status file contains the rest of the keys.
```starlark
def _rule_impl(ctx):
args = ctx.actions.args()
inputs = []
stamp = maybe_stamp(ctx)
if stamp:
args.add("--volatile_status_file", stamp.volatile_status)
args.add("--stable_status_file", stamp.stable_status)
inputs.extend([stamp.volatile_status, stamp.stable_status])
# ... call actions which parse the stamp files and do something with the values ...
```
Finally, in the declaration of the rule, include the `STAMP_ATTRS` to declare attributes
which are read by that `maybe_stamp` function above.
```starlark
my_stamp_aware_rule = rule(
attrs = dict({
# ... my attributes ...
}, **STAMP_ATTRS),
)
```
<a id="maybe_stamp"></a>
## maybe_stamp
<pre>
maybe_stamp(<a href="#maybe_stamp-ctx">ctx</a>)
</pre>
Provide the bazel-out/stable_status.txt and bazel-out/volatile_status.txt files.
**PARAMETERS**
| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="maybe_stamp-ctx"></a>ctx | The rule context | none |
**RETURNS**
If stamping is not enabled for this rule under the current build, returns None.
Otherwise, returns a struct containing (volatile_status_file, stable_status_file) keys

View File

@ -1,4 +1,5 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("//lib/private:stamping.bzl", "stamp_build_setting")
exports_files(
glob(["*.bzl"]),
@ -8,6 +9,9 @@ exports_files(
],
)
# Macro that creates targets enabling the use of `--stamp` in Starlark rules
stamp_build_setting(name = "stamp")
toolchain_type(
name = "jq_toolchain_type",
visibility = ["//visibility:public"],
@ -165,3 +169,10 @@ bzl_library(
"//lib/private:host_repo",
],
)
bzl_library(
name = "stamping",
srcs = ["stamping.bzl"],
visibility = ["//visibility:public"],
deps = ["//lib/private:stamping"],
)

View File

@ -139,6 +139,7 @@ bzl_library(
visibility = ["//lib:__subpackages__"],
deps = [
":expand_make_vars",
"//lib:stamping",
"@bazel_skylib//lib:dicts",
],
)
@ -168,3 +169,9 @@ bzl_library(
visibility = ["//lib:__subpackages__"],
deps = [":repo_utils"],
)
bzl_library(
name = "stamping",
srcs = ["stamping.bzl"],
visibility = ["//lib:__subpackages__"],
)

60
lib/private/stamping.bzl Normal file
View File

@ -0,0 +1,60 @@
"""A small utility module dedicated to detecting whether or not the `--stamp` flag is enabled
This module can be removed likely after the following PRs ar addressed:
- https://github.com/bazelbuild/bazel/issues/11164
"""
StampSettingInfo = provider(
doc = "Information about the `--stamp` command line flag",
fields = {
"value": "bool: Whether or not the `--stamp` flag was enabled",
},
)
def _stamp_build_setting_impl(ctx):
return StampSettingInfo(value = ctx.attr.value)
_stamp_build_setting = rule(
doc = "Adapter from our config_setting to a Provider for downstream rules",
implementation = _stamp_build_setting_impl,
attrs = {
"value": attr.bool(
doc = "The default value of the stamp build flag",
mandatory = True,
),
},
)
def stamp_build_setting(name, visibility = ["//visibility:public"]):
native.config_setting(
name = "stamp_detect",
values = {"stamp": "1"},
visibility = visibility,
)
_stamp_build_setting(
name = name,
value = select({
":stamp_detect": True,
"//conditions:default": False,
}),
visibility = visibility,
)
def is_stamping_enabled(attr):
"""Determine whether or not build stamping is enabled.
Args:
attr (struct): A rule's struct of attributes (`ctx.attr`)
Returns:
bool: The stamp value
"""
stamp_num = getattr(attr, "stamp", -1)
if stamp_num > 0:
return True
elif stamp_num == 0:
return False
elif stamp_num < 0:
stamp_flag = getattr(attr, "_stamp_flag", None)
return stamp_flag[StampSettingInfo].value if stamp_flag else False
else:
fail("Unexpected `stamp` value: {}".format(stamp_num))

131
lib/stamping.bzl Normal file
View File

@ -0,0 +1,131 @@
"""Version Stamping
Bazel is generally only a build tool, and is unaware of your version control system.
However, when publishing releases, you may want to embed version information in the resulting distribution.
Bazel supports this with the concept of a "Workspace status" which is evaluated before each build.
See [the Bazel workspace status docs](https://docs.bazel.build/versions/master/user-manual.html#workspace_status)
To stamp a build, you pass the `--stamp` argument to Bazel.
> Note: https://github.com/bazelbuild/bazel/issues/14341 proposes that Bazel enforce this by
> only giving constant values to rule implementations when stamping isn't enabled.
Stamping is typically performed on a later action in the graph, like on a linking or packaging rule (`pkg_*`).
This means that a changed status variable only causes that action, not re-compilation and thus does not cause cascading re-builds.
Bazel provides a couple of statuses by default, such as `BUILD_EMBED_LABEL` which is the value of the `--embed_label`
argument, as well as `BUILD_HOST` and `BUILD_USER`. You can supply more with the workspace status script, see below.
Some rules accept an attribute that uses the status variables.
They will usually say something like "subject to stamp variable replacements".
## Stamping with a Workspace status script
To define additional statuses, pass the `--workspace_status_command` argument to `bazel`.
The value of this flag is a path to a script that prints space-separated key/value pairs, one per line, such as
```bash
#!/usr/bin/env bash
echo STABLE_GIT_COMMIT $(git rev-parse HEAD)
```
> For a more full-featured script, take a look at the [bazel_stamp_vars in Angular]
Make sure you set the executable bit, eg. `chmod 755 tools/bazel_stamp_vars.sh`.
> **NOTE** keys that start with `STABLE_` will cause a re-build when they change.
> Other keys will NOT cause a re-build, so stale values can appear in your app.
> Non-stable (volatile) keys should typically be things like timestamps that always vary between builds.
You might like to encode your setup using an entry in `.bazelrc` such as:
```sh
# This tells Bazel how to interact with the version control system
# Enable this with --config=release
build:release --stamp --workspace_status_command=./tools/bazel_stamp_vars.sh
```
[bazel_stamp_vars in Angular]: https://github.com/angular/angular/blob/master/tools/bazel_stamp_vars.sh
## Writing a custom rule which reads stamp variables
First, load the helpers:
```starlark
load("@aspect_bazel_lib//lib:stamping.bzl", "STAMP_ATTRS", "maybe_stamp")
```
In your rule implementation, call the `maybe_stamp` function.
If it returns `None` then this build doesn't have stamping enabled.
Otherwise you can use the returned struct to access two files.
The stable_status file contains the keys which were prefixed with `STABLE_`, see above.
The volatile_status file contains the rest of the keys.
```starlark
def _rule_impl(ctx):
args = ctx.actions.args()
inputs = []
stamp = maybe_stamp(ctx)
if stamp:
args.add("--volatile_status_file", stamp.volatile_status)
args.add("--stable_status_file", stamp.stable_status)
inputs.extend([stamp.volatile_status, stamp.stable_status])
# ... call actions which parse the stamp files and do something with the values ...
```
Finally, in the declaration of the rule, include the `STAMP_ATTRS` to declare attributes
which are read by that `maybe_stamp` function above.
```starlark
my_stamp_aware_rule = rule(
attrs = dict({
# ... my attributes ...
}, **STAMP_ATTRS),
)
```
"""
load("//lib/private:stamping.bzl", "is_stamping_enabled")
def maybe_stamp(ctx):
"""Provide the bazel-out/stable_status.txt and bazel-out/volatile_status.txt files.
Args:
ctx: The rule context
Returns:
If stamping is not enabled for this rule under the current build, returns None.
Otherwise, returns a struct containing (volatile_status_file, stable_status_file) keys
"""
if is_stamping_enabled(ctx.attr):
return struct(
volatile_status_file = ctx.version_file,
stable_status_file = ctx.info_file,
)
return None
STAMP_ATTRS = {
"stamp": attr.int(
doc = """\
Whether to encode build information into the output. Possible values:
- `stamp = 1`: Always stamp the build information into the output, even in
[--nostamp](https://docs.bazel.build/versions/main/user-manual.html#flag--stamp) builds.
This setting should be avoided, since it is non-deterministic.
It potentially causes remote cache misses for the target and
any downstream actions that depend on the result.
- `stamp = 0`: Never stamp, instead replace build information by constant values.
This gives good build result caching.
- `stamp = -1`: Embedding of build information is controlled by the
[--[no]stamp](https://docs.bazel.build/versions/main/user-manual.html#flag--stamp) flag.
Stamped targets are not rebuilt unless their dependencies change.
""",
default = -1,
values = [1, 0, -1],
),
"_stamp_flag": attr.label(
doc = "Internal use only. A setting used to determine whether or not the `--stamp` flag is enabled.",
default = Label("//lib:stamp"),
),
}

View File

@ -0,0 +1,32 @@
load(":stamp_aware_rule.bzl", "my_stamp_aware_rule")
load("//lib:run_binary.bzl", "run_binary")
my_stamp_aware_rule(
name = "test_stamped",
out = "always",
stamp = 1,
)
my_stamp_aware_rule(
name = "test_unstamped",
out = "never",
stamp = 0,
)
my_stamp_aware_rule(
name = "test_default",
out = "default",
)
sh_binary(
name = "stamper",
srcs = ["stamper.sh"],
)
# Build this with --stamp enabled to see your username in the resulting output file
run_binary(
name = "run_stamper",
outs = ["stamped"],
args = ["$(location stamped)"],
tool = "stamper",
)

View File

@ -0,0 +1,30 @@
"Example of a rule that can version-stamp its outputs"
load("//lib:stamping.bzl", "STAMP_ATTRS", "maybe_stamp")
def _stamp_aware_rule_impl(ctx):
args = ctx.actions.args()
inputs = []
outputs = [ctx.outputs.out]
stamp = maybe_stamp(ctx)
if stamp:
args.add("--volatile_status_file", stamp.volatile_status_file)
args.add("--stable_status_file", stamp.stable_status_file)
inputs.extend([stamp.volatile_status_file, stamp.stable_status_file])
ctx.actions.run_shell(
inputs = inputs,
arguments = [args],
outputs = outputs,
# In reality, this program would also read from the status files.
command = "echo $@ > " + outputs[0].path,
)
return [DefaultInfo(files = depset(outputs))]
my_stamp_aware_rule = rule(
implementation = _stamp_aware_rule_impl,
attrs = dict({
"out": attr.output(mandatory = True),
}, **STAMP_ATTRS),
)

13
lib/tests/stamping/stamper.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -o nounset
# Snippet to parse Bazel's status file format.
# https://github.com/bazelbuild/bazel/issues/11164#issuecomment-996186921
# is another option, which requires Bash 4 for associative arrays.
while IFS= read -r line; do
read key value <<< "$line"
declare $key="$value"
done < <(cat "${BAZEL_STABLE_STATUS_FILE:-/dev/null}" "${BAZEL_VOLATILE_STATUS_FILE:-/dev/null}")
# A real program would do something useful with the stamp info, like pass it to a linker.
echo "${BUILD_USER:-unstamped}" > $1