use anyhow::{Context, Result}; use serde::Deserialize; use std::io::Write; use std::{collections::HashMap, path::Path}; use tracing::{debug, error, trace, warn}; use crate::manifest::types::PickConfig; use super::{PickTool, TaskContext}; #[derive(Debug, Deserialize, Clone)] pub struct ToolBash { #[serde(default)] pub before: Option, #[serde(default)] pub after: Option, #[serde(default)] pub env: HashMap, #[serde(default)] pub script: HashMap, } impl PickTool for ToolBash { fn before(&self, manifest_path: &Path, cfg: &PickConfig) -> Result<()> { let script = match &self.before { None => { return Ok(()); } Some(script) => { debug!("Running `before` script"); let mut temp_file = tempfile::NamedTempFile::new().context("while creating temporary script")?; writeln!(temp_file, "{script}").context("while creating temporary script")?; temp_file } }; let mut cmd = std::process::Command::new("bash"); cmd.arg(script.path()); cmd.current_dir(&cfg.work_dir(manifest_path)?); for (key, value) in &self.env { cmd.env(key, value); } let output = match cmd.output() { Ok(output) => output, Err(error) => { error!("Failed to execute `before` script: {error}"); return Ok(()); } }; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!( "`before` script failed with status {}: {stderr}", output.status.code().unwrap_or(-1) ); } else { let stdout = String::from_utf8_lossy(&output.stdout); #[expect(clippy::print_stdout)] if !stdout.is_empty() { println!("{}", stdout.trim()); } } return Ok(()); } fn after(&self, manifest_path: &Path, cfg: &PickConfig) -> Result<()> { let script = match &self.after { None => { return Ok(()); } Some(script) => { debug!("Running `after` script"); let mut temp_file = tempfile::NamedTempFile::new().context("while creating temporary script")?; writeln!(temp_file, "{script}").context("while creating temporary script")?; temp_file } }; let mut cmd = std::process::Command::new("bash"); cmd.arg(script.path()); cmd.current_dir(&cfg.work_dir(manifest_path)?); for (key, value) in &self.env { cmd.env(key, value); } let output = match cmd.output() { Ok(output) => output, Err(error) => { error!("Failed to execute `after` script: {error}"); return Ok(()); } }; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!( "`after` script failed with status {}: {stderr}", output.status.code().unwrap_or(-1) ); } else { let stdout = String::from_utf8_lossy(&output.stdout); #[expect(clippy::print_stdout)] if !stdout.is_empty() { println!("{}", stdout.trim()); } } return Ok(()); } fn run(&self, manifest_path: &Path, cfg: &PickConfig, ctx: TaskContext) -> Result<()> { let script = match self.script.get(&ctx.task) { None => { warn!("No script named \"{}\"", ctx.task); return Ok(()); } Some(script) => { trace!("Running script for {}: {}", ctx.path_rel_str, ctx.task); let mut temp_file = tempfile::NamedTempFile::new().context("while creating temporary script")?; writeln!(temp_file, "{script}").context("while creating temporary script")?; temp_file } }; let mut cmd = std::process::Command::new("bash"); cmd.arg(script.path()); cmd.current_dir(&cfg.work_dir(manifest_path)?); cmd.env("PICK_FILE", &ctx.path_abs_str); cmd.env("PICK_RELATIVE", &ctx.path_rel_str); for (key, value) in &self.env { cmd.env(key, value); } let output = match cmd.output() { Ok(output) => output, Err(error) => { error!("Failed to execute script for {}: {error}", ctx.path_rel_str); return Ok(()); } }; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); error!( "Script for {} failed with status {}: {stderr}", ctx.path_rel_str, output.status.code().unwrap_or(-1) ); } else { let stdout = String::from_utf8_lossy(&output.stdout); #[expect(clippy::print_stdout)] if !stdout.is_empty() { println!("{}", stdout.trim()); } } return Ok(()); } }