/
link_node_modules.bzl
185 lines (155 loc) · 7.85 KB
/
link_node_modules.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
"""Helper function and aspect to collect first-party packages.
These are used in node rules to link the node_modules before launching a program.
This supports path re-mapping, to support short module names.
See pathMapping doc: https://github.com/Microsoft/TypeScript/issues/5039
This reads the module_root and module_name attributes from rules in
the transitive closure, rolling these up to provide a mapping to the
linker, which uses the mappings to link a node_modules directory for
runtimes to locate all the first-party packages.
"""
# Can't load from //:providers.bzl directly as that introduces a circular dep
load("//internal/providers:external_npm_package_info.bzl", "ExternalNpmPackageInfo")
load("//internal/providers:linkable_package_info.bzl", "LinkablePackageInfo")
def _debug(vars, *args):
if "VERBOSE_LOGS" in vars.keys():
print("[link_node_modules.bzl]", *args)
# Arbitrary name; must be chosen to globally avoid conflicts with any other aspect
MODULE_MAPPINGS_ASPECT_RESULTS_NAME = "link_node_modules__aspect_result"
# Traverse 'srcs' in addition so that we can go across a genrule
_MODULE_MAPPINGS_DEPS_NAMES = ["data", "deps", "srcs"]
def add_arg(args, arg):
"""Add an argument
Args:
args: either a list or a ctx.actions.Args object
arg: string arg to append on the end
"""
if (type(args) == type([])):
args.append(arg)
else:
args.add(arg)
def _link_mapping(label, mappings, k, v):
# Check that two package name mapping do not map to two different source paths
package_name = k.split(":")[0]
link_path = v
for iter_key, iter_values in mappings.items():
# Map key is of format "package_name:package_path"
# Map values are of format [deprecated, link_path]
iter_package_name = iter_key.split(":")[0]
iter_source_path = iter_values
if package_name == iter_package_name and link_path != iter_source_path:
fail("conflicting mapping at '%s': '%s' and '%s' map to conflicting %s and %s" % (label, k, iter_key, link_path, iter_source_path))
return True
def write_node_modules_manifest(ctx, extra_data = [], mnemonic = None, link_workspace_root = False):
"""Writes a manifest file read by the linker, containing info about resolving runtime dependencies
Args:
ctx: starlark rule execution context
extra_data: labels to search for npm packages that need to be linked (ctx.attr.deps and ctx.attr.data will always be searched)
mnemonic: optional action mnemonic, used to differentiate module mapping files from the same rule context
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.
"""
mappings = {ctx.workspace_name: ctx.bin_dir.path} if link_workspace_root else {}
node_modules_roots = {}
# Look through data/deps attributes to find the root directories for the third-party node_modules;
# we'll symlink local "node_modules" to them
for dep in extra_data + getattr(ctx.attr, "data", []) + getattr(ctx.attr, "deps", []):
if ExternalNpmPackageInfo in dep:
path = dep[ExternalNpmPackageInfo].path
workspace = dep[ExternalNpmPackageInfo].workspace
if path in node_modules_roots:
other_workspace = node_modules_roots[path]
if workspace != other_workspace:
fail("All npm dependencies at the path '%s' must come from a single workspace. Found '%s' and '%s'." % (path, other_workspace, workspace))
node_modules_roots[path] = workspace
# Look through data/deps attributes to find first party deps to link
for dep in extra_data + getattr(ctx.attr, "data", []) + getattr(ctx.attr, "deps", []):
for k, v in getattr(dep, MODULE_MAPPINGS_ASPECT_RESULTS_NAME, {}).items():
map_key_split = k.split(":")
package_name = map_key_split[0]
package_path = map_key_split[1] if len(map_key_split) > 1 else ""
if package_path not in node_modules_roots:
node_modules_roots[package_path] = ""
if _link_mapping(dep.label, mappings, k, v):
_debug(ctx.var, "Linking %s: %s" % (k, v))
mappings[k] = v
# Convert mappings to a module sets (modules per package package_path)
# {
# "package_path": {
# "package_name": "link_path",
# ...
# },
# ...
# }
module_sets = {}
for k, v in mappings.items():
map_key_split = k.split(":")
package_name = map_key_split[0]
package_path = map_key_split[1] if len(map_key_split) > 1 else ""
link_path = v
if package_path not in module_sets:
module_sets[package_path] = {}
module_sets[package_path][package_name] = link_path
# Write the result to a file, and use the magic node option --bazel_node_modules_manifest
# The launcher.sh will peel off this argument and pass it to the linker rather than the program.
prefix = ctx.label.name
if mnemonic != None:
prefix += "_%s" % mnemonic
modules_manifest = ctx.actions.declare_file("_%s.module_mappings.json" % prefix)
content = {
"bin": ctx.bin_dir.path,
"module_sets": module_sets,
"roots": node_modules_roots,
"workspace": ctx.workspace_name,
}
ctx.actions.write(modules_manifest, str(content))
return modules_manifest
def _get_module_mappings(target, ctx):
"""Gathers module mappings from LinkablePackageInfo which maps "package_name:package_path" to link_path.
Args:
target: target
ctx: ctx
Returns:
Returns module mappings of shape:
{ "package_name:package_path": link_path, ... }
"""
mappings = {}
# Propogate transitive mappings
for name in _MODULE_MAPPINGS_DEPS_NAMES:
for dep in getattr(ctx.rule.attr, name, []):
for k, v in getattr(dep, MODULE_MAPPINGS_ASPECT_RESULTS_NAME, {}).items():
if _link_mapping(target.label, mappings, k, v):
_debug(ctx.var, "target %s propagating module mapping %s: %s" % (dep.label, k, v))
mappings[k] = v
# Look for LinkablePackageInfo mapping in this node
if not LinkablePackageInfo in target:
# No mappings contributed here, short-circuit with the transitive ones we collected
_debug(ctx.var, "No LinkablePackageInfo for", target.label)
return mappings
linkable_package_info = target[LinkablePackageInfo]
# LinkablePackageInfo may be provided without a package_name so check for that case as well
if not linkable_package_info.package_name:
# No mappings contributed here, short-circuit with the transitive ones we collected
_debug(ctx.var, "No package_name in LinkablePackageInfo for", target.label)
return mappings
package_path = linkable_package_info.package_path if hasattr(linkable_package_info, "package_path") else ""
map_key = "%s:%s" % (linkable_package_info.package_name, package_path)
map_value = linkable_package_info.path
if _link_mapping(target.label, mappings, map_key, map_value):
_debug(ctx.var, "target %s adding module mapping %s: %s" % (target.label, map_key, map_value))
mappings[map_key] = map_value
# Returns mappings of shape:
# {
# "package_name:package_path": link_path,
# ...
# }
return mappings
def _module_mappings_aspect_impl(target, ctx):
# Use a dictionary to construct the result struct
# so that we can reference the MODULE_MAPPINGS_ASPECT_RESULTS_NAME variable
return struct(**{
MODULE_MAPPINGS_ASPECT_RESULTS_NAME: _get_module_mappings(target, ctx),
})
module_mappings_aspect = aspect(
_module_mappings_aspect_impl,
attr_aspects = _MODULE_MAPPINGS_DEPS_NAMES,
)