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