diff --git a/cargo-dinghy/src/main.rs b/cargo-dinghy/src/main.rs index 15d8afc..6ef3f2b 100644 --- a/cargo-dinghy/src/main.rs +++ b/cargo-dinghy/src/main.rs @@ -24,16 +24,14 @@ use crate::cli::{DinghyCli, DinghyMode, DinghySubcommand, SubCommandWrapper}; mod cli; fn main() { - let cli = DinghyCli::parse(); - - //env::set_var("DINGHY_LOG", "trace"); - env_logger::init_from_env( env_logger::Env::new() .filter("DINGHY_LOG") .write_style("DINGHY_LOG_STYLE"), ); + let cli = DinghyCli::parse(); + set_current_verbosity(cli.args.verbose as i8); if let Err(e) = run_command(cli) { diff --git a/dinghy-lib/src/lib.rs b/dinghy-lib/src/lib.rs index 0bc47fd..81131bd 100644 --- a/dinghy-lib/src/lib.rs +++ b/dinghy-lib/src/lib.rs @@ -11,6 +11,7 @@ pub mod device; mod host; pub mod overlay; pub mod platform; +pub mod plugin; pub mod project; mod script; mod ssh; @@ -65,6 +66,9 @@ impl Dinghy { managers.push(Box::new(man)); } } + if let Some(man) = plugin::PluginManager::probe(conf.clone()) { + managers.push(Box::new(man)); + } let mut devices = vec![]; let mut platforms = vec![]; diff --git a/dinghy-lib/src/plugin/mod.rs b/dinghy-lib/src/plugin/mod.rs new file mode 100644 index 0000000..e705a39 --- /dev/null +++ b/dinghy-lib/src/plugin/mod.rs @@ -0,0 +1,231 @@ +use crate::config::{PlatformConfiguration, ScriptDeviceConfiguration, SshDeviceConfiguration}; +use crate::platform::regular_platform::RegularPlatform; +use crate::{Configuration, Device, Platform, PlatformManager}; +use anyhow::{anyhow, bail, Context, Result}; +use log::debug; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; +use std::sync::Arc; +use std::{env, fs}; + +/// This platform manager will auto-detect any executable in the PATH that starts with +/// `cargo-dinghy-` and try to use them as a plugin to provide devices and platforms. +/// +/// To be a valid plugin, an executable must implement the following subcommands: +/// - `devices`: must output a TOML file with a `DevicePluginOutput` structure +/// - `platforms`: must output a TOML file with a `BTreeMap` structure +/// +/// Here is example of output for a `cargo-dinghy-foo` plugin configuring a `bar` device and a `baz` +/// platform: +/// +/// ```no_compile +/// $ cargo-dinghy-foo devices +/// [ssh_devices.bar] +/// hostname = "127.0.0.1" +/// username = "user" +/// +/// $ cargo-dinghy-foo platforms +/// [baz] +/// rustc_triple = "aarch64-unknown-linux-gnu" +/// toolchain = "/path/to/toolchain" +/// ``` +/// This is quite useful if you have a bench of devices and platforms that can be auto-detected +/// or are already configured in another tool. +pub struct PluginManager { + conf: Arc, + auto_detected_plugins: Vec, +} + +impl PluginManager { + pub fn probe(conf: Arc) -> Option { + let auto_detected_plugins = auto_detect_plugins(); + + if auto_detected_plugins.is_empty() { + debug!("No auto-detected plugins found"); + None + } else { + debug!("Auto-detected plugins: {:?}", auto_detected_plugins); + Some(Self { + conf, + auto_detected_plugins, + }) + } + } + fn create_script_devices( + &self, + provider: &String, + script_devices: BTreeMap, + ) -> Vec> { + script_devices + .into_iter() + .filter_map(|(id, conf)| { + if self.conf.script_devices.get(&id).is_none() { + debug!("registering script device {id} from {provider}"); + Some(Box::new(crate::script::ScriptDevice { id, conf }) as _) + } else { + debug!("ignoring script device {id} from {provider} as is was already registered in configuration"); + None + } + }) + .collect() + } + + fn create_ssh_devices( + &self, + provider: &String, + ssh_devices: BTreeMap, + ) -> Vec> { + ssh_devices.into_iter().filter_map(|(id, conf)| { + if self.conf.script_devices.get(&id).is_none() { + debug!("registering ssh device {id} from {provider}"); + Some(Box::new(crate::ssh::SshDevice { + id, + conf, + }) as _) + } else { + debug!("ignoring ssh device {id} from {provider} as is was already registered in configuration"); + None + } + }).collect() + } +} + +impl PlatformManager for PluginManager { + fn devices(&self) -> Result>> { + let mut result: Vec> = vec![]; + + self.auto_detected_plugins.iter().for_each(|provider| { + match get_devices_from_plugin(provider) { + Ok(DevicePluginOutput{script_devices, ssh_devices}) => { + if let Some(script_devices) = script_devices { + result.append(&mut self.create_script_devices(provider, script_devices)) + } + + if let Some(ssh_devices) = ssh_devices { + result.append(&mut self.create_ssh_devices(provider, ssh_devices)) + } + + } + Err(e) => { + debug!( + "failed to get devices from auto detected script provider: {provider}, {e:?}", + ); + } + } + }); + + Ok(result) + } + + fn platforms(&self) -> anyhow::Result>> { + let mut script_platforms = BTreeMap::new(); + + self.auto_detected_plugins.iter().for_each( + |provider| match get_platforms_from_plugin(provider) { + Ok(platforms) => { + platforms.into_iter().for_each(|(id, platform)| { + if script_platforms.get(&id).is_none() && self.conf.platforms.get(&id).is_none() { + debug!("registering platform {id} from {provider}"); + script_platforms.insert(id.clone(), platform); + } else { + debug!( + "ignoring platform {id} from plugin {provider} as is was already registered" + ); + } + }); + } + Err(e) => { + debug!( + "failed to get platforms from auto detected script provider: {provider}, {:?}", + e + ); + } + }, + ); + + Ok(script_platforms.into_values().collect()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DevicePluginOutput { + pub ssh_devices: Option>, + pub script_devices: Option>, +} + +fn get_devices_from_plugin(plugin: &str) -> Result { + let output = Command::new(plugin).arg("devices").output()?; + + if !output.status.success() { + bail!("failed to get devices from auto detected script provider: {:?}, non success return code", plugin); + } + + Ok(toml::from_str( + &String::from_utf8(output.stdout) + .with_context(|| format!("Failed to parse string output from {plugin} devices"))?, + ) + .with_context(|| format!("Failed to parse toml output from {plugin} devices"))?) +} + +fn get_platforms_from_plugin(plugin: &str) -> Result>> { + let output = Command::new(plugin).arg("platforms").output()?; + + if !output.status.success() { + bail!("failed to get platforms from auto detected script provider: {:?}, non success return code", plugin); + } + + let platform_configs = toml::from_str::>( + &String::from_utf8(output.stdout) + .with_context(|| format!("Failed to parse string output from {plugin} platforms"))?, + ) + .with_context(|| format!("Failed to parse toml output from {plugin} platforms"))?; + + platform_configs + .into_iter() + .map(|(name, conf)| { + let triple = conf + .rustc_triple + .clone() + .ok_or_else(|| anyhow!("Platform {name} from {plugin} has no rustc_triple"))?; + let toolchain = conf + .toolchain + .clone() + .ok_or_else(|| anyhow!("Toolchain missing for platform {name} from {plugin}"))?; + Ok(( + name.clone(), + RegularPlatform::new(conf, name, triple, toolchain)?, + )) + }) + .collect() +} + +// dinghy will auto-detect any executable in the PATH that starts with `cargo-dinghy-` and try to +// use it as a plugin. +fn auto_detect_plugins() -> Vec { + let mut binaries = Vec::new(); + + if let Some(paths) = env::var_os("PATH") { + for path in env::split_paths(&paths) { + if let Ok(entries) = fs::read_dir(&path) { + for entry in entries.filter_map(|e| e.ok()) { + let path = entry.path(); + if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) { + if file_name.starts_with("cargo-dinghy-") + && (path.is_file() + && path + .metadata() + .map(|m| m.permissions().mode() & 0o111 != 0) + .unwrap_or(false)) + { + binaries.push(file_name.to_string()); + } + } + } + } + } + } + binaries.sort(); // ensure a deterministic order + binaries +} diff --git a/dinghy-lib/src/script/mod.rs b/dinghy-lib/src/script/mod.rs index 362ae93..04ff763 100644 --- a/dinghy-lib/src/script/mod.rs +++ b/dinghy-lib/src/script/mod.rs @@ -3,7 +3,7 @@ use std::sync; mod device; -use self::device::ScriptDevice; +pub use self::device::ScriptDevice; pub struct ScriptDeviceManager { conf: sync::Arc, diff --git a/dinghy-lib/src/ssh/mod.rs b/dinghy-lib/src/ssh/mod.rs index 4c03f42..6ab3067 100644 --- a/dinghy-lib/src/ssh/mod.rs +++ b/dinghy-lib/src/ssh/mod.rs @@ -2,7 +2,7 @@ mod device; use crate::{Configuration, Device, Platform, PlatformManager, Result}; use std::sync; -use self::device::SshDevice; +pub use self::device::SshDevice; pub struct SshDeviceManager { conf: sync::Arc,