Skip to main content

abbaye/builders/
script.rs

1use std::path::PathBuf;
2
3use crate::builders::{ArtifactPath, Builder};
4use miette::{IntoDiagnostic, Result, miette};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use tokio::process::Command;
8
9/// Configuration for [`ScriptBuilder`].
10#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
11pub struct ScriptBuilderConfig {
12    /// Shell commands to execute in order. Each line is passed to `sh -c`,
13    /// so any POSIX shell syntax is supported.
14    ///
15    /// The environment variable `ABBAYE_BUILDING_VERSION` is set to the version
16    /// being built. (e.g. the git tag `v0.1.0` or whatever. Not Abbaye's own version)
17    ///
18    /// The build fails immediately if any command exits with a non-zero status.
19    ///
20    /// ```toml
21    /// [[builders]]
22    /// type = "script"
23    /// script = [
24    ///   "make release",
25    ///   "strip target/mybin",
26    /// ]
27    /// outputs = ["target/mybin"]
28    /// ```
29    pub script: Vec<String>,
30
31    /// Paths of the files or directories produced by the script that should be
32    /// treated as release artifacts (copied to `dist/` and listed on the
33    /// release page). Each path is resolved relative to the working directory
34    /// in which `abbaye` is run.
35    ///
36    /// If a listed path is missing, the build fails.
37    pub outputs: Vec<ScriptBuilderOutput>,
38}
39
40/// Lets the user specify a path for a script output, optionally with a custom name.
41/// I need to check but it should enable the user to specify a directory as output.
42/// If that's a good idea is an other question.
43#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
44#[serde(untagged)]
45pub enum ScriptBuilderOutput {
46    PathWithName { path: PathBuf, name: String },
47    Path(PathBuf),
48}
49
50/// Executes a script and treats the listed output paths as release artifacts.
51pub struct ScriptBuilder;
52
53impl Builder for ScriptBuilder {
54    type ConfigType = ScriptBuilderConfig;
55
56    async fn build(&self, config: Self::ConfigType, version: &str) -> Result<Vec<ArtifactPath>> {
57        for line in &config.script {
58            let status = Command::new("sh")
59                .args(["-c", line])
60                .env("ABBAYE_BUILDING_VERSION", version)
61                .status()
62                .await
63                .into_diagnostic()?;
64
65            if !status.success() {
66                return Err(miette!(
67                    "script command failed (exit {}):\n  {}",
68                    status.code().unwrap_or(-1),
69                    line
70                ));
71            }
72        }
73
74        // Collect every declared output path as an artifact.
75        let mut artifacts = Vec::new();
76        for output in &config.outputs {
77            let (path, name) = match output {
78                ScriptBuilderOutput::Path(p) => {
79                    let name = p
80                        .file_name()
81                        .ok_or_else(|| miette!("output path has no file name: {}", p.display()))?
82                        .to_string_lossy()
83                        .into_owned();
84                    (p, name)
85                }
86                ScriptBuilderOutput::PathWithName { path: p, name } => (p, name.clone()),
87            };
88            if !path.exists() {
89                return Err(miette!(
90                    "declared script output does not exist: {}",
91                    path.display()
92                ));
93            }
94            artifacts.push(ArtifactPath {
95                path: path.clone(),
96                name,
97                hash: None,
98            });
99        }
100
101        Ok(artifacts)
102    }
103}