blob: e27eb2b7d128dce21fec5819e6422cb1dfa15d15 [file] [log] [blame]
"A generic rule to run a tool that appears in node_modules/.bin"
load("//:providers.bzl", "DeclarationInfo", "ExternalNpmPackageInfo", "JSModuleInfo", "node_modules_aspect", "run_node")
load("//internal/common:expand_variables.bzl", "expand_variables")
load("//internal/linker:link_node_modules.bzl", "module_mappings_aspect")
# Note: this API is chosen to match nodejs_binary
# so that we can generate macros that act as either an output-producing tool or an executable
_ATTRS = {
"args": attr.string_list(mandatory = True),
"chdir": attr.string(),
"configuration_env_vars": attr.string_list(default = []),
"data": attr.label_list(allow_files = True, aspects = [module_mappings_aspect, node_modules_aspect]),
"exit_code_out": attr.output(),
"link_workspace_root": attr.bool(),
"output_dir": attr.bool(),
"outs": attr.output_list(),
"stderr": attr.output(),
"stdout": attr.output(),
"tool": attr.label(
executable = True,
cfg = "host",
mandatory = True,
),
}
def _expand_locations(ctx, s):
# `.split(" ")` is a work-around https://github.com/bazelbuild/bazel/issues/10309
# _expand_locations returns an array of args to support $(execpaths) expansions.
# TODO: If the string has intentional spaces or if one or more of the expanded file
# locations has a space in the name, we will incorrectly split it into multiple arguments
return ctx.expand_location(s, targets = ctx.attr.data).split(" ")
def _inputs(ctx):
# Also include files from npm fine grained deps as inputs.
# These deps are identified by the ExternalNpmPackageInfo provider.
inputs_depsets = []
for d in ctx.attr.data:
if ExternalNpmPackageInfo in d:
inputs_depsets.append(d[ExternalNpmPackageInfo].sources)
if JSModuleInfo in d:
inputs_depsets.append(d[JSModuleInfo].sources)
if DeclarationInfo in d:
inputs_depsets.append(d[DeclarationInfo].declarations)
return depset(ctx.files.data, transitive = inputs_depsets).to_list()
def _impl(ctx):
if ctx.attr.output_dir and ctx.outputs.outs:
fail("Only one of output_dir and outs may be specified")
if not ctx.attr.output_dir and not ctx.outputs.outs and not ctx.attr.stdout:
fail("One of output_dir, outs or stdout must be specified")
args = ctx.actions.args()
inputs = _inputs(ctx)
outputs = []
if ctx.attr.output_dir:
outputs = [ctx.actions.declare_directory(ctx.attr.name)]
else:
outputs = ctx.outputs.outs
for a in ctx.attr.args:
args.add_all([expand_variables(ctx, e, outs = ctx.outputs.outs, output_dir = ctx.attr.output_dir) for e in _expand_locations(ctx, a)])
tool_outputs = []
if ctx.outputs.stdout:
tool_outputs.append(ctx.outputs.stdout)
if ctx.outputs.stderr:
tool_outputs.append(ctx.outputs.stderr)
if ctx.outputs.exit_code_out:
tool_outputs.append(ctx.outputs.exit_code_out)
run_node(
ctx,
executable = "tool",
inputs = inputs,
outputs = outputs,
arguments = [args],
configuration_env_vars = ctx.attr.configuration_env_vars,
chdir = expand_variables(ctx, ctx.attr.chdir),
stdout = ctx.outputs.stdout,
stderr = ctx.outputs.stderr,
exit_code_out = ctx.outputs.exit_code_out,
link_workspace_root = ctx.attr.link_workspace_root,
)
return [DefaultInfo(files = depset(outputs + tool_outputs))]
_npm_package_bin = rule(
_impl,
attrs = _ATTRS,
)
def npm_package_bin(tool = None, package = None, package_bin = None, data = [], outs = [], args = [], output_dir = False, link_workspace_root = False, chdir = None, **kwargs):
"""Run an arbitrary npm package binary (e.g. a program under node_modules/.bin/*) under Bazel.
It must produce outputs. If you just want to run a program with `bazel run`, use the nodejs_binary rule.
This is like a genrule() except that it runs our launcher script that first
links the node_modules tree before running the program.
By default, Bazel runs actions with a working directory set to your workspace root.
Use the `chdir` attribute to change the working directory before the program runs.
This is a great candidate to wrap with a macro, as documented:
https://docs.bazel.build/versions/master/skylark/macros.html#full-example
Args:
data: similar to [genrule.srcs](https://docs.bazel.build/versions/master/be/general.html#genrule.srcs)
may also include targets that produce or reference npm packages which are needed by the tool
outs: similar to [genrule.outs](https://docs.bazel.build/versions/master/be/general.html#genrule.outs)
output_dir: set to True if you want the output to be a directory
Exactly one of `outs`, `output_dir` may be used.
If you output a directory, there can only be one output, which will be a directory named the same as the target.
stderr: set to capture the stderr of the binary to a file, which can later be used as an input to another target
subject to the same semantics as `outs`
stdout: set to capture the stdout of the binary to a file, which can later be used as an input to another target
subject to the same semantics as `outs`
exit_code_out: set to capture the exit code of the binary to a file, which can later be used as an input to another target
subject to the same semantics as `outs`. Note that setting this will force the binary to exit 0.
If the binary creates outputs and these are declared, they must still be created
args: Command-line arguments to the tool.
Subject to 'Make variable' substitution. See https://docs.bazel.build/versions/master/be/make-variables.html.
1. Predefined source/output path substitions is applied first:
See https://docs.bazel.build/versions/master/be/make-variables.html#predefined_label_variables.
Use $(execpath) $(execpaths) to expand labels to the execroot (where Bazel runs build actions).
Use $(rootpath) $(rootpaths) to expand labels to the runfiles path that a built binary can use
to find its dependencies.
Since npm_package_bin is used primarily for build actions, in most cases you'll want to
use $(execpath) or $(execpaths) to expand locations.
Using $(location) and $(locations) expansions is not recommended as these are a synonyms
for either $(execpath) or $(rootpath) depending on the context.
2. "Make" variables are expanded second:
Predefined "Make" variables such as $(COMPILATION_MODE) and $(TARGET_CPU) are expanded.
See https://docs.bazel.build/versions/master/be/make-variables.html#predefined_variables.
Like genrule, you may also use some syntax sugar for locations.
- `$@`: if you have only one output file, the location of the output
- `$(@D)`: The output directory. If output_dir=False and there is only one file name in outs, this expands to the directory
containing that file. If there are multiple files, this instead expands to the package's root directory in the genfiles
tree, even if all generated files belong to the same subdirectory! If output_dir=True then this corresponds
to the output directory which is the $(RULEDIR)/{target_name}.
- `$(RULEDIR)`: the root output directory of the rule, corresponding with its package
(can be used with output_dir=True or False)
See https://docs.bazel.build/versions/master/be/make-variables.html#predefined_genrule_variables.
Custom variables are also expanded including variables set through the Bazel CLI with --define=SOME_VAR=SOME_VALUE.
See https://docs.bazel.build/versions/master/be/make-variables.html#custom_variables.
package: an npm package whose binary to run, like "terser". Assumes your node_modules are installed in a workspace called "npm"
package_bin: the "bin" entry from `package` that should be run. By default package_bin is the same string as `package`
tool: a label for a binary to run, like `@npm//terser/bin:terser`. This is the longer form of package/package_bin.
Note that you can also refer to a binary in your local workspace.
link_workspace_root: Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'.
If source files need to be required then they can be copied to the bin_dir with copy_to_bin.
chdir: Working directory to run the binary or test in, relative to the workspace.
By default, Bazel always runs in the workspace root.
To run in the directory containing the `npm_package_bin` under the source tree, use
`chdir = package_name()`
(or if you're in a macro, use `native.package_name()`).
To run in the output directory where the npm_package_bin writes outputs, use
`chdir = "$(RULEDIR)"`
WARNING: this will affect other paths passed to the program, either as arguments or in configuration files,
which are workspace-relative.
You may need `../../` segments to re-relativize such paths to the new working directory.
In a `BUILD` file you could do something like this to point to the output path:
```python
_package_segments = len(package_name().split("/"))
npm_package_bin(
...
chdir = package_name(),
# ../.. segments to re-relative paths from the chdir back to workspace
args = ["/".join([".."] * _package_segments + ["$@"])],
)
```
**kwargs: additional undocumented keyword args
"""
if not tool:
if not package:
fail("You must supply either the tool or package attribute")
if not package_bin:
package_bin = package
tool = "@npm//%s/bin:%s" % (package, package_bin)
_npm_package_bin(
data = data,
outs = outs,
args = args,
chdir = chdir,
output_dir = output_dir,
tool = tool,
link_workspace_root = link_workspace_root,
**kwargs
)