Skip to main content

abbaye/builders/
script.rs

1use 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/// Configuration for [`ScriptBuilder`].
12#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
13pub struct ScriptBuilderConfig {
14    /// Shell commands to execute in order. Each line is passed to `sh -c`,
15    /// so any POSIX shell syntax is supported.
16    ///
17    /// The environment variable `ABBAYE_BUILDING_VERSION` is set to the version
18    /// being built. (e.g. the git tag `v0.1.0` or whatever. Not Abbaye's own version)
19    ///
20    /// The build fails immediately if any command exits with a non-zero status.
21    ///
22    /// ```toml
23    /// [[builders]]
24    /// type = "script"
25    /// script = [
26    ///   "make release",
27    ///   "strip target/mybin",
28    /// ]
29    /// outputs = ["target/mybin"]
30    /// ```
31    pub script: Vec<String>,
32
33    /// Paths of the files or directories produced by the script that should be
34    /// treated as release artifacts (copied to `dist/` and listed on the
35    /// release page). Each path is resolved relative to the working directory
36    /// in which `abbaye` is run.
37    ///
38    /// If a listed path is missing, the build fails.
39    pub outputs: Vec<ScriptBuilderOutput>,
40}
41
42/// Lets the user specify a path for a script output, optionally with a custom name.
43/// I need to check but it should enable the user to specify a directory as output.
44/// If that's a good idea is an other question.
45#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
46#[serde(untagged)]
47pub enum ScriptBuilderOutput {
48    PathWithName { path: PathBuf, name: String },
49    Path(PathBuf),
50}
51
52/// Executes a script and treats the listed output paths as release artifacts.
53pub 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            // Merge stdout and stderr into the log channel concurrently.
74            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            // Drain I/O tasks before checking the exit code.
93            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        // Collect every declared output path as an artifact.
105        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}