abbaye/builders/
script.rs1use std::path::PathBuf;
2use std::process::Stdio;
3
4use crate::builders::{ArtifactPath, Builder, LogEvent, LogSender};
5use crate::utils::expand_variables;
6use miette::{IntoDiagnostic, Result, miette};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use tokio::io::{AsyncBufReadExt, BufReader};
10use tokio::process::Command;
11
12#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
14pub struct ScriptBuilderConfig {
15 pub script: Vec<String>,
33
34 pub outputs: Vec<ScriptBuilderOutput>,
41}
42
43#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
47#[serde(untagged)]
48pub enum ScriptBuilderOutput {
49 PathWithName { path: PathBuf, name: String },
50 Path(PathBuf),
51}
52
53pub struct ScriptBuilder;
55
56impl Builder for ScriptBuilder {
57 type ConfigType = ScriptBuilderConfig;
58
59 async fn build(
60 &self,
61 config: Self::ConfigType,
62 version: &str,
63 log: LogSender,
64 ) -> Result<Vec<ArtifactPath>> {
65 for line in &config.script {
66 let mut child = Command::new("sh")
67 .args(["-c", line])
68 .env("ABBAYE_BUILDING_VERSION", version)
69 .stdout(Stdio::piped())
70 .stderr(Stdio::piped())
71 .spawn()
72 .into_diagnostic()?;
73
74 let stdout = child.stdout.take().expect("stdout piped");
76 let stderr = child.stderr.take().expect("stderr piped");
77 let log_out = log.clone();
78 let log_err = log.clone();
79 let stdout_task = tokio::spawn(async move {
80 let mut lines = BufReader::new(stdout).lines();
81 while let Ok(Some(l)) = lines.next_line().await {
82 let _ = log_out.send(LogEvent::Line(l));
83 }
84 });
85 let stderr_task = tokio::spawn(async move {
86 let mut lines = BufReader::new(stderr).lines();
87 while let Ok(Some(l)) = lines.next_line().await {
88 let _ = log_err.send(LogEvent::Line(l));
89 }
90 });
91
92 let status = child.wait().await.into_diagnostic()?;
93 let _ = tokio::join!(stdout_task, stderr_task);
95
96 if !status.success() {
97 return Err(miette!(
98 "script command failed (exit {}):\n {}",
99 status.code().unwrap_or(-1),
100 line
101 ));
102 }
103 }
104
105 let mut artifacts = Vec::new();
107 for output in &config.outputs {
108 let vars = vec![("ABBAYE_BUILDING_VERSION", version)];
109
110 let (pattern, name_override) = match output {
111 ScriptBuilderOutput::Path(p) => (expand_variables(p, vars), None),
112 ScriptBuilderOutput::PathWithName { path: p, name } => {
113 (expand_variables(p, vars), Some(name.clone()))
114 }
115 };
116
117 let pattern_str = pattern.to_string_lossy().to_string();
118
119 if pattern_str.contains(['*', '?', '[']) {
121 let glob = globset::Glob::new(&pattern_str)
123 .into_diagnostic()?
124 .compile_matcher();
125
126 let mut matched = false;
127
128 let parent = pattern
130 .parent()
131 .unwrap_or_else(|| std::path::Path::new("."));
132 for entry in walkdir::WalkDir::new(parent)
133 .into_iter()
134 .filter_map(|e| e.ok())
135 {
136 let path = entry.path();
137 if glob.is_match(path) {
138 matched = true;
139 let name = name_override.clone().unwrap_or_else(|| {
140 path.file_name()
141 .unwrap_or_default()
142 .to_string_lossy()
143 .into_owned()
144 });
145 artifacts.push(ArtifactPath {
146 path: path.to_path_buf(),
147 name,
148 hash: None,
149 });
150 }
151 }
152
153 if !matched {
154 return Err(miette!("no files matched output pattern: {}", pattern_str));
155 }
156 } else {
157 if !pattern.exists() {
159 return Err(miette!(
160 "declared script output does not exist: {}",
161 pattern.display()
162 ));
163 }
164 let name = name_override.unwrap_or_else(|| {
165 pattern
166 .file_name()
167 .unwrap_or_default()
168 .to_string_lossy()
169 .into_owned()
170 });
171 artifacts.push(ArtifactPath {
172 path: pattern.clone(),
173 name,
174 hash: None,
175 });
176 }
177 }
178 Ok(artifacts)
179 }
180}