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 built: usize,
529 reused: usize,
530 input_resolution_runs: usize,
531 input_resolution_reuses: usize,
532 input_resolution_session_reuses: usize,
533 input_resolution_prepared_reuses: usize,
534 successful_timings: WasmBuildTimings,
535 total: Duration,
536}
537
538#[non_exhaustive]
540#[derive(Clone, Debug, Eq, PartialEq)]
541pub enum WasmBuildBatchProgressEvent {
542 BuildStarted {
544 index: usize,
546 label: String,
548 total: usize,
550 },
551 BuildProgress {
553 index: usize,
555 label: String,
557 event: WasmBuildProgressEvent,
559 },
560 BuildFinished {
562 index: usize,
564 label: String,
566 },
567 BuildFailed {
569 index: usize,
571 label: String,
573 },
574}
575
576impl WasmBuildBatchReport {
577 #[must_use]
579 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
580 &self.entries
581 }
582
583 #[must_use]
585 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
586 self.entries
587 }
588
589 pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
591 self.entries.iter().filter_map(|entry| {
592 entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
593 index: entry.index,
594 label: &entry.label,
595 outcome,
596 entry_elapsed: entry.entry_elapsed,
597 })
598 })
599 }
600
601 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
603 self.entries.iter().filter_map(|entry| {
604 entry
605 .result
606 .as_ref()
607 .err()
608 .map(|(error, details)| WasmBuildBatchFailure {
609 index: entry.index,
610 label: &entry.label,
611 error,
612 details: *details,
613 entry_elapsed: entry.entry_elapsed,
614 })
615 })
616 }
617
618 pub fn shared_incremental_maintenance_outcomes(
623 &self,
624 ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
625 self.outcomes().filter_map(|entry| {
626 entry
627 .outcome
628 .record()
629 .shared_incremental_maintenance()
630 .map(|outcome| WasmBuildBatchMaintenanceEntry {
631 index: entry.index,
632 label: entry.label,
633 outcome,
634 })
635 })
636 }
637
638 #[must_use]
640 pub const fn total(&self) -> Duration {
641 self.total
642 }
643
644 #[must_use]
646 pub fn is_success(&self) -> bool {
647 self.entries.iter().all(WasmBuildBatchEntry::is_success)
648 }
649
650 #[must_use]
652 pub fn metrics(&self) -> WasmBuildBatchMetrics {
653 let mut metrics = WasmBuildBatchMetrics {
654 specifications: self.entries.len(),
655 input_resolution_runs: self.input_resolution.runs,
656 input_resolution_reuses: self.input_resolution.reuses,
657 input_resolution_session_reuses: self.input_resolution.session_reuses,
658 input_resolution_prepared_reuses: self.input_resolution.prepared_reuses,
659 total: self.total,
660 ..WasmBuildBatchMetrics::default()
661 };
662 for entry in self.outcomes() {
663 let outcome = entry.outcome();
664 if outcome.is_reused() {
665 metrics.reused += 1;
666 } else {
667 metrics.built += 1;
668 }
669 metrics.successful_timings = metrics
670 .successful_timings
671 .saturating_add(outcome.record().timings());
672 }
673 metrics
674 }
675}
676
677impl WasmBuildBatchMetrics {
678 #[must_use]
680 pub const fn specifications(self) -> usize {
681 self.specifications
682 }
683
684 #[must_use]
686 pub const fn succeeded(self) -> usize {
687 self.built + self.reused
688 }
689
690 #[must_use]
692 pub const fn failed(self) -> usize {
693 self.specifications - self.succeeded()
694 }
695
696 #[must_use]
698 pub const fn built(self) -> usize {
699 self.built
700 }
701
702 #[must_use]
704 pub const fn reused(self) -> usize {
705 self.reused
706 }
707
708 #[must_use]
710 pub const fn input_resolution_runs(self) -> usize {
711 self.input_resolution_runs
712 }
713
714 #[must_use]
716 pub const fn input_resolution_reuses(self) -> usize {
717 self.input_resolution_reuses
718 }
719
720 #[must_use]
722 pub const fn input_resolution_session_reuses(self) -> usize {
723 self.input_resolution_session_reuses
724 }
725
726 #[must_use]
728 pub const fn input_resolution_prepared_reuses(self) -> usize {
729 self.input_resolution_prepared_reuses
730 }
731
732 #[must_use]
734 pub const fn successful_timings(self) -> WasmBuildTimings {
735 self.successful_timings
736 }
737
738 #[must_use]
740 pub const fn total(self) -> Duration {
741 self.total
742 }
743}
744
745impl WasmBuildBatchConfig {
746 #[must_use]
748 pub const fn new() -> Self {
749 Self {
750 shared_incremental_maintenance: None,
751 }
752 }
753
754 #[must_use]
756 pub const fn with_shared_incremental_target_maintenance(
757 mut self,
758 config: SharedIncrementalTargetMaintenanceConfig,
759 ) -> Self {
760 self.shared_incremental_maintenance = Some(config);
761 self
762 }
763
764 #[must_use]
766 pub const fn with_shared_incremental_target_maintenance_at_most_every(
767 self,
768 policy: SharedIncrementalTargetPrunePolicy,
769 minimum_interval: Duration,
770 ) -> Self {
771 self.with_shared_incremental_target_maintenance(
772 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
773 )
774 }
775
776 #[must_use]
778 pub const fn shared_incremental_target_maintenance(
779 self,
780 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
781 self.shared_incremental_maintenance
782 }
783}
784
785impl std::fmt::Display for WasmBuildBatchReport {
786 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787 let metrics = self.metrics();
788 write!(
789 formatter,
790 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} input_resolution_prepared_reuses={} successful_timings=({}) total={:?}",
791 metrics.specifications(),
792 metrics.succeeded(),
793 metrics.failed(),
794 metrics.built(),
795 metrics.reused(),
796 metrics.input_resolution_runs(),
797 metrics.input_resolution_reuses(),
798 metrics.input_resolution_session_reuses(),
799 metrics.input_resolution_prepared_reuses(),
800 metrics.successful_timings(),
801 metrics.total(),
802 )
803 }
804}
805
806pub fn build_wasm_canisters_cached_batch(
813 specs: &[LabeledWasmBuildSpec],
814) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
815 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
816}
817
818pub fn build_wasm_canisters_cached_batch_with_config(
825 specs: &[LabeledWasmBuildSpec],
826 config: WasmBuildBatchConfig,
827) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
828 run_wasm_batch(specs, config, None, None)
829}
830
831pub fn build_wasm_canisters_cached_batch_with_progress<F>(
837 specs: &[LabeledWasmBuildSpec],
838 config: WasmBuildProgressConfig,
839 observer: F,
840) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
841where
842 F: FnMut(WasmBuildBatchProgressEvent),
843{
844 build_wasm_canisters_cached_batch_with_config_and_progress(
845 specs,
846 WasmBuildBatchConfig::new(),
847 config,
848 observer,
849 )
850}
851
852pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
854 specs: &[LabeledWasmBuildSpec],
855 batch_config: WasmBuildBatchConfig,
856 progress_config: WasmBuildProgressConfig,
857 mut observer: F,
858) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
859where
860 F: FnMut(WasmBuildBatchProgressEvent),
861{
862 run_wasm_batch(
863 specs,
864 batch_config,
865 None,
866 Some((progress_config, &mut observer)),
867 )
868}
869
870fn run_wasm_batch(
871 specs: &[LabeledWasmBuildSpec],
872 batch_config: WasmBuildBatchConfig,
873 reuse: Option<WasmBuildInputReuse<'_>>,
874 mut observation: Option<(
875 WasmBuildProgressConfig,
876 &mut dyn FnMut(WasmBuildBatchProgressEvent),
877 )>,
878) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
879 validate_batch_labels(specs)?;
880 validate_input_reuse(specs, reuse.as_ref())?;
881 let count = specs.len();
882 let mut resolver =
883 WasmBuildBatchInputResolver::new(specs.iter().map(LabeledWasmBuildSpec::spec), reuse);
884 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
885 let Some((progress_config, observer)) = observation.as_mut() else {
886 return build_wasm_canisters_cached_in_batch(spec, index, &mut resolver);
887 };
888 let label = specs[index].label.clone();
889 observer(WasmBuildBatchProgressEvent::BuildStarted {
890 index,
891 label: label.clone(),
892 total: count,
893 });
894 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
895 spec,
896 index,
897 &mut resolver,
898 *progress_config,
899 |event| {
900 observer(WasmBuildBatchProgressEvent::BuildProgress {
901 index,
902 label: label.clone(),
903 event,
904 });
905 },
906 );
907 observer(match &attempt.result {
908 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
909 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
910 });
911 attempt
912 });
913 report.input_resolution = resolver.metrics();
914 Ok(report)
915}
916
917fn build_wasm_batch<F>(
918 specs: &[LabeledWasmBuildSpec],
919 config: WasmBuildBatchConfig,
920 mut build: F,
921) -> WasmBuildBatchReport
922where
923 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
924{
925 let started = Instant::now();
926 let mut entries = Vec::with_capacity(specs.len());
927 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
928 for (index, labeled) in specs.iter().enumerate() {
929 let entry_started = Instant::now();
930 let spec = &labeled.spec;
931 let result = if config.shared_incremental_maintenance.is_some()
932 && spec.shared_incremental_target_maintenance().is_some()
933 {
934 Err((
935 batch_maintenance_ownership_error(),
936 WasmBuildFailureDetails::specification(entry_started.elapsed()),
937 ))
938 } else {
939 match maintenance.prepare_spec(spec) {
940 Ok(configured) => build(configured.as_ref().unwrap_or(spec), index).result,
941 Err(error) => Err((
942 error,
943 WasmBuildFailureDetails::specification(entry_started.elapsed()),
944 )),
945 }
946 };
947 entries.push(WasmBuildBatchEntry {
948 index,
949 label: labeled.label.clone(),
950 result,
951 entry_elapsed: entry_started.elapsed(),
952 });
953 }
954 WasmBuildBatchReport {
955 entries,
956 input_resolution: WasmBuildBatchInputMetrics::default(),
957 total: started.elapsed(),
958 }
959}
960
961fn validate_batch_labels(
962 specs: &[LabeledWasmBuildSpec],
963) -> Result<(), WasmBuildBatchContractError> {
964 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
965 {
966 BatchLabelError::Empty { index } => WasmBuildBatchContractError::EmptyLabel { index },
967 BatchLabelError::Duplicate {
968 label,
969 first_index,
970 duplicate_index,
971 } => WasmBuildBatchContractError::DuplicateLabel {
972 label,
973 first_index,
974 duplicate_index,
975 },
976 })
977}
978
979fn validate_input_reuse(
980 specs: &[LabeledWasmBuildSpec],
981 reuse: Option<&WasmBuildInputReuse<'_>>,
982) -> Result<(), WasmBuildBatchContractError> {
983 match reuse {
984 Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
985 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
986 }
987 Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
988 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
989 }
990 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
991 for (index, labeled) in specs.iter().enumerate() {
992 if !snapshot.contains(&labeled.spec) {
993 return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
994 index,
995 label: labeled.label.clone(),
996 });
997 }
998 }
999 Ok(())
1000 }
1001 _ => Ok(()),
1002 }
1003}
1004
1005struct BatchMaintenanceTracker {
1006 config: Option<SharedIncrementalTargetMaintenanceConfig>,
1007 configured_targets: HashSet<PathBuf>,
1008}
1009
1010impl BatchMaintenanceTracker {
1011 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
1012 Self {
1013 config,
1014 configured_targets: HashSet::new(),
1015 }
1016 }
1017
1018 fn prepare_spec(
1019 &mut self,
1020 spec: &WasmBuildSpec,
1021 ) -> Result<Option<WasmBuildSpec>, WasmBuildError> {
1022 let Some(config) = self.config else {
1023 return Ok(None);
1024 };
1025 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
1026 let Some(target_dir) = shared_incremental_target(spec) else {
1027 return Ok(None);
1028 };
1029 let canonical =
1030 canonicalize_allow_missing(&target_dir).map_err(|source| WasmBuildError::Io {
1031 operation: "resolve batch shared incremental target",
1032 path: target_dir,
1033 source,
1034 })?;
1035 if !self.configured_targets.insert(canonical) {
1036 return Ok(None);
1037 }
1038 Ok(Some(
1039 spec.clone()
1040 .with_shared_incremental_target_maintenance(config),
1041 ))
1042 }
1043}
1044
1045fn batch_maintenance_ownership_error() -> WasmBuildError {
1046 WasmBuildError::InvalidSpec {
1047 message:
1048 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
1049 .to_owned(),
1050 }
1051}
1052
1053impl std::fmt::Display for WasmBuildBatchContractError {
1054 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1055 match self {
1056 Self::EmptyLabel { index } => {
1057 write!(formatter, "Wasm batch label at index {index} is empty")
1058 }
1059 Self::DuplicateLabel {
1060 label,
1061 first_index,
1062 duplicate_index,
1063 } => write!(
1064 formatter,
1065 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
1066 ),
1067 Self::SourceLeaseInvalidated => formatter
1068 .write_str("Wasm build source lease was invalidated by a detected input mutation"),
1069 Self::SpecificationNotPrepared { index, label } => write!(
1070 formatter,
1071 "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
1072 ),
1073 }
1074 }
1075}
1076
1077impl std::error::Error for WasmBuildBatchContractError {}
1078
1079#[cfg(test)]
1080mod tests;