makepad/tools/makepad_builder/src/jobs.rs
Admin d8f765668d tools, ci, docs, workspace: the Builder ships as source on Windows and is one shell script on macOS and Linux, the CI box, makepad-agents, cargo-makepad, and one workspace for every crate
The Makepad Builder (tools/makepad_builder: the Windows download is makepad-builder.zip with the Builder's source; macOS and Linux run makepad.sh), the CI runner (tools/ci), cargo-makepad, the agents tool, the root workspace and the documents.

Covers work ebf442a94..2b1a41df4 outside platform, draw, widgets, libs, apps and examples:

- wm: the Android super-app compiles tiles on the phone against the host engine
- cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust
- widgets, storybook: the widget catalogue app, some eighty new widgets, a theme store with mixable style sheets, and the rule that a press belongs to
- wm, widgets: the style tween starts with all eight weights, and the new style sheet takes the last number instead of macOS's
- platform: the Android desk swipes at 120 Hz — offscreen render passes and framebuffers cached, SVG meshes kept, font families that complete
- cef: a page's audio can be captured instead of played
- Button: activate from the keyboard when focused (#1241)
- libs/ai: exclude hub_ui and services from the AI workspace (#1242)
- flow: the flow-ui app and the flowgraph canvas return to the workspace, with staged progress in libs/flow
- svg: a stroke that bends tighter than its half width collapses its inner edge to the corner
- the accent a name is written with stops deciding whether search finds it
- macOS: keep the paint clock beating while the window is minimized (#1244)
- Linux: only link libdrm for the direct backend (#1245)
- cargo-makepad: find dependency crate dirs via cargo metadata (#1246)
- app_main!: only ship the fonts an app declares (#1247)
- ai stems: LaneDemixer, VocalsModel and the platform Stage origin/main needs
- platform, widgets, wm: every Vulkan pass renders through a negative-height viewport again, so a Wayland or X11 window stands upright, and no capture
- calendar: its own colour contrast is named explicitly, now that the theme store exports one under the same name through the widgets glob
- widgets: the supersampled resolve and the map's shadow mask sample their textures as stored, like every other render texture
- platform: every OpenGL texture pass stores top-left rows, custom cameras included, and culls with the winding its inverted projection gives
- platform, wm, director: a hosted app's software frame arrives as a top-left texture and the window manager and the director carry no flip term at al
- widgets, platform: the last twelve warnings go
- The Builder compiles itself again from the Makepad tree Scope's release pins and removes the source snapshots nothing is built from any more
- The regex engine accepts \b and \B, reports the earliest match so a scan can resume after it, and can decide word boundaries on ASCII so a code sear
- workspace: Scope's settings carry the code map's tab width and the directories each prepared project hides from its map
- AGENTS.md: a platform change made for one application needs the user's feedback and checks first, and app specifics never leak into the shared layer
- wm: the desktop style menu lists each style once
- cef, video, platform: what Stage's browser needs from the shared layers
- macOS: re-arm the display links when a window leaves the Dock (#1251)
- platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows
- ci: tools/ci, a Splash runtime that watches a branch and runs every ci.splash it finds
- stitch: the crate says what the rest of the repository says about its license
- ci: a run is one target dir and one cargo batch per target, and the wall draws
- ci: the wall shows THIS run and is fit for an OLED
- ci: untested is grey and nothing else
- ci: warming fills the cache and blames nobody
- Vulkan: stop printing the loader's startup narration by default (#1252)
- ci: the terminal gets its pty helper, director is a desktop tool, and a refused input is asked again
- platform: no warnings on tvOS and linux_direct, and a missing pty helper says so
- apps: what the CI box found
- ScrollBar: add `show_handle` for a view that scrolls without a grabbable bar (#1254)
- cargo-makepad: keep the android SDK at a stable path (#1253)
- ci: the window is a dashboard
- Remove Flow, asset and VJ applications moved into Stage
- ci: a stop or a restart is not a test result
- ci: a quiet header, a build that visibly moves, and scripts that leave the machine alone
- widgets: children a lookup discovers reach the dump, the snapshot and the flood searches
- tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests
- platform: a hidden window wears no hands-off frame
- examples: the UI tests pass, for the reasons they failed
- ci: a library's warnings turn the workspace block yellow, and a desktop tool is not warmed for the web
- libs: no warnings in the workspace check on any row
- ci: the workspace script gives the wall back after warming
- ci: a judgement is never lost to the way it was phrased, and every "retry" of the bridge is retried
- tests: the last three failures of the CI box's night
- Linux: fix window chrome button hovers and how maximized/fullscreen windows work (#1255)
- ci: the header names the tip being tested
- tools: makepad-screen is makepad-agents, and its binary is `agents`
- gif: the crate's doc examples compile
- ci: the tests are the platform's own, in release, in two minutes
- Tooltip: position anchored tooltips in the same draw (#1256)
- cef: a hosted browser survives its host's exit
- ci: a card is as high as its content
- tests: the three binaries over ten seconds in release come under it
- widgets: let the host install script mods into every Splash isolate
- widgets: a pooled test context forgets the last case's Escape claim
- widgets: the pooled test context's resets replace the globals
- wm: the shell menu says what it did, and the CI waits for that
- platform: the storage module says how much a volume has free
- git: the memory ledger can take what it is asked for
- ci: a driven app takes every key once, and the storage module builds for the browser
- RadioButton: fix its touch hover state and click-off behavior (#1258)
- HtmlLink: fix its hover and pressed states (#1257)
- RadioButton: take key focus on the click, not on the press (#1259)
- script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix
- script: port Octoscript WS1 — worklist equality with fuel/deadline/work bail, uncaught-error bail, allow_debug_output
- script: port Octoscript parser/tokenizer/control-flow VM patches (ws2)
- script: port Octoscript heap/string/array/object hardening onto upstream's allocation budget
- script: port orphaned Octoscript VM hardening (execution caps, clear_type_methods, parser diagnostics)
- script: keep silenced streaming evals running past uncaught errors
- script: the Octoscript hardening keeps a Makepad host's semantics and its speed
- fix(text): enable ttf-parser gvar-alloc for many-tuple variable fonts
- feat(text): CoreText outline fallback for hvgl-only fonts (macOS)
- draw text: the CoreText outline fallback is only ever resolved for a face whose outlines live solely in hvgl, and font-family diagnostics are the `f
- mail: the phone layout lines up
- clock: the phone layout fits both orientations
- calendar, reminders, calculator, sheets: the phone layouts fit their space
- platform, widgets, wm: a touch that is taken away is cancelled, never released
- weather, notes, finance, photos, route: the phone layouts tidy up
- files: the phone layout tidies up
- wm: the phone shell's surfaces match the app and the grid
- wm on Android: every app is its own process
- clock: a cancelled release neither opens an alarm nor switches tab
- platform, wm: hosted apps ask the WM for what they cannot do themselves
- wm on Android: apps build on the phone
- wm on Android: app transitions and gestures feel like the phone's
- android: rotated Vulkan windows stop rebuilding their swapchain every frame, and hosted children get touch
- vulkan (android): a window released on suspend is not released again
- android: hosted children hand frames over with GPU fences and draw only when they have work
- wm on Android: gestures and surfaces settle like the phone's
- widgets: touch lists scroll with Android's physics
- wm on Android: the shell follows the Pixel launcher's motion
- wm on Android: Recents shows the apps over a receding home, and the home screen switches looks
- wm on Android: the look switcher stays put, the iOS dock shows its icons, background apps follow a look change
- task, cargo-makepad, widgets, audio: the task manager chooses its columns, graphs every process' network and disk traffic and installs itself as a D
- agents: a user touching an app under test no longer stops the agent
- flowgraph: ordered ports take many wires, sockets can override their icon, and the canvas embeds as a viewport
- cef: captured frames and audio carry callback clocks and a navigation epoch
- git: timings read a portable clock
- ai: the hub's rig-fixture tests find the asset library without the asset client
- ai llm: a Metal main buffer is filled in 32 MB chunks
- diskmap: a scan lists each macOS directory in one getattrlistbulk call and classifies files without allocating or locking
- files: the tile view's three projections are toolbar buttons
- mail: the local view takes a theme-derived palette and line icons, search pages share records
- director: agent lanes form a tree and Grok joins the usage bar
- widgets, draw: skeuomorphic surfaces light each other through an optional relief buffer
- widgets, platform: a texture can light the relief buffer, menus take colour chips, and a style reload recompiles changed shader functions
- widgets: relief surfaces can travel like mechanical keys, and dark surfaces take less neighbour light
- platform, widgets, audio_route, ai: window crossfades and whole-frame presents, caption controls that click, themed menus, a stereo-pair audio tap,
- builder: the Builder TUI matches the new terminal design, Windows executables carry app icons, and every app has one
- platform: Android builds without Vulkan compile again, and the font-selection test counts the hosted entry point
- platform: the pipeline-skip repaint mark exists only on Apple, where Metal reads it
- builder: the terminal scripts delete nothing and read top to bottom, and every Builder delete stays inside its own folder
- files: the Files app deletes nothing and never overwrites
- director: coding agents ask before acting unless you choose otherwise
- sheets, score, git, home: saves cannot cut a file short or overwrite another one, and checkouts cannot write outside the repository
- git: worktree status matches git on real repositories and can be cancelled
- builder: one forward-only bar per setup component, green checks on finished setup rows, a black Builder window and a smaller Windows ZIP
- builder: the GPU notice is remembered
- cuda: builds link only the CUDA toolkit they are given, never a system install
- builder: a refused email opens the editor again with what was typed and says why, and the app section is YOUR APPS
- builder: a refused email says "Email not recognised" and keeps what was typed for fixing
- platform (windows): every window keeps animating with several windows open
- builder previews: long work gets a page of its own (steps as a checklist, the current one carrying its bar, footer working · ctrl+c stops), Account
- builder: long work runs on its own page
- builder: a first build resolves online, the Rust row and bars move when the work does, and the window fits the TUI
- audio_route: one API on macOS, Windows and Linux, monitoring by default and processing only when asked, plus shaders that discard a value compile to
- platform, widgets, ai: a restyle recompiles nothing it already has, shows only complete frames, and a window can own its caption
- builder: Compile shows a real bar
- builder: the compile line is the bar and n / total crates
- builder: apps get the icon their package declares
- builder: no Scope command
- widgets: relief surfaces can swing their light toward a point
- builder: a HEAD request is a HEAD
- builder: the menu waits for a choice
- builder: no menu flash after log-in
- builder: wait out Windows security on fresh Rust
- builder: the menu opens on the first of YOUR APPS
- platform (windows): half-float RGBA textures have four channels
- builder: CUDA whenever the machine has NVIDIA
- audio_route example: print the samples that reached the processor each second
- widgets, builder: the big widget families are features, all on by default, and the Builder takes none of them
- platform (macos): the display link is kept until it is invalidated
- ai hub: a local chat with no model says so once, plainly
- builder: the Windows exe is built for size
- windows: desktop apps open no console window, and still speak through pipes
- ai: Claude Desktop drives any app with the F10 panel
- Constrain Splash external I/O to the host service bridge (#1243)
- PortalList: stop following the end when scrolling to an earlier item (#1261)
- wm: starts on Windows
- ci: a full disk empties the build output before the run, not every row after it
- builder: an incremental compile's bar follows what really compiles
- builder: the compile bar's total is the app's own crates
- builder: menus wrap around
- wm: children open no console windows on Windows
- wm: the whole deck out of a Builder's source, compiled only when the person opens an app
- builder: CUDA crates build in an installation whose path has a space
- ai: Connect hands the .mcpb to Claude Desktop itself
- wm: the clock and calendar know the date on Windows
- platform (windows): a popup opened for the first time draws its rows
- wm: opening an app rebuilds it when its binary is out of date; warm instances never build
- builder: an app edited after its build shows as needing a compile
- civil-time: one local wall clock for every app, and it knows the zone on Windows
- platform + wm: a hosted child redraws at its tile's new size
- platform (windows): a texture pass drawn before its window has a size is skipped, not a crash
- draw: glass of a view that stopped drawing leaves the screen
- calendar: the Calendars sheet is an opaque panel, and the wide layout does not float one over its sidebar
- build: dev keeps line tables only, release is incremental without LTO, the parallel frontend is a documented local opt-in
- cargo-makepad: a binary with several app_main! entry points bundles
- script: #[derive(Script)] emits one helper call per field, and ScriptNew's default methods keep their bodies out of every type
- widgets: the Widget lookup methods and with_script_vm_id are compiled once, not once per widget type
- platform: studio-protocol no longer waits for script, so platform starts ~0.5 s earlier
- ai-speech: makepad-ai-sfx (and with it ai-h3) is built only for IndexTTS
- platform: an app's package dir, icons and bundle name rebuild only that app
- script + wasm_bridge: no build script that reruns on every file
- build: a private app cloned into apps/<name> joins this workspace, Stage first
- build: the AI stack, csg, Scope and Source Library join the one workspace
- builder: every app builds in the one Makepad workspace, one target directory per source snapshot
- ai chat: the chat's backends are the CLI, MCP and cloud providers; the local model is the `localai` feature
- apps: every app that hosts the chat has a `localai` feature for the local model, off unless the app's own job runs one
- builder: time each install component and phase
- builder: Build tools, Windows SDK, Rust and CUDA install side by side, each with its own bar
- builder bootstrap: rustc, rust-std and cargo download and unpack side by side under one Rust bar
- widgets: the library moves to makepad-widgets-core in widgets/core, and makepad-widgets becomes its front crate
- widgets: the Window, widget tree and panel theme reach the tweaker, voice, AI slot and dock through hooks, not through their modules
- widgets: every widget family is a crate of its own under widgets/families, and makepad-widgets links, re-exports and registers the ones its features
- aichat, widgets: the chat reads the design feedback through the tweaker's hook, so linking the chat no longer needs the tweaker
- apps, examples, libs: each crate builds only the widget families it uses, and the design overlay is each app's own default feature
- builder: an app release builds without the app's development defaults, and the catalog names every feature a release ships with
- widgets: the build scripts rerun on their own edits only, and makepad-widgets no longer reads MAKEPAD
- livepipe: takes makepad-widgets without its default families, now that the AI stack is in the one workspace
- builder: the catalog names the new localai defaults of route and ai-hub
- feedback: Send feedback, a caption icon and a small panel that shows exactly what is sent
- builder: an app it launches knows who the person is, for Send feedback
- feedback: the panel's wording reads right and draws in any font
- feedback: takes makepad-widgets without its default families
- builder: clear build data deletes target/ itself and says why when it cannot
- ai chat: a build without local AI starts on the first provider that answers here, not on "No model"
- platform: MAKEPAD=gpusim builds again
- cargo-makepad: a Windows desktop build links the app's icon into its own binary, not into RUSTFLAGS
- thiserror: RUSTC_BOOTSTRAP no longer turns on the unstable generic member access API
- builder: every end-user build uses rustc's parallel frontend, and falls back to one thread when it fails
- builder: the Windows SDK downloads the 25 cabinets its MSIs name instead of all 149, no screen or log shows a \\?\ path, long status messages wrap,
- builder: CUDA kernels compile in any install folder, and when they cannot the app is built without them
- builder: makepad-builder.exe declares itself like a well-formed Windows program and stops a stalled build through a job object instead of taskkill
- platform (macos): a contained panic no longer frees the windows under AppKit or leaves an app that ignores quit
- builder: macOS and Linux run the Builder as one shell script, the same TUI as Windows, and nothing is compiled for it
- builder + ai-cuda: the CUDA kernel progress shows in the Builder's Compile row, not scribbled over its screen
- builder: an app built with CUDA starts on Windows
- builder (windows): the downloaded exe is built large again
- builder (unix): the email check takes real addresses
- repo: no third-party comic archive, generated models or sample asset in the tree
- remove big example media files
- cargo_makepad: no stray KNMI radar frame in the Android Java sources
- widgets: the empty parts of an app's caption bar drag the window again
- ScrollBar: fade out when idle, like macOS overlay scrollers (#1264)
- platform: every app builds the same `windows` crate
- wm: in a Makepad Builder installation apps build through the Builder, however wm was started
- platform, draw: web builds start again
- platform: web text draws again
- widgets: the keyboard reaches boxes and modals
- widgets: a hover tooltip never sits under the mouse cursor
- platform: clipboard_read, reading text or an image from the system clipboard
- feedback: Send clipboard, and the dialog works from the keyboard
- builder: the Windows Builder ships as source, runs in its own console, and apps compile with Rust's GNU toolchain or Microsoft's
- platform: an app also finds its resource map in the builder folder beside it
- win_resource: PNG decode and encode on makepad-fast-inflate instead of zune-png
- builder: one folder layout everywhere, makepad-builder.zip with only the files a Windows build reads, a Rust download over parallel connections, and
- builder (windows): the .bat runs from a folder with spaces and parentheses, and the ZIP always carries it with CRLF
- builder: coding agents start from the macOS/Linux Builder, its compile bar has its end, the app rows say where each app is in a word or two, and Win
- builder (unix): running curl | sh again opens the existing installation with its saved email
- builder: log out from the Account row
- builder: `makepad-builder build APP` compiles an app offline exactly as the Builder does

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:11:31 +02:00

342 lines
13 KiB
Rust

//! Installing components side by side.
//!
//! [`run`] starts one thread per component (Build tools, Windows SDK, Rust,
//! CUDA), one shared unpack pool sized to the machine and the download
//! threads (see `fetch`). Components hand CPU work to the pool with
//! [`spawn`] and downloads to `fetch`; neither starts threads of its own, so
//! the cores are never oversubscribed however many components run.
//!
//! Progress is per thread (see `progress`): each component thread and every
//! task on its behalf forwards its events over one channel to the thread
//! that called [`run`], which hands them to its own hook (the work page).
use crate::{fetch, progress, timing};
use std::{
cell::RefCell,
collections::VecDeque,
panic::{catch_unwind, AssertUnwindSafe},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc, Condvar, Mutex,
},
thread,
time::{Duration, Instant},
};
/// One component to install: its row label and its work.
pub struct Job<'a> {
pub label: &'static str,
pub run: Box<dyn FnOnce() -> Result<(), String> + Send + 'a>,
}
impl<'a> Job<'a> {
pub fn new(label: &'static str, run: impl FnOnce() -> Result<(), String> + Send + 'a) -> Self {
Job { label, run: Box::new(run) }
}
}
/// What the threads of one [`run`] share.
pub struct Env {
pool: Arc<Pool>,
pub(crate) fetch: Arc<fetch::Fetcher>,
pub(crate) claims: crate::extract::Claims,
cancel: AtomicBool,
}
thread_local! {
static ENV: RefCell<Option<Arc<Env>>> = const { RefCell::new(None) };
}
pub(crate) fn env() -> Option<Arc<Env>> {
ENV.with(|e| e.borrow().clone())
}
/// True once another component failed: start nothing new, stop cleanly.
pub fn cancelled() -> bool {
env().is_some_and(|e| e.cancel.load(Ordering::Relaxed))
}
pub fn check_cancelled() -> Result<(), String> {
if cancelled() { Err(CANCELLED.into()) } else { Ok(()) }
}
const CANCELLED: &str = "stopped: another component failed";
/// Everything a thread's work depends on, to continue it on another thread.
#[derive(Clone)]
pub struct Ctx {
progress: progress::Ctx,
timing: String,
env: Option<Arc<Env>>,
}
impl Ctx {
pub fn current() -> Ctx {
Ctx { progress: progress::Ctx::current(), timing: timing::current(), env: env() }
}
pub fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
let previous = ENV.with(|e| e.replace(self.env.clone()));
struct Restore(Option<Arc<Env>>);
impl Drop for Restore {
fn drop(&mut self) {
ENV.with(|e| *e.borrow_mut() = self.0.take());
}
}
let _restore = Restore(previous);
let _timing = timing::adopt(&self.timing);
self.progress.enter(f)
}
}
/// The result of work running elsewhere.
pub struct Pending<R> {
rx: mpsc::Receiver<Result<R, String>>,
}
impl<R> Pending<R> {
pub fn channel() -> (mpsc::Sender<Result<R, String>>, Pending<R>) {
let (tx, rx) = mpsc::channel();
(tx, Pending { rx })
}
pub fn ready(value: Result<R, String>) -> Pending<R> {
let (tx, pending) = Pending::channel();
let _ = tx.send(value);
pending
}
/// Wait for the result. A pool thread runs queued tasks meanwhile, so
/// tasks may wait for tasks they spawned without starving the pool.
pub fn wait(self) -> Result<R, String> {
let pool = if IN_POOL.with(|p| p.get()) { env().map(|e| e.pool.clone()) } else { None };
let Some(pool) = pool else {
return self.rx.recv().unwrap_or_else(|_| Err("a worker stopped without a result".into()));
};
loop {
match self.rx.try_recv() {
Ok(result) => return result,
Err(mpsc::TryRecvError::Disconnected) => return Err("a worker stopped without a result".into()),
Err(mpsc::TryRecvError::Empty) => {}
}
match pool.try_pop() {
Some(task) => task(),
None => match self.rx.recv_timeout(Duration::from_millis(5)) {
Ok(result) => return result,
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => return Err("a worker stopped without a result".into()),
},
}
}
}
}
/// Wait for all, returning the first error after every one has finished
/// (nothing may still use what the caller borrowed to them).
pub fn wait_all<R>(pending: impl IntoIterator<Item = Pending<R>>) -> Result<Vec<R>, String> {
let mut out = Vec::new();
let mut error = None;
for p in pending {
match p.wait() {
Ok(r) => out.push(r),
Err(e) => {
if error.is_none() || error.as_deref() == Some(CANCELLED) {
error = Some(e);
}
}
}
}
match error {
Some(e) => Err(e),
None => Ok(out),
}
}
type Task = Box<dyn FnOnce() + Send>;
/// The unpack pool: decompressing and writing, one thread per core less one
/// (the download threads and the terminal stay responsive).
pub(crate) struct Pool {
queue: Mutex<(VecDeque<Task>, bool)>,
ready: Condvar,
}
thread_local! {
static IN_POOL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
impl Pool {
fn push(&self, task: Task) {
self.queue.lock().unwrap_or_else(|e| e.into_inner()).0.push_back(task);
self.ready.notify_one();
}
fn try_pop(&self) -> Option<Task> {
self.queue.lock().unwrap_or_else(|e| e.into_inner()).0.pop_front()
}
fn close(&self) {
self.queue.lock().unwrap_or_else(|e| e.into_inner()).1 = true;
self.ready.notify_all();
}
fn work(&self) {
IN_POOL.with(|p| p.set(true));
loop {
let task = {
let mut queue = self.queue.lock().unwrap_or_else(|e| e.into_inner());
loop {
if let Some(task) = queue.0.pop_front() {
break Some(task);
}
if queue.1 {
break None;
}
queue = self.ready.wait(queue).unwrap_or_else(|e| e.into_inner());
}
};
match task {
Some(task) => task(),
None => return,
}
}
}
}
/// Pool size: `MAKEPAD_BUILDER_UNPACK_THREADS`, else one per logical core
/// less one.
pub fn pool_threads() -> usize {
std::env::var("MAKEPAD_BUILDER_UNPACK_THREADS").ok().and_then(|s| s.parse().ok()).filter(|n| *n > 0)
.unwrap_or_else(|| thread::available_parallelism().map_or(4, |n| n.get()).saturating_sub(1).max(1))
}
/// Run `f` on the unpack pool, in this thread's context. Without a pool
/// (outside [`run`]) it runs right here.
pub fn spawn<R: Send + 'static>(f: impl FnOnce() -> Result<R, String> + Send + 'static) -> Pending<R> {
let Some(env) = env() else {
return Pending::ready(f());
};
let ctx = Ctx::current();
let (tx, pending) = Pending::channel();
env.pool.push(Box::new(move || {
let result = ctx.enter(|| catch_unwind(AssertUnwindSafe(f)));
let _ = tx.send(result.unwrap_or_else(|panic| Err(panic_message(panic))));
}));
pending
}
pub(crate) fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
let text = panic.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| panic.downcast_ref::<String>().cloned());
format!("internal error: {}", text.unwrap_or_else(|| "worker panicked".into()))
}
enum Event {
Progress(progress::Progress),
Done(&'static str, Result<(), String>, Duration),
}
/// Install `jobs` side by side and wait for all of them. Their progress
/// reaches this thread's hook; each finished one is reported as a "Ready"
/// or "Failed" event under its label.
///
/// A real failure stops the others at their next payload (downloads land
/// in the cache and unpacked payloads stay whole); the error names the
/// component. A compiler that Windows security software still holds does
/// not stop the others, and is returned (unchanged, so the caller can offer
/// the retry) only when nothing else failed.
pub fn run(jobs: Vec<Job<'_>>) -> Result<(), String> {
if jobs.is_empty() {
return Ok(());
}
let pool = Arc::new(Pool { queue: Mutex::new((VecDeque::new(), false)), ready: Condvar::new() });
let fetch = Arc::new(fetch::Fetcher::new());
let env = Arc::new(Env { pool: pool.clone(), fetch: fetch.clone(), claims: Default::default(), cancel: AtomicBool::new(false) });
let (tx, rx) = mpsc::channel::<Event>();
let labels: Vec<&'static str> = jobs.iter().map(|j| j.label).collect();
fetch.expect(&labels);
let mut results: Vec<(&'static str, Result<(), String>)> = Vec::new();
thread::scope(|s| {
for _ in 0..pool_threads() {
let pool = pool.clone();
s.spawn(move || pool.work());
}
for _ in 0..fetch.threads() {
let fetch = fetch.clone();
s.spawn(move || fetch.work());
}
for job in jobs {
let tx = tx.clone();
let env = env.clone();
s.spawn(move || {
let forward = tx.clone();
let label = job.label;
let hook: progress::Forward = Arc::new(move |mut p: progress::Progress| {
p.row = label.into();
let _ = forward.send(Event::Progress(p));
});
let start = Instant::now();
let ctx = Ctx { progress: progress::Ctx::default(), timing: String::new(), env: Some(env.clone()) };
let result = ctx.enter(|| {
progress::forward(hook, || {
let _timing = timing::component(label);
catch_unwind(AssertUnwindSafe(job.run)).unwrap_or_else(|panic| Err(panic_message(panic)))
})
});
env.fetch.ready(label);
if let Err(error) = &result {
if !crate::rustc::needs_compiler_retry(error) && error != CANCELLED {
env.cancel.store(true, Ordering::Relaxed);
}
}
let _ = tx.send(Event::Done(label, result, start.elapsed()));
});
}
drop(tx);
let mut open = labels.len();
while open > 0 {
let Ok(event) = rx.recv() else { break };
match event {
Event::Progress(p) => progress::deliver(p),
Event::Done(label, result, took) => {
open -= 1;
let (stage, detail) = match &result {
Ok(()) => (DONE, format!("{:.1}", took.as_secs_f64())),
Err(e) if e == CANCELLED => (STOPPED, e.clone()),
Err(e) if crate::rustc::needs_compiler_retry(e) => (WAITING, e.clone()),
Err(e) => (FAILED, e.clone()),
};
progress::deliver(finished(label, stage, &detail));
results.push((label, result));
}
}
}
// Component threads are done; the pool and downloads have no more work.
pool.close();
fetch.close();
});
for line in fetch.report() {
timing::note(line);
}
let (overlaps, examples) = env.claims.overlaps();
timing::note(format!("timing: unpack pool {} threads, {} connections, {overlaps} paths written by more than one payload",
pool_threads(), fetch.threads()));
for example in examples {
timing::note(format!("timing: twice: {example}"));
}
let mut retry = None;
let mut stopped = None;
for label in &labels {
let Some((_, result)) = results.iter().find(|(l, _)| l == label) else { continue };
match result {
Ok(()) => {}
Err(e) if e == CANCELLED => stopped = stopped.or(Some(format!("{label}: {e}"))),
Err(e) if crate::rustc::needs_compiler_retry(e) => retry = retry.or(Some(e.clone())),
Err(e) => return Err(format!("{label}: {e}")),
}
}
match (retry, stopped) {
(Some(e), _) => Err(e),
(None, Some(e)) => Err(e),
(None, None) => Ok(()),
}
}
/// Stages of the event that closes a component's row: done (its detail is
/// the seconds it took), failed, stopped for another's failure, or waiting
/// for security software (the error, for the retry).
pub const DONE: &str = "Component done";
pub const FAILED: &str = "Component failed";
pub const STOPPED: &str = "Component stopped";
pub const WAITING: &str = "Component waiting";
fn finished(label: &str, stage: &str, detail: &str) -> progress::Progress {
progress::Progress {
stage: stage.into(),
detail: detail.into(),
loaded: 0,
total: 0,
frac: 1.0,
unit: progress::Unit::None,
package: progress::Package::default(),
overall: None,
row: label.into(),
}
}