dinghy_lib/plugin/
mod.rs

1use crate::config::{PlatformConfiguration, ScriptDeviceConfiguration, SshDeviceConfiguration};
2use crate::platform::regular_platform::RegularPlatform;
3use crate::{Configuration, Device, Platform, PlatformManager};
4use anyhow::{anyhow, bail, Context, Result};
5use log::debug;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::os::unix::fs::PermissionsExt;
9use std::process::Command;
10use std::sync::Arc;
11use std::{env, fs};
12
13/// This platform manager will auto-detect any executable in the PATH that starts with
14/// `cargo-dinghy-` and try to use them as a plugin to provide devices and platforms.
15///
16/// To be a valid plugin, an executable must implement the following subcommands:
17/// - `devices`: must output a TOML file with a `DevicePluginOutput` structure
18/// - `platforms`: must output a TOML file with a `BTreeMap<String, PlatformConfiguration>` structure
19///
20/// Here is example of output for a `cargo-dinghy-foo` plugin configuring a `bar` device and a `baz`
21/// platform:
22///
23/// ```no_compile
24/// $ cargo-dinghy-foo devices
25/// [ssh_devices.bar]
26/// hostname = "127.0.0.1"
27/// username = "user"
28///
29/// $ cargo-dinghy-foo platforms
30/// [baz]
31/// rustc_triple = "aarch64-unknown-linux-gnu"
32/// toolchain = "/path/to/toolchain"
33/// ```
34/// This is quite useful if you have a bench of devices and platforms that can be auto-detected
35/// or are already configured in another tool.
36pub struct PluginManager {
37    conf: Arc<Configuration>,
38    auto_detected_plugins: Vec<String>,
39}
40
41impl PluginManager {
42    pub fn probe(conf: Arc<Configuration>) -> Option<PluginManager> {
43        let auto_detected_plugins = auto_detect_plugins();
44
45        if auto_detected_plugins.is_empty() {
46            debug!("No auto-detected plugins found");
47            None
48        } else {
49            debug!("Auto-detected plugins: {:?}", auto_detected_plugins);
50            Some(Self {
51                conf,
52                auto_detected_plugins,
53            })
54        }
55    }
56    fn create_script_devices(
57        &self,
58        provider: &String,
59        script_devices: BTreeMap<String, ScriptDeviceConfiguration>,
60    ) -> Vec<Box<dyn Device>> {
61        script_devices
62            .into_iter()
63            .filter_map(|(id, conf)| {
64                if self.conf.script_devices.get(&id).is_none() {
65                    debug!("registering script device {id} from {provider}");
66                    Some(Box::new(crate::script::ScriptDevice { id, conf }) as _)
67                } else {
68                    debug!("ignoring script device {id} from {provider} as is was already registered in configuration");
69                    None
70                }
71            })
72            .collect()
73    }
74
75    fn create_ssh_devices(
76        &self,
77        provider: &String,
78        ssh_devices: BTreeMap<String, SshDeviceConfiguration>,
79    ) -> Vec<Box<dyn Device>> {
80        ssh_devices.into_iter().filter_map(|(id, conf)| {
81            if self.conf.script_devices.get(&id).is_none() {
82                debug!("registering ssh device {id} from {provider}");
83                Some(Box::new(crate::ssh::SshDevice {
84                    id,
85                    conf,
86                }) as _)
87            } else {
88                debug!("ignoring ssh device {id} from {provider} as is was already registered in configuration");
89                None
90            }
91        }).collect()
92    }
93}
94
95impl PlatformManager for PluginManager {
96    fn devices(&self) -> Result<Vec<Box<dyn Device>>> {
97        let mut result: Vec<Box<dyn Device>> = vec![];
98
99        self.auto_detected_plugins.iter().for_each(|provider| {
100            match get_devices_from_plugin(provider) {
101                Ok(DevicePluginOutput{script_devices, ssh_devices}) => {
102                    if let Some(script_devices) = script_devices {
103                        result.append(&mut self.create_script_devices(provider, script_devices))
104                    }
105
106                    if let Some(ssh_devices) = ssh_devices {
107                        result.append(&mut self.create_ssh_devices(provider, ssh_devices))
108                    }
109
110                }
111                Err(e) => {
112                    debug!(
113                        "failed to get devices from auto detected script provider: {provider}, {e:?}",
114                    );
115                }
116            }
117        });
118
119        Ok(result)
120    }
121
122    fn platforms(&self) -> anyhow::Result<Vec<Box<dyn Platform>>> {
123        let mut script_platforms = BTreeMap::new();
124
125        self.auto_detected_plugins.iter().for_each(
126            |provider| match get_platforms_from_plugin(provider) {
127                Ok(platforms) => {
128                    platforms.into_iter().for_each(|(id, platform)| {
129                        if script_platforms.get(&id).is_none() && self.conf.platforms.get(&id).is_none() {
130                            debug!("registering platform {id} from {provider}");
131                            script_platforms.insert(id.clone(), platform);
132                        } else {
133                            debug!(
134                                "ignoring platform {id} from plugin {provider} as is was already registered"
135                            );
136                        }
137                    });
138                }
139                Err(e) => {
140                    debug!(
141                        "failed to get platforms from auto detected script provider: {provider}, {:?}",
142                        e
143                    );
144                }
145            },
146        );
147
148        Ok(script_platforms.into_values().collect())
149    }
150}
151
152#[derive(Debug, Serialize, Deserialize)]
153pub struct DevicePluginOutput {
154    pub ssh_devices: Option<BTreeMap<String, SshDeviceConfiguration>>,
155    pub script_devices: Option<BTreeMap<String, ScriptDeviceConfiguration>>,
156}
157
158fn get_devices_from_plugin(plugin: &str) -> Result<DevicePluginOutput> {
159    let output = Command::new(plugin).arg("devices").output()?;
160
161    if !output.status.success() {
162        bail!("failed to get devices from auto detected script provider: {:?}, non success return code", plugin);
163    }
164
165    Ok(toml::from_str(
166        &String::from_utf8(output.stdout)
167            .with_context(|| format!("Failed to parse string output from {plugin} devices"))?,
168    )
169    .with_context(|| format!("Failed to parse toml output from {plugin} devices"))?)
170}
171
172fn get_platforms_from_plugin(plugin: &str) -> Result<BTreeMap<String, Box<dyn Platform>>> {
173    let output = Command::new(plugin).arg("platforms").output()?;
174
175    if !output.status.success() {
176        bail!("failed to get platforms from auto detected script provider: {:?}, non success return code", plugin);
177    }
178
179    let platform_configs = toml::from_str::<BTreeMap<String, PlatformConfiguration>>(
180        &String::from_utf8(output.stdout)
181            .with_context(|| format!("Failed to parse string output from {plugin} platforms"))?,
182    )
183    .with_context(|| format!("Failed to parse toml output from {plugin} platforms"))?;
184
185    platform_configs
186        .into_iter()
187        .map(|(name, conf)| {
188            let triple = conf
189                .rustc_triple
190                .clone()
191                .ok_or_else(|| anyhow!("Platform {name} from {plugin} has no rustc_triple"))?;
192            let toolchain = conf
193                .toolchain
194                .clone()
195                .ok_or_else(|| anyhow!("Toolchain missing for platform {name} from {plugin}"))?;
196            Ok((
197                name.clone(),
198                RegularPlatform::new(conf, name, triple, toolchain)?,
199            ))
200        })
201        .collect()
202}
203
204// dinghy will auto-detect any executable in the PATH that starts with `cargo-dinghy-` and try to
205// use it as a plugin.
206fn auto_detect_plugins() -> Vec<String> {
207    let mut binaries = Vec::new();
208
209    if let Some(paths) = env::var_os("PATH") {
210        for path in env::split_paths(&paths) {
211            if let Ok(entries) = fs::read_dir(&path) {
212                for entry in entries.filter_map(|e| e.ok()) {
213                    let path = entry.path();
214                    if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) {
215                        if file_name.starts_with("cargo-dinghy-")
216                            && (path.is_file()
217                                && path
218                                    .metadata()
219                                    .map(|m| m.permissions().mode() & 0o111 != 0)
220                                    .unwrap_or(false))
221                        {
222                            binaries.push(file_name.to_string());
223                        }
224                    }
225                }
226            }
227        }
228    }
229    binaries.sort(); // ensure a deterministic order
230    binaries
231}