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