dinghy_lib/
config.rs

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            // TODO Remove key
172            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}