| use crate::compiler::artifact::match_artifacts_kind_with_targets; |
| use crate::compiler::{CompileKind, CompileKindFallback, RustcTargetData}; |
| use crate::ops::{self, Packages}; |
| use crate::resolver::{HasDevUnits, Resolve, features::CliFeatures}; |
| use crate::util::CargoResult; |
| use crate::util::interning::InternedString; |
| use crate::workspace::dependency::DepKind; |
| use crate::workspace::package::SerializedPackage; |
| use crate::workspace::{Package, PackageId, PackageIdSpec, Workspace}; |
| use cargo_platform::Platform; |
| use serde::Serialize; |
| use std::collections::BTreeMap; |
| use std::path::PathBuf; |
| |
| const VERSION: u32 = 1; |
| |
| pub struct OutputMetadataOptions { |
| pub cli_features: CliFeatures, |
| pub no_deps: bool, |
| pub version: u32, |
| pub filter_platforms: Vec<String>, |
| } |
| |
| /// Loads the manifest, resolves the dependencies of the package to the concrete |
| /// used versions - considering overrides - and writes all dependencies in a JSON |
| /// format to stdout. |
| pub fn output_metadata(ws: &Workspace<'_>, opt: &OutputMetadataOptions) -> CargoResult<ExportInfo> { |
| if opt.version != VERSION { |
| anyhow::bail!( |
| "metadata version {} not supported, only {} is currently supported", |
| opt.version, |
| VERSION |
| ); |
| } |
| let (packages, resolve) = if opt.no_deps { |
| let packages = ws |
| .members() |
| .map(|pkg| pkg.serialized(ws.gctx().cli_unstable(), ws.unstable_features())) |
| .collect(); |
| (packages, None) |
| } else { |
| let (packages, resolve) = build_resolve_graph(ws, opt)?; |
| (packages, Some(resolve)) |
| }; |
| |
| Ok(ExportInfo { |
| packages, |
| workspace_members: ws.members().map(|pkg| pkg.package_id().to_spec()).collect(), |
| workspace_default_members: ws |
| .default_members() |
| .map(|pkg| pkg.package_id().to_spec()) |
| .collect(), |
| resolve, |
| target_directory: ws.target_dir().into_path_unlocked(), |
| build_directory: ws.build_dir().into_path_unlocked(), |
| version: VERSION, |
| workspace_root: ws.root().to_path_buf(), |
| metadata: ws.custom_metadata().cloned(), |
| }) |
| } |
| |
| /// This is the structure that is serialized and displayed to the user. |
| /// |
| /// See cargo-metadata.adoc for detailed documentation of the format. |
| #[derive(Serialize)] |
| pub struct ExportInfo { |
| packages: Vec<SerializedPackage>, |
| workspace_members: Vec<PackageIdSpec>, |
| workspace_default_members: Vec<PackageIdSpec>, |
| resolve: Option<MetadataResolve>, |
| target_directory: PathBuf, |
| build_directory: PathBuf, |
| version: u32, |
| workspace_root: PathBuf, |
| metadata: Option<toml::Value>, |
| } |
| |
| #[derive(Serialize)] |
| struct MetadataResolve { |
| nodes: Vec<MetadataResolveNode>, |
| root: Option<PackageIdSpec>, |
| } |
| |
| #[derive(Serialize)] |
| struct MetadataResolveNode { |
| id: PackageIdSpec, |
| dependencies: Vec<PackageIdSpec>, |
| deps: Vec<Dep>, |
| features: Vec<InternedString>, |
| } |
| |
| #[derive(Serialize)] |
| struct Dep { |
| // TODO(bindeps): after -Zbindeps gets stabilized, |
| // mark this field as deprecated in the help manual of cargo-metadata |
| name: InternedString, |
| pkg: PackageIdSpec, |
| #[serde(skip)] |
| pkg_id: PackageId, |
| dep_kinds: Vec<DepKindInfo>, |
| } |
| |
| #[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)] |
| struct DepKindInfo { |
| kind: DepKind, |
| target: Option<Platform>, |
| |
| // vvvvv The fields below are introduced for `-Z bindeps`. |
| /// What the manifest calls the crate. |
| /// |
| /// A renamed dependency will show the rename instead of original name. |
| // TODO(bindeps): Remove `Option` after -Zbindeps get stabilized. |
| #[serde(skip_serializing_if = "Option::is_none")] |
| extern_name: Option<InternedString>, |
| /// Artifact's crate type, e.g. staticlib, cdylib, bin... |
| #[serde(skip_serializing_if = "Option::is_none")] |
| artifact: Option<&'static str>, |
| /// Equivalent to `{ target = "…" }` in an artifact dependency requirement. |
| /// |
| /// * If the target points to a custom target JSON file, the path will be absolute. |
| /// * If the target is a build assumed target `{ target = "target" }`, it will show as `<target>`. |
| #[serde(skip_serializing_if = "Option::is_none")] |
| compile_target: Option<InternedString>, |
| /// Executable name for an artifact binary dependency. |
| #[serde(skip_serializing_if = "Option::is_none")] |
| bin_name: Option<String>, |
| // ^^^^^ The fields above are introduced for `-Z bindeps`. |
| } |
| |
| /// Builds the resolve graph as it will be displayed to the user. |
| fn build_resolve_graph( |
| ws: &Workspace<'_>, |
| metadata_opts: &OutputMetadataOptions, |
| ) -> CargoResult<(Vec<SerializedPackage>, MetadataResolve)> { |
| // TODO: Without --filter-platform, features are being resolved for `host` only. |
| // How should this work? |
| // |
| // Otherwise note that "just host" is used as the fallback here if |
| // `filter_platforms` is empty to intentionally avoid reading |
| // `$CARGO_BUILD_TARGET` (or `build.target`) which makes sense for other |
| // subcommands like `cargo build` but does not fit with this command. |
| let requested_kinds = CompileKind::from_requested_targets_with_fallback( |
| ws.gctx(), |
| &metadata_opts.filter_platforms, |
| CompileKindFallback::JustHost, |
| )?; |
| let mut target_data = RustcTargetData::new(ws, &requested_kinds)?; |
| // Resolve entire workspace. |
| let specs = Packages::All(Vec::new()).to_package_id_specs(ws)?; |
| let force_all = if metadata_opts.filter_platforms.is_empty() { |
| crate::resolver::features::ForceAllTargets::Yes |
| } else { |
| crate::resolver::features::ForceAllTargets::No |
| }; |
| |
| // Note that even with --filter-platform we end up downloading host dependencies as well, |
| // as that is the behavior of download_accessible. |
| let dry_run = false; |
| let ws_resolve = ops::resolve_ws_with_opts( |
| ws, |
| &mut target_data, |
| &requested_kinds, |
| &metadata_opts.cli_features, |
| &specs, |
| HasDevUnits::Yes, |
| force_all, |
| dry_run, |
| )?; |
| |
| let package_map: BTreeMap<PackageId, Package> = ws_resolve |
| .pkg_set |
| .packages() |
| // This is a little lazy, but serde doesn't handle Rc fields very well. |
| .map(|pkg| (pkg.package_id(), Package::clone(pkg))) |
| .collect(); |
| |
| // Start from the workspace roots, and recurse through filling out the |
| // map, filtering targets as necessary. |
| let mut node_map = BTreeMap::new(); |
| for member_pkg in ws.members() { |
| build_resolve_graph_r( |
| &mut node_map, |
| member_pkg.package_id(), |
| &ws_resolve.targeted_resolve, |
| &package_map, |
| &target_data, |
| &requested_kinds, |
| )?; |
| } |
| // Get a Vec of Packages. |
| let actual_packages = package_map |
| .into_iter() |
| .filter_map(|(pkg_id, pkg)| node_map.get(&pkg_id).map(|_| pkg)) |
| .map(|pkg| pkg.serialized(ws.gctx().cli_unstable(), ws.unstable_features())) |
| .collect(); |
| |
| let mr = MetadataResolve { |
| nodes: node_map.into_iter().map(|(_pkg_id, node)| node).collect(), |
| root: ws.current_opt().map(|pkg| pkg.package_id().to_spec()), |
| }; |
| Ok((actual_packages, mr)) |
| } |
| |
| fn build_resolve_graph_r( |
| node_map: &mut BTreeMap<PackageId, MetadataResolveNode>, |
| pkg_id: PackageId, |
| resolve: &Resolve, |
| package_map: &BTreeMap<PackageId, Package>, |
| target_data: &RustcTargetData<'_>, |
| requested_kinds: &[CompileKind], |
| ) -> CargoResult<()> { |
| if node_map.contains_key(&pkg_id) { |
| return Ok(()); |
| } |
| // This normalizes the IDs so that they are consistent between the |
| // `packages` array and the `resolve` map. This is a bit of a hack to |
| // compensate for the fact that |
| // SourceKind::Git(GitReference::Branch("master")) is the same as |
| // SourceKind::Git(GitReference::DefaultBranch). We want IDs in the JSON |
| // to be opaque, and compare with basic string equality, so this will |
| // always prefer the style of ID in the Package instead of the resolver. |
| // Cargo generally only exposes PackageIds from the Package struct, and |
| // AFAIK this is the only place where the resolver variant is exposed. |
| // |
| // This diverges because the SourceIds created for Packages are built |
| // based on the Dependency declaration, but the SourceIds in the resolver |
| // are deserialized from Cargo.lock. Cargo.lock may have been generated by |
| // an older (or newer!) version of Cargo which uses a different style. |
| let normalize_id = |id| -> PackageId { *package_map.get_key_value(&id).unwrap().0 }; |
| let features = resolve.features(pkg_id).to_vec(); |
| |
| let deps = { |
| let mut dep_metadatas = Vec::new(); |
| let iter = resolve.deps(pkg_id).filter(|(_dep_id, deps)| { |
| if requested_kinds == [CompileKind::Host] { |
| true |
| } else { |
| requested_kinds.iter().any(|kind| { |
| deps.iter() |
| .any(|dep| target_data.dep_platform_activated(dep, *kind)) |
| }) |
| } |
| }); |
| for (dep_id, deps) in iter { |
| let mut dep_kinds = Vec::new(); |
| |
| let targets = package_map[&dep_id].targets(); |
| |
| // Try to get the extern name for lib, or crate name for bins. |
| let extern_name = |target| { |
| resolve |
| .extern_crate_name_and_dep_name(pkg_id, dep_id, target) |
| .map(|(ext_crate_name, _)| ext_crate_name) |
| }; |
| |
| let lib_target = targets.iter().find(|t| t.is_lib()); |
| |
| for dep in deps.iter() { |
| if let Some(target) = lib_target { |
| // When we do have a library target, include them in deps if... |
| let included = match dep.artifact() { |
| // it is not an artifact dep at all |
| None => true, |
| // it is also an artifact dep with `{ …, lib = true }` |
| Some(a) if a.is_lib() => true, |
| _ => false, |
| }; |
| // TODO(bindeps): Cargo shouldn't have `extern_name` field |
| // if the user is not using -Zbindeps. |
| // Remove this condition ` after -Zbindeps gets stabilized. |
| let extern_name = if dep.artifact().is_some() { |
| Some(extern_name(target)?) |
| } else { |
| None |
| }; |
| if included { |
| dep_kinds.push(DepKindInfo { |
| kind: dep.kind(), |
| target: dep.platform().cloned(), |
| extern_name, |
| artifact: None, |
| compile_target: None, |
| bin_name: None, |
| }); |
| } |
| } |
| |
| // No need to proceed if there is no artifact dependency. |
| let Some(artifact_requirements) = dep.artifact() else { |
| continue; |
| }; |
| |
| let compile_target = match artifact_requirements.target() { |
| Some(t) => t |
| .to_compile_target() |
| .map(|t| t.rustc_target()) |
| // Given that Cargo doesn't know which target it should resolve to, |
| // when an artifact dep is specified with { target = "target" }, |
| // keep it with a special "<target>" string, |
| .or_else(|| Some("<target>".into())), |
| None => None, |
| }; |
| |
| let target_set = |
| match_artifacts_kind_with_targets(dep, targets, pkg_id.name().as_str())?; |
| dep_kinds.reserve(target_set.len()); |
| for (kind, target) in target_set.into_iter() { |
| dep_kinds.push(DepKindInfo { |
| kind: dep.kind(), |
| target: dep.platform().cloned(), |
| extern_name: extern_name(target).ok(), |
| artifact: Some(kind.crate_type()), |
| compile_target, |
| bin_name: target.is_bin().then(|| target.name().to_string()), |
| }) |
| } |
| } |
| |
| dep_kinds.sort(); |
| |
| let pkg_id = normalize_id(dep_id); |
| |
| let dep = match (lib_target, dep_kinds.len()) { |
| (Some(target), _) => Dep { |
| name: extern_name(target)?, |
| pkg: pkg_id.to_spec(), |
| pkg_id, |
| dep_kinds, |
| }, |
| // No lib target exists but contains artifact deps. |
| (None, 1..) => Dep { |
| name: "".into(), |
| pkg: pkg_id.to_spec(), |
| pkg_id, |
| dep_kinds, |
| }, |
| // No lib or artifact dep exists. |
| // Usually this mean parent depending on non-lib bin crate. |
| (None, _) => continue, |
| }; |
| |
| dep_metadatas.push(dep) |
| } |
| dep_metadatas |
| }; |
| |
| let to_visit: Vec<PackageId> = deps.iter().map(|dep| dep.pkg_id).collect(); |
| let node = MetadataResolveNode { |
| id: normalize_id(pkg_id).to_spec(), |
| dependencies: to_visit.iter().map(|id| id.to_spec()).collect(), |
| deps, |
| features, |
| }; |
| node_map.insert(pkg_id, node); |
| for dep_id in to_visit { |
| build_resolve_graph_r( |
| node_map, |
| dep_id, |
| resolve, |
| package_map, |
| target_data, |
| requested_kinds, |
| )?; |
| } |
| |
| Ok(()) |
| } |