project_model/
cargo_workspace.rs

1//! See [`CargoWorkspace`].
2
3use 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/// [`CargoWorkspace`] represents the logical structure of, well, a Cargo
32/// workspace. It pretty closely mirrors `cargo metadata` output.
33///
34/// Note that internally, rust-analyzer uses a different structure:
35/// `CrateGraph`. `CrateGraph` is lower-level: it knows only about the crates,
36/// while this knows about `Packages` & `Targets`: purely cargo-related
37/// concepts.
38///
39/// We use absolute paths here, `cargo metadata` guarantees to always produce
40/// abs paths.
41#[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    /// Whether this workspace represents the sysroot workspace.
50    is_sysroot: bool,
51    /// Environment variables set in the `.cargo/config` file.
52    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/// Describes how to set the rustc source directory.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub enum RustLibSource {
73    /// Explicit path for the rustc source directory.
74    Path(AbsPathBuf),
75    /// Try to automatically detect where the rustc source directory is.
76    Discover,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum CargoFeatures {
81    All,
82    Selected {
83        /// List of features to activate.
84        features: Vec<String>,
85        /// Do not activate the `default` feature.
86        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    /// Whether to pass `--all-targets` to cargo invocations.
99    pub all_targets: bool,
100    /// List of features to activate.
101    pub features: CargoFeatures,
102    /// rustc target
103    pub target: Option<String>,
104    /// Sysroot loading behavior
105    pub sysroot: Option<RustLibSource>,
106    pub sysroot_src: Option<AbsPathBuf>,
107    /// rustc private crate source
108    pub rustc_source: Option<RustLibSource>,
109    /// Extra includes to add to the VFS.
110    pub extra_includes: Vec<AbsPathBuf>,
111    pub cfg_overrides: CfgOverrides,
112    /// Invoke `cargo check` through the RUSTC_WRAPPER.
113    pub wrap_rustc_in_build_scripts: bool,
114    /// The command to run instead of `cargo check` for building build scripts.
115    pub run_build_script_command: Option<Vec<String>>,
116    /// Extra args to pass to the cargo command.
117    pub extra_args: Vec<String>,
118    /// Extra env vars to set when invoking the cargo command
119    pub extra_env: FxHashMap<String, Option<String>>,
120    pub invocation_strategy: InvocationStrategy,
121    /// Optional path to use instead of `target` when building
122    pub target_dir: Option<Utf8PathBuf>,
123    /// Gate `#[test]` behind `#[cfg(test)]`
124    pub set_test: bool,
125    /// Load the project without any dependencies
126    pub no_deps: bool,
127}
128
129pub type Package = Idx<PackageData>;
130
131pub type Target = Idx<TargetData>;
132
133/// Information associated with a cargo crate
134#[derive(Debug, Clone, Eq, PartialEq)]
135pub struct PackageData {
136    /// Version given in the `Cargo.toml`
137    pub version: semver::Version,
138    /// Name as given in the `Cargo.toml`
139    pub name: String,
140    /// Repository as given in the `Cargo.toml`
141    pub repository: Option<String>,
142    /// Path containing the `Cargo.toml`
143    pub manifest: ManifestPath,
144    /// Targets provided by the crate (lib, bin, example, test, ...)
145    pub targets: Vec<Target>,
146    /// Does this package come from the local filesystem (and is editable)?
147    pub is_local: bool,
148    /// Whether this package is a member of the workspace
149    pub is_member: bool,
150    /// List of packages this package depends on
151    pub dependencies: Vec<PackageDependency>,
152    /// Rust edition for this package
153    pub edition: Edition,
154    /// Features provided by the crate, mapped to the features required by that feature.
155    pub features: FxHashMap<String, Vec<String>>,
156    /// List of features enabled on this package
157    pub active_features: Vec<String>,
158    /// String representation of package id
159    pub id: String,
160    /// Authors as given in the `Cargo.toml`
161    pub authors: Vec<String>,
162    /// Description as given in the `Cargo.toml`
163    pub description: Option<String>,
164    /// Homepage as given in the `Cargo.toml`
165    pub homepage: Option<String>,
166    /// License as given in the `Cargo.toml`
167    pub license: Option<String>,
168    /// License file as given in the `Cargo.toml`
169    pub license_file: Option<Utf8PathBuf>,
170    /// Readme file as given in the `Cargo.toml`
171    pub readme: Option<Utf8PathBuf>,
172    /// Rust version as given in the `Cargo.toml`
173    pub rust_version: Option<semver::Version>,
174    /// The contents of [package.metadata.rust-analyzer]
175    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    /// Available to the library, binary, and dev targets in the package (but not the build script).
193    Normal,
194    /// Available only to test and bench targets (and the library target, when built with `cfg(test)`).
195    Dev,
196    /// Available only to the build script target.
197    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/// Information associated with a package's target
219#[derive(Debug, Clone, Eq, PartialEq)]
220pub struct TargetData {
221    /// Package that provided this target
222    pub package: Package,
223    /// Name as given in the `Cargo.toml` or generated from the file name
224    pub name: String,
225    /// Path to the main source file of the target
226    pub root: AbsPathBuf,
227    /// Kind of target
228    pub kind: TargetKind,
229    /// Required features of the target without which it won't build
230    pub required_features: Vec<String>,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum TargetKind {
235    Bin,
236    /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
237    Lib {
238        /// Is this target a proc-macro
239        is_proc_macro: bool,
240    },
241    Example,
242    Test,
243    Bench,
244    /// Cargo calls this kind `custom-build`
245    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    /// If this is a valid cargo target, returns the name cargo uses in command line arguments
279    /// and output, otherwise None.
280    /// https://docs.rs/cargo_metadata/latest/cargo_metadata/enum.TargetKind.html
281    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    /// List of features to activate.
298    pub features: CargoFeatures,
299    /// rustc targets
300    pub targets: Vec<String>,
301    /// Extra args to pass to the cargo command.
302    pub extra_args: Vec<String>,
303    /// Extra env vars to set when invoking the cargo command
304    pub extra_env: FxHashMap<String, Option<String>>,
305    /// What kind of metadata are we fetching: workspace, rustc, or sysroot.
306    pub kind: &'static str,
307    /// The toolchain version, if known.
308    /// Used to conditionally enable unstable cargo features.
309    pub toolchain_version: Option<semver::Version>,
310}
311
312// Deserialize helper for the cargo metadata
313#[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            // We treat packages without source as "local" packages. That includes all members of
371            // the current workspace, as well as any path dependency outside the workspace.
372            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                        // cargo strips the script part of a cargo script away and places the
413                        // modified manifest file into a special target dir which is then used as
414                        // the source path. We don't want that, we want the original here so map it
415                        // back
416                        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        // some packages has this pkg as dep. return their manifests
502        if !parent_manifests.is_empty() {
503            return Some(parent_manifests);
504        }
505
506        // this pkg is inside this cargo workspace, fallback to workspace root
507        if found {
508            return Some(vec![
509                ManifestPath::try_from(self.workspace_root().join("Cargo.toml")).ok()?,
510            ]);
511        }
512
513        // not in this workspace
514        None
515    }
516
517    /// Returns the union of the features of all member crates in this workspace.
518    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    /// Builds a command to fetch metadata for the given `cargo_toml` manifest.
569    ///
570    /// Performs a lightweight pre-fetch using the `--no-deps` option,
571    /// available via [`FetchMetadata::no_deps_metadata`], to gather basic
572    /// information such as the `target-dir`.
573    ///
574    /// The provided sysroot is used to set the `RUSTUP_TOOLCHAIN`
575    /// environment variable when invoking Cargo, ensuring that the
576    /// rustup proxy selects the correct toolchain.
577    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        // cargo metadata only supports a subset of flags of what cargo usually accepts, and usually
606        // the only relevant flags for metadata here are unstable ones, so we pass those along
607        // but nothing else
608        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        // Pre-fetch basic metadata using `--no-deps`, which:
638        // - avoids fetching registries like crates.io,
639        // - skips dependency resolution and does not modify lockfiles,
640        // - and thus doesn't require progress reporting or copying lockfiles.
641        //
642        // Useful as a fast fallback to extract info like `target-dir`.
643        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    /// Executes the metadata-fetching command.
672    ///
673    /// A successful result may still contain a metadata error if the full fetch failed,
674    /// but the fallback `--no-deps` pre-fetch succeeded during command construction.
675    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        // No need to lock it if we copied the lockfile, we won't modify the original after all/
711        // This way cargo cannot error out on us if the lockfile requires updating.
712        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 we failed to fetch metadata with deps, return pre-fetched result without them.
739                    // This makes r-a still work partially when offline.
740                    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}