@@ -21,7 +21,17 @@ def _debug(vars, *args):
21
21
LinkerPackageMappingInfo = provider (
22
22
doc = """Provider capturing package mappings for the linker to consume.""" ,
23
23
fields = {
24
- "mappings" : "Dictionary of mappings. Maps package names to an exec path." ,
24
+ "mappings" : """Depset of structs with mapping info.
25
+
26
+ Each struct has the following fields:
27
+ package_name: The name of the package.
28
+ package_path: The root path of the node_modules under which this package should be linked.
29
+ link_path: The exec path under which the package is available.
30
+
31
+ Note: The depset may contain multiple entries per (package_name, package_path) pair.
32
+ Consumers should handle these duplicated appropriately. The depset uses topological order to ensure
33
+ that a target's mappings come before possibly conflicting mappings of its dependencies.""" ,
34
+ "node_modules_roots" : "Depset of node_module roots." ,
25
35
},
26
36
)
27
37
@@ -40,39 +50,54 @@ def add_arg(args, arg):
40
50
else :
41
51
args .add (arg )
42
52
43
- def _link_mapping (label , mappings , k , v ):
44
- # Check that two package name mapping do not map to two different source paths
45
- k_segs = k .split (":" )
46
- package_name = k_segs [0 ]
47
- package_path = k_segs [1 ] if len (k_segs ) > 1 else ""
48
- link_path = v
49
-
50
- for iter_key , iter_values in mappings .items ():
51
- # Map key is of format "package_name:package_path"
52
- # Map values are of format [deprecated, link_path]
53
- iter_segs = iter_key .split (":" )
54
- iter_package_name = iter_segs [0 ]
55
- iter_package_path = iter_segs [1 ] if len (iter_segs ) > 1 else ""
56
- iter_source_path = iter_values
57
- if package_name == iter_package_name and package_path == iter_package_path :
58
- # If we're linking to the output tree be tolerant of linking to different
59
- # output trees since we can have "static" links that come from cfg="exec" binaries.
60
- # In the future when we static link directly into runfiles without the linker
61
- # we can remove this logic.
62
- link_path_segments = link_path .split ("/" )
63
- iter_source_path_segments = iter_source_path .split ("/" )
64
- bin_links = len (link_path_segments ) >= 3 and len (iter_source_path_segments ) >= 3 and link_path_segments [0 ] == "bazel-out" and iter_source_path_segments [0 ] == "bazel-out" and link_path_segments [2 ] == "bin" and iter_source_path_segments [2 ] == "bin"
65
- if bin_links :
66
- compare_link_path = "/" .join (link_path_segments [3 :]) if len (link_path_segments ) > 3 else ""
67
- compare_iter_source_path = "/" .join (iter_source_path_segments [3 :]) if len (iter_source_path_segments ) > 3 else ""
68
- else :
69
- compare_link_path = link_path
70
- compare_iter_source_path = iter_source_path
71
- if compare_link_path != compare_iter_source_path :
72
- msg = "conflicting mapping at '%s': '%s' and '%s' map to conflicting %s and %s" % (label , k , iter_key , compare_link_path , compare_iter_source_path )
73
- fail (msg )
74
-
75
- return True
53
+ def _detect_conflicts (module_sets , mapping ):
54
+ """Verifies that the new mapping does not conflict with existing mappings in module_sets."""
55
+ if mapping .package_path not in module_sets :
56
+ return
57
+ existing_link_path = module_sets [mapping .package_path ].get (mapping .package_name )
58
+ if existing_link_path == None :
59
+ return
60
+
61
+ # If we're linking to the output tree be tolerant of linking to different
62
+ # output trees since we can have "static" links that come from cfg="exec" binaries.
63
+ # In the future when we static link directly into runfiles without the linker
64
+ # we can remove this logic.
65
+ link_path_segments = mapping .link_path .split ("/" )
66
+ existing_link_path_segments = existing_link_path .split ("/" )
67
+ bin_links = len (link_path_segments ) >= 3 and len (existing_link_path_segments ) >= 3 and link_path_segments [0 ] == "bazel-out" and existing_link_path_segments [0 ] == "bazel-out" and link_path_segments [2 ] == "bin" and existing_link_path_segments [2 ] == "bin"
68
+ if bin_links :
69
+ compare_link_path = "/" .join (link_path_segments [3 :]) if len (link_path_segments ) > 3 else ""
70
+ compare_existing_link_path = "/" .join (existing_link_path_segments [3 :]) if len (existing_link_path_segments ) > 3 else ""
71
+ else :
72
+ compare_link_path = mapping .link_path
73
+ compare_existing_link_path = existing_link_path
74
+ if compare_link_path != compare_existing_link_path :
75
+ msg = "conflicting mapping: '%s' (package path: %s) maps to conflicting paths '%s' and '%s'" % (mapping .package_name , mapping .package_path , compare_link_path , compare_existing_link_path )
76
+ fail (msg )
77
+
78
+ def _flatten_to_module_set (mappings_depset ):
79
+ """Convert a depset of mapping to a module sets (modules per package package_path).
80
+
81
+ The returned dictionary has the following structure:
82
+ {
83
+ "package_path": {
84
+ "package_name": "link_path",
85
+ ...
86
+ },
87
+ ...
88
+ }
89
+ """
90
+
91
+ # FIXME: Flattens a depset during the analysis phase. Ideally, this would be done during the
92
+ # execution phase using an Args object.
93
+ flattens_mappings = mappings_depset .to_list ()
94
+ module_sets = {}
95
+ for mapping in flattens_mappings :
96
+ _detect_conflicts (module_sets , mapping )
97
+ if mapping .package_path not in module_sets :
98
+ module_sets [mapping .package_path ] = {}
99
+ module_sets [mapping .package_path ][mapping .package_name ] = mapping .link_path
100
+ return module_sets
76
101
77
102
def write_node_modules_manifest (ctx , extra_data = [], mnemonic = None , link_workspace_root = False ):
78
103
"""Writes a manifest file read by the linker, containing info about resolving runtime dependencies
@@ -85,7 +110,6 @@ def write_node_modules_manifest(ctx, extra_data = [], mnemonic = None, link_work
85
110
If source files need to be required then they can be copied to the bin_dir with copy_to_bin.
86
111
"""
87
112
88
- mappings = {ctx .workspace_name : ctx .bin_dir .path } if link_workspace_root else {}
89
113
node_modules_roots = {}
90
114
91
115
# Look through data/deps attributes to find the root directories for the third-party node_modules;
@@ -100,38 +124,33 @@ def write_node_modules_manifest(ctx, extra_data = [], mnemonic = None, link_work
100
124
fail ("All npm dependencies at the path '%s' must come from a single workspace. Found '%s' and '%s'." % (path , other_workspace , workspace ))
101
125
node_modules_roots [path ] = workspace
102
126
103
- # Look through data/deps attributes to find first party deps to link
127
+ direct_mappings = []
128
+ direct_node_modules_roots = []
129
+ if link_workspace_root :
130
+ direct_mappings .append (struct (
131
+ package_name = ctx .workspace_name ,
132
+ package_path = "" ,
133
+ link_path = ctx .bin_dir .path ,
134
+ ))
135
+ direct_node_modules_roots .append ("" )
136
+
137
+ transitive_mappings = []
138
+ transitive_node_modules_roots = []
104
139
for dep in extra_data + getattr (ctx .attr , "data" , []) + getattr (ctx .attr , "deps" , []):
105
140
if not LinkerPackageMappingInfo in dep :
106
141
continue
142
+ transitive_mappings .append (dep [LinkerPackageMappingInfo ].mappings )
143
+ transitive_node_modules_roots .append (dep [LinkerPackageMappingInfo ].node_modules_roots )
107
144
108
- for k , v in dep [LinkerPackageMappingInfo ].mappings .items ():
109
- map_key_split = k .split (":" )
110
- package_name = map_key_split [0 ]
111
- package_path = map_key_split [1 ] if len (map_key_split ) > 1 else ""
112
- if package_path not in node_modules_roots :
113
- node_modules_roots [package_path ] = ""
114
- if _link_mapping (dep .label , mappings , k , v ):
115
- _debug (ctx .var , "Linking %s: %s" % (k , v ))
116
- mappings [k ] = v
117
-
118
- # Convert mappings to a module sets (modules per package package_path)
119
- # {
120
- # "package_path": {
121
- # "package_name": "link_path",
122
- # ...
123
- # },
124
- # ...
125
- # }
126
- module_sets = {}
127
- for k , v in mappings .items ():
128
- map_key_split = k .split (":" )
129
- package_name = map_key_split [0 ]
130
- package_path = map_key_split [1 ] if len (map_key_split ) > 1 else ""
131
- link_path = v
132
- if package_path not in module_sets :
133
- module_sets [package_path ] = {}
134
- module_sets [package_path ][package_name ] = link_path
145
+ mappings = depset (direct_mappings , transitive = transitive_mappings , order = "topological" )
146
+ module_sets = _flatten_to_module_set (mappings )
147
+
148
+ # FIXME: Flattens a depset during the analysis phase. Ideally, this would be done during the
149
+ # execution phase using an Args object.
150
+ linker_node_modules_roots = depset (direct_node_modules_roots , transitive = transitive_node_modules_roots ).to_list ()
151
+ for node_modules_root in linker_node_modules_roots :
152
+ if node_modules_root not in node_modules_roots :
153
+ node_modules_roots [node_modules_root ] = ""
135
154
136
155
# Write the result to a file, and use the magic node option --bazel_node_modules_manifest
137
156
# The launcher.sh will peel off this argument and pass it to the linker rather than the program.
@@ -148,58 +167,60 @@ def write_node_modules_manifest(ctx, extra_data = [], mnemonic = None, link_work
148
167
ctx .actions .write (modules_manifest , str (content ))
149
168
return modules_manifest
150
169
151
- def _get_module_mappings (target , ctx ):
152
- """Gathers module mappings from LinkablePackageInfo which maps "package_name:package_path" to link_path .
170
+ def _get_linker_package_mapping_info (target , ctx ):
171
+ """Transitively gathers module mappings and node_modules roots from LinkablePackageInfo .
153
172
154
173
Args:
155
174
target: target
156
175
ctx: ctx
157
176
158
177
Returns:
159
- Returns module mappings of shape:
160
- { "package_name:package_path": link_path, ... }
178
+ A LinkerPackageMappingInfo provider that contains the mappings and roots for the current
179
+ target and all its transitive dependencies.
161
180
"""
162
- mappings = {}
163
181
164
- # Propagate transitive mappings
182
+ transitive_mappings = []
183
+ transitive_node_modules_roots = []
165
184
for name in _MODULE_MAPPINGS_DEPS_NAMES :
166
185
for dep in getattr (ctx .rule .attr , name , []):
167
186
if not LinkerPackageMappingInfo in dep :
168
187
continue
188
+ transitive_mappings .append (dep [LinkerPackageMappingInfo ].mappings )
189
+ transitive_node_modules_roots .append (dep [LinkerPackageMappingInfo ].node_modules_roots )
169
190
170
- for k , v in dep [LinkerPackageMappingInfo ].mappings .items ():
171
- if _link_mapping (target .label , mappings , k , v ):
172
- _debug (ctx .var , "target %s propagating module mapping %s: %s" % (dep .label , k , v ))
173
- mappings [k ] = v
191
+ direct_mappings = []
192
+ direct_node_modules_roots = []
174
193
175
194
# Look for LinkablePackageInfo mapping in this node
195
+ # LinkablePackageInfo may be provided without a package_name so check for that case as well
176
196
if not LinkablePackageInfo in target :
177
- # No mappings contributed here, short-circuit with the transitive ones we collected
178
197
_debug (ctx .var , "No LinkablePackageInfo for" , target .label )
179
- return mappings
180
-
181
- linkable_package_info = target [LinkablePackageInfo ]
182
-
183
- # LinkablePackageInfo may be provided without a package_name so check for that case as well
184
- if not linkable_package_info .package_name :
185
- # No mappings contributed here, short-circuit with the transitive ones we collected
198
+ elif not target [LinkablePackageInfo ].package_name :
186
199
_debug (ctx .var , "No package_name in LinkablePackageInfo for" , target .label )
187
- return mappings
188
-
189
- package_path = linkable_package_info .package_path if hasattr (linkable_package_info , "package_path" ) else ""
190
- map_key = "%s:%s" % (linkable_package_info .package_name , package_path )
191
- map_value = linkable_package_info .path
192
-
193
- if _link_mapping (target .label , mappings , map_key , map_value ):
194
- _debug (ctx .var , "target %s adding module mapping %s: %s" % (target .label , map_key , map_value ))
195
- mappings [map_key ] = map_value
196
-
197
- # Returns mappings of shape:
198
- # {
199
- # "package_name:package_path": link_path,
200
- # ...
201
- # }
202
- return mappings
200
+ else :
201
+ linkable_package_info = target [LinkablePackageInfo ]
202
+ package_path = linkable_package_info .package_path if hasattr (linkable_package_info , "package_path" ) else ""
203
+ direct_mappings .append (
204
+ struct (
205
+ package_name = linkable_package_info .package_name ,
206
+ package_path = package_path ,
207
+ link_path = linkable_package_info .path ,
208
+ ),
209
+ )
210
+ _debug (ctx .var , "target %s (package path: %s) adding module mapping %s: %s" % (
211
+ target .label ,
212
+ package_path ,
213
+ linkable_package_info .package_name ,
214
+ linkable_package_info .path ,
215
+ ))
216
+ direct_node_modules_roots .append (package_path )
217
+
218
+ mappings = depset (direct_mappings , transitive = transitive_mappings , order = "topological" )
219
+ node_modules_roots = depset (direct_node_modules_roots , transitive = transitive_node_modules_roots )
220
+ return LinkerPackageMappingInfo (
221
+ mappings = mappings ,
222
+ node_modules_roots = node_modules_roots ,
223
+ )
203
224
204
225
def _module_mappings_aspect_impl (target , ctx ):
205
226
# If the target explicitly provides mapping information, we will not propagate
@@ -209,7 +230,7 @@ def _module_mappings_aspect_impl(target, ctx):
209
230
return []
210
231
211
232
return [
212
- LinkerPackageMappingInfo ( mappings = _get_module_mappings ( target , ctx ) ),
233
+ _get_linker_package_mapping_info ( target , ctx ),
213
234
]
214
235
215
236
module_mappings_aspect = aspect (
0 commit comments