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, WasmBuildFailurePhase, WasmBuildFailureTimings,
15 WasmBuildInputReuse, WasmBuildInputSnapshotState, WasmBuildOutcome, WasmBuildProgressConfig,
16 WasmBuildProgressEvent, WasmBuildSessionState, WasmBuildSpec, WasmBuildTimings,
17 WasmInputResolutionTimings, build_wasm_canisters_cached_in_batch,
18 build_wasm_canisters_cached_in_batch_with_progress, shared_incremental_target,
19};
20
21#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub struct WasmBuildBatchConfig {
24 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct LabeledWasmBuildSpec {
33 label: String,
34 spec: WasmBuildSpec,
35}
36
37#[derive(Debug)]
39pub struct WasmBuildBatchReport {
40 entries: Vec<WasmBuildBatchEntry>,
41 input_resolution: WasmBuildBatchInputMetrics,
42 total: Duration,
43}
44
45pub struct WasmBuildSession<'guard> {
51 state: WasmBuildSessionState,
52 _source_guard: PhantomData<&'guard ()>,
53}
54
55pub struct WasmBuildInputSnapshot<'guard> {
62 state: WasmBuildInputSnapshotState,
63 _source_guard: PhantomData<&'guard ()>,
64}
65
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68pub struct WasmBuildSessionMetrics {
69 snapshots: usize,
70 snapshot_reuses: usize,
71 invalidated: bool,
72}
73
74#[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#[derive(Debug)]
87pub struct WasmBuildBatchEntry {
88 index: usize,
89 label: String,
90 result: Result<WasmBuildOutcome, WasmBuildError>,
91 failure: Option<WasmBuildFailureDetails>,
92 entry_elapsed: Duration,
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub struct WasmBuildFailureDetails {
98 phase: WasmBuildFailurePhase,
99 timings: WasmBuildFailureTimings,
100}
101
102#[derive(Clone, Copy, Debug)]
104pub struct WasmBuildBatchOutcomeEntry<'a> {
105 index: usize,
106 label: &'a str,
107 outcome: &'a WasmBuildOutcome,
108 entry_elapsed: Duration,
109}
110
111#[derive(Clone, Copy, Debug)]
113pub struct WasmBuildBatchFailure<'a> {
114 index: usize,
115 label: &'a str,
116 error: &'a WasmBuildError,
117 details: WasmBuildFailureDetails,
118 entry_elapsed: Duration,
119}
120
121#[derive(Clone, Copy, Debug)]
123pub struct WasmBuildBatchMaintenanceEntry<'a> {
124 index: usize,
125 label: &'a str,
126 outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
127}
128
129#[non_exhaustive]
131#[derive(Clone, Debug, Eq, PartialEq)]
132pub enum WasmBuildBatchContractError {
133 EmptyLabel {
135 index: usize,
137 },
138 DuplicateLabel {
140 label: String,
142 first_index: usize,
144 duplicate_index: usize,
146 },
147 SourceLeaseInvalidated,
149 SpecificationNotPrepared {
151 index: usize,
153 label: String,
155 },
156}
157
158impl LabeledWasmBuildSpec {
159 #[must_use]
161 pub fn new(label: impl Into<String>, spec: WasmBuildSpec) -> Self {
162 Self {
163 label: label.into(),
164 spec,
165 }
166 }
167
168 #[must_use]
170 pub fn label(&self) -> &str {
171 &self.label
172 }
173
174 #[must_use]
176 pub const fn spec(&self) -> &WasmBuildSpec {
177 &self.spec
178 }
179
180 #[must_use]
182 pub fn into_parts(self) -> (String, WasmBuildSpec) {
183 (self.label, self.spec)
184 }
185}
186
187impl<'guard> WasmBuildSession<'guard> {
188 #[must_use]
197 pub fn assume_sources_immutable<Guard: ?Sized>(_source_write_guard: &'guard Guard) -> Self {
198 Self {
199 state: WasmBuildSessionState::new(),
200 _source_guard: PhantomData,
201 }
202 }
203
204 pub fn build_batch(
206 &mut self,
207 specs: &[LabeledWasmBuildSpec],
208 config: WasmBuildBatchConfig,
209 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
210 run_wasm_batch(
211 specs,
212 config,
213 Some(WasmBuildInputReuse::Session(&mut self.state)),
214 None,
215 )
216 }
217
218 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 #[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 #[must_use]
251 pub const fn snapshots(self) -> usize {
252 self.snapshots
253 }
254
255 #[must_use]
257 pub const fn snapshot_reuses(self) -> usize {
258 self.snapshot_reuses
259 }
260
261 #[must_use]
263 pub const fn is_invalidated(self) -> bool {
264 self.invalidated
265 }
266}
267
268impl<'guard> WasmBuildInputSnapshot<'guard> {
269 pub fn prepare_assuming_sources_immutable<Guard: ?Sized>(
276 _source_write_guard: &'guard Guard,
277 specs: &[WasmBuildSpec],
278 ) -> Result<Self, WasmBuildError> {
279 Ok(Self {
280 state: WasmBuildInputSnapshotState::prepare(specs)?,
281 _source_guard: PhantomData,
282 })
283 }
284
285 pub fn build_batch(
290 &self,
291 specs: &[LabeledWasmBuildSpec],
292 config: WasmBuildBatchConfig,
293 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
294 run_wasm_batch(
295 specs,
296 config,
297 Some(WasmBuildInputReuse::Snapshot(&self.state)),
298 None,
299 )
300 }
301
302 pub fn build_batch_with_progress<F>(
306 &self,
307 specs: &[LabeledWasmBuildSpec],
308 batch_config: WasmBuildBatchConfig,
309 progress_config: WasmBuildProgressConfig,
310 mut observer: F,
311 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
312 where
313 F: FnMut(WasmBuildBatchProgressEvent),
314 {
315 run_wasm_batch(
316 specs,
317 batch_config,
318 Some(WasmBuildInputReuse::Snapshot(&self.state)),
319 Some((progress_config, &mut observer)),
320 )
321 }
322
323 #[must_use]
325 pub fn metrics(&self) -> WasmBuildInputSnapshotMetrics {
326 let preparation = self.state.preparation_metrics();
327 WasmBuildInputSnapshotMetrics {
328 specifications: self.state.specification_count(),
329 input_resolution_runs: preparation.runs,
330 input_resolution_reuses: preparation.reuses,
331 input_resolution_timings: self.state.preparation_timings(),
332 reader_reuses: self.state.reader_reuses(),
333 invalidated: self.state.is_invalidated(),
334 }
335 }
336}
337
338impl WasmBuildInputSnapshotMetrics {
339 #[must_use]
341 pub const fn specifications(self) -> usize {
342 self.specifications
343 }
344
345 #[must_use]
347 pub const fn input_resolution_runs(self) -> usize {
348 self.input_resolution_runs
349 }
350
351 #[must_use]
353 pub const fn input_resolution_reuses(self) -> usize {
354 self.input_resolution_reuses
355 }
356
357 #[must_use]
359 pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
360 self.input_resolution_timings
361 }
362
363 #[must_use]
365 pub const fn reader_reuses(self) -> usize {
366 self.reader_reuses
367 }
368
369 #[must_use]
371 pub const fn is_invalidated(self) -> bool {
372 self.invalidated
373 }
374}
375
376impl WasmBuildBatchEntry {
377 #[must_use]
379 pub const fn index(&self) -> usize {
380 self.index
381 }
382
383 #[must_use]
385 pub fn label(&self) -> &str {
386 &self.label
387 }
388
389 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
391 self.result.as_ref()
392 }
393
394 #[must_use]
396 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
397 self.result.as_ref().ok()
398 }
399
400 #[must_use]
402 pub fn error(&self) -> Option<&WasmBuildError> {
403 self.result.as_ref().err()
404 }
405
406 #[must_use]
408 pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
409 self.failure
410 }
411
412 #[must_use]
414 pub const fn entry_elapsed(&self) -> Duration {
415 self.entry_elapsed
416 }
417
418 #[must_use]
420 pub const fn is_success(&self) -> bool {
421 self.result.is_ok()
422 }
423
424 pub fn into_parts(
426 self,
427 ) -> (
428 usize,
429 String,
430 Result<WasmBuildOutcome, WasmBuildError>,
431 Option<WasmBuildFailureDetails>,
432 Duration,
433 ) {
434 (
435 self.index,
436 self.label,
437 self.result,
438 self.failure,
439 self.entry_elapsed,
440 )
441 }
442}
443
444impl WasmBuildFailureDetails {
445 #[must_use]
447 pub const fn phase(self) -> WasmBuildFailurePhase {
448 self.phase
449 }
450
451 #[must_use]
453 pub const fn timings(self) -> WasmBuildFailureTimings {
454 self.timings
455 }
456}
457
458impl<'a> WasmBuildBatchOutcomeEntry<'a> {
459 #[must_use]
461 pub const fn index(self) -> usize {
462 self.index
463 }
464
465 #[must_use]
467 pub const fn label(self) -> &'a str {
468 self.label
469 }
470
471 #[must_use]
473 pub const fn outcome(self) -> &'a WasmBuildOutcome {
474 self.outcome
475 }
476
477 #[must_use]
479 pub const fn entry_elapsed(self) -> Duration {
480 self.entry_elapsed
481 }
482}
483
484impl<'a> WasmBuildBatchFailure<'a> {
485 #[must_use]
487 pub const fn index(self) -> usize {
488 self.index
489 }
490
491 #[must_use]
493 pub const fn label(self) -> &'a str {
494 self.label
495 }
496
497 #[must_use]
499 pub const fn error(self) -> &'a WasmBuildError {
500 self.error
501 }
502
503 #[must_use]
505 pub const fn phase(self) -> WasmBuildFailurePhase {
506 self.details.phase
507 }
508
509 #[must_use]
511 pub const fn timings(self) -> WasmBuildFailureTimings {
512 self.details.timings
513 }
514
515 #[must_use]
517 pub const fn entry_elapsed(self) -> Duration {
518 self.entry_elapsed
519 }
520}
521
522impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
523 #[must_use]
525 pub const fn index(self) -> usize {
526 self.index
527 }
528
529 #[must_use]
531 pub const fn label(self) -> &'a str {
532 self.label
533 }
534
535 #[must_use]
537 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
538 self.outcome
539 }
540}
541
542#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
544pub struct WasmBuildBatchMetrics {
545 specifications: usize,
546 succeeded: usize,
547 failed: usize,
548 built: usize,
549 reused: usize,
550 input_resolution_runs: usize,
551 input_resolution_reuses: usize,
552 input_resolution_session_reuses: usize,
553 input_resolution_prepared_reuses: usize,
554 successful_timings: WasmBuildTimings,
555 total: Duration,
556}
557
558#[non_exhaustive]
560#[derive(Clone, Debug, Eq, PartialEq)]
561pub enum WasmBuildBatchProgressEvent {
562 BuildStarted {
564 index: usize,
566 label: String,
568 total: usize,
570 },
571 BuildProgress {
573 index: usize,
575 label: String,
577 event: WasmBuildProgressEvent,
579 },
580 BuildFinished {
582 index: usize,
584 label: String,
586 },
587 BuildFailed {
589 index: usize,
591 label: String,
593 },
594}
595
596impl WasmBuildBatchReport {
597 #[must_use]
599 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
600 &self.entries
601 }
602
603 #[must_use]
605 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
606 self.entries
607 }
608
609 pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
611 self.entries.iter().filter_map(|entry| {
612 entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
613 index: entry.index,
614 label: &entry.label,
615 outcome,
616 entry_elapsed: entry.entry_elapsed,
617 })
618 })
619 }
620
621 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
623 self.entries.iter().filter_map(|entry| {
624 entry.error().map(|error| WasmBuildBatchFailure {
625 index: entry.index,
626 label: &entry.label,
627 error,
628 details: entry
629 .failure
630 .expect("failed Wasm batch entry must retain failure details"),
631 entry_elapsed: entry.entry_elapsed,
632 })
633 })
634 }
635
636 pub fn shared_incremental_maintenance_outcomes(
641 &self,
642 ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
643 self.outcomes().filter_map(|entry| {
644 entry
645 .outcome
646 .record()
647 .shared_incremental_maintenance()
648 .map(|outcome| WasmBuildBatchMaintenanceEntry {
649 index: entry.index,
650 label: entry.label,
651 outcome,
652 })
653 })
654 }
655
656 #[must_use]
658 pub const fn total(&self) -> Duration {
659 self.total
660 }
661
662 #[must_use]
664 pub fn is_success(&self) -> bool {
665 self.entries.iter().all(WasmBuildBatchEntry::is_success)
666 }
667
668 #[must_use]
670 pub fn metrics(&self) -> WasmBuildBatchMetrics {
671 let mut metrics = WasmBuildBatchMetrics {
672 specifications: self.entries.len(),
673 input_resolution_runs: self.input_resolution.runs,
674 input_resolution_reuses: self.input_resolution.reuses,
675 input_resolution_session_reuses: self.input_resolution.session_reuses,
676 input_resolution_prepared_reuses: self.input_resolution.prepared_reuses,
677 total: self.total,
678 ..WasmBuildBatchMetrics::default()
679 };
680 for entry in &self.entries {
681 match &entry.result {
682 Ok(outcome) => {
683 metrics.succeeded += 1;
684 if outcome.is_reused() {
685 metrics.reused += 1;
686 } else {
687 metrics.built += 1;
688 }
689 metrics.successful_timings = metrics
690 .successful_timings
691 .saturating_add(outcome.record().timings());
692 }
693 Err(_) => metrics.failed += 1,
694 }
695 }
696 metrics
697 }
698}
699
700impl WasmBuildBatchMetrics {
701 #[must_use]
703 pub const fn specifications(self) -> usize {
704 self.specifications
705 }
706
707 #[must_use]
709 pub const fn succeeded(self) -> usize {
710 self.succeeded
711 }
712
713 #[must_use]
715 pub const fn failed(self) -> usize {
716 self.failed
717 }
718
719 #[must_use]
721 pub const fn built(self) -> usize {
722 self.built
723 }
724
725 #[must_use]
727 pub const fn reused(self) -> usize {
728 self.reused
729 }
730
731 #[must_use]
733 pub const fn input_resolution_runs(self) -> usize {
734 self.input_resolution_runs
735 }
736
737 #[must_use]
739 pub const fn input_resolution_reuses(self) -> usize {
740 self.input_resolution_reuses
741 }
742
743 #[must_use]
745 pub const fn input_resolution_session_reuses(self) -> usize {
746 self.input_resolution_session_reuses
747 }
748
749 #[must_use]
751 pub const fn input_resolution_prepared_reuses(self) -> usize {
752 self.input_resolution_prepared_reuses
753 }
754
755 #[must_use]
757 pub const fn successful_timings(self) -> WasmBuildTimings {
758 self.successful_timings
759 }
760
761 #[must_use]
763 pub const fn total(self) -> Duration {
764 self.total
765 }
766}
767
768impl WasmBuildBatchConfig {
769 #[must_use]
771 pub const fn new() -> Self {
772 Self {
773 shared_incremental_maintenance: None,
774 }
775 }
776
777 #[must_use]
779 pub const fn with_shared_incremental_target_maintenance(
780 mut self,
781 config: SharedIncrementalTargetMaintenanceConfig,
782 ) -> Self {
783 self.shared_incremental_maintenance = Some(config);
784 self
785 }
786
787 #[must_use]
789 pub const fn with_shared_incremental_target_maintenance_at_most_every(
790 self,
791 policy: SharedIncrementalTargetPrunePolicy,
792 minimum_interval: Duration,
793 ) -> Self {
794 self.with_shared_incremental_target_maintenance(
795 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
796 )
797 }
798
799 #[must_use]
801 pub const fn shared_incremental_target_maintenance(
802 self,
803 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
804 self.shared_incremental_maintenance
805 }
806}
807
808impl std::fmt::Display for WasmBuildBatchReport {
809 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
810 let metrics = self.metrics();
811 write!(
812 formatter,
813 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} input_resolution_prepared_reuses={} successful_timings=({}) total={:?}",
814 metrics.specifications(),
815 metrics.succeeded(),
816 metrics.failed(),
817 metrics.built(),
818 metrics.reused(),
819 metrics.input_resolution_runs(),
820 metrics.input_resolution_reuses(),
821 metrics.input_resolution_session_reuses(),
822 metrics.input_resolution_prepared_reuses(),
823 metrics.successful_timings(),
824 metrics.total(),
825 )
826 }
827}
828
829pub fn build_wasm_canisters_cached_batch(
836 specs: &[LabeledWasmBuildSpec],
837) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
838 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
839}
840
841pub fn build_wasm_canisters_cached_batch_with_config(
848 specs: &[LabeledWasmBuildSpec],
849 config: WasmBuildBatchConfig,
850) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
851 run_wasm_batch(specs, config, None, None)
852}
853
854pub fn build_wasm_canisters_cached_batch_with_progress<F>(
860 specs: &[LabeledWasmBuildSpec],
861 config: WasmBuildProgressConfig,
862 observer: F,
863) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
864where
865 F: FnMut(WasmBuildBatchProgressEvent),
866{
867 build_wasm_canisters_cached_batch_with_config_and_progress(
868 specs,
869 WasmBuildBatchConfig::new(),
870 config,
871 observer,
872 )
873}
874
875pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
877 specs: &[LabeledWasmBuildSpec],
878 batch_config: WasmBuildBatchConfig,
879 progress_config: WasmBuildProgressConfig,
880 mut observer: F,
881) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
882where
883 F: FnMut(WasmBuildBatchProgressEvent),
884{
885 run_wasm_batch(
886 specs,
887 batch_config,
888 None,
889 Some((progress_config, &mut observer)),
890 )
891}
892
893fn run_wasm_batch(
894 specs: &[LabeledWasmBuildSpec],
895 batch_config: WasmBuildBatchConfig,
896 reuse: Option<WasmBuildInputReuse<'_>>,
897 mut observation: Option<(
898 WasmBuildProgressConfig,
899 &mut dyn FnMut(WasmBuildBatchProgressEvent),
900 )>,
901) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
902 validate_batch_labels(specs)?;
903 validate_input_reuse(specs, reuse.as_ref())?;
904 let count = specs.len();
905 let build_specs = specs
906 .iter()
907 .map(|labeled| labeled.spec.clone())
908 .collect::<Vec<_>>();
909 let mut resolver = WasmBuildBatchInputResolver::new(&build_specs, reuse);
910 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
911 let Some((progress_config, observer)) = observation.as_mut() else {
912 return build_wasm_canisters_cached_in_batch(spec, index, &mut resolver);
913 };
914 let label = specs[index].label.clone();
915 observer(WasmBuildBatchProgressEvent::BuildStarted {
916 index,
917 label: label.clone(),
918 total: count,
919 });
920 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
921 spec,
922 index,
923 &mut resolver,
924 *progress_config,
925 |event| {
926 observer(WasmBuildBatchProgressEvent::BuildProgress {
927 index,
928 label: label.clone(),
929 event,
930 });
931 },
932 );
933 observer(match &attempt.result {
934 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
935 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
936 });
937 attempt
938 });
939 report.input_resolution = resolver.metrics();
940 Ok(report)
941}
942
943fn build_wasm_batch<F>(
944 specs: &[LabeledWasmBuildSpec],
945 config: WasmBuildBatchConfig,
946 mut build: F,
947) -> WasmBuildBatchReport
948where
949 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
950{
951 let started = Instant::now();
952 let mut entries = Vec::with_capacity(specs.len());
953 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
954 for (index, labeled) in specs.iter().enumerate() {
955 let entry_started = Instant::now();
956 let spec = &labeled.spec;
957 let attempt = if config.shared_incremental_maintenance.is_some()
958 && spec.shared_incremental_target_maintenance().is_some()
959 {
960 WasmBuildBatchAttempt::invalid_spec(
961 batch_maintenance_ownership_error(),
962 entry_started.elapsed(),
963 )
964 } else {
965 match maintenance.prepare_spec(spec) {
966 Ok(configured) => build(configured.as_ref().unwrap_or(spec), index),
967 Err(error) => WasmBuildBatchAttempt::invalid_spec(error, entry_started.elapsed()),
968 }
969 };
970 let failure = attempt
971 .failure
972 .map(|(phase, timings)| WasmBuildFailureDetails { phase, timings });
973 entries.push(WasmBuildBatchEntry {
974 index,
975 label: labeled.label.clone(),
976 result: attempt.result,
977 failure,
978 entry_elapsed: entry_started.elapsed(),
979 });
980 }
981 WasmBuildBatchReport {
982 entries,
983 input_resolution: WasmBuildBatchInputMetrics::default(),
984 total: started.elapsed(),
985 }
986}
987
988fn validate_batch_labels(
989 specs: &[LabeledWasmBuildSpec],
990) -> Result<(), WasmBuildBatchContractError> {
991 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
992 {
993 BatchLabelError::Empty { index } => WasmBuildBatchContractError::EmptyLabel { index },
994 BatchLabelError::Duplicate {
995 label,
996 first_index,
997 duplicate_index,
998 } => WasmBuildBatchContractError::DuplicateLabel {
999 label,
1000 first_index,
1001 duplicate_index,
1002 },
1003 })
1004}
1005
1006fn validate_input_reuse(
1007 specs: &[LabeledWasmBuildSpec],
1008 reuse: Option<&WasmBuildInputReuse<'_>>,
1009) -> Result<(), WasmBuildBatchContractError> {
1010 match reuse {
1011 Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
1012 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1013 }
1014 Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
1015 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
1016 }
1017 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
1018 for (index, labeled) in specs.iter().enumerate() {
1019 if !snapshot.contains(&labeled.spec) {
1020 return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
1021 index,
1022 label: labeled.label.clone(),
1023 });
1024 }
1025 }
1026 Ok(())
1027 }
1028 _ => Ok(()),
1029 }
1030}
1031
1032struct BatchMaintenanceTracker {
1033 config: Option<SharedIncrementalTargetMaintenanceConfig>,
1034 configured_targets: HashSet<PathBuf>,
1035}
1036
1037impl BatchMaintenanceTracker {
1038 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
1039 Self {
1040 config,
1041 configured_targets: HashSet::new(),
1042 }
1043 }
1044
1045 fn prepare_spec(
1046 &mut self,
1047 spec: &WasmBuildSpec,
1048 ) -> Result<Option<WasmBuildSpec>, WasmBuildError> {
1049 let Some(config) = self.config else {
1050 return Ok(None);
1051 };
1052 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
1053 let Some(target_dir) = shared_incremental_target(spec) else {
1054 return Ok(None);
1055 };
1056 let canonical =
1057 canonicalize_allow_missing(&target_dir).map_err(|source| WasmBuildError::Io {
1058 operation: "resolve batch shared incremental target",
1059 path: target_dir,
1060 source,
1061 })?;
1062 if !self.configured_targets.insert(canonical) {
1063 return Ok(None);
1064 }
1065 Ok(Some(
1066 spec.clone()
1067 .with_shared_incremental_target_maintenance(config),
1068 ))
1069 }
1070}
1071
1072fn batch_maintenance_ownership_error() -> WasmBuildError {
1073 WasmBuildError::InvalidSpec {
1074 message:
1075 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
1076 .to_owned(),
1077 }
1078}
1079
1080impl std::fmt::Display for WasmBuildBatchContractError {
1081 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082 match self {
1083 Self::EmptyLabel { index } => {
1084 write!(formatter, "Wasm batch label at index {index} is empty")
1085 }
1086 Self::DuplicateLabel {
1087 label,
1088 first_index,
1089 duplicate_index,
1090 } => write!(
1091 formatter,
1092 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
1093 ),
1094 Self::SourceLeaseInvalidated => formatter
1095 .write_str("Wasm build source lease was invalidated by a detected input mutation"),
1096 Self::SpecificationNotPrepared { index, label } => write!(
1097 formatter,
1098 "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
1099 ),
1100 }
1101 }
1102}
1103
1104impl std::error::Error for WasmBuildBatchContractError {}
1105
1106#[cfg(test)]
1107mod tests;