blob: db017a8ed14bcb629c26d1738cdd5b77143f3bab [file]
"A generic rule to run a tool that appears in node_modules/.bin"
load("@build_bazel_rules_nodejs//:providers.bzl", "NpmPackageInfo", "node_modules_aspect", "run_node")
load("@build_bazel_rules_nodejs//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 = {
"outs": attr.output_list(),
"args": attr.string_list(mandatory = True),
"data": attr.label_list(allow_files = True, aspects = [module_mappings_aspect, node_modules_aspect]),
"output_dir": attr.bool(),
"tool": attr.label(
executable = True,
cfg = "host",
mandatory = True,
),
}
# Need a custom expand_location function
# because the output_dir is a tree artifact
# so we weren't able to give it a label
def _expand_location(ctx, s):
outdir_segments = [ctx.bin_dir.path, ctx.label.package]
if ctx.attr.output_dir:
# We'll write into a newly created directory named after the rule
outdir_segments.append(ctx.attr.name)
# The list comprehension removes empty segments like if we are in the root package
s = s.replace("$@", "/".join([o for o in outdir_segments if o]))
return ctx.expand_location(s, targets = ctx.attr.data)
def _inputs(ctx):
# Also include files from npm fine grained deps as inputs.
# These deps are identified by the NpmPackageInfo provider.
inputs_depsets = []
for d in ctx.attr.data:
if NpmPackageInfo in d:
inputs_depsets.append(d[NpmPackageInfo].sources)
return depset(ctx.files.data, transitive = inputs_depsets).to_list()
def _impl(ctx):
if ctx.attr.output_dir and ctx.attr.outs:
fail("Only one of output_dir and outs may be specified")
if not ctx.attr.output_dir and not ctx.attr.outs:
fail("One of output_dir and outs 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(_expand_location(ctx, a))
run_node(
ctx,
executable = "tool",
inputs = inputs,
outputs = outputs,
arguments = [args],
)
return [DefaultInfo(files = depset(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, **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.
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.
args: Command-line arguments to the tool.
Subject to 'Make variable' substitution.
Can use $(location) expansion. See https://docs.bazel.build/versions/master/be/make-variables.html
You may also refer to the location of the output_dir with the special `$@` replacement, like genrule.
If output_dir=False then $@ will refer to the output directory for this package.
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.
"""
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,
output_dir = output_dir,
tool = tool,
**kwargs
)