Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 1 | # 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 | |
| 17 | load("@bazel_tools//tools/cpp:toolchain_utils.bzl", find_rules_cc_toolchain = "find_cpp_toolchain") |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 18 | load(":providers.bzl", "BuildInfo", "CrateGroupInfo", "CrateInfo", "DepInfo", "DepVariantInfo") |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 19 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 20 | UNSUPPORTED_FEATURES = [ |
| 21 | "thin_lto", |
| 22 | "module_maps", |
| 23 | "use_header_modules", |
| 24 | "fdo_instrument", |
| 25 | "fdo_optimize", |
| 26 | ] |
| 27 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 28 | def find_toolchain(ctx): |
| 29 | """Finds the first rust toolchain that is configured. |
| 30 | |
| 31 | Args: |
| 32 | ctx (ctx): The ctx object for the current target. |
| 33 | |
| 34 | Returns: |
| 35 | rust_toolchain: A Rust toolchain context. |
| 36 | """ |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 37 | return ctx.toolchains[Label("//rust:toolchain_type")] |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 38 | |
| 39 | def find_cc_toolchain(ctx): |
| 40 | """Extracts a CcToolchain from the current target's context |
| 41 | |
| 42 | Args: |
| 43 | ctx (ctx): The current target's rule context object |
| 44 | |
| 45 | Returns: |
| 46 | tuple: A tuple of (CcToolchain, FeatureConfiguration) |
| 47 | """ |
| 48 | cc_toolchain = find_rules_cc_toolchain(ctx) |
| 49 | |
| 50 | feature_configuration = cc_common.configure_features( |
| 51 | ctx = ctx, |
| 52 | cc_toolchain = cc_toolchain, |
| 53 | requested_features = ctx.features, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 54 | unsupported_features = UNSUPPORTED_FEATURES + ctx.disabled_features, |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 55 | ) |
| 56 | return cc_toolchain, feature_configuration |
| 57 | |
| 58 | # TODO: Replace with bazel-skylib's `path.dirname`. This requires addressing some |
| 59 | # dependency issues or generating docs will break. |
| 60 | def relativize(path, start): |
| 61 | """Returns the relative path from start to path. |
| 62 | |
| 63 | Args: |
| 64 | path (str): The path to relativize. |
| 65 | start (str): The ancestor path against which to relativize. |
| 66 | |
| 67 | Returns: |
| 68 | str: The portion of `path` that is relative to `start`. |
| 69 | """ |
| 70 | src_parts = _path_parts(start) |
| 71 | dest_parts = _path_parts(path) |
| 72 | n = 0 |
| 73 | for src_part, dest_part in zip(src_parts, dest_parts): |
| 74 | if src_part != dest_part: |
| 75 | break |
| 76 | n += 1 |
| 77 | |
| 78 | relative_path = "" |
| 79 | for _ in range(n, len(src_parts)): |
| 80 | relative_path += "../" |
| 81 | relative_path += "/".join(dest_parts[n:]) |
| 82 | |
| 83 | return relative_path |
| 84 | |
| 85 | def _path_parts(path): |
| 86 | """Takes a path and returns a list of its parts with all "." elements removed. |
| 87 | |
| 88 | The main use case of this function is if one of the inputs to relativize() |
| 89 | is a relative path, such as "./foo". |
| 90 | |
| 91 | Args: |
| 92 | path (str): A string representing a unix path |
| 93 | |
| 94 | Returns: |
| 95 | list: A list containing the path parts with all "." elements removed. |
| 96 | """ |
| 97 | path_parts = path.split("/") |
| 98 | return [part for part in path_parts if part != "."] |
| 99 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 100 | def get_lib_name_default(lib): |
| 101 | """Returns the name of a library artifact. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 102 | |
| 103 | Args: |
| 104 | lib (File): A library file |
| 105 | |
| 106 | Returns: |
| 107 | str: The name of the library |
| 108 | """ |
| 109 | # On macos and windows, dynamic/static libraries always end with the |
| 110 | # extension and potential versions will be before the extension, and should |
| 111 | # be part of the library name. |
| 112 | # On linux, the version usually comes after the extension. |
| 113 | # So regardless of the platform we want to find the extension and make |
| 114 | # everything left to it the library name. |
| 115 | |
| 116 | # Search for the extension - starting from the right - by removing any |
| 117 | # trailing digit. |
| 118 | comps = lib.basename.split(".") |
| 119 | for comp in reversed(comps): |
| 120 | if comp.isdigit(): |
| 121 | comps.pop() |
| 122 | else: |
| 123 | break |
| 124 | |
| 125 | # The library name is now everything minus the extension. |
| 126 | libname = ".".join(comps[:-1]) |
| 127 | |
| 128 | if libname.startswith("lib"): |
| 129 | return libname[3:] |
| 130 | else: |
| 131 | return libname |
| 132 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 133 | # TODO: Could we remove this function in favor of a "windows" parameter in the |
| 134 | # above function? It looks like currently lambdas cannot accept local parameters |
| 135 | # so the following doesn't work: |
| 136 | # args.add_all( |
| 137 | # cc_toolchain.dynamic_runtime_lib(feature_configuration = feature_configuration), |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 138 | # map_each = lambda x: get_lib_name(x, for_windows = toolchain.target_os.startswith("windows)), |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 139 | # format_each = "-ldylib=%s", |
| 140 | # ) |
| 141 | def get_lib_name_for_windows(lib): |
| 142 | """Returns the name of a library artifact for Windows builds. |
| 143 | |
| 144 | Args: |
| 145 | lib (File): A library file |
| 146 | |
| 147 | Returns: |
| 148 | str: The name of the library |
| 149 | """ |
| 150 | # On macos and windows, dynamic/static libraries always end with the |
| 151 | # extension and potential versions will be before the extension, and should |
| 152 | # be part of the library name. |
| 153 | # On linux, the version usually comes after the extension. |
| 154 | # So regardless of the platform we want to find the extension and make |
| 155 | # everything left to it the library name. |
| 156 | |
| 157 | # Search for the extension - starting from the right - by removing any |
| 158 | # trailing digit. |
| 159 | comps = lib.basename.split(".") |
| 160 | for comp in reversed(comps): |
| 161 | if comp.isdigit(): |
| 162 | comps.pop() |
| 163 | else: |
| 164 | break |
| 165 | |
| 166 | # The library name is now everything minus the extension. |
| 167 | libname = ".".join(comps[:-1]) |
| 168 | |
| 169 | return libname |
| 170 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 171 | def abs(value): |
| 172 | """Returns the absolute value of a number. |
| 173 | |
| 174 | Args: |
| 175 | value (int): A number. |
| 176 | |
| 177 | Returns: |
| 178 | int: The absolute value of the number. |
| 179 | """ |
| 180 | if value < 0: |
| 181 | return -value |
| 182 | return value |
| 183 | |
| 184 | def determine_output_hash(crate_root, label): |
| 185 | """Generates a hash of the crate root file's path. |
| 186 | |
| 187 | Args: |
| 188 | crate_root (File): The crate's root file (typically `lib.rs`). |
| 189 | label (Label): The label of the target. |
| 190 | |
| 191 | Returns: |
| 192 | str: A string representation of the hash. |
| 193 | """ |
| 194 | |
| 195 | # Take the absolute value of hash() since it could be negative. |
| 196 | h = abs(hash(crate_root.path) + hash(repr(label))) |
| 197 | return repr(h) |
| 198 | |
| 199 | def get_preferred_artifact(library_to_link, use_pic): |
| 200 | """Get the first available library to link from a LibraryToLink object. |
| 201 | |
| 202 | Args: |
| 203 | library_to_link (LibraryToLink): See the followg links for additional details: |
| 204 | https://docs.bazel.build/versions/master/skylark/lib/LibraryToLink.html |
| 205 | use_pic: If set, prefers pic_static_library over static_library. |
| 206 | |
| 207 | Returns: |
| 208 | File: Returns the first valid library type (only one is expected) |
| 209 | """ |
| 210 | if use_pic: |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 211 | # Order consistent with https://github.com/bazelbuild/bazel/blob/815dfdabb7df31d4e99b6fc7616ff8e2f9385d98/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkingContext.java#L437. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 212 | return ( |
| 213 | library_to_link.pic_static_library or |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 214 | library_to_link.static_library or |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 215 | library_to_link.interface_library or |
| 216 | library_to_link.dynamic_library |
| 217 | ) |
| 218 | else: |
| 219 | return ( |
| 220 | library_to_link.static_library or |
| 221 | library_to_link.pic_static_library or |
| 222 | library_to_link.interface_library or |
| 223 | library_to_link.dynamic_library |
| 224 | ) |
| 225 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 226 | # The normal ctx.expand_location, but with an additional deduplication step. |
| 227 | # We do this to work around a potential crash, see |
| 228 | # https://github.com/bazelbuild/bazel/issues/16664 |
| 229 | def dedup_expand_location(ctx, input, targets = []): |
| 230 | return ctx.expand_location(input, _deduplicate(targets)) |
| 231 | |
| 232 | def _deduplicate(xs): |
| 233 | return {x: True for x in xs}.keys() |
| 234 | |
| 235 | def concat(xss): |
| 236 | return [x for xs in xss for x in xs] |
| 237 | |
| 238 | def _expand_location_for_build_script_runner(ctx, env, data): |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 239 | """A trivial helper for `expand_dict_value_locations` and `expand_list_element_locations` |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 240 | |
| 241 | Args: |
| 242 | ctx (ctx): The rule's context object |
| 243 | env (str): The value possibly containing location macros to expand. |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 244 | data (sequence of Targets): See one of the parent functions. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 245 | |
| 246 | Returns: |
| 247 | string: The location-macro expanded version of the string. |
| 248 | """ |
| 249 | for directive in ("$(execpath ", "$(location "): |
| 250 | if directive in env: |
| 251 | # build script runner will expand pwd to execroot for us |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 252 | env = env.replace(directive, "$${pwd}/" + directive) |
| 253 | return ctx.expand_make_variables( |
| 254 | env, |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 255 | dedup_expand_location(ctx, env, data), |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 256 | {}, |
| 257 | ) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 258 | |
| 259 | def expand_dict_value_locations(ctx, env, data): |
| 260 | """Performs location-macro expansion on string values. |
| 261 | |
| 262 | $(execroot ...) and $(location ...) are prefixed with ${pwd}, |
| 263 | which process_wrapper and build_script_runner will expand at run time |
| 264 | to the absolute path. This is necessary because include_str!() is relative |
| 265 | to the currently compiled file, and build scripts run relative to the |
| 266 | manifest dir, so we can not use execroot-relative paths. |
| 267 | |
| 268 | $(rootpath ...) is unmodified, and is useful for passing in paths via |
| 269 | rustc_env that are encoded in the binary with env!(), but utilized at |
| 270 | runtime, such as in tests. The absolute paths are not usable in this case, |
| 271 | as compilation happens in a separate sandbox folder, so when it comes time |
| 272 | to read the file at runtime, the path is no longer valid. |
| 273 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 274 | For detailed documentation, see: |
| 275 | - [`expand_location`](https://bazel.build/rules/lib/ctx#expand_location) |
| 276 | - [`expand_make_variables`](https://bazel.build/rules/lib/ctx#expand_make_variables) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 277 | |
| 278 | Args: |
| 279 | ctx (ctx): The rule's context object |
| 280 | env (dict): A dict whose values we iterate over |
| 281 | data (sequence of Targets): The targets which may be referenced by |
| 282 | location macros. This is expected to be the `data` attribute of |
| 283 | the target, though may have other targets or attributes mixed in. |
| 284 | |
| 285 | Returns: |
| 286 | dict: A dict of environment variables with expanded location macros |
| 287 | """ |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 288 | return dict([(k, _expand_location_for_build_script_runner(ctx, v, data)) for (k, v) in env.items()]) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 289 | |
| 290 | def expand_list_element_locations(ctx, args, data): |
| 291 | """Performs location-macro expansion on a list of string values. |
| 292 | |
| 293 | $(execroot ...) and $(location ...) are prefixed with ${pwd}, |
| 294 | which process_wrapper and build_script_runner will expand at run time |
| 295 | to the absolute path. |
| 296 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 297 | For detailed documentation, see: |
| 298 | - [`expand_location`](https://bazel.build/rules/lib/ctx#expand_location) |
| 299 | - [`expand_make_variables`](https://bazel.build/rules/lib/ctx#expand_make_variables) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 300 | |
| 301 | Args: |
| 302 | ctx (ctx): The rule's context object |
| 303 | args (list): A list we iterate over |
| 304 | data (sequence of Targets): The targets which may be referenced by |
| 305 | location macros. This is expected to be the `data` attribute of |
| 306 | the target, though may have other targets or attributes mixed in. |
| 307 | |
| 308 | Returns: |
| 309 | list: A list of arguments with expanded location macros |
| 310 | """ |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 311 | return [_expand_location_for_build_script_runner(ctx, arg, data) for arg in args] |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 312 | |
| 313 | def name_to_crate_name(name): |
| 314 | """Converts a build target's name into the name of its associated crate. |
| 315 | |
| 316 | Crate names cannot contain certain characters, such as -, which are allowed |
| 317 | in build target names. All illegal characters will be converted to |
| 318 | underscores. |
| 319 | |
| 320 | This is a similar conversion as that which cargo does, taking a |
| 321 | `Cargo.toml`'s `package.name` and canonicalizing it |
| 322 | |
| 323 | Note that targets can specify the `crate_name` attribute to customize their |
| 324 | crate name; in situations where this is important, use the |
| 325 | compute_crate_name() function instead. |
| 326 | |
| 327 | Args: |
| 328 | name (str): The name of the target. |
| 329 | |
| 330 | Returns: |
| 331 | str: The name of the crate for this target. |
| 332 | """ |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 333 | for illegal in ("-", "/"): |
| 334 | name = name.replace(illegal, "_") |
| 335 | return name |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 336 | |
| 337 | def _invalid_chars_in_crate_name(name): |
| 338 | """Returns any invalid chars in the given crate name. |
| 339 | |
| 340 | Args: |
| 341 | name (str): Name to test. |
| 342 | |
| 343 | Returns: |
| 344 | list: List of invalid characters in the crate name. |
| 345 | """ |
| 346 | |
| 347 | return dict([(c, ()) for c in name.elems() if not (c.isalnum() or c == "_")]).keys() |
| 348 | |
| 349 | def compute_crate_name(workspace_name, label, toolchain, name_override = None): |
| 350 | """Returns the crate name to use for the current target. |
| 351 | |
| 352 | Args: |
| 353 | workspace_name (string): The current workspace name. |
| 354 | label (struct): The label of the current target. |
| 355 | toolchain (struct): The toolchain in use for the target. |
| 356 | name_override (String): An optional name to use (as an override of label.name). |
| 357 | |
| 358 | Returns: |
| 359 | str: The crate name to use for this target. |
| 360 | """ |
| 361 | if name_override: |
| 362 | invalid_chars = _invalid_chars_in_crate_name(name_override) |
| 363 | if invalid_chars: |
| 364 | fail("Crate name '{}' contains invalid character(s): {}".format( |
| 365 | name_override, |
| 366 | " ".join(invalid_chars), |
| 367 | )) |
| 368 | return name_override |
| 369 | |
| 370 | if (toolchain and label and toolchain._rename_first_party_crates and |
| 371 | should_encode_label_in_crate_name(workspace_name, label, toolchain._third_party_dir)): |
| 372 | crate_name = encode_label_as_crate_name(label.package, label.name) |
| 373 | else: |
| 374 | crate_name = name_to_crate_name(label.name) |
| 375 | |
| 376 | invalid_chars = _invalid_chars_in_crate_name(crate_name) |
| 377 | if invalid_chars: |
| 378 | fail( |
| 379 | "Crate name '{}' ".format(crate_name) + |
| 380 | "derived from Bazel target name '{}' ".format(label.name) + |
| 381 | "contains invalid character(s): {}\n".format(" ".join(invalid_chars)) + |
| 382 | "Consider adding a crate_name attribute to set a valid crate name", |
| 383 | ) |
| 384 | return crate_name |
| 385 | |
| 386 | def dedent(doc_string): |
| 387 | """Remove any common leading whitespace from every line in text. |
| 388 | |
| 389 | This functionality is similar to python's `textwrap.dedent` functionality |
| 390 | https://docs.python.org/3/library/textwrap.html#textwrap.dedent |
| 391 | |
| 392 | Args: |
| 393 | doc_string (str): A docstring style string |
| 394 | |
| 395 | Returns: |
| 396 | str: A string optimized for stardoc rendering |
| 397 | """ |
| 398 | lines = doc_string.splitlines() |
| 399 | if not lines: |
| 400 | return doc_string |
| 401 | |
| 402 | # If the first line is empty, use the second line |
| 403 | first_line = lines[0] |
| 404 | if not first_line: |
| 405 | first_line = lines[1] |
| 406 | |
| 407 | # Detect how much space prepends the first line and subtract that from all lines |
| 408 | space_count = len(first_line) - len(first_line.lstrip()) |
| 409 | |
| 410 | # If there are no leading spaces, do not alter the docstring |
| 411 | if space_count == 0: |
| 412 | return doc_string |
| 413 | else: |
| 414 | # Remove the leading block of spaces from the current line |
| 415 | block = " " * space_count |
| 416 | return "\n".join([line.replace(block, "", 1).rstrip() for line in lines]) |
| 417 | |
| 418 | def make_static_lib_symlink(actions, rlib_file): |
| 419 | """Add a .a symlink to an .rlib file. |
| 420 | |
| 421 | The name of the symlink is derived from the <name> of the <name>.rlib file as follows: |
| 422 | * `<name>.a`, if <name> starts with `lib` |
| 423 | * `lib<name>.a`, otherwise. |
| 424 | |
| 425 | For example, the name of the symlink for |
| 426 | * `libcratea.rlib` is `libcratea.a` |
| 427 | * `crateb.rlib` is `libcrateb.a`. |
| 428 | |
| 429 | Args: |
| 430 | actions (actions): The rule's context actions object. |
| 431 | rlib_file (File): The file to symlink, which must end in .rlib. |
| 432 | |
| 433 | Returns: |
| 434 | The symlink's File. |
| 435 | """ |
| 436 | if not rlib_file.basename.endswith(".rlib"): |
| 437 | fail("file is not an .rlib: ", rlib_file.basename) |
| 438 | basename = rlib_file.basename[:-5] |
| 439 | if not basename.startswith("lib"): |
| 440 | basename = "lib" + basename |
| 441 | dot_a = actions.declare_file(basename + ".a", sibling = rlib_file) |
| 442 | actions.symlink(output = dot_a, target_file = rlib_file) |
| 443 | return dot_a |
| 444 | |
| 445 | def is_exec_configuration(ctx): |
| 446 | """Determine if a context is building for the exec configuration. |
| 447 | |
| 448 | This is helpful when processing command line flags that should apply |
| 449 | to the target configuration but not the exec configuration. |
| 450 | |
| 451 | Args: |
| 452 | ctx (ctx): The ctx object for the current target. |
| 453 | |
| 454 | Returns: |
| 455 | True if the exec configuration is detected, False otherwise. |
| 456 | """ |
| 457 | |
| 458 | # TODO(djmarcin): Is there any better way to determine cfg=exec? |
| 459 | return ctx.genfiles_dir.path.find("-exec-") != -1 |
| 460 | |
| 461 | def transform_deps(deps): |
| 462 | """Transforms a [Target] into [DepVariantInfo]. |
| 463 | |
| 464 | This helper function is used to transform ctx.attr.deps and ctx.attr.proc_macro_deps into |
| 465 | [DepVariantInfo]. |
| 466 | |
| 467 | Args: |
| 468 | deps (list of Targets): Dependencies coming from ctx.attr.deps or ctx.attr.proc_macro_deps |
| 469 | |
| 470 | Returns: |
| 471 | list of DepVariantInfos. |
| 472 | """ |
| 473 | return [DepVariantInfo( |
| 474 | crate_info = dep[CrateInfo] if CrateInfo in dep else None, |
| 475 | dep_info = dep[DepInfo] if DepInfo in dep else None, |
| 476 | build_info = dep[BuildInfo] if BuildInfo in dep else None, |
| 477 | cc_info = dep[CcInfo] if CcInfo in dep else None, |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 478 | crate_group_info = dep[CrateGroupInfo] if CrateGroupInfo in dep else None, |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 479 | ) for dep in deps] |
| 480 | |
| 481 | def get_import_macro_deps(ctx): |
| 482 | """Returns a list of targets to be added to proc_macro_deps. |
| 483 | |
| 484 | Args: |
| 485 | ctx (struct): the ctx of the current target. |
| 486 | |
| 487 | Returns: |
| 488 | list of Targets. Either empty (if the fake import macro implementation |
| 489 | is being used), or a singleton list with the real implementation. |
| 490 | """ |
| 491 | if ctx.attr._import_macro_dep.label.name == "fake_import_macro_impl": |
| 492 | return [] |
| 493 | |
| 494 | return [ctx.attr._import_macro_dep] |
| 495 | |
| 496 | def should_encode_label_in_crate_name(workspace_name, label, third_party_dir): |
| 497 | """Determines if the crate's name should include the Bazel label, encoded. |
| 498 | |
| 499 | Crate names may only encode the label if the target is in the current repo, |
| 500 | the target is not in the third_party_dir, and the current repo is not |
| 501 | rules_rust. |
| 502 | |
| 503 | Args: |
| 504 | workspace_name (string): The name of the current workspace. |
| 505 | label (Label): The package in question. |
| 506 | third_party_dir (string): The directory in which third-party packages are kept. |
| 507 | |
| 508 | Returns: |
| 509 | True if the crate name should encode the label, False otherwise. |
| 510 | """ |
| 511 | |
| 512 | # TODO(hlopko): This code assumes a monorepo; make it work with external |
| 513 | # repositories as well. |
| 514 | return ( |
| 515 | workspace_name != "rules_rust" and |
| 516 | not label.workspace_root and |
| 517 | not ("//" + label.package + "/").startswith(third_party_dir + "/") |
| 518 | ) |
| 519 | |
| 520 | # This is a list of pairs, where the first element of the pair is a character |
| 521 | # that is allowed in Bazel package or target names but not in crate names; and |
| 522 | # the second element is an encoding of that char suitable for use in a crate |
| 523 | # name. |
| 524 | _encodings = ( |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 525 | (":", "x"), |
| 526 | ("!", "excl"), |
| 527 | ("%", "prc"), |
| 528 | ("@", "ao"), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 529 | ("^", "caret"), |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 530 | ("`", "bt"), |
| 531 | (" ", "sp"), |
| 532 | ("\"", "dq"), |
| 533 | ("#", "octo"), |
| 534 | ("$", "dllr"), |
| 535 | ("&", "amp"), |
| 536 | ("'", "sq"), |
| 537 | ("(", "lp"), |
| 538 | (")", "rp"), |
| 539 | ("*", "astr"), |
| 540 | ("-", "d"), |
| 541 | ("+", "pl"), |
| 542 | (",", "cm"), |
| 543 | (";", "sm"), |
| 544 | ("<", "la"), |
| 545 | ("=", "eq"), |
| 546 | (">", "ra"), |
| 547 | ("?", "qm"), |
| 548 | ("[", "lbk"), |
| 549 | ("]", "rbk"), |
| 550 | ("{", "lbe"), |
| 551 | ("|", "pp"), |
| 552 | ("}", "rbe"), |
| 553 | ("~", "td"), |
| 554 | ("/", "y"), |
| 555 | (".", "pd"), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 556 | ) |
| 557 | |
| 558 | # For each of the above encodings, we generate two substitution rules: one that |
| 559 | # ensures any occurrences of the encodings themselves in the package/target |
| 560 | # aren't clobbered by this translation, and one that does the encoding itself. |
| 561 | # We also include a rule that protects the clobbering-protection rules from |
| 562 | # getting clobbered. |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 563 | _substitutions = [("_z", "_zz_")] + [ |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 564 | subst |
| 565 | for (pattern, replacement) in _encodings |
| 566 | for subst in ( |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 567 | ("_{}_".format(replacement), "_z{}_".format(replacement)), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 568 | (pattern, "_{}_".format(replacement)), |
| 569 | ) |
| 570 | ] |
| 571 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 572 | # Expose the substitutions for testing only. |
| 573 | substitutions_for_testing = _substitutions |
| 574 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 575 | def encode_label_as_crate_name(package, name): |
| 576 | """Encodes the package and target names in a format suitable for a crate name. |
| 577 | |
| 578 | Args: |
| 579 | package (string): The package of the target in question. |
| 580 | name (string): The name of the target in question. |
| 581 | |
| 582 | Returns: |
| 583 | A string that encodes the package and target name, to be used as the crate's name. |
| 584 | """ |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 585 | return _encode_raw_string(package + ":" + name) |
| 586 | |
| 587 | def _encode_raw_string(str): |
| 588 | """Encodes a string using the above encoding format. |
| 589 | |
| 590 | Args: |
| 591 | str (string): The string to be encoded. |
| 592 | |
| 593 | Returns: |
| 594 | An encoded version of the input string. |
| 595 | """ |
| 596 | return _replace_all(str, _substitutions) |
| 597 | |
| 598 | # Expose the underlying encoding function for testing only. |
| 599 | encode_raw_string_for_testing = _encode_raw_string |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 600 | |
| 601 | def decode_crate_name_as_label_for_testing(crate_name): |
| 602 | """Decodes a crate_name that was encoded by encode_label_as_crate_name. |
| 603 | |
| 604 | This is used to check that the encoding is bijective; it is expected to only |
| 605 | be used in tests. |
| 606 | |
| 607 | Args: |
| 608 | crate_name (string): The name of the crate. |
| 609 | |
| 610 | Returns: |
| 611 | A string representing the Bazel label (package and target). |
| 612 | """ |
| 613 | return _replace_all(crate_name, [(t[1], t[0]) for t in _substitutions]) |
| 614 | |
| 615 | def _replace_all(string, substitutions): |
| 616 | """Replaces occurrences of the given patterns in `string`. |
| 617 | |
| 618 | There are a few reasons this looks complicated: |
| 619 | * The substitutions are performed with some priority, i.e. patterns that are |
| 620 | listed first in `substitutions` are higher priority than patterns that are |
| 621 | listed later. |
| 622 | * We also take pains to avoid doing replacements that overlap with each |
| 623 | other, since overlaps invalidate pattern matches. |
| 624 | * To avoid hairy offset invalidation, we apply the substitutions |
| 625 | right-to-left. |
| 626 | * To avoid the "_quote" -> "_quotequote_" rule introducing new pattern |
| 627 | matches later in the string during decoding, we take the leftmost |
| 628 | replacement, in cases of overlap. (Note that no rule can induce new |
| 629 | pattern matches *earlier* in the string.) (E.g. "_quotedot_" encodes to |
| 630 | "_quotequote_dot_". Note that "_quotequote_" and "_dot_" both occur in |
| 631 | this string, and overlap.). |
| 632 | |
| 633 | Args: |
| 634 | string (string): the string in which the replacements should be performed. |
| 635 | substitutions: the list of patterns and replacements to apply. |
| 636 | |
| 637 | Returns: |
| 638 | A string with the appropriate substitutions performed. |
| 639 | """ |
| 640 | |
| 641 | # Find the highest-priority pattern matches for each string index, going |
| 642 | # left-to-right and skipping indices that are already involved in a |
| 643 | # pattern match. |
| 644 | plan = {} |
| 645 | matched_indices_set = {} |
| 646 | for pattern_start in range(len(string)): |
| 647 | if pattern_start in matched_indices_set: |
| 648 | continue |
| 649 | for (pattern, replacement) in substitutions: |
| 650 | if not string.startswith(pattern, pattern_start): |
| 651 | continue |
| 652 | length = len(pattern) |
| 653 | plan[pattern_start] = (length, replacement) |
| 654 | matched_indices_set.update([(pattern_start + i, True) for i in range(length)]) |
| 655 | break |
| 656 | |
| 657 | # Execute the replacement plan, working from right to left. |
| 658 | for pattern_start in sorted(plan.keys(), reverse = True): |
| 659 | length, replacement = plan[pattern_start] |
| 660 | after_pattern = pattern_start + length |
| 661 | string = string[:pattern_start] + replacement + string[after_pattern:] |
| 662 | |
| 663 | return string |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 664 | |
| 665 | def can_build_metadata(toolchain, ctx, crate_type): |
| 666 | """Can we build metadata for this rust_library? |
| 667 | |
| 668 | Args: |
| 669 | toolchain (toolchain): The rust toolchain |
| 670 | ctx (ctx): The rule's context object |
| 671 | crate_type (String): one of lib|rlib|dylib|staticlib|cdylib|proc-macro |
| 672 | |
| 673 | Returns: |
| 674 | bool: whether we can build metadata for this rule. |
| 675 | """ |
| 676 | |
| 677 | # In order to enable pipelined compilation we require that: |
| 678 | # 1) The _pipelined_compilation flag is enabled, |
| 679 | # 2) the OS running the rule is something other than windows as we require sandboxing (for now), |
| 680 | # 3) process_wrapper is enabled (this is disabled when compiling process_wrapper itself), |
| 681 | # 4) the crate_type is rlib or lib. |
| 682 | return toolchain._pipelined_compilation and \ |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 683 | toolchain.exec_triple.system != "windows" and \ |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 684 | ctx.attr._process_wrapper and \ |
| 685 | crate_type in ("rlib", "lib") |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 686 | |
| 687 | def crate_root_src(name, srcs, crate_type): |
| 688 | """Determines the source file for the crate root, should it not be specified in `attr.crate_root`. |
| 689 | |
| 690 | Args: |
| 691 | name (str): The name of the target. |
| 692 | srcs (list): A list of all sources for the target Crate. |
| 693 | crate_type (str): The type of this crate ("bin", "lib", "rlib", "cdylib", etc). |
| 694 | |
| 695 | Returns: |
| 696 | File: The root File object for a given crate. See the following links for more details: |
| 697 | - https://doc.rust-lang.org/cargo/reference/cargo-targets.html#library |
| 698 | - https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries |
| 699 | """ |
| 700 | default_crate_root_filename = "main.rs" if crate_type == "bin" else "lib.rs" |
| 701 | |
| 702 | crate_root = ( |
| 703 | (srcs[0] if len(srcs) == 1 else None) or |
| 704 | _shortest_src_with_basename(srcs, default_crate_root_filename) or |
| 705 | _shortest_src_with_basename(srcs, name + ".rs") |
| 706 | ) |
| 707 | if not crate_root: |
| 708 | file_names = [default_crate_root_filename, name + ".rs"] |
| 709 | fail("No {} source file found.".format(" or ".join(file_names)), "srcs") |
| 710 | return crate_root |
| 711 | |
| 712 | def _shortest_src_with_basename(srcs, basename): |
| 713 | """Finds the shortest among the paths in srcs that match the desired basename. |
| 714 | |
| 715 | Args: |
| 716 | srcs (list): A list of File objects |
| 717 | basename (str): The target basename to match against. |
| 718 | |
| 719 | Returns: |
| 720 | File: The File object with the shortest path that matches `basename` |
| 721 | """ |
| 722 | shortest = None |
| 723 | for f in srcs: |
| 724 | if f.basename == basename: |
| 725 | if not shortest or len(f.dirname) < len(shortest.dirname): |
| 726 | shortest = f |
| 727 | return shortest |