1use anyhow::{Context, Result, anyhow};
14use cargo_metadata::MetadataCommand;
15use colored::*;
16use std::env;
17use std::fs;
18use std::path::Path;
19use std::path::PathBuf;
20use std::process::{Command, Stdio};
21
22#[cfg(windows)]
23const PATH_ENV_VAR_SEPARATOR: char = ';';
24#[cfg(windows)]
25const COMMAND_EXT: &str = "exe";
26
27#[cfg(not(windows))]
28const PATH_ENV_VAR_SEPARATOR: char = ':';
29#[cfg(not(windows))]
30const COMMAND_EXT: &str = "";
31
32#[derive(Clone, Debug, PartialEq)]
33pub enum CommandType {
34 Installed,
35 Development,
36}
37
38#[derive(Clone, Debug)]
39pub struct CommandInfo {
40 pub name: String,
41 pub path: PathBuf,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct PathsList {
46 pub build: Vec<PathBuf>,
47 pub install: Vec<PathBuf>,
48}
49
50pub trait Environment {
51 fn install_paths() -> Result<Vec<PathBuf>>;
52 fn build_paths() -> Result<Vec<PathBuf>>;
53}
54
55pub struct HostEnvironment;
56
57impl HostEnvironment {
58 pub fn target_dir() -> Result<PathBuf> {
59 let target_dir = MetadataCommand::new()
60 .exec()
61 .context("Failed to execute cargo metadata")?
62 .target_directory
63 .into_std_path_buf();
64 Ok(target_dir)
65 }
66}
67
68impl Environment for HostEnvironment {
69 fn install_paths() -> Result<Vec<PathBuf>> {
71 let mut install_paths: Vec<PathBuf> = env::var("PATH")
72 .context("Failed to read PATH environment variable")?
73 .split(PATH_ENV_VAR_SEPARATOR)
74 .map(PathBuf::from)
75 .filter(|p| p.is_dir())
76 .collect();
77
78 install_paths.sort();
79 install_paths.dedup();
80
81 Ok(install_paths)
82 }
83
84 fn build_paths() -> Result<Vec<PathBuf>> {
85 let target_dir = Self::target_dir()?;
86 let build_paths: Vec<PathBuf> = fs::read_dir(target_dir)?
87 .filter_map(|entry| {
88 if let Ok(entry) = entry {
89 if entry.path().is_dir() {
90 return Some(entry.path());
91 }
92 }
93 None
94 })
95 .collect();
96
97 Ok(build_paths)
98 }
99}
100
101pub struct ExternalCommandFinder<E: Environment> {
102 _phantom: core::marker::PhantomData<E>,
103}
104
105impl<E> ExternalCommandFinder<E>
106where
107 E: Environment,
108{
109 fn parse_command_name(path: &Path, prefix: &str) -> Result<String> {
110 let file_stem = path
111 .file_stem()
112 .and_then(|os_str| os_str.to_str())
113 .ok_or_else(|| anyhow!("Invalid file name"))?;
114
115 let command_name = file_stem.strip_prefix(prefix).ok_or_else(|| {
116 anyhow!(
117 "Not a {} command: {}",
118 prefix.trim_end_matches('-'),
119 file_stem
120 )
121 })?;
122
123 let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
124 if extension == COMMAND_EXT {
125 Ok(command_name.to_string())
126 } else {
127 Err(anyhow!("Invalid file extension: {}", extension))
128 }
129 }
130
131 fn list_commands_in_path(
132 path: &Path,
133 prefix: &str,
134 command_type: CommandType,
135 ) -> Result<Vec<CommandInfo>> {
136 let commands = fs::read_dir(path)
137 .with_context(|| format!("Failed to read directory at: {:?}", path.to_str()))?
138 .map(|entry| {
139 entry.map(|e| e.path()).with_context(|| {
140 format!("Failed to read entry in directory: {:?}", path.to_str())
141 })
142 })
143 .filter_map(|entry_path| {
144 entry_path
145 .as_ref()
146 .map_err(|e| anyhow!("Failed to get PathBuf: {}", e))
147 .and_then(|entry_path_buf| {
148 Self::parse_command_name(entry_path_buf, prefix)
149 .map(|parsed_name| {
150 let mut command_name = parsed_name.to_string();
156 if command_type == CommandType::Development {
157 if let Some(build_type) =
158 path.file_name().and_then(|os_str| os_str.to_str())
159 {
160 const NAME_SEPARATOR: &str = "-";
161 command_name.push_str(NAME_SEPARATOR);
162 command_name.push_str(build_type);
163 }
164 };
165
166 CommandInfo {
167 name: command_name,
168 path: entry_path_buf.to_owned(),
169 }
170 })
171 .map_err(|e| anyhow!("Failed to parse command name: {}", e))
172 })
173 .ok()
174 })
175 .collect();
176
177 Ok(commands)
178 }
179
180 pub fn paths_for_prefix(_prefix: &str) -> Result<PathsList> {
181 let build = E::build_paths().unwrap_or_default();
182 let install = E::install_paths().unwrap_or_default();
183
184 Ok(PathsList { build, install })
185 }
186
187 pub fn commands_with_prefix(prefix: &str) -> Result<Vec<CommandInfo>> {
188 let search_paths = Self::paths_for_prefix(prefix).context("Failed to list paths")?;
189 let mut commands = Vec::new();
190
191 for path in &search_paths.build {
192 commands.extend(Self::list_commands_in_path(
193 path,
194 prefix,
195 CommandType::Development,
196 )?);
197 }
198 for path in &search_paths.install {
199 commands.extend(Self::list_commands_in_path(
200 path,
201 prefix,
202 CommandType::Installed,
203 )?);
204 }
205 commands.sort_by_cached_key(|command| {
206 command.path.file_name().unwrap_or_default().to_os_string()
207 });
208
209 Ok(commands)
210 }
211}
212
213pub trait CommandExecutor {
214 fn execute(command_info: &CommandInfo, args: Option<&[String]>) -> Result<()>;
215}
216
217pub struct ExternalCommandExecutor;
218
219impl CommandExecutor for ExternalCommandExecutor {
220 fn execute(command_info: &CommandInfo, args: Option<&[String]>) -> Result<()> {
221 let mut command = Command::new(&command_info.path);
222 command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
223 if let Some(arguments) = args {
224 command.args(arguments);
225 }
226 command
227 .status()
228 .with_context(|| format!("Failed to execute command: {:?}", command_info.path))?;
229 Ok(())
230 }
231}
232
233pub fn execute<E: Environment>(
234 prefix: &str,
235 command_name: &str,
236 args: Option<&[String]>,
237) -> Result<()> {
238 let all_commands = ExternalCommandFinder::<E>::commands_with_prefix(prefix)
239 .context("Failed to find command binaries")?;
240
241 let command = all_commands
242 .into_iter()
243 .find(|command| command.name == command_name)
244 .ok_or_else(|| anyhow!("Command not found: {}", command_name))?;
245
246 ExternalCommandExecutor::execute(&command, args)
247}
248
249pub fn list<E: Environment>(prefix: &str) -> Result<()> {
250 let commands = ExternalCommandFinder::<E>::commands_with_prefix(prefix)?;
251
252 println!("{}", "Discovered Commands:".bright_green().bold());
253 for command in commands {
254 println!(" {}", command.name.bold());
255 }
256
257 Ok(())
258}
259
260pub fn paths<E: Environment>(prefix: &str) -> Result<()> {
261 let search_paths = ExternalCommandFinder::<E>::paths_for_prefix(prefix)
262 .context("Failed to list search paths")?;
263
264 if !search_paths.build.is_empty() {
265 println!("{}", "Build Paths:".bright_green().bold());
266 for dir in &search_paths.build {
267 println!(" {}", dir.display().to_string().bold());
268 }
269 println!();
270 }
271 if !search_paths.install.is_empty() {
272 println!("{}", "Install Paths:".bright_green().bold());
273 for dir in &search_paths.install {
274 println!(" {}", dir.display().to_string().bold());
275 }
276 }
277
278 Ok(())
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use iceoryx2_bb_testing::assert_that;
285 use std::env;
286 use std::fs::File;
287 use tempfile::TempDir;
288
289 const PREFIX: &str = "iox2-";
290 const FOO_COMMAND: &str = "Xt7bK9pL";
291 const BAR_COMMAND: &str = "m3Qf8RzN";
292 const BAZ_COMMAND: &str = "P5hJ2wAc";
293
294 fn create_noop_executable(file_path: &std::path::Path) -> std::io::Result<()> {
295 use std::process::Command;
296
297 let src_file = file_path.with_extension("rs");
298 std::fs::write(&src_file, "fn main() {}")?;
299 let output = Command::new("rustc")
300 .arg(&src_file)
301 .arg("-o")
302 .arg(file_path)
303 .arg("--crate-type")
304 .arg("bin")
305 .output()?;
306 std::fs::remove_file(&src_file).ok();
307
308 if !output.status.success() {
309 return Err(std::io::Error::other(format!(
310 "Failed to compile noop executable: {}",
311 String::from_utf8_lossy(&output.stderr)
312 )));
313 }
314
315 Ok(())
316 }
317
318 macro_rules! create_file {
319 ($path:expr, $file:expr) => {{
320 let file_path = $path.join($file);
321
322 #[cfg(unix)]
323 const COMMAND_EXT: &str = "";
324 #[cfg(windows)]
325 const COMMAND_EXT: &str = "exe";
326
327 let extension = file_path
328 .extension()
329 .and_then(|ext| ext.to_str())
330 .unwrap_or("");
331
332 if extension == COMMAND_EXT || (cfg!(unix) && extension.is_empty()) {
333 create_noop_executable(&file_path).expect("Failed to create noop executable");
334 } else {
335 File::create(&file_path).expect("Failed to create file");
337 }
338 }};
339 }
340
341 struct TestEnv {
342 _temp_dir: TempDir,
343 original_path: String,
344 }
345
346 impl TestEnv {
347 fn setup() -> Self {
348 let original_path = env::var("PATH").expect("Failed to get PATH");
349
350 let temp_dir = TempDir::new().expect("Failed to create temp dir");
351 let temp_path = temp_dir.path().to_path_buf();
352
353 let mut paths = env::split_paths(&original_path).collect::<Vec<_>>();
354 paths.push(temp_path.clone());
355 let new_path = env::join_paths(paths).expect("Failed to join paths");
356 unsafe {
357 env::set_var("PATH", &new_path);
358 }
359
360 create_file!(temp_path, format!("{}{}", PREFIX, FOO_COMMAND));
361 create_file!(temp_path, format!("{}{}.d", PREFIX, FOO_COMMAND));
362 create_file!(temp_path, format!("{}{}.exe", PREFIX, FOO_COMMAND));
363 create_file!(temp_path, format!("{}{}", PREFIX, BAR_COMMAND));
364 create_file!(temp_path, format!("{}{}.d", PREFIX, BAR_COMMAND));
365 create_file!(temp_path, format!("{}{}.exe", PREFIX, BAR_COMMAND));
366 create_file!(temp_path, BAZ_COMMAND);
367 create_file!(temp_path, format!("{}.d", BAZ_COMMAND));
368 create_file!(temp_path, format!("{}.exe", BAZ_COMMAND));
369
370 TestEnv {
371 _temp_dir: temp_dir,
372 original_path,
373 }
374 }
375 }
376
377 impl Drop for TestEnv {
378 fn drop(&mut self) {
379 unsafe {
380 env::set_var("PATH", &self.original_path);
381 }
382 }
383 }
384
385 #[test]
386 fn test_list() {
387 let _test_env = TestEnv::setup();
388
389 let commands = ExternalCommandFinder::<HostEnvironment>::commands_with_prefix(PREFIX)
390 .expect("Failed to retrieve commands");
391
392 assert_that!(
393 commands,
394 contains_match | command | command.name == FOO_COMMAND
395 );
396 assert_that!(
397 commands,
398 contains_match | command | command.name == BAR_COMMAND
399 );
400 assert_that!(
401 commands,
402 not_contains_match | command | command.name == BAZ_COMMAND
403 );
404 }
405
406 #[test]
407 fn test_execute() {
408 let _test_env = TestEnv::setup();
409
410 let commands = ExternalCommandFinder::<HostEnvironment>::commands_with_prefix(PREFIX)
411 .unwrap_or_else(|e| {
412 panic!("Failed to retrieve commands: {}", e);
413 });
414
415 let [foo_command, ..] = commands
416 .iter()
417 .filter(|cmd| cmd.name == FOO_COMMAND)
418 .collect::<Vec<_>>()[..]
419 else {
420 panic!("Failed to extract CommandInfo of test files");
421 };
422
423 let result = ExternalCommandExecutor::execute(foo_command, None);
424 if let Err(ref e) = result {
425 println!("Error executing command: {}", e);
426 }
427
428 assert_that!(result, is_ok);
429
430 let args = vec!["arg1".to_string(), "arg2".to_string()];
431 let result = ExternalCommandExecutor::execute(foo_command, Some(&args));
432 assert_that!(result, is_ok);
433 }
434}