Skip to content

Commit 01bfe4d

Browse files
Alex Eaglealexeagle
authored andcommitted
feat(builtin): accept any stamp vars in pkg_npm
NOTE: instead of replacing the replace_with_version with 0.0.0 in unstamped builds, we now omit the replacement. See #1694
1 parent ba4fdb8 commit 01bfe4d

7 files changed

Lines changed: 126 additions & 60 deletions

File tree

docs/stamping.md

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,61 @@ toc: true
77
# Stamping
88

99
Bazel is generally only a build tool, and is unaware of your version control system.
10-
However, when publishing releases, you typically want to embed version information in the resulting distribution.
11-
Bazel supports this natively, using the following approach:
10+
However, when publishing releases, you may want to embed version information in the resulting distribution.
11+
Bazel supports this with the concept of a "Workspace status" which is evaluated before each build.
12+
See [the Bazel workspace status docs](https://docs.bazel.build/versions/master/user-manual.html#workspace_status)
1213

1314
To stamp a build, you must pass the `--stamp` argument to Bazel.
1415

15-
> Previous releases of rules_nodejs stamped builds always.
16-
> However this caused stamp-aware actions to never be remotely cached, since the volatile
17-
> status file is passed as an input and its checksum always changes.
16+
Stamping is typically performed on a later action in the graph, like on a packaging rule (`pkg_*`). This means that
17+
a changed status variable only causes re-packaging, not re-compilation and thus does not cause cascading re-builds.
1818

19-
Also pass the `workspace_status_command` argument to `bazel build`.
20-
We prefer to do these with an entry in `.bazelrc`:
19+
Bazel provides a couple of statuses by default, such as `BUILD_EMBED_LABEL` which is the value of the `--embed_label`
20+
argument, as well as `BUILD_HOST` and `BUILD_USER`. You can supply more with the workspace status script, see below.
2121

22-
```sh
23-
# This tells Bazel how to interact with the version control system
24-
# Enable this with --config=release
25-
build:release --stamp --workspace_status_command=./tools/bazel_stamp_vars.sh
22+
Some rules accept an attribute that uses the status variables.
23+
For example, in a `pkg_npm` you can use the `substitutions` attribute like:
24+
25+
```python
26+
pkg_npm(
27+
name = "npm_package",
28+
substitutions = {"0.0.0-PLACEHOLDER": "{STABLE_GIT_COMMIT}"},
29+
)
2630
```
2731

28-
Then create `tools/bazel_stamp_vars.sh`.
32+
In a `--stamp` build, this will replace the string "0.0.0-PLACEHOLDER" in any file included in the package with the current value of the `STABLE_GIT_COMMIT` variable.
33+
However without stamping the placeholder will be left as-is.
2934

30-
This is a script that prints variable/value pairs.
31-
Make sure you set the executable bit, eg. `chmod 755 tools/bazel_stamp_vars.sh`.
32-
For example, we could run `git describe` to get the current tag:
35+
## Stamping with a Workspace status script
36+
37+
To define additional statuses, pass the `--workspace_status_command` argument to `bazel`.
38+
The value of this flag is a path to a script that prints space-separated key/value pairs, one per line, such as
3339

3440
```bash
3541
#!/usr/bin/env bash
36-
echo BUILD_SCM_VERSION $(git describe --abbrev=7 --tags HEAD)
42+
echo STABLE_GIT_COMMIT $(git rev-parse HEAD)
43+
```
44+
> For a more full-featured script, take a look at the [bazel_stamp_vars in Angular]
45+
46+
Make sure you set the executable bit, eg. `chmod 755 tools/bazel_stamp_vars.sh`.
47+
48+
> **NOTE** keys start start with `STABLE_` will cause a re-build when they change.
49+
> Other keys will NOT cause a re-build, so stale values can appear in your app.
50+
> Non-stable (volatile) keys should typically be things like timestamps that always vary between builds.
51+
52+
You might like to encode your setup using an entry in `.bazelrc` such as:
53+
54+
```sh
55+
# This tells Bazel how to interact with the version control system
56+
# Enable this with --config=release
57+
build:release --stamp --workspace_status_command=./tools/bazel_stamp_vars.sh
3758
```
3859

39-
For a more full-featured script, take a look at the [bazel_stamp_vars in Angular]
60+
## Release script
4061

41-
Finally, we recommend a release script around Bazel. We typically have more than one npm package published from one Bazel workspace, so we do a `bazel query` to find them, and publish in a loop. Here is a template to get you started:
62+
If you publish more than one package from your workspace, you might want a release script around Bazel.
63+
A nice pattern is to do a `bazel query` to find publishable targets, build them in parallel, then publish in a loop.
64+
Here is a template to get you started:
4265

4366
```sh
4467
#!/usr/bin/env bash
@@ -63,8 +86,6 @@ for pkg in $PKG_NPM_LABELS ; do
6386
done
6487
```
6588

66-
> WARNING: Bazel can't track changes to git tags. That means it won't rebuild a target if only the result of the workspace_status_command has changed. So changes to the version information may not be reflected if you re-build the package or bundle, and nothing in the package or bundle has changed.
67-
6889
See https://www.kchodorow.com/blog/2017/03/27/stamping-your-builds/ for more background.
6990

7091
[bazel_stamp_vars in Angular]: https://github.com/angular/angular/blob/master/tools/bazel_stamp_vars.sh

internal/npm_version_check.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
// Fetch the version of this package from its package.json
44
const pkg = require('./package.json');
5-
const pkgVersion = pkg.version || '0.0.0';
5+
const pkgVersion = pkg.version ? pkg.version.split('-')[0] : '0.0.0';
66

77
// BUILD_BAZEL_RULES_NODEJS_VERSION is only set when within the bazel context
88
const rulesVersion = process.env['BUILD_BAZEL_RULES_NODEJS_VERSION'] || '0.0.0';
@@ -18,4 +18,4 @@ if (rulesVersion !== '0.0.0' && pkgVersion !== '0.0.0' &&
1818
${pkg.name} - ${pkgVersion}
1919
@build_bazel_rules_nodejs - ${rulesVersion}
2020
See https://github.com/bazelbuild/rules_nodejs/wiki/Avoiding-version-skew`);
21-
}
21+
}

