1use std::{
2 collections::HashSet,
3 path::PathBuf,
4 time::{Duration, Instant},
5};
6
7use super::wasm_cache::{
8 SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
9 SharedIncrementalTargetPrunePolicy, WasmBuildCacheMode, WasmBuildError, WasmBuildOutcome,
10 WasmBuildProgressConfig, WasmBuildProgressEvent, WasmBuildSpec, build_wasm_canisters_cached,
11 build_wasm_canisters_cached_with_progress,
12};
13
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
16pub struct WasmBuildBatchConfig {
17 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct WasmBuildBatchOutcome {
23 outcomes: Vec<WasmBuildOutcome>,
24 total: Duration,
25}
26
27#[non_exhaustive]
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub enum WasmBuildBatchProgressEvent {
31 BuildStarted {
33 index: usize,
35 total: usize,
37 },
38 BuildProgress {
40 index: usize,
42 event: WasmBuildProgressEvent,
44 },
45 BuildFinished {
47 index: usize,
49 },
50}
51
52#[derive(Debug)]
54pub struct WasmBuildBatchError {
55 failed_index: usize,
56 completed: Vec<WasmBuildOutcome>,
57 total: Duration,
58 source: WasmBuildError,
59}
60
61impl WasmBuildBatchOutcome {
62 #[must_use]
64 pub fn outcomes(&self) -> &[WasmBuildOutcome] {
65 &self.outcomes
66 }
67
68 #[must_use]
70 pub fn into_outcomes(self) -> Vec<WasmBuildOutcome> {
71 self.outcomes
72 }
73
74 #[must_use]
76 pub const fn total(&self) -> Duration {
77 self.total
78 }
79
80 pub fn shared_incremental_maintenance_outcomes(
85 &self,
86 ) -> impl Iterator<Item = (usize, &SharedIncrementalTargetMaintenanceOutcome)> {
87 self.outcomes
88 .iter()
89 .enumerate()
90 .filter_map(|(index, outcome)| {
91 outcome
92 .record()
93 .shared_incremental_maintenance()
94 .map(|maintenance| (index, maintenance))
95 })
96 }
97}
98
99impl WasmBuildBatchConfig {
100 #[must_use]
102 pub const fn new() -> Self {
103 Self {
104 shared_incremental_maintenance: None,
105 }
106 }
107
108 #[must_use]
110 pub const fn with_shared_incremental_target_maintenance(
111 mut self,
112 config: SharedIncrementalTargetMaintenanceConfig,
113 ) -> Self {
114 self.shared_incremental_maintenance = Some(config);
115 self
116 }
117
118 #[must_use]
120 pub const fn with_shared_incremental_target_maintenance_at_most_every(
121 self,
122 policy: SharedIncrementalTargetPrunePolicy,
123 minimum_interval: Duration,
124 ) -> Self {
125 self.with_shared_incremental_target_maintenance(
126 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
127 )
128 }
129
130 #[must_use]
132 pub const fn shared_incremental_target_maintenance(
133 self,
134 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
135 self.shared_incremental_maintenance
136 }
137}
138
139impl WasmBuildBatchError {
140 #[must_use]
142 pub const fn failed_index(&self) -> usize {
143 self.failed_index
144 }
145
146 #[must_use]
148 pub fn completed(&self) -> &[WasmBuildOutcome] {
149 &self.completed
150 }
151
152 #[must_use]
154 pub fn into_parts(self) -> (Vec<WasmBuildOutcome>, WasmBuildError) {
155 (self.completed, self.source)
156 }
157
158 #[must_use]
160 pub const fn total(&self) -> Duration {
161 self.total
162 }
163}
164
165impl std::fmt::Display for WasmBuildBatchOutcome {
166 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 let reused = self
168 .outcomes
169 .iter()
170 .filter(|outcome| outcome.is_reused())
171 .count();
172 write!(
173 formatter,
174 "builds={} built={} reused={} total={:?}",
175 self.outcomes.len(),
176 self.outcomes.len().saturating_sub(reused),
177 reused,
178 self.total,
179 )
180 }
181}
182
183pub fn build_wasm_canisters_cached_batch(
191 specs: &[WasmBuildSpec],
192) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError> {
193 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
194}
195
196pub fn build_wasm_canisters_cached_batch_with_config(
204 specs: &[WasmBuildSpec],
205 config: WasmBuildBatchConfig,
206) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError> {
207 build_wasm_batch(specs, config, |spec, _index| {
208 build_wasm_canisters_cached(spec)
209 })
210}
211
212pub fn build_wasm_canisters_cached_batch_with_progress<F>(
218 specs: &[WasmBuildSpec],
219 config: WasmBuildProgressConfig,
220 observer: F,
221) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError>
222where
223 F: FnMut(WasmBuildBatchProgressEvent),
224{
225 build_wasm_canisters_cached_batch_with_config_and_progress(
226 specs,
227 WasmBuildBatchConfig::new(),
228 config,
229 observer,
230 )
231}
232
233pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
235 specs: &[WasmBuildSpec],
236 batch_config: WasmBuildBatchConfig,
237 progress_config: WasmBuildProgressConfig,
238 mut observer: F,
239) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError>
240where
241 F: FnMut(WasmBuildBatchProgressEvent),
242{
243 let count = specs.len();
244 build_wasm_batch(specs, batch_config, |spec, index| {
245 observer(WasmBuildBatchProgressEvent::BuildStarted {
246 index,
247 total: count,
248 });
249 let outcome = build_wasm_canisters_cached_with_progress(spec, progress_config, |event| {
250 observer(WasmBuildBatchProgressEvent::BuildProgress { index, event });
251 })?;
252 observer(WasmBuildBatchProgressEvent::BuildFinished { index });
253 Ok(outcome)
254 })
255}
256
257fn build_wasm_batch<F>(
258 specs: &[WasmBuildSpec],
259 config: WasmBuildBatchConfig,
260 mut build: F,
261) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError>
262where
263 F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
264{
265 let started = Instant::now();
266 if let Some(failed_index) = config.shared_incremental_maintenance.and_then(|_| {
267 specs
268 .iter()
269 .position(|spec| spec.shared_incremental_target_maintenance().is_some())
270 }) {
271 return Err(WasmBuildBatchError {
272 failed_index,
273 completed: Vec::new(),
274 total: started.elapsed(),
275 source: batch_maintenance_ownership_error(),
276 });
277 }
278 let mut outcomes = Vec::with_capacity(specs.len());
279 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
280 for (index, spec) in specs.iter().enumerate() {
281 let configured = maintenance.prepare_spec(spec);
282 match build(configured.as_ref().unwrap_or(spec), index) {
283 Ok(outcome) => outcomes.push(outcome),
284 Err(source) => {
285 return Err(WasmBuildBatchError {
286 failed_index: index,
287 completed: outcomes,
288 total: started.elapsed(),
289 source,
290 });
291 }
292 }
293 }
294 Ok(WasmBuildBatchOutcome {
295 outcomes,
296 total: started.elapsed(),
297 })
298}
299
300struct BatchMaintenanceTracker {
301 config: Option<SharedIncrementalTargetMaintenanceConfig>,
302 configured_targets: HashSet<PathBuf>,
303}
304
305impl BatchMaintenanceTracker {
306 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
307 Self {
308 config,
309 configured_targets: HashSet::new(),
310 }
311 }
312
313 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
314 let config = self.config?;
315 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
316 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
317 return None;
318 };
319 if !self.configured_targets.insert(target_dir.clone()) {
320 return None;
321 }
322 Some(
323 spec.clone()
324 .with_shared_incremental_target_maintenance(config),
325 )
326 }
327}
328
329fn batch_maintenance_ownership_error() -> WasmBuildError {
330 WasmBuildError::InvalidSpec {
331 message:
332 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
333 .to_owned(),
334 }
335}
336
337impl std::fmt::Display for WasmBuildBatchError {
338 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 write!(
340 formatter,
341 "independent Wasm build {} failed after {} successful build(s): {}",
342 self.failed_index,
343 self.completed.len(),
344 self.source,
345 )
346 }
347}
348
349impl std::error::Error for WasmBuildBatchError {
350 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
351 Some(&self.source)
352 }
353}
354
355#[cfg(test)]
356mod tests;