1use itertools::Itertools;
2use serde::de;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::result;
6use std::{collections, path};
7
8use crate::errors::*;
9
10#[derive(Clone, Debug)]
11pub struct TestData {
12 pub id: String,
13 pub base: path::PathBuf,
14 pub source: String,
15 pub target: String,
16 pub copy_git_ignored: bool,
17}
18
19#[derive(Serialize, Debug, Clone)]
20pub struct TestDataConfiguration {
21 pub copy_git_ignored: bool,
22 pub source: String,
23 pub target: Option<String>,
24}
25
26#[derive(Serialize, Deserialize, Debug, Clone)]
27pub struct DetailedTestDataConfiguration {
28 pub source: String,
29 pub copy_git_ignored: bool,
30 pub target: Option<String>,
31}
32
33impl<'de> de::Deserialize<'de> for TestDataConfiguration {
34 fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
35 where
36 D: de::Deserializer<'de>,
37 {
38 struct TestDataVisitor;
39
40 impl<'de> de::Visitor<'de> for TestDataVisitor {
41 type Value = TestDataConfiguration;
42
43 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
44 formatter.write_str(
45 "a path like \"tests/my_test_data\" or a \
46 detailed dependency like { source = \
47 \"tests/my_test_data\", copy_git_ignored = true }",
48 )
49 }
50
51 fn visit_str<E>(self, s: &str) -> result::Result<Self::Value, E>
52 where
53 E: de::Error,
54 {
55 Ok(TestDataConfiguration {
56 copy_git_ignored: false,
57 source: s.to_owned(),
58 target: None,
59 })
60 }
61
62 fn visit_map<V>(self, map: V) -> result::Result<Self::Value, V::Error>
63 where
64 V: de::MapAccess<'de>,
65 {
66 let mvd = de::value::MapAccessDeserializer::new(map);
67 let detailed = DetailedTestDataConfiguration::deserialize(mvd)?;
68 Ok(TestDataConfiguration {
69 copy_git_ignored: detailed.copy_git_ignored,
70 source: detailed.source,
71 target: detailed.target,
72 })
73 }
74 }
75
76 deserializer.deserialize_any(TestDataVisitor)
77 }
78}
79
80#[derive(Clone, Debug, Default)]
81pub struct Configuration {
82 pub platforms: collections::BTreeMap<String, PlatformConfiguration>,
83 pub ssh_devices: collections::BTreeMap<String, SshDeviceConfiguration>,
84 pub script_devices: collections::BTreeMap<String, ScriptDeviceConfiguration>,
85 pub test_data: Vec<TestData>,
86 pub skip_source_copy: bool,
87}
88
89#[derive(Clone, Serialize, Deserialize, Debug, Default)]
90struct ConfigurationFileContent {
91 pub platforms: Option<collections::BTreeMap<String, PlatformConfiguration>>,
92 pub ssh_devices: Option<collections::BTreeMap<String, SshDeviceConfiguration>>,
93 pub script_devices: Option<collections::BTreeMap<String, ScriptDeviceConfiguration>>,
94 pub test_data: Option<collections::BTreeMap<String, TestDataConfiguration>>,
95 pub skip_source_copy: Option<bool>,
96}
97
98#[derive(Clone, Serialize, Deserialize, Debug, Default)]
99pub struct PlatformConfiguration {
100 pub deb_multiarch: Option<String>,
101 pub env: Option<collections::HashMap<String, String>>,
102 pub overlays: Option<collections::HashMap<String, OverlayConfiguration>>,
103 pub rustc_triple: Option<String>,
104 pub sysroot: Option<String>,
105 pub toolchain: Option<String>,
106}
107
108impl PlatformConfiguration {
109 pub fn empty() -> Self {
110 PlatformConfiguration {
111 deb_multiarch: None,
112 env: None,
113 overlays: None,
114 rustc_triple: None,
115 sysroot: None,
116 toolchain: None,
117 }
118 }
119
120 pub fn env(&self) -> Vec<(String, String)> {
121 self.env
122 .as_ref()
123 .map(|it| {
124 it.iter()
125 .map(|(key, value)| (key.to_string(), value.to_string()))
126 .collect_vec()
127 })
128 .unwrap_or(vec![])
129 }
130}
131
132#[derive(Clone, Serialize, Deserialize, Debug, Default)]
133pub struct OverlayConfiguration {
134 pub path: String,
135 pub scope: Option<String>,
136}
137
138#[derive(Clone, Serialize, Deserialize, Debug)]
139pub struct SshDeviceConfiguration {
140 pub hostname: String,
141 pub username: String,
142 pub port: Option<u16>,
143 pub path: Option<String>,
144 pub target: Option<String>,
145 pub toolchain: Option<String>,
146 pub platform: Option<String>,
147 #[serde(default)]
148 pub remote_shell_vars: collections::HashMap<String, String>,
149 pub install_adhoc_rsync_local_path: Option<String>,
150 pub use_legacy_scp_protocol_for_adhoc_rsync_copy: Option<bool>,
151}
152
153#[derive(Clone, Serialize, Deserialize, Debug)]
154pub struct ScriptDeviceConfiguration {
155 pub path: String,
156 pub platform: Option<String>,
157}
158
159impl Configuration {
160 pub fn merge(&mut self, file: &path::Path) -> Result<()> {
161 let other = read_config_file(&file)?;
162 if let Some(pfs) = other.platforms {
163 self.platforms.extend(pfs)
164 }
165 self.ssh_devices
166 .extend(other.ssh_devices.unwrap_or(collections::BTreeMap::new()));
167 self.script_devices
168 .extend(other.script_devices.unwrap_or(collections::BTreeMap::new()));
169 for (id, source) in other.test_data.unwrap_or(collections::BTreeMap::new()) {
170 self.test_data.push(TestData {
172 id: id.to_string(),
173 base: file.to_path_buf(),
174 source: source.source.clone(),
175 target: source.target.unwrap_or(source.source.clone()),
176 copy_git_ignored: source.copy_git_ignored,
177 })
178 }
179 if let Some(skip_source_copy) = other.skip_source_copy {
180 self.skip_source_copy = skip_source_copy
181 }
182 Ok(())
183 }
184}
185
186fn read_config_file<P: AsRef<path::Path>>(file: P) -> Result<ConfigurationFileContent> {
187 let file = file.as_ref();
188 let data = std::fs::read_to_string(file).with_context(|| format!("Reading {file:?}"))?;
189 Ok(toml::from_str(&data)?)
190}
191
192pub fn dinghy_config<P: AsRef<path::Path>>(dir: P) -> Result<Configuration> {
193 let mut conf = Configuration::default();
194
195 let mut files_to_try = vec![];
196 let dir = dir.as_ref().to_path_buf();
197 let mut d = dir.as_path();
198 while d.parent().is_some() {
199 files_to_try.push(d.join("dinghy.toml"));
200 files_to_try.push(d.join(".dinghy.toml"));
201 files_to_try.push(d.join(".dinghy").join("dinghy.toml"));
202 files_to_try.push(d.join(".dinghy").join(".dinghy.toml"));
203 d = d.parent().unwrap();
204 }
205 files_to_try.push(d.join(".dinghy.toml"));
206 if let Some(home) = dirs::home_dir() {
207 if !dir.starts_with(&home) {
208 files_to_try.push(home.join("dinghy.toml"));
209 files_to_try.push(home.join(".dinghy.toml"));
210 files_to_try.push(home.join(".dinghy").join("dinghy.toml"));
211 files_to_try.push(home.join(".dinghy").join(".dinghy.toml"));
212 }
213 }
214 for file in files_to_try {
215 if path::Path::new(&file).exists() {
216 log::debug!("Loading configuration from {:?}", file);
217 conf.merge(&file)?;
218 } else {
219 log::trace!("No configuration found at {:?}", file);
220 }
221 }
222
223 log::debug!("Configuration: {:#?}", conf);
224
225 Ok(conf)
226}
227
228#[cfg(test)]
229mod tests {
230 #[test]
231 fn load_config_with_str_test_data() {
232 let config_file = ::std::env::current_exe()
233 .unwrap()
234 .parent()
235 .unwrap()
236 .join("../../../test-ws/test-app/.dinghy.toml");
237 super::read_config_file(config_file).unwrap();
238 }
239}