internal/pkg_npm/packager.js

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,29 @@ function unquoteArgs(s) {
6060
return s.replace(/^'(.*)'$/, '$1');
6161
}
6262

63+
/**
64+
* The status files are expected to look like
65+
* BUILD_SCM_HASH 83c699db39cfd74526cdf9bebb75aa6f122908bb
66+
* BUILD_SCM_LOCAL_CHANGES true
67+
* STABLE_BUILD_SCM_VERSION 6.0.0-beta.6+12.sha-83c699d.with-local-changes
68+
* BUILD_TIMESTAMP 1520021990506
69+
*
70+
* @param {string} p the path to the status file
71+
* @returns a two-dimensional array of key/value pairs
72+
*/
73+
function parseStatusFile(p) {
74+
if (!p) return [];
75+
return fs.readFileSync(p, {encoding: 'utf-8'})
76+
.split('\n')
77+
.filter(t => !!t)
78+
.map(t => t.split(' '));
79+
}
80+
6381
function main(args) {
6482
args = fs.readFileSync(args[0], {encoding: 'utf-8'}).split('\n').map(unquoteArgs);
6583
const
6684
[outDir, baseDir, srcsArg, binDir, genDir, depsArg, packagesArg, substitutionsArg,
67-
replaceWithVersion, stampFile, vendorExternalArg] = args;
85+
volatileFile, infoFile, vendorExternalArg] = args;
6886

