blob: 8129c11ef17ea53413e74e2531efe63576876a72 [file] [log] [blame]
//! Convert annotated metadata into a renderable context
pub(crate) mod crate_context;
mod platforms;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::config::CrateId;
use crate::context::platforms::resolve_cfg_platforms;
use crate::lockfile::Digest;
use crate::metadata::{Annotations, Dependency};
use crate::select::Select;
use crate::utils::target_triple::TargetTriple;
pub(crate) use self::crate_context::*;
/// A struct containing information about a Cargo dependency graph in an easily to consume
/// format for rendering reproducible Bazel targets.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Context {
/// The collective checksum of all inputs to the context
pub(crate) checksum: Option<Digest>,
/// The collection of all crates that make up the dependency graph
pub(crate) crates: BTreeMap<CrateId, CrateContext>,
/// A subset of only crates with binary targets
pub(crate) binary_crates: BTreeSet<CrateId>,
/// A subset of workspace members mapping to their workspace
/// path relative to the workspace root
pub(crate) workspace_members: BTreeMap<CrateId, String>,
/// A mapping of `cfg` flags to platform triples supporting the configuration
pub(crate) conditions: BTreeMap<String, BTreeSet<TargetTriple>>,
/// A list of crates visible to any bazel module.
pub(crate) direct_deps: BTreeSet<CrateId>,
/// A list of crates visible to this bazel module.
pub(crate) direct_dev_deps: BTreeSet<CrateId>,
}
impl Context {
pub(crate) fn try_from_path<T: AsRef<Path>>(path: T) -> Result<Self> {
let data = fs::read_to_string(path.as_ref())?;
Ok(serde_json::from_str(&data)?)
}
pub(crate) fn new(annotations: Annotations, sources_are_present: bool) -> anyhow::Result<Self> {
// Build a map of crate contexts
let crates: BTreeMap<CrateId, CrateContext> = annotations
.metadata
.crates
.values()
.map(|annotation| {
let context = CrateContext::new(
annotation,
&annotations.metadata.packages,
&annotations.lockfile.crates,
&annotations.pairred_extras,
&annotations.metadata.workspace_metadata.tree_metadata,
annotations.config.generate_binaries,
annotations.config.generate_build_scripts,
sources_are_present,
)?;
let id = CrateId::new(context.name.clone(), context.version.clone());
Ok::<_, anyhow::Error>((id, context))
})
.collect::<Result<_, _>>()?;
// Filter for any crate that contains a binary
let binary_crates: BTreeSet<CrateId> = crates
.iter()
.filter(|(_, ctx)| ctx.targets.iter().any(|t| matches!(t, Rule::Binary(..))))
// Only consider remote repositories (so non-workspace members).
.filter(|(_, ctx)| ctx.repository.is_some())
.map(|(id, _)| id.clone())
.collect();
// Given a list of all conditional dependencies, build a set of platform
// triples which satisfy the conditions.
let conditions = resolve_cfg_platforms(
crates.values().collect(),
&annotations.config.supported_platform_triples,
)?;
// Generate a list of all workspace members
let workspace_members = annotations
.metadata
.workspace_members
.iter()
.filter_map(|id| {
let pkg = &annotations.metadata.packages[id];
let package_path_id = match Self::get_package_path_id(
pkg,
&annotations.metadata.workspace_root,
&annotations.metadata.workspace_metadata.workspace_prefix,
&annotations.metadata.workspace_metadata.package_prefixes,
) {
Ok(id) => id,
Err(e) => return Some(Err(e)),
};
let crate_id = CrateId::from(pkg);
// Crates that have repository information are not considered workspace members.
// The assumpion is that they are "extra workspace members".
match crates[&crate_id].repository {
Some(_) => None,
None => Some(Ok((crate_id, package_path_id))),
}
})
.collect::<Result<BTreeMap<CrateId, String>>>()?;
let add_crate_ids = |crates: &mut BTreeSet<CrateId>,
deps: &Select<BTreeSet<Dependency>>| {
for dep in deps.values() {
crates.insert(CrateId::from(
&annotations.metadata.packages[&dep.package_id],
));
}
};
let mut direct_deps: BTreeSet<CrateId> = BTreeSet::new();
let mut direct_dev_deps: BTreeSet<CrateId> = BTreeSet::new();
for id in &annotations.metadata.workspace_members {
let deps = &annotations.metadata.crates[id].deps;
add_crate_ids(&mut direct_deps, &deps.normal_deps);
add_crate_ids(&mut direct_deps, &deps.proc_macro_deps);
add_crate_ids(&mut direct_deps, &deps.build_deps);
add_crate_ids(&mut direct_deps, &deps.build_link_deps);
add_crate_ids(&mut direct_deps, &deps.build_proc_macro_deps);
add_crate_ids(&mut direct_dev_deps, &deps.normal_dev_deps);
add_crate_ids(&mut direct_dev_deps, &deps.proc_macro_dev_deps);
}
Ok(Self {
checksum: None,
crates,
binary_crates,
workspace_members,
conditions,
direct_dev_deps: direct_dev_deps.difference(&direct_deps).cloned().collect(),
direct_deps,
})
}
// A helper function for locating the unique path in a workspace to a workspace member
fn get_package_path_id(
package: &cargo_metadata::Package,
workspace_root: &Path,
workspace_prefix: &Option<String>,
package_prefixes: &BTreeMap<String, String>,
) -> Result<String> {
// Locate the package's manifest directory
let manifest_dir = package
.manifest_path
.parent()
.expect("Every manifest should have a parent")
.as_std_path();
// Compare it with the root of the workspace
let package_path_diff = pathdiff::diff_paths(manifest_dir, workspace_root)
.expect("Every workspace member's manifest is a child of the workspace root");
// Ensure the package paths are adjusted in the macros according to the splicing results
let package_path = match package_prefixes.get(&package.name) {
// Any package prefix should be absolute and therefore always applied
Some(prefix) => PathBuf::from(prefix).join(package_path_diff),
// If no package prefix is present, attempt to apply the workspace prefix
// since workspace members would not have shown up with their own label
None => match workspace_prefix {
Some(prefix) => PathBuf::from(prefix).join(package_path_diff),
None => package_path_diff,
},
};
// Sanitize the path for increased consistency
let package_path_id = package_path
.display()
.to_string()
.replace('\\', "/")
.trim_matches('/')
.to_owned();
Ok(package_path_id)
}
/// Create a set of all direct dependencies of workspace member crates.
pub(crate) fn workspace_member_deps(&self) -> BTreeSet<CrateDependency> {
self.workspace_members
.keys()
.map(move |id| &self.crates[id])
.flat_map(|ctx| {
IntoIterator::into_iter([
&ctx.common_attrs.deps,
&ctx.common_attrs.deps_dev,
&ctx.common_attrs.proc_macro_deps,
&ctx.common_attrs.proc_macro_deps_dev,
])
.flat_map(|deps| deps.values())
})
.collect()
}
pub(crate) fn has_duplicate_workspace_member_dep(&self, dep: &CrateDependency) -> bool {
1 < self
.workspace_member_deps()
.into_iter()
.filter(|check| check.id.name == dep.id.name && check.alias == dep.alias)
.count()
}
pub(crate) fn has_duplicate_binary_crate(&self, bin: &CrateId) -> bool {
1 < self
.binary_crates
.iter()
.filter(|check| check.name == bin.name)
.count()
}
}
#[cfg(test)]
mod test {
use super::*;
use semver::Version;
use crate::config::Config;
fn mock_context_common() -> Context {
let annotations = Annotations::new(
crate::test::metadata::common(),
crate::test::lockfile::common(),
Config::default(),
)
.unwrap();
Context::new(annotations, false).unwrap()
}
fn mock_context_aliases() -> Context {
let annotations = Annotations::new(
crate::test::metadata::alias(),
crate::test::lockfile::alias(),
Config::default(),
)
.unwrap();
Context::new(annotations, false).unwrap()
}
#[test]
fn workspace_member_deps_collection() {
let context = mock_context_common();
let workspace_member_deps = context.workspace_member_deps();
assert_eq! {
workspace_member_deps
.iter()
.map(|dep| (&dep.id, context.has_duplicate_workspace_member_dep(dep)))
.collect::<Vec<_>>(),
[
(&CrateId::new("bitflags".to_owned(), Version::new(1, 3, 2)), false),
(&CrateId::new("cfg-if".to_owned(), Version::new(1, 0, 0)), false),
],
}
}
#[test]
fn workspace_member_deps_with_aliases() {
let context = mock_context_aliases();
let workspace_member_deps = context.workspace_member_deps();
assert_eq! {
workspace_member_deps
.iter()
.map(|dep| (&dep.id, context.has_duplicate_workspace_member_dep(dep)))
.collect::<Vec<_>>(),
[
(&CrateId::new("log".to_owned(), Version::new(0, 3, 9)), false),
(&CrateId::new("log".to_owned(), Version::new(0, 4, 21)), false),
(&CrateId::new("names".to_owned(), Version::parse("0.12.1-dev").unwrap()), false),
(&CrateId::new("names".to_owned(), Version::new(0, 13, 0)), false),
(&CrateId::new("surrealdb".to_owned(), Version::new(1, 3, 1)), false),
(&CrateId::new("value-bag".to_owned(), Version::parse("1.0.0-alpha.7").unwrap()), false),
],
}
}
#[test]
fn serialization() {
let context = mock_context_aliases();
// Serialize and deserialize the context object
let json_text = serde_json::to_string(&context).unwrap();
let deserialized_context: Context = serde_json::from_str(&json_text).unwrap();
// The data should be identical
assert_eq!(context, deserialized_context);
}
}