xref: /aosp_15_r20/external/bazelbuild-rules_rust/rust/private/rust_analyzer.bzl (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1# Copyright 2020 Google
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"""
16Rust Analyzer Bazel rules.
17
18rust_analyzer will generate a rust-project.json file for the
19given targets. This file can be consumed by rust-analyzer as an alternative
20to Cargo.toml files.
21"""
22
23load("//rust/platform:triple_mappings.bzl", "system_to_dylib_ext", "triple_to_system")
24load("//rust/private:common.bzl", "rust_common")
25load("//rust/private:providers.bzl", "RustAnalyzerGroupInfo", "RustAnalyzerInfo")
26load("//rust/private:rustc.bzl", "BuildInfo")
27load(
28    "//rust/private:utils.bzl",
29    "concat",
30    "dedent",
31    "dedup_expand_location",
32    "find_toolchain",
33)
34
35def write_rust_analyzer_spec_file(ctx, attrs, owner, base_info):
36    """Write a rust-analyzer spec info file.
37
38    Args:
39        ctx (ctx): The current rule's context object.
40        attrs (dict): A mapping of attributes.
41        owner (Label): The label of the owner of the spec info.
42        base_info (RustAnalyzerInfo): The data the resulting RustAnalyzerInfo is based on.
43
44    Returns:
45        RustAnalyzerInfo: Info with the embedded spec file.
46    """
47    crate_spec = ctx.actions.declare_file("{}.rust_analyzer_crate_spec.json".format(owner.name))
48
49    # Recreate the provider with the spec file embedded in it.
50    rust_analyzer_info = RustAnalyzerInfo(
51        aliases = base_info.aliases,
52        crate = base_info.crate,
53        cfgs = base_info.cfgs,
54        env = base_info.env,
55        deps = base_info.deps,
56        crate_specs = depset(direct = [crate_spec], transitive = [base_info.crate_specs]),
57        proc_macro_dylib_path = base_info.proc_macro_dylib_path,
58        build_info = base_info.build_info,
59    )
60
61    ctx.actions.write(
62        output = crate_spec,
63        content = json.encode_indent(
64            _create_single_crate(
65                ctx,
66                attrs,
67                rust_analyzer_info,
68            ),
69            indent = " " * 4,
70        ),
71    )
72
73    return rust_analyzer_info
74
75def _accumulate_rust_analyzer_info(dep_infos_to_accumulate, label_index_to_accumulate, dep):
76    if dep == None:
77        return
78    if RustAnalyzerInfo in dep:
79        label_index_to_accumulate[dep.label] = dep[RustAnalyzerInfo]
80        dep_infos_to_accumulate.append(dep[RustAnalyzerInfo])
81    if RustAnalyzerGroupInfo in dep:
82        for expanded_dep in dep[RustAnalyzerGroupInfo].deps:
83            label_index_to_accumulate[expanded_dep.crate.owner] = expanded_dep
84            dep_infos_to_accumulate.append(expanded_dep)
85
86def _accumulate_rust_analyzer_infos(dep_infos_to_accumulate, label_index_to_accumulate, deps_attr):
87    for dep in deps_attr:
88        _accumulate_rust_analyzer_info(dep_infos_to_accumulate, label_index_to_accumulate, dep)
89
90def _rust_analyzer_aspect_impl(target, ctx):
91    if (rust_common.crate_info not in target and
92        rust_common.test_crate_info not in target and
93        rust_common.crate_group_info not in target):
94        return []
95
96    if RustAnalyzerInfo in target or RustAnalyzerGroupInfo in target:
97        return []
98
99    toolchain = find_toolchain(ctx)
100
101    # Always add `test` & `debug_assertions`. See rust-analyzer source code:
102    # https://github.com/rust-analyzer/rust-analyzer/blob/2021-11-15/crates/project_model/src/workspace.rs#L529-L531
103    cfgs = ["test", "debug_assertions"]
104    if hasattr(ctx.rule.attr, "crate_features"):
105        cfgs += ['feature="{}"'.format(f) for f in ctx.rule.attr.crate_features]
106    if hasattr(ctx.rule.attr, "rustc_flags"):
107        cfgs += [f[6:] for f in ctx.rule.attr.rustc_flags if f.startswith("--cfg ") or f.startswith("--cfg=")]
108
109    build_info = None
110    dep_infos = []
111    labels_to_rais = {}
112
113    for dep in getattr(ctx.rule.attr, "deps", []):
114        # Save BuildInfo if we find any (for build script output)
115        if BuildInfo in dep:
116            build_info = dep[BuildInfo]
117
118    _accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "deps", []))
119    _accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "proc_macro_deps", []))
120
121    _accumulate_rust_analyzer_info(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "crate", None))
122    _accumulate_rust_analyzer_info(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "actual", None))
123
124    if rust_common.crate_group_info in target:
125        return [RustAnalyzerGroupInfo(deps = dep_infos)]
126    elif rust_common.crate_info in target:
127        crate_info = target[rust_common.crate_info]
128    elif rust_common.test_crate_info in target:
129        crate_info = target[rust_common.test_crate_info].crate
130    else:
131        fail("Unexpected target type: {}".format(target))
132
133    aliases = {}
134    for aliased_target, aliased_name in getattr(ctx.rule.attr, "aliases", {}).items():
135        if aliased_target.label in labels_to_rais:
136            aliases[labels_to_rais[aliased_target.label]] = aliased_name
137
138    rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
139        aliases = aliases,
140        crate = crate_info,
141        cfgs = cfgs,
142        env = crate_info.rustc_env,
143        deps = dep_infos,
144        crate_specs = depset(transitive = [dep.crate_specs for dep in dep_infos]),
145        proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target),
146        build_info = build_info,
147    ))
148
149    return [
150        rust_analyzer_info,
151        OutputGroupInfo(rust_analyzer_crate_spec = rust_analyzer_info.crate_specs),
152    ]
153
154def find_proc_macro_dylib_path(toolchain, target):
155    """Find the proc_macro_dylib_path of target. Returns None if target crate is not type proc-macro.
156
157    Args:
158        toolchain: The current rust toolchain.
159        target: The current target.
160    Returns:
161        (path): The path to the proc macro dylib, or None if this crate is not a proc-macro.
162    """
163    if rust_common.crate_info in target:
164        crate_info = target[rust_common.crate_info]
165    elif rust_common.test_crate_info in target:
166        crate_info = target[rust_common.test_crate_info].crate
167    else:
168        return None
169
170    if crate_info.type != "proc-macro":
171        return None
172
173    dylib_ext = system_to_dylib_ext(triple_to_system(toolchain.target_triple))
174    for action in target.actions:
175        for output in action.outputs.to_list():
176            if output.extension == dylib_ext[1:]:
177                return output.path
178
179    # Failed to find the dylib path inside a proc-macro crate.
180    # TODO: Should this be an error?
181    return None
182
183rust_analyzer_aspect = aspect(
184    attr_aspects = ["deps", "proc_macro_deps", "crate", "actual", "proto"],
185    implementation = _rust_analyzer_aspect_impl,
186    toolchains = [str(Label("//rust:toolchain_type"))],
187    doc = "Annotates rust rules with RustAnalyzerInfo later used to build a rust-project.json",
188)
189
190_EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
191_OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"
192
193def _crate_id(crate_info):
194    """Returns a unique stable identifier for a crate
195
196    Returns:
197        (string): This crate's unique stable id.
198    """
199    return "ID-" + crate_info.root.path
200
201def _create_single_crate(ctx, attrs, info):
202    """Creates a crate in the rust-project.json format.
203
204    Args:
205        ctx (ctx): The rule context.
206        attrs (dict): A mapping of attributes.
207        info (RustAnalyzerInfo): RustAnalyzerInfo for the current crate.
208
209    Returns:
210        (dict) The crate rust-project.json representation
211    """
212    crate_name = info.crate.name
213    crate = dict()
214    crate_id = _crate_id(info.crate)
215    crate["crate_id"] = crate_id
216    crate["display_name"] = crate_name
217    crate["edition"] = info.crate.edition
218    crate["env"] = {}
219    crate["crate_type"] = info.crate.type
220
221    # Switch on external/ to determine if crates are in the workspace or remote.
222    # TODO: Some folks may want to override this for vendored dependencies.
223    is_external = info.crate.root.path.startswith("external/")
224    is_generated = not info.crate.root.is_source
225    path_prefix = _EXEC_ROOT_TEMPLATE if is_external or is_generated else ""
226    crate["is_workspace_member"] = not is_external
227    crate["root_module"] = path_prefix + info.crate.root.path
228    crate["source"] = {"exclude_dirs": [], "include_dirs": []}
229
230    if is_generated:
231        srcs = getattr(ctx.rule.files, "srcs", [])
232        src_map = {src.short_path: src for src in srcs if src.is_source}
233        if info.crate.root.short_path in src_map:
234            crate["root_module"] = src_map[info.crate.root.short_path].path
235            crate["source"]["include_dirs"].append(path_prefix + info.crate.root.dirname)
236
237    if info.build_info != None and info.build_info.out_dir != None:
238        out_dir_path = info.build_info.out_dir.path
239        crate["env"].update({"OUT_DIR": _EXEC_ROOT_TEMPLATE + out_dir_path})
240
241        # We have to tell rust-analyzer about our out_dir since it's not under the crate root.
242        crate["source"]["include_dirs"].extend([
243            path_prefix + info.crate.root.dirname,
244            _EXEC_ROOT_TEMPLATE + out_dir_path,
245        ])
246
247    # TODO: The only imagined use case is an env var holding a filename in the workspace passed to a
248    # macro like include_bytes!. Other use cases might exist that require more complex logic.
249    expand_targets = concat([getattr(attrs, attr, []) for attr in ["data", "compile_data"]])
250
251    crate["env"].update({k: dedup_expand_location(ctx, v, expand_targets) for k, v in info.env.items()})
252
253    # Omit when a crate appears to depend on itself (e.g. foo_test crates).
254    # It can happen a single source file is present in multiple crates - there can
255    # be a `rust_library` with a `lib.rs` file, and a `rust_test` for the `test`
256    # module in that file. Tests can declare more dependencies than what library
257    # had. Therefore we had to collect all RustAnalyzerInfos for a given crate
258    # and take deps from all of them.
259
260    # There's one exception - if the dependency is the same crate name as the
261    # the crate being processed, we don't add it as a dependency to itself. This is
262    # common and expected - `rust_test.crate` pointing to the `rust_library`.
263    crate["deps"] = [_crate_id(dep.crate) for dep in info.deps if _crate_id(dep.crate) != crate_id]
264    crate["aliases"] = {_crate_id(alias_target.crate): alias_name for alias_target, alias_name in info.aliases.items()}
265    crate["cfg"] = info.cfgs
266    crate["target"] = find_toolchain(ctx).target_triple.str
267    if info.proc_macro_dylib_path != None:
268        crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path
269    return crate
270
271def _rust_analyzer_toolchain_impl(ctx):
272    toolchain = platform_common.ToolchainInfo(
273        proc_macro_srv = ctx.executable.proc_macro_srv,
274        rustc = ctx.executable.rustc,
275        rustc_srcs = ctx.attr.rustc_srcs,
276    )
277
278    return [toolchain]
279
280rust_analyzer_toolchain = rule(
281    implementation = _rust_analyzer_toolchain_impl,
282    doc = "A toolchain for [rust-analyzer](https://rust-analyzer.github.io/).",
283    attrs = {
284        "proc_macro_srv": attr.label(
285            doc = "The path to a `rust_analyzer_proc_macro_srv` binary.",
286            cfg = "exec",
287            executable = True,
288            allow_single_file = True,
289        ),
290        "rustc": attr.label(
291            doc = "The path to a `rustc` binary.",
292            cfg = "exec",
293            executable = True,
294            allow_single_file = True,
295            mandatory = True,
296        ),
297        "rustc_srcs": attr.label(
298            doc = "The source code of rustc.",
299            mandatory = True,
300        ),
301    },
302)
303
304def _rust_analyzer_detect_sysroot_impl(ctx):
305    rust_analyzer_toolchain = ctx.toolchains[Label("@rules_rust//rust/rust_analyzer:toolchain_type")]
306
307    if not rust_analyzer_toolchain.rustc_srcs:
308        fail(
309            "Current Rust-Analyzer toolchain doesn't contain rustc sources in `rustc_srcs` attribute.",
310            "These are needed by rust-analyzer. If you are using the default Rust toolchain, add `rust_repositories(include_rustc_srcs = True, ...).` to your WORKSPACE file.",
311        )
312
313    rustc_srcs = rust_analyzer_toolchain.rustc_srcs
314
315    sysroot_src = rustc_srcs.label.package + "/library"
316    if rustc_srcs.label.workspace_root:
317        sysroot_src = _OUTPUT_BASE_TEMPLATE + rustc_srcs.label.workspace_root + "/" + sysroot_src
318
319    rustc = rust_analyzer_toolchain.rustc
320    sysroot_dir, _, bin_dir = rustc.dirname.rpartition("/")
321    if bin_dir != "bin":
322        fail("The rustc path is expected to be relative to the sysroot as `bin/rustc`. Instead got: {}".format(
323            rustc.path,
324        ))
325
326    sysroot = "{}/{}".format(
327        _OUTPUT_BASE_TEMPLATE,
328        sysroot_dir,
329    )
330
331    toolchain_info = {
332        "sysroot": sysroot,
333        "sysroot_src": sysroot_src,
334    }
335
336    output = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_toolchain.json")
337    ctx.actions.write(
338        output = output,
339        content = json.encode_indent(toolchain_info, indent = " " * 4),
340    )
341
342    return [DefaultInfo(files = depset([output]))]
343
344rust_analyzer_detect_sysroot = rule(
345    implementation = _rust_analyzer_detect_sysroot_impl,
346    toolchains = [
347        "@rules_rust//rust:toolchain_type",
348        "@rules_rust//rust/rust_analyzer:toolchain_type",
349    ],
350    doc = dedent("""\
351        Detect the sysroot and store in a file for use by the gen_rust_project tool.
352    """),
353)
354