use anyhow::{Context, Result, anyhow};
use cargo_metadata::MetadataCommand;
use colored::*;
use std::env;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::{Command, Stdio};
#[cfg(windows)]
const PATH_ENV_VAR_SEPARATOR: char = ';';
#[cfg(windows)]
const COMMAND_EXT: &str = "exe";
#[cfg(not(windows))]
const PATH_ENV_VAR_SEPARATOR: char = ':';
#[cfg(not(windows))]
const COMMAND_EXT: &str = "";
#[derive(Clone, Debug, PartialEq)]
pub enum CommandType {
Installed,
Development,
}
#[derive(Clone, Debug)]
pub struct CommandInfo {
pub name: String,
pub path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathsList {
pub build: Vec<PathBuf>,
pub install: Vec<PathBuf>,
}
pub trait Environment {
fn install_paths() -> Result<Vec<PathBuf>>;
fn build_paths() -> Result<Vec<PathBuf>>;
}
pub struct HostEnvironment;
impl HostEnvironment {
pub fn target_dir() -> Result<PathBuf> {
let target_dir = MetadataCommand::new()
.exec()
.context("Failed to execute cargo metadata")?
.target_directory
.into_std_path_buf();
Ok(target_dir)
}
}
impl Environment for HostEnvironment {
fn install_paths() -> Result<Vec<PathBuf>> {
let mut install_paths: Vec<PathBuf> = env::var("PATH")
.context("Failed to read PATH environment variable")?
.split(PATH_ENV_VAR_SEPARATOR)
.map(PathBuf::from)
.filter(|p| p.is_dir())
.collect();
install_paths.sort();
install_paths.dedup();
Ok(install_paths)
}
fn build_paths() -> Result<Vec<PathBuf>> {
let target_dir = Self::target_dir()?;
let build_paths: Vec<PathBuf> = fs::read_dir(target_dir)?
.filter_map(|entry| {
if let Ok(entry) = entry {
if entry.path().is_dir() {
return Some(entry.path());
}
}
None
})
.collect();
Ok(build_paths)
}
}
pub struct ExternalCommandFinder<E: Environment> {
_phantom: core::marker::PhantomData<E>,
}
impl<E> ExternalCommandFinder<E>
where
E: Environment,
{
fn parse_command_name(path: &Path, prefix: &str) -> Result<String> {
let file_stem = path
.file_stem()
.and_then(|os_str| os_str.to_str())
.ok_or_else(|| anyhow!("Invalid file name"))?;
let command_name = file_stem.strip_prefix(prefix).ok_or_else(|| {
anyhow!(
"Not a {} command: {}",
prefix.trim_end_matches('-'),
file_stem
)
})?;
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
if extension == COMMAND_EXT {
Ok(command_name.to_string())
} else {
Err(anyhow!("Invalid file extension: {}", extension))
}
}
fn list_commands_in_path(
path: &Path,
prefix: &str,
command_type: CommandType,
) -> Result<Vec<CommandInfo>> {
let commands = fs::read_dir(path)
.with_context(|| format!("Failed to read directory at: {:?}", path.to_str()))?
.map(|entry| {
entry.map(|e| e.path()).with_context(|| {
format!("Failed to read entry in directory: {:?}", path.to_str())
})
})
.filter_map(|entry_path| {
entry_path
.as_ref()
.map_err(|e| anyhow!("Failed to get PathBuf: {}", e))
.and_then(|entry_path_buf| {
Self::parse_command_name(entry_path_buf, prefix)
.map(|parsed_name| {
let mut command_name = parsed_name.to_string();
if command_type == CommandType::Development {
if let Some(build_type) =
path.file_name().and_then(|os_str| os_str.to_str())
{
const NAME_SEPARATOR: &str = "-";
command_name.push_str(NAME_SEPARATOR);
command_name.push_str(build_type);
}
};
CommandInfo {
name: command_name,
path: entry_path_buf.to_owned(),
}
})
.map_err(|e| anyhow!("Failed to parse command name: {}", e))
})
.ok()
})
.collect();
Ok(commands)
}
pub fn paths_for_prefix(_prefix: &str) -> Result<PathsList> {
let build = E::build_paths().unwrap_or_default();
let install = E::install_paths().unwrap_or_default();
Ok(PathsList { build, install })
}
pub fn commands_with_prefix(prefix: &str) -> Result<Vec<CommandInfo>> {
let search_paths = Self::paths_for_prefix(prefix).context("Failed to list paths")?;
let mut commands = Vec::new();
for path in &search_paths.build {
commands.extend(Self::list_commands_in_path(
path,
prefix,
CommandType::Development,
)?);
}
for path in &search_paths.install {
commands.extend(Self::list_commands_in_path(
path,
prefix,
CommandType::Installed,
)?);
}
commands.sort_by_cached_key(|command| {
command.path.file_name().unwrap_or_default().to_os_string()
});
Ok(commands)
}
}
pub trait CommandExecutor {
fn execute(command_info: &CommandInfo, args: Option<&[String]>) -> Result<()>;
}
pub struct ExternalCommandExecutor;
impl CommandExecutor for ExternalCommandExecutor {
fn execute(command_info: &CommandInfo, args: Option<&[String]>) -> Result<()> {
let mut command = Command::new(&command_info.path);
command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
if let Some(arguments) = args {
command.args(arguments);
}
command
.status()
.with_context(|| format!("Failed to execute command: {:?}", command_info.path))?;
Ok(())
}
}
pub fn execute<E: Environment>(
prefix: &str,
command_name: &str,
args: Option<&[String]>,
) -> Result<()> {
let all_commands = ExternalCommandFinder::<E>::commands_with_prefix(prefix)
.context("Failed to find command binaries")?;
let command = all_commands
.into_iter()
.find(|command| command.name == command_name)
.ok_or_else(|| anyhow!("Command not found: {}", command_name))?;
ExternalCommandExecutor::execute(&command, args)
}
pub fn list<E: Environment>(prefix: &str) -> Result<()> {
let commands = ExternalCommandFinder::<E>::commands_with_prefix(prefix)?;
println!("{}", "Discovered Commands:".bright_green().bold());
for command in commands {
println!(" {}", command.name.bold());
}
Ok(())
}
pub fn paths<E: Environment>(prefix: &str) -> Result<()> {
let search_paths = ExternalCommandFinder::<E>::paths_for_prefix(prefix)
.context("Failed to list search paths")?;
if !search_paths.build.is_empty() {
println!("{}", "Build Paths:".bright_green().bold());
for dir in &search_paths.build {
println!(" {}", dir.display().to_string().bold());
}
println!();
}
if !search_paths.install.is_empty() {
println!("{}", "Install Paths:".bright_green().bold());
for dir in &search_paths.install {
println!(" {}", dir.display().to_string().bold());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use iceoryx2_bb_testing::assert_that;
use std::env;
use std::fs::File;
use tempfile::TempDir;
const PREFIX: &str = "iox2-";
const FOO_COMMAND: &str = "Xt7bK9pL";
const BAR_COMMAND: &str = "m3Qf8RzN";
const BAZ_COMMAND: &str = "P5hJ2wAc";
fn create_noop_executable(file_path: &std::path::Path) -> std::io::Result<()> {
use std::process::Command;
let src_file = file_path.with_extension("rs");
std::fs::write(&src_file, "fn main() {}")?;
let output = Command::new("rustc")
.arg(&src_file)
.arg("-o")
.arg(file_path)
.arg("--crate-type")
.arg("bin")
.output()?;
std::fs::remove_file(&src_file).ok();
if !output.status.success() {
return Err(std::io::Error::other(format!(
"Failed to compile noop executable: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(())
}
macro_rules! create_file {
($path:expr, $file:expr) => {{
let file_path = $path.join($file);
#[cfg(unix)]
const COMMAND_EXT: &str = "";
#[cfg(windows)]
const COMMAND_EXT: &str = "exe";
let extension = file_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
if extension == COMMAND_EXT || (cfg!(unix) && extension.is_empty()) {
create_noop_executable(&file_path).expect("Failed to create noop executable");
} else {
File::create(&file_path).expect("Failed to create file");
}
}};
}
struct TestEnv {
_temp_dir: TempDir,
original_path: String,
}
impl TestEnv {
fn setup() -> Self {
let original_path = env::var("PATH").expect("Failed to get PATH");
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_path = temp_dir.path().to_path_buf();
let mut paths = env::split_paths(&original_path).collect::<Vec<_>>();
paths.push(temp_path.clone());
let new_path = env::join_paths(paths).expect("Failed to join paths");
unsafe {
env::set_var("PATH", &new_path);
}
create_file!(temp_path, format!("{}{}", PREFIX, FOO_COMMAND));
create_file!(temp_path, format!("{}{}.d", PREFIX, FOO_COMMAND));
create_file!(temp_path, format!("{}{}.exe", PREFIX, FOO_COMMAND));
create_file!(temp_path, format!("{}{}", PREFIX, BAR_COMMAND));
create_file!(temp_path, format!("{}{}.d", PREFIX, BAR_COMMAND));
create_file!(temp_path, format!("{}{}.exe", PREFIX, BAR_COMMAND));
create_file!(temp_path, BAZ_COMMAND);
create_file!(temp_path, format!("{}.d", BAZ_COMMAND));
create_file!(temp_path, format!("{}.exe", BAZ_COMMAND));
TestEnv {
_temp_dir: temp_dir,
original_path,
}
}
}
impl Drop for TestEnv {
fn drop(&mut self) {
unsafe {
env::set_var("PATH", &self.original_path);
}
}
}
#[test]
fn test_list() {
let _test_env = TestEnv::setup();
let commands = ExternalCommandFinder::<HostEnvironment>::commands_with_prefix(PREFIX)
.expect("Failed to retrieve commands");
assert_that!(
commands,
contains_match | command | command.name == FOO_COMMAND
);
assert_that!(
commands,
contains_match | command | command.name == BAR_COMMAND
);
assert_that!(
commands,
not_contains_match | command | command.name == BAZ_COMMAND
);
}
#[test]
fn test_execute() {
let _test_env = TestEnv::setup();
let commands = ExternalCommandFinder::<HostEnvironment>::commands_with_prefix(PREFIX)
.unwrap_or_else(|e| {
panic!("Failed to retrieve commands: {}", e);
});
let [foo_command, ..] = commands
.iter()
.filter(|cmd| cmd.name == FOO_COMMAND)
.collect::<Vec<_>>()[..]
else {
panic!("Failed to extract CommandInfo of test files");
};
let result = ExternalCommandExecutor::execute(foo_command, None);
if let Err(ref e) = result {
println!("Error executing command: {}", e);
}
assert_that!(result, is_ok);
let args = vec!["arg1".to_string(), "arg2".to_string()];
let result = ExternalCommandExecutor::execute(foo_command, Some(&args));
assert_that!(result, is_ok);
}
}