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