//! See [`CargoWorkspace`]. use std::{borrow::Cow, ops, str::from_utf8}; use anyhow::Context; use base_db::Env; use cargo_metadata::{CargoOpt, MetadataCommand, PackageId}; use la_arena::{Arena, Idx}; use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf}; use rustc_hash::{FxHashMap, FxHashSet}; use serde_derive::Deserialize; use serde_json::from_value; use span::Edition; use stdx::process::spawn_with_streaming_output; use toolchain::{NO_RUSTUP_AUTO_INSTALL_ENV, Tool}; use triomphe::Arc; use crate::{ CfgOverrides, InvocationStrategy, ManifestPath, Sysroot, cargo_config_file::{LockfileCopy, LockfileUsage, make_lockfile_copy}, }; /// [`CargoWorkspace`] represents the logical structure of, well, a Cargo /// workspace. It pretty closely mirrors `cargo metadata` output. /// /// Note that internally, rust-analyzer uses a different structure: /// `CrateGraph`. `CrateGraph` is lower-level: it knows only about the crates, /// while this knows about `Packages` & `Targets`: purely cargo-related /// concepts. /// /// We use absolute paths here, `cargo metadata` guarantees to always produce /// abs paths. #[derive(Debug, Clone, Eq, PartialEq)] pub struct CargoWorkspace { packages: Arena, targets: Arena, workspace_root: AbsPathBuf, target_directory: AbsPathBuf, manifest_path: ManifestPath, is_virtual_workspace: bool, /// Whether this workspace represents the sysroot workspace. is_sysroot: bool, /// Environment variables set in the `.cargo/config` file and the extraEnv /// configuration option. env: Env, requires_rustc_private: bool, } impl ops::Index for CargoWorkspace { type Output = PackageData; fn index(&self, index: Package) -> &PackageData { &self.packages[index] } } impl ops::Index for CargoWorkspace { type Output = TargetData; fn index(&self, index: Target) -> &TargetData { &self.targets[index] } } /// Describes how to set the rustc source directory. #[derive(Clone, Debug, PartialEq, Eq)] pub enum RustLibSource { /// Explicit path for the rustc source directory. Path(AbsPathBuf), /// Try to automatically detect where the rustc source directory is. Discover, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum CargoFeatures { All, Selected { /// List of features to activate. features: Vec, /// Do not activate the `default` feature. no_default_features: bool, }, } impl Default for CargoFeatures { fn default() -> Self { CargoFeatures::Selected { features: vec![], no_default_features: false } } } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub enum TargetDirectoryConfig { #[default] None, UseSubdirectory, Directory(Utf8PathBuf), } impl TargetDirectoryConfig { pub fn target_dir<'a>( &'a self, ws_target_dir: Option<&'a Utf8Path>, ) -> Option> { match self { TargetDirectoryConfig::None => None, TargetDirectoryConfig::UseSubdirectory => { Some(Cow::Owned(ws_target_dir?.join("rust-analyzer"))) } TargetDirectoryConfig::Directory(dir) => Some(Cow::Borrowed(dir)), } } } #[derive(Default, Clone, Debug, PartialEq, Eq)] pub struct CargoConfig { /// Whether to pass `--all-targets` to cargo invocations. pub all_targets: bool, /// List of features to activate. pub features: CargoFeatures, /// rustc target pub target: Option, /// Sysroot loading behavior pub sysroot: Option, pub sysroot_src: Option, /// rustc private crate source pub rustc_source: Option, /// Extra includes to add to the VFS. pub extra_includes: Vec, pub cfg_overrides: CfgOverrides, /// Invoke `cargo check` through the RUSTC_WRAPPER. pub wrap_rustc_in_build_scripts: bool, /// The command to run instead of `cargo check` for building build scripts. pub run_build_script_command: Option>, /// Extra args to pass to the cargo command. pub extra_args: Vec, /// Extra env vars to set when invoking the cargo command pub extra_env: FxHashMap>, pub invocation_strategy: InvocationStrategy, /// Optional path to use instead of `target` when building pub target_dir_config: TargetDirectoryConfig, /// Gate `#[test]` behind `#[cfg(test)]` pub set_test: bool, /// Load the project without any dependencies pub no_deps: bool, } pub type Package = Idx; pub type Target = Idx; /// Information associated with a cargo crate #[derive(Debug, Clone, Eq, PartialEq)] pub struct PackageData { /// Version given in the `Cargo.toml` pub version: semver::Version, /// Name as given in the `Cargo.toml` pub name: String, /// Repository as given in the `Cargo.toml` pub repository: Option, /// Path containing the `Cargo.toml` pub manifest: ManifestPath, /// Targets provided by the crate (lib, bin, example, test, ...) pub targets: Vec, /// Does this package come from the local filesystem (and is editable)? pub is_local: bool, /// Whether this package is a member of the workspace pub is_member: bool, /// List of packages this package depends on pub dependencies: Vec, /// Rust edition for this package pub edition: Edition, /// Features provided by the crate, mapped to the features required by that feature. pub features: FxHashMap>, /// List of features enabled on this package pub active_features: Vec, /// Package id pub id: Arc, /// Authors as given in the `Cargo.toml` pub authors: Vec, /// Description as given in the `Cargo.toml` pub description: Option, /// Homepage as given in the `Cargo.toml` pub homepage: Option, /// License as given in the `Cargo.toml` pub license: Option, /// License file as given in the `Cargo.toml` pub license_file: Option, /// Readme file as given in the `Cargo.toml` pub readme: Option, /// Rust version as given in the `Cargo.toml` pub rust_version: Option, /// The contents of [package.metadata.rust-analyzer] pub metadata: RustAnalyzerPackageMetaData, /// If this package is a member of the workspace, store all direct and transitive /// dependencies as long as they are workspace members, to track dependency relationships /// between members. pub all_member_deps: Option>, } #[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)] pub struct RustAnalyzerPackageMetaData { pub rustc_private: bool, } #[derive(Debug, Clone, Eq, PartialEq)] pub struct PackageDependency { pub pkg: Package, pub name: String, pub kind: DepKind, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DepKind { /// Available to the library, binary, and dev targets in the package (but not the build script). Normal, /// Available only to test and bench targets (and the library target, when built with `cfg(test)`). Dev, /// Available only to the build script target. Build, } impl DepKind { fn iter(list: &[cargo_metadata::DepKindInfo]) -> impl Iterator { let mut dep_kinds = [None; 3]; if list.is_empty() { dep_kinds[0] = Some(Self::Normal); } for info in list { match info.kind { cargo_metadata::DependencyKind::Normal => dep_kinds[0] = Some(Self::Normal), cargo_metadata::DependencyKind::Development => dep_kinds[1] = Some(Self::Dev), cargo_metadata::DependencyKind::Build => dep_kinds[2] = Some(Self::Build), cargo_metadata::DependencyKind::Unknown => continue, } } dep_kinds.into_iter().flatten() } } /// Information associated with a package's target #[derive(Debug, Clone, Eq, PartialEq)] pub struct TargetData { /// Package that provided this target pub package: Package, /// Name as given in the `Cargo.toml` or generated from the file name pub name: String, /// Path to the main source file of the target pub root: AbsPathBuf, /// Kind of target pub kind: TargetKind, /// Required features of the target without which it won't build pub required_features: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TargetKind { Bin, /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...). Lib { /// Is this target a proc-macro is_proc_macro: bool, }, Example, Test, Bench, /// Cargo calls this kind `custom-build` BuildScript, Other, } impl TargetKind { pub fn new(kinds: &[cargo_metadata::TargetKind]) -> TargetKind { for kind in kinds { return match kind { cargo_metadata::TargetKind::Bin => TargetKind::Bin, cargo_metadata::TargetKind::Test => TargetKind::Test, cargo_metadata::TargetKind::Bench => TargetKind::Bench, cargo_metadata::TargetKind::Example => TargetKind::Example, cargo_metadata::TargetKind::CustomBuild => TargetKind::BuildScript, cargo_metadata::TargetKind::ProcMacro => TargetKind::Lib { is_proc_macro: true }, cargo_metadata::TargetKind::Lib | cargo_metadata::TargetKind::DyLib | cargo_metadata::TargetKind::CDyLib | cargo_metadata::TargetKind::StaticLib | cargo_metadata::TargetKind::RLib => TargetKind::Lib { is_proc_macro: false }, _ => continue, }; } TargetKind::Other } pub fn is_executable(self) -> bool { matches!(self, TargetKind::Bin | TargetKind::Example) } pub fn is_proc_macro(self) -> bool { matches!(self, TargetKind::Lib { is_proc_macro: true }) } /// If this is a valid cargo target, returns the name cargo uses in command line arguments /// and output, otherwise None. /// pub fn as_cargo_target(self) -> Option<&'static str> { match self { TargetKind::Bin => Some("bin"), TargetKind::Lib { is_proc_macro: true } => Some("proc-macro"), TargetKind::Lib { is_proc_macro: false } => Some("lib"), TargetKind::Example => Some("example"), TargetKind::Test => Some("test"), TargetKind::Bench => Some("bench"), TargetKind::BuildScript => Some("custom-build"), TargetKind::Other => None, } } } #[derive(Default, Clone, Debug, PartialEq, Eq)] pub struct CargoMetadataConfig { /// List of features to activate. pub features: CargoFeatures, /// rustc targets pub targets: Vec, /// Extra args to pass to the cargo command. pub extra_args: Vec, /// Extra env vars to set when invoking the cargo command pub extra_env: FxHashMap>, /// What kind of metadata are we fetching: workspace, rustc, or sysroot. pub kind: &'static str, /// The toolchain version, if known. /// Used to conditionally enable unstable cargo features. pub toolchain_version: Option, } // Deserialize helper for the cargo metadata #[derive(Deserialize, Default)] struct PackageMetadata { #[serde(rename = "rust-analyzer")] rust_analyzer: Option, } impl CargoWorkspace { pub fn new( mut meta: cargo_metadata::Metadata, ws_manifest_path: ManifestPath, cargo_env: Env, is_sysroot: bool, ) -> CargoWorkspace { let mut pkg_by_id = FxHashMap::default(); let mut packages = Arena::default(); let mut targets = Arena::default(); let ws_members = &meta.workspace_members; let workspace_root = AbsPathBuf::assert(meta.workspace_root); let target_directory = AbsPathBuf::assert(meta.target_directory); let mut is_virtual_workspace = true; let mut requires_rustc_private = false; let mut members = FxHashSet::default(); meta.packages.sort_by(|a, b| a.id.cmp(&b.id)); for meta_pkg in meta.packages { let cargo_metadata::Package { name, version, id, source, targets: meta_targets, features, manifest_path, repository, edition, metadata, authors, description, homepage, license, license_file, readme, rust_version, .. } = meta_pkg; let id = Arc::new(id); let meta = from_value::(metadata).unwrap_or_default(); let edition = match edition { cargo_metadata::Edition::E2015 => Edition::Edition2015, cargo_metadata::Edition::E2018 => Edition::Edition2018, cargo_metadata::Edition::E2021 => Edition::Edition2021, cargo_metadata::Edition::E2024 => Edition::Edition2024, _ => { tracing::error!("Unsupported edition `{:?}`", edition); Edition::CURRENT } }; // We treat packages without source as "local" packages. That includes all members of // the current workspace, as well as any path dependency outside the workspace. let is_local = source.is_none(); let is_member = ws_members.contains(&id); let manifest = ManifestPath::try_from(AbsPathBuf::assert(manifest_path)).unwrap(); is_virtual_workspace &= manifest != ws_manifest_path; let pkg = packages.alloc(PackageData { id: id.clone(), name: name.to_string(), version, manifest: manifest.clone(), targets: Vec::new(), is_local, is_member, edition, repository, authors, description, homepage, license, license_file, readme, rust_version, dependencies: Vec::new(), features: features.into_iter().collect(), active_features: Vec::new(), metadata: meta.rust_analyzer.unwrap_or_default(), all_member_deps: None, }); if is_member { members.insert(pkg); } let pkg_data = &mut packages[pkg]; requires_rustc_private |= pkg_data.metadata.rustc_private; pkg_by_id.insert(id, pkg); for meta_tgt in meta_targets { let cargo_metadata::Target { name, kind, required_features, src_path, .. } = meta_tgt; let kind = TargetKind::new(&kind); let tgt = targets.alloc(TargetData { package: pkg, name, root: if kind == TargetKind::Bin && manifest.extension().is_some_and(|ext| ext == "rs") { // cargo strips the script part of a cargo script away and places the // modified manifest file into a special target dir which is then used as // the source path. We don't want that, we want the original here so map it // back manifest.clone().into() } else { AbsPathBuf::assert(src_path) }, kind, required_features, }); pkg_data.targets.push(tgt); } } for mut node in meta.resolve.map_or_else(Vec::new, |it| it.nodes) { let &source = pkg_by_id.get(&node.id).unwrap(); node.deps.sort_by(|a, b| a.pkg.cmp(&b.pkg)); let dependencies = node .deps .iter() .flat_map(|dep| DepKind::iter(&dep.dep_kinds).map(move |kind| (dep, kind))); for (dep_node, kind) in dependencies { let &pkg = pkg_by_id.get(&dep_node.pkg).unwrap(); let dep = PackageDependency { name: dep_node.name.to_string(), pkg, kind }; packages[source].dependencies.push(dep); } packages[source] .active_features .extend(node.features.into_iter().map(|it| it.to_string())); } fn saturate_all_member_deps( packages: &mut Arena, to_visit: Package, visited: &mut FxHashSet, members: &FxHashSet, ) { let pkg_data = &mut packages[to_visit]; if !visited.insert(to_visit) { return; } let deps: Vec<_> = pkg_data .dependencies .iter() .filter_map(|dep| { let pkg = dep.pkg; if members.contains(&pkg) { Some(pkg) } else { None } }) .collect(); let mut all_member_deps = FxHashSet::from_iter(deps.iter().copied()); for dep in deps { saturate_all_member_deps(packages, dep, visited, members); if let Some(transitives) = &packages[dep].all_member_deps { all_member_deps.extend(transitives); } } packages[to_visit].all_member_deps = Some(all_member_deps); } let mut visited = FxHashSet::default(); for member in members.iter() { saturate_all_member_deps(&mut packages, *member, &mut visited, &members); } CargoWorkspace { packages, targets, workspace_root, target_directory, manifest_path: ws_manifest_path, is_virtual_workspace, requires_rustc_private, is_sysroot, env: cargo_env, } } pub fn packages(&self) -> impl ExactSizeIterator + '_ { self.packages.iter().map(|(id, _pkg)| id) } pub fn target_by_root(&self, root: &AbsPath) -> Option { self.packages() .filter(|&pkg| self[pkg].is_member) .find_map(|pkg| self[pkg].targets.iter().find(|&&it| self[it].root == root)) .copied() } pub fn workspace_root(&self) -> &AbsPath { &self.workspace_root } pub fn manifest_path(&self) -> &ManifestPath { &self.manifest_path } pub fn target_directory(&self) -> &AbsPath { &self.target_directory } pub fn package_flag(&self, package: &PackageData) -> String { if self.is_unique(&package.name) { package.name.clone() } else { format!("{}:{}", package.name, package.version) } } pub fn parent_manifests(&self, manifest_path: &ManifestPath) -> Option> { let mut found = false; let parent_manifests = self .packages() .filter_map(|pkg| { if !found && &self[pkg].manifest == manifest_path { found = true } self[pkg].dependencies.iter().find_map(|dep| { (&self[dep.pkg].manifest == manifest_path).then(|| self[pkg].manifest.clone()) }) }) .collect::>(); // some packages has this pkg as dep. return their manifests if !parent_manifests.is_empty() { return Some(parent_manifests); } // this pkg is inside this cargo workspace, fallback to workspace root if found { return Some(vec![ ManifestPath::try_from(self.workspace_root().join("Cargo.toml")).ok()?, ]); } // not in this workspace None } /// Returns the union of the features of all member crates in this workspace. pub fn workspace_features(&self) -> FxHashSet { self.packages() .filter_map(|package| { let package = &self[package]; if package.is_member { Some(package.features.keys().cloned().chain( package.features.keys().map(|key| format!("{}/{key}", package.name)), )) } else { None } }) .flatten() .collect() } fn is_unique(&self, name: &str) -> bool { self.packages.iter().filter(|(_, v)| v.name == name).count() == 1 } pub fn is_virtual_workspace(&self) -> bool { self.is_virtual_workspace } pub fn env(&self) -> &Env { &self.env } pub fn is_sysroot(&self) -> bool { self.is_sysroot } pub fn requires_rustc_private(&self) -> bool { self.requires_rustc_private } } pub(crate) struct FetchMetadata { command: cargo_metadata::MetadataCommand, #[expect(dead_code)] manifest_path: ManifestPath, lockfile_copy: Option, #[expect(dead_code)] kind: &'static str, no_deps: bool, no_deps_result: anyhow::Result, other_options: Vec, } impl FetchMetadata { /// Builds a command to fetch metadata for the given `cargo_toml` manifest. /// /// Performs a lightweight pre-fetch using the `--no-deps` option, /// available via `FetchMetadata::no_deps_metadata`, to gather basic /// information such as the `target-dir`. /// /// The provided sysroot is used to set the `RUSTUP_TOOLCHAIN` /// environment variable when invoking Cargo, ensuring that the /// rustup proxy selects the correct toolchain. pub(crate) fn new( cargo_toml: &ManifestPath, current_dir: &AbsPath, config: &CargoMetadataConfig, sysroot: &Sysroot, no_deps: bool, ) -> Self { let cargo = sysroot.tool(Tool::Cargo, current_dir, &config.extra_env); let mut command = MetadataCommand::new(); command.env(NO_RUSTUP_AUTO_INSTALL_ENV.0, NO_RUSTUP_AUTO_INSTALL_ENV.1); command.cargo_path(cargo.get_program()); cargo.get_envs().for_each(|(var, val)| _ = command.env(var, val.unwrap_or_default())); command.manifest_path(cargo_toml.to_path_buf()); match &config.features { CargoFeatures::All => { command.features(CargoOpt::AllFeatures); } CargoFeatures::Selected { features, no_default_features } => { if *no_default_features { command.features(CargoOpt::NoDefaultFeatures); } if !features.is_empty() { command.features(CargoOpt::SomeFeatures(features.clone())); } } } command.current_dir(current_dir); let mut other_options = vec![]; // cargo metadata only supports a subset of flags of what cargo usually accepts, and usually // the only relevant flags for metadata here are unstable ones, so we pass those along // but nothing else let mut extra_args = config.extra_args.iter(); while let Some(arg) = extra_args.next() { if arg == "-Z" && let Some(arg) = extra_args.next() { other_options.push("-Z".to_owned()); other_options.push(arg.to_owned()); } } let mut lockfile_copy = None; if cargo_toml.is_rust_manifest() { other_options.push("-Zscript".to_owned()); } else if let Some(v) = config.toolchain_version.as_ref() { lockfile_copy = make_lockfile_copy( v, &<_ as AsRef>::as_ref(cargo_toml).with_extension("lock"), ); } if !config.targets.is_empty() { other_options.extend( config.targets.iter().flat_map(|it| ["--filter-platform".to_owned(), it.clone()]), ); } command.other_options(other_options.clone()); // Pre-fetch basic metadata using `--no-deps`, which: // - avoids fetching registries like crates.io, // - skips dependency resolution and does not modify lockfiles, // - and thus doesn't require progress reporting or copying lockfiles. // // Useful as a fast fallback to extract info like `target-dir`. let cargo_command; let no_deps_result = if no_deps { command.no_deps(); cargo_command = command.cargo_command(); command.exec() } else { let mut no_deps_command = command.clone(); no_deps_command.no_deps(); cargo_command = no_deps_command.cargo_command(); no_deps_command.exec() } .with_context(|| format!("Failed to run `{cargo_command:?}`")); Self { manifest_path: cargo_toml.clone(), command, lockfile_copy, kind: config.kind, no_deps, no_deps_result, other_options, } } /// Executes the metadata-fetching command. /// /// A successful result may still contain a metadata error if the full fetch failed, /// but the fallback `--no-deps` pre-fetch succeeded during command construction. pub(crate) fn exec( self, locked: bool, progress: &dyn Fn(String), ) -> anyhow::Result<(cargo_metadata::Metadata, Option)> { let Self { mut command, manifest_path: _, lockfile_copy, kind: _, no_deps, no_deps_result, mut other_options, } = self; if no_deps { return no_deps_result.map(|m| (m, None)); } let mut using_lockfile_copy = false; if let Some(lockfile_copy) = &lockfile_copy { match lockfile_copy.usage { LockfileUsage::WithFlag => { other_options.push("--lockfile-path".to_owned()); other_options.push(lockfile_copy.path.to_string()); } LockfileUsage::WithEnvVar => { other_options.push("-Zlockfile-path".to_owned()); command.env("CARGO_RESOLVER_LOCKFILE_PATH", lockfile_copy.path.as_os_str()); } } using_lockfile_copy = true; } if using_lockfile_copy || other_options.iter().any(|it| it.starts_with("-Z")) { command.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly"); other_options.push("-Zunstable-options".to_owned()); } // No need to lock it if we copied the lockfile, we won't modify the original after all/ // This way cargo cannot error out on us if the lockfile requires updating. if !using_lockfile_copy && locked { other_options.push("--locked".to_owned()); } command.other_options(other_options); progress("cargo metadata: started".to_owned()); let res = (|| -> anyhow::Result<(_, _)> { let mut errored = false; tracing::debug!("Running `{:?}`", command.cargo_command()); let output = spawn_with_streaming_output(command.cargo_command(), &mut |_| (), &mut |line| { errored = errored || line.starts_with("error") || line.starts_with("warning"); if errored { progress("cargo metadata: ?".to_owned()); return; } progress(format!("cargo metadata: {line}")); })?; if !output.status.success() { progress(format!("cargo metadata: failed {}", output.status)); let error = cargo_metadata::Error::CargoMetadata { stderr: String::from_utf8(output.stderr)?, } .into(); if !no_deps { // If we failed to fetch metadata with deps, return pre-fetched result without them. // This makes r-a still work partially when offline. if let Ok(metadata) = no_deps_result { tracing::warn!( ?error, "`cargo metadata` failed and returning succeeded result with `--no-deps`" ); return Ok((metadata, Some(error))); } } return Err(error); } let stdout = from_utf8(&output.stdout)? .lines() .find(|line| line.starts_with('{')) .ok_or(cargo_metadata::Error::NoJson)?; Ok((cargo_metadata::MetadataCommand::parse(stdout)?, None)) })() .with_context(|| format!("Failed to run `{:?}`", command.cargo_command())); progress("cargo metadata: finished".to_owned()); res } }