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 mut resolver =
890 WasmBuildBatchInputResolver::new(specs.iter().map(LabeledWasmBuildSpec::spec), reuse);
891 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
892 let Some((progress_config, observer)) = observation.as_mut() else {
893 return build_wasm_canisters_cached_in_batch(spec, index, &mut resolver);
894 };
895 let label = specs[index].label.clone();
896 observer(WasmBuildBatchProgressEvent::BuildStarted {
897 index,
898 label: label.clone(),
899 total: count,
900 });
901 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
902 spec,
903 index,
904 &mut resolver,
905 *progress_config,
906 |event| {
907 observer(WasmBuildBatchProgressEvent::BuildProgress {
908 index,
909 label: label.clone(),
910 event,
911 });
912 },
913 );
914 observer(match &attempt.result {
915 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
916 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
917 });
918 attempt
919 });
920 report.input_resolution = resolver.metrics();
921 Ok(report)
922}
923
924fn build_wasm_batch<F>(
925 specs: &[LabeledWasmBuildSpec],
926 config: WasmBuildBatchConfig,
927 mut build: F,
928) -> WasmBuildBatchReport
929where
930 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
931{
932 let started = Instant::now();
933 let mut entries = Vec::with_capacity(specs.len());
934 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
935 for (index, labeled) in specs.iter().enumerate() {
936 let entry_started = Instant::now();
937 let spec = &labeled.spec;
938 let result = if config.shared_incremental_maintenance.is_some()
939 && spec.shared_incremental_target_maintenance().is_some()
940 {
941 Err((
942 batch_maintenance_ownership_error(),
943 WasmBuildFailureDetails::specification(entry_started.elapsed()),
944 ))
945 } else {
946 match maintenance.prepare_spec(spec) {
947 Ok(configured) => build(configured.as_ref().unwrap_or(spec), index).result,
948 Err(error) => Err((
949 error,
950 WasmBuildFailureDetails::specification(entry_started.elapsed()),
951 )),
952 }
953 };
954 entries.push(WasmBuildBatchEntry {
955 index,
956 label: labeled.label.clone(),
957 result,
958 entry_elapsed: entry_started.elapsed(),
959 });
960 }
961 WasmBuildBatchReport {
962 entries,
963 input_resolution: WasmBuildBatchInputMetrics::default(),
964 total: started.elapsed(),
965 }
966}
967
968fn validate_batch_labels(
969 specs: &[LabeledWasmBuildSpec],
970) -> Result<(), WasmBuildBatchContractError> {
971 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
972 {
973 BatchLabelError::Empty { index } => WasmBuildBatchContractError::EmptyLabel { index },
974 BatchLabelError::Duplicate {
975 label,
976 first_index,
977 duplicate_index,
978 } => WasmBuildBatchContractError::DuplicateLabel {
979 label,
980 first_index,
981 duplicate_index,
982 },
983 })
984}
985
986fn validate_input_reuse(
987 specs: &[LabeledWasmBuildSpec],
988 reuse: Option<&WasmBuildInputReuse<'_>>,
989) -> Result<(), WasmBuildBatchContractError> {
990 match reuse {
991 Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
992 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
993 }
994 Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
995 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
996 }
997 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
998 for (index, labeled) in specs.iter().enumerate() {
999 if !snapshot.contains(&labeled.spec) {
1000 return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
1001 index,
1002 label: labeled.label.clone(),
1003 });
1004 }
1005 }
1006 Ok(())
1007 }
1008 _ => Ok(()),
1009 }
1010}
1011
1012struct BatchMaintenanceTracker {
1013 config: Option<SharedIncrementalTargetMaintenanceConfig>,
1014 configured_targets: HashSet<PathBuf>,
1015}
1016
1017impl BatchMaintenanceTracker {
1018 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
1019 Self {
1020 config,
1021 configured_targets: HashSet::new(),
1022 }
1023 }
1024
1025 fn prepare_spec(
1026 &mut self,
1027 spec: &WasmBuildSpec,
1028 ) -> Result<Option<WasmBuildSpec>, WasmBuildError> {
1029 let Some(config) = self.config else {
1030 return Ok(None);
1031 };
1032 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
1033 let Some(target_dir) = shared_incremental_target(spec) else {
1034 return Ok(None);
1035 };
1036 let canonical =
1037 canonicalize_allow_missing(&target_dir).map_err(|source| WasmBuildError::Io {
1038 operation: "resolve batch shared incremental target",
1039 path: target_dir,
1040 source,
1041 })?;
1042 if !self.configured_targets.insert(canonical) {
1043 return Ok(None);
1044 }
1045 Ok(Some(
1046 spec.clone()
1047 .with_shared_incremental_target_maintenance(config),
1048 ))
1049 }
1050}
1051
1052fn batch_maintenance_ownership_error() -> WasmBuildError {
1053 WasmBuildError::InvalidSpec {
1054 message:
1055 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
1056 .to_owned(),
1057 }
1058}
1059
1060impl std::fmt::Display for WasmBuildBatchContractError {
1061 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1062 match self {
1063 Self::EmptyLabel { index } => {
1064 write!(formatter, "Wasm batch label at index {index} is empty")
1065 }
1066 Self::DuplicateLabel {
1067 label,
1068 first_index,
1069 duplicate_index,
1070 } => write!(
1071 formatter,
1072 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
1073 ),
1074 Self::SourceLeaseInvalidated => formatter
1075 .write_str("Wasm build source lease was invalidated by a detected input mutation"),
1076 Self::SpecificationNotPrepared { index, label } => write!(
1077 formatter,
1078 "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
1079 ),
1080 }
1081 }
1082}
1083
1084impl std::error::Error for WasmBuildBatchContractError {}
1085
1086#[cfg(test)]
1087mod tests;