Skip to main content

crate2nix/
config.rs

1//! Managing the `crate2nix.json` config.
2
3use anyhow::{Context, Error};
4use serde::{Deserialize, Serialize};
5use std::{
6    collections::BTreeMap,
7    fmt::Display,
8    fs::File,
9    io::{BufReader, BufWriter},
10    path::Path,
11};
12
13impl Config {
14    /// Read config from path.
15    pub fn read_from_or_default(path: &Path) -> Result<Config, Error> {
16        if !path.exists() {
17            return Ok(Config::default());
18        }
19
20        let file = File::open(path).context(format!("while opening {}", path.to_string_lossy()))?;
21        let reader = BufReader::new(file);
22        serde_json::from_reader(reader).context(format!(
23            "while deserializing config: {}",
24            path.to_string_lossy()
25        ))
26    }
27
28    /// Write config to path.
29    pub fn write_to(&self, path: &Path) -> Result<(), Error> {
30        let file =
31            File::create(path).context(format!("while opening {}", path.to_string_lossy()))?;
32        let writer = BufWriter::new(file);
33        Ok(serde_json::to_writer_pretty(writer, self)?)
34    }
35}
36
37/// The `crate2nix.json` config data.
38#[derive(Debug, Default, Serialize, Deserialize)]
39pub struct Config {
40    /// Out of tree sources.
41    pub sources: BTreeMap<String, Source>,
42}
43
44impl Config {
45    /// Add or replace a source. Returns the old source if there was one.
46    pub fn upsert_source(
47        &mut self,
48        explicit_name: Option<String>,
49        source: Source,
50    ) -> Option<Source> {
51        let name = explicit_name
52            .or_else(|| source.name().map(|s| s.to_string()))
53            .expect("No name given");
54        self.sources.insert(name, source)
55    }
56
57    /// Prints all sources to stdout.
58    pub fn print_sources(&self) {
59        if self.sources.is_empty() {
60            eprintln!("No sources configured.\n");
61            return;
62        }
63
64        let max_len = self
65            .sources
66            .keys()
67            .map(|n| n.len())
68            .max()
69            .unwrap_or_default();
70        for (name, source) in &self.sources {
71            println!("{:width$} {}", name, source, width = max_len);
72            println!();
73            println!(
74                "{:width$} crate2nix source add {}",
75                "",
76                source.as_command(name),
77                width = max_len
78            );
79            println!();
80        }
81    }
82}
83
84/// An out of tree source.
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86#[serde(tag = "type")]
87pub enum Source {
88    /// Get the source from crates.io.
89    CratesIo {
90        /// The crate name.
91        name: String,
92        /// The exact crate version to fetch.
93        version: semver::Version,
94        /// The sha256 hash of the source.
95        sha256: String,
96    },
97    /// Get the source from crates.io.
98    Registry {
99        /// The registry's URL
100        registry: String,
101        /// The crate name.
102        name: String,
103        /// The exact crate version to fetch.
104        version: semver::Version,
105        /// The sha256 hash of the source.
106        sha256: String,
107    },
108    /// Get the source from git.
109    Git {
110        /// The URL of the git repository.
111        ///
112        /// E.g. https://github.com/kolloch/crate2nix.git
113        url: url::Url,
114        /// The revision hash.
115        rev: String,
116        /// The sha256 of the fetched result.
117        sha256: String,
118    },
119    /// Get the source from a nix expression.
120    Nix {
121        /// The nixfile to include.
122        #[serde(flatten)]
123        file: NixFile,
124        /// A Nix attribute path which will be resolved against the file.
125        #[serde(skip_serializing_if = "Option::is_none")]
126        attr: Option<String>,
127    },
128}
129
130/// A nix file path which is either included by `import` or `callPackage`.
131#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq, Hash)]
132pub enum NixFile {
133    /// A file path that should be imported.
134    #[serde(rename = "import")]
135    Import(String),
136    /// A file path the should be included by `pkgs.callPackage`.
137    #[serde(rename = "package")]
138    Package(String),
139}
140
141impl Display for NixFile {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::Import(path) => write!(f, "import {}", path),
145            Self::Package(path) => write!(f, "pkgs.callPackage {} {{}}", path),
146        }
147    }
148}
149
150impl NixFile {
151    /// Returns the chosen file option as CLI string.
152    pub fn as_command(&self) -> String {
153        match self {
154            Self::Import(path) => format!("--import '{}'", path),
155            Self::Package(path) => format!("--package '{}'", path),
156        }
157    }
158}
159
160impl Source {
161    /// The name of the source.
162    pub fn name(&self) -> Option<&str> {
163        match self {
164            Source::CratesIo { name, .. } => Some(name),
165            Source::Git { url, .. } => {
166                let path = url.path();
167                let after_last_slash = path.split('/').last().unwrap_or(path);
168                let without_dot_git = after_last_slash
169                    .strip_suffix(".git")
170                    .unwrap_or(after_last_slash);
171                Some(without_dot_git)
172            }
173            Source::Nix {
174                attr: Some(attr), ..
175            } => attr.split('.').last().or(if attr.trim().is_empty() {
176                None
177            } else {
178                Some(attr.trim())
179            }),
180            _ => None,
181        }
182    }
183}
184
185impl Display for Source {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        match self {
188            Source::CratesIo {
189                name,
190                version,
191                sha256,
192            } => write!(f, "{} {} from crates.io: {}", name, version, sha256),
193            Source::Registry {
194                name,
195                version,
196                sha256,
197                registry,
198                ..
199            } => write!(f, "{} {} from {}: {}", name, version, registry, sha256),
200            Source::Git { url, rev, sha256 } => write!(f, "{}#{} via git: {}", url, rev, sha256),
201            Source::Nix { file, attr: None } => write!(f, "{}", file),
202            Source::Nix {
203                file,
204                attr: Some(attr),
205            } => write!(f, "({}).{}", file, attr),
206        }
207    }
208}
209
210impl Source {
211    /// Returns a CLI string to reproduce this source.
212    pub fn as_command(&self, name: &str) -> String {
213        match self {
214            Source::CratesIo {
215                name: crate_name,
216                version,
217                ..
218            } => format!("cratesIo --name '{}' '{}' '{}'", name, crate_name, version),
219            Source::Registry {
220                name: crate_name,
221                version,
222                registry,
223                ..
224            } => format!(
225                "registry --registry '{}' --name '{}' '{}' '{}'",
226                registry, name, crate_name, version
227            ),
228            Source::Git { url, rev, .. } => {
229                format!("git --name '{}' '{}' --rev {}", name, url, rev)
230            }
231            Source::Nix { file, attr: None } => {
232                format!("nix --name '{}' {}", name, file.as_command())
233            }
234            Source::Nix {
235                file,
236                attr: Some(attr),
237            } => format!("nix --name '{}' {} '{}'", name, file.as_command(), attr),
238        }
239    }
240}