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