blob: e28ca30c4268142c6d658cd1ecca5b04fb1e6ea2 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001# 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:rustc.bzl", "BuildInfo")
26load("//rust/private:utils.bzl", "dedent", "find_toolchain")
27
28RustAnalyzerInfo = provider(
29 doc = "RustAnalyzerInfo holds rust crate metadata for targets",
30 fields = {
31 "build_info": "BuildInfo: build info for this crate if present",
32 "cfgs": "List[String]: features or other compilation --cfg settings",
33 "crate": "rust_common.crate_info",
34 "crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
35 "deps": "List[RustAnalyzerInfo]: direct dependencies",
36 "env": "Dict{String: String}: Environment variables, used for the `env!` macro",
37 "proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule",
38 },
39)
40
41def _rust_analyzer_aspect_impl(target, ctx):
42 if rust_common.crate_info not in target:
43 return []
44
45 toolchain = find_toolchain(ctx)
46
47 # Always add `test` & `debug_assertions`. See rust-analyzer source code:
48 # https://github.com/rust-analyzer/rust-analyzer/blob/2021-11-15/crates/project_model/src/workspace.rs#L529-L531
49 cfgs = ["test", "debug_assertions"]
50 if hasattr(ctx.rule.attr, "crate_features"):
51 cfgs += ['feature="{}"'.format(f) for f in ctx.rule.attr.crate_features]
52 if hasattr(ctx.rule.attr, "rustc_flags"):
53 cfgs += [f[6:] for f in ctx.rule.attr.rustc_flags if f.startswith("--cfg ") or f.startswith("--cfg=")]
54
55 # Save BuildInfo if we find any (for build script output)
56 build_info = None
57 for dep in ctx.rule.attr.deps:
58 if BuildInfo in dep:
59 build_info = dep[BuildInfo]
60
61 dep_infos = [dep[RustAnalyzerInfo] for dep in ctx.rule.attr.deps if RustAnalyzerInfo in dep]
62 if hasattr(ctx.rule.attr, "proc_macro_deps"):
63 dep_infos += [dep[RustAnalyzerInfo] for dep in ctx.rule.attr.proc_macro_deps if RustAnalyzerInfo in dep]
64 if hasattr(ctx.rule.attr, "crate") and ctx.rule.attr.crate != None:
65 dep_infos.append(ctx.rule.attr.crate[RustAnalyzerInfo])
66
67 crate_spec = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_crate_spec")
68
69 crate_info = target[rust_common.crate_info]
70
71 rust_analyzer_info = RustAnalyzerInfo(
72 crate = crate_info,
73 cfgs = cfgs,
74 env = getattr(ctx.rule.attr, "rustc_env", {}),
75 deps = dep_infos,
76 crate_specs = depset(direct = [crate_spec], transitive = [dep.crate_specs for dep in dep_infos]),
77 proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target),
78 build_info = build_info,
79 )
80
81 ctx.actions.write(
82 output = crate_spec,
83 content = json.encode(_create_single_crate(ctx, rust_analyzer_info)),
84 )
85
86 return [
87 rust_analyzer_info,
88 OutputGroupInfo(rust_analyzer_crate_spec = rust_analyzer_info.crate_specs),
89 ]
90
91def find_proc_macro_dylib_path(toolchain, target):
92 """Find the proc_macro_dylib_path of target. Returns None if target crate is not type proc-macro.
93
94 Args:
95 toolchain: The current rust toolchain.
96 target: The current target.
97 Returns:
98 (path): The path to the proc macro dylib, or None if this crate is not a proc-macro.
99 """
100 if target[rust_common.crate_info].type != "proc-macro":
101 return None
102
103 dylib_ext = system_to_dylib_ext(triple_to_system(toolchain.target_triple))
104 for action in target.actions:
105 for output in action.outputs.to_list():
106 if output.extension == dylib_ext[1:]:
107 return output.path
108
109 # Failed to find the dylib path inside a proc-macro crate.
110 # TODO: Should this be an error?
111 return None
112
113rust_analyzer_aspect = aspect(
114 attr_aspects = ["deps", "proc_macro_deps", "crate"],
115 implementation = _rust_analyzer_aspect_impl,
116 toolchains = [str(Label("//rust:toolchain"))],
117 incompatible_use_toolchain_transition = True,
118 doc = "Annotates rust rules with RustAnalyzerInfo later used to build a rust-project.json",
119)
120
121_exec_root_tmpl = "__EXEC_ROOT__/"
122
123def _crate_id(crate_info):
124 """Returns a unique stable identifier for a crate
125
126 Returns:
127 (string): This crate's unique stable id.
128 """
129 return "ID-" + crate_info.root.path
130
131def _create_single_crate(ctx, info):
132 """Creates a crate in the rust-project.json format.
133
134 Args:
135 ctx (ctx): The rule context
136 info (RustAnalyzerInfo): RustAnalyzerInfo for the current crate
137
138 Returns:
139 (dict) The crate rust-project.json representation
140 """
141 crate_name = info.crate.name
142 crate = dict()
143 crate_id = _crate_id(info.crate)
144 crate["crate_id"] = crate_id
145 crate["display_name"] = crate_name
146 crate["edition"] = info.crate.edition
147 crate["env"] = {}
148 crate["crate_type"] = info.crate.type
149
150 # Switch on external/ to determine if crates are in the workspace or remote.
151 # TODO: Some folks may want to override this for vendored dependencies.
152 root_path = info.crate.root.path
153 root_dirname = info.crate.root.dirname
154 if root_path.startswith("external/"):
155 crate["is_workspace_member"] = False
156 crate["root_module"] = _exec_root_tmpl + root_path
157 crate_root = _exec_root_tmpl + root_dirname
158 else:
159 crate["is_workspace_member"] = True
160 crate["root_module"] = root_path
161 crate_root = root_dirname
162
163 if info.build_info != None:
164 out_dir_path = info.build_info.out_dir.path
165 crate["env"].update({"OUT_DIR": _exec_root_tmpl + out_dir_path})
166 crate["source"] = {
167 # We have to tell rust-analyzer about our out_dir since it's not under the crate root.
168 "exclude_dirs": [],
169 "include_dirs": [crate_root, _exec_root_tmpl + out_dir_path],
170 }
171
172 # TODO: The only imagined use case is an env var holding a filename in the workspace passed to a
173 # macro like include_bytes!. Other use cases might exist that require more complex logic.
174 expand_targets = getattr(ctx.rule.attr, "data", []) + getattr(ctx.rule.attr, "compile_data", [])
175 crate["env"].update({k: ctx.expand_location(v, expand_targets) for k, v in info.env.items()})
176
177 # Omit when a crate appears to depend on itself (e.g. foo_test crates).
178 # It can happen a single source file is present in multiple crates - there can
179 # be a `rust_library` with a `lib.rs` file, and a `rust_test` for the `test`
180 # module in that file. Tests can declare more dependencies than what library
181 # had. Therefore we had to collect all RustAnalyzerInfos for a given crate
182 # and take deps from all of them.
183
184 # There's one exception - if the dependency is the same crate name as the
185 # the crate being processed, we don't add it as a dependency to itself. This is
186 # common and expected - `rust_test.crate` pointing to the `rust_library`.
187 crate["deps"] = [_crate_id(dep.crate) for dep in info.deps if _crate_id(dep.crate) != crate_id]
188 crate["cfg"] = info.cfgs
189 crate["target"] = find_toolchain(ctx).target_triple
190 if info.proc_macro_dylib_path != None:
191 crate["proc_macro_dylib_path"] = _exec_root_tmpl + info.proc_macro_dylib_path
192 return crate
193
194def _rust_analyzer_detect_sysroot_impl(ctx):
195 rust_toolchain = find_toolchain(ctx)
196
197 if not rust_toolchain.rustc_srcs:
198 fail(
199 "Current Rust toolchain doesn't contain rustc sources in `rustc_srcs` attribute.",
200 "These are needed by rust analyzer.",
201 "If you are using the default Rust toolchain, add `rust_repositories(include_rustc_srcs = True, ...).` to your WORKSPACE file.",
202 )
203 sysroot_src = rust_toolchain.rustc_srcs.label.package + "/library"
204 if rust_toolchain.rustc_srcs.label.workspace_root:
205 sysroot_src = _exec_root_tmpl + rust_toolchain.rustc_srcs.label.workspace_root + "/" + sysroot_src
206
207 sysroot_src_file = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_sysroot_src")
208 ctx.actions.write(
209 output = sysroot_src_file,
210 content = sysroot_src,
211 )
212
213 return [DefaultInfo(files = depset([sysroot_src_file]))]
214
215rust_analyzer_detect_sysroot = rule(
216 implementation = _rust_analyzer_detect_sysroot_impl,
217 toolchains = ["@rules_rust//rust:toolchain"],
218 incompatible_use_toolchain_transition = True,
219 doc = dedent("""\
220 Detect the sysroot and store in a file for use by the gen_rust_project tool.
221 """),
222)
223
224def _rust_analyzer_impl(_ctx):
225 pass
226
227rust_analyzer = rule(
228 attrs = {
229 "targets": attr.label_list(
230 aspects = [rust_analyzer_aspect],
231 doc = "List of all targets to be included in the index",
232 ),
233 },
234 implementation = _rust_analyzer_impl,
235 toolchains = [str(Label("//rust:toolchain"))],
236 incompatible_use_toolchain_transition = True,
237 doc = dedent("""\
238 Deprecated: gen_rust_project can now create a rust-project.json without a rust_analyzer rule.
239 """),
240)