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