Skip to main content

ic_build/artifacts/
wasm_batch.rs

1use super::cache_fs::canonicalize_allow_missing;
2use crate::batch::{BatchLabelError, validate_labels};
3
4use std::{
5    collections::HashSet,
6    marker::PhantomData,
7    path::PathBuf,
8    time::{Duration, Instant},
9};
10
11use super::wasm_cache::{
12    SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
13    SharedIncrementalTargetPrunePolicy, WasmBuildBatchAttempt, WasmBuildBatchInputMetrics,
14    WasmBuildBatchInputResolver, WasmBuildError, WasmBuildFailureDetails, WasmBuildFailurePhase,
15    WasmBuildFailureTimings, WasmBuildInputReuse, WasmBuildInputSnapshotState, WasmBuildOutcome,
16    WasmBuildProgressConfig, WasmBuildProgressEvent, WasmBuildSessionState, WasmBuildSpec,
17    WasmBuildTimings, WasmInputResolutionTimings, build_wasm_canisters_cached_in_batch,
18    build_wasm_canisters_cached_in_batch_with_progress, shared_incremental_target,
19};
20
21/// Orchestration shared by every entry in one independent Wasm build batch.
22#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub struct WasmBuildBatchConfig {
24    shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
25}
26
27/// Caller-labeled specification for one exact Wasm batch entry.
28///
29/// The label is report and progress identity only; it does not alter the
30/// underlying exact Wasm fingerprint or cache key.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct LabeledWasmBuildSpec {
33    label: String,
34    spec: WasmBuildSpec,
35}
36
37/// Ordered outcomes and failures from a collect-all Wasm build batch.
38#[derive(Debug)]
39pub struct WasmBuildBatchReport {
40    entries: Vec<WasmBuildBatchEntry>,
41    input_resolution: WasmBuildBatchInputMetrics,
42    total: Duration,
43}
44
45/// Explicit cross-call input snapshot scoped to a caller-held source lease.
46///
47/// The session contains no global state. It may reuse successful Cargo/rustc
48/// identity, metadata, input-discovery, and content-digest work while the
49/// caller keeps the supplied write-exclusion guard alive and unchanged.
50pub struct WasmBuildSession<'guard> {
51    state: WasmBuildSessionState,
52    _source_guard: PhantomData<&'guard ()>,
53}
54
55/// Immutable prepared Cargo input resolution shared by concurrent readers.
56///
57/// Preparation resolves the complete declared specification set while the
58/// caller holds a genuine source write-exclusion guard. Reader batches may run
59/// concurrently through `&self`, but cannot introduce specifications that
60/// were not declared during preparation.
61pub struct WasmBuildInputSnapshot<'guard> {
62    state: WasmBuildInputSnapshotState,
63    _source_guard: PhantomData<&'guard ()>,
64}
65
66/// Aggregate state retained by one explicit Wasm build session.
67#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68pub struct WasmBuildSessionMetrics {
69    snapshots: usize,
70    snapshot_reuses: usize,
71    invalidated: bool,
72}
73
74/// Preparation and reader-reuse counters for one immutable input snapshot.
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub struct WasmBuildInputSnapshotMetrics {
77    specifications: usize,
78    input_resolution_runs: usize,
79    input_resolution_reuses: usize,
80    input_resolution_timings: WasmInputResolutionTimings,
81    reader_reuses: usize,
82    invalidated: bool,
83}
84
85/// One ordered caller-labeled result from a Wasm build batch.
86#[derive(Debug)]
87pub struct WasmBuildBatchEntry {
88    index: usize,
89    label: String,
90    result: Result<WasmBuildOutcome, (WasmBuildError, WasmBuildFailureDetails)>,
91    entry_elapsed: Duration,
92}
93
94/// One successful Wasm batch entry.
95#[derive(Clone, Copy, Debug)]
96pub struct WasmBuildBatchOutcomeEntry<'a> {
97    index: usize,
98    label: &'a str,
99    outcome: &'a WasmBuildOutcome,
100    entry_elapsed: Duration,
101}
102
103/// One failed Wasm batch entry with its retained wall-clock time.
104#[derive(Clone, Copy, Debug)]
105pub struct WasmBuildBatchFailure<'a> {
106    index: usize,
107    label: &'a str,
108    error: &'a WasmBuildError,
109    details: WasmBuildFailureDetails,
110    entry_elapsed: Duration,
111}
112
113/// One integrated shared-target maintenance outcome from a Wasm batch.
114#[derive(Clone, Copy, Debug)]
115pub struct WasmBuildBatchMaintenanceEntry<'a> {
116    index: usize,
117    label: &'a str,
118    outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
119}
120
121/// Structural error that prevents a labeled Wasm batch from starting.
122#[non_exhaustive]
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub enum WasmBuildBatchContractError {
125    /// An entry label was empty.
126    EmptyLabel {
127        /// Zero-based position of the invalid entry.
128        index: usize,
129    },
130    /// Two entries used the same label.
131    DuplicateLabel {
132        /// Duplicated caller label.
133        label: String,
134        /// Position where the label first appeared.
135        first_index: usize,
136        /// Position where the label was repeated.
137        duplicate_index: usize,
138    },
139    /// A source mutation invalidated the caller's immutable-source lease.
140    SourceLeaseInvalidated,
141    /// A prepared snapshot reader requested a specification absent at preparation.
142    SpecificationNotPrepared {
143        /// Zero-based position of the undeclared entry.
144        index: usize,
145        /// Caller-owned label of the undeclared entry.
146        label: String,
147    },
148}
149
150impl LabeledWasmBuildSpec {
151    /// Attach a caller-owned stable label to one Wasm build specification.
152    #[must_use]
153    pub fn new(label: impl Into<String>, spec: WasmBuildSpec) -> Self {
154        Self {
155            label: label.into(),
156            spec,
157        }
158    }
159
160    /// Caller-owned report and progress label.
161    #[must_use]
162    pub fn label(&self) -> &str {
163        &self.label
164    }
165
166    /// Underlying exact Wasm build specification.
167    #[must_use]
168    pub const fn spec(&self) -> &WasmBuildSpec {
169        &self.spec
170    }
171
172    /// Consume the entry into its label and Wasm build specification.
173    #[must_use]
174    pub fn into_parts(self) -> (String, WasmBuildSpec) {
175        (self.label, self.spec)
176    }
177}
178
179impl<'guard> WasmBuildSession<'guard> {
180    /// Assert source immutability and bind reuse to the supplied guard's lifetime.
181    ///
182    /// The guard must prevent mutation of every Cargo/rustc executable,
183    /// manifest, configuration file, discovered source, declared additional
184    /// input, and relevant environment value used by every specification sent
185    /// through this session. The guard must remain held until the session is
186    /// dropped. This method cannot verify the guard's provenance; supplying an
187    /// unrelated value can permit stale cache reuse.
188    #[must_use]
189    pub fn assume_sources_immutable<Guard: ?Sized>(_source_write_guard: &'guard Guard) -> Self {
190        Self {
191            state: WasmBuildSessionState::new(),
192            _source_guard: PhantomData,
193        }
194    }
195
196    /// Build one sequential collect-all batch using retained immutable inputs.
197    ///
198    /// # Errors
199    /// Returns invalid labels or an invalidated source-lease error before work.
200    /// Individual build failures are retained in the returned report.
201    pub fn build_batch(
202        &mut self,
203        specs: &[LabeledWasmBuildSpec],
204        config: WasmBuildBatchConfig,
205    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
206        run_wasm_batch(
207            specs,
208            config,
209            Some(WasmBuildInputReuse::Session(&mut self.state)),
210            None,
211        )
212    }
213
214    /// Build one observed sequential batch using retained immutable inputs.
215    ///
216    /// # Errors
217    /// Returns invalid labels or an invalidated source-lease error before work.
218    /// Individual build failures are retained in the returned report.
219    pub fn build_batch_with_progress<F>(
220        &mut self,
221        specs: &[LabeledWasmBuildSpec],
222        batch_config: WasmBuildBatchConfig,
223        progress_config: WasmBuildProgressConfig,
224        mut observer: F,
225    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
226    where
227        F: FnMut(WasmBuildBatchProgressEvent),
228    {
229        run_wasm_batch(
230            specs,
231            batch_config,
232            Some(WasmBuildInputReuse::Session(&mut self.state)),
233            Some((progress_config, &mut observer)),
234        )
235    }
236
237    /// Current retained snapshot, reuse, and invalidation counters.
238    #[must_use]
239    pub const fn metrics(&self) -> WasmBuildSessionMetrics {
240        WasmBuildSessionMetrics {
241            snapshots: self.state.snapshot_count(),
242            snapshot_reuses: self.state.snapshot_reuses(),
243            invalidated: self.state.is_invalidated(),
244        }
245    }
246}
247
248impl WasmBuildSessionMetrics {
249    /// Number of successful exact specification snapshots currently retained.
250    #[must_use]
251    pub const fn snapshots(self) -> usize {
252        self.snapshots
253    }
254
255    /// Number of later entries resolved from a retained snapshot.
256    #[must_use]
257    pub const fn snapshot_reuses(self) -> usize {
258        self.snapshot_reuses
259    }
260
261    /// Whether a detected source race permanently invalidated this session.
262    #[must_use]
263    pub const fn is_invalidated(self) -> bool {
264        self.invalidated
265    }
266}
267
268impl<'guard> WasmBuildInputSnapshot<'guard> {
269    /// Resolve and freeze the complete specification set under a source lease.
270    ///
271    /// The guard must prevent mutation of every Cargo/rustc executable,
272    /// manifest, configuration file, discovered source, declared additional
273    /// input, and relevant environment value used by the supplied
274    /// specifications. The type system cannot verify guard provenance.
275    ///
276    /// # Errors
277    /// Returns specification, tool/metadata, filesystem or input-resolution errors.
278    pub fn prepare_assuming_sources_immutable<Guard: ?Sized>(
279        _source_write_guard: &'guard Guard,
280        specs: &[WasmBuildSpec],
281    ) -> Result<Self, WasmBuildError> {
282        Ok(Self {
283            state: WasmBuildInputSnapshotState::prepare(specs)?,
284            _source_guard: PhantomData,
285        })
286    }
287
288    /// Build one sequential collect-all batch from prepared inputs.
289    ///
290    /// Separate calls may run concurrently. Every exact specification must
291    /// have been supplied to [`Self::prepare_assuming_sources_immutable`].
292    ///
293    /// # Errors
294    /// Returns invalid labels, an invalidated lease or an unprepared specification
295    /// error before work. Individual build failures are retained in the report.
296    pub fn build_batch(
297        &self,
298        specs: &[LabeledWasmBuildSpec],
299        config: WasmBuildBatchConfig,
300    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
301        run_wasm_batch(
302            specs,
303            config,
304            Some(WasmBuildInputReuse::Snapshot(&self.state)),
305            None,
306        )
307    }
308
309    /// Build one observed sequential batch from prepared inputs.
310    ///
311    /// Separate calls may run concurrently and use independent observers.
312    ///
313    /// # Errors
314    /// Returns invalid labels, an invalidated lease or an unprepared specification
315    /// error before work. Individual build failures are retained in the report.
316    pub fn build_batch_with_progress<F>(
317        &self,
318        specs: &[LabeledWasmBuildSpec],
319        batch_config: WasmBuildBatchConfig,
320        progress_config: WasmBuildProgressConfig,
321        mut observer: F,
322    ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
323    where
324        F: FnMut(WasmBuildBatchProgressEvent),
325    {
326        run_wasm_batch(
327            specs,
328            batch_config,
329            Some(WasmBuildInputReuse::Snapshot(&self.state)),
330            Some((progress_config, &mut observer)),
331        )
332    }
333
334    /// Current preparation, reader-reuse, and invalidation metrics.
335    #[must_use]
336    pub fn metrics(&self) -> WasmBuildInputSnapshotMetrics {
337        let preparation = self.state.preparation_metrics();
338        WasmBuildInputSnapshotMetrics {
339            specifications: self.state.specification_count(),
340            input_resolution_runs: preparation.runs,
341            input_resolution_reuses: preparation.reuses,
342            input_resolution_timings: self.state.preparation_timings(),
343            reader_reuses: self.state.reader_reuses(),
344            invalidated: self.state.is_invalidated(),
345        }
346    }
347}
348
349impl WasmBuildInputSnapshotMetrics {
350    /// Number of exact specifications captured during preparation.
351    #[must_use]
352    pub const fn specifications(self) -> usize {
353        self.specifications
354    }
355
356    /// Number of workspace/toolchain resolution snapshots prepared.
357    #[must_use]
358    pub const fn input_resolution_runs(self) -> usize {
359        self.input_resolution_runs
360    }
361
362    /// Number of prepared specifications sharing another resolution run.
363    #[must_use]
364    pub const fn input_resolution_reuses(self) -> usize {
365        self.input_resolution_reuses
366    }
367
368    /// Complete tool, metadata, discovery, and hashing preparation timings.
369    #[must_use]
370    pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
371        self.input_resolution_timings
372    }
373
374    /// Cumulative exact specification resolutions served to readers.
375    #[must_use]
376    pub const fn reader_reuses(self) -> usize {
377        self.reader_reuses
378    }
379
380    /// Whether any reader detected a violation of the source lease.
381    #[must_use]
382    pub const fn is_invalidated(self) -> bool {
383        self.invalidated
384    }
385}
386
387impl WasmBuildBatchEntry {
388    /// Zero-based position in the supplied labeled specification slice.
389    #[must_use]
390    pub const fn index(&self) -> usize {
391        self.index
392    }
393
394    /// Caller-owned stable label.
395    #[must_use]
396    pub fn label(&self) -> &str {
397        &self.label
398    }
399
400    /// Structured success or failure for this entry.
401    ///
402    /// # Errors
403    /// Returns this entry's recorded build error when the entry failed.
404    pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
405        match &self.result {
406            Ok(outcome) => Ok(outcome),
407            Err((error, _)) => Err(error),
408        }
409    }
410
411    /// Successful Wasm outcome, when this entry succeeded.
412    #[must_use]
413    pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
414        self.result.as_ref().ok()
415    }
416
417    /// Structured build failure, when this entry failed.
418    #[must_use]
419    pub fn error(&self) -> Option<&WasmBuildError> {
420        self.result().err()
421    }
422
423    /// Structured phase and partial timings when this entry failed.
424    #[must_use]
425    pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
426        match &self.result {
427            Ok(_) => None,
428            Err((_, details)) => Some(*details),
429        }
430    }
431
432    /// Complete wall-clock time retained for this entry.
433    #[must_use]
434    pub const fn entry_elapsed(&self) -> Duration {
435        self.entry_elapsed
436    }
437
438    /// Whether this entry completed successfully.
439    #[must_use]
440    pub const fn is_success(&self) -> bool {
441        self.result.is_ok()
442    }
443
444    /// Consume the entry into its identity, result, optional failure details, and wall time.
445    pub fn into_parts(
446        self,
447    ) -> (
448        usize,
449        String,
450        Result<WasmBuildOutcome, WasmBuildError>,
451        Option<WasmBuildFailureDetails>,
452        Duration,
453    ) {
454        let (result, failure) = match self.result {
455            Ok(outcome) => (Ok(outcome), None),
456            Err((error, details)) => (Err(error), Some(details)),
457        };
458        (self.index, self.label, result, failure, self.entry_elapsed)
459    }
460}
461
462impl<'a> WasmBuildBatchOutcomeEntry<'a> {
463    /// Zero-based position in the supplied labeled specification slice.
464    #[must_use]
465    pub const fn index(self) -> usize {
466        self.index
467    }
468
469    /// Caller-owned stable label.
470    #[must_use]
471    pub const fn label(self) -> &'a str {
472        self.label
473    }
474
475    /// Successful Wasm build outcome.
476    #[must_use]
477    pub const fn outcome(self) -> &'a WasmBuildOutcome {
478        self.outcome
479    }
480
481    /// Complete wall-clock time retained for this successful entry.
482    #[must_use]
483    pub const fn entry_elapsed(self) -> Duration {
484        self.entry_elapsed
485    }
486}
487
488impl<'a> WasmBuildBatchFailure<'a> {
489    /// Zero-based position in the supplied specification slice.
490    #[must_use]
491    pub const fn index(self) -> usize {
492        self.index
493    }
494
495    /// Caller-owned stable label.
496    #[must_use]
497    pub const fn label(self) -> &'a str {
498        self.label
499    }
500
501    /// Structured acquisition failure.
502    #[must_use]
503    pub const fn error(self) -> &'a WasmBuildError {
504        self.error
505    }
506
507    /// Primary acquisition phase that returned the failure.
508    #[must_use]
509    pub const fn phase(self) -> WasmBuildFailurePhase {
510        self.details.phase()
511    }
512
513    /// Partial phase timings retained before the failure returned.
514    #[must_use]
515    pub const fn timings(self) -> WasmBuildFailureTimings {
516        self.details.timings()
517    }
518
519    /// Complete wall-clock time retained for this failed entry.
520    #[must_use]
521    pub const fn entry_elapsed(self) -> Duration {
522        self.entry_elapsed
523    }
524}
525
526impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
527    /// Zero-based position in the supplied labeled specification slice.
528    #[must_use]
529    pub const fn index(self) -> usize {
530        self.index
531    }
532
533    /// Caller-owned stable label.
534    #[must_use]
535    pub const fn label(self) -> &'a str {
536        self.label
537    }
538
539    /// Structured shared-target maintenance outcome.
540    #[must_use]
541    pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
542        self.outcome
543    }
544}
545
546/// Aggregate counters and successful-acquisition timings for a Wasm build batch.
547#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
548pub struct WasmBuildBatchMetrics {
549    specifications: usize,
550    built: usize,
551    reused: usize,
552    input_resolution_runs: usize,
553    input_resolution_reuses: usize,
554    input_resolution_session_reuses: usize,
555    input_resolution_prepared_reuses: usize,
556    successful_timings: WasmBuildTimings,
557    total: Duration,
558}
559
560/// Structured progress for an independent sequence of exact Wasm builds.
561#[non_exhaustive]
562#[derive(Clone, Debug, Eq, PartialEq)]
563pub enum WasmBuildBatchProgressEvent {
564    /// One independently resolved build specification is about to start.
565    BuildStarted {
566        /// Zero-based position in the supplied specification slice.
567        index: usize,
568        /// Caller-owned stable label.
569        label: String,
570        /// Total number of supplied specifications.
571        total: usize,
572    },
573    /// Progress forwarded from one independent build.
574    BuildProgress {
575        /// Zero-based position in the supplied specification slice.
576        index: usize,
577        /// Caller-owned stable label.
578        label: String,
579        /// Event emitted by that build.
580        event: WasmBuildProgressEvent,
581    },
582    /// One independent build completed successfully.
583    BuildFinished {
584        /// Zero-based position in the supplied specification slice.
585        index: usize,
586        /// Caller-owned stable label.
587        label: String,
588    },
589    /// One independent build failed.
590    BuildFailed {
591        /// Zero-based position in the supplied specification slice.
592        index: usize,
593        /// Caller-owned stable label.
594        label: String,
595    },
596}
597
598impl WasmBuildBatchReport {
599    /// Ordered labeled entries.
600    #[must_use]
601    pub fn entries(&self) -> &[WasmBuildBatchEntry] {
602        &self.entries
603    }
604
605    /// Consume the report into its ordered labeled entries.
606    #[must_use]
607    pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
608        self.entries
609    }
610
611    /// Structured successful entries with labels and wall-clock times.
612    pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
613        self.entries.iter().filter_map(|entry| {
614            entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
615                index: entry.index,
616                label: &entry.label,
617                outcome,
618                entry_elapsed: entry.entry_elapsed,
619            })
620        })
621    }
622
623    /// Structured failed entries with labels and wall-clock times.
624    pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
625        self.entries.iter().filter_map(|entry| {
626            entry
627                .result
628                .as_ref()
629                .err()
630                .map(|(error, details)| WasmBuildBatchFailure {
631                    index: entry.index,
632                    label: &entry.label,
633                    error,
634                    details: *details,
635                    entry_elapsed: entry.entry_elapsed,
636                })
637        })
638    }
639
640    /// Labeled integrated shared-target maintenance outcomes.
641    ///
642    /// Batch-owned maintenance contributes at most one outcome for each
643    /// distinct configured shared-target path.
644    pub fn shared_incremental_maintenance_outcomes(
645        &self,
646    ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
647        self.outcomes().filter_map(|entry| {
648            entry
649                .outcome
650                .record()
651                .shared_incremental_maintenance()
652                .map(|outcome| WasmBuildBatchMaintenanceEntry {
653                    index: entry.index,
654                    label: entry.label,
655                    outcome,
656                })
657        })
658    }
659
660    /// Complete wall-clock time for the sequential collect-all batch.
661    #[must_use]
662    pub const fn total(&self) -> Duration {
663        self.total
664    }
665
666    /// Whether every specification completed successfully.
667    #[must_use]
668    pub fn is_success(&self) -> bool {
669        self.entries.iter().all(WasmBuildBatchEntry::is_success)
670    }
671
672    /// Aggregate outcome, input-resolution reuse, and timing counters.
673    #[must_use]
674    pub fn metrics(&self) -> WasmBuildBatchMetrics {
675        let mut metrics = WasmBuildBatchMetrics {
676            specifications: self.entries.len(),
677            input_resolution_runs: self.input_resolution.runs,
678            input_resolution_reuses: self.input_resolution.reuses,
679            input_resolution_session_reuses: self.input_resolution.session_reuses,
680            input_resolution_prepared_reuses: self.input_resolution.prepared_reuses,
681            total: self.total,
682            ..WasmBuildBatchMetrics::default()
683        };
684        for entry in self.outcomes() {
685            let outcome = entry.outcome();
686            if outcome.is_reused() {
687                metrics.reused += 1;
688            } else {
689                metrics.built += 1;
690            }
691            metrics.successful_timings = metrics
692                .successful_timings
693                .saturating_add(outcome.record().timings());
694        }
695        metrics
696    }
697}
698
699impl WasmBuildBatchMetrics {
700    /// Number of supplied specifications.
701    #[must_use]
702    pub const fn specifications(self) -> usize {
703        self.specifications
704    }
705
706    /// Number of successful specifications.
707    #[must_use]
708    pub const fn succeeded(self) -> usize {
709        self.built + self.reused
710    }
711
712    /// Number of failed specifications.
713    #[must_use]
714    pub const fn failed(self) -> usize {
715        self.specifications - self.succeeded()
716    }
717
718    /// Number of newly built Wasm artifact sets.
719    #[must_use]
720    pub const fn built(self) -> usize {
721        self.built
722    }
723
724    /// Number of Wasm artifact sets reused from the exact cache.
725    #[must_use]
726    pub const fn reused(self) -> usize {
727        self.reused
728    }
729
730    /// Number of workspace/toolchain input-resolution snapshots performed.
731    #[must_use]
732    pub const fn input_resolution_runs(self) -> usize {
733        self.input_resolution_runs
734    }
735
736    /// Number of specifications resolved by reusing another batch snapshot.
737    #[must_use]
738    pub const fn input_resolution_reuses(self) -> usize {
739        self.input_resolution_reuses
740    }
741
742    /// Number of specifications resolved from an explicit session snapshot.
743    #[must_use]
744    pub const fn input_resolution_session_reuses(self) -> usize {
745        self.input_resolution_session_reuses
746    }
747
748    /// Number of specifications resolved from a prepared concurrent snapshot.
749    #[must_use]
750    pub const fn input_resolution_prepared_reuses(self) -> usize {
751        self.input_resolution_prepared_reuses
752    }
753
754    /// Sum of timings from successful acquisitions.
755    #[must_use]
756    pub const fn successful_timings(self) -> WasmBuildTimings {
757        self.successful_timings
758    }
759
760    /// Complete wall-clock time for the sequential batch.
761    #[must_use]
762    pub const fn total(self) -> Duration {
763        self.total
764    }
765}
766
767impl WasmBuildBatchConfig {
768    /// Create batch orchestration without batch-owned target maintenance.
769    #[must_use]
770    pub const fn new() -> Self {
771        Self {
772            shared_incremental_maintenance: None,
773        }
774    }
775
776    /// Maintain each distinct resolved shared target once through its first batch entry.
777    #[must_use]
778    pub const fn with_shared_incremental_target_maintenance(
779        mut self,
780        config: SharedIncrementalTargetMaintenanceConfig,
781    ) -> Self {
782        self.shared_incremental_maintenance = Some(config);
783        self
784    }
785
786    /// Strictly maintain each distinct shared target at most once per interval.
787    #[must_use]
788    pub const fn with_shared_incremental_target_maintenance_at_most_every(
789        self,
790        policy: SharedIncrementalTargetPrunePolicy,
791        minimum_interval: Duration,
792    ) -> Self {
793        self.with_shared_incremental_target_maintenance(
794            SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
795        )
796    }
797
798    /// Batch-owned shared-target maintenance, when configured.
799    #[must_use]
800    pub const fn shared_incremental_target_maintenance(
801        self,
802    ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
803        self.shared_incremental_maintenance
804    }
805}
806
807impl std::fmt::Display for WasmBuildBatchReport {
808    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
809        let metrics = self.metrics();
810        write!(
811            formatter,
812            "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} input_resolution_prepared_reuses={} successful_timings=({}) total={:?}",
813            metrics.specifications(),
814            metrics.succeeded(),
815            metrics.failed(),
816            metrics.built(),
817            metrics.reused(),
818            metrics.input_resolution_runs(),
819            metrics.input_resolution_reuses(),
820            metrics.input_resolution_session_reuses(),
821            metrics.input_resolution_prepared_reuses(),
822            metrics.successful_timings(),
823            metrics.total(),
824        )
825    }
826}
827
828/// Build every Wasm specification as an independent Cargo invocation.
829///
830/// Specifications run sequentially and every result is retained. Each entry
831/// keeps its own package set, profile arguments, feature resolution,
832/// fingerprint, locks, and cache policy. Packages are never combined into one
833/// Cargo command because doing so can unify shared dependency features.
834///
835/// # Errors
836/// Returns a contract error for an empty or duplicate label before any work.
837/// Individual build failures are retained in the returned report.
838pub fn build_wasm_canisters_cached_batch(
839    specs: &[LabeledWasmBuildSpec],
840) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
841    build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
842}
843
844/// Build an independent Wasm batch with shared batch orchestration.
845///
846/// Batch-owned maintenance is attached only to the first specification for
847/// each distinct configured shared-target path. Isolated specifications are
848/// unaffected. An entry mixing batch-owned and per-spec integrated maintenance
849/// reports an indexed error without preventing later entries from running.
850///
851/// # Errors
852/// Returns a contract error for an empty or duplicate label before any work.
853/// Individual build failures are retained in the returned report.
854pub fn build_wasm_canisters_cached_batch_with_config(
855    specs: &[LabeledWasmBuildSpec],
856    config: WasmBuildBatchConfig,
857) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
858    run_wasm_batch(specs, config, None, None)
859}
860
861/// Build an independent Wasm batch while forwarding structured progress.
862///
863/// The same observation configuration is applied to every entry. Batch events
864/// identify the originating specification without altering the standalone
865/// build semantics.
866///
867/// # Errors
868/// Returns a contract error for an empty or duplicate label before any work.
869/// Individual build failures are retained in the returned report.
870pub fn build_wasm_canisters_cached_batch_with_progress<F>(
871    specs: &[LabeledWasmBuildSpec],
872    config: WasmBuildProgressConfig,
873    observer: F,
874) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
875where
876    F: FnMut(WasmBuildBatchProgressEvent),
877{
878    build_wasm_canisters_cached_batch_with_config_and_progress(
879        specs,
880        WasmBuildBatchConfig::new(),
881        config,
882        observer,
883    )
884}
885
886/// Build a configured independent Wasm batch while forwarding structured progress.
887///
888/// # Errors
889/// Returns a contract error for an empty or duplicate label before any work.
890/// Individual build failures are retained in the returned report.
891pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
892    specs: &[LabeledWasmBuildSpec],
893    batch_config: WasmBuildBatchConfig,
894    progress_config: WasmBuildProgressConfig,
895    mut observer: F,
896) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
897where
898    F: FnMut(WasmBuildBatchProgressEvent),
899{
900    run_wasm_batch(
901        specs,
902        batch_config,
903        None,
904        Some((progress_config, &mut observer)),
905    )
906}
907
908fn run_wasm_batch(
909    specs: &[LabeledWasmBuildSpec],
910    batch_config: WasmBuildBatchConfig,
911    reuse: Option<WasmBuildInputReuse<'_>>,
912    mut observation: Option<(
913        WasmBuildProgressConfig,
914        &mut dyn FnMut(WasmBuildBatchProgressEvent),
915    )>,
916) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
917    validate_batch_labels(specs)?;
918    validate_input_reuse(specs, reuse.as_ref())?;
919    let count = specs.len();
920    let mut resolver =
921        WasmBuildBatchInputResolver::new(specs.iter().map(LabeledWasmBuildSpec::spec), reuse);
922    let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
923        let Some((progress_config, observer)) = observation.as_mut() else {
924            return build_wasm_canisters_cached_in_batch(spec, index, &mut resolver);
925        };
926        let label = specs[index].label.clone();
927        observer(WasmBuildBatchProgressEvent::BuildStarted {
928            index,
929            label: label.clone(),
930            total: count,
931        });
932        let attempt = build_wasm_canisters_cached_in_batch_with_progress(
933            spec,
934            index,
935            &mut resolver,
936            *progress_config,
937            |event| {
938                observer(WasmBuildBatchProgressEvent::BuildProgress {
939                    index,
940                    label: label.clone(),
941                    event,
942                });
943            },
944        );
945        observer(match &attempt.result {
946            Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
947            Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
948        });
949        attempt
950    });
951    report.input_resolution = resolver.metrics();
952    Ok(report)
953}
954
955fn build_wasm_batch<F>(
956    specs: &[LabeledWasmBuildSpec],
957    config: WasmBuildBatchConfig,
958    mut build: F,
959) -> WasmBuildBatchReport
960where
961    F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
962{
963    let started = Instant::now();
964    let mut entries = Vec::with_capacity(specs.len());
965    let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
966    for (index, labeled) in specs.iter().enumerate() {
967        let entry_started = Instant::now();
968        let spec = &labeled.spec;
969        let result = if config.shared_incremental_maintenance.is_some()
970            && spec.shared_incremental_target_maintenance().is_some()
971        {
972            Err((
973                batch_maintenance_ownership_error(),
974                WasmBuildFailureDetails::specification(entry_started.elapsed()),
975            ))
976        } else {
977            match maintenance.prepare_spec(spec) {
978                Ok(configured) => build(configured.as_ref().unwrap_or(spec), index).result,
979                Err(error) => Err((
980                    error,
981                    WasmBuildFailureDetails::specification(entry_started.elapsed()),
982                )),
983            }
984        };
985        entries.push(WasmBuildBatchEntry {
986            index,
987            label: labeled.label.clone(),
988            result,
989            entry_elapsed: entry_started.elapsed(),
990        });
991    }
992    WasmBuildBatchReport {
993        entries,
994        input_resolution: WasmBuildBatchInputMetrics::default(),
995        total: started.elapsed(),
996    }
997}
998
999fn validate_batch_labels(
1000    specs: &[LabeledWasmBuildSpec],
1001) -> Result<(), WasmBuildBatchContractError> {
1002    validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
1003    {
1004        BatchLabelError::Empty { index } => WasmBuildBatchContractError::EmptyLabel { index },
1005        BatchLabelError::Duplicate {
1006            label,
1007            first_index,
1008            duplicate_index,
1009        } => WasmBuildBatchContractError::DuplicateLabel {
1010            label,
1011            first_index,
1012            duplicate_index,
1013        },
1014    })
1015}
1016
1017fn validate_input_reuse(
1018    specs: &[LabeledWasmBuildSpec],
1019    reuse: Option<&WasmBuildInputReuse<'_>>,
1020) -> Result<(), WasmBuildBatchContractError> {
1021    match reuse {
1022        Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
1023            Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1024        }
1025        Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
1026            Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1027        }
1028        Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
1029            for (index, labeled) in specs.iter().enumerate() {
1030                if !snapshot.contains(&labeled.spec) {
1031                    return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
1032                        index,
1033                        label: labeled.label.clone(),
1034                    });
1035                }
1036            }
1037            Ok(())
1038        }
1039        _ => Ok(()),
1040    }
1041}
1042
1043struct BatchMaintenanceTracker {
1044    config: Option<SharedIncrementalTargetMaintenanceConfig>,
1045    configured_targets: HashSet<PathBuf>,
1046}
1047
1048impl BatchMaintenanceTracker {
1049    fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
1050        Self {
1051            config,
1052            configured_targets: HashSet::new(),
1053        }
1054    }
1055
1056    fn prepare_spec(
1057        &mut self,
1058        spec: &WasmBuildSpec,
1059    ) -> Result<Option<WasmBuildSpec>, WasmBuildError> {
1060        let Some(config) = self.config else {
1061            return Ok(None);
1062        };
1063        debug_assert!(spec.shared_incremental_target_maintenance().is_none());
1064        let Some(target_dir) = shared_incremental_target(spec) else {
1065            return Ok(None);
1066        };
1067        let canonical =
1068            canonicalize_allow_missing(&target_dir).map_err(|source| WasmBuildError::Io {
1069                operation: "resolve batch shared incremental target",
1070                path: target_dir,
1071                source,
1072            })?;
1073        if !self.configured_targets.insert(canonical) {
1074            return Ok(None);
1075        }
1076        Ok(Some(
1077            spec.clone()
1078                .with_shared_incremental_target_maintenance(config),
1079        ))
1080    }
1081}
1082
1083fn batch_maintenance_ownership_error() -> WasmBuildError {
1084    WasmBuildError::InvalidSpec {
1085        message:
1086            "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
1087                .to_owned(),
1088    }
1089}
1090
1091impl std::fmt::Display for WasmBuildBatchContractError {
1092    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1093        match self {
1094            Self::EmptyLabel { index } => {
1095                write!(formatter, "Wasm batch label at index {index} is empty")
1096            }
1097            Self::DuplicateLabel {
1098                label,
1099                first_index,
1100                duplicate_index,
1101            } => write!(
1102                formatter,
1103                "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
1104            ),
1105            Self::SourceLeaseInvalidated => formatter
1106                .write_str("Wasm build source lease was invalidated by a detected input mutation"),
1107            Self::SpecificationNotPrepared { index, label } => write!(
1108                formatter,
1109                "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
1110            ),
1111        }
1112    }
1113}
1114
1115impl std::error::Error for WasmBuildBatchContractError {}
1116
1117#[cfg(test)]
1118mod tests;