cargo/ops/
cargo_output_metadata.rs

1use crate::core::compiler::artifact::match_artifacts_kind_with_targets;
2use crate::core::compiler::{CompileKind, CompileKindFallback, RustcTargetData};
3use crate::core::dependency::DepKind;
4use crate::core::package::SerializedPackage;
5use crate::core::resolver::{features::CliFeatures, HasDevUnits, Resolve};
6use crate::core::{Package, PackageId, PackageIdSpec, Workspace};
7use crate::ops::{self, Packages};
8use crate::util::interning::InternedString;
9use crate::util::CargoResult;
10use cargo_platform::Platform;
11use serde::Serialize;
12use std::collections::BTreeMap;
13use std::path::PathBuf;
14
15const VERSION: u32 = 1;
16
17pub struct OutputMetadataOptions {
18    pub cli_features: CliFeatures,
19    pub no_deps: bool,
20    pub version: u32,
21    pub filter_platforms: Vec<String>,
22}
23
24/// Loads the manifest, resolves the dependencies of the package to the concrete
25/// used versions - considering overrides - and writes all dependencies in a JSON
26/// format to stdout.
27pub fn output_metadata(ws: &Workspace<'_>, opt: &OutputMetadataOptions) -> CargoResult<ExportInfo> {
28    if opt.version != VERSION {
29        anyhow::bail!(
30            "metadata version {} not supported, only {} is currently supported",
31            opt.version,
32            VERSION
33        );
34    }
35    let (packages, resolve) = if opt.no_deps {
36        let packages = ws
37            .members()
38            .map(|pkg| pkg.serialized(ws.gctx().cli_unstable(), ws.unstable_features()))
39            .collect();
40        (packages, None)
41    } else {
42        let (packages, resolve) = build_resolve_graph(ws, opt)?;
43        (packages, Some(resolve))
44    };
45
46    Ok(ExportInfo {
47        packages,
48        workspace_members: ws.members().map(|pkg| pkg.package_id().to_spec()).collect(),
49        workspace_default_members: ws
50            .default_members()
51            .map(|pkg| pkg.package_id().to_spec())
52            .collect(),
53        resolve,
54        target_directory: ws.target_dir().into_path_unlocked(),
55        build_directory: ws
56            .gctx()
57            .cli_unstable()
58            .build_dir
59            .then(|| ws.build_dir().into_path_unlocked()),
60        version: VERSION,
61        workspace_root: ws.root().to_path_buf(),
62        metadata: ws.custom_metadata().cloned(),
63    })
64}
65
66/// This is the structure that is serialized and displayed to the user.
67///
68/// See cargo-metadata.adoc for detailed documentation of the format.
69#[derive(Serialize)]
70pub struct ExportInfo {
71    packages: Vec<SerializedPackage>,
72    workspace_members: Vec<PackageIdSpec>,
73    workspace_default_members: Vec<PackageIdSpec>,
74    resolve: Option<MetadataResolve>,
75    target_directory: PathBuf,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    build_directory: Option<PathBuf>,
78    version: u32,
79    workspace_root: PathBuf,
80    metadata: Option<toml::Value>,
81}
82
83#[derive(Serialize)]
84struct MetadataResolve {
85    nodes: Vec<MetadataResolveNode>,
86    root: Option<PackageIdSpec>,
87}
88
89#[derive(Serialize)]
90struct MetadataResolveNode {
91    id: PackageIdSpec,
92    dependencies: Vec<PackageIdSpec>,
93    deps: Vec<Dep>,
94    features: Vec<InternedString>,
95}
96
97#[derive(Serialize)]
98struct Dep {
99    // TODO(bindeps): after -Zbindeps gets stabilized,
100    // mark this field as deprecated in the help manual of cargo-metadata
101    name: InternedString,
102    pkg: PackageIdSpec,
103    #[serde(skip)]
104    pkg_id: PackageId,
105    dep_kinds: Vec<DepKindInfo>,
106}
107
108#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
109struct DepKindInfo {
110    kind: DepKind,
111    target: Option<Platform>,
112
113    // vvvvv The fields below are introduced for `-Z bindeps`.
114    /// What the manifest calls the crate.
115    ///
116    /// A renamed dependency will show the rename instead of original name.
117    // TODO(bindeps): Remove `Option` after -Zbindeps get stabilized.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    extern_name: Option<InternedString>,
120    /// Artifact's crate type, e.g. staticlib, cdylib, bin...
121    #[serde(skip_serializing_if = "Option::is_none")]
122    artifact: Option<&'static str>,
123    /// Equivalent to `{ target = "…" }` in an artifact dependency requirement.
124    ///
125    /// * If the target points to a custom target JSON file, the path will be absolute.
126    /// * If the target is a build assumed target `{ target = "target" }`, it will show as `<target>`.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    compile_target: Option<InternedString>,
129    /// Executable name for an artifact binary dependency.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    bin_name: Option<String>,
132    // ^^^^^ The fields above are introduced for `-Z bindeps`.
133}
134
135/// Builds the resolve graph as it will be displayed to the user.
136fn build_resolve_graph(
137    ws: &Workspace<'_>,
138    metadata_opts: &OutputMetadataOptions,
139) -> CargoResult<(Vec<SerializedPackage>, MetadataResolve)> {
140    // TODO: Without --filter-platform, features are being resolved for `host` only.
141    // How should this work?
142    //
143    // Otherwise note that "just host" is used as the fallback here if
144    // `filter_platforms` is empty to intentionally avoid reading
145    // `$CARGO_BUILD_TARGET` (or `build.target`) which makes sense for other
146    // subcommands like `cargo build` but does not fit with this command.
147    let requested_kinds = CompileKind::from_requested_targets_with_fallback(
148        ws.gctx(),
149        &metadata_opts.filter_platforms,
150        CompileKindFallback::JustHost,
151    )?;
152    let mut target_data = RustcTargetData::new(ws, &requested_kinds)?;
153    // Resolve entire workspace.
154    let specs = Packages::All(Vec::new()).to_package_id_specs(ws)?;
155    let force_all = if metadata_opts.filter_platforms.is_empty() {
156        crate::core::resolver::features::ForceAllTargets::Yes
157    } else {
158        crate::core::resolver::features::ForceAllTargets::No
159    };
160
161    // Note that even with --filter-platform we end up downloading host dependencies as well,
162    // as that is the behavior of download_accessible.
163    let dry_run = false;
164    let ws_resolve = ops::resolve_ws_with_opts(
165        ws,
166        &mut target_data,
167        &requested_kinds,
168        &metadata_opts.cli_features,
169        &specs,
170        HasDevUnits::Yes,
171        force_all,
172        dry_run,
173    )?;
174
175    let package_map: BTreeMap<PackageId, Package> = ws_resolve
176        .pkg_set
177        .packages()
178        // This is a little lazy, but serde doesn't handle Rc fields very well.
179        .map(|pkg| (pkg.package_id(), Package::clone(pkg)))
180        .collect();
181
182    // Start from the workspace roots, and recurse through filling out the
183    // map, filtering targets as necessary.
184    let mut node_map = BTreeMap::new();
185    for member_pkg in ws.members() {
186        build_resolve_graph_r(
187            &mut node_map,
188            member_pkg.package_id(),
189            &ws_resolve.targeted_resolve,
190            &package_map,
191            &target_data,
192            &requested_kinds,
193        )?;
194    }
195    // Get a Vec of Packages.
196    let actual_packages = package_map
197        .into_iter()
198        .filter_map(|(pkg_id, pkg)| node_map.get(&pkg_id).map(|_| pkg))
199        .map(|pkg| pkg.serialized(ws.gctx().cli_unstable(), ws.unstable_features()))
200        .collect();
201
202    let mr = MetadataResolve {
203        nodes: node_map.into_iter().map(|(_pkg_id, node)| node).collect(),
204        root: ws.current_opt().map(|pkg| pkg.package_id().to_spec()),
205    };
206    Ok((actual_packages, mr))
207}
208
209fn build_resolve_graph_r(
210    node_map: &mut BTreeMap<PackageId, MetadataResolveNode>,
211    pkg_id: PackageId,
212    resolve: &Resolve,
213    package_map: &BTreeMap<PackageId, Package>,
214    target_data: &RustcTargetData<'_>,
215    requested_kinds: &[CompileKind],
216) -> CargoResult<()> {
217    if node_map.contains_key(&pkg_id) {
218        return Ok(());
219    }
220    // This normalizes the IDs so that they are consistent between the
221    // `packages` array and the `resolve` map. This is a bit of a hack to
222    // compensate for the fact that
223    // SourceKind::Git(GitReference::Branch("master")) is the same as
224    // SourceKind::Git(GitReference::DefaultBranch). We want IDs in the JSON
225    // to be opaque, and compare with basic string equality, so this will
226    // always prefer the style of ID in the Package instead of the resolver.
227    // Cargo generally only exposes PackageIds from the Package struct, and
228    // AFAIK this is the only place where the resolver variant is exposed.
229    //
230    // This diverges because the SourceIds created for Packages are built
231    // based on the Dependency declaration, but the SourceIds in the resolver
232    // are deserialized from Cargo.lock. Cargo.lock may have been generated by
233    // an older (or newer!) version of Cargo which uses a different style.
234    let normalize_id = |id| -> PackageId { *package_map.get_key_value(&id).unwrap().0 };
235    let features = resolve.features(pkg_id).to_vec();
236
237    let deps = {
238        let mut dep_metadatas = Vec::new();
239        let iter = resolve.deps(pkg_id).filter(|(_dep_id, deps)| {
240            if requested_kinds == [CompileKind::Host] {
241                true
242            } else {
243                requested_kinds.iter().any(|kind| {
244                    deps.iter()
245                        .any(|dep| target_data.dep_platform_activated(dep, *kind))
246                })
247            }
248        });
249        for (dep_id, deps) in iter {
250            let mut dep_kinds = Vec::new();
251
252            let targets = package_map[&dep_id].targets();
253
254            // Try to get the extern name for lib, or crate name for bins.
255            let extern_name = |target| {
256                resolve
257                    .extern_crate_name_and_dep_name(pkg_id, dep_id, target)
258                    .map(|(ext_crate_name, _)| ext_crate_name)
259            };
260
261            let lib_target = targets.iter().find(|t| t.is_lib());
262
263            for dep in deps.iter() {
264                if let Some(target) = lib_target {
265                    // When we do have a library target, include them in deps if...
266                    let included = match dep.artifact() {
267                        // it is not an artifact dep at all
268                        None => true,
269                        // it is also an artifact dep with `{ …, lib = true }`
270                        Some(a) if a.is_lib() => true,
271                        _ => false,
272                    };
273                    // TODO(bindeps): Cargo shouldn't have `extern_name` field
274                    // if the user is not using -Zbindeps.
275                    // Remove this condition ` after -Zbindeps gets stabilized.
276                    let extern_name = if dep.artifact().is_some() {
277                        Some(extern_name(target)?)
278                    } else {
279                        None
280                    };
281                    if included {
282                        dep_kinds.push(DepKindInfo {
283                            kind: dep.kind(),
284                            target: dep.platform().cloned(),
285                            extern_name,
286                            artifact: None,
287                            compile_target: None,
288                            bin_name: None,
289                        });
290                    }
291                }
292
293                // No need to proceed if there is no artifact dependency.
294                let Some(artifact_requirements) = dep.artifact() else {
295                    continue;
296                };
297
298                let compile_target = match artifact_requirements.target() {
299                    Some(t) => t
300                        .to_compile_target()
301                        .map(|t| t.rustc_target())
302                        // Given that Cargo doesn't know which target it should resolve to,
303                        // when an artifact dep is specified with { target = "target" },
304                        // keep it with a special "<target>" string,
305                        .or_else(|| Some("<target>".into())),
306                    None => None,
307                };
308
309                let target_set =
310                    match_artifacts_kind_with_targets(dep, targets, pkg_id.name().as_str())?;
311                dep_kinds.reserve(target_set.len());
312                for (kind, target) in target_set.into_iter() {
313                    dep_kinds.push(DepKindInfo {
314                        kind: dep.kind(),
315                        target: dep.platform().cloned(),
316                        extern_name: extern_name(target).ok(),
317                        artifact: Some(kind.crate_type()),
318                        compile_target,
319                        bin_name: target.is_bin().then(|| target.name().to_string()),
320                    })
321                }
322            }
323
324            dep_kinds.sort();
325
326            let pkg_id = normalize_id(dep_id);
327
328            let dep = match (lib_target, dep_kinds.len()) {
329                (Some(target), _) => Dep {
330                    name: extern_name(target)?,
331                    pkg: pkg_id.to_spec(),
332                    pkg_id,
333                    dep_kinds,
334                },
335                // No lib target exists but contains artifact deps.
336                (None, 1..) => Dep {
337                    name: "".into(),
338                    pkg: pkg_id.to_spec(),
339                    pkg_id,
340                    dep_kinds,
341                },
342                // No lib or artifact dep exists.
343                // Usually this mean parent depending on non-lib bin crate.
344                (None, _) => continue,
345            };
346
347            dep_metadatas.push(dep)
348        }
349        dep_metadatas
350    };
351
352    let to_visit: Vec<PackageId> = deps.iter().map(|dep| dep.pkg_id).collect();
353    let node = MetadataResolveNode {
354        id: normalize_id(pkg_id).to_spec(),
355        dependencies: to_visit.iter().map(|id| id.to_spec()).collect(),
356        deps,
357        features,
358    };
359    node_map.insert(pkg_id, node);
360    for dep_id in to_visit {
361        build_resolve_graph_r(
362            node_map,
363            dep_id,
364            resolve,
365            package_map,
366            target_data,
367            requested_kinds,
368        )?;
369    }
370
371    Ok(())
372}