at 6297799
//! Builder types, the [`Builder`] trait, and the parallel execution model. //! //! Each `[[builders]]` entry in `abbaye.toml` deserialises into a //! [`BuilderEntry`] and is run as an independent Tokio task. This module //! owns the [`AnyBuilder`] enum (the TOML type-tag dispatch), the //! [`LogEvent`] channel used for progress-bar updates, and the [`Builder`] //! trait that every concrete builder must implement. //! //! --- //! //! # Parallel Builder Execution //! //! This section explains how Abbaye runs the `[[builders]]` entries defined in //! `abbaye.toml` in parallel, how inter-builder dependencies are enforced, and //! how the progress UI is kept in sync with the running tasks. //! //! ## Overview //! //! Every `[[builders]]` entry is executed as an independent Tokio async task. //! Tasks are launched at the same time (via a shared `JoinSet`) and may //! therefore run fully concurrently. When a builder declares `depends_on`, //! its task simply waits — without blocking any thread — until all named //! prerequisites have completed successfully before invoking the underlying //! build logic. //! //! ## Configuration Model //! //! ### `BuilderEntry` //! //! Each `[[builders]]` table in the TOML file deserialises into a //! [`BuilderEntry`]: //! //! | Field | Type | Purpose | //! |--------------|-------------------|-------------------------------------------------------------------| //! | `builder` | [`AnyBuilder`] | Type-tagged union holding the builder's own config (flattened). | //! | `id` | `Option<String>` | Stable name that other builders can reference in `depends_on`. | //! | `depends_on` | `Vec<String>` | IDs of builders that must succeed before this one starts. | //! //! Example: //! //! ```toml //! [[builders]] //! type = "cargo" //! id = "compile" //! //! [[builders]] //! type = "script" //! script = ["strip target/release/mybin"] //! outputs = ["target/release/mybin"] //! depends_on = ["compile"] # waits for the cargo builder above //! ``` //! //! ### `AnyBuilder` variants //! //! | TOML `type` | Rust variant | What it does | //! |--------------|---------------|-----------------------------------------------------------| //! | `archive` | `Archive` | Creates a `.tar.gz` of the source tree. | //! | `cargo` | `Cargo` | `cargo build --release`, optionally for multiple targets. | //! | `cargo_doc` | `CargoDoc` | `cargo doc`. | //! | `markdown` | `Markdown` | Renders a directory of `.md` files to HTML. | //! | `script` | `Script` | Runs an arbitrary sequence of `sh -c` commands. | //! //! ## Orchestration in `build_site` (`src/site.rs`) //! //! The entire builder pipeline lives inside `build_site`, in three phases. //! //! ### Phase 1 — Validation //! //! Before any task is spawned, two checks are performed: //! //! 1. **Reference check** — every string in `depends_on` must match an `id` //! of some other builder in the same config. Unknown IDs are reported as a //! hard error immediately. //! //! 2. **Cycle detection** — a depth-first search visits the dependency graph //! and returns an error if a cycle is found. //! //! The DFS tracks three states per node: `0 = unvisited`, `1 = in the current //! stack`, `2 = fully processed`. Encountering a node in state `1` means //! there is a back-edge, i.e. a cycle. //! //! ### Phase 2 — Completion channels //! //! For each builder that carries an `id`, a `tokio::sync::watch` channel is //! created: //! //! ```text //! watch::channel(None::<bool>) //! ↑ //! └─ initial value: None (pending) //! ``` //! //! The channel is later sent `Some(true)` (success) or `Some(false)` //! (failure). Dependents hold a cloned `Receiver` and call //! `wait_for(|v| v.is_some())`, which suspends the task cooperatively until //! the value changes. //! //! If a dependency's `watch::Sender` is dropped without ever sending a value //! (e.g. the task panicked), `wait_for` returns an `Err`, which is treated as //! a failure and the dependent is skipped. //! //! ### Phase 3 — Task spawning and collection //! //! All tasks are submitted to a single `tokio::task::JoinSet`. Every task //! follows the same lifecycle: //! //! - **Spawned** → waits for all declared dependencies. //! - **Running** → all deps succeeded; the builder executes. //! - **Skipped** → a dependency failed; returns `Ok(vec![])` and signals //! `Some(false)` so its own dependents also skip. //! - **Done / Failed** → signals `Some(true)` or `Some(false)`. //! //! After all tasks finish, collected [`ArtifactPath`]s are classified: //! //! - **File** artifacts → `dist/` (distribution binaries, archives, etc.). //! - **Directory** artifacts → `docs/` (rustdoc output, rendered Markdown, …). //! //! If any builder returned an error, the first error is propagated to the //! caller after all spinners have settled, so the full UI is always rendered //! to completion. //! //! ## Progress UI //! //! The UI is built with the `indicatif` crate. Each builder task owns: //! //! - A **parent spinner** inserted *above* a shared summary bar. //! - An **mpsc log channel** ([`LogSender`]) over which the builder streams //! events. //! - A **log-consumer task** that receives [`LogEvent`]s and updates the //! spinner. //! //! ### `LogEvent` variants //! //! | Variant | Meaning | //! |------------------------------|---------------------------------------------------| //! | `Line(String)` | Update the parent spinner message. | //! | `ChildStart { id, label }` | Create a sub-spinner below the parent. | //! | `ChildLine { id, line }` | Update a child spinner's message. | //! | `ChildFinish { id, … }` | Close a child spinner with ✓ or ✗. | //! //! The `ChildStart` / `ChildLine` / `ChildFinish` events are used only by the //! `cargo` builder when it compiles for multiple targets in parallel (see //! below). //! //! ## Inner Parallelism: `CargoBuilder` //! //! When a `cargo` builder lists multiple `targets`, it spawns one Tokio task //! per target inside its own inner `JoinSet` — a second level of concurrency //! nested inside the outer builder task. //! //! Each inner task: //! //! 1. Creates a [`tempfile::TempDir`] and passes it as `--target-dir` so the //! `cargo build` process does not contend with sibling builds on cargo's //! file lock. //! 2. Emits a `ChildStart` event so the UI creates a dedicated sub-spinner. //! 3. Uses `line_bridge` to adapt the plain-string stderr stream from //! `run_cargo_build` into `ChildLine` events on the parent [`LogSender`]. //! 4. After the build, copies artifacts to stable `target/<triple>/release/` //! paths before the `TempDir` is dropped. //! 5. Emits a `ChildFinish` event when done. //! //! Within `run_cargo_build` itself, stderr and the JSON stdout are consumed //! concurrently in separate `tokio::spawn` tasks so neither stream blocks the //! other. //! //! ## Other Builders //! //! | Builder | Async strategy | //! |-------------|---------------------------------------------------------------------------------| //! | `archive` | `tokio::task::spawn_blocking` for the CPU-bound tar walk; sends `Line` events. | //! | `cargo_doc` | Spawns one `tokio::spawn` to stream stderr; awaits `cargo doc` exit. | //! | `markdown` | Sequential per-file render loop; `spawn_blocking` for the directory walk. | //! | `script` | Runs commands sequentially; streams stdout **and** stderr in parallel tasks. | use miette::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use tokio::sync::mpsc::UnboundedSender; pub mod archive; pub mod cargo; pub mod markdown; pub mod script; use archive::{ArchiveBuilder, ArchiveBuilderConfig}; use cargo::{CargoBuilder, CargoBuilderConfig, CargoDocBuilder, CargoDocBuilderConfig}; use markdown::{MarkdownBuilder, MarkdownBuilderConfig}; use script::{ScriptBuilder, ScriptBuilderConfig}; /// Events emitted by builders and forwarded to the progress-bar manager in /// [`crate::site`]. /// /// Most builders only ever send [`LogEvent::Line`]. Builders that fan out /// into parallel sub-tasks (e.g. [`crate::builders::cargo::CargoBuilder`] /// compiling for multiple targets simultaneously) additionally use the /// `Child*` variants so the UI can show a dedicated spinner per sub-task. #[derive(Debug)] pub enum LogEvent { /// A plain output line from the top-level builder task. Line(String), /// Register a new child task. The receiver will create a sub-spinner /// immediately below the parent spinner. ChildStart { /// Unique key used to route subsequent [`LogEvent::ChildLine`] and /// [`LogEvent::ChildFinish`] messages. id: String, /// Human-readable label shown in the sub-spinner's prefix. label: String, }, /// A log line produced by a specific child task. ChildLine { id: String, line: String }, /// Signal that a child task has finished. ChildFinish { id: String, success: bool, /// Short one-line summary displayed as the spinner's final message. summary: String, }, } /// A sender used to stream [`LogEvent`]s from a builder back to the /// progress-bar manager in [`crate::site`]. Errors sending are silently /// ignored because the receiver may already be gone when a builder finishes. pub type LogSender = UnboundedSender<LogEvent>; /// A single `[[builders]]` entry: the builder itself plus optional dependency /// metadata. /// /// # Dependency ordering /// /// By default every builder runs concurrently with all others. When a builder /// must only start *after* another one has finished successfully, give the /// prerequisite an `id` and list that id in the dependent's `depends_on`: /// /// ```toml /// [[builders]] /// type = "cargo" /// id = "compile" # ← give this builder a name /// /// [[builders]] /// type = "script" /// script = ["strip target/release/my_bin"] /// outputs = ["target/release/my_bin"] /// depends_on = ["compile"] # ← wait for the builder above /// ``` /// /// Circular dependencies are detected before any builder starts and reported /// as an error. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] pub struct BuilderEntry { /// The builder's type-specific configuration (type tag + all builder /// fields), serialised flat into the same TOML table. #[serde(flatten)] pub builder: AnyBuilder, /// Optional identifier for this builder. Other builders reference this /// string in their `depends_on` list. #[serde(skip_serializing_if = "Option::is_none")] pub id: Option<String>, /// IDs of builders that must complete successfully before this one starts. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub depends_on: Vec<String>, } impl BuilderEntry { /// Short human-readable label forwarded from the inner builder. pub fn label(&self) -> &'static str { self.builder.label() } /// Run the inner builder, forwarding the version string and log sender. pub async fn build(&self, version: &str, log: LogSender) -> Result<Vec<ArtifactPath>> { self.builder.build(version, log).await } } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AnyBuilder { /// Creates a `.tar.gz` archive of the source tree, automatically excluding /// files and directories matched by any `.gitignore` found in the hierarchy. /// /// ```toml /// [[builders]] /// type = "archive" /// source_dir = "." # optional, defaults to CWD /// output = "myproject-1.0.0.tar.gz" # optional, defaults to source.tar.gz /// prefix = "myproject-1.0.0" # optional, defaults to source_dir name /// ``` Archive(ArchiveBuilderConfig), /// Compiles the crate in release mode with `cargo build --release`. /// One or more target triples can be specified for cross-compilation; /// omitting `targets` builds for the host platform. /// /// ```toml /// [[builders]] /// type = "cargo" /// targets = ["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl"] /// manifest_path = "Cargo.toml" # optional /// ``` Cargo(CargoBuilderConfig), /// Generates API documentation with `cargo doc`. /// Returns the per-crate doc directory (e.g. `target/doc/my_crate`) as an /// artifact so it can be published or archived by a later pipeline step. /// /// ```toml /// [[builders]] /// type = "cargo_doc" /// no_deps = true # optional, skip dependency docs /// manifest_path = "Cargo.toml" # optional /// ``` CargoDoc(CargoDocBuilderConfig), /// Runs an arbitrary sequence of shell commands and collects declared /// output paths as release artifacts. /// /// Each script line is passed to `sh -c`; the build fails immediately if /// any command exits with a non-zero status. /// /// ```toml /// [[builders]] /// type = "script" /// script = [ /// "make release", /// "strip target/mybin", /// ] /// outputs = ["target/mybin"] /// ``` Script(ScriptBuilderConfig), /// Renders a directory of Markdown files to standalone HTML5 documents. /// /// Every `.md` file is rendered to a mirrored `.html` file in the output /// directory. Non-Markdown files referenced by local links or image /// embeds are copied to the output directory so that relative URLs remain /// valid. Files excluded by `.gitignore` rules are automatically skipped. /// /// ```toml /// [[builders]] /// type = "markdown" /// input = "docs/" /// output = "docs-html/" # optional /// recursive = true # optional, default true /// ``` Markdown(MarkdownBuilderConfig), } impl AnyBuilder { /// A short human-readable label used in progress-bar prefixes. pub fn label(&self) -> &'static str { match self { Self::Archive(_) => "archive", Self::Cargo(_) => "cargo-build", Self::CargoDoc(_) => "cargo-doc", Self::Markdown(_) => "markdown", Self::Script(_) => "script", } } pub async fn build(&self, version: &str, log: LogSender) -> Result<Vec<ArtifactPath>> { match self { Self::Archive(config) => ArchiveBuilder.build(config.clone(), version, log).await, Self::Cargo(config) => CargoBuilder.build(config.clone(), version, log).await, Self::CargoDoc(config) => CargoDocBuilder.build(config.clone(), version, log).await, Self::Markdown(config) => MarkdownBuilder.build(config.clone(), version, log).await, Self::Script(config) => ScriptBuilder.build(config.clone(), version, log).await, } } } pub struct ArtifactPath { pub path: PathBuf, pub name: String, /// Lowercase hexadecimal SHA1 digest of the artifact's contents, if computed. pub hash: Option<String>, } /// # Implementing a New Builder /// /// This section walks you through adding a brand-new builder type to Abbaye, /// from an empty file to a fully working `[[builders]]` entry that users can /// drop into their `abbaye.toml`. /// /// The worked example is a **`make` builder** that runs one or more `make` /// targets and collects declared output paths as release artifacts. /// /// ## How Builders Plug In /// /// Every builder type is one variant of the [`AnyBuilder`] enum. The enum /// acts as the TOML-visible type tag (`type = "make"`) and routes calls down /// to the concrete implementation. The [`Builder`] trait is the interface /// every implementation must satisfy: /// /// ```text /// abbaye.toml ──► AnyBuilder enum ──► BuilderEntry /// │ /// ▼ /// Builder trait /// │ /// ▼ /// src/builders/make.rs /// ``` /// /// ## Step 1 — Create the builder file /// /// Create `src/builders/make.rs`. Start with imports that mirror what the /// existing builders use: /// /// ```rust,ignore /// use std::{path::PathBuf, process::Stdio}; /// /// use miette::{IntoDiagnostic, Result, miette}; /// use schemars::JsonSchema; /// use serde::{Deserialize, Serialize}; /// use tokio::io::{AsyncBufReadExt, BufReader}; /// use tokio::process::Command; /// /// use crate::builders::{ArtifactPath, Builder, LogEvent, LogSender}; /// ``` /// /// | Import | Purpose | /// |-----------------|-------------------------------------------------------------------| /// | `ArtifactPath` | The return type: a path plus display name and optional hash. | /// | `Builder` | The trait your struct must implement. | /// | `LogEvent` | The structured event type sent over the log channel. | /// | `LogSender` | An `mpsc::UnboundedSender<LogEvent>` — your handle to the UI. | /// /// ## Step 2 — Write the config struct /// /// The config struct holds every field that can appear in the TOML table. /// It must derive exactly these traits (all are required for the builder /// machinery): /// /// ```rust,ignore /// /// Configuration for [`MakeBuilder`]. /// #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] /// pub struct MakeBuilderConfig { /// /// `make` target to build. Defaults to the default target when absent. /// pub target: Option<String>, /// /// /// Path to the Makefile. Passed as `-f <path>`. /// pub makefile: Option<PathBuf>, /// /// /// Paths of the files produced by `make` to treat as release artifacts. /// #[serde(default)] /// pub outputs: Vec<PathBuf>, /// } /// ``` /// /// | Derive | Why it is required | /// |---------------|-------------------------------------------------------------------| /// | `Debug` | Diagnostic printing. | /// | `Default` | The [`Builder`] trait bound requires it for the associated type. | /// | `Clone` | [`BuilderEntry::clone`] is used when spawning the builder task. | /// | `Deserialize` | Figment deserialises the TOML table into this struct. | /// | `Serialize` | Needed for `abbaye init` and JSON Schema generation. | /// | `JsonSchema` | Powers `abbaye dump-schema` so editors get completion/validation. | /// /// Every public field should have a doc comment — these become the field /// descriptions in the generated JSON Schema and appear in editor tooltips. /// /// If `Default` cannot be derived because a field has a non-false/zero/empty /// default (e.g. a `bool` that defaults to `true`), implement `Default` /// manually and provide a `fn default_fieldname() -> T` free function for the /// `#[serde(default = "...")]` attribute. See [`crate::builders::cargo`] for /// an example. /// /// ## Step 3 — Implement the `Builder` trait /// /// Define a zero-sized marker struct and implement [`Builder`] for it: /// /// ```rust,ignore /// pub struct MakeBuilder; /// /// impl Builder for MakeBuilder { /// type ConfigType = MakeBuilderConfig; /// /// async fn build( /// &self, /// config: Self::ConfigType, /// version: &str, /// log: LogSender, /// ) -> Result<Vec<ArtifactPath>> { /// let mut cmd = Command::new("make"); /// /// // Always expose the version being built. /// cmd.env("ABBAYE_BUILDING_VERSION", version); /// /// if let Some(ref makefile) = config.makefile { /// cmd.arg("-f").arg(makefile); /// } /// if let Some(ref target) = config.target { /// cmd.arg(target); /// } /// /// let mut child = cmd /// .stdout(Stdio::piped()) /// .stderr(Stdio::piped()) /// .spawn() /// .into_diagnostic()?; /// /// // Stream stdout and stderr concurrently so neither pipe fills up /// // and deadlocks the child process. /// let stdout = child.stdout.take().expect("stdout was piped"); /// let stderr = child.stderr.take().expect("stderr was piped"); /// let log_out = log.clone(); /// let log_err = log.clone(); /// let stdout_task = tokio::spawn(async move { /// let mut lines = BufReader::new(stdout).lines(); /// while let Ok(Some(line)) = lines.next_line().await { /// let _ = log_out.send(LogEvent::Line(line)); /// } /// }); /// let stderr_task = tokio::spawn(async move { /// let mut lines = BufReader::new(stderr).lines(); /// while let Ok(Some(line)) = lines.next_line().await { /// let _ = log_err.send(LogEvent::Line(line)); /// } /// }); /// /// // Drain I/O tasks *before* checking the exit code to avoid /// // losing the last lines of output. /// let status = child.wait().await.into_diagnostic()?; /// let _ = tokio::join!(stdout_task, stderr_task); /// /// if !status.success() { /// return Err(miette!( /// "make failed with exit status: {}", /// status.code().unwrap_or(-1) /// )); /// } /// /// // Collect declared outputs as artifacts. /// let mut artifacts = Vec::new(); /// for output in &config.outputs { /// if !output.exists() { /// return Err(miette!( /// "declared output does not exist after make: {}", /// output.display() /// )); /// } /// let name = output /// .file_name() /// .ok_or_else(|| miette!("output path has no file name"))? /// .to_string_lossy() /// .into_owned(); /// artifacts.push(ArtifactPath { path: output.clone(), name, hash: None }); /// } /// Ok(artifacts) /// } /// } /// ``` /// /// ### Key conventions /// /// **Always set `ABBAYE_BUILDING_VERSION`.** Every builder must expose the /// version being built through this environment variable so that build scripts /// can embed it without extra plumbing. /// /// **Always drain I/O tasks before checking the exit status.** If you /// `.await` the child first, the subprocess's pipes may fill up and cause it /// to block forever before it can exit. /// /// **Errors from `log.send(…)` are silently ignored** (`let _ = …`). The /// receiver may already be gone by the time a builder finishes — that is /// expected and harmless. /// /// **File artifacts → `dist/`, directory artifacts → `docs/`.** The /// orchestrator in `site.rs` distinguishes them by `path.is_dir()`. Return /// a directory path only for documentation trees. /// /// **CPU-bound or blocking I/O must use `spawn_blocking`.** Tokio runs on a /// small thread pool; blocking it starves other tasks. /// /// ## Step 4 — Register in `mod.rs` /// /// Open `src/builders/mod.rs` and make four additions. /// /// **4a — Declare the module:** /// /// ```rust,ignore /// pub mod make; /// ``` /// /// **4b — Import the types:** /// /// ```rust,ignore /// use make::{MakeBuilder, MakeBuilderConfig}; /// ``` /// /// **4c — Add a variant to [`AnyBuilder`]** with a doc comment containing a /// canonical TOML usage example (this becomes the JSON Schema description): /// /// ```rust,ignore /// /// Runs `make [target]` and collects declared output paths as artifacts. /// /// /// /// ```toml /// /// [[builders]] /// /// type = "make" /// /// target = "release" # optional /// /// outputs = ["dist/mybin"] /// /// ``` /// Make(MakeBuilderConfig), /// ``` /// /// The variant name is lowercased by `rename_all = "snake_case"`, so `Make` /// becomes `type = "make"` in TOML. Multi-word names use `CamelCase` in /// Rust and become `snake_case` in TOML (e.g. `WasmPack` → `wasm_pack`). /// /// **4d — Wire up `label()` and `build()`:** /// /// ```rust,ignore /// Self::Make(_) => "make", // in label() /// Self::Make(c) => MakeBuilder.build(c.clone(), version, log).await, // in build() /// ``` /// /// `label()` provides the default spinner prefix when the user hasn't given /// the builder an `id`. Keep it short and lowercase-kebab. /// /// ## Step 5 — Verify /// /// ```sh /// cargo build /// cargo run -- dump-schema | python3 -m json.tool | grep -A 20 '"make"' /// ``` /// /// ## Step 6 — Use it in `abbaye.toml` /// /// ```toml /// [[builders]] /// type = "make" /// target = "release" /// outputs = ["build/myprogram"] /// /// # With dependency ordering: /// [[builders]] /// type = "make" /// id = "make-release" /// target = "release" /// outputs = ["build/myprogram"] /// /// [[builders]] /// type = "script" /// script = ["upx build/myprogram"] /// outputs = ["build/myprogram"] /// depends_on = ["make-release"] /// ``` /// /// ## Advanced — Parallel sub-tasks with child spinners /// /// If your builder naturally fans out into independent sub-tasks (like the /// `cargo` builder running one `cargo build` per target triple), you can run /// them in parallel and give each its own sub-spinner in the UI. /// /// The pattern is always the same: /// /// 1. Send [`LogEvent::ChildStart`] `{ id, label }` to create the sub-spinner. /// 2. Forward output lines as [`LogEvent::ChildLine`] `{ id, line }`. /// 3. Send [`LogEvent::ChildFinish`] `{ id, success, summary }` to close it. /// /// ```rust,ignore /// join_set.spawn(async move { /// let _ = log.send(LogEvent::ChildStart { /// id: target.clone(), /// label: target.clone(), /// }); /// /// // ... do work, sending ChildLine events ... /// /// let _ = log.send(LogEvent::ChildFinish { /// id: target.clone(), /// success: result.is_ok(), /// summary: "done".to_owned(), /// }); /// }); /// ``` /// /// The `line_bridge` helper in [`crate::builders::cargo`] is a reusable /// pattern for adapting a plain-string sender to structured [`LogEvent`]s — /// feel free to copy or generalise it. /// /// ## Reviewer checklist /// /// - Config struct derives `Debug`, `Default` (or has a manual impl), /// `Clone`, `Deserialize`, `Serialize`, `JsonSchema`. /// - Every public config field has a doc comment. /// - The builder sets `ABBAYE_BUILDING_VERSION` in every subprocess /// environment. /// - Stdout and stderr are consumed concurrently to prevent pipe deadlocks. /// - Log send errors are silently ignored (`let _ = log.send(…)`). /// - Blocking I/O uses `tokio::task::spawn_blocking`. /// - Both `label()` and `build()` arms are added to [`AnyBuilder`]. /// - `pub mod` declaration and `use` imports are present in `mod.rs`. /// - `cargo build` succeeds with no new warnings. /// - `abbaye dump-schema` shows the new variant and all its fields. #[allow(async_fn_in_trait)] pub trait Builder { type ConfigType: Default + for<'de> Deserialize<'de> + Clone; /// Run the builder and return the produced artifacts. /// /// `version` is the abbaye release version currently being built (e.g. /// `"1.2.3"`). Implementations that spawn subprocesses must expose it as /// the `ABBAYE_BUILDING_VERSION` environment variable. /// /// `log` receives one-line status/output messages from the builder's /// subprocesses. Lines are sent without a trailing newline. The caller /// displays these in the UI (e.g. as a spinner message); errors sending /// are ignored because the receiver may already be closed. async fn build( &self, config: Self::ConfigType, version: &str, log: LogSender, ) -> Result<Vec<ArtifactPath>>; }