at e1dfdc3
use std::path::PathBuf; use std::process::Stdio; use crate::builders::{ArtifactPath, Builder, LogEvent, LogSender}; use crate::utils::expand_variables; use miette::{IntoDiagnostic, Result, miette}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; /// Configuration for [`ScriptBuilder`]. #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] pub struct ScriptBuilderConfig { /// Shell commands to execute in order. Each line is passed to `sh -c`, /// so any POSIX shell syntax is supported. /// /// The environment variable `ABBAYE_BUILDING_VERSION` is set to the version /// being built. (e.g. the git tag `v0.1.0` or whatever. Not Abbaye's own version) /// /// The build fails immediately if any command exits with a non-zero status. /// /// ```toml /// [[builders]] /// type = "script" /// script = [ /// "make release", /// "strip target/mybin", /// ] /// outputs = ["target/mybin"] /// ``` pub script: Vec<String>, /// Paths of the files or directories produced by the script that should be /// treated as release artifacts (copied to `dist/` and listed on the /// release page). Each path is resolved relative to the working directory /// in which `abbaye` is run. /// /// If a listed path is missing, the build fails. pub outputs: Vec<ScriptBuilderOutput>, } /// Lets the user specify a path for a script output, optionally with a custom name. /// I need to check but it should enable the user to specify a directory as output. /// If that's a good idea is an other question. #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] #[serde(untagged)] pub enum ScriptBuilderOutput { PathWithName { path: PathBuf, name: String }, Path(PathBuf), } /// Executes a script and treats the listed output paths as release artifacts. pub struct ScriptBuilder; impl Builder for ScriptBuilder { type ConfigType = ScriptBuilderConfig; async fn build( &self, config: Self::ConfigType, version: &str, log: LogSender, ) -> Result<Vec<ArtifactPath>> { for line in &config.script { let mut child = Command::new("sh") .args(["-c", line]) .env("ABBAYE_BUILDING_VERSION", version) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .into_diagnostic()?; // Merge stdout and stderr into the log channel concurrently. let stdout = child.stdout.take().expect("stdout piped"); let stderr = child.stderr.take().expect("stderr piped"); let log_out = log.clone(); let log_err = log.clone(); let stdout_task = tokio::spawn(async move { let mut lines = BufReader::new(stdout).lines(); while let Ok(Some(l)) = lines.next_line().await { let _ = log_out.send(LogEvent::Line(l)); } }); let stderr_task = tokio::spawn(async move { let mut lines = BufReader::new(stderr).lines(); while let Ok(Some(l)) = lines.next_line().await { let _ = log_err.send(LogEvent::Line(l)); } }); let status = child.wait().await.into_diagnostic()?; // Drain I/O tasks before checking the exit code. let _ = tokio::join!(stdout_task, stderr_task); if !status.success() { return Err(miette!( "script command failed (exit {}):\n {}", status.code().unwrap_or(-1), line )); } } // Collect every declared output path as an artifact. let mut artifacts = Vec::new(); for output in &config.outputs { let vars = vec![("ABBAYE_BUILDING_VERSION", version)]; let (pattern, name_override) = match output { ScriptBuilderOutput::Path(p) => (expand_variables(p, vars), None), ScriptBuilderOutput::PathWithName { path: p, name } => { (expand_variables(p, vars), Some(name.clone())) } }; let pattern_str = pattern.to_string_lossy().to_string(); // Check if pattern contains glob characters if pattern_str.contains(['*', '?', '[']) { // Compile the glob pattern let glob = globset::Glob::new(&pattern_str) .into_diagnostic()? .compile_matcher(); let mut matched = false; // Walk the directory to find matching paths let parent = pattern .parent() .unwrap_or_else(|| std::path::Path::new(".")); for entry in walkdir::WalkDir::new(parent) .into_iter() .filter_map(|e| e.ok()) { let path = entry.path(); if glob.is_match(path) { matched = true; let name = name_override.clone().unwrap_or_else(|| { path.file_name() .unwrap_or_default() .to_string_lossy() .into_owned() }); artifacts.push(ArtifactPath { path: path.to_path_buf(), name, hash: None, }); } } if !matched { return Err(miette!("no files matched output pattern: {}", pattern_str)); } } else { // Regular path (non-glob) if !pattern.exists() { return Err(miette!( "declared script output does not exist: {}", pattern.display() )); } let name = name_override.unwrap_or_else(|| { pattern .file_name() .unwrap_or_default() .to_string_lossy() .into_owned() }); artifacts.push(ArtifactPath { path: pattern.clone(), name, hash: None, }); } } Ok(artifacts) } }