1 //! See [`CargoWorkspace`].
3 use std
::path
::PathBuf
;
4 use std
::str::from_utf8
;
5 use std
::{ops, process::Command}
;
9 use cargo_metadata
::{CargoOpt, MetadataCommand}
;
10 use la_arena
::{Arena, Idx}
;
11 use paths
::{AbsPath, AbsPathBuf}
;
12 use rustc_hash
::{FxHashMap, FxHashSet}
;
13 use serde
::Deserialize
;
14 use serde_json
::from_value
;
16 use crate::{utf8_stdout, InvocationLocation, ManifestPath}
;
17 use crate::{CfgOverrides, InvocationStrategy}
;
19 /// [`CargoWorkspace`] represents the logical structure of, well, a Cargo
20 /// workspace. It pretty closely mirrors `cargo metadata` output.
22 /// Note that internally, rust-analyzer uses a different structure:
23 /// `CrateGraph`. `CrateGraph` is lower-level: it knows only about the crates,
24 /// while this knows about `Packages` & `Targets`: purely cargo-related
27 /// We use absolute paths here, `cargo metadata` guarantees to always produce
29 #[derive(Debug, Clone, Eq, PartialEq)]
30 pub struct CargoWorkspace
{
31 packages
: Arena
<PackageData
>,
32 targets
: Arena
<TargetData
>,
33 workspace_root
: AbsPathBuf
,
34 target_directory
: AbsPathBuf
,
37 impl ops
::Index
<Package
> for CargoWorkspace
{
38 type Output
= PackageData
;
39 fn index(&self, index
: Package
) -> &PackageData
{
44 impl ops
::Index
<Target
> for CargoWorkspace
{
45 type Output
= TargetData
;
46 fn index(&self, index
: Target
) -> &TargetData
{
51 /// Describes how to set the rustc source directory.
52 #[derive(Clone, Debug, PartialEq, Eq)]
53 pub enum RustLibSource
{
54 /// Explicit path for the rustc source directory.
56 /// Try to automatically detect where the rustc source directory is.
60 #[derive(Clone, Debug, PartialEq, Eq)]
61 pub enum CargoFeatures
{
64 /// List of features to activate.
65 features
: Vec
<String
>,
66 /// Do not activate the `default` feature.
67 no_default_features
: bool
,
71 impl Default
for CargoFeatures
{
72 fn default() -> Self {
73 CargoFeatures
::Selected { features: vec![], no_default_features: false }
77 #[derive(Default, Clone, Debug, PartialEq, Eq)]
78 pub struct CargoConfig
{
79 /// List of features to activate.
80 pub features
: CargoFeatures
,
82 pub target
: Option
<String
>,
83 /// Sysroot loading behavior
84 pub sysroot
: Option
<RustLibSource
>,
85 pub sysroot_src
: Option
<AbsPathBuf
>,
86 /// rustc private crate source
87 pub rustc_source
: Option
<RustLibSource
>,
88 pub cfg_overrides
: CfgOverrides
,
89 /// Invoke `cargo check` through the RUSTC_WRAPPER.
90 pub wrap_rustc_in_build_scripts
: bool
,
91 /// The command to run instead of `cargo check` for building build scripts.
92 pub run_build_script_command
: Option
<Vec
<String
>>,
93 /// Extra args to pass to the cargo command.
94 pub extra_args
: Vec
<String
>,
95 /// Extra env vars to set when invoking the cargo command
96 pub extra_env
: FxHashMap
<String
, String
>,
97 pub invocation_strategy
: InvocationStrategy
,
98 pub invocation_location
: InvocationLocation
,
99 /// Optional path to use instead of `target` when building
100 pub target_dir
: Option
<PathBuf
>,
103 pub type Package
= Idx
<PackageData
>;
105 pub type Target
= Idx
<TargetData
>;
107 /// Information associated with a cargo crate
108 #[derive(Debug, Clone, Eq, PartialEq)]
109 pub struct PackageData
{
110 /// Version given in the `Cargo.toml`
111 pub version
: semver
::Version
,
112 /// Name as given in the `Cargo.toml`
114 /// Repository as given in the `Cargo.toml`
115 pub repository
: Option
<String
>,
116 /// Path containing the `Cargo.toml`
117 pub manifest
: ManifestPath
,
118 /// Targets provided by the crate (lib, bin, example, test, ...)
119 pub targets
: Vec
<Target
>,
120 /// Does this package come from the local filesystem (and is editable)?
122 /// Whether this package is a member of the workspace
124 /// List of packages this package depends on
125 pub dependencies
: Vec
<PackageDependency
>,
126 /// Rust edition for this package
127 pub edition
: Edition
,
128 /// Features provided by the crate, mapped to the features required by that feature.
129 pub features
: FxHashMap
<String
, Vec
<String
>>,
130 /// List of features enabled on this package
131 pub active_features
: Vec
<String
>,
132 /// String representation of package id
134 /// The contents of [package.metadata.rust-analyzer]
135 pub metadata
: RustAnalyzerPackageMetaData
,
138 #[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)]
139 pub struct RustAnalyzerPackageMetaData
{
140 pub rustc_private
: bool
,
143 #[derive(Debug, Clone, Eq, PartialEq)]
144 pub struct PackageDependency
{
150 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
152 /// Available to the library, binary, and dev targets in the package (but not the build script).
154 /// Available only to test and bench targets (and the library target, when built with `cfg(test)`).
156 /// Available only to the build script target.
161 fn iter(list
: &[cargo_metadata
::DepKindInfo
]) -> impl Iterator
<Item
= Self> {
162 let mut dep_kinds
= [None
; 3];
164 dep_kinds
[0] = Some(Self::Normal
);
168 cargo_metadata
::DependencyKind
::Normal
=> dep_kinds
[0] = Some(Self::Normal
),
169 cargo_metadata
::DependencyKind
::Development
=> dep_kinds
[1] = Some(Self::Dev
),
170 cargo_metadata
::DependencyKind
::Build
=> dep_kinds
[2] = Some(Self::Build
),
171 cargo_metadata
::DependencyKind
::Unknown
=> continue,
174 dep_kinds
.into_iter().flatten()
178 /// Information associated with a package's target
179 #[derive(Debug, Clone, Eq, PartialEq)]
180 pub struct TargetData
{
181 /// Package that provided this target
182 pub package
: Package
,
183 /// Name as given in the `Cargo.toml` or generated from the file name
185 /// Path to the main source file of the target
186 pub root
: AbsPathBuf
,
188 pub kind
: TargetKind
,
189 /// Is this target a proc-macro
190 pub is_proc_macro
: bool
,
191 /// Required features of the target without which it won't build
192 pub required_features
: Vec
<String
>,
195 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
196 pub enum TargetKind
{
198 /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
208 fn new(kinds
: &[String
]) -> TargetKind
{
210 return match kind
.as_str() {
211 "bin" => TargetKind
::Bin
,
212 "test" => TargetKind
::Test
,
213 "bench" => TargetKind
::Bench
,
214 "example" => TargetKind
::Example
,
215 "custom-build" => TargetKind
::BuildScript
,
216 "proc-macro" => TargetKind
::Lib
,
217 _
if kind
.contains("lib") => TargetKind
::Lib
,
225 // Deserialize helper for the cargo metadata
226 #[derive(Deserialize, Default)]
227 struct PackageMetadata
{
228 #[serde(rename = "rust-analyzer")]
229 rust_analyzer
: Option
<RustAnalyzerPackageMetaData
>,
232 impl CargoWorkspace
{
233 pub fn fetch_metadata(
234 cargo_toml
: &ManifestPath
,
235 current_dir
: &AbsPath
,
236 config
: &CargoConfig
,
237 progress
: &dyn Fn(String
),
238 ) -> anyhow
::Result
<cargo_metadata
::Metadata
> {
239 let targets
= find_list_of_build_targets(config
, cargo_toml
);
241 let mut meta
= MetadataCommand
::new();
242 meta
.cargo_path(toolchain
::cargo());
243 meta
.manifest_path(cargo_toml
.to_path_buf());
244 match &config
.features
{
245 CargoFeatures
::All
=> {
246 meta
.features(CargoOpt
::AllFeatures
);
248 CargoFeatures
::Selected { features, no_default_features }
=> {
249 if *no_default_features
{
250 meta
.features(CargoOpt
::NoDefaultFeatures
);
252 if !features
.is_empty() {
253 meta
.features(CargoOpt
::SomeFeatures(features
.clone()));
257 meta
.current_dir(current_dir
.as_os_str());
259 let mut other_options
= vec
![];
260 // cargo metadata only supports a subset of flags of what cargo usually accepts, and usually
261 // the only relevant flags for metadata here are unstable ones, so we pass those along
263 let mut extra_args
= config
.extra_args
.iter();
264 while let Some(arg
) = extra_args
.next() {
266 if let Some(arg
) = extra_args
.next() {
267 other_options
.push("-Z".to_owned());
268 other_options
.push(arg
.to_owned());
273 if !targets
.is_empty() {
274 other_options
.append(
277 .flat_map(|target
| ["--filter-platform".to_owned().to_string(), target
])
281 meta
.other_options(other_options
);
283 // FIXME: Fetching metadata is a slow process, as it might require
284 // calling crates.io. We should be reporting progress here, but it's
285 // unclear whether cargo itself supports it.
286 progress("metadata".to_string());
288 (|| -> Result
<cargo_metadata
::Metadata
, cargo_metadata
::Error
> {
289 let mut command
= meta
.cargo_command();
290 command
.envs(&config
.extra_env
);
291 let output
= command
.output()?
;
292 if !output
.status
.success() {
293 return Err(cargo_metadata
::Error
::CargoMetadata
{
294 stderr
: String
::from_utf8(output
.stderr
)?
,
297 let stdout
= from_utf8(&output
.stdout
)?
299 .find(|line
| line
.starts_with('
{'
))
300 .ok_or(cargo_metadata
::Error
::NoJson
)?
;
301 cargo_metadata
::MetadataCommand
::parse(stdout
)
303 .with_context(|| format
!("Failed to run `{:?}`", meta
.cargo_command()))
306 pub fn new(mut meta
: cargo_metadata
::Metadata
) -> CargoWorkspace
{
307 let mut pkg_by_id
= FxHashMap
::default();
308 let mut packages
= Arena
::default();
309 let mut targets
= Arena
::default();
311 let ws_members
= &meta
.workspace_members
;
313 meta
.packages
.sort_by(|a
, b
| a
.id
.cmp(&b
.id
));
314 for meta_pkg
in meta
.packages
{
315 let cargo_metadata
::Package
{
320 targets
: meta_targets
,
328 let meta
= from_value
::<PackageMetadata
>(metadata
).unwrap_or_default();
329 let edition
= match edition
{
330 cargo_metadata
::Edition
::E2015
=> Edition
::Edition2015
,
331 cargo_metadata
::Edition
::E2018
=> Edition
::Edition2018
,
332 cargo_metadata
::Edition
::E2021
=> Edition
::Edition2021
,
334 tracing
::error
!("Unsupported edition `{:?}`", edition
);
338 // We treat packages without source as "local" packages. That includes all members of
339 // the current workspace, as well as any path dependency outside the workspace.
340 let is_local
= source
.is_none();
341 let is_member
= ws_members
.contains(&id
);
343 let pkg
= packages
.alloc(PackageData
{
347 manifest
: AbsPathBuf
::assert(manifest_path
.into()).try_into().unwrap(),
353 dependencies
: Vec
::new(),
354 features
: features
.into_iter().collect(),
355 active_features
: Vec
::new(),
356 metadata
: meta
.rust_analyzer
.unwrap_or_default(),
358 let pkg_data
= &mut packages
[pkg
];
359 pkg_by_id
.insert(id
, pkg
);
360 for meta_tgt
in meta_targets
{
361 let cargo_metadata
::Target { name, kind, required_features, src_path, .. }
=
363 let tgt
= targets
.alloc(TargetData
{
366 root
: AbsPathBuf
::assert(src_path
.into()),
367 kind
: TargetKind
::new(&kind
),
368 is_proc_macro
: &*kind
== ["proc-macro"],
371 pkg_data
.targets
.push(tgt
);
374 let resolve
= meta
.resolve
.expect("metadata executed with deps");
375 for mut node
in resolve
.nodes
{
376 let &source
= pkg_by_id
.get(&node
.id
).unwrap();
377 node
.deps
.sort_by(|a
, b
| a
.pkg
.cmp(&b
.pkg
));
378 let dependencies
= node
381 .flat_map(|dep
| DepKind
::iter(&dep
.dep_kinds
).map(move |kind
| (dep
, kind
)));
382 for (dep_node
, kind
) in dependencies
{
383 let &pkg
= pkg_by_id
.get(&dep_node
.pkg
).unwrap();
384 let dep
= PackageDependency { name: dep_node.name.clone(), pkg, kind }
;
385 packages
[source
].dependencies
.push(dep
);
387 packages
[source
].active_features
.extend(node
.features
);
391 AbsPathBuf
::assert(PathBuf
::from(meta
.workspace_root
.into_os_string()));
393 let target_directory
=
394 AbsPathBuf
::assert(PathBuf
::from(meta
.target_directory
.into_os_string()));
396 CargoWorkspace { packages, targets, workspace_root, target_directory }
399 pub fn packages(&self) -> impl Iterator
<Item
= Package
> + ExactSizeIterator
+ '_
{
400 self.packages
.iter().map(|(id
, _pkg
)| id
)
403 pub fn target_by_root(&self, root
: &AbsPath
) -> Option
<Target
> {
405 .filter(|&pkg
| self[pkg
].is_member
)
406 .find_map(|pkg
| self[pkg
].targets
.iter().find(|&&it
| &self[it
].root
== root
))
410 pub fn workspace_root(&self) -> &AbsPath
{
414 pub fn target_directory(&self) -> &AbsPath
{
415 &self.target_directory
418 pub fn package_flag(&self, package
: &PackageData
) -> String
{
419 if self.is_unique(&package
.name
) {
422 format
!("{}:{}", package
.name
, package
.version
)
426 pub fn parent_manifests(&self, manifest_path
: &ManifestPath
) -> Option
<Vec
<ManifestPath
>> {
427 let mut found
= false;
428 let parent_manifests
= self
431 if !found
&& &self[pkg
].manifest
== manifest_path
{
434 self[pkg
].dependencies
.iter().find_map(|dep
| {
435 (&self[dep
.pkg
].manifest
== manifest_path
).then(|| self[pkg
].manifest
.clone())
438 .collect
::<Vec
<ManifestPath
>>();
440 // some packages has this pkg as dep. return their manifests
441 if parent_manifests
.len() > 0 {
442 return Some(parent_manifests
);
445 // this pkg is inside this cargo workspace, fallback to workspace root
448 ManifestPath
::try_from(self.workspace_root().join("Cargo.toml")).ok()?
452 // not in this workspace
456 /// Returns the union of the features of all member crates in this workspace.
457 pub fn workspace_features(&self) -> FxHashSet
<String
> {
459 .filter_map(|package
| {
460 let package
= &self[package
];
461 if package
.is_member
{
462 Some(package
.features
.keys().cloned())
471 fn is_unique(&self, name
: &str) -> bool
{
472 self.packages
.iter().filter(|(_
, v
)| v
.name
== name
).count() == 1
476 fn find_list_of_build_targets(config
: &CargoConfig
, cargo_toml
: &ManifestPath
) -> Vec
<String
> {
477 if let Some(target
) = &config
.target
{
478 return [target
.into()].to_vec();
481 let build_targets
= cargo_config_build_target(cargo_toml
, &config
.extra_env
);
482 if !build_targets
.is_empty() {
483 return build_targets
;
486 rustc_discover_host_triple(cargo_toml
, &config
.extra_env
).into_iter().collect()
489 fn rustc_discover_host_triple(
490 cargo_toml
: &ManifestPath
,
491 extra_env
: &FxHashMap
<String
, String
>,
492 ) -> Option
<String
> {
493 let mut rustc
= Command
::new(toolchain
::rustc());
494 rustc
.envs(extra_env
);
495 rustc
.current_dir(cargo_toml
.parent()).arg("-vV");
496 tracing
::debug
!("Discovering host platform by {:?}", rustc
);
497 match utf8_stdout(rustc
) {
499 let field
= "host: ";
500 let target
= stdout
.lines().find_map(|l
| l
.strip_prefix(field
));
501 if let Some(target
) = target
{
502 Some(target
.to_string())
504 // If we fail to resolve the host platform, it's not the end of the world.
505 tracing
::info
!("rustc -vV did not report host platform, got:\n{}", stdout
);
510 tracing
::warn
!("Failed to discover host platform: {}", e
);
516 fn cargo_config_build_target(
517 cargo_toml
: &ManifestPath
,
518 extra_env
: &FxHashMap
<String
, String
>,
520 let mut cargo_config
= Command
::new(toolchain
::cargo());
521 cargo_config
.envs(extra_env
);
523 .current_dir(cargo_toml
.parent())
524 .args(["-Z", "unstable-options", "config", "get", "build.target"])
525 .env("RUSTC_BOOTSTRAP", "1");
526 // if successful we receive `build.target = "target-triple"`
527 // or `build.target = ["<target 1>", ..]`
528 tracing
::debug
!("Discovering cargo config target by {:?}", cargo_config
);
529 utf8_stdout(cargo_config
).map(parse_output_cargo_config_build_target
).unwrap_or_default()
532 fn parse_output_cargo_config_build_target(stdout
: String
) -> Vec
<String
> {
533 let trimmed
= stdout
.trim_start_matches("build.target = ").trim_matches('
"');
535 if !trimmed.starts_with('[') {
536 return [trimmed.to_string()].to_vec();
539 let res = serde_json::from_str(trimmed);
540 if let Err(e) = &res {
541 tracing::warn!("Failed to parse `build
.target`
as an array of target
: {}`
", e);
543 res.unwrap_or_default()