Skip to content

Commit 0fde42b

Browse files
Alex Eaglealexeagle
authored andcommitted
feat(builtin): add a chdir attribute to nodejs_test and npm_package_bin
Fixes #2323
1 parent 9d7827b commit 0fde42b

File tree

8 files changed

+100
-50
lines changed

8 files changed

+100
-50
lines changed

internal/node/launcher.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ ALL_ARGS=(TEMPLATED_args "$@")
176176
STDOUT_CAPTURE=""
177177
STDERR_CAPTURE=""
178178
EXIT_CODE_CAPTURE=""
179+
NODE_WORKING_DIR=""
179180

180181
RUN_LINKER=true
181182
NODE_PATCHES=true
@@ -205,6 +206,7 @@ for ARG in ${ALL_ARGS[@]+"${ALL_ARGS[@]}"}; do
205206
--nobazel_run_linker) RUN_LINKER=false PATCH_REQUIRE=true ;;
206207
# If running an NPM package, run it from execroot instead of from external
207208
--bazel_run_from_execroot) FROM_EXECROOT=true ;;
209+
--bazel_node_working_dir=*) NODE_WORKING_DIR="${ARG#--bazel_node_working_dir=}" ;;
208210
# Let users pass through arguments to node itself
209211
--node_options=*) USER_NODE_OPTIONS+=( "${ARG#--node_options=}" ) ;;
210212
# Remaining argv is collected to pass to the program
@@ -302,6 +304,11 @@ else
302304
fi
303305
fi
304306

307+
if [[ -n "$NODE_WORKING_DIR" ]]; then
308+
echo "process.chdir(__dirname)" > "$NODE_WORKING_DIR/__chdir.js"
309+
LAUNCHER_NODE_OPTIONS+=( "--require" "./$NODE_WORKING_DIR/__chdir.js" )
310+
fi
311+
305312
# The EXPECTED_EXIT_CODE lets us write bazel tests which assert that
306313
# a binary fails to run. Otherwise any failure would make such a test
307314
# fail before we could assert that we expected that failure.

internal/node/node.bzl

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,15 @@ fi
228228
# legacy uses of "$(rlocation"
229229
expanded_args = [preserve_legacy_templated_args(a) for a in ctx.attr.templated_args]
230230

231-
# First expand predefined source/output path variables:
231+
# Add the working dir argument before expansions
232+
if ctx.attr.chdir:
233+
expanded_args.append("--bazel_node_working_dir=" + ctx.attr.chdir)
234+
235+
# Next expand predefined source/output path variables:
232236
# $(execpath), $(rootpath) & legacy $(location)
233237
expanded_args = [expand_location_into_runfiles(ctx, a, ctx.attr.data) for a in expanded_args]
234238

235-
# Next expand predefined variables & custom variables
239+
# Finally expand predefined variables & custom variables
236240
rule_dir = _join(ctx.bin_dir.path, ctx.label.workspace_root, ctx.label.package)
237241
additional_substitutions = {
238242
"@D": rule_dir,
@@ -324,6 +328,18 @@ fi
324328
]
325329

