| @@ -86,14 +86,103 @@ pub(crate) async fn run_builders( |
| let total = builders.len(); |
| |
| if crate::utils::is_interactive() { |
| - run_builders_with_bars(builders, version, completion_txs, completion_rxs, total).await |
| + run_with_bars(builders, version, completion_txs, completion_rxs, total).await |
| } else { |
| - run_builders_with_logs(builders, version, completion_txs, completion_rxs, total).await |
| + run_with_logs(builders, version, completion_txs, completion_rxs, total).await |
| } |
| } |
| |
| -/// Interactive variant: uses `indicatif` progress bars and spinners. |
| -async fn run_builders_with_bars( |
| +// ── Shared builder-task body ──────────────────────────────────────────────── |
| + |
| +/// Events reported by the shared builder loop to the UI layer. |
| +enum BuildEvent { |
| + DepWait(String), |
| + Skip(String), |
| + Run, |
| + Success(usize), |
| + Failure(String), |
| +} |
| + |
| +/// Run a single builder inside a `JoinSet` task, handling dependency waiting, |
| +/// completion signalling, and reporting events back to the UI layer. |
| +async fn run_builder_task( |
| + entry: BuilderEntry, |
| + version: String, |
| + dep_receivers: Vec<(String, watch::Receiver<Option<bool>>)>, |
| + my_tx: Option<watch::Sender<Option<bool>>>, |
| + log_tx: mpsc::UnboundedSender<LogEvent>, |
| + on_event: impl Fn(BuildEvent) + Send + 'static, |
| +) -> Result<Vec<ArtifactPath>> { |
| + for (dep_id, mut rx) in dep_receivers { |
| + on_event(BuildEvent::DepWait(dep_id.clone())); |
| + |
| + let resolved = rx.wait_for(|v| v.is_some()).await; |
| + |
| + let succeeded = match resolved { |
| + Err(_) => false, |
| + Ok(r) => r.unwrap_or(false), |
| + }; |
| + |
| + if !succeeded { |
| + on_event(BuildEvent::Skip(dep_id.clone())); |
| + if let Some(tx) = &my_tx { |
| + let _ = tx.send(Some(false)); |
| + } |
| + return Ok(vec![]); |
| + } |
| + } |
| + |
| + on_event(BuildEvent::Run); |
| + let result = entry.build(&version, log_tx).await; |
| + let succeeded = result.is_ok(); |
| + |
| + if let Some(tx) = my_tx { |
| + let _ = tx.send(Some(succeeded)); |
| + } |
| + |
| + match &result { |
| + Ok(artifacts) => on_event(BuildEvent::Success(artifacts.len())), |
| + Err(e) => on_event(BuildEvent::Failure(e.to_string())), |
| + } |
| + result |
| +} |
| + |
| +/// Collect finished `JoinSet` tasks and split results into dist / doc artifacts. |
| +async fn collect_results( |
| + mut join_set: JoinSet<Result<Vec<ArtifactPath>>>, |
| + on_done: impl FnOnce(bool), |
| +) -> Result<(Vec<ArtifactPath>, Vec<ArtifactPath>)> { |
| + let mut errors: Vec<miette::Report> = Vec::new(); |
| + let mut dist_artifacts = Vec::new(); |
| + let mut doc_artifacts = Vec::new(); |
| + |
| + while let Some(res) = join_set.join_next().await { |
| + match res.into_diagnostic()? { |
| + Ok(artifacts) => { |
| + for artifact in artifacts { |
| + if artifact.path.is_dir() { |
| + doc_artifacts.push(artifact); |
| + } else { |
| + dist_artifacts.push(artifact); |
| + } |
| + } |
| + } |
| + Err(e) => errors.push(e), |
| + } |
| + } |
| + |
| + on_done(errors.is_empty()); |
| + |
| + if let Some(first_err) = errors.into_iter().next() { |
| + return Err(first_err); |
| + } |
| + |
| + Ok((dist_artifacts, doc_artifacts)) |
| +} |
| + |
| +// ── Interactive (progress bar) UI ─────────────────────────────────────────── |
| + |
| +async fn run_with_bars( |
| builders: &[BuilderEntry], |
| version: &str, |
| mut completion_txs: HashMap<String, watch::Sender<Option<bool>>>, |
| @@ -193,53 +282,57 @@ async fn run_builders_with_bars( |
| let summary_task = summary.clone(); |
| |
| join_set.spawn(async move { |
| - for (dep_id, mut rx) in dep_receivers { |
| - pb_task.set_message(format!("waiting for '{dep_id}'…")); |
| - |
| - let resolved = rx.wait_for(|v| v.is_some()).await; |
| - |
| - let succeeded = match resolved { |
| - Err(_) => false, |
| - Ok(r) => r.unwrap_or(false), |
| - }; |
| - |
| - if !succeeded { |
| - summary_task.inc(1); |
| - pb_task.finish_with_message(format!( |
| - "{YELLOW}\u{29B8} skipped{RESET} (dependency '{dep_id}' failed)" |
| - )); |
| - if let Some(tx) = &my_tx { |
| - let _ = tx.send(Some(false)); |
| + run_builder_task( |
| + entry, |
| + version, |
| + dep_receivers, |
| + my_tx, |
| + log_tx, |
| + move |event| match event { |
| + BuildEvent::DepWait(dep_id) => { |
| + pb_task.set_message(format!("waiting for '{dep_id}'…")); |
| } |
| - return Ok(vec![]); |
| - } |
| - } |
| - |
| - pb_task.set_message("running…"); |
| - let result = entry.build(&version, log_tx).await; |
| - let succeeded = result.is_ok(); |
| - |
| - if let Some(tx) = my_tx { |
| - let _ = tx.send(Some(succeeded)); |
| - } |
| - |
| - summary_task.inc(1); |
| - match &result { |
| - Ok(artifacts) => pb_task.finish_with_message(format!( |
| - "{GREEN}\u{2713} done{RESET} ({} artifact(s))", |
| - artifacts.len() |
| - )), |
| - Err(e) => pb_task.finish_with_message(format!("{RED}\u{2717} failed:{RESET} {e}")), |
| - } |
| - result |
| + BuildEvent::Skip(dep_id) => { |
| + summary_task.inc(1); |
| + pb_task.finish_with_message(format!( |
| + "{YELLOW}\u{29B8} skipped{RESET} (dependency '{dep_id}' failed)" |
| + )); |
| + } |
| + BuildEvent::Run => { |
| + pb_task.set_message("running…"); |
| + } |
| + BuildEvent::Success(count) => { |
| + summary_task.inc(1); |
| + pb_task.finish_with_message(format!( |
| + "{GREEN}\u{2713} done{RESET} ({count} artifact(s))" |
| + )); |
| + } |
| + BuildEvent::Failure(error) => { |
| + summary_task.inc(1); |
| + pb_task |
| + .finish_with_message(format!("{RED}\u{2717} failed:{RESET} {error}")); |
| + } |
| + }, |
| + ) |
| + .await |
| }); |
| } |
| |
| - collect_and_finish(join_set, &summary).await |
| + let summary_bar = summary.clone(); |
| + collect_results(join_set, move |ok| { |
| + let msg = if ok { |
| + format!("{GREEN}\u{2713} all done{RESET}") |
| + } else { |
| + format!("{RED}\u{2717} some builders failed{RESET}") |
| + }; |
| + summary_bar.finish_with_message(msg); |
| + }) |
| + .await |
| } |
| |
| -/// Non-interactive variant: logs progress via `tracing` instead of spinners. |
| -async fn run_builders_with_logs( |
| +// ── Non-interactive (tracing) UI ──────────────────────────────────────────── |
| + |
| +async fn run_with_logs( |
| builders: &[BuilderEntry], |
| version: &str, |
| mut completion_txs: HashMap<String, watch::Sender<Option<bool>>>, |
| @@ -299,107 +392,40 @@ async fn run_builders_with_logs( |
| let task_label = label.clone(); |
| |
| join_set.spawn(async move { |
| - for (dep_id, mut rx) in dep_receivers { |
| - info!("[{task_label}] waiting for '{dep_id}' …"); |
| - |
| - let resolved = rx.wait_for(|v| v.is_some()).await; |
| - |
| - let succeeded = match resolved { |
| - Err(_) => false, |
| - Ok(r) => r.unwrap_or(false), |
| - }; |
| - |
| - if !succeeded { |
| - info!("[{task_label}] skipped (dependency '{dep_id}' failed)"); |
| - if let Some(tx) = &my_tx { |
| - let _ = tx.send(Some(false)); |
| + run_builder_task( |
| + entry, |
| + version, |
| + dep_receivers, |
| + my_tx, |
| + log_tx, |
| + move |event| match event { |
| + BuildEvent::DepWait(dep_id) => { |
| + info!("[{task_label}] waiting for '{dep_id}' …"); |
| } |
| - return Ok(vec![]); |
| - } |
| - } |
| - |
| - info!("[{task_label}] running …"); |
| - let result = entry.build(&version, log_tx).await; |
| - let succeeded = result.is_ok(); |
| - |
| - if let Some(tx) = my_tx { |
| - let _ = tx.send(Some(succeeded)); |
| - } |
| - |
| - match &result { |
| - Ok(artifacts) => info!("[{task_label}] done ({} artifact(s))", artifacts.len()), |
| - Err(e) => warn!("[{task_label}] failed: {e}"), |
| - } |
| - result |
| - }); |
| - } |
| - |
| - let mut dist_artifacts = Vec::new(); |
| - let mut doc_artifacts = Vec::new(); |
| - let mut errors: Vec<miette::Report> = Vec::new(); |
| - while let Some(res) = join_set.join_next().await { |
| - match res.into_diagnostic()? { |
| - Ok(artifacts) => { |
| - for artifact in artifacts { |
| - if artifact.path.is_dir() { |
| - doc_artifacts.push(artifact); |
| - } else { |
| - dist_artifacts.push(artifact); |
| + BuildEvent::Skip(dep_id) => { |
| + info!("[{task_label}] skipped (dependency '{dep_id}' failed)"); |
| } |
| - } |
| - } |
| - Err(e) => errors.push(e), |
| - } |
| - } |
| - |
| - if errors.is_empty() { |
| - info!("All builders completed successfully."); |
| - } else { |
| - warn!("Some builders failed."); |
| - } |
| - |
| - if let Some(first_err) = errors.into_iter().next() { |
| - return Err(first_err); |
| - } |
| - |
| - Ok((dist_artifacts, doc_artifacts)) |
| -} |
| - |
| -/// Collect results from the `JoinSet` and finish the summary bar (interactive |
| -/// variant only). |
| -async fn collect_and_finish( |
| - mut join_set: JoinSet<Result<Vec<ArtifactPath>>>, |
| - summary: &ProgressBar, |
| -) -> Result<(Vec<ArtifactPath>, Vec<ArtifactPath>)> { |
| - let mut errors: Vec<miette::Report> = Vec::new(); |
| - let mut dist_artifacts = Vec::new(); |
| - let mut doc_artifacts = Vec::new(); |
| - |
| - while let Some(res) = join_set.join_next().await { |
| - match res.into_diagnostic()? { |
| - Ok(artifacts) => { |
| - for artifact in artifacts { |
| - if artifact.path.is_dir() { |
| - doc_artifacts.push(artifact); |
| - } else { |
| - dist_artifacts.push(artifact); |
| + BuildEvent::Run => { |
| + info!("[{task_label}] running …"); |
| } |
| - } |
| - } |
| - Err(e) => errors.push(e), |
| - } |
| - } |
| - |
| - let summary_msg = if errors.is_empty() { |
| - format!("{GREEN}\u{2713} all done{RESET}") |
| - } else { |
| - format!("{RED}\u{2717} some builders failed{RESET}") |
| - }; |
| - summary.finish_with_message(summary_msg); |
| - |
| - if let Some(first_err) = errors.into_iter().next() { |
| - return Err(first_err); |
| + BuildEvent::Success(count) => { |
| + info!("[{task_label}] done ({count} artifact(s))"); |
| + } |
| + BuildEvent::Failure(error) => { |
| + warn!("[{task_label}] failed: {error}"); |
| + } |
| + }, |
| + ) |
| + .await |
| + }); |
| } |
| |
| - Ok((dist_artifacts, doc_artifacts)) |
| + collect_results(join_set, |ok| { |
| + if ok { |
| + info!("All builders completed successfully."); |
| + } else { |
| + warn!("Some builders failed."); |
| + } |
| + }) |
| + .await |
| } |