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#[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, WasmBuildFailureDetails)>,
91 entry_elapsed: Duration,
92}
93
94#[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#[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#[derive(Clone, Copy, Debug)]
115pub struct WasmBuildBatchMaintenanceEntry<'a> {
116 index: usize,
117 label: &'a str,
118 outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
119}
120
121#[non_exhaustive]
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub enum WasmBuildBatchContractError {
125 EmptyLabel {
127 index: usize,
129 },
130 DuplicateLabel {
132 label: String,
134 first_index: usize,
136 duplicate_index: usize,
138 },
139 SourceLeaseInvalidated,
141 SpecificationNotPrepared {
143 index: usize,
145 label: String,
147 },
148}
149
150impl LabeledWasmBuildSpec {
151 #[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 #[must_use]
162 pub fn label(&self) -> &str {
163 &self.label
164 }
165
166 #[must_use]
168 pub const fn spec(&self) -> &WasmBuildSpec {
169 &self.spec
170 }
171
172 #[must_use]
174 pub fn into_parts(self) -> (String, WasmBuildSpec) {
175 (self.label, self.spec)
176 }
177}
178
179impl<'guard> WasmBuildSession<'guard> {
180 #[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 pub fn build_batch(
198 &mut self,
199 specs: &[LabeledWasmBuildSpec],
200 config: WasmBuildBatchConfig,
201 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
202 run_wasm_batch(
203 specs,
204 config,
205 Some(WasmBuildInputReuse::Session(&mut self.state)),
206 None,
207 )
208 }
209
210 pub fn build_batch_with_progress<F>(
212 &mut self,
213 specs: &[LabeledWasmBuildSpec],
214 batch_config: WasmBuildBatchConfig,
215 progress_config: WasmBuildProgressConfig,
216 mut observer: F,
217 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
218 where
219 F: FnMut(WasmBuildBatchProgressEvent),
220 {
221 run_wasm_batch(
222 specs,
223 batch_config,
224 Some(WasmBuildInputReuse::Session(&mut self.state)),
225 Some((progress_config, &mut observer)),
226 )
227 }
228
229 #[must_use]
231 pub const fn metrics(&self) -> WasmBuildSessionMetrics {
232 WasmBuildSessionMetrics {
233 snapshots: self.state.snapshot_count(),
234 snapshot_reuses: self.state.snapshot_reuses(),
235 invalidated: self.state.is_invalidated(),
236 }
237 }
238}
239
240impl WasmBuildSessionMetrics {
241 #[must_use]
243 pub const fn snapshots(self) -> usize {
244 self.snapshots
245 }
246
247 #[must_use]
249 pub const fn snapshot_reuses(self) -> usize {
250 self.snapshot_reuses
251 }
252
253 #[must_use]
255 pub const fn is_invalidated(self) -> bool {
256 self.invalidated
257 }
258}
259
260impl<'guard> WasmBuildInputSnapshot<'guard> {
261 pub fn prepare_assuming_sources_immutable<Guard: ?Sized>(
268 _source_write_guard: &'guard Guard,
269 specs: &[WasmBuildSpec],
270 ) -> Result<Self, WasmBuildError> {
271 Ok(Self {
272 state: WasmBuildInputSnapshotState::prepare(specs)?,
273 _source_guard: PhantomData,
274 })
275 }
276
277 pub fn build_batch(
282 &self,
283 specs: &[LabeledWasmBuildSpec],
284 config: WasmBuildBatchConfig,
285 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
286 run_wasm_batch(
287 specs,
288 config,
289 Some(WasmBuildInputReuse::Snapshot(&self.state)),
290 None,
291 )
292 }
293
294 pub fn build_batch_with_progress<F>(
298 &self,
299 specs: &[LabeledWasmBuildSpec],
300 batch_config: WasmBuildBatchConfig,
301 progress_config: WasmBuildProgressConfig,
302 mut observer: F,
303 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
304 where
305 F: FnMut(WasmBuildBatchProgressEvent),
306 {
307 run_wasm_batch(
308 specs,
309 batch_config,
310 Some(WasmBuildInputReuse::Snapshot(&self.state)),
311 Some((progress_config, &mut observer)),
312 )
313 }
314
315 #[must_use]
317 pub fn metrics(&self) -> WasmBuildInputSnapshotMetrics {
318 let preparation = self.state.preparation_metrics();
319 WasmBuildInputSnapshotMetrics {
320 specifications: self.state.specification_count(),
321 input_resolution_runs: preparation.runs,
322 input_resolution_reuses: preparation.reuses,
323 input_resolution_timings: self.state.preparation_timings(),
324 reader_reuses: self.state.reader_reuses(),
325 invalidated: self.state.is_invalidated(),
326 }
327 }
328}
329
330impl WasmBuildInputSnapshotMetrics {
331 #[must_use]
333 pub const fn specifications(self) -> usize {
334 self.specifications
335 }
336
337 #[must_use]
339 pub const fn input_resolution_runs(self) -> usize {
340 self.input_resolution_runs
341 }
342
343 #[must_use]
345 pub const fn input_resolution_reuses(self) -> usize {
346 self.input_resolution_reuses
347 }
348
349 #[must_use]
351 pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
352 self.input_resolution_timings
353 }
354
355 #[must_use]
357 pub const fn reader_reuses(self) -> usize {
358 self.reader_reuses
359 }
360
361 #[must_use]
363 pub const fn is_invalidated(self) -> bool {
364 self.invalidated
365 }
366}
367
368impl WasmBuildBatchEntry {
369 #[must_use]
371 pub const fn index(&self) -> usize {
372 self.index
373 }
374
375 #[must_use]
377 pub fn label(&self) -> &str {
378 &self.label
379 }
380
381 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
383 match &self.result {
384 Ok(outcome) => Ok(outcome),
385 Err((error, _)) => Err(error),
386 }
387 }
388
389 #[must_use]
391 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
392 self.result.as_ref().ok()
393 }
394
395 #[must_use]
397 pub fn error(&self) -> Option<&WasmBuildError> {
398 self.result().err()
399 }
400
401 #[must_use]
403 pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
404 match &self.result {
405 Ok(_) => None,
406 Err((_, details)) => Some(*details),
407 }
408 }
409
410 #[must_use]
412 pub const fn entry_elapsed(&self) -> Duration {
413 self.entry_elapsed
414 }
415
416 #[must_use]
418 pub const fn is_success(&self) -> bool {
419 self.result.is_ok()
420 }
421
422 pub fn into_parts(
424 self,
425 ) -> (
426 usize,
427 String,
428 Result<WasmBuildOutcome, WasmBuildError>,
429 Option<WasmBuildFailureDetails>,
430 Duration,
431 ) {
432 let (result, failure) = match self.result {
433 Ok(outcome) => (Ok(outcome), None),
434 Err((error, details)) => (Err(error), Some(details)),
435 };
436 (self.index, self.label, result, failure, self.entry_elapsed)
437 }
438}
439
440impl<'a> WasmBuildBatchOutcomeEntry<'a> {
441 #[must_use]
443 pub const fn index(self) -> usize {
444 self.index
445 }
446
447 #[must_use]
449 pub const fn label(self) -> &'a str {
450 self.label
451 }
452
453 #[must_use]
455 pub const fn outcome(self) -> &'a WasmBuildOutcome {
456 self.outcome
457 }
458
459 #[must_use]
461 pub const fn entry_elapsed(self) -> Duration {
462 self.entry_elapsed
463 }
464}
465
466impl<'a> WasmBuildBatchFailure<'a> {
467 #[must_use]
469 pub const fn index(self) -> usize {
470 self.index
471 }
472
473 #[must_use]
475 pub const fn label(self) -> &'a str {
476 self.label
477 }
478
479 #[must_use]
481 pub const fn error(self) -> &'a WasmBuildError {
482 self.error
483 }
484
485 #[must_use]
487 pub const fn phase(self) -> WasmBuildFailurePhase {
488 self.details.phase()
489 }
490
491 #[must_use]
493 pub const fn timings(self) -> WasmBuildFailureTimings {
494 self.details.timings()
495 }
496
497 #[must_use]
499 pub const fn entry_elapsed(self) -> Duration {
500 self.entry_elapsed
501 }
502}
503
504impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
505 #[must_use]
507 pub const fn index(self) -> usize {
508 self.index
509 }
510
511 #[must_use]
513 pub const fn label(self) -> &'a str {
514 self.label
515 }
516
517 #[must_use]
519 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
520 self.outcome
521 }
522}
523
524#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
526pub struct WasmBuildBatchMetrics {
527 specifications: usize,
528 succeeded: usize,
529 failed: usize,
530 built: usize,
531 reused: usize,
532 input_resolution_runs: usize,
533 input_resolution_reuses: usize,
534 input_resolution_session_reuses: usize,
535 input_resolution_prepared_reuses: usize,
536 successful_timings: WasmBuildTimings,
537 total: Duration,
538}
539
540#[non_exhaustive]
542#[derive(Clone, Debug, Eq, PartialEq)]
543pub enum WasmBuildBatchProgressEvent {
544 BuildStarted {
546 index: usize,
548 label: String,
550 total: usize,
552 },
553 BuildProgress {
555 index: usize,
557 label: String,
559 event: WasmBuildProgressEvent,
561 },
562 BuildFinished {
564 index: usize,
566 label: String,
568 },
569 BuildFailed {
571 index: usize,
573 label: String,
575 },
576}
577
578impl WasmBuildBatchReport {
579 #[must_use]
581 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
582 &self.entries
583 }
584
585 #[must_use]
587 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
588 self.entries
589 }
590
591 pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
593 self.entries.iter().filter_map(|entry| {
594 entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
595 index: entry.index,
596 label: &entry.label,
597 outcome,
598 entry_elapsed: entry.entry_elapsed,
599 })
600 })
601 }
602
603 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
605 self.entries.iter().filter_map(|entry| {
606 entry
607 .result
608 .as_ref()
609 .err()
610 .map(|(error, details)| WasmBuildBatchFailure {
611 index: entry.index,
612 label: &entry.label,
613 error,
614 details: *details,
615 entry_elapsed: entry.entry_elapsed,
616 })
617 })
618 }
619
620 pub fn shared_incremental_maintenance_outcomes(
625 &self,
626 ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
627 self.outcomes().filter_map(|entry| {
628 entry
629 .outcome
630 .record()
631 .shared_incremental_maintenance()
632 .map(|outcome| WasmBuildBatchMaintenanceEntry {
633 index: entry.index,
634 label: entry.label,
635 outcome,
636 })
637 })
638 }
639
640 #[must_use]
642 pub const fn total(&self) -> Duration {
643 self.total
644 }
645
646 #[must_use]
648 pub fn is_success(&self) -> bool {
649 self.entries.iter().all(WasmBuildBatchEntry::is_success)
650 }
651
652 #[must_use]
654 pub fn metrics(&self) -> WasmBuildBatchMetrics {
655 let mut metrics = WasmBuildBatchMetrics {
656 specifications: self.entries.len(),
657 input_resolution_runs: self.input_resolution.runs,
658 input_resolution_reuses: self.input_resolution.reuses,
659 input_resolution_session_reuses: self.input_resolution.session_reuses,
660 input_resolution_prepared_reuses: self.input_resolution.prepared_reuses,
661 total: self.total,
662 ..WasmBuildBatchMetrics::default()
663 };
664 for entry in &self.entries {
665 match &entry.result {
666 Ok(outcome) => {
667 metrics.succeeded += 1;
668 if outcome.is_reused() {
669 metrics.reused += 1;
670 } else {
671 metrics.built += 1;
672 }
673 metrics.successful_timings = metrics
674 .successful_timings
675 .saturating_add(outcome.record().timings());
676 }
677 Err(_) => metrics.failed += 1,
678 }
679 }
680 metrics
681 }
682}
683
684impl WasmBuildBatchMetrics {
685 #[must_use]
687 pub const fn specifications(self) -> usize {
688 self.specifications
689 }
690
691 #[must_use]
693 pub const fn succeeded(self) -> usize {
694 self.succeeded
695 }
696
697 #[must_use]
699 pub const fn failed(self) -> usize {
700 self.failed
701 }
702
703 #[must_use]
705 pub const fn built(self) -> usize {
706 self.built
707 }
708
709 #[must_use]
711 pub const fn reused(self) -> usize {
712 self.reused
713 }
714
715 #[must_use]
717 pub const fn input_resolution_runs(self) -> usize {
718 self.input_resolution_runs
719 }
720
721 #[must_use]
723 pub const fn input_resolution_reuses(self) -> usize {
724 self.input_resolution_reuses
725 }
726
727 #[must_use]
729 pub const fn input_resolution_session_reuses(self) -> usize {
730 self.input_resolution_session_reuses
731 }
732
733 #[must_use]
735 pub const fn input_resolution_prepared_reuses(self) -> usize {
736 self.input_resolution_prepared_reuses
737 }
738
739 #[must_use]
741 pub const fn successful_timings(self) -> WasmBuildTimings {
742 self.successful_timings
743 }
744
745 #[must_use]
747 pub const fn total(self) -> Duration {
748 self.total
749 }
750}
751
752impl WasmBuildBatchConfig {
753 #[must_use]
755 pub const fn new() -> Self {
756 Self {
757 shared_incremental_maintenance: None,
758 }
759 }
760
761 #[must_use]
763 pub const fn with_shared_incremental_target_maintenance(
764 mut self,
765 config: SharedIncrementalTargetMaintenanceConfig,
766 ) -> Self {
767 self.shared_incremental_maintenance = Some(config);
768 self
769 }
770
771 #[must_use]
773 pub const fn with_shared_incremental_target_maintenance_at_most_every(
774 self,
775 policy: SharedIncrementalTargetPrunePolicy,
776 minimum_interval: Duration,
777 ) -> Self {
778 self.with_shared_incremental_target_maintenance(
779 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
780 )
781 }
782
783 #[must_use]
785 pub const fn shared_incremental_target_maintenance(
786 self,
787 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
788 self.shared_incremental_maintenance
789 }
790}
791
792impl std::fmt::Display for WasmBuildBatchReport {
793 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
794 let metrics = self.metrics();
795 write!(
796 formatter,
797 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} input_resolution_prepared_reuses={} successful_timings=({}) total={:?}",
798 metrics.specifications(),
799 metrics.succeeded(),
800 metrics.failed(),
801 metrics.built(),
802 metrics.reused(),
803 metrics.input_resolution_runs(),
804 metrics.input_resolution_reuses(),
805 metrics.input_resolution_session_reuses(),
806 metrics.input_resolution_prepared_reuses(),
807 metrics.successful_timings(),
808 metrics.total(),
809 )
810 }
811}
812
813pub fn build_wasm_canisters_cached_batch(
820 specs: &[LabeledWasmBuildSpec],
821) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
822 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
823}
824
825pub fn build_wasm_canisters_cached_batch_with_config(
832 specs: &[LabeledWasmBuildSpec],
833 config: WasmBuildBatchConfig,
834) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
835 run_wasm_batch(specs, config, None, None)
836}
837
838pub fn build_wasm_canisters_cached_batch_with_progress<F>(
844 specs: &[LabeledWasmBuildSpec],
845 config: WasmBuildProgressConfig,
846 observer: F,
847) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
848where
849 F: FnMut(WasmBuildBatchProgressEvent),
850{
851 build_wasm_canisters_cached_batch_with_config_and_progress(
852 specs,
853 WasmBuildBatchConfig::new(),
854 config,
855 observer,
856 )
857}
858
859pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
861 specs: &[LabeledWasmBuildSpec],
862 batch_config: WasmBuildBatchConfig,
863 progress_config: WasmBuildProgressConfig,
864 mut observer: F,
865) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
866where
867 F: FnMut(WasmBuildBatchProgressEvent),
868{
869 run_wasm_batch(
870 specs,
871 batch_config,
872 None,
873 Some((progress_config, &mut observer)),
874 )
875}
876
877fn run_wasm_batch(
878 specs: &[LabeledWasmBuildSpec],
879 batch_config: WasmBuildBatchConfig,
880 reuse: Option<WasmBuildInputReuse<'_>>,
881 mut observation: Option<(
882 WasmBuildProgressConfig,
883 &mut dyn FnMut(WasmBuildBatchProgressEvent),
884 )>,
885) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
886 validate_batch_labels(specs)?;
887 validate_input_reuse(specs, reuse.as_ref())?;
888 let count = specs.len();
889 let build_specs = specs
890 .iter()
891 .map(|labeled| labeled.spec.clone())
892 .collect::<Vec<_>>();
893 let mut resolver = WasmBuildBatchInputResolver::new(&build_specs, reuse);
894 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
895 let Some((progress_config, observer)) = observation.as_mut() else {
896 return build_wasm_canisters_cached_in_batch(spec, index, &mut resolver);
897 };
898 let label = specs[index].label.clone();
899 observer(WasmBuildBatchProgressEvent::BuildStarted {
900 index,
901 label: label.clone(),
902 total: count,
903 });
904 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
905 spec,
906 index,
907 &mut resolver,
908 *progress_config,
909 |event| {
910 observer(WasmBuildBatchProgressEvent::BuildProgress {
911 index,
912 label: label.clone(),
913 event,
914 });
915 },
916 );
917 observer(match &attempt.result {
918 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
919 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
920 });
921 attempt
922 });
923 report.input_resolution = resolver.metrics();
924 Ok(report)
925}
926
927fn build_wasm_batch<F>(
928 specs: &[LabeledWasmBuildSpec],
929 config: WasmBuildBatchConfig,
930 mut build: F,
931) -> WasmBuildBatchReport
932where
933 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
934{
935 let started = Instant::now();
936 let mut entries = Vec::with_capacity(specs.len());
937 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
938 for (index, labeled) in specs.iter().enumerate() {
939 let entry_started = Instant::now();
940 let spec = &labeled.spec;
941 let result = if config.shared_incremental_maintenance.is_some()
942 && spec.shared_incremental_target_maintenance().is_some()
943 {
944 Err((
945 batch_maintenance_ownership_error(),
946 WasmBuildFailureDetails::specification(entry_started.elapsed()),
947 ))
948 } else {
949 match maintenance.prepare_spec(spec) {
950 Ok(configured) => build(configured.as_ref().unwrap_or(spec), index).result,
951 Err(error) => Err((
952 error,
953 WasmBuildFailureDetails::specification(entry_started.elapsed()),
954 )),
955 }
956 };
957 entries.push(WasmBuildBatchEntry {
958 index,
959 label: labeled.label.clone(),
960 result,
961 entry_elapsed: entry_started.elapsed(),
962 });
963 }
964 WasmBuildBatchReport {
965 entries,
966 input_resolution: WasmBuildBatchInputMetrics::default(),
967 total: started.elapsed(),
968 }
969}
970
971fn validate_batch_labels(
972 specs: &[LabeledWasmBuildSpec],
973) -> Result<(), WasmBuildBatchContractError> {
974 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
975 {
976 BatchLabelError::Empty { index } => WasmBuildBatchContractError::EmptyLabel { index },
977 BatchLabelError::Duplicate {
978 label,
979 first_index,
980 duplicate_index,
981 } => WasmBuildBatchContractError::DuplicateLabel {
982 label,
983 first_index,
984 duplicate_index,
985 },
986 })
987}
988
989fn validate_input_reuse(
990 specs: &[LabeledWasmBuildSpec],
991 reuse: Option<&WasmBuildInputReuse<'_>>,
992) -> Result<(), WasmBuildBatchContractError> {
993 match reuse {
994 Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
995 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
996 }
997 Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
998 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
999 }
1000 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
1001 for (index, labeled) in specs.iter().enumerate() {
1002 if !snapshot.contains(&labeled.spec) {
1003 return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
1004 index,
1005 label: labeled.label.clone(),
1006 });
1007 }
1008 }
1009 Ok(())
1010 }
1011 _ => Ok(()),
1012 }
1013}
1014
1015struct BatchMaintenanceTracker {
1016 config: Option<SharedIncrementalTargetMaintenanceConfig>,
1017 configured_targets: HashSet<PathBuf>,
1018}
1019
1020impl BatchMaintenanceTracker {
1021 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
1022 Self {
1023 config,
1024 configured_targets: HashSet::new(),
1025 }
1026 }
1027
1028 fn prepare_spec(
1029 &mut self,
1030 spec: &WasmBuildSpec,
1031 ) -> Result<Option<WasmBuildSpec>, WasmBuildError> {
1032 let Some(config) = self.config else {
1033 return Ok(None);
1034 };
1035 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
1036 let Some(target_dir) = shared_incremental_target(spec) else {
1037 return Ok(None);
1038 };
1039 let canonical =
1040 canonicalize_allow_missing(&target_dir).map_err(|source| WasmBuildError::Io {
1041 operation: "resolve batch shared incremental target",
1042 path: target_dir,
1043 source,
1044 })?;
1045 if !self.configured_targets.insert(canonical) {
1046 return Ok(None);
1047 }
1048 Ok(Some(
1049 spec.clone()
1050 .with_shared_incremental_target_maintenance(config),
1051 ))
1052 }
1053}
1054
1055fn batch_maintenance_ownership_error() -> WasmBuildError {
1056 WasmBuildError::InvalidSpec {
1057 message:
1058 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
1059 .to_owned(),
1060 }
1061}
1062
1063impl std::fmt::Display for WasmBuildBatchContractError {
1064 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1065 match self {
1066 Self::EmptyLabel { index } => {
1067 write!(formatter, "Wasm batch label at index {index} is empty")
1068 }
1069 Self::DuplicateLabel {
1070 label,
1071 first_index,
1072 duplicate_index,
1073 } => write!(
1074 formatter,
1075 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
1076 ),
1077 Self::SourceLeaseInvalidated => formatter
1078 .write_str("Wasm build source lease was invalidated by a detected input mutation"),
1079 Self::SpecificationNotPrepared { index, label } => write!(
1080 formatter,
1081 "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
1082 ),
1083 }
1084 }
1085}
1086
1087impl std::error::Error for WasmBuildBatchContractError {}
1088
1089#[cfg(test)]
1090mod tests;