326330
_NODEJS_EXECUTABLE_ATTRS = {
331+
"chdir": attr.string(
332+
doc = """Working directory to run the binary or test in, relative to the workspace.
333+
By default, Bazel always runs in the workspace root.
334+
335+
To run in the directory containing the `nodejs_binary` / `nodejs_test` use
336+
`chdir = package_name()`
337+
(or if you're in a macro, use `native.package_name()`)
338+
339+
NOTE that this can affect other paths passed to the program, which are workspace-relative.
340+
You may need `../../` segments to re-relativize such paths to the new working directory.
341+
""",
342+
),
327343
"configuration_env_vars": attr.string_list(
328344
doc = """Pass these configuration environment variables to the resulting binary.
329345
Chooses a subset of the configuration environment variables (taken from `ctx.var`), which also
@@ -589,11 +605,8 @@ If you just want to run a standard test using a test runner from npm, use the ge
589605
*_test target created by npm_install/yarn_install, such as `mocha_test`.
590606
Some test runners like Karma and Jasmine have custom rules with added features, e.g. `jasmine_node_test`.
591607
592-
Bazel always runs tests with a working directory set to your workspace root.
593-
If your test needs to run in a different directory, you can write a `process.chdir` helper script
594-
and invoke it before the test with a `--require` argument, like
595-
`templated_args = ["--node_options=--require=./$(rootpath chdir.js)"]`.
596-
See rules_nodejs/internal/node/test/chdir for an example.
608+
By default, Bazel runs tests with a working directory set to your workspace root.
609+
Use the `chdir` attribute to change the working directory before the program starts.
597610
598611
To debug a Node.js test, we recommend saving a group of flags together in a "config".
599612
Put this in your `tools/bazel.rc` so it's shared with your team:

internal/node/npm_package_bin.bzl

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ load("//internal/linker:link_node_modules.bzl", "module_mappings_aspect")
88
# so that we can generate macros that act as either an output-producing tool or an executable
99
_ATTRS = {
1010
"args": attr.string_list(mandatory = True),
11+
"chdir": attr.string(),
1112
"configuration_env_vars": attr.string_list(default = []),
1213
"data": attr.label_list(allow_files = True, aspects = [module_mappings_aspect, node_modules_aspect]),
1314
"exit_code_out": attr.output(),
@@ -78,6 +79,7 @@ def _impl(ctx):
7879
outputs = outputs,
7980
arguments = [args],
8081
configuration_env_vars = ctx.attr.configuration_env_vars,
82+
chdir = expand_variables(ctx, ctx.attr.chdir),
8183
stdout = ctx.outputs.stdout,
8284
stderr = ctx.outputs.stderr,
8385
exit_code_out = ctx.outputs.exit_code_out,
@@ -91,19 +93,16 @@ _npm_package_bin = rule(
9193
attrs = _ATTRS,
9294
)
9395

94-
def npm_package_bin(tool = None, package = None, package_bin = None, data = [], outs = [], args = [], output_dir = False, link_workspace_root = False, **kwargs):
96+
def npm_package_bin(tool = None, package = None, package_bin = None, data = [], outs = [], args = [], output_dir = False, link_workspace_root = False, chdir = None, **kwargs):
9597
"""Run an arbitrary npm package binary (e.g. a program under node_modules/.bin/*) under Bazel.
9698
9799
It must produce outputs. If you just want to run a program with `bazel run`, use the nodejs_binary rule.
98100
99101
This is like a genrule() except that it runs our launcher script that first
100102
links the node_modules tree before running the program.
101103
102-
Bazel always runs actions with a working directory set to your workspace root.
103-
If your tool needs to run in a different directory, you can write a `process.chdir` helper script
104-
and invoke it before the action with a `--require` argument, like
105-
`args = ["--node_options=--require=./$(execpath chdir.js)"]`
106-
See rules_nodejs/internal/node/test/chdir for an example.
104+
By default, Bazel runs actions with a working directory set to your workspace root.
105+
Use the `chdir` attribute to change the working directory before the program runs.
107106
108107
This is a great candidate to wrap with a macro, as documented:
109108
https://docs.bazel.build/versions/master/skylark/macros.html#full-example
@@ -168,6 +167,30 @@ def npm_package_bin(tool = None, package = None, package_bin = None, data = [],
168167
Note that you can also refer to a binary in your local workspace.
169168
link_workspace_root: Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'.
170169
If source files need to be required then they can be copied to the bin_dir with copy_to_bin.
170+
chdir: Working directory to run the binary or test in, relative to the workspace.
171+
172+
By default, Bazel always runs in the workspace root.
173+
174+
To run in the directory containing the `npm_package_bin` under the source tree, use
175+
`chdir = package_name()`
176+
(or if you're in a macro, use `native.package_name()`).
177+
178+
To run in the output directory where the npm_package_bin writes outputs, use
179+
`chdir = "$(RULEDIR)"`
180+
181+
NOTE that this can affect other paths passed to the program, which are workspace-relative.
182+
You may need `../../` segments to re-relativize such paths to the new working directory.
183+
In a BUILD file you could do something like this to point to the output path:
184+
185+
```python
186+
_package_segments = len(package_name().split("/"))
187+
npm_package_bin(
188+
...
189+
chdir = package_name(),
190+
args = ["/".join([".."] * _package_segments + ["$@"])],
191+
)
192+
```
193+
**kwargs: additional undocumented keyword args
171194
"""
172195
if not tool:
173196
if not package:
@@ -179,6 +202,7 @@ def npm_package_bin(tool = None, package = None, package_bin = None, data = [],
179202
data = data,
180203
outs = outs,
181204
args = args,
205+
chdir = chdir,
182206
output_dir = output_dir,
183207
tool = tool,
184208
link_workspace_root = link_workspace_root,

internal/node/test/chdir/BUILD.bazel

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,29 @@
33
# the project when you call them.
44
# See https://github.com/bazelbuild/rules_nodejs/issues/1840
55

6-
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", "nodejs_binary", "nodejs_test", "npm_package_bin")
7-
load("//third_party/github.com/bazelbuild/bazel-skylib:rules/write_file.bzl", "write_file")
6+
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary", "nodejs_test", "npm_package_bin")
7+
8+
# Trivial tool that expects to run in source directory under the package
9+
nodejs_binary(
10+
name = "tool_cp",
11+
entry_point = "cp.js",
12+
)
813

914
# A tool like react-scripts needs to run in the output directory since it writes outputs
1015
# to $pwd/build
1116
# That means it also needs to find inputs in that directory.
1217
# So we copy all the inputs it needs.
13-
copy_to_bin(
14-
name = "copy_input",
15-
srcs = ["package.json"],
16-
)
18+
_package_segments = len(package_name().split("/"))
1719

18-
# Here's our trick: write a process.chdir one-liner JS script
19-
write_file(
20-
name = "write_chdir_script",
21-
out = "chdir.js",
22-
# __dirname will be whatever directory the other outputs
23-
# from this package are in, either in execroot or runfiles root
24-
# depending on where Bazel places this file.
25-
content = ["process.chdir(__dirname)"],
20+
npm_package_bin(
21+
name = "do_copy",
22+
outs = ["package.json"],
23+
# We have to compensate for the changed directory, adapting other arguments
24+
# to reach back to parent directory
25+
args = ["/".join([".."] * _package_segments + ["$@"])],
26+
chdir = package_name(),
27+
data = ["_package.json"],
28+
tool = ":tool_cp",
2629
)
2730

2831
# Trivial tool to mock react-scripts
@@ -36,27 +39,15 @@ nodejs_binary(
3639
npm_package_bin(
3740
name = "call_tool",
3841
outs = ["build/app.js"],
39-
# This tool produces outputs, so it is a build action and needs execpath helper
40-
args = ["--node_options=--require=./$(execpath chdir.js)"],
41-
# We can't reference label "package.json" here because it's ambiguous
42-
# whether that points to the InputArtifact package.json in the src
43-
# folder or the output. Using "copy_input" is unambiguous.
44-
data = [
45-
"chdir.js",
46-
"copy_input",
47-
],
42+
chdir = "$(RULEDIR)",
43+
data = ["package.json"],
4844
tool = ":tool_bin",
4945
)
5046

5147
nodejs_test(
5248
name = "test",
53-
data = [
54-
"build/app.js",
55-
"chdir.js",
56-
],
49+
# Also run a test in the output directory
50+
chdir = package_name(),
51+
data = ["build/app.js"],
5752
entry_point = "test.js",
58-
# Also run a test in the output directory, this needs rootpath helper
59-
# NB: --require=./$(rootpath chdir.js) works when runfiles are symlinked
60-
# but $$(rlocation) is needed for Windows.
61-
templated_args = ["--node_options=--require=$$(rlocation $(rootpath chdir.js))"],
6253
)

internal/node/test/chdir/cp.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// assumes the working directory contains _package.json
2+
const fs = require('fs');
3+
const {dirname} = require('path');
4+
const dest = process.argv[2];
5+
6+
function mkdirp(p) {
7+
if (!fs.existsSync(p)) {
8+
mkdirp(dirname(p));
9+
fs.mkdirSync(p);
10+
}
11+
}
12+
13+
mkdirp(dirname(dest));
14+
fs.copyFileSync('_package.json', dest);

internal/node/test/chdir/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ const assert = require('assert');
33

44
// this test should be run in a working directory with
55
// that file in it
6-
assert.equal('console.log("hello world")', readFileSync('build/app.js', 'utf-8'));
6+
assert.strictEqual('console.log("hello world")', readFileSync('build/app.js', 'utf-8'));

internal/providers/node_runtime_deps_info.bzl

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def _compute_node_modules_root(ctx):
5656
fail("All npm dependencies need to come from a single workspace. Found '%s' and '%s'." % (node_modules_root, possible_root))
5757
return node_modules_root
5858

59-
def run_node(ctx, inputs, arguments, executable, **kwargs):
59+
def run_node(ctx, inputs, arguments, executable, chdir = None, **kwargs):
6060
"""Helper to replace ctx.actions.run
6161
6262
This calls node programs with a node_modules directory in place
@@ -66,10 +66,8 @@ def run_node(ctx, inputs, arguments, executable, **kwargs):
6666
inputs: list or depset of inputs to the action
6767
arguments: list or ctx.actions.Args object containing arguments to pass to the executable
6868
executable: stringy representation of the executable this action will run, eg eg. "my_executable" rather than ctx.executable.my_executable
69-
mnemonic: optional action mnemonic, used to differentiate module mapping files from the same rule context
70-
link_workspace_root: Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'.
71-
If source files need to be required then they can be copied to the bin_dir with copy_to_bin.
72-
kwargs: all other args accepted by ctx.actions.run
69+
chdir: directory we should change to be the working dir
70+
**kwargs: all other args accepted by ctx.actions.run
7371
"""
7472
if (type(executable) != "string"):
7573
fail("""run_node requires that executable be provided as a string,
@@ -112,6 +110,9 @@ def run_node(ctx, inputs, arguments, executable, **kwargs):
112110
add_arg(arguments, "--bazel_capture_exit_code=%s" % exit_code_file.path)
113111
outputs = outputs + [exit_code_file]
114112

113+
if chdir:
114+
add_arg(arguments, "--bazel_node_working_dir=" + chdir)
115+
115116
env = kwargs.pop("env", {})
116117

117118
# Always forward the COMPILATION_MODE to node process as an environment variable

0 commit comments

Comments
 (0)