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(
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 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>(
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 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 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 #[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 #[must_use]
352 pub const fn specifications(self) -> usize {
353 self.specifications
354 }
355
356 #[must_use]
358 pub const fn input_resolution_runs(self) -> usize {
359 self.input_resolution_runs
360 }
361
362 #[must_use]
364 pub const fn input_resolution_reuses(self) -> usize {
365 self.input_resolution_reuses
366 }
367
368 #[must_use]
370 pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
371 self.input_resolution_timings
372 }
373
374 #[must_use]
376 pub const fn reader_reuses(self) -> usize {
377 self.reader_reuses
378 }
379
380 #[must_use]
382 pub const fn is_invalidated(self) -> bool {
383 self.invalidated
384 }
385}
386
387impl WasmBuildBatchEntry {
388 #[must_use]
390 pub const fn index(&self) -> usize {
391 self.index
392 }
393
394 #[must_use]
396 pub fn label(&self) -> &str {
397 &self.label
398 }
399
400 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 #[must_use]
413 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
414 self.result.as_ref().ok()
415 }
416
417 #[must_use]
419 pub fn error(&self) -> Option<&WasmBuildError> {
420 self.result().err()
421 }
422
423 #[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 #[must_use]
434 pub const fn entry_elapsed(&self) -> Duration {
435 self.entry_elapsed
436 }
437
438 #[must_use]
440 pub const fn is_success(&self) -> bool {
441 self.result.is_ok()
442 }
443
444 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 #[must_use]
465 pub const fn index(self) -> usize {
466 self.index
467 }
468
469 #[must_use]
471 pub const fn label(self) -> &'a str {
472 self.label
473 }
474
475 #[must_use]
477 pub const fn outcome(self) -> &'a WasmBuildOutcome {
478 self.outcome
479 }
480
481 #[must_use]
483 pub const fn entry_elapsed(self) -> Duration {
484 self.entry_elapsed
485 }
486}
487
488impl<'a> WasmBuildBatchFailure<'a> {
489 #[must_use]
491 pub const fn index(self) -> usize {
492 self.index
493 }
494
495 #[must_use]
497 pub const fn label(self) -> &'a str {
498 self.label
499 }
500
501 #[must_use]
503 pub const fn error(self) -> &'a WasmBuildError {
504 self.error
505 }
506
507 #[must_use]
509 pub const fn phase(self) -> WasmBuildFailurePhase {
510 self.details.phase()
511 }
512
513 #[must_use]
515 pub const fn timings(self) -> WasmBuildFailureTimings {
516 self.details.timings()
517 }
518
519 #[must_use]
521 pub const fn entry_elapsed(self) -> Duration {
522 self.entry_elapsed
523 }
524}
525
526impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
527 #[must_use]
529 pub const fn index(self) -> usize {
530 self.index
531 }
532
533 #[must_use]
535 pub const fn label(self) -> &'a str {
536 self.label
537 }
538
539 #[must_use]
541 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
542 self.outcome
543 }
544}
545
546#[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#[non_exhaustive]
562#[derive(Clone, Debug, Eq, PartialEq)]
563pub enum WasmBuildBatchProgressEvent {
564 BuildStarted {
566 index: usize,
568 label: String,
570 total: usize,
572 },
573 BuildProgress {
575 index: usize,
577 label: String,
579 event: WasmBuildProgressEvent,
581 },
582 BuildFinished {
584 index: usize,
586 label: String,
588 },
589 BuildFailed {
591 index: usize,
593 label: String,
595 },
596}
597
598impl WasmBuildBatchReport {
599 #[must_use]
601 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
602 &self.entries
603 }
604
605 #[must_use]
607 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
608 self.entries
609 }
610
611 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 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 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 #[must_use]
662 pub const fn total(&self) -> Duration {
663 self.total
664 }
665
666 #[must_use]
668 pub fn is_success(&self) -> bool {
669 self.entries.iter().all(WasmBuildBatchEntry::is_success)
670 }
671
672 #[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 #[must_use]
702 pub const fn specifications(self) -> usize {
703 self.specifications
704 }
705
706 #[must_use]
708 pub const fn succeeded(self) -> usize {
709 self.built + self.reused
710 }
711
712 #[must_use]
714 pub const fn failed(self) -> usize {
715 self.specifications - self.succeeded()
716 }
717
718 #[must_use]
720 pub const fn built(self) -> usize {
721 self.built
722 }
723
724 #[must_use]
726 pub const fn reused(self) -> usize {
727 self.reused
728 }
729
730 #[must_use]
732 pub const fn input_resolution_runs(self) -> usize {
733 self.input_resolution_runs
734 }
735
736 #[must_use]
738 pub const fn input_resolution_reuses(self) -> usize {
739 self.input_resolution_reuses
740 }
741
742 #[must_use]
744 pub const fn input_resolution_session_reuses(self) -> usize {
745 self.input_resolution_session_reuses
746 }
747
748 #[must_use]
750 pub const fn input_resolution_prepared_reuses(self) -> usize {
751 self.input_resolution_prepared_reuses
752 }
753
754 #[must_use]
756 pub const fn successful_timings(self) -> WasmBuildTimings {
757 self.successful_timings
758 }
759
760 #[must_use]
762 pub const fn total(self) -> Duration {
763 self.total
764 }
765}
766
767impl WasmBuildBatchConfig {
768 #[must_use]
770 pub const fn new() -> Self {
771 Self {
772 shared_incremental_maintenance: None,
773 }
774 }
775
776 #[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 #[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 #[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
828pub 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
844pub 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
861pub 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
886pub 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;