1use std::ops;
4use std::str::from_utf8;
5
6use anyhow::Context;
7use base_db::Env;
8use cargo_metadata::{CargoOpt, MetadataCommand};
9use la_arena::{Arena, Idx};
10use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf};
11use rustc_hash::{FxHashMap, FxHashSet};
12use serde_derive::Deserialize;
13use serde_json::from_value;
14use span::Edition;
15use stdx::process::spawn_with_streaming_output;
16use toolchain::Tool;
17
18use crate::cargo_config_file::make_lockfile_copy;
19use crate::{CfgOverrides, InvocationStrategy};
20use crate::{ManifestPath, Sysroot};
21
22pub(crate) const MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH: semver::Version =
23 semver::Version {
24 major: 1,
25 minor: 82,
26 patch: 0,
27 pre: semver::Prerelease::EMPTY,
28 build: semver::BuildMetadata::EMPTY,
29 };
30
31#[derive(Debug, Clone, Eq, PartialEq)]
42pub struct CargoWorkspace {
43 packages: Arena<PackageData>,
44 targets: Arena<TargetData>,
45 workspace_root: AbsPathBuf,
46 target_directory: AbsPathBuf,
47 manifest_path: ManifestPath,
48 is_virtual_workspace: bool,
49 is_sysroot: bool,
51 config_env: Env,
53 requires_rustc_private: bool,
54}
55
56impl ops::Index<Package> for CargoWorkspace {
57 type Output = PackageData;
58 fn index(&self, index: Package) -> &PackageData {
59 &self.packages[index]
60 }
61}
62
63impl ops::Index<Target> for CargoWorkspace {
64 type Output = TargetData;
65 fn index(&self, index: Target) -> &TargetData {
66 &self.targets[index]
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
72pub enum RustLibSource {
73 Path(AbsPathBuf),
75 Discover,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum CargoFeatures {
81 All,
82 Selected {
83 features: Vec<String>,
85 no_default_features: bool,
87 },
88}
89
90impl Default for CargoFeatures {
91 fn default() -> Self {
92 CargoFeatures::Selected { features: vec![], no_default_features: false }
93 }
94}
95
96#[derive(Default, Clone, Debug, PartialEq, Eq)]
97pub struct CargoConfig {
98 pub all_targets: bool,
100 pub features: CargoFeatures,
102 pub target: Option<String>,
104 pub sysroot: Option<RustLibSource>,
106 pub sysroot_src: Option<AbsPathBuf>,
107 pub rustc_source: Option<RustLibSource>,
109 pub extra_includes: Vec<AbsPathBuf>,
111 pub cfg_overrides: CfgOverrides,
112 pub wrap_rustc_in_build_scripts: bool,
114 pub run_build_script_command: Option<Vec<String>>,
116 pub extra_args: Vec<String>,
118 pub extra_env: FxHashMap<String, Option<String>>,
120 pub invocation_strategy: InvocationStrategy,
121 pub target_dir: Option<Utf8PathBuf>,
123 pub set_test: bool,
125 pub no_deps: bool,
127}
128
129pub type Package = Idx<PackageData>;
130
131pub type Target = Idx<TargetData>;
132
133#[derive(Debug, Clone, Eq, PartialEq)]
135pub struct PackageData {
136 pub version: semver::Version,
138 pub name: String,
140 pub repository: Option<String>,
142 pub manifest: ManifestPath,
144 pub targets: Vec<Target>,
146 pub is_local: bool,
148 pub is_member: bool,
150 pub dependencies: Vec<PackageDependency>,
152 pub edition: Edition,
154 pub features: FxHashMap<String, Vec<String>>,
156 pub active_features: Vec<String>,
158 pub id: String,
160 pub authors: Vec<String>,
162 pub description: Option<String>,
164 pub homepage: Option<String>,
166 pub license: Option<String>,
168 pub license_file: Option<Utf8PathBuf>,
170 pub readme: Option<Utf8PathBuf>,
172 pub rust_version: Option<semver::Version>,
174 pub metadata: RustAnalyzerPackageMetaData,
176}
177
178#[derive(Deserialize, Default, Debug, Clone, Eq, PartialEq)]
179pub struct RustAnalyzerPackageMetaData {
180 pub rustc_private: bool,
181}
182
183#[derive(Debug, Clone, Eq, PartialEq)]
184pub struct PackageDependency {
185 pub pkg: Package,
186 pub name: String,
187 pub kind: DepKind,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum DepKind {
192 Normal,
194 Dev,
196 Build,
198}
199
200impl DepKind {
201 fn iter(list: &[cargo_metadata::DepKindInfo]) -> impl Iterator<Item = Self> {
202 let mut dep_kinds = [None; 3];
203 if list.is_empty() {
204 dep_kinds[0] = Some(Self::Normal);
205 }
206 for info in list {
207 match info.kind {
208 cargo_metadata::DependencyKind::Normal => dep_kinds[0] = Some(Self::Normal),
209 cargo_metadata::DependencyKind::Development => dep_kinds[1] = Some(Self::Dev),
210 cargo_metadata::DependencyKind::Build => dep_kinds[2] = Some(Self::Build),
211 cargo_metadata::DependencyKind::Unknown => continue,
212 }
213 }
214 dep_kinds.into_iter().flatten()
215 }
216}
217
218#[derive(Debug, Clone, Eq, PartialEq)]
220pub struct TargetData {
221 pub package: Package,
223 pub name: String,
225 pub root: AbsPathBuf,
227 pub kind: TargetKind,
229 pub required_features: Vec<String>,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum TargetKind {
235 Bin,
236 Lib {
238 is_proc_macro: bool,
240 },
241 Example,
242 Test,
243 Bench,
244 BuildScript,
246 Other,
247}
248
249impl TargetKind {
250 pub fn new(kinds: &[cargo_metadata::TargetKind]) -> TargetKind {
251 for kind in kinds {
252 return match kind {
253 cargo_metadata::TargetKind::Bin => TargetKind::Bin,
254 cargo_metadata::TargetKind::Test => TargetKind::Test,
255 cargo_metadata::TargetKind::Bench => TargetKind::Bench,
256 cargo_metadata::TargetKind::Example => TargetKind::Example,
257 cargo_metadata::TargetKind::CustomBuild => TargetKind::BuildScript,
258 cargo_metadata::TargetKind::ProcMacro => TargetKind::Lib { is_proc_macro: true },
259 cargo_metadata::TargetKind::Lib
260 | cargo_metadata::TargetKind::DyLib
261 | cargo_metadata::TargetKind::CDyLib
262 | cargo_metadata::TargetKind::StaticLib
263 | cargo_metadata::TargetKind::RLib => TargetKind::Lib { is_proc_macro: false },
264 _ => continue,
265 };
266 }
267 TargetKind::Other
268 }
269
270 pub fn is_executable(self) -> bool {
271 matches!(self, TargetKind::Bin | TargetKind::Example)
272 }
273
274 pub fn is_proc_macro(self) -> bool {
275 matches!(self, TargetKind::Lib { is_proc_macro: true })
276 }
277
278 pub fn as_cargo_target(self) -> Option<&'static str> {
282 match self {
283 TargetKind::Bin => Some("bin"),
284 TargetKind::Lib { is_proc_macro: true } => Some("proc-macro"),
285 TargetKind::Lib { is_proc_macro: false } => Some("lib"),
286 TargetKind::Example => Some("example"),
287 TargetKind::Test => Some("test"),
288 TargetKind::Bench => Some("bench"),
289 TargetKind::BuildScript => Some("custom-build"),
290 TargetKind::Other => None,
291 }
292 }
293}
294
295#[derive(Default, Clone, Debug, PartialEq, Eq)]
296pub struct CargoMetadataConfig {
297 pub features: CargoFeatures,
299 pub targets: Vec<String>,
301 pub extra_args: Vec<String>,
303 pub extra_env: FxHashMap<String, Option<String>>,
305 pub kind: &'static str,
307 pub toolchain_version: Option<semver::Version>,
310}
311
312#[derive(Deserialize, Default)]
314struct PackageMetadata {
315 #[serde(rename = "rust-analyzer")]
316 rust_analyzer: Option<RustAnalyzerPackageMetaData>,
317}
318
319impl CargoWorkspace {
320 pub fn new(
321 mut meta: cargo_metadata::Metadata,
322 ws_manifest_path: ManifestPath,
323 cargo_config_env: Env,
324 is_sysroot: bool,
325 ) -> CargoWorkspace {
326 let mut pkg_by_id = FxHashMap::default();
327 let mut packages = Arena::default();
328 let mut targets = Arena::default();
329
330 let ws_members = &meta.workspace_members;
331
332 let workspace_root = AbsPathBuf::assert(meta.workspace_root);
333 let target_directory = AbsPathBuf::assert(meta.target_directory);
334 let mut is_virtual_workspace = true;
335 let mut requires_rustc_private = false;
336
337 meta.packages.sort_by(|a, b| a.id.cmp(&b.id));
338 for meta_pkg in meta.packages {
339 let cargo_metadata::Package {
340 name,
341 version,
342 id,
343 source,
344 targets: meta_targets,
345 features,
346 manifest_path,
347 repository,
348 edition,
349 metadata,
350 authors,
351 description,
352 homepage,
353 license,
354 license_file,
355 readme,
356 rust_version,
357 ..
358 } = meta_pkg;
359 let meta = from_value::<PackageMetadata>(metadata).unwrap_or_default();
360 let edition = match edition {
361 cargo_metadata::Edition::E2015 => Edition::Edition2015,
362 cargo_metadata::Edition::E2018 => Edition::Edition2018,
363 cargo_metadata::Edition::E2021 => Edition::Edition2021,
364 cargo_metadata::Edition::E2024 => Edition::Edition2024,
365 _ => {
366 tracing::error!("Unsupported edition `{:?}`", edition);
367 Edition::CURRENT
368 }
369 };
370 let is_local = source.is_none();
373 let is_member = ws_members.contains(&id);
374
375 let manifest = ManifestPath::try_from(AbsPathBuf::assert(manifest_path)).unwrap();
376 is_virtual_workspace &= manifest != ws_manifest_path;
377 let pkg = packages.alloc(PackageData {
378 id: id.repr.clone(),
379 name: name.to_string(),
380 version,
381 manifest: manifest.clone(),
382 targets: Vec::new(),
383 is_local,
384 is_member,
385 edition,
386 repository,
387 authors,
388 description,
389 homepage,
390 license,
391 license_file,
392 readme,
393 rust_version,
394 dependencies: Vec::new(),
395 features: features.into_iter().collect(),
396 active_features: Vec::new(),
397 metadata: meta.rust_analyzer.unwrap_or_default(),
398 });
399 let pkg_data = &mut packages[pkg];
400 requires_rustc_private |= pkg_data.metadata.rustc_private;
401 pkg_by_id.insert(id, pkg);
402 for meta_tgt in meta_targets {
403 let cargo_metadata::Target { name, kind, required_features, src_path, .. } =
404 meta_tgt;
405 let kind = TargetKind::new(&kind);
406 let tgt = targets.alloc(TargetData {
407 package: pkg,
408 name,
409 root: if kind == TargetKind::Bin
410 && manifest.extension().is_some_and(|ext| ext == "rs")
411 {
412 manifest.clone().into()
417 } else {
418 AbsPathBuf::assert(src_path)
419 },
420 kind,
421 required_features,
422 });
423 pkg_data.targets.push(tgt);
424 }
425 }
426 for mut node in meta.resolve.map_or_else(Vec::new, |it| it.nodes) {
427 let &source = pkg_by_id.get(&node.id).unwrap();
428 node.deps.sort_by(|a, b| a.pkg.cmp(&b.pkg));
429 let dependencies = node
430 .deps
431 .iter()
432 .flat_map(|dep| DepKind::iter(&dep.dep_kinds).map(move |kind| (dep, kind)));
433 for (dep_node, kind) in dependencies {
434 let &pkg = pkg_by_id.get(&dep_node.pkg).unwrap();
435 let dep = PackageDependency { name: dep_node.name.to_string(), pkg, kind };
436 packages[source].dependencies.push(dep);
437 }
438 packages[source]
439 .active_features
440 .extend(node.features.into_iter().map(|it| it.to_string()));
441 }
442
443 CargoWorkspace {
444 packages,
445 targets,
446 workspace_root,
447 target_directory,
448 manifest_path: ws_manifest_path,
449 is_virtual_workspace,
450 requires_rustc_private,
451 is_sysroot,
452 config_env: cargo_config_env,
453 }
454 }
455
456 pub fn packages(&self) -> impl ExactSizeIterator<Item = Package> + '_ {
457 self.packages.iter().map(|(id, _pkg)| id)
458 }
459
460 pub fn target_by_root(&self, root: &AbsPath) -> Option<Target> {
461 self.packages()
462 .filter(|&pkg| self[pkg].is_member)
463 .find_map(|pkg| self[pkg].targets.iter().find(|&&it| self[it].root == root))
464 .copied()
465 }
466
467 pub fn workspace_root(&self) -> &AbsPath {
468 &self.workspace_root
469 }
470
471 pub fn manifest_path(&self) -> &ManifestPath {
472 &self.manifest_path
473 }
474
475 pub fn target_directory(&self) -> &AbsPath {
476 &self.target_directory
477 }
478
479 pub fn package_flag(&self, package: &PackageData) -> String {
480 if self.is_unique(&package.name) {
481 package.name.clone()
482 } else {
483 format!("{}:{}", package.name, package.version)
484 }
485 }
486
487 pub fn parent_manifests(&self, manifest_path: &ManifestPath) -> Option<Vec<ManifestPath>> {
488 let mut found = false;
489 let parent_manifests = self
490 .packages()
491 .filter_map(|pkg| {
492 if !found && &self[pkg].manifest == manifest_path {
493 found = true
494 }
495 self[pkg].dependencies.iter().find_map(|dep| {
496 (&self[dep.pkg].manifest == manifest_path).then(|| self[pkg].manifest.clone())
497 })
498 })
499 .collect::<Vec<ManifestPath>>();
500
501 if !parent_manifests.is_empty() {
503 return Some(parent_manifests);
504 }
505
506 if found {
508 return Some(vec![
509 ManifestPath::try_from(self.workspace_root().join("Cargo.toml")).ok()?,
510 ]);
511 }
512
513 None
515 }
516
517 pub fn workspace_features(&self) -> FxHashSet<String> {
519 self.packages()
520 .filter_map(|package| {
521 let package = &self[package];
522 if package.is_member {
523 Some(package.features.keys().cloned().chain(
524 package.features.keys().map(|key| format!("{}/{key}", package.name)),
525 ))
526 } else {
527 None
528 }
529 })
530 .flatten()
531 .collect()
532 }
533
534 fn is_unique(&self, name: &str) -> bool {
535 self.packages.iter().filter(|(_, v)| v.name == name).count() == 1
536 }
537
538 pub fn is_virtual_workspace(&self) -> bool {
539 self.is_virtual_workspace
540 }
541
542 pub fn env(&self) -> &Env {
543 &self.config_env
544 }
545
546 pub fn is_sysroot(&self) -> bool {
547 self.is_sysroot
548 }
549
550 pub fn requires_rustc_private(&self) -> bool {
551 self.requires_rustc_private
552 }
553}
554
555pub(crate) struct FetchMetadata {
556 command: cargo_metadata::MetadataCommand,
557 #[expect(dead_code)]
558 manifest_path: ManifestPath,
559 lockfile_path: Option<Utf8PathBuf>,
560 #[expect(dead_code)]
561 kind: &'static str,
562 no_deps: bool,
563 no_deps_result: anyhow::Result<cargo_metadata::Metadata>,
564 other_options: Vec<String>,
565}
566
567impl FetchMetadata {
568 pub(crate) fn new(
578 cargo_toml: &ManifestPath,
579 current_dir: &AbsPath,
580 config: &CargoMetadataConfig,
581 sysroot: &Sysroot,
582 no_deps: bool,
583 ) -> Self {
584 let cargo = sysroot.tool(Tool::Cargo, current_dir, &config.extra_env);
585 let mut command = MetadataCommand::new();
586 command.cargo_path(cargo.get_program());
587 cargo.get_envs().for_each(|(var, val)| _ = command.env(var, val.unwrap_or_default()));
588 command.manifest_path(cargo_toml.to_path_buf());
589 match &config.features {
590 CargoFeatures::All => {
591 command.features(CargoOpt::AllFeatures);
592 }
593 CargoFeatures::Selected { features, no_default_features } => {
594 if *no_default_features {
595 command.features(CargoOpt::NoDefaultFeatures);
596 }
597 if !features.is_empty() {
598 command.features(CargoOpt::SomeFeatures(features.clone()));
599 }
600 }
601 }
602 command.current_dir(current_dir);
603
604 let mut other_options = vec![];
605 let mut extra_args = config.extra_args.iter();
609 while let Some(arg) = extra_args.next() {
610 if arg == "-Z"
611 && let Some(arg) = extra_args.next()
612 {
613 other_options.push("-Z".to_owned());
614 other_options.push(arg.to_owned());
615 }
616 }
617
618 let mut lockfile_path = None;
619 if cargo_toml.is_rust_manifest() {
620 other_options.push("-Zscript".to_owned());
621 } else if config
622 .toolchain_version
623 .as_ref()
624 .is_some_and(|v| *v >= MINIMUM_TOOLCHAIN_VERSION_SUPPORTING_LOCKFILE_PATH)
625 {
626 lockfile_path = Some(<_ as AsRef<Utf8Path>>::as_ref(cargo_toml).with_extension("lock"));
627 }
628
629 if !config.targets.is_empty() {
630 other_options.extend(
631 config.targets.iter().flat_map(|it| ["--filter-platform".to_owned(), it.clone()]),
632 );
633 }
634
635 command.other_options(other_options.clone());
636
637 let cargo_command;
644 let no_deps_result = if no_deps {
645 command.no_deps();
646 cargo_command = command.cargo_command();
647 command.exec()
648 } else {
649 let mut no_deps_command = command.clone();
650 no_deps_command.no_deps();
651 cargo_command = no_deps_command.cargo_command();
652 no_deps_command.exec()
653 }
654 .with_context(|| format!("Failed to run `{cargo_command:?}`"));
655
656 Self {
657 manifest_path: cargo_toml.clone(),
658 command,
659 lockfile_path,
660 kind: config.kind,
661 no_deps,
662 no_deps_result,
663 other_options,
664 }
665 }
666
667 pub(crate) fn no_deps_metadata(&self) -> Option<&cargo_metadata::Metadata> {
668 self.no_deps_result.as_ref().ok()
669 }
670
671 pub(crate) fn exec(
676 self,
677 target_dir: &Utf8Path,
678 locked: bool,
679 progress: &dyn Fn(String),
680 ) -> anyhow::Result<(cargo_metadata::Metadata, Option<anyhow::Error>)> {
681 _ = target_dir;
682 let Self {
683 mut command,
684 manifest_path: _,
685 lockfile_path,
686 kind: _,
687 no_deps,
688 no_deps_result,
689 mut other_options,
690 } = self;
691
692 if no_deps {
693 return no_deps_result.map(|m| (m, None));
694 }
695
696 let mut using_lockfile_copy = false;
697 let mut _temp_dir_guard;
698 if let Some(lockfile) = lockfile_path
699 && let Some((temp_dir, target_lockfile)) = make_lockfile_copy(&lockfile)
700 {
701 _temp_dir_guard = temp_dir;
702 other_options.push("--lockfile-path".to_owned());
703 other_options.push(target_lockfile.to_string());
704 using_lockfile_copy = true;
705 }
706 if using_lockfile_copy || other_options.iter().any(|it| it.starts_with("-Z")) {
707 command.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
708 other_options.push("-Zunstable-options".to_owned());
709 }
710 if !using_lockfile_copy && locked {
713 other_options.push("--locked".to_owned());
714 }
715 command.other_options(other_options);
716
717 progress("cargo metadata: started".to_owned());
718
719 let res = (|| -> anyhow::Result<(_, _)> {
720 let mut errored = false;
721 tracing::debug!("Running `{:?}`", command.cargo_command());
722 let output =
723 spawn_with_streaming_output(command.cargo_command(), &mut |_| (), &mut |line| {
724 errored = errored || line.starts_with("error") || line.starts_with("warning");
725 if errored {
726 progress("cargo metadata: ?".to_owned());
727 return;
728 }
729 progress(format!("cargo metadata: {line}"));
730 })?;
731 if !output.status.success() {
732 progress(format!("cargo metadata: failed {}", output.status));
733 let error = cargo_metadata::Error::CargoMetadata {
734 stderr: String::from_utf8(output.stderr)?,
735 }
736 .into();
737 if !no_deps {
738 if let Ok(metadata) = no_deps_result {
741 tracing::warn!(
742 ?error,
743 "`cargo metadata` failed and returning succeeded result with `--no-deps`"
744 );
745 return Ok((metadata, Some(error)));
746 }
747 }
748 return Err(error);
749 }
750 let stdout = from_utf8(&output.stdout)?
751 .lines()
752 .find(|line| line.starts_with('{'))
753 .ok_or(cargo_metadata::Error::NoJson)?;
754 Ok((cargo_metadata::MetadataCommand::parse(stdout)?, None))
755 })()
756 .with_context(|| format!("Failed to run `{:?}`", command.cargo_command()));
757 progress("cargo metadata: finished".to_owned());
758 res
759 }
760}