blob: 912cd803593f500b0092023921eaef21db3e0c40 [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;
use crate::utils::starlark::{Select, SelectList};
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, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
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<String>>,
}
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
.iter()
// Convert the crate annotations into more renderable contexts
.map(|(_, annotation)| {
let context = CrateContext::new(
annotation,
&annotations.metadata.packages,
&annotations.lockfile.crates,
&annotations.pairred_extras,
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::new(pkg.name.clone(), pkg.version.to_string());
// 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>>>()?;
Ok(Self {
checksum: None,
crates,
binary_crates,
workspace_members,
conditions,
})
}
// 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)
}
/// Filter a crate's dependencies to only ones with aliases
pub fn crate_aliases(
&self,
crate_id: &CrateId,
build: bool,
include_dev: bool,
) -> SelectList<&CrateDependency> {
let ctx = &self.crates[crate_id];
let mut set = SelectList::default();
// Return a set of aliases for build dependencies
// vs normal dependencies when requested.
if build {
// Note that there may not be build dependencies so no dependencies
// will be gathered in this case
if let Some(attrs) = &ctx.build_script_attrs {
let collection: Vec<(Option<String>, &CrateDependency)> = attrs
.deps
.configurations()
.into_iter()
.flat_map(move |conf| {
attrs
.deps
.get_iter(conf)
.expect("Iterating over known keys should never panic")
.filter(|dep| dep.alias.is_some())
.map(move |dep| (conf.cloned(), dep))
})
.chain(attrs.proc_macro_deps.configurations().into_iter().flat_map(
move |conf| {
attrs
.proc_macro_deps
.get_iter(conf)
.expect("Iterating over known keys should never panic")
.filter(|dep| dep.alias.is_some())
.map(move |dep| (conf.cloned(), dep))
},
))
.collect();
for (config, dep) in collection {
set.insert(dep, config);
}
}
} else {
let attrs = &ctx.common_attrs;
let mut collection: Vec<(Option<String>, &CrateDependency)> =
attrs
.deps
.configurations()
.into_iter()
.flat_map(move |conf| {
attrs
.deps
.get_iter(conf)
.expect("Iterating over known keys should never panic")
.filter(|dep| dep.alias.is_some())
.map(move |dep| (conf.cloned(), dep))
})
.chain(attrs.proc_macro_deps.configurations().into_iter().flat_map(
move |conf| {
attrs
.proc_macro_deps
.get_iter(conf)
.expect("Iterating over known keys should never panic")
.filter(|dep| dep.alias.is_some())
.map(move |dep| (conf.cloned(), dep))
},
))
.collect();
// Optionally include dev dependencies
if include_dev {
collection = collection
.into_iter()
.chain(
attrs
.deps_dev
.configurations()
.into_iter()
.flat_map(move |conf| {
attrs
.deps_dev
.get_iter(conf)
.expect("Iterating over known keys should never panic")
.filter(|dep| dep.alias.is_some())
.map(move |dep| (conf.cloned(), dep))
}),
)
.chain(
attrs
.proc_macro_deps_dev
.configurations()
.into_iter()
.flat_map(move |conf| {
attrs
.proc_macro_deps_dev
.get_iter(conf)
.expect("Iterating over known keys should never panic")
.filter(|dep| dep.alias.is_some())
.map(move |dep| (conf.cloned(), dep))
}),
)
.collect();
}
for (config, dep) in collection {
set.insert(dep, config);
}
}
set
}
/// Create a set of all direct dependencies of workspace member crates and map them to
/// optional alternative names that allow them to be uniquely identified. This typically
/// results in a mapping of ([CrateId], [None]) where [None] defaults to using the crate
/// name. The next most common would be using ([CrateId], `Some(alias)`) as some projects
/// may use aliases in Cargo as a way to differentiate different versions of the same dep.
pub fn flat_workspace_member_deps(&self) -> BTreeMap<CrateId, Option<String>> {
let workspace_member_dependencies: BTreeSet<CrateDependency> = self
.workspace_members
.iter()
.map(|(id, _)| &self.crates[id])
.flat_map(|ctx| {
// Build an interator of all dependency CrateIds.
// TODO: This expansion is horribly verbose and should be refactored but closures
// were not playing nice when I tried it.
ctx.common_attrs
.deps
.configurations()
.into_iter()
.flat_map(move |conf| {
ctx.common_attrs
.deps
.get_iter(conf)
.expect("Lookup should be guaranteed")
})
.chain(
ctx.common_attrs
.deps_dev
.configurations()
.into_iter()
.flat_map(move |conf| {
ctx.common_attrs
.deps_dev
.get_iter(conf)
.expect("Lookup should be guaranteed")
}),
)
.chain(
ctx.common_attrs
.proc_macro_deps
.configurations()
.into_iter()
.flat_map(move |conf| {
ctx.common_attrs
.proc_macro_deps
.get_iter(conf)
.expect("Lookup should be guaranteed")
}),
)
.chain(
ctx.common_attrs
.proc_macro_deps_dev
.configurations()
.into_iter()
.flat_map(move |conf| {
ctx.common_attrs
.proc_macro_deps_dev
.get_iter(conf)
.expect("Lookup should be guaranteed")
}),
)
})
.cloned()
.collect();
// Search for any duplicate workspace member definitions
let duplicate_deps: Vec<CrateDependency> = workspace_member_dependencies
.iter()
.filter(|dep| {
workspace_member_dependencies
.iter()
.filter(|check| dep.id.name == check.id.name)
.count()
> 1
})
.cloned()
.collect();
workspace_member_dependencies
.into_iter()
.map(|dep| {
if duplicate_deps.contains(&dep) {
if let Some(alias) = &dep.alias {
// Check for any duplicate aliases
let aliases = duplicate_deps
.iter()
.filter(|dupe| dupe.id.name == dep.id.name)
.filter(|dupe| dupe.alias.is_some())
.filter(|dupe| dupe.alias == dep.alias);
// If there are multiple aliased crates with the same name, the name is updated to
// be `{alias}-{version}` to differentiate them.
if aliases.count() >= 2 {
let rename = format!("{}-{}", &alias, &dep.id.version);
(dep.id, Some(rename))
} else {
(dep.id, Some(alias.clone()))
}
} else {
// Check for all duplicates that match the current dependency and have no alias
let unaliased = duplicate_deps
.iter()
.filter(|dupe| dupe.id.name == dep.id.name)
.filter(|dupe| dupe.alias.is_none());
// If there are multiple unaliased crates with the same name, the name is updated to
// be `{name}-{version}` to differentiate them.
if unaliased.count() >= 2 {
let rename = format!("{}-{}", &dep.id.name, &dep.id.version);
(dep.id, Some(rename))
} else {
(dep.id, None)
}
}
} else {
(dep.id, dep.alias)
}
})
.collect()
}
/// Produce a list of binary dependencies with optional aliases which prevent duplicate
/// targets from being generated.
pub fn flat_binary_deps(&self) -> BTreeMap<CrateId, Option<String>> {
// Check for any duplicate binary crate names. If one exists provide an alias to differentiate them
self.binary_crates
.iter()
.map(|crate_id| {
let dupe_count = self
.binary_crates
.iter()
.filter(|id| crate_id.name == id.name)
.count();
// For targets that appear twice (which can happen if one crate aliases a binary dependency)
if dupe_count >= 2 {
let rename = format!("{}-{}", crate_id.name, crate_id.version);
(crate_id.clone(), Some(rename))
} else {
(crate_id.clone(), None)
}
})
.collect()
}
}
#[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 flat_workspace_member_deps() {
let context = mock_context_common();
let workspace_member_deps = context.flat_workspace_member_deps();
assert_eq!(
workspace_member_deps,
BTreeMap::from([
(
CrateId::new("bitflags".to_owned(), "1.3.2".to_owned()),
None
),
(CrateId::new("cfg-if".to_owned(), "1.0.0".to_owned()), None),
])
);
}
#[test]
fn flat_workspace_member_deps_with_alises() {
let context = mock_context_aliases();
let workspace_member_deps = context.flat_workspace_member_deps();
assert_eq!(
workspace_member_deps,
BTreeMap::from([
(
CrateId {
name: "log".to_owned(),
version: "0.3.9".to_owned(),
},
Some("pinned_log".to_owned())
),
(
CrateId {
name: "log".to_owned(),
version: "0.4.14".to_owned(),
},
None
),
(
CrateId {
name: "names".to_owned(),
version: "0.12.1-dev".to_owned(),
},
Some("pinned_names".to_owned())
),
(
CrateId {
name: "names".to_owned(),
version: "0.13.0".to_owned(),
},
None
),
(
CrateId {
name: "value-bag".to_owned(),
version: "1.0.0-alpha.7".to_owned(),
},
None
),
])
);
}
}