blob: 3eea6c08419a184ad333ce8b92bf404ddd15591f [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")
Adam Snaider1c095c92023-07-08 02:09:58 -040026load(
27 "//rust/private:utils.bzl",
28 "concat",
29 "dedent",
30 "dedup_expand_location",
31 "find_toolchain",
32)
Brian Silvermancc09f182022-03-09 15:40:20 -080033
34RustAnalyzerInfo = provider(
35 doc = "RustAnalyzerInfo holds rust crate metadata for targets",
36 fields = {
37 "build_info": "BuildInfo: build info for this crate if present",
38 "cfgs": "List[String]: features or other compilation --cfg settings",
39 "crate": "rust_common.crate_info",
40 "crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
41 "deps": "List[RustAnalyzerInfo]: direct dependencies",
42 "env": "Dict{String: String}: Environment variables, used for the `env!` macro",
43 "proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule",
44 },
45)
46
Adam Snaider1c095c92023-07-08 02:09:58 -040047RustAnalyzerGroupInfo = provider(
48 doc = "RustAnalyzerGroupInfo holds multiple RustAnalyzerInfos",
49 fields = {
50 "deps": "List[RustAnalyzerInfo]: direct dependencies",
51 },
52)
53
Brian Silvermancc09f182022-03-09 15:40:20 -080054def _rust_analyzer_aspect_impl(target, ctx):
Adam Snaider1c095c92023-07-08 02:09:58 -040055 if (rust_common.crate_info not in target and
56 rust_common.test_crate_info not in target and
57 rust_common.crate_group_info not in target):
Brian Silvermancc09f182022-03-09 15:40:20 -080058 return []
59
60 toolchain = find_toolchain(ctx)
61
62 # Always add `test` & `debug_assertions`. See rust-analyzer source code:
63 # https://github.com/rust-analyzer/rust-analyzer/blob/2021-11-15/crates/project_model/src/workspace.rs#L529-L531
64 cfgs = ["test", "debug_assertions"]
65 if hasattr(ctx.rule.attr, "crate_features"):
66 cfgs += ['feature="{}"'.format(f) for f in ctx.rule.attr.crate_features]
67 if hasattr(ctx.rule.attr, "rustc_flags"):
68 cfgs += [f[6:] for f in ctx.rule.attr.rustc_flags if f.startswith("--cfg ") or f.startswith("--cfg=")]
69
Brian Silvermancc09f182022-03-09 15:40:20 -080070 build_info = None
Brian Silverman5f6f2762022-08-13 19:30:05 -070071 dep_infos = []
72 if hasattr(ctx.rule.attr, "deps"):
73 for dep in ctx.rule.attr.deps:
74 # Save BuildInfo if we find any (for build script output)
75 if BuildInfo in dep:
76 build_info = dep[BuildInfo]
77 dep_infos = [dep[RustAnalyzerInfo] for dep in ctx.rule.attr.deps if RustAnalyzerInfo in dep]
Brian Silvermancc09f182022-03-09 15:40:20 -080078
Adam Snaider1c095c92023-07-08 02:09:58 -040079 group_infos = [dep[RustAnalyzerGroupInfo] for dep in ctx.rule.attr.deps if RustAnalyzerGroupInfo in dep]
80 for group_info in group_infos:
81 dep_infos.extend(group_info.deps)
82
Brian Silvermancc09f182022-03-09 15:40:20 -080083 if hasattr(ctx.rule.attr, "proc_macro_deps"):
84 dep_infos += [dep[RustAnalyzerInfo] for dep in ctx.rule.attr.proc_macro_deps if RustAnalyzerInfo in dep]
Brian Silvermancc09f182022-03-09 15:40:20 -080085
Adam Snaider1c095c92023-07-08 02:09:58 -040086 group_infos = [dep[RustAnalyzerGroupInfo] for dep in ctx.rule.attr.proc_macro_deps if RustAnalyzerGroupInfo in dep]
87 for group_info in group_infos:
88 dep_infos.extend(group_info.deps)
89
90 if hasattr(ctx.rule.attr, "crate") and ctx.rule.attr.crate != None:
91 if RustAnalyzerInfo in ctx.rule.attr.crate:
92 dep_infos.append(ctx.rule.attr.crate[RustAnalyzerInfo])
93
94 if RustAnalyzerGroupInfo in ctx.rule.attr.crate:
95 dep_infos.extend(ctx.rule.attr.crate[RustAnalyzerGroupInfo])
96
97 if hasattr(ctx.rule.attr, "actual") and ctx.rule.attr.actual != None:
98 if RustAnalyzerInfo in ctx.rule.attr.actual:
99 dep_infos.append(ctx.rule.attr.actual[RustAnalyzerInfo])
100
101 if RustAnalyzerGroupInfo in ctx.rule.attr.actual:
102 dep_infos.extend(ctx.rule.attr.actul[RustAnalyzerGroupInfo])
103
104 if rust_common.crate_group_info in target:
105 return [RustAnalyzerGroupInfo(deps = dep_infos)]
Brian Silvermancc09f182022-03-09 15:40:20 -0800106
Brian Silverman5f6f2762022-08-13 19:30:05 -0700107 if rust_common.crate_info in target:
108 crate_info = target[rust_common.crate_info]
109 elif rust_common.test_crate_info in target:
110 crate_info = target[rust_common.test_crate_info].crate
111 else:
112 fail("Unexpected target type: {}".format(target))
Brian Silvermancc09f182022-03-09 15:40:20 -0800113
Adam Snaider1c095c92023-07-08 02:09:58 -0400114 crate_spec = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_crate_spec")
115
Brian Silvermancc09f182022-03-09 15:40:20 -0800116 rust_analyzer_info = RustAnalyzerInfo(
117 crate = crate_info,
118 cfgs = cfgs,
119 env = getattr(ctx.rule.attr, "rustc_env", {}),
120 deps = dep_infos,
121 crate_specs = depset(direct = [crate_spec], transitive = [dep.crate_specs for dep in dep_infos]),
122 proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target),
123 build_info = build_info,
124 )
125
126 ctx.actions.write(
127 output = crate_spec,
128 content = json.encode(_create_single_crate(ctx, rust_analyzer_info)),
129 )
130
131 return [
132 rust_analyzer_info,
133 OutputGroupInfo(rust_analyzer_crate_spec = rust_analyzer_info.crate_specs),
134 ]
135
136def find_proc_macro_dylib_path(toolchain, target):
137 """Find the proc_macro_dylib_path of target. Returns None if target crate is not type proc-macro.
138
139 Args:
140 toolchain: The current rust toolchain.
141 target: The current target.
142 Returns:
143 (path): The path to the proc macro dylib, or None if this crate is not a proc-macro.
144 """
Brian Silverman5f6f2762022-08-13 19:30:05 -0700145 if rust_common.crate_info in target:
146 crate_info = target[rust_common.crate_info]
147 elif rust_common.test_crate_info in target:
148 crate_info = target[rust_common.test_crate_info].crate
149 else:
150 return None
151
152 if crate_info.type != "proc-macro":
Brian Silvermancc09f182022-03-09 15:40:20 -0800153 return None
154
155 dylib_ext = system_to_dylib_ext(triple_to_system(toolchain.target_triple))
156 for action in target.actions:
157 for output in action.outputs.to_list():
158 if output.extension == dylib_ext[1:]:
159 return output.path
160
161 # Failed to find the dylib path inside a proc-macro crate.
162 # TODO: Should this be an error?
163 return None
164
165rust_analyzer_aspect = aspect(
Brian Silverman5f6f2762022-08-13 19:30:05 -0700166 attr_aspects = ["deps", "proc_macro_deps", "crate", "actual"],
Brian Silvermancc09f182022-03-09 15:40:20 -0800167 implementation = _rust_analyzer_aspect_impl,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700168 toolchains = [str(Label("//rust:toolchain_type"))],
Brian Silvermancc09f182022-03-09 15:40:20 -0800169 incompatible_use_toolchain_transition = True,
170 doc = "Annotates rust rules with RustAnalyzerInfo later used to build a rust-project.json",
171)
172
Brian Silverman5f6f2762022-08-13 19:30:05 -0700173_EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
174_OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"
Brian Silvermancc09f182022-03-09 15:40:20 -0800175
176def _crate_id(crate_info):
177 """Returns a unique stable identifier for a crate
178
179 Returns:
180 (string): This crate's unique stable id.
181 """
182 return "ID-" + crate_info.root.path
183
184def _create_single_crate(ctx, info):
185 """Creates a crate in the rust-project.json format.
186
187 Args:
188 ctx (ctx): The rule context
189 info (RustAnalyzerInfo): RustAnalyzerInfo for the current crate
190
191 Returns:
192 (dict) The crate rust-project.json representation
193 """
194 crate_name = info.crate.name
195 crate = dict()
196 crate_id = _crate_id(info.crate)
197 crate["crate_id"] = crate_id
198 crate["display_name"] = crate_name
199 crate["edition"] = info.crate.edition
200 crate["env"] = {}
201 crate["crate_type"] = info.crate.type
202
203 # Switch on external/ to determine if crates are in the workspace or remote.
204 # TODO: Some folks may want to override this for vendored dependencies.
Brian Silverman5f6f2762022-08-13 19:30:05 -0700205 is_external = info.crate.root.path.startswith("external/")
206 is_generated = not info.crate.root.is_source
207 path_prefix = _EXEC_ROOT_TEMPLATE if is_external or is_generated else ""
208 crate["is_workspace_member"] = not is_external
209 crate["root_module"] = path_prefix + info.crate.root.path
210 crate_root = path_prefix + info.crate.root.dirname
Brian Silvermancc09f182022-03-09 15:40:20 -0800211
212 if info.build_info != None:
213 out_dir_path = info.build_info.out_dir.path
Brian Silverman5f6f2762022-08-13 19:30:05 -0700214 crate["env"].update({"OUT_DIR": _EXEC_ROOT_TEMPLATE + out_dir_path})
Brian Silvermancc09f182022-03-09 15:40:20 -0800215 crate["source"] = {
216 # We have to tell rust-analyzer about our out_dir since it's not under the crate root.
217 "exclude_dirs": [],
Brian Silverman5f6f2762022-08-13 19:30:05 -0700218 "include_dirs": [crate_root, _EXEC_ROOT_TEMPLATE + out_dir_path],
Brian Silvermancc09f182022-03-09 15:40:20 -0800219 }
220
221 # TODO: The only imagined use case is an env var holding a filename in the workspace passed to a
222 # macro like include_bytes!. Other use cases might exist that require more complex logic.
Adam Snaider1c095c92023-07-08 02:09:58 -0400223 expand_targets = concat([getattr(ctx.rule.attr, attr, []) for attr in ["data", "compile_data"]])
224
225 crate["env"].update({k: dedup_expand_location(ctx, v, expand_targets) for k, v in info.env.items()})
Brian Silvermancc09f182022-03-09 15:40:20 -0800226
227 # Omit when a crate appears to depend on itself (e.g. foo_test crates).
228 # It can happen a single source file is present in multiple crates - there can
229 # be a `rust_library` with a `lib.rs` file, and a `rust_test` for the `test`
230 # module in that file. Tests can declare more dependencies than what library
231 # had. Therefore we had to collect all RustAnalyzerInfos for a given crate
232 # and take deps from all of them.
233
234 # There's one exception - if the dependency is the same crate name as the
235 # the crate being processed, we don't add it as a dependency to itself. This is
236 # common and expected - `rust_test.crate` pointing to the `rust_library`.
237 crate["deps"] = [_crate_id(dep.crate) for dep in info.deps if _crate_id(dep.crate) != crate_id]
238 crate["cfg"] = info.cfgs
Adam Snaider1c095c92023-07-08 02:09:58 -0400239 crate["target"] = find_toolchain(ctx).target_triple.str
Brian Silvermancc09f182022-03-09 15:40:20 -0800240 if info.proc_macro_dylib_path != None:
Brian Silverman5f6f2762022-08-13 19:30:05 -0700241 crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path
Brian Silvermancc09f182022-03-09 15:40:20 -0800242 return crate
243
Brian Silverman5f6f2762022-08-13 19:30:05 -0700244def _rust_analyzer_toolchain_impl(ctx):
245 toolchain = platform_common.ToolchainInfo(
Adam Snaider1c095c92023-07-08 02:09:58 -0400246 proc_macro_srv = ctx.executable.proc_macro_srv,
247 rustc = ctx.executable.rustc,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700248 rustc_srcs = ctx.attr.rustc_srcs,
249 )
Brian Silvermancc09f182022-03-09 15:40:20 -0800250
Brian Silverman5f6f2762022-08-13 19:30:05 -0700251 return [toolchain]
252
253rust_analyzer_toolchain = rule(
254 implementation = _rust_analyzer_toolchain_impl,
255 doc = "A toolchain for [rust-analyzer](https://rust-analyzer.github.io/).",
256 attrs = {
Adam Snaider1c095c92023-07-08 02:09:58 -0400257 "proc_macro_srv": attr.label(
258 doc = "The path to a `rust_analyzer_proc_macro_srv` binary.",
259 cfg = "exec",
260 executable = True,
261 allow_single_file = True,
262 ),
263 "rustc": attr.label(
264 doc = "The path to a `rustc` binary.",
265 cfg = "exec",
266 executable = True,
267 allow_single_file = True,
268 mandatory = True,
269 ),
Brian Silverman5f6f2762022-08-13 19:30:05 -0700270 "rustc_srcs": attr.label(
271 doc = "The source code of rustc.",
272 mandatory = True,
273 ),
274 },
275 incompatible_use_toolchain_transition = True,
276)
277
278def _rust_analyzer_detect_sysroot_impl(ctx):
279 rust_analyzer_toolchain = ctx.toolchains[Label("@rules_rust//rust/rust_analyzer:toolchain_type")]
280
281 if not rust_analyzer_toolchain.rustc_srcs:
Brian Silvermancc09f182022-03-09 15:40:20 -0800282 fail(
Brian Silverman5f6f2762022-08-13 19:30:05 -0700283 "Current Rust-Analyzer toolchain doesn't contain rustc sources in `rustc_srcs` attribute.",
284 "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.",
Brian Silvermancc09f182022-03-09 15:40:20 -0800285 )
Brian Silverman5f6f2762022-08-13 19:30:05 -0700286
287 rustc_srcs = rust_analyzer_toolchain.rustc_srcs
288
289 sysroot_src = rustc_srcs.label.package + "/library"
290 if rustc_srcs.label.workspace_root:
291 sysroot_src = _OUTPUT_BASE_TEMPLATE + rustc_srcs.label.workspace_root + "/" + sysroot_src
Brian Silvermancc09f182022-03-09 15:40:20 -0800292
Adam Snaider1c095c92023-07-08 02:09:58 -0400293 rustc = rust_analyzer_toolchain.rustc
294 sysroot_dir, _, bin_dir = rustc.dirname.rpartition("/")
295 if bin_dir != "bin":
296 fail("The rustc path is expected to be relative to the sysroot as `bin/rustc`. Instead got: {}".format(
297 rustc.path,
298 ))
299
300 sysroot = "{}/{}".format(
301 _OUTPUT_BASE_TEMPLATE,
302 sysroot_dir,
Brian Silvermancc09f182022-03-09 15:40:20 -0800303 )
304
Adam Snaider1c095c92023-07-08 02:09:58 -0400305 toolchain_info = {
306 "sysroot": sysroot,
307 "sysroot_src": sysroot_src,
308 }
309
310 output = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_toolchain.json")
311 ctx.actions.write(
312 output = output,
313 content = json.encode_indent(toolchain_info, indent = " " * 4),
314 )
315
316 return [DefaultInfo(files = depset([output]))]
Brian Silvermancc09f182022-03-09 15:40:20 -0800317
318rust_analyzer_detect_sysroot = rule(
319 implementation = _rust_analyzer_detect_sysroot_impl,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700320 toolchains = [
321 "@rules_rust//rust:toolchain_type",
322 "@rules_rust//rust/rust_analyzer:toolchain_type",
323 ],
Brian Silvermancc09f182022-03-09 15:40:20 -0800324 incompatible_use_toolchain_transition = True,
325 doc = dedent("""\
326 Detect the sysroot and store in a file for use by the gen_rust_project tool.
327 """),
328)