abbaye/builders/
script.rs1use std::path::PathBuf;
2use std::process::Stdio;
3
4use crate::builders::{ArtifactPath, Builder, LogEvent, LogSender};
5use miette::{IntoDiagnostic, Result, miette};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use tokio::io::{AsyncBufReadExt, BufReader};
9use tokio::process::Command;
10
11#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
13pub struct ScriptBuilderConfig {
14 pub script: Vec<String>,
32
33 pub outputs: Vec<ScriptBuilderOutput>,
40}
41
42#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
46#[serde(untagged)]
47pub enum ScriptBuilderOutput {
48 PathWithName { path: PathBuf, name: String },
49 Path(PathBuf),
50}
51
52pub struct ScriptBuilder;
54
55impl Builder for ScriptBuilder {
56 type ConfigType = ScriptBuilderConfig;
57
58 async fn build(
59 &self,
60 config: Self::ConfigType,
61 version: &str,
62 log: LogSender,
63 ) -> Result<Vec<ArtifactPath>> {
64 for line in &config.script {
65 let mut child = Command::new("sh")
66 .args(["-c", line])
67 .env("ABBAYE_BUILDING_VERSION", version)
68 .stdout(Stdio::piped())
69 .stderr(Stdio::piped())
70 .spawn()
71 .into_diagnostic()?;
72
73 let stdout = child.stdout.take().expect("stdout piped");
75 let stderr = child.stderr.take().expect("stderr piped");
76 let log_out = log.clone();
77 let log_err = log.clone();
78 let stdout_task = tokio::spawn(async move {
79 let mut lines = BufReader::new(stdout).lines();
80 while let Ok(Some(l)) = lines.next_line().await {
81 let _ = log_out.send(LogEvent::Line(l));
82 }
83 });
84 let stderr_task = tokio::spawn(async move {
85 let mut lines = BufReader::new(stderr).lines();
86 while let Ok(Some(l)) = lines.next_line().await {
87 let _ = log_err.send(LogEvent::Line(l));
88 }
89 });
90
91 let status = child.wait().await.into_diagnostic()?;
92 let _ = tokio::join!(stdout_task, stderr_task);
94
95 if !status.success() {
96 return Err(miette!(
97 "script command failed (exit {}):\n {}",
98 status.code().unwrap_or(-1),
99 line
100 ));
101 }
102 }
103
104 let mut artifacts = Vec::new();
106 for output in &config.outputs {
107 let (path, name) = match output {
108 ScriptBuilderOutput::Path(p) => {
109 let name = p
110 .file_name()
111 .ok_or_else(|| miette!("output path has no file name: {}", p.display()))?
112 .to_string_lossy()
113 .into_owned();
114 (p, name)
115 }
116 ScriptBuilderOutput::PathWithName { path: p, name } => (p, name.clone()),
117 };
118 if !path.exists() {
119 return Err(miette!(
120 "declared script output does not exist: {}",
121 path.display()
122 ));
123 }
124 artifacts.push(ArtifactPath {
125 path: path.clone(),
126 name,
127 hash: None,
128 });
129 }
130
131 Ok(artifacts)
132 }
133}