diff --git a/src/main.rs b/src/main.rs index d900cdf..cf403a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod procdata; mod profile; mod rootfs; mod scanner; +mod shcall; use crate::profile::Profile; use clap::{ArgMatches, Command}; use colored::Colorize; diff --git a/src/procdata.rs b/src/procdata.rs index a977f69..c52657a 100644 --- a/src/procdata.rs +++ b/src/procdata.rs @@ -3,6 +3,7 @@ use crate::{ profile::Profile, rootfs, scanner::{binlib::ElfScanner, debpkg::DebPackageScanner, dlst::ContentFormatter, general::Scanner}, + shcall::ShellScript, }; use std::fs::{self, canonicalize, remove_file, DirEntry, File}; use std::{ @@ -141,14 +142,42 @@ impl TintProcessor { np } + /// Call a script hook + fn call_script(s: String) -> Result<(), Error> { + // XXX: It can run args, but from where pass them? Profile? CLI? Both? None at all?.. + let (stdout, stderr) = ShellScript::new(s, None).run()?; + + if !stdout.is_empty() { + log::debug!("Post-hook stdout:"); + log::debug!("{}", stdout); + } + + if !stderr.is_empty() { + log::error!("Post-hook error:"); + log::error!("{}", stderr); + } + + Ok(()) + } + // Start tint processor pub fn start(&self) -> Result<(), Error> { self.switch_root()?; + // Bail-out if the image is already processed if self.lockfile.exists() { return Err(Error::new(std::io::ErrorKind::AlreadyExists, "This container seems already tinted.")); } + // Run pre-hook, if any + if self.profile.has_pre_hook() { + if self.dry_run { + log::debug!("Pre-hook:\n{}", self.profile.get_pre_hook()); + } else { + Self::call_script(self.profile.get_pre_hook())?; + } + } + // Paths to keep let mut paths: HashSet = HashSet::default(); @@ -210,8 +239,15 @@ impl TintProcessor { paths.sort(); if self.dry_run { + if self.profile.has_post_hook() { + log::debug!("Post-hook:\n{}", self.profile.get_post_hook()); + } ContentFormatter::new(&paths).set_removed(&p).format(); } else { + // Run post-hook (doesn't affect changes apply) + if self.profile.has_post_hook() { + Self::call_script(self.profile.get_post_hook())?; + } self.apply_changes(p)?; } diff --git a/src/profile.rs b/src/profile.rs index b9b33ed..9fa4345 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -14,6 +14,13 @@ pub struct PTargets { targets: Vec, packages: Option>, config: Option, + hooks: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct PHooks { + before: Option, + after: Option, } /// Profile @@ -33,6 +40,10 @@ pub struct Profile { packages: Vec, dropped_packages: Vec, targets: Vec, + + // hooks + s_pre: String, + s_post: String, } impl Profile { @@ -48,11 +59,15 @@ impl Profile { f_log: true, f_img: true, f_arc: true, + packages: vec![], dropped_packages: vec![], targets: vec![], f_expl_prune: vec![], f_expl_keep: vec![], + + s_post: String::from(""), + s_pre: String::from(""), } } @@ -129,6 +144,16 @@ impl Profile { } } + // Get hooks + if let Some(hooks) = p.hooks { + if let Some(pre) = hooks.before { + self.s_pre = pre; + } + if let Some(post) = hooks.after { + self.s_post = post; + } + } + Ok(()) } @@ -270,4 +295,24 @@ impl Profile { pub fn get_dropped_packages(&self) -> &Vec { &self.dropped_packages } + + /// Returns True if pre-hook defined + pub fn has_pre_hook(&self) -> bool { + !self.s_pre.is_empty() + } + + /// Get pre-hook + pub fn get_pre_hook(&self) -> String { + self.s_pre.to_owned() + } + + /// Returns True if post-hook defined + pub fn has_post_hook(&self) -> bool { + !self.s_post.is_empty() + } + + /// Get post-hook + pub fn get_post_hook(&self) -> String { + self.s_post.to_owned() + } } diff --git a/src/shcall.rs b/src/shcall.rs new file mode 100644 index 0000000..8ef8e73 --- /dev/null +++ b/src/shcall.rs @@ -0,0 +1,63 @@ +/* +Shell call + */ + +use std::{ + io::{Error, Write}, + path::PathBuf, + process::{Command, Stdio}, +}; + +const SHELL_DEFAULT: &str = "/usr/bin/sh"; +const SHELLS: &[&str] = &["/usr/bin/bash", "/usr/bin/ksh", "/usr/bin/dash", "/usr/bin/zsh", "/usr/bin/ash"]; + +pub struct ShellScript { + data: String, + + #[allow(dead_code)] + args: Vec, +} + +impl ShellScript { + /// Create a new Script wrapper + pub fn new(data: String, args: Option>) -> Self { + let mut a: Vec = Vec::default(); + if let Some(args) = args { + a.extend(args); + } + + let s = data.trim().to_string(); + Self { data: if s.is_empty() { format!("#!{}\n", SHELL_DEFAULT) } else { s }, args: a } + } + + /// Get script shebang or suggest one + fn detach_shebang(&self) -> Result<(String, String), Error> { + let data = self.data.split('\n').collect::>(); + let shebang = data[0].trim().strip_prefix("#!").unwrap_or_default().to_string(); + if PathBuf::from(&shebang).exists() { + return Ok((shebang, data[1..].join("\n"))); + } + + for s in SHELLS { + if PathBuf::from(s).exists() { + return Ok((s.to_string(), data.join("\n"))); + } + } + + Err(Error::new(std::io::ErrorKind::NotFound, "No supported shell has been found")) + } + + /// Run script + pub fn run(&self) -> Result<(String, String), Error> { + let (shebang, script) = self.detach_shebang()?; + + let mut p = Command::new(shebang).stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?; + p.stdin.as_mut().unwrap().write_all(script.as_bytes())?; + + let out = p.wait_with_output()?; + Ok(( + String::from_utf8(out.stdout).unwrap_or_else(|e| format!("Cannot get STDOUT: {}", e)), + String::from_utf8(out.stderr).unwrap_or_else(|e| format!("Cannot get STDERR: {}", e)), + )) + } +}