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