binstalk_manifests/
cargo_config.rs1use std::{
8 borrow::Cow,
9 collections::BTreeMap,
10 fs::File,
11 io,
12 path::{Path, PathBuf},
13};
14
15use compact_str::CompactString;
16use fs_lock::FileLock;
17use home::cargo_home;
18use miette::Diagnostic;
19use serde::Deserialize;
20use thiserror::Error;
21
22#[derive(Debug, Deserialize)]
23pub struct Install {
24 pub root: Option<PathBuf>,
26}
27
28#[derive(Debug, Deserialize)]
29pub struct Http {
30 pub proxy: Option<CompactString>,
34 pub timeout: Option<u64>,
38 pub cainfo: Option<PathBuf>,
40}
41
42#[derive(Eq, PartialEq, Debug, Deserialize)]
43#[serde(untagged)]
44pub enum Env {
45 Value(CompactString),
46 WithOptions {
47 value: CompactString,
48 force: Option<bool>,
49 relative: Option<bool>,
50 },
51}
52
53#[derive(Debug, Deserialize)]
54pub struct Registry {
55 pub index: Option<CompactString>,
56}
57
58#[derive(Debug, Deserialize)]
59pub struct DefaultRegistry {
60 pub default: Option<CompactString>,
61}
62
63#[derive(Debug, Default, Deserialize)]
64pub struct Config {
65 pub install: Option<Install>,
66 pub http: Option<Http>,
67 pub env: Option<BTreeMap<CompactString, Env>>,
68 pub registries: Option<BTreeMap<CompactString, Registry>>,
69 pub registry: Option<DefaultRegistry>,
70}
71
72fn join_if_relative(path: Option<&mut PathBuf>, dir: &Path) {
73 match path {
74 Some(path) if path.is_relative() => *path = dir.join(&*path),
75 _ => (),
76 }
77}
78
79impl Config {
80 pub fn default_path() -> Result<PathBuf, ConfigLoadError> {
81 Ok(cargo_home()?.join("config.toml"))
82 }
83
84 pub fn load() -> Result<Self, ConfigLoadError> {
85 Self::load_from_path(Self::default_path()?)
86 }
87
88 pub fn load_from_reader<R: io::Read>(
93 mut reader: R,
94 dir: &Path,
95 ) -> Result<Self, ConfigLoadError> {
96 fn inner(reader: &mut dyn io::Read, dir: &Path) -> Result<Config, ConfigLoadError> {
97 let mut vec = Vec::new();
98 reader.read_to_end(&mut vec)?;
99
100 if vec.is_empty() {
101 Ok(Default::default())
102 } else {
103 let mut config: Config = toml_edit::de::from_slice(&vec)?;
104 join_if_relative(
105 config
106 .install
107 .as_mut()
108 .and_then(|install| install.root.as_mut()),
109 dir,
110 );
111 join_if_relative(
112 config.http.as_mut().and_then(|http| http.cainfo.as_mut()),
113 dir,
114 );
115 if let Some(envs) = config.env.as_mut() {
116 for env in envs.values_mut() {
117 if let Env::WithOptions {
118 value,
119 relative: Some(true),
120 ..
121 } = env
122 {
123 let path = Cow::Borrowed(Path::new(&value));
124 if path.is_relative() {
125 *value = dir.join(&path).to_string_lossy().into();
126 }
127 }
128 }
129 }
130 Ok(config)
131 }
132 }
133
134 inner(&mut reader, dir)
135 }
136
137 pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, ConfigLoadError> {
138 fn inner(path: &Path) -> Result<Config, ConfigLoadError> {
139 match File::open(path) {
140 Ok(file) => {
141 let file = FileLock::new_shared(file)?.set_file_path(path);
142 Config::load_from_reader(file, path.parent().unwrap())
144 }
145 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Default::default()),
146 Err(err) => Err(err.into()),
147 }
148 }
149
150 inner(path.as_ref())
151 }
152}
153
154#[derive(Debug, Diagnostic, Error)]
155#[non_exhaustive]
156pub enum ConfigLoadError {
157 #[error("I/O Error: {0}")]
158 Io(#[from] io::Error),
159
160 #[error("Failed to deserialize toml: {0}")]
161 TomlParse(Box<toml_edit::de::Error>),
162}
163
164impl From<toml_edit::de::Error> for ConfigLoadError {
165 fn from(e: toml_edit::de::Error) -> Self {
166 ConfigLoadError::TomlParse(Box::new(e))
167 }
168}
169
170impl From<toml_edit::TomlError> for ConfigLoadError {
171 fn from(e: toml_edit::TomlError) -> Self {
172 ConfigLoadError::TomlParse(Box::new(e.into()))
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 use std::{io::Cursor, path::MAIN_SEPARATOR};
181
182 use compact_str::format_compact;
183
184 const CONFIG: &str = r#"
185[env]
186# Set ENV_VAR_NAME=value for any process run by Cargo
187ENV_VAR_NAME = "value"
188# Set even if already present in environment
189ENV_VAR_NAME_2 = { value = "value", force = true }
190# Value is relative to .cargo directory containing `config.toml`, make absolute
191ENV_VAR_NAME_3 = { value = "relative-path", relative = true }
192
193[http]
194debug = false # HTTP debugging
195proxy = "host:port" # HTTP proxy in libcurl format
196timeout = 30 # timeout for each HTTP request, in seconds
197cainfo = "cert.pem" # path to Certificate Authority (CA) bundle
198
199[install]
200root = "/some/path" # `cargo install` destination directory
201 "#;
202
203 #[test]
204 fn test_loading() {
205 let config = Config::load_from_reader(Cursor::new(&CONFIG), Path::new("root")).unwrap();
206
207 assert_eq!(
208 config.install.unwrap().root.as_deref().unwrap(),
209 Path::new("/some/path")
210 );
211
212 let http = config.http.unwrap();
213 assert_eq!(http.proxy.unwrap(), CompactString::const_new("host:port"));
214 assert_eq!(http.timeout.unwrap(), 30);
215 assert_eq!(http.cainfo.unwrap(), Path::new("root").join("cert.pem"));
216
217 let env = config.env.unwrap();
218 assert_eq!(env.len(), 3);
219 assert_eq!(
220 env.get("ENV_VAR_NAME").unwrap(),
221 &Env::Value(CompactString::const_new("value"))
222 );
223 assert_eq!(
224 env.get("ENV_VAR_NAME_2").unwrap(),
225 &Env::WithOptions {
226 value: CompactString::new("value"),
227 force: Some(true),
228 relative: None,
229 }
230 );
231 assert_eq!(
232 env.get("ENV_VAR_NAME_3").unwrap(),
233 &Env::WithOptions {
234 value: format_compact!("root{MAIN_SEPARATOR}relative-path"),
235 force: None,
236 relative: Some(true),
237 }
238 );
239 }
240}