blob: fa7318b2c969560580b91da7e95b2a8b2766fd87 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001# Copyright 2015 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Utility functions not specific to the rust toolchain."""
16
17load("@bazel_tools//tools/cpp:toolchain_utils.bzl", find_rules_cc_toolchain = "find_cpp_toolchain")
18load(":providers.bzl", "BuildInfo", "CrateInfo", "DepInfo", "DepVariantInfo")
19
20def find_toolchain(ctx):
21 """Finds the first rust toolchain that is configured.
22
23 Args:
24 ctx (ctx): The ctx object for the current target.
25
26 Returns:
27 rust_toolchain: A Rust toolchain context.
28 """
29 return ctx.toolchains[Label("//rust:toolchain")]
30
31def find_cc_toolchain(ctx):
32 """Extracts a CcToolchain from the current target's context
33
34 Args:
35 ctx (ctx): The current target's rule context object
36
37 Returns:
38 tuple: A tuple of (CcToolchain, FeatureConfiguration)
39 """
40 cc_toolchain = find_rules_cc_toolchain(ctx)
41
42 feature_configuration = cc_common.configure_features(
43 ctx = ctx,
44 cc_toolchain = cc_toolchain,
45 requested_features = ctx.features,
46 unsupported_features = ctx.disabled_features,
47 )
48 return cc_toolchain, feature_configuration
49
50# TODO: Replace with bazel-skylib's `path.dirname`. This requires addressing some
51# dependency issues or generating docs will break.
52def relativize(path, start):
53 """Returns the relative path from start to path.
54
55 Args:
56 path (str): The path to relativize.
57 start (str): The ancestor path against which to relativize.
58
59 Returns:
60 str: The portion of `path` that is relative to `start`.
61 """
62 src_parts = _path_parts(start)
63 dest_parts = _path_parts(path)
64 n = 0
65 for src_part, dest_part in zip(src_parts, dest_parts):
66 if src_part != dest_part:
67 break
68 n += 1
69
70 relative_path = ""
71 for _ in range(n, len(src_parts)):
72 relative_path += "../"
73 relative_path += "/".join(dest_parts[n:])
74
75 return relative_path
76
77def _path_parts(path):
78 """Takes a path and returns a list of its parts with all "." elements removed.
79
80 The main use case of this function is if one of the inputs to relativize()
81 is a relative path, such as "./foo".
82
83 Args:
84 path (str): A string representing a unix path
85
86 Returns:
87 list: A list containing the path parts with all "." elements removed.
88 """
89 path_parts = path.split("/")
90 return [part for part in path_parts if part != "."]
91
92def get_lib_name(lib):
93 """Returns the name of a library artifact, eg. libabc.a -> abc
94
95 Args:
96 lib (File): A library file
97
98 Returns:
99 str: The name of the library
100 """
101 # On macos and windows, dynamic/static libraries always end with the
102 # extension and potential versions will be before the extension, and should
103 # be part of the library name.
104 # On linux, the version usually comes after the extension.
105 # So regardless of the platform we want to find the extension and make
106 # everything left to it the library name.
107
108 # Search for the extension - starting from the right - by removing any
109 # trailing digit.
110 comps = lib.basename.split(".")
111 for comp in reversed(comps):
112 if comp.isdigit():
113 comps.pop()
114 else:
115 break
116
117 # The library name is now everything minus the extension.
118 libname = ".".join(comps[:-1])
119
120 if libname.startswith("lib"):
121 return libname[3:]
122 else:
123 return libname
124
125def abs(value):
126 """Returns the absolute value of a number.
127
128 Args:
129 value (int): A number.
130
131 Returns:
132 int: The absolute value of the number.
133 """
134 if value < 0:
135 return -value
136 return value
137
138def determine_output_hash(crate_root, label):
139 """Generates a hash of the crate root file's path.
140
141 Args:
142 crate_root (File): The crate's root file (typically `lib.rs`).
143 label (Label): The label of the target.
144
145 Returns:
146 str: A string representation of the hash.
147 """
148
149 # Take the absolute value of hash() since it could be negative.
150 h = abs(hash(crate_root.path) + hash(repr(label)))
151 return repr(h)
152
153def get_preferred_artifact(library_to_link, use_pic):
154 """Get the first available library to link from a LibraryToLink object.
155
156 Args:
157 library_to_link (LibraryToLink): See the followg links for additional details:
158 https://docs.bazel.build/versions/master/skylark/lib/LibraryToLink.html
159 use_pic: If set, prefers pic_static_library over static_library.
160
161 Returns:
162 File: Returns the first valid library type (only one is expected)
163 """
164 if use_pic:
165 return (
166 library_to_link.pic_static_library or
167 library_to_link.interface_library or
168 library_to_link.dynamic_library
169 )
170 else:
171 return (
172 library_to_link.static_library or
173 library_to_link.pic_static_library or
174 library_to_link.interface_library or
175 library_to_link.dynamic_library
176 )
177
178def _expand_location(ctx, env, data):
179 """A trivial helper for `_expand_locations`
180
181 Args:
182 ctx (ctx): The rule's context object
183 env (str): The value possibly containing location macros to expand.
184 data (sequence of Targets): see `_expand_locations`
185
186 Returns:
187 string: The location-macro expanded version of the string.
188 """
189 for directive in ("$(execpath ", "$(location "):
190 if directive in env:
191 # build script runner will expand pwd to execroot for us
192 env = env.replace(directive, "${pwd}/" + directive)
193 return ctx.expand_location(env, data)
194
195def expand_dict_value_locations(ctx, env, data):
196 """Performs location-macro expansion on string values.
197
198 $(execroot ...) and $(location ...) are prefixed with ${pwd},
199 which process_wrapper and build_script_runner will expand at run time
200 to the absolute path. This is necessary because include_str!() is relative
201 to the currently compiled file, and build scripts run relative to the
202 manifest dir, so we can not use execroot-relative paths.
203
204 $(rootpath ...) is unmodified, and is useful for passing in paths via
205 rustc_env that are encoded in the binary with env!(), but utilized at
206 runtime, such as in tests. The absolute paths are not usable in this case,
207 as compilation happens in a separate sandbox folder, so when it comes time
208 to read the file at runtime, the path is no longer valid.
209
210 See [`expand_location`](https://docs.bazel.build/versions/main/skylark/lib/ctx.html#expand_location) for detailed documentation.
211
212 Args:
213 ctx (ctx): The rule's context object
214 env (dict): A dict whose values we iterate over
215 data (sequence of Targets): The targets which may be referenced by
216 location macros. This is expected to be the `data` attribute of
217 the target, though may have other targets or attributes mixed in.
218
219 Returns:
220 dict: A dict of environment variables with expanded location macros
221 """
222 return dict([(k, _expand_location(ctx, v, data)) for (k, v) in env.items()])
223
224def expand_list_element_locations(ctx, args, data):
225 """Performs location-macro expansion on a list of string values.
226
227 $(execroot ...) and $(location ...) are prefixed with ${pwd},
228 which process_wrapper and build_script_runner will expand at run time
229 to the absolute path.
230
231 See [`expand_location`](https://docs.bazel.build/versions/main/skylark/lib/ctx.html#expand_location) for detailed documentation.
232
233 Args:
234 ctx (ctx): The rule's context object
235 args (list): A list we iterate over
236 data (sequence of Targets): The targets which may be referenced by
237 location macros. This is expected to be the `data` attribute of
238 the target, though may have other targets or attributes mixed in.
239
240 Returns:
241 list: A list of arguments with expanded location macros
242 """
243 return [_expand_location(ctx, arg, data) for arg in args]
244
245def name_to_crate_name(name):
246 """Converts a build target's name into the name of its associated crate.
247
248 Crate names cannot contain certain characters, such as -, which are allowed
249 in build target names. All illegal characters will be converted to
250 underscores.
251
252 This is a similar conversion as that which cargo does, taking a
253 `Cargo.toml`'s `package.name` and canonicalizing it
254
255 Note that targets can specify the `crate_name` attribute to customize their
256 crate name; in situations where this is important, use the
257 compute_crate_name() function instead.
258
259 Args:
260 name (str): The name of the target.
261
262 Returns:
263 str: The name of the crate for this target.
264 """
265 return name.replace("-", "_")
266
267def _invalid_chars_in_crate_name(name):
268 """Returns any invalid chars in the given crate name.
269
270 Args:
271 name (str): Name to test.
272
273 Returns:
274 list: List of invalid characters in the crate name.
275 """
276
277 return dict([(c, ()) for c in name.elems() if not (c.isalnum() or c == "_")]).keys()
278
279def compute_crate_name(workspace_name, label, toolchain, name_override = None):
280 """Returns the crate name to use for the current target.
281
282 Args:
283 workspace_name (string): The current workspace name.
284 label (struct): The label of the current target.
285 toolchain (struct): The toolchain in use for the target.
286 name_override (String): An optional name to use (as an override of label.name).
287
288 Returns:
289 str: The crate name to use for this target.
290 """
291 if name_override:
292 invalid_chars = _invalid_chars_in_crate_name(name_override)
293 if invalid_chars:
294 fail("Crate name '{}' contains invalid character(s): {}".format(
295 name_override,
296 " ".join(invalid_chars),
297 ))
298 return name_override
299
300 if (toolchain and label and toolchain._rename_first_party_crates and
301 should_encode_label_in_crate_name(workspace_name, label, toolchain._third_party_dir)):
302 crate_name = encode_label_as_crate_name(label.package, label.name)
303 else:
304 crate_name = name_to_crate_name(label.name)
305
306 invalid_chars = _invalid_chars_in_crate_name(crate_name)
307 if invalid_chars:
308 fail(
309 "Crate name '{}' ".format(crate_name) +
310 "derived from Bazel target name '{}' ".format(label.name) +
311 "contains invalid character(s): {}\n".format(" ".join(invalid_chars)) +
312 "Consider adding a crate_name attribute to set a valid crate name",
313 )
314 return crate_name
315
316def dedent(doc_string):
317 """Remove any common leading whitespace from every line in text.
318
319 This functionality is similar to python's `textwrap.dedent` functionality
320 https://docs.python.org/3/library/textwrap.html#textwrap.dedent
321
322 Args:
323 doc_string (str): A docstring style string
324
325 Returns:
326 str: A string optimized for stardoc rendering
327 """
328 lines = doc_string.splitlines()
329 if not lines:
330 return doc_string
331
332 # If the first line is empty, use the second line
333 first_line = lines[0]
334 if not first_line:
335 first_line = lines[1]
336
337 # Detect how much space prepends the first line and subtract that from all lines
338 space_count = len(first_line) - len(first_line.lstrip())
339
340 # If there are no leading spaces, do not alter the docstring
341 if space_count == 0:
342 return doc_string
343 else:
344 # Remove the leading block of spaces from the current line
345 block = " " * space_count
346 return "\n".join([line.replace(block, "", 1).rstrip() for line in lines])
347
348def make_static_lib_symlink(actions, rlib_file):
349 """Add a .a symlink to an .rlib file.
350
351 The name of the symlink is derived from the <name> of the <name>.rlib file as follows:
352 * `<name>.a`, if <name> starts with `lib`
353 * `lib<name>.a`, otherwise.
354
355 For example, the name of the symlink for
356 * `libcratea.rlib` is `libcratea.a`
357 * `crateb.rlib` is `libcrateb.a`.
358
359 Args:
360 actions (actions): The rule's context actions object.
361 rlib_file (File): The file to symlink, which must end in .rlib.
362
363 Returns:
364 The symlink's File.
365 """
366 if not rlib_file.basename.endswith(".rlib"):
367 fail("file is not an .rlib: ", rlib_file.basename)
368 basename = rlib_file.basename[:-5]
369 if not basename.startswith("lib"):
370 basename = "lib" + basename
371 dot_a = actions.declare_file(basename + ".a", sibling = rlib_file)
372 actions.symlink(output = dot_a, target_file = rlib_file)
373 return dot_a
374
375def is_exec_configuration(ctx):
376 """Determine if a context is building for the exec configuration.
377
378 This is helpful when processing command line flags that should apply
379 to the target configuration but not the exec configuration.
380
381 Args:
382 ctx (ctx): The ctx object for the current target.
383
384 Returns:
385 True if the exec configuration is detected, False otherwise.
386 """
387
388 # TODO(djmarcin): Is there any better way to determine cfg=exec?
389 return ctx.genfiles_dir.path.find("-exec-") != -1
390
391def transform_deps(deps):
392 """Transforms a [Target] into [DepVariantInfo].
393
394 This helper function is used to transform ctx.attr.deps and ctx.attr.proc_macro_deps into
395 [DepVariantInfo].
396
397 Args:
398 deps (list of Targets): Dependencies coming from ctx.attr.deps or ctx.attr.proc_macro_deps
399
400 Returns:
401 list of DepVariantInfos.
402 """
403 return [DepVariantInfo(
404 crate_info = dep[CrateInfo] if CrateInfo in dep else None,
405 dep_info = dep[DepInfo] if DepInfo in dep else None,
406 build_info = dep[BuildInfo] if BuildInfo in dep else None,
407 cc_info = dep[CcInfo] if CcInfo in dep else None,
408 ) for dep in deps]
409
410def get_import_macro_deps(ctx):
411 """Returns a list of targets to be added to proc_macro_deps.
412
413 Args:
414 ctx (struct): the ctx of the current target.
415
416 Returns:
417 list of Targets. Either empty (if the fake import macro implementation
418 is being used), or a singleton list with the real implementation.
419 """
420 if ctx.attr._import_macro_dep.label.name == "fake_import_macro_impl":
421 return []
422
423 return [ctx.attr._import_macro_dep]
424
425def should_encode_label_in_crate_name(workspace_name, label, third_party_dir):
426 """Determines if the crate's name should include the Bazel label, encoded.
427
428 Crate names may only encode the label if the target is in the current repo,
429 the target is not in the third_party_dir, and the current repo is not
430 rules_rust.
431
432 Args:
433 workspace_name (string): The name of the current workspace.
434 label (Label): The package in question.
435 third_party_dir (string): The directory in which third-party packages are kept.
436
437 Returns:
438 True if the crate name should encode the label, False otherwise.
439 """
440
441 # TODO(hlopko): This code assumes a monorepo; make it work with external
442 # repositories as well.
443 return (
444 workspace_name != "rules_rust" and
445 not label.workspace_root and
446 not ("//" + label.package + "/").startswith(third_party_dir + "/")
447 )
448
449# This is a list of pairs, where the first element of the pair is a character
450# that is allowed in Bazel package or target names but not in crate names; and
451# the second element is an encoding of that char suitable for use in a crate
452# name.
453_encodings = (
454 (":", "colon"),
455 ("!", "bang"),
456 ("%", "percent"),
457 ("@", "at"),
458 ("^", "caret"),
459 ("`", "backtick"),
460 (" ", "space"),
461 ("\"", "quote"),
462 ("#", "hash"),
463 ("$", "dollar"),
464 ("&", "ampersand"),
465 ("'", "backslash"),
466 ("(", "lparen"),
467 (")", "rparen"),
468 ("*", "star"),
469 ("-", "dash"),
470 ("+", "plus"),
471 (",", "comma"),
472 (";", "semicolon"),
473 ("<", "langle"),
474 ("=", "equal"),
475 (">", "rangle"),
476 ("?", "question"),
477 ("[", "lbracket"),
478 ("]", "rbracket"),
479 ("{", "lbrace"),
480 ("|", "pipe"),
481 ("}", "rbrace"),
482 ("~", "tilde"),
483 ("/", "slash"),
484 (".", "dot"),
485)
486
487# For each of the above encodings, we generate two substitution rules: one that
488# ensures any occurrences of the encodings themselves in the package/target
489# aren't clobbered by this translation, and one that does the encoding itself.
490# We also include a rule that protects the clobbering-protection rules from
491# getting clobbered.
492_substitutions = [("_quote", "_quotequote_")] + [
493 subst
494 for (pattern, replacement) in _encodings
495 for subst in (
496 ("_{}_".format(replacement), "_quote{}_".format(replacement)),
497 (pattern, "_{}_".format(replacement)),
498 )
499]
500
501def encode_label_as_crate_name(package, name):
502 """Encodes the package and target names in a format suitable for a crate name.
503
504 Args:
505 package (string): The package of the target in question.
506 name (string): The name of the target in question.
507
508 Returns:
509 A string that encodes the package and target name, to be used as the crate's name.
510 """
511 full_name = package + ":" + name
512 return _replace_all(full_name, _substitutions)
513
514def decode_crate_name_as_label_for_testing(crate_name):
515 """Decodes a crate_name that was encoded by encode_label_as_crate_name.
516
517 This is used to check that the encoding is bijective; it is expected to only
518 be used in tests.
519
520 Args:
521 crate_name (string): The name of the crate.
522
523 Returns:
524 A string representing the Bazel label (package and target).
525 """
526 return _replace_all(crate_name, [(t[1], t[0]) for t in _substitutions])
527
528def _replace_all(string, substitutions):
529 """Replaces occurrences of the given patterns in `string`.
530
531 There are a few reasons this looks complicated:
532 * The substitutions are performed with some priority, i.e. patterns that are
533 listed first in `substitutions` are higher priority than patterns that are
534 listed later.
535 * We also take pains to avoid doing replacements that overlap with each
536 other, since overlaps invalidate pattern matches.
537 * To avoid hairy offset invalidation, we apply the substitutions
538 right-to-left.
539 * To avoid the "_quote" -> "_quotequote_" rule introducing new pattern
540 matches later in the string during decoding, we take the leftmost
541 replacement, in cases of overlap. (Note that no rule can induce new
542 pattern matches *earlier* in the string.) (E.g. "_quotedot_" encodes to
543 "_quotequote_dot_". Note that "_quotequote_" and "_dot_" both occur in
544 this string, and overlap.).
545
546 Args:
547 string (string): the string in which the replacements should be performed.
548 substitutions: the list of patterns and replacements to apply.
549
550 Returns:
551 A string with the appropriate substitutions performed.
552 """
553
554 # Find the highest-priority pattern matches for each string index, going
555 # left-to-right and skipping indices that are already involved in a
556 # pattern match.
557 plan = {}
558 matched_indices_set = {}
559 for pattern_start in range(len(string)):
560 if pattern_start in matched_indices_set:
561 continue
562 for (pattern, replacement) in substitutions:
563 if not string.startswith(pattern, pattern_start):
564 continue
565 length = len(pattern)
566 plan[pattern_start] = (length, replacement)
567 matched_indices_set.update([(pattern_start + i, True) for i in range(length)])
568 break
569
570 # Execute the replacement plan, working from right to left.
571 for pattern_start in sorted(plan.keys(), reverse = True):
572 length, replacement = plan[pattern_start]
573 after_pattern = pattern_start + length
574 string = string[:pattern_start] + replacement + string[after_pattern:]
575
576 return string