Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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 | """ |
| 16 | Rust Analyzer Bazel rules. |
| 17 | |
| 18 | rust_analyzer will generate a rust-project.json file for the |
| 19 | given targets. This file can be consumed by rust-analyzer as an alternative |
| 20 | to Cargo.toml files. |
| 21 | """ |
| 22 | |
| 23 | load("//rust/platform:triple_mappings.bzl", "system_to_dylib_ext", "triple_to_system") |
| 24 | load("//rust/private:common.bzl", "rust_common") |
| 25 | load("//rust/private:rustc.bzl", "BuildInfo") |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 26 | load( |
| 27 | "//rust/private:utils.bzl", |
| 28 | "concat", |
| 29 | "dedent", |
| 30 | "dedup_expand_location", |
| 31 | "find_toolchain", |
| 32 | ) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 33 | |
| 34 | RustAnalyzerInfo = 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 Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 47 | RustAnalyzerGroupInfo = provider( |
| 48 | doc = "RustAnalyzerGroupInfo holds multiple RustAnalyzerInfos", |
| 49 | fields = { |
| 50 | "deps": "List[RustAnalyzerInfo]: direct dependencies", |
| 51 | }, |
| 52 | ) |
| 53 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 54 | def _rust_analyzer_aspect_impl(target, ctx): |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 55 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 58 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 70 | build_info = None |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 71 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 78 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 79 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 83 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 85 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 86 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 106 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 107 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 113 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 114 | crate_spec = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_crate_spec") |
| 115 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 116 | 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 | |
| 136 | def 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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 145 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 153 | 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 | |
| 165 | rust_analyzer_aspect = aspect( |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 166 | attr_aspects = ["deps", "proc_macro_deps", "crate", "actual"], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 167 | implementation = _rust_analyzer_aspect_impl, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 168 | toolchains = [str(Label("//rust:toolchain_type"))], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 169 | incompatible_use_toolchain_transition = True, |
| 170 | doc = "Annotates rust rules with RustAnalyzerInfo later used to build a rust-project.json", |
| 171 | ) |
| 172 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 173 | _EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/" |
| 174 | _OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/" |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 175 | |
| 176 | def _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 | |
| 184 | def _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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 205 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 211 | |
| 212 | if info.build_info != None: |
| 213 | out_dir_path = info.build_info.out_dir.path |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 214 | crate["env"].update({"OUT_DIR": _EXEC_ROOT_TEMPLATE + out_dir_path}) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 215 | 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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 218 | "include_dirs": [crate_root, _EXEC_ROOT_TEMPLATE + out_dir_path], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 219 | } |
| 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 Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 223 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 226 | |
| 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 Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 239 | crate["target"] = find_toolchain(ctx).target_triple.str |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 240 | if info.proc_macro_dylib_path != None: |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 241 | crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 242 | return crate |
| 243 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 244 | def _rust_analyzer_toolchain_impl(ctx): |
| 245 | toolchain = platform_common.ToolchainInfo( |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 246 | proc_macro_srv = ctx.executable.proc_macro_srv, |
| 247 | rustc = ctx.executable.rustc, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 248 | rustc_srcs = ctx.attr.rustc_srcs, |
| 249 | ) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 250 | |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 251 | return [toolchain] |
| 252 | |
| 253 | rust_analyzer_toolchain = rule( |
| 254 | implementation = _rust_analyzer_toolchain_impl, |
| 255 | doc = "A toolchain for [rust-analyzer](https://rust-analyzer.github.io/).", |
| 256 | attrs = { |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 257 | "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 Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 270 | "rustc_srcs": attr.label( |
| 271 | doc = "The source code of rustc.", |
| 272 | mandatory = True, |
| 273 | ), |
| 274 | }, |
| 275 | incompatible_use_toolchain_transition = True, |
| 276 | ) |
| 277 | |
| 278 | def _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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 282 | fail( |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 283 | "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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 285 | ) |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 286 | |
| 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 292 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 293 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 303 | ) |
| 304 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 305 | 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 317 | |
| 318 | rust_analyzer_detect_sysroot = rule( |
| 319 | implementation = _rust_analyzer_detect_sysroot_impl, |
Brian Silverman | 5f6f276 | 2022-08-13 19:30:05 -0700 | [diff] [blame] | 320 | toolchains = [ |
| 321 | "@rules_rust//rust:toolchain_type", |
| 322 | "@rules_rust//rust/rust_analyzer:toolchain_type", |
| 323 | ], |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 324 | 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 | ) |