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