#[cfg(feature = "cli-support")]
use crate::summaries::HakariBuilderSummary;
use crate::{
hakari::{HakariBuilder, OutputMap},
helpers::VersionDisplay,
DepFormatVersion,
};
use camino::Utf8PathBuf;
use cfg_if::cfg_if;
use guppy::{
errors::TargetSpecError,
graph::{cargo::BuildPlatform, ExternalSource, GitReq, PackageMetadata, PackageSource},
PackageId,
};
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
error, fmt,
hash::{Hash, Hasher},
};
use toml_edit::{Array, Document, InlineTable, Item, Table, Value};
use twox_hash::XxHash64;
#[derive(Clone, Debug)]
pub struct HakariOutputOptions {
pub(crate) exact_versions: bool,
pub(crate) absolute_paths: bool,
#[cfg(feature = "cli-support")]
pub(crate) builder_summary: bool,
}
impl HakariOutputOptions {
pub fn new() -> Self {
Self {
exact_versions: false,
absolute_paths: false,
#[cfg(feature = "cli-support")]
builder_summary: false,
}
}
pub fn set_exact_versions(&mut self, exact_versions: bool) -> &mut Self {
self.exact_versions = exact_versions;
self
}
pub fn set_absolute_paths(&mut self, absolute_paths: bool) -> &mut Self {
self.absolute_paths = absolute_paths;
self
}
#[cfg(feature = "cli-support")]
pub fn set_builder_summary(&mut self, builder_summary: bool) -> &mut Self {
self.builder_summary = builder_summary;
self
}
}
impl Default for HakariOutputOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum TomlOutError {
Platform(TargetSpecError),
#[cfg(feature = "cli-support")]
Toml {
context: Cow<'static, str>,
err: toml::ser::Error,
},
FmtWrite(fmt::Error),
PathWithoutHakari {
package_id: PackageId,
rel_path: Utf8PathBuf,
},
UnrecognizedExternal {
package_id: PackageId,
source: String,
},
UnrecognizedRegistry {
package_id: PackageId,
registry_url: String,
},
}
pub(crate) fn toml_name_map<'g>(
output_map: &OutputMap<'g>,
dep_format: DepFormatVersion,
) -> HashMap<Cow<'g, str>, PackageMetadata<'g>> {
let mut packages_by_name: HashMap<&'g str, HashMap<_, _>> = HashMap::new();
for vals in output_map.values() {
for (&package_id, (package, _)) in vals {
packages_by_name
.entry(package.name())
.or_default()
.insert(package_id, package);
}
}
let mut toml_name_map = HashMap::new();
for (name, packages) in packages_by_name {
if packages.len() > 1 {
for (_, package) in packages {
let hashed_name = make_hashed_name(package, dep_format);
toml_name_map.insert(Cow::Owned(hashed_name), *package);
}
} else {
toml_name_map.insert(
Cow::Borrowed(name),
*packages.into_values().next().expect("at least 1 element"),
);
}
}
toml_name_map
}
impl From<TargetSpecError> for TomlOutError {
fn from(err: TargetSpecError) -> Self {
TomlOutError::Platform(err)
}
}
impl From<fmt::Error> for TomlOutError {
fn from(err: fmt::Error) -> Self {
TomlOutError::FmtWrite(err)
}
}
impl fmt::Display for TomlOutError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TomlOutError::Platform(_) => write!(f, "while serializing platform information"),
#[cfg(feature = "cli-support")]
TomlOutError::Toml { context, .. } => write!(f, "while serializing TOML: {}", context),
TomlOutError::FmtWrite(_) => write!(f, "while writing to fmt::Write"),
TomlOutError::PathWithoutHakari {
package_id,
rel_path,
} => write!(
f,
"for path dependency '{}', no Hakari package was specified (relative path {})",
package_id, rel_path,
),
TomlOutError::UnrecognizedExternal { package_id, source } => write!(
f,
"for third-party dependency '{}', unrecognized external source {}",
package_id, source,
),
TomlOutError::UnrecognizedRegistry {
package_id,
registry_url,
} => {
write!(
f,
"for third-party dependency '{}', unrecognized registry at URL {}",
package_id, registry_url,
)
}
}
}
}
impl error::Error for TomlOutError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
TomlOutError::Platform(err) => Some(err),
#[cfg(feature = "cli-support")]
TomlOutError::Toml { err, .. } => Some(err),
TomlOutError::FmtWrite(err) => Some(err),
TomlOutError::PathWithoutHakari { .. }
| TomlOutError::UnrecognizedExternal { .. }
| TomlOutError::UnrecognizedRegistry { .. } => None,
}
}
}
pub(crate) fn write_toml(
builder: &HakariBuilder<'_>,
output_map: &OutputMap<'_>,
options: &HakariOutputOptions,
dep_format: DepFormatVersion,
mut out: impl fmt::Write,
) -> Result<(), TomlOutError> {
cfg_if! {
if #[cfg(feature = "cli-support")] {
if options.builder_summary {
let summary = HakariBuilderSummary::new(builder)?;
summary.write_comment(&mut out)?;
writeln!(out)?;
}
}
}
let mut packages_by_name: HashMap<&str, HashSet<_>> = HashMap::new();
for vals in output_map.values() {
for (&package_id, (package, _)) in vals {
packages_by_name
.entry(package.name())
.or_default()
.insert(package_id);
}
}
let hakari_path = builder.hakari_package().map(|package| {
package
.source()
.workspace_path()
.expect("hakari package is in workspace")
});
let mut document = Document::new();
let mut first_element = true;
for (key, vals) in output_map {
let dep_table_parent = match key.platform_idx {
Some(idx) => {
let target_table = get_or_insert_table(document.as_table_mut(), "target");
get_or_insert_table(target_table, builder.platforms[idx].triple_str())
}
None => document.as_table_mut(),
};
let dep_table = match key.build_platform {
BuildPlatform::Target => get_or_insert_table(dep_table_parent, "dependencies"),
BuildPlatform::Host => get_or_insert_table(dep_table_parent, "build-dependencies"),
};
if first_element {
dep_table.decor_mut().set_prefix("");
first_element = false;
}
for (dep, all_features) in vals.values() {
let mut itable = InlineTable::new();
let name: Cow<str> = if packages_by_name[dep.name()].len() > 1 {
itable.insert("package", dep.name().into());
make_hashed_name(dep, dep_format).into()
} else {
dep.name().into()
};
let source = dep.source();
if source.is_crates_io() {
itable.insert(
"version",
format!(
"{}",
VersionDisplay::new(
dep.version(),
options.exact_versions,
dep_format < DepFormatVersion::V3
)
)
.into(),
);
} else {
match source {
PackageSource::Workspace(path) | PackageSource::Path(path) => {
let path_out = if options.absolute_paths {
builder.graph().workspace().root().join(path)
} else {
let hakari_path =
hakari_path.ok_or_else(|| TomlOutError::PathWithoutHakari {
package_id: dep.id().clone(),
rel_path: path.to_path_buf(),
})?;
pathdiff::diff_utf8_paths(path, hakari_path)
.expect("both hakari_path and path are relative")
}
.into_string();
cfg_if! {
if #[cfg(windows)] {
let path_out = path_out.replace("\\", "/");
itable.insert("path", path_out.into());
} else {
itable.insert("path", path_out.into());
}
};
}
PackageSource::External(s) => match source.parse_external() {
Some(ExternalSource::Git {
repository, req, ..
}) => {
itable.insert("git", repository.into());
match req {
GitReq::Branch(branch) => {
itable.insert("branch", branch.into());
}
GitReq::Tag(tag) => {
itable.insert("tag", tag.into());
}
GitReq::Rev(rev) => {
itable.insert("rev", rev.into());
}
GitReq::Default => {}
_ => {
return Err(TomlOutError::UnrecognizedExternal {
package_id: dep.id().clone(),
source: s.to_string(),
});
}
};
}
Some(ExternalSource::Registry(registry_url)) => {
let registry_name = builder
.registries
.get_by_right(registry_url)
.ok_or_else(|| TomlOutError::UnrecognizedRegistry {
package_id: dep.id().clone(),
registry_url: registry_url.to_owned(),
})?;
itable.insert(
"version",
format!(
"{}",
VersionDisplay::new(
dep.version(),
options.exact_versions,
dep_format < DepFormatVersion::V3
)
)
.into(),
);
itable.insert("registry", registry_name.into());
}
_ => {
return Err(TomlOutError::UnrecognizedExternal {
package_id: dep.id().clone(),
source: s.to_string(),
});
}
},
}
};
if !all_features.contains(&"default") {
itable.insert("default-features", false.into());
}
let feature_array: Array = all_features
.iter()
.filter_map(|&label| {
match label {
x if x == "default" => None,
feature_name => Some(feature_name),
}
})
.collect();
if !feature_array.is_empty() {
itable.insert("features", feature_array.into());
}
itable.fmt();
dep_table.insert(name.as_ref(), Item::Value(Value::InlineTable(itable)));
}
if dep_format >= DepFormatVersion::V4 {
dep_table.sort_values();
}
}
write!(out, "{}", document)?;
if !document.is_empty() {
writeln!(out)?;
}
Ok(())
}
fn make_hashed_name(dep: &PackageMetadata<'_>, dep_format: DepFormatVersion) -> String {
let mut hasher = XxHash64::default();
let minimal_version = format!(
"{}",
VersionDisplay::new(dep.version(), false, dep_format < DepFormatVersion::V3)
);
minimal_version.hash(&mut hasher);
dep.source().hash(&mut hasher);
let hash = hasher.finish();
format!("{}-{:x}", dep.name(), hash)
}
fn get_or_insert_table<'t>(parent: &'t mut Table, key: &str) -> &'t mut Table {
let table = parent
.entry(key)
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.expect("just inserted this table");
table.set_implicit(true);
table
}
#[cfg(test)]
mod tests {
use super::*;
use fixtures::json::*;
use guppy::graph::DependencyDirection;
use std::collections::{btree_map::Entry, BTreeMap};
#[test]
fn make_package_name_unique() {
for (&name, fixture) in JsonFixture::all_fixtures() {
let mut names_seen: BTreeMap<String, PackageMetadata<'_>> = BTreeMap::new();
let graph = fixture.graph();
for package in graph.resolve_all().packages(DependencyDirection::Forward) {
match names_seen.entry(make_hashed_name(&package, DepFormatVersion::V3)) {
Entry::Vacant(entry) => {
entry.insert(package);
}
Entry::Occupied(entry) => {
panic!(
"for fixture '{}', duplicate generated package name '{}'. packages\n\
* {}\n\
* {}",
name,
entry.key(),
entry.get().id(),
package.id()
);
}
}
}
}
}
#[test]
fn alternate_registries() {
let fixture = JsonFixture::metadata_alternate_registries();
let mut builder =
HakariBuilder::new(fixture.graph(), None).expect("builder initialization succeeded");
builder.set_output_single_feature(true);
let hakari = builder.compute();
let output_options = HakariOutputOptions::new();
hakari
.to_toml_string(&output_options)
.expect_err("no alternate registry specified => error");
let mut builder =
HakariBuilder::new(fixture.graph(), None).expect("builder initialization succeeded");
builder.set_output_single_feature(true);
builder.add_registries([("alt-registry", METADATA_ALTERNATE_REGISTRY_URL)]);
let hakari = builder.compute();
let output = hakari
.to_toml_string(&output_options)
.expect("alternate registry specified => success");
static MATCH_STRINGS: &[&str] = &[
r#"serde-e7e45184a9cd0878 = { package = "serde", version = "1", registry = "alt-registry", default-features = false, "#,
r#"serde-dff4ba8e3ae991db = { package = "serde", version = "1", default-features = false, "#,
r#"serde_derive = { version = "1", registry = "alt-registry" }"#,
r#"itoa = { version = "0.4", default-features = false }"#,
];
for &needle in MATCH_STRINGS {
assert!(
output.contains(needle),
"output did not contain string '{}', actual output follows:\n***\n{}\n",
needle,
output
);
}
}
}