Skip to main content

hakari/
summaries.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Manage configuration and generate summaries for `hakari`.
5//!
6//! Requires the `cli-support` feature to be enabled.
7
8use crate::{
9    HakariBuilder, HakariOutputOptions, TomlOutError, UnifyTargetHost,
10    hakari::{DepFormatVersion, WorkspaceHackLineStyle},
11};
12use guppy::{
13    errors::TargetSpecError,
14    graph::{PackageGraph, cargo::CargoResolverVersion, summaries::PackageSetSummary},
15};
16use serde::{Deserialize, Serialize};
17use std::{collections::BTreeMap, fmt, str::FromStr};
18
19/// The location of the configuration used by `cargo hakari`, relative to the workspace root.
20pub static DEFAULT_CONFIG_PATH: &str = ".config/hakari.toml";
21
22/// The fallback location, used by previous versions of `cargo hakari`.
23pub static FALLBACK_CONFIG_PATH: &str = ".guppy/hakari.toml";
24
25/// Configuration for `hakari`.
26///
27/// Requires the `cli-support` feature to be enabled.
28#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
29#[serde(rename_all = "kebab-case")]
30#[non_exhaustive]
31pub struct HakariConfig {
32    /// Builder options.
33    #[serde(flatten)]
34    pub builder: HakariBuilderSummary,
35
36    /// Output options.
37    #[serde(flatten)]
38    pub output: OutputOptionsSummary,
39}
40
41impl FromStr for HakariConfig {
42    type Err = toml::de::Error;
43
44    /// Deserializes a [`HakariConfig`] from the given TOML string.
45    fn from_str(input: &str) -> Result<Self, Self::Err> {
46        toml::from_str(input)
47    }
48}
49
50/// A `HakariBuilder` in serializable form. This forms the configuration file format for `hakari`.
51///
52/// For an example, see the
53/// [cargo-hakari README](https://github.com/guppy-rs/guppy/tree/main/tools/hakari#configuration).
54///
55/// Requires the `cli-support` feature to be enabled.
56#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "kebab-case")]
58#[non_exhaustive]
59pub struct HakariBuilderSummary {
60    /// The name of the Hakari package in the workspace.
61    pub hakari_package: Option<String>,
62
63    /// The Cargo resolver version used.
64    ///
65    /// For more information, see the documentation for [`CargoResolverVersion`].
66    #[serde(alias = "version")]
67    pub resolver: CargoResolverVersion,
68
69    /// Unification across target and host.
70    #[serde(default)]
71    pub unify_target_host: UnifyTargetHost,
72
73    /// Whether all dependencies were unified.
74    #[serde(default)]
75    pub output_single_feature: bool,
76
77    /// Format version for hakari.
78    #[serde(default)]
79    pub dep_format_version: DepFormatVersion,
80
81    /// Format kind for `workspace-hack = { ... }` lines.
82    #[serde(default)]
83    pub workspace_hack_line_style: WorkspaceHackLineStyle,
84
85    /// The platforms used by the `HakariBuilder`.
86    #[serde(default)]
87    pub platforms: Vec<String>,
88
89    /// The list of packages excluded during graph traversals.
90    #[serde(default)]
91    pub traversal_excludes: PackageSetSummary,
92
93    /// The list of packages excluded from the final output.
94    #[serde(default)]
95    pub final_excludes: PackageSetSummary,
96
97    /// The list of alternate registries, as a map of name to URL.
98    ///
99    /// This is a temporary workaround until [Cargo issue #9052](https://github.com/rust-lang/cargo/issues/9052)
100    /// is resolved.
101    #[serde(
102        default,
103        skip_serializing_if = "BTreeMap::is_empty",
104        with = "registries_impl"
105    )]
106    pub registries: BTreeMap<String, String>,
107}
108
109impl HakariBuilderSummary {
110    /// Creates a new `HakariBuilderSummary` from a builder.
111    ///
112    /// Requires the `cli-support` feature to be enabled.
113    ///
114    /// Returns an error if there are any custom platforms. Serializing custom platforms is
115    /// currently unsupported.
116    pub fn new(builder: &HakariBuilder<'_>) -> Result<Self, TargetSpecError> {
117        Ok(Self {
118            hakari_package: builder
119                .hakari_package()
120                .map(|package| package.name().to_string()),
121            platforms: builder
122                .platforms()
123                .map(|triple_str| triple_str.to_owned())
124                .collect::<Vec<_>>(),
125            resolver: builder.resolver(),
126            traversal_excludes: PackageSetSummary::from_package_ids(
127                builder.graph(),
128                builder.traversal_excludes_only(),
129            )
130            .expect("all package IDs are valid"),
131            final_excludes: PackageSetSummary::from_package_ids(
132                builder.graph(),
133                builder.final_excludes(),
134            )
135            .expect("all package IDs are valid"),
136            registries: builder
137                .registries
138                .iter()
139                .map(|registry| (registry.name.clone(), registry.url.clone()))
140                .collect(),
141            unify_target_host: builder.unify_target_host(),
142            output_single_feature: builder.output_single_feature(),
143            dep_format_version: builder.dep_format_version,
144            workspace_hack_line_style: builder.workspace_hack_line_style,
145        })
146    }
147
148    /// Creates a `HakariBuilder` from this summary and a `PackageGraph`.
149    ///
150    /// Returns an error if this summary references a package that's not present, or if there was
151    /// some other issue while creating a `HakariBuilder` from this summary.
152    pub fn to_hakari_builder<'g>(
153        &self,
154        graph: &'g PackageGraph,
155    ) -> Result<HakariBuilder<'g>, guppy::Error> {
156        HakariBuilder::from_summary(graph, self)
157    }
158
159    /// Serializes this summary to a TOML string.
160    ///
161    /// Returns an error if writing out the TOML was unsuccessful.
162    pub fn to_string(&self) -> Result<String, toml::ser::Error> {
163        let mut dst = String::new();
164        self.write_to_string(&mut dst)?;
165        Ok(dst)
166    }
167
168    /// Serializes this summary to a TOML string, and adds `#` comment markers to the beginning of
169    /// each line.
170    ///
171    /// Returns an error if writing out the TOML was unsuccessful.
172    pub fn write_comment(&self, mut out: impl fmt::Write) -> Result<(), TomlOutError> {
173        // Begin with a comment.
174        let summary = self.to_string().map_err(|err| TomlOutError::Toml {
175            context: "while serializing HakariBuilderSummary as comment".into(),
176            err,
177        })?;
178        for line in summary.lines() {
179            if line.is_empty() {
180                writeln!(out, "#")?;
181            } else {
182                writeln!(out, "# {line}")?;
183            }
184        }
185        Ok(())
186    }
187
188    /// Writes out the contents of this summary as TOML to the given string.
189    ///
190    /// Returns an error if writing out the TOML was unsuccessful.
191    pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error> {
192        let table = toml::Table::try_from(self)?;
193        guppy::graph::summaries::toml_compat::write_table(&table, dst)
194    }
195}
196
197impl HakariBuilder<'_> {
198    /// Converts this `HakariBuilder` to a serializable summary.
199    ///
200    /// Requires the `cli-support` feature to be enabled.
201    ///
202    /// Returns an error if there are any custom platforms. Serializing custom platforms is
203    /// currently unsupported.
204    pub fn to_summary(&self) -> Result<HakariBuilderSummary, TargetSpecError> {
205        HakariBuilderSummary::new(self)
206    }
207}
208
209/// Options for `hakari` TOML output, in serializable form.
210///
211/// TODO: add a configuration.md file.
212#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
213#[serde(rename_all = "kebab-case")]
214#[non_exhaustive]
215pub struct OutputOptionsSummary {
216    /// Output exact versions in package version fields.
217    #[serde(default)]
218    exact_versions: bool,
219
220    /// Output absolute paths for path dependencies.
221    #[serde(default)]
222    absolute_paths: bool,
223
224    /// Output a [`HakariBuilderSummary`] as comments.
225    #[serde(default)]
226    builder_summary: bool,
227}
228
229impl OutputOptionsSummary {
230    /// Creates a new `OutputOptionsSummary`.
231    pub fn new(options: &HakariOutputOptions) -> Self {
232        Self {
233            exact_versions: options.exact_versions,
234            absolute_paths: options.absolute_paths,
235            builder_summary: options.builder_summary,
236        }
237    }
238
239    /// Converts this summary to the options.
240    pub fn to_options(&self) -> HakariOutputOptions {
241        HakariOutputOptions {
242            exact_versions: self.exact_versions,
243            absolute_paths: self.absolute_paths,
244            builder_summary: self.builder_summary,
245        }
246    }
247}
248
249mod registries_impl {
250    use super::*;
251    use serde::{Deserializer, Serializer};
252
253    #[derive(Debug, Deserialize)]
254    #[serde(deny_unknown_fields)]
255    struct RegistryDe {
256        index: String,
257    }
258
259    #[derive(Debug, Serialize)]
260    struct RegistrySer<'a> {
261        index: &'a str,
262    }
263
264    /// Serializes a path using forward slashes.
265    pub fn serialize<S>(
266        registry_map: &BTreeMap<String, String>,
267        serializer: S,
268    ) -> Result<S::Ok, S::Error>
269    where
270        S: Serializer,
271    {
272        let ser_map: BTreeMap<_, _> = registry_map
273            .iter()
274            .map(|(name, index)| {
275                (
276                    name.as_str(),
277                    RegistrySer {
278                        index: index.as_str(),
279                    },
280                )
281            })
282            .collect();
283        ser_map.serialize(serializer)
284    }
285
286    /// Deserializes a path, converting forward slashes to backslashes.
287    pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
288    where
289        D: Deserializer<'de>,
290    {
291        let de_map = BTreeMap::<String, RegistryDe>::deserialize(deserializer)?;
292        let registry_map = de_map
293            .into_iter()
294            .map(|(name, RegistryDe { index })| (name, index))
295            .collect();
296        Ok(registry_map)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use fixtures::json::*;
304
305    #[test]
306    fn parse_registries() {
307        static PARSE_REGISTRIES_INPUT: &str = r#"
308        resolver = "2"
309
310        [traversal-excludes]
311        third-party = [
312            { name = "serde_derive", registry = "my-registry" },
313        ]
314
315        [registries]
316        my-registry = { index = "https://github.com/fakeorg/crates.io-index" }
317        your-registry = { index = "https://foobar" }
318        "#;
319
320        let summary: HakariBuilderSummary =
321            toml::from_str(PARSE_REGISTRIES_INPUT).expect("failed to parse toml");
322        // Need an arbitrary graph for this.
323        let builder = summary
324            .to_hakari_builder(JsonFixture::metadata_alternate_registries().graph())
325            .expect("summary => builder conversion");
326
327        assert_eq!(
328            summary.registries.get("my-registry").map(|s| s.as_str()),
329            Some(METADATA_ALTERNATE_REGISTRY_URL),
330            "my-registry is correct"
331        );
332        assert_eq!(
333            summary.registries.get("your-registry").map(|s| s.as_str()),
334            Some("https://foobar"),
335            "your-registry is correct"
336        );
337
338        let summary2 = builder.to_summary().expect("builder => summary conversion");
339        let builder2 = summary
340            .to_hakari_builder(JsonFixture::metadata_alternate_registries().graph())
341            .expect("summary2 => builder2 conversion");
342        assert_eq!(
343            builder.traversal_excludes, builder2.traversal_excludes,
344            "builder == builder2 traversal excludes"
345        );
346
347        let serialized = toml::to_string(&summary2).expect("serialized to TOML correctly");
348        let summary3: HakariBuilderSummary =
349            toml::from_str(&serialized).expect("deserialized from TOML correctly");
350        assert_eq!(
351            summary2, summary3,
352            "summary => serialized => summary roundtrip"
353        );
354    }
355}