6987
const substitutions = [
7088
// Strip content between BEGIN-INTERNAL / END-INTERNAL comments
@@ -74,27 +92,27 @@ function main(args) {
7492
for (const key of Object.keys(rawReplacements)) {
7593
substitutions.push([new RegExp(key, 'g'), rawReplacements[key]])
7694
}
77-
// Replace version last so that earlier substitutions can add
78-
// the version placeholder
79-
if (replaceWithVersion) {
80-
let version = '0.0.0';
81-
if (stampFile) {
82-
// The stamp file is expected to look like
83-
// BUILD_SCM_HASH 83c699db39cfd74526cdf9bebb75aa6f122908bb
84-
// BUILD_SCM_LOCAL_CHANGES true
85-
// BUILD_SCM_VERSION 6.0.0-beta.6+12.sha-83c699d.with-local-changes
86-
// BUILD_TIMESTAMP 1520021990506
87-
//
88-
// We want version to be the 6.0.0-beta... part
89-
const versionTag = fs.readFileSync(stampFile, {encoding: 'utf-8'})
90-
.split('\n')
91-
.find(s => s.startsWith('BUILD_SCM_VERSION'));
92-
// Don't assume BUILD_SCM_VERSION exists
93-
if (versionTag) {
94-
version = versionTag.split(' ')[1].replace(/^v/, '').trim();
95+
// Replace statuses last so that earlier substitutions can add
96+
// status-related placeholders
97+
if (volatileFile || infoFile) {
98+
const statusEntries = parseStatusFile(volatileFile)
99+
statusEntries.push(...parseStatusFile(infoFile))
100+
// Looks like {'BUILD_SCM_VERSION': 'v1.2.3'}
101+
const statuses = new Map(statusEntries)
102+
for (let idx = 0; idx < substitutions.length; idx++) {
103+
const match = substitutions[idx][1].match(/\{(.*)\}/);
104+
if (!match) continue;
105+
const statusKey = match[1];
106+
let statusValue = statuses.get(statusKey);
107+
if (statusValue) {
108+
// npm versions must be numeric, so if the VCS tag starts with leading 'v', strip it
109+
// See https://github.com/bazelbuild/rules_nodejs/pull/1591
110+
if (statusKey.endsWith('_VERSION')) {
111+
statusValue = statusValue.replace(/^v/, '');
112+
}
113+
substitutions[idx][1] = statusValue;
95114
}
96115
}
97-
substitutions.push([new RegExp(replaceWithVersion, 'g'), version]);
98116
}
99117

100118
// src like baseDir/my/path is just copied to outDir/my/path

internal/pkg_npm/pkg_npm.bzl

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,26 @@ PKG_NPM_ATTRS = {
9191
doc = """Optional package_name that this npm package may be imported as.""",
9292
),
9393
"replace_with_version": attr.string(
94-
doc = """If set this value is replaced with the version stamp data.
95-
See the section on stamping in the README.""",
94+
doc = """DEPRECATED: use substitutions instead.
95+
96+
`replace_with_version = "my_version_placeholder"` is just syntax sugar for
97+
`substitutions = {"my_version_placeholder": "{BUILD_SCM_VERSION}"}`.
98+
99+
Follow this deprecation at https://github.com/bazelbuild/rules_nodejs/issues/2158
100+
""",
96101
default = "0.0.0-PLACEHOLDER",
97102
),
98103
"srcs": attr.label_list(
99104
doc = """Files inside this directory which are simply copied into the package.""",
100105
allow_files = True,
101106
),
102107
"substitutions": attr.string_dict(
103-
doc = """Key-value pairs which are replaced in all the files while building the package.""",
108+
doc = """Key-value pairs which are replaced in all the files while building the package.
109+
110+
You can use values from the workspace status command using curly braces, for example
111+
`{"0.0.0-PLACEHOLDER": "{STABLE_GIT_VERSION}"}`.
112+
See the section on stamping in the README
113+
""",
104114
),
105115
"vendor_external": attr.string_list(
106116
doc = """External workspaces whose contents should be vendored into this workspace.
@@ -184,7 +194,15 @@ def create_package(ctx, deps_files, nested_packages):
184194
# current package unless explicitely specified.
185195
filtered_deps_sources = _filter_out_external_files(ctx, deps_files, package_path)
186196

197+
# Back-compat for the replace_with_version stamping
198+
# see https://github.com/bazelbuild/rules_nodejs/issues/2158 for removal
199+
substitutions = dict(**ctx.attr.substitutions)
200+
if stamp and ctx.attr.replace_with_version:
201+
substitutions[ctx.attr.replace_with_version] = "{BUILD_SCM_VERSION}"
202+
187203
args = ctx.actions.args()
204+
inputs = ctx.files.srcs + deps_files + nested_packages
205+
188206
args.use_param_file("%s", use_always = True)
189207
args.add(package_dir.path)
190208
args.add(package_path)
@@ -193,19 +211,23 @@ def create_package(ctx, deps_files, nested_packages):
193211
args.add(ctx.genfiles_dir.path)
194212
args.add_joined(filtered_deps_sources, join_with = ",", omit_if_empty = False)
195213
args.add_joined([p.path for p in nested_packages], join_with = ",", omit_if_empty = False)
196-
args.add(ctx.attr.substitutions)
197-
args.add(ctx.attr.replace_with_version)
198-
args.add(ctx.version_file.path if stamp else "")
199-
args.add_joined(ctx.attr.vendor_external, join_with = ",", omit_if_empty = False)
214+
args.add(substitutions)
200215

201-
inputs = ctx.files.srcs + deps_files + nested_packages
202-
203-
# The version_file is an undocumented attribute of the ctx that lets us read the volatile-status.txt file
204-
# produced by the --workspace_status_command. That command will be executed whenever
205-
# this action runs, so we get the latest version info on each execution.
206-
# See https://github.com/bazelbuild/bazel/issues/1054
207216
if stamp:
217+
# The version_file is an undocumented attribute of the ctx that lets us read the volatile-status.txt file
218+
# produced by the --workspace_status_command.
219+
# Similarly info_file reads the stable-status.txt file.
220+
# That command will be executed whenever
221+
# this action runs, so we get the latest version info on each execution.
222+
# See https://github.com/bazelbuild/bazel/issues/1054
223+
args.add(ctx.version_file.path)
208224
inputs.append(ctx.version_file)
225+
args.add(ctx.info_file.path)
226+
inputs.append(ctx.info_file)
227+
else:
228+
args.add_all(["", ""])
229+
230+
args.add_joined(ctx.attr.vendor_external, join_with = ",", omit_if_empty = False)
209231

210232
ctx.actions.run(
211233
progress_message = "Assembling npm package %s" % package_dir.short_path,

packages/rollup/rollup_bundle.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,14 +361,19 @@ def _rollup_bundle(ctx):
361361
template = ctx.file.config_file,
362362
output = config,
363363
substitutions = {
364+
"bazel_info_file": "\"%s\"" % ctx.info_file.path if stamp else "undefined",
365+
# Back-compat: we used to replace a variable "bazel_stamp_file"
366+
# Remove in 3.0: https://github.com/bazelbuild/rules_nodejs/issues/2158
364367
"bazel_stamp_file": "\"%s\"" % ctx.version_file.path if stamp else "undefined",
368+
"bazel_version_file": "\"%s\"" % ctx.version_file.path if stamp else "undefined",
365369
},
366370
)
367371

368372
args.add_all(["--config", config.path])
369373
inputs.append(config)
370374

371375
if stamp:
376+
inputs.append(ctx.info_file)
372377
inputs.append(ctx.version_file)
373378

374379
# Prevent rollup's module resolver from hopping outside Bazel's sandbox

packages/rollup/test/integration/rollup.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import nodeResolve from '@rollup/plugin-node-resolve';
44

55
// Parse the stamp file produced by Bazel from the version control system
66
let version = '<unknown>';
7-
if (bazel_stamp_file) {
7+
if (bazel_version_file) {
88
const versionTag = require('fs')
9-
.readFileSync(bazel_stamp_file, {encoding: 'utf-8'})
9+
.readFileSync(bazel_version_file, {encoding: 'utf-8'})
1010
.split('\n')
1111
.find(s => s.startsWith('BUILD_SCM_VERSION'));
1212
// Don't assume BUILD_SCM_VERSION exists

packages/rollup/test/version_stamp/rollup.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ const DEBUG = process.env['COMPILATION_MODE'] === 'dbg';
22

33
// Parse the stamp file produced by Bazel from the version control system
44
let version = '<unknown>';
5-
if (bazel_stamp_file) {
5+
if (bazel_version_file) {
66
const versionTag = require('fs')
7-
.readFileSync(bazel_stamp_file, {encoding: 'utf-8'})
7+
.readFileSync(bazel_version_file, {encoding: 'utf-8'})
88
.split('\n')
99
.find(s => s.startsWith('BUILD_SCM_VERSION'));
1010
// Don't assume BUILD_SCM_VERSION exists
@@ -24,4 +24,4 @@ const banner = `/**
2424

2525
module.exports = {
2626
output: {banner},
27-
};
27+
};

0 commit comments

Comments
 (0)