blob: 47dc994a7865cca96211dffb5af743e77494d2cc [file] [log] [blame]
//! Convert annotated metadata into a renderable context
pub 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::crate_context::{CrateContext, CrateDependency, Rule};
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 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 struct Context {
/// The collective checksum of all inputs to the context
pub checksum: Option<Digest>,
/// The collection of all crates that make up the dependency graph
pub crates: BTreeMap<CrateId, CrateContext>,
/// A subset of only crates with binary targets
pub binary_crates: BTreeSet<CrateId>,
/// A subset of workspace members mapping to their workspace
/// path relative to the workspace root
pub workspace_members: BTreeMap<CrateId, String>,
/// A mapping of `cfg` flags to platform triples supporting the configuration
pub conditions: BTreeMap<String, BTreeSet<TargetTriple>>,
/// A list of crates visible to any bazel module.
pub direct_deps: BTreeSet<CrateId>,
/// A list of crates visible to this bazel module.
pub direct_dev_deps: BTreeSet<CrateId>,
}
impl Context {
pub 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 fn new(annotations: Annotations) -> 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.crate_features,
annotations.config.generate_binaries,
annotations.config.generate_build_scripts,
);
let id = CrateId::new(context.name.clone(), context.version.clone());
(id, context)
})
.collect();
// 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 satsify 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 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 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 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 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).unwrap()
}
fn mock_context_aliases() -> Context {
let annotations = Annotations::new(
crate::test::metadata::alias(),
crate::test::lockfile::alias(),
Config::default(),
)
.unwrap();
Context::new(annotations).unwrap()
}
#[test]
fn workspace_member_deps() {
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(), "1.3.2".to_owned()), false),
(&CrateId::new("cfg-if".to_owned(), "1.0.0".to_owned()), 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(), "0.3.9".to_owned()), false),
(&CrateId::new("log".to_owned(), "0.4.14".to_owned()), false),
(&CrateId::new("names".to_owned(), "0.12.1-dev".to_owned()), false),
(&CrateId::new("names".to_owned(), "0.13.0".to_owned()), false),
(&CrateId::new("value-bag".to_owned(), "1.0.0-alpha.7".to_owned()), false),
],
}
}
#[test]
fn seralization() {
let context = mock_context_aliases();
// Seralize and deseralize 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);
}
}