Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce plugin platform #242

Merged
merged 1 commit into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions cargo-dinghy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions dinghy-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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![];
Expand Down
231 changes: 231 additions & 0 deletions dinghy-lib/src/plugin/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String, PlatformConfiguration>` 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<Configuration>,
auto_detected_plugins: Vec<String>,
}

impl PluginManager {
pub fn probe(conf: Arc<Configuration>) -> Option<PluginManager> {
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<String, ScriptDeviceConfiguration>,
) -> Vec<Box<dyn Device>> {
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<String, SshDeviceConfiguration>,
) -> Vec<Box<dyn Device>> {
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<Vec<Box<dyn Device>>> {
let mut result: Vec<Box<dyn Device>> = 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<Vec<Box<dyn Platform>>> {
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<BTreeMap<String, SshDeviceConfiguration>>,
pub script_devices: Option<BTreeMap<String, ScriptDeviceConfiguration>>,
}

fn get_devices_from_plugin(plugin: &str) -> Result<DevicePluginOutput> {
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<BTreeMap<String, Box<dyn Platform>>> {
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::<BTreeMap<String, PlatformConfiguration>>(
&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<String> {
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
}
2 changes: 1 addition & 1 deletion dinghy-lib/src/script/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::sync;

mod device;

use self::device::ScriptDevice;
pub use self::device::ScriptDevice;

pub struct ScriptDeviceManager {
conf: sync::Arc<Configuration>,
Expand Down
2 changes: 1 addition & 1 deletion dinghy-lib/src/ssh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Configuration>,
Expand Down
Loading