use serde::{Deserialize, Serialize}; use std::ffi::OsStr; use std::io; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::Mutex; use super::local::LocalDriver; use super::schroot::SchrootDriver; use super::ssh::SshDriver; use super::unshare::UnshareDriver; pub trait ContextDriver { fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result; fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>; fn list_files(&self, path: &Path) -> io::Result>; fn run( &self, program: &str, args: &[String], env: &[(String, String)], cwd: Option<&str>, ) -> io::Result; fn run_output( &self, program: &str, args: &[String], env: &[(String, String)], cwd: Option<&str>, ) -> io::Result; fn create_temp_dir(&self) -> io::Result; fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()>; fn read_file(&self, path: &Path) -> io::Result; fn write_file(&self, path: &Path, content: &str) -> io::Result<()>; } /// Represents an execution environment (Local or via SSH). /// /// This is the data model that configuration files store. It defines *where* operations happen. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type")] #[derive(Default)] pub enum ContextConfig { #[serde(rename = "local")] #[default] Local, #[serde(rename = "ssh")] Ssh { host: String, user: Option, port: Option, }, #[serde(rename = "schroot")] Schroot { name: String, parent: Option, }, #[serde(rename = "unshare")] Unshare { path: String, parent: Option, }, } pub struct Context { pub config: ContextConfig, pub parent: Option>, driver: Mutex>>, } impl Context { pub fn new(config: ContextConfig) -> Self { let parent = match &config { ContextConfig::Schroot { parent: Some(parent_name), .. } | ContextConfig::Unshare { parent: Some(parent_name), .. } => { let config_lock = crate::context::manager::MANAGER.get_config(); let parent_config = config_lock .contexts .get(parent_name) .cloned() .expect("Parent context not found"); Some(Arc::new(Context::new(parent_config))) } _ => None, }; Self { config, parent, driver: Mutex::new(None), } } pub fn with_parent(config: ContextConfig, parent: Arc) -> Self { Self { config, parent: Some(parent), driver: Mutex::new(None), } } pub fn command>(&self, program: S) -> ContextCommand<'_> { ContextCommand { context: self, program: program.as_ref().to_string_lossy().to_string(), args: Vec::new(), env: Vec::new(), cwd: None, } } /// Ensures that the source file/directory exists at the destination context. /// /// If Local: Returns the absolute path of `src`. /// If Remote: Copies `src` to `dest_root` on the remote and returns the path to the copied entity. pub fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result { self.driver() .as_ref() .unwrap() .ensure_available(src, dest_root) } pub fn create_temp_dir(&self) -> io::Result { self.driver().as_ref().unwrap().create_temp_dir() } /// Retrieve a file or directory from the context to the local filesystem. /// /// The `src` path is on the context, `dest` is on the local machine. /// If `src` is a directory, it is copied recursively. pub fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> { self.driver().as_ref().unwrap().retrieve_path(src, dest) } /// List files in a directory on the context. pub fn list_files(&self, path: &Path) -> io::Result> { self.driver().as_ref().unwrap().list_files(path) } pub fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> { self.driver().as_ref().unwrap().copy_path(src, dest) } pub fn read_file(&self, path: &Path) -> io::Result { self.driver().as_ref().unwrap().read_file(path) } pub fn write_file(&self, path: &Path, content: &str) -> io::Result<()> { self.driver().as_ref().unwrap().write_file(path, content) } pub fn driver( &self, ) -> std::sync::MutexGuard<'_, Option>> { let mut driver_lock = self.driver.lock().unwrap(); if driver_lock.is_none() { let driver: Box = match &self.config { ContextConfig::Local => Box::new(LocalDriver), ContextConfig::Ssh { host, user, port } => Box::new(SshDriver { host: host.clone(), user: user.clone(), port: *port, }), ContextConfig::Schroot { name, .. } => Box::new(SchrootDriver { name: name.clone(), session: std::sync::Mutex::new(None), parent: self.parent.clone(), }), ContextConfig::Unshare { path, .. } => Box::new(UnshareDriver { path: path.clone(), parent: self.parent.clone(), }), }; *driver_lock = Some(driver); } driver_lock } pub fn clone_raw(&self) -> Self { Self { config: self.config.clone(), parent: self.parent.clone(), driver: std::sync::Mutex::new(None), } } } /// A builder and executor for commands within a specific `Context`. /// /// This struct acts as a high-level facade (API) for command creation. It hides the implementation /// details of *how* the command is actually executed, allowing the user to simple chain arguments /// and call `status()` or `output()`. /// /// It delegates the actual work to a `ContextDriver`. pub struct ContextCommand<'a> { context: &'a Context, program: String, args: Vec, env: Vec<(String, String)>, cwd: Option, } impl<'a> ContextCommand<'a> { pub fn arg>(&mut self, arg: S) -> &mut Self { self.args.push(arg.as_ref().to_string_lossy().to_string()); self } // Support chaining args pub fn args(&mut self, args: I) -> &mut Self where I: IntoIterator, S: AsRef, { for arg in args { self.arg(arg); } self } pub fn env(&mut self, key: K, val: V) -> &mut Self where K: AsRef, V: AsRef, { self.env.push(( key.as_ref().to_string_lossy().to_string(), val.as_ref().to_string_lossy().to_string(), )); self } pub fn envs(&mut self, vars: I) -> &mut Self where I: IntoIterator, K: AsRef, V: AsRef, { for (key, val) in vars { self.env(key, val); } self } pub fn current_dir>(&mut self, dir: P) -> &mut Self { self.cwd = Some(dir.as_ref().to_string_lossy().to_string()); self } pub fn status(&mut self) -> io::Result { self.context.driver().as_ref().unwrap().run( &self.program, &self.args, &self.env, self.cwd.as_deref(), ) } // Capture output pub fn output(&mut self) -> io::Result { self.context.driver().as_ref().unwrap().run_output( &self.program, &self.args, &self.env, self.cwd.as_deref(), ) } }