abbaye/builders/mod.rs
1//! Builder types, the [`Builder`] trait, and the parallel execution model.
2//!
3//! Each `[[builders]]` entry in `abbaye.toml` deserialises into a
4//! [`BuilderEntry`] and is run as an independent Tokio task. This module
5//! owns the [`AnyBuilder`] enum (the TOML type-tag dispatch), the
6//! [`LogEvent`] channel used for progress-bar updates, and the [`Builder`]
7//! trait that every concrete builder must implement.
8//!
9//! ---
10//!
11//! # Parallel Builder Execution
12//!
13//! This section explains how Abbaye runs the `[[builders]]` entries defined in
14//! `abbaye.toml` in parallel, how inter-builder dependencies are enforced, and
15//! how the progress UI is kept in sync with the running tasks.
16//!
17//! ## Overview
18//!
19//! Every `[[builders]]` entry is executed as an independent Tokio async task.
20//! Tasks are launched at the same time (via a shared `JoinSet`) and may
21//! therefore run fully concurrently. When a builder declares `depends_on`,
22//! its task simply waits — without blocking any thread — until all named
23//! prerequisites have completed successfully before invoking the underlying
24//! build logic.
25//!
26//! ## Configuration Model
27//!
28//! ### `BuilderEntry`
29//!
30//! Each `[[builders]]` table in the TOML file deserialises into a
31//! [`BuilderEntry`]:
32//!
33//! | Field | Type | Purpose |
34//! |--------------|-------------------|-------------------------------------------------------------------|
35//! | `builder` | [`AnyBuilder`] | Type-tagged union holding the builder's own config (flattened). |
36//! | `id` | `Option<String>` | Stable name that other builders can reference in `depends_on`. |
37//! | `depends_on` | `Vec<String>` | IDs of builders that must succeed before this one starts. |
38//!
39//! Example:
40//!
41//! ```toml
42//! [[builders]]
43//! type = "cargo"
44//! id = "compile"
45//!
46//! [[builders]]
47//! type = "script"
48//! script = ["strip target/release/mybin"]
49//! outputs = ["target/release/mybin"]
50//! depends_on = ["compile"] # waits for the cargo builder above
51//! ```
52//!
53//! ### `AnyBuilder` variants
54//!
55//! | TOML `type` | Rust variant | What it does |
56//! |--------------|---------------|-----------------------------------------------------------|
57//! | `archive` | `Archive` | Creates a `.tar.gz` of the source tree. |
58//! | `cargo` | `Cargo` | `cargo build --release`, optionally for multiple targets. |
59//! | `cargo_doc` | `CargoDoc` | `cargo doc`. |
60//! | `markdown` | `Markdown` | Renders a directory of `.md` files to HTML. |
61//! | `script` | `Script` | Runs an arbitrary sequence of `sh -c` commands. |
62//!
63//! ## Orchestration in `build_site` (`src/site.rs`)
64//!
65//! The entire builder pipeline lives inside `build_site`, in three phases.
66//!
67//! ### Phase 1 — Validation
68//!
69//! Before any task is spawned, two checks are performed:
70//!
71//! 1. **Reference check** — every string in `depends_on` must match an `id`
72//! of some other builder in the same config. Unknown IDs are reported as a
73//! hard error immediately.
74//!
75//! 2. **Cycle detection** — a depth-first search visits the dependency graph
76//! and returns an error if a cycle is found.
77//!
78//! The DFS tracks three states per node: `0 = unvisited`, `1 = in the current
79//! stack`, `2 = fully processed`. Encountering a node in state `1` means
80//! there is a back-edge, i.e. a cycle.
81//!
82//! ### Phase 2 — Completion channels
83//!
84//! For each builder that carries an `id`, a `tokio::sync::watch` channel is
85//! created:
86//!
87//! ```text
88//! watch::channel(None::<bool>)
89//! ↑
90//! └─ initial value: None (pending)
91//! ```
92//!
93//! The channel is later sent `Some(true)` (success) or `Some(false)`
94//! (failure). Dependents hold a cloned `Receiver` and call
95//! `wait_for(|v| v.is_some())`, which suspends the task cooperatively until
96//! the value changes.
97//!
98//! If a dependency's `watch::Sender` is dropped without ever sending a value
99//! (e.g. the task panicked), `wait_for` returns an `Err`, which is treated as
100//! a failure and the dependent is skipped.
101//!
102//! ### Phase 3 — Task spawning and collection
103//!
104//! All tasks are submitted to a single `tokio::task::JoinSet`. Every task
105//! follows the same lifecycle:
106//!
107//! - **Spawned** → waits for all declared dependencies.
108//! - **Running** → all deps succeeded; the builder executes.
109//! - **Skipped** → a dependency failed; returns `Ok(vec![])` and signals
110//! `Some(false)` so its own dependents also skip.
111//! - **Done / Failed** → signals `Some(true)` or `Some(false)`.
112//!
113//! After all tasks finish, collected [`ArtifactPath`]s are classified:
114//!
115//! - **File** artifacts → `dist/` (distribution binaries, archives, etc.).
116//! - **Directory** artifacts → `docs/` (rustdoc output, rendered Markdown, …).
117//!
118//! If any builder returned an error, the first error is propagated to the
119//! caller after all spinners have settled, so the full UI is always rendered
120//! to completion.
121//!
122//! ## Progress UI
123//!
124//! The UI is built with the `indicatif` crate. Each builder task owns:
125//!
126//! - A **parent spinner** inserted *above* a shared summary bar.
127//! - An **mpsc log channel** ([`LogSender`]) over which the builder streams
128//! events.
129//! - A **log-consumer task** that receives [`LogEvent`]s and updates the
130//! spinner.
131//!
132//! ### `LogEvent` variants
133//!
134//! | Variant | Meaning |
135//! |------------------------------|---------------------------------------------------|
136//! | `Line(String)` | Update the parent spinner message. |
137//! | `ChildStart { id, label }` | Create a sub-spinner below the parent. |
138//! | `ChildLine { id, line }` | Update a child spinner's message. |
139//! | `ChildFinish { id, … }` | Close a child spinner with ✓ or ✗. |
140//!
141//! The `ChildStart` / `ChildLine` / `ChildFinish` events are used only by the
142//! `cargo` builder when it compiles for multiple targets in parallel (see
143//! below).
144//!
145//! ## Inner Parallelism: `CargoBuilder`
146//!
147//! When a `cargo` builder lists multiple `targets`, it spawns one Tokio task
148//! per target inside its own inner `JoinSet` — a second level of concurrency
149//! nested inside the outer builder task.
150//!
151//! Each inner task:
152//!
153//! 1. Creates a [`tempfile::TempDir`] and passes it as `--target-dir` so the
154//! `cargo build` process does not contend with sibling builds on cargo's
155//! file lock.
156//! 2. Emits a `ChildStart` event so the UI creates a dedicated sub-spinner.
157//! 3. Uses `line_bridge` to adapt the plain-string stderr stream from
158//! `run_cargo_build` into `ChildLine` events on the parent [`LogSender`].
159//! 4. After the build, copies artifacts to stable `target/<triple>/release/`
160//! paths before the `TempDir` is dropped.
161//! 5. Emits a `ChildFinish` event when done.
162//!
163//! Within `run_cargo_build` itself, stderr and the JSON stdout are consumed
164//! concurrently in separate `tokio::spawn` tasks so neither stream blocks the
165//! other.
166//!
167//! ## Other Builders
168//!
169//! | Builder | Async strategy |
170//! |-------------|---------------------------------------------------------------------------------|
171//! | `archive` | `tokio::task::spawn_blocking` for the CPU-bound tar walk; sends `Line` events. |
172//! | `cargo_doc` | Spawns one `tokio::spawn` to stream stderr; awaits `cargo doc` exit. |
173//! | `markdown` | Sequential per-file render loop; `spawn_blocking` for the directory walk. |
174//! | `script` | Runs commands sequentially; streams stdout **and** stderr in parallel tasks. |
175
176use miette::Result;
177use schemars::JsonSchema;
178use serde::{Deserialize, Serialize};
179use std::path::PathBuf;
180use tokio::sync::mpsc::UnboundedSender;
181
182pub mod archive;
183pub mod cargo;
184pub mod markdown;
185pub mod script;
186
187use archive::{ArchiveBuilder, ArchiveBuilderConfig};
188use cargo::{CargoBuilder, CargoBuilderConfig, CargoDocBuilder, CargoDocBuilderConfig};
189use markdown::{MarkdownBuilder, MarkdownBuilderConfig};
190use script::{ScriptBuilder, ScriptBuilderConfig};
191
192/// Events emitted by builders and forwarded to the progress-bar manager in
193/// [`crate::site`].
194///
195/// Most builders only ever send [`LogEvent::Line`]. Builders that fan out
196/// into parallel sub-tasks (e.g. [`crate::builders::cargo::CargoBuilder`]
197/// compiling for multiple targets simultaneously) additionally use the
198/// `Child*` variants so the UI can show a dedicated spinner per sub-task.
199#[derive(Debug)]
200pub enum LogEvent {
201 /// A plain output line from the top-level builder task.
202 Line(String),
203 /// Register a new child task. The receiver will create a sub-spinner
204 /// immediately below the parent spinner.
205 ChildStart {
206 /// Unique key used to route subsequent [`LogEvent::ChildLine`] and
207 /// [`LogEvent::ChildFinish`] messages.
208 id: String,
209 /// Human-readable label shown in the sub-spinner's prefix.
210 label: String,
211 },
212 /// A log line produced by a specific child task.
213 ChildLine { id: String, line: String },
214 /// Signal that a child task has finished.
215 ChildFinish {
216 id: String,
217 success: bool,
218 /// Short one-line summary displayed as the spinner's final message.
219 summary: String,
220 },
221}
222
223/// A sender used to stream [`LogEvent`]s from a builder back to the
224/// progress-bar manager in [`crate::site`]. Errors sending are silently
225/// ignored because the receiver may already be gone when a builder finishes.
226pub type LogSender = UnboundedSender<LogEvent>;
227
228/// A single `[[builders]]` entry: the builder itself plus optional dependency
229/// metadata.
230///
231/// # Dependency ordering
232///
233/// By default every builder runs concurrently with all others. When a builder
234/// must only start *after* another one has finished successfully, give the
235/// prerequisite an `id` and list that id in the dependent's `depends_on`:
236///
237/// ```toml
238/// [[builders]]
239/// type = "cargo"
240/// id = "compile" # ← give this builder a name
241///
242/// [[builders]]
243/// type = "script"
244/// script = ["strip target/release/my_bin"]
245/// outputs = ["target/release/my_bin"]
246/// depends_on = ["compile"] # ← wait for the builder above
247/// ```
248///
249/// Circular dependencies are detected before any builder starts and reported
250/// as an error.
251#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
252pub struct BuilderEntry {
253 /// The builder's type-specific configuration (type tag + all builder
254 /// fields), serialised flat into the same TOML table.
255 #[serde(flatten)]
256 pub builder: AnyBuilder,
257
258 /// Optional identifier for this builder. Other builders reference this
259 /// string in their `depends_on` list.
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub id: Option<String>,
262
263 /// IDs of builders that must complete successfully before this one starts.
264 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 pub depends_on: Vec<String>,
266}
267
268impl BuilderEntry {
269 /// Short human-readable label forwarded from the inner builder.
270 pub fn label(&self) -> &'static str {
271 self.builder.label()
272 }
273
274 /// Run the inner builder, forwarding the version string and log sender.
275 pub async fn build(&self, version: &str, log: LogSender) -> Result<Vec<ArtifactPath>> {
276 self.builder.build(version, log).await
277 }
278}
279
280#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
281#[serde(tag = "type", rename_all = "snake_case")]
282pub enum AnyBuilder {
283 /// Creates a `.tar.gz` archive of the source tree, automatically excluding
284 /// files and directories matched by any `.gitignore` found in the hierarchy.
285 ///
286 /// ```toml
287 /// [[builders]]
288 /// type = "archive"
289 /// source_dir = "." # optional, defaults to CWD
290 /// output = "myproject-1.0.0.tar.gz" # optional, defaults to source.tar.gz
291 /// prefix = "myproject-1.0.0" # optional, defaults to source_dir name
292 /// ```
293 Archive(ArchiveBuilderConfig),
294
295 /// Compiles the crate in release mode with `cargo build --release`.
296 /// One or more target triples can be specified for cross-compilation;
297 /// omitting `targets` builds for the host platform.
298 ///
299 /// ```toml
300 /// [[builders]]
301 /// type = "cargo"
302 /// targets = ["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl"]
303 /// manifest_path = "Cargo.toml" # optional
304 /// ```
305 Cargo(CargoBuilderConfig),
306
307 /// Generates API documentation with `cargo doc`.
308 /// Returns the per-crate doc directory (e.g. `target/doc/my_crate`) as an
309 /// artifact so it can be published or archived by a later pipeline step.
310 ///
311 /// ```toml
312 /// [[builders]]
313 /// type = "cargo_doc"
314 /// no_deps = true # optional, skip dependency docs
315 /// manifest_path = "Cargo.toml" # optional
316 /// ```
317 CargoDoc(CargoDocBuilderConfig),
318
319 /// Runs an arbitrary sequence of shell commands and collects declared
320 /// output paths as release artifacts.
321 ///
322 /// Each script line is passed to `sh -c`; the build fails immediately if
323 /// any command exits with a non-zero status.
324 ///
325 /// ```toml
326 /// [[builders]]
327 /// type = "script"
328 /// script = [
329 /// "make release",
330 /// "strip target/mybin",
331 /// ]
332 /// outputs = ["target/mybin"]
333 /// ```
334 Script(ScriptBuilderConfig),
335
336 /// Renders a directory of Markdown files to standalone HTML5 documents.
337 ///
338 /// Every `.md` file is rendered to a mirrored `.html` file in the output
339 /// directory. Non-Markdown files referenced by local links or image
340 /// embeds are copied to the output directory so that relative URLs remain
341 /// valid. Files excluded by `.gitignore` rules are automatically skipped.
342 ///
343 /// ```toml
344 /// [[builders]]
345 /// type = "markdown"
346 /// input = "docs/"
347 /// output = "docs-html/" # optional
348 /// recursive = true # optional, default true
349 /// ```
350 Markdown(MarkdownBuilderConfig),
351}
352
353impl AnyBuilder {
354 /// A short human-readable label used in progress-bar prefixes.
355 pub fn label(&self) -> &'static str {
356 match self {
357 Self::Archive(_) => "archive",
358 Self::Cargo(_) => "cargo-build",
359 Self::CargoDoc(_) => "cargo-doc",
360 Self::Markdown(_) => "markdown",
361 Self::Script(_) => "script",
362 }
363 }
364
365 pub async fn build(&self, version: &str, log: LogSender) -> Result<Vec<ArtifactPath>> {
366 match self {
367 Self::Archive(config) => ArchiveBuilder.build(config.clone(), version, log).await,
368 Self::Cargo(config) => CargoBuilder.build(config.clone(), version, log).await,
369 Self::CargoDoc(config) => CargoDocBuilder.build(config.clone(), version, log).await,
370 Self::Markdown(config) => MarkdownBuilder.build(config.clone(), version, log).await,
371 Self::Script(config) => ScriptBuilder.build(config.clone(), version, log).await,
372 }
373 }
374}
375
376pub struct ArtifactPath {
377 pub path: PathBuf,
378 pub name: String,
379 /// Lowercase hexadecimal SHA1 digest of the artifact's contents, if computed.
380 pub hash: Option<String>,
381}
382
383/// # Implementing a New Builder
384///
385/// This section walks you through adding a brand-new builder type to Abbaye,
386/// from an empty file to a fully working `[[builders]]` entry that users can
387/// drop into their `abbaye.toml`.
388///
389/// The worked example is a **`make` builder** that runs one or more `make`
390/// targets and collects declared output paths as release artifacts.
391///
392/// ## How Builders Plug In
393///
394/// Every builder type is one variant of the [`AnyBuilder`] enum. The enum
395/// acts as the TOML-visible type tag (`type = "make"`) and routes calls down
396/// to the concrete implementation. The [`Builder`] trait is the interface
397/// every implementation must satisfy:
398///
399/// ```text
400/// abbaye.toml ──► AnyBuilder enum ──► BuilderEntry
401/// │
402/// ▼
403/// Builder trait
404/// │
405/// ▼
406/// src/builders/make.rs
407/// ```
408///
409/// ## Step 1 — Create the builder file
410///
411/// Create `src/builders/make.rs`. Start with imports that mirror what the
412/// existing builders use:
413///
414/// ```rust,ignore
415/// use std::{path::PathBuf, process::Stdio};
416///
417/// use miette::{IntoDiagnostic, Result, miette};
418/// use schemars::JsonSchema;
419/// use serde::{Deserialize, Serialize};
420/// use tokio::io::{AsyncBufReadExt, BufReader};
421/// use tokio::process::Command;
422///
423/// use crate::builders::{ArtifactPath, Builder, LogEvent, LogSender};
424/// ```
425///
426/// | Import | Purpose |
427/// |-----------------|-------------------------------------------------------------------|
428/// | `ArtifactPath` | The return type: a path plus display name and optional hash. |
429/// | `Builder` | The trait your struct must implement. |
430/// | `LogEvent` | The structured event type sent over the log channel. |
431/// | `LogSender` | An `mpsc::UnboundedSender<LogEvent>` — your handle to the UI. |
432///
433/// ## Step 2 — Write the config struct
434///
435/// The config struct holds every field that can appear in the TOML table.
436/// It must derive exactly these traits (all are required for the builder
437/// machinery):
438///
439/// ```rust,ignore
440/// /// Configuration for [`MakeBuilder`].
441/// #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
442/// pub struct MakeBuilderConfig {
443/// /// `make` target to build. Defaults to the default target when absent.
444/// pub target: Option<String>,
445///
446/// /// Path to the Makefile. Passed as `-f <path>`.
447/// pub makefile: Option<PathBuf>,
448///
449/// /// Paths of the files produced by `make` to treat as release artifacts.
450/// #[serde(default)]
451/// pub outputs: Vec<PathBuf>,
452/// }
453/// ```
454///
455/// | Derive | Why it is required |
456/// |---------------|-------------------------------------------------------------------|
457/// | `Debug` | Diagnostic printing. |
458/// | `Default` | The [`Builder`] trait bound requires it for the associated type. |
459/// | `Clone` | [`BuilderEntry::clone`] is used when spawning the builder task. |
460/// | `Deserialize` | Figment deserialises the TOML table into this struct. |
461/// | `Serialize` | Needed for `abbaye init` and JSON Schema generation. |
462/// | `JsonSchema` | Powers `abbaye dump-schema` so editors get completion/validation. |
463///
464/// Every public field should have a doc comment — these become the field
465/// descriptions in the generated JSON Schema and appear in editor tooltips.
466///
467/// If `Default` cannot be derived because a field has a non-false/zero/empty
468/// default (e.g. a `bool` that defaults to `true`), implement `Default`
469/// manually and provide a `fn default_fieldname() -> T` free function for the
470/// `#[serde(default = "...")]` attribute. See [`crate::builders::cargo`] for
471/// an example.
472///
473/// ## Step 3 — Implement the `Builder` trait
474///
475/// Define a zero-sized marker struct and implement [`Builder`] for it:
476///
477/// ```rust,ignore
478/// pub struct MakeBuilder;
479///
480/// impl Builder for MakeBuilder {
481/// type ConfigType = MakeBuilderConfig;
482///
483/// async fn build(
484/// &self,
485/// config: Self::ConfigType,
486/// version: &str,
487/// log: LogSender,
488/// ) -> Result<Vec<ArtifactPath>> {
489/// let mut cmd = Command::new("make");
490///
491/// // Always expose the version being built.
492/// cmd.env("ABBAYE_BUILDING_VERSION", version);
493///
494/// if let Some(ref makefile) = config.makefile {
495/// cmd.arg("-f").arg(makefile);
496/// }
497/// if let Some(ref target) = config.target {
498/// cmd.arg(target);
499/// }
500///
501/// let mut child = cmd
502/// .stdout(Stdio::piped())
503/// .stderr(Stdio::piped())
504/// .spawn()
505/// .into_diagnostic()?;
506///
507/// // Stream stdout and stderr concurrently so neither pipe fills up
508/// // and deadlocks the child process.
509/// let stdout = child.stdout.take().expect("stdout was piped");
510/// let stderr = child.stderr.take().expect("stderr was piped");
511/// let log_out = log.clone();
512/// let log_err = log.clone();
513/// let stdout_task = tokio::spawn(async move {
514/// let mut lines = BufReader::new(stdout).lines();
515/// while let Ok(Some(line)) = lines.next_line().await {
516/// let _ = log_out.send(LogEvent::Line(line));
517/// }
518/// });
519/// let stderr_task = tokio::spawn(async move {
520/// let mut lines = BufReader::new(stderr).lines();
521/// while let Ok(Some(line)) = lines.next_line().await {
522/// let _ = log_err.send(LogEvent::Line(line));
523/// }
524/// });
525///
526/// // Drain I/O tasks *before* checking the exit code to avoid
527/// // losing the last lines of output.
528/// let status = child.wait().await.into_diagnostic()?;
529/// let _ = tokio::join!(stdout_task, stderr_task);
530///
531/// if !status.success() {
532/// return Err(miette!(
533/// "make failed with exit status: {}",
534/// status.code().unwrap_or(-1)
535/// ));
536/// }
537///
538/// // Collect declared outputs as artifacts.
539/// let mut artifacts = Vec::new();
540/// for output in &config.outputs {
541/// if !output.exists() {
542/// return Err(miette!(
543/// "declared output does not exist after make: {}",
544/// output.display()
545/// ));
546/// }
547/// let name = output
548/// .file_name()
549/// .ok_or_else(|| miette!("output path has no file name"))?
550/// .to_string_lossy()
551/// .into_owned();
552/// artifacts.push(ArtifactPath { path: output.clone(), name, hash: None });
553/// }
554/// Ok(artifacts)
555/// }
556/// }
557/// ```
558///
559/// ### Key conventions
560///
561/// **Always set `ABBAYE_BUILDING_VERSION`.** Every builder must expose the
562/// version being built through this environment variable so that build scripts
563/// can embed it without extra plumbing.
564///
565/// **Always drain I/O tasks before checking the exit status.** If you
566/// `.await` the child first, the subprocess's pipes may fill up and cause it
567/// to block forever before it can exit.
568///
569/// **Errors from `log.send(…)` are silently ignored** (`let _ = …`). The
570/// receiver may already be gone by the time a builder finishes — that is
571/// expected and harmless.
572///
573/// **File artifacts → `dist/`, directory artifacts → `docs/`.** The
574/// orchestrator in `site.rs` distinguishes them by `path.is_dir()`. Return
575/// a directory path only for documentation trees.
576///
577/// **CPU-bound or blocking I/O must use `spawn_blocking`.** Tokio runs on a
578/// small thread pool; blocking it starves other tasks.
579///
580/// ## Step 4 — Register in `mod.rs`
581///
582/// Open `src/builders/mod.rs` and make four additions.
583///
584/// **4a — Declare the module:**
585///
586/// ```rust,ignore
587/// pub mod make;
588/// ```
589///
590/// **4b — Import the types:**
591///
592/// ```rust,ignore
593/// use make::{MakeBuilder, MakeBuilderConfig};
594/// ```
595///
596/// **4c — Add a variant to [`AnyBuilder`]** with a doc comment containing a
597/// canonical TOML usage example (this becomes the JSON Schema description):
598///
599/// ```rust,ignore
600/// /// Runs `make [target]` and collects declared output paths as artifacts.
601/// ///
602/// /// ```toml
603/// /// [[builders]]
604/// /// type = "make"
605/// /// target = "release" # optional
606/// /// outputs = ["dist/mybin"]
607/// /// ```
608/// Make(MakeBuilderConfig),
609/// ```
610///
611/// The variant name is lowercased by `rename_all = "snake_case"`, so `Make`
612/// becomes `type = "make"` in TOML. Multi-word names use `CamelCase` in
613/// Rust and become `snake_case` in TOML (e.g. `WasmPack` → `wasm_pack`).
614///
615/// **4d — Wire up `label()` and `build()`:**
616///
617/// ```rust,ignore
618/// Self::Make(_) => "make", // in label()
619/// Self::Make(c) => MakeBuilder.build(c.clone(), version, log).await, // in build()
620/// ```
621///
622/// `label()` provides the default spinner prefix when the user hasn't given
623/// the builder an `id`. Keep it short and lowercase-kebab.
624///
625/// ## Step 5 — Verify
626///
627/// ```sh
628/// cargo build
629/// cargo run -- dump-schema | python3 -m json.tool | grep -A 20 '"make"'
630/// ```
631///
632/// ## Step 6 — Use it in `abbaye.toml`
633///
634/// ```toml
635/// [[builders]]
636/// type = "make"
637/// target = "release"
638/// outputs = ["build/myprogram"]
639///
640/// # With dependency ordering:
641/// [[builders]]
642/// type = "make"
643/// id = "make-release"
644/// target = "release"
645/// outputs = ["build/myprogram"]
646///
647/// [[builders]]
648/// type = "script"
649/// script = ["upx build/myprogram"]
650/// outputs = ["build/myprogram"]
651/// depends_on = ["make-release"]
652/// ```
653///
654/// ## Advanced — Parallel sub-tasks with child spinners
655///
656/// If your builder naturally fans out into independent sub-tasks (like the
657/// `cargo` builder running one `cargo build` per target triple), you can run
658/// them in parallel and give each its own sub-spinner in the UI.
659///
660/// The pattern is always the same:
661///
662/// 1. Send [`LogEvent::ChildStart`] `{ id, label }` to create the sub-spinner.
663/// 2. Forward output lines as [`LogEvent::ChildLine`] `{ id, line }`.
664/// 3. Send [`LogEvent::ChildFinish`] `{ id, success, summary }` to close it.
665///
666/// ```rust,ignore
667/// join_set.spawn(async move {
668/// let _ = log.send(LogEvent::ChildStart {
669/// id: target.clone(),
670/// label: target.clone(),
671/// });
672///
673/// // ... do work, sending ChildLine events ...
674///
675/// let _ = log.send(LogEvent::ChildFinish {
676/// id: target.clone(),
677/// success: result.is_ok(),
678/// summary: "done".to_owned(),
679/// });
680/// });
681/// ```
682///
683/// The `line_bridge` helper in [`crate::builders::cargo`] is a reusable
684/// pattern for adapting a plain-string sender to structured [`LogEvent`]s —
685/// feel free to copy or generalise it.
686///
687/// ## Reviewer checklist
688///
689/// - Config struct derives `Debug`, `Default` (or has a manual impl),
690/// `Clone`, `Deserialize`, `Serialize`, `JsonSchema`.
691/// - Every public config field has a doc comment.
692/// - The builder sets `ABBAYE_BUILDING_VERSION` in every subprocess
693/// environment.
694/// - Stdout and stderr are consumed concurrently to prevent pipe deadlocks.
695/// - Log send errors are silently ignored (`let _ = log.send(…)`).
696/// - Blocking I/O uses `tokio::task::spawn_blocking`.
697/// - Both `label()` and `build()` arms are added to [`AnyBuilder`].
698/// - `pub mod` declaration and `use` imports are present in `mod.rs`.
699/// - `cargo build` succeeds with no new warnings.
700/// - `abbaye dump-schema` shows the new variant and all its fields.
701#[allow(async_fn_in_trait)]
702pub trait Builder {
703 type ConfigType: Default + for<'de> Deserialize<'de> + Clone;
704
705 /// Run the builder and return the produced artifacts.
706 ///
707 /// `version` is the abbaye release version currently being built (e.g.
708 /// `"1.2.3"`). Implementations that spawn subprocesses must expose it as
709 /// the `ABBAYE_BUILDING_VERSION` environment variable.
710 ///
711 /// `log` receives one-line status/output messages from the builder's
712 /// subprocesses. Lines are sent without a trailing newline. The caller
713 /// displays these in the UI (e.g. as a spinner message); errors sending
714 /// are ignored because the receiver may already be closed.
715 async fn build(
716 &self,
717 config: Self::ConfigType,
718 version: &str,
719 log: LogSender,
720 ) -> Result<Vec<ArtifactPath>>;
721}