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}