-
Notifications
You must be signed in to change notification settings - Fork 522
/
node.bzl
249 lines (230 loc) · 8.73 KB
/
node.bzl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rules for executing programs in the nodejs runtime.
"""
load(":common/module_mappings.bzl", "module_mappings_runtime_aspect")
def _sources_aspect_impl(target, ctx):
result = depset()
if hasattr(ctx.rule.attr, "deps"):
for dep in ctx.rule.attr.deps:
if hasattr(dep, "node_sources"):
result += dep.node_sources
# Note layering: until we have JS interop providers, this needs to know how to
# get TypeScript outputs.
if hasattr(target, "typescript"):
result += target.typescript.es5_sources
elif hasattr(target, "files"):
result += target.files
return struct(node_sources = result)
sources_aspect = aspect(
_sources_aspect_impl,
attr_aspects = ["deps"],
)
def _write_loader_script(ctx):
# Generates the JavaScript snippet of module roots mappings, with each entry
# in the form:
# {module_name: /^mod_name\b/, module_root: 'path/to/mod_name'}
module_mappings = []
for d in ctx.attr.data:
if hasattr(d, "runfiles_module_mappings"):
for [mn, mr] in d.runfiles_module_mappings.items():
escaped = mn.replace("/", r"\/").replace(".", r"\.")
mapping = r"{module_name: /^%s\b/, module_root: '%s'}" % (escaped, mr)
module_mappings.append(mapping)
ctx.template_action(
template=ctx.file._loader_template,
output=ctx.outputs.loader,
substitutions={
"TEMPLATED_module_roots": "\n " + ",\n ".join(module_mappings),
"TEMPLATED_entry_point": ctx.attr.entry_point,
"TEMPLATED_label_package": ctx.attr.node_modules.label.package,
# There are two workspaces in general:
# A) The user's workspace is the one where the bazel command is run
# B) The label's workspace contains the target being built/run
#
# If A has an npm dependency on B, then we'll look in the node_modules
# to find B's dependency D. It could be in two different places
# depending on hoisting [1]:
# A/node_modules/B/node_modules/D
# A/node_modules/D
# That means we must resolve runfiles relative to A.
#
# However if A has a bazel dependency on B, then B is not under A's
# node_modules directory.
# A
# B/node_modules/D
# That means we must resolve runfiles relative to B.
#
# Since Bazel does not tell us whether the label's workspace was
# created with `local_repository(path="node_modules/blah")` we can't
# distinguish the two cases. Therefore we add both workspaces to the
# resolution search paths.
#
# [1] https://yarnpkg.com/lang/en/docs/workspaces/#toc-limitations-caveats
"TEMPLATED_user_workspace_name": ctx.workspace_name,
"TEMPLATED_label_workspace_name": (
ctx.label.workspace_root.split("/")[1]
if ctx.label.workspace_root
# If the label is in the same workspace as the user, we don't
# need another search location.
else ""
),
},
executable=True,
)
def expand_location_into_runfiles(ctx, path):
"""If the path has a location expansion, expand it. Otherwise return as-is.
"""
if path.find('$(location') < 0:
return path
return expand_path_into_runfiles(ctx, path)
def expand_path_into_runfiles(ctx, path):
"""Given a file path that might contain a $(location) label expansion,
provide the path to the file in runfiles.
See https://docs.bazel.build/versions/master/skylark/lib/ctx.html#expand_location
"""
targets = ctx.attr.data if hasattr(ctx.attr, "data") else []
expanded = ctx.expand_location(path, targets)
if expanded.startswith(ctx.bin_dir.path):
expanded = expanded[len(ctx.bin_dir.path + "/"):]
if expanded.startswith(ctx.genfiles_dir.path):
expanded = expanded[len(ctx.genfiles_dir.path + "/"):]
return ctx.workspace_name + "/" + expanded
def _nodejs_binary_impl(ctx):
node = ctx.file._node
node_modules = ctx.files.node_modules
sources = depset()
for d in ctx.attr.data:
if hasattr(d, "node_sources"):
sources += d.node_sources
_write_loader_script(ctx)
# Avoid writing non-normalized paths (workspace/../other_workspace/path)
if ctx.outputs.loader.short_path.startswith("../"):
script_path = ctx.outputs.loader.short_path[len("../"):]
else:
script_path = "/".join([
ctx.workspace_name,
ctx.outputs.loader.short_path,
])
substitutions = {
"TEMPLATED_node": ctx.workspace_name + "/" + node.path,
"TEMPLATED_args": " ".join([
expand_location_into_runfiles(ctx, a)
for a in ctx.attr.templated_args]),
"TEMPLATED_script_path": script_path,
}
# Write the output twice.
# In order to have the name "nodejs_test", the rule must be declared
# with test = True, which means we must write an output called "executable".
# However, in order to wrap with a sh_test for Windows, we must be able to
# get a single output file with a ".sh" extension.
ctx.template_action(
template=ctx.file._launcher_template,
output=ctx.outputs.executable,
substitutions=substitutions,
executable=True,
)
ctx.template_action(
template=ctx.file._launcher_template,
output=ctx.outputs.script,
substitutions=substitutions,
executable=True,
)
runfiles = depset(sources)
runfiles += [node]
runfiles += [ctx.outputs.loader]
runfiles += node_modules
return struct(
runfiles = ctx.runfiles(
transitive_files = runfiles,
files = [node, ctx.outputs.loader] + node_modules + sources.to_list(),
collect_data = True,
),
)
_NODEJS_EXECUTABLE_ATTRS = {
"entry_point": attr.string(mandatory = True),
"data": attr.label_list(
allow_files = True,
cfg = "data",
aspects=[sources_aspect, module_mappings_runtime_aspect]),
"templated_args": attr.string_list(),
"_node": attr.label(
default = Label("@nodejs//:node"),
allow_files = True,
single_file = True),
"node_modules": attr.label(
# By default, binaries use the node_modules in the workspace
# where the bazel command is run. This assumes that any needed
# dependencies are installed there, commonly due to a transitive
# dependency on a package like @bazel/typescript.
# See discussion: https://github.com/bazelbuild/rules_typescript/issues/13
default = Label("@//:node_modules")),
"_launcher_template": attr.label(
default = Label("//internal:node_launcher.sh"),
allow_files = True,
single_file = True),
"_loader_template": attr.label(
default = Label("//internal:node_loader.js"),
allow_files = True,
single_file = True),
}
_NODEJS_EXECUTABLE_OUTPUTS = {
"loader": "%{name}_loader.js",
"script": "%{name}.sh",
}
# The name of the declared rule appears in
# bazel query --output=label_kind
# So we make these match what the user types in their BUILD file
# and duplicate the definitions to give two distinct symbols.
nodejs_binary = rule(
implementation = _nodejs_binary_impl,
attrs = _NODEJS_EXECUTABLE_ATTRS,
executable = True,
outputs = _NODEJS_EXECUTABLE_OUTPUTS,
)
nodejs_test = rule(
implementation = _nodejs_binary_impl,
attrs = _NODEJS_EXECUTABLE_ATTRS,
test = True,
outputs = _NODEJS_EXECUTABLE_OUTPUTS,
)
# Wrap in an sh_binary for windows .exe wrapper.
def nodejs_binary_macro(name, args=[], visibility=None, tags=[], **kwargs):
nodejs_binary(
name = "%s_bin" % name,
**kwargs
)
native.sh_binary(
name = name,
args = args,
tags = tags,
srcs = [":%s_bin.sh" % name],
data = [":%s_bin" % name],
visibility = visibility,
)
# Wrap in an sh_test for windows .exe wrapper.
def nodejs_test_macro(name, args=[], visibility=None, tags=[], **kwargs):
nodejs_test(
name = "%s_bin" % name,
testonly = 1,
**kwargs
)
native.sh_test(
name = name,
args = args,
tags = tags,
visibility = visibility,
srcs = [":%s_bin.sh" % name],
data = [":%s_bin" % name],
)