1use std::{
2 collections::{HashMap, HashSet},
3 marker::PhantomData,
4 path::PathBuf,
5 time::{Duration, Instant},
6};
7
8use super::wasm_cache::{
9 SharedIncrementalTargetMaintenanceConfig, SharedIncrementalTargetMaintenanceOutcome,
10 SharedIncrementalTargetPrunePolicy, WasmBuildBatchAttempt, WasmBuildBatchInputMetrics,
11 WasmBuildBatchInputResolver, WasmBuildCacheMode, WasmBuildError, WasmBuildFailurePhase,
12 WasmBuildFailureTimings, WasmBuildOutcome, WasmBuildProgressConfig, WasmBuildProgressEvent,
13 WasmBuildSessionState, WasmBuildSpec, WasmBuildTimings, build_wasm_canisters_cached_in_batch,
14 build_wasm_canisters_cached_in_batch_with_progress,
15};
16
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19pub struct WasmBuildBatchConfig {
20 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceConfig>,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct LabeledWasmBuildSpec {
29 label: String,
30 spec: WasmBuildSpec,
31}
32
33#[derive(Debug)]
35pub struct WasmBuildBatchReport {
36 entries: Vec<WasmBuildBatchEntry>,
37 input_resolution: WasmBuildBatchInputMetrics,
38 total: Duration,
39}
40
41pub struct WasmBuildSession<'guard> {
47 state: WasmBuildSessionState,
48 _source_guard: PhantomData<&'guard ()>,
49}
50
51#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
53pub struct WasmBuildSessionMetrics {
54 snapshots: usize,
55 snapshot_reuses: usize,
56 invalidated: bool,
57}
58
59#[derive(Debug)]
61pub struct WasmBuildBatchEntry {
62 index: usize,
63 label: String,
64 result: Result<WasmBuildOutcome, WasmBuildError>,
65 failure: Option<WasmBuildFailureDetails>,
66 entry_elapsed: Duration,
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct WasmBuildFailureDetails {
72 phase: WasmBuildFailurePhase,
73 timings: WasmBuildFailureTimings,
74}
75
76#[derive(Clone, Copy, Debug)]
78pub struct WasmBuildBatchOutcomeEntry<'a> {
79 index: usize,
80 label: &'a str,
81 outcome: &'a WasmBuildOutcome,
82 entry_elapsed: Duration,
83}
84
85#[derive(Clone, Copy, Debug)]
87pub struct WasmBuildBatchFailure<'a> {
88 index: usize,
89 label: &'a str,
90 error: &'a WasmBuildError,
91 details: WasmBuildFailureDetails,
92 entry_elapsed: Duration,
93}
94
95#[derive(Clone, Copy, Debug)]
97pub struct WasmBuildBatchMaintenanceEntry<'a> {
98 index: usize,
99 label: &'a str,
100 outcome: &'a SharedIncrementalTargetMaintenanceOutcome,
101}
102
103#[non_exhaustive]
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub enum WasmBuildBatchContractError {
107 EmptyLabel {
109 index: usize,
111 },
112 DuplicateLabel {
114 label: String,
116 first_index: usize,
118 duplicate_index: usize,
120 },
121 SourceLeaseInvalidated,
123}
124
125impl LabeledWasmBuildSpec {
126 #[must_use]
128 pub fn new(label: impl Into<String>, spec: WasmBuildSpec) -> Self {
129 Self {
130 label: label.into(),
131 spec,
132 }
133 }
134
135 #[must_use]
137 pub fn label(&self) -> &str {
138 &self.label
139 }
140
141 #[must_use]
143 pub const fn spec(&self) -> &WasmBuildSpec {
144 &self.spec
145 }
146
147 #[must_use]
149 pub fn into_parts(self) -> (String, WasmBuildSpec) {
150 (self.label, self.spec)
151 }
152}
153
154impl<'guard> WasmBuildSession<'guard> {
155 #[must_use]
164 pub fn assume_sources_immutable<Guard: ?Sized>(_source_write_guard: &'guard Guard) -> Self {
165 Self {
166 state: WasmBuildSessionState::new(),
167 _source_guard: PhantomData,
168 }
169 }
170
171 pub fn build_batch(
173 &mut self,
174 specs: &[LabeledWasmBuildSpec],
175 config: WasmBuildBatchConfig,
176 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
177 build_wasm_canisters_cached_batch_with_session(specs, config, &mut self.state)
178 }
179
180 pub fn build_batch_with_progress<F>(
182 &mut self,
183 specs: &[LabeledWasmBuildSpec],
184 batch_config: WasmBuildBatchConfig,
185 progress_config: WasmBuildProgressConfig,
186 observer: F,
187 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
188 where
189 F: FnMut(WasmBuildBatchProgressEvent),
190 {
191 build_wasm_canisters_cached_batch_with_session_and_progress(
192 specs,
193 batch_config,
194 progress_config,
195 &mut self.state,
196 observer,
197 )
198 }
199
200 #[must_use]
202 pub const fn metrics(&self) -> WasmBuildSessionMetrics {
203 WasmBuildSessionMetrics {
204 snapshots: self.state.snapshot_count(),
205 snapshot_reuses: self.state.snapshot_reuses(),
206 invalidated: self.state.is_invalidated(),
207 }
208 }
209}
210
211impl WasmBuildSessionMetrics {
212 #[must_use]
214 pub const fn snapshots(self) -> usize {
215 self.snapshots
216 }
217
218 #[must_use]
220 pub const fn snapshot_reuses(self) -> usize {
221 self.snapshot_reuses
222 }
223
224 #[must_use]
226 pub const fn is_invalidated(self) -> bool {
227 self.invalidated
228 }
229}
230
231impl WasmBuildBatchEntry {
232 #[must_use]
234 pub const fn index(&self) -> usize {
235 self.index
236 }
237
238 #[must_use]
240 pub fn label(&self) -> &str {
241 &self.label
242 }
243
244 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
246 self.result.as_ref()
247 }
248
249 #[must_use]
251 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
252 self.result.as_ref().ok()
253 }
254
255 #[must_use]
257 pub fn error(&self) -> Option<&WasmBuildError> {
258 self.result.as_ref().err()
259 }
260
261 #[must_use]
263 pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
264 self.failure
265 }
266
267 #[must_use]
269 pub const fn entry_elapsed(&self) -> Duration {
270 self.entry_elapsed
271 }
272
273 #[must_use]
275 pub const fn is_success(&self) -> bool {
276 self.result.is_ok()
277 }
278
279 pub fn into_parts(
281 self,
282 ) -> (
283 usize,
284 String,
285 Result<WasmBuildOutcome, WasmBuildError>,
286 Option<WasmBuildFailureDetails>,
287 Duration,
288 ) {
289 (
290 self.index,
291 self.label,
292 self.result,
293 self.failure,
294 self.entry_elapsed,
295 )
296 }
297}
298
299impl WasmBuildFailureDetails {
300 #[must_use]
302 pub const fn phase(self) -> WasmBuildFailurePhase {
303 self.phase
304 }
305
306 #[must_use]
308 pub const fn timings(self) -> WasmBuildFailureTimings {
309 self.timings
310 }
311}
312
313impl<'a> WasmBuildBatchOutcomeEntry<'a> {
314 #[must_use]
316 pub const fn index(self) -> usize {
317 self.index
318 }
319
320 #[must_use]
322 pub const fn label(self) -> &'a str {
323 self.label
324 }
325
326 #[must_use]
328 pub const fn outcome(self) -> &'a WasmBuildOutcome {
329 self.outcome
330 }
331
332 #[must_use]
334 pub const fn entry_elapsed(self) -> Duration {
335 self.entry_elapsed
336 }
337}
338
339impl<'a> WasmBuildBatchFailure<'a> {
340 #[must_use]
342 pub const fn index(self) -> usize {
343 self.index
344 }
345
346 #[must_use]
348 pub const fn label(self) -> &'a str {
349 self.label
350 }
351
352 #[must_use]
354 pub const fn error(self) -> &'a WasmBuildError {
355 self.error
356 }
357
358 #[must_use]
360 pub const fn phase(self) -> WasmBuildFailurePhase {
361 self.details.phase
362 }
363
364 #[must_use]
366 pub const fn timings(self) -> WasmBuildFailureTimings {
367 self.details.timings
368 }
369
370 #[must_use]
372 pub const fn entry_elapsed(self) -> Duration {
373 self.entry_elapsed
374 }
375}
376
377impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
378 #[must_use]
380 pub const fn index(self) -> usize {
381 self.index
382 }
383
384 #[must_use]
386 pub const fn label(self) -> &'a str {
387 self.label
388 }
389
390 #[must_use]
392 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
393 self.outcome
394 }
395}
396
397#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
399pub struct WasmBuildBatchMetrics {
400 specifications: usize,
401 succeeded: usize,
402 failed: usize,
403 built: usize,
404 reused: usize,
405 input_resolution_runs: usize,
406 input_resolution_reuses: usize,
407 input_resolution_session_reuses: usize,
408 successful_timings: WasmBuildTimings,
409 total: Duration,
410}
411
412#[non_exhaustive]
414#[derive(Clone, Debug, Eq, PartialEq)]
415pub enum WasmBuildBatchProgressEvent {
416 BuildStarted {
418 index: usize,
420 label: String,
422 total: usize,
424 },
425 BuildProgress {
427 index: usize,
429 label: String,
431 event: WasmBuildProgressEvent,
433 },
434 BuildFinished {
436 index: usize,
438 label: String,
440 },
441 BuildFailed {
443 index: usize,
445 label: String,
447 },
448}
449
450impl WasmBuildBatchReport {
451 #[must_use]
453 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
454 &self.entries
455 }
456
457 #[must_use]
459 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
460 self.entries
461 }
462
463 pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
465 self.entries.iter().filter_map(|entry| {
466 entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
467 index: entry.index,
468 label: &entry.label,
469 outcome,
470 entry_elapsed: entry.entry_elapsed,
471 })
472 })
473 }
474
475 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
477 self.entries.iter().filter_map(|entry| {
478 entry.error().map(|error| WasmBuildBatchFailure {
479 index: entry.index,
480 label: &entry.label,
481 error,
482 details: entry
483 .failure
484 .expect("failed Wasm batch entry must retain failure details"),
485 entry_elapsed: entry.entry_elapsed,
486 })
487 })
488 }
489
490 pub fn shared_incremental_maintenance_outcomes(
495 &self,
496 ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
497 self.outcomes().filter_map(|entry| {
498 entry
499 .outcome
500 .record()
501 .shared_incremental_maintenance()
502 .map(|outcome| WasmBuildBatchMaintenanceEntry {
503 index: entry.index,
504 label: entry.label,
505 outcome,
506 })
507 })
508 }
509
510 #[must_use]
512 pub const fn total(&self) -> Duration {
513 self.total
514 }
515
516 #[must_use]
518 pub fn is_success(&self) -> bool {
519 self.entries.iter().all(WasmBuildBatchEntry::is_success)
520 }
521
522 #[must_use]
524 pub fn metrics(&self) -> WasmBuildBatchMetrics {
525 let mut metrics = WasmBuildBatchMetrics {
526 specifications: self.entries.len(),
527 input_resolution_runs: self.input_resolution.runs,
528 input_resolution_reuses: self.input_resolution.reuses,
529 input_resolution_session_reuses: self.input_resolution.session_reuses,
530 total: self.total,
531 ..WasmBuildBatchMetrics::default()
532 };
533 for entry in &self.entries {
534 match &entry.result {
535 Ok(outcome) => {
536 metrics.succeeded += 1;
537 if outcome.is_reused() {
538 metrics.reused += 1;
539 } else {
540 metrics.built += 1;
541 }
542 metrics.successful_timings = metrics
543 .successful_timings
544 .saturating_add(outcome.record().timings());
545 }
546 Err(_) => metrics.failed += 1,
547 }
548 }
549 metrics
550 }
551}
552
553impl WasmBuildBatchMetrics {
554 #[must_use]
556 pub const fn specifications(self) -> usize {
557 self.specifications
558 }
559
560 #[must_use]
562 pub const fn succeeded(self) -> usize {
563 self.succeeded
564 }
565
566 #[must_use]
568 pub const fn failed(self) -> usize {
569 self.failed
570 }
571
572 #[must_use]
574 pub const fn built(self) -> usize {
575 self.built
576 }
577
578 #[must_use]
580 pub const fn reused(self) -> usize {
581 self.reused
582 }
583
584 #[must_use]
586 pub const fn input_resolution_runs(self) -> usize {
587 self.input_resolution_runs
588 }
589
590 #[must_use]
592 pub const fn input_resolution_reuses(self) -> usize {
593 self.input_resolution_reuses
594 }
595
596 #[must_use]
598 pub const fn input_resolution_session_reuses(self) -> usize {
599 self.input_resolution_session_reuses
600 }
601
602 #[must_use]
604 pub const fn successful_timings(self) -> WasmBuildTimings {
605 self.successful_timings
606 }
607
608 #[must_use]
610 pub const fn total(self) -> Duration {
611 self.total
612 }
613}
614
615impl WasmBuildBatchConfig {
616 #[must_use]
618 pub const fn new() -> Self {
619 Self {
620 shared_incremental_maintenance: None,
621 }
622 }
623
624 #[must_use]
626 pub const fn with_shared_incremental_target_maintenance(
627 mut self,
628 config: SharedIncrementalTargetMaintenanceConfig,
629 ) -> Self {
630 self.shared_incremental_maintenance = Some(config);
631 self
632 }
633
634 #[must_use]
636 pub const fn with_shared_incremental_target_maintenance_at_most_every(
637 self,
638 policy: SharedIncrementalTargetPrunePolicy,
639 minimum_interval: Duration,
640 ) -> Self {
641 self.with_shared_incremental_target_maintenance(
642 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
643 )
644 }
645
646 #[must_use]
648 pub const fn shared_incremental_target_maintenance(
649 self,
650 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
651 self.shared_incremental_maintenance
652 }
653}
654
655impl std::fmt::Display for WasmBuildBatchReport {
656 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657 let metrics = self.metrics();
658 write!(
659 formatter,
660 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} successful_timings=({}) total={:?}",
661 metrics.specifications(),
662 metrics.succeeded(),
663 metrics.failed(),
664 metrics.built(),
665 metrics.reused(),
666 metrics.input_resolution_runs(),
667 metrics.input_resolution_reuses(),
668 metrics.input_resolution_session_reuses(),
669 metrics.successful_timings(),
670 metrics.total(),
671 )
672 }
673}
674
675pub fn build_wasm_canisters_cached_batch(
682 specs: &[LabeledWasmBuildSpec],
683) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
684 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
685}
686
687pub fn build_wasm_canisters_cached_batch_with_config(
694 specs: &[LabeledWasmBuildSpec],
695 config: WasmBuildBatchConfig,
696) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
697 build_wasm_canisters_cached_batch_internal(specs, config, None)
698}
699
700fn build_wasm_canisters_cached_batch_with_session(
701 specs: &[LabeledWasmBuildSpec],
702 config: WasmBuildBatchConfig,
703 session: &mut WasmBuildSessionState,
704) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
705 build_wasm_canisters_cached_batch_internal(specs, config, Some(session))
706}
707
708fn build_wasm_canisters_cached_batch_internal(
709 specs: &[LabeledWasmBuildSpec],
710 config: WasmBuildBatchConfig,
711 session: Option<&mut WasmBuildSessionState>,
712) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
713 validate_batch_labels(specs)?;
714 if session
715 .as_deref()
716 .is_some_and(WasmBuildSessionState::is_invalidated)
717 {
718 return Err(WasmBuildBatchContractError::SourceLeaseInvalidated);
719 }
720 let build_specs = specs
721 .iter()
722 .map(|labeled| labeled.spec.clone())
723 .collect::<Vec<_>>();
724 let mut resolver = session.map_or_else(
725 || WasmBuildBatchInputResolver::new(&build_specs),
726 |session| WasmBuildBatchInputResolver::with_session(&build_specs, session),
727 );
728 let mut report = build_wasm_batch(specs, config, |spec, index| {
729 build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
730 });
731 report.input_resolution = resolver.metrics();
732 Ok(report)
733}
734
735pub fn build_wasm_canisters_cached_batch_with_progress<F>(
741 specs: &[LabeledWasmBuildSpec],
742 config: WasmBuildProgressConfig,
743 observer: F,
744) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
745where
746 F: FnMut(WasmBuildBatchProgressEvent),
747{
748 build_wasm_canisters_cached_batch_with_config_and_progress(
749 specs,
750 WasmBuildBatchConfig::new(),
751 config,
752 observer,
753 )
754}
755
756pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
758 specs: &[LabeledWasmBuildSpec],
759 batch_config: WasmBuildBatchConfig,
760 progress_config: WasmBuildProgressConfig,
761 observer: F,
762) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
763where
764 F: FnMut(WasmBuildBatchProgressEvent),
765{
766 build_wasm_canisters_cached_batch_with_progress_internal(
767 specs,
768 batch_config,
769 progress_config,
770 None,
771 observer,
772 )
773}
774
775fn build_wasm_canisters_cached_batch_with_session_and_progress<F>(
776 specs: &[LabeledWasmBuildSpec],
777 batch_config: WasmBuildBatchConfig,
778 progress_config: WasmBuildProgressConfig,
779 session: &mut WasmBuildSessionState,
780 observer: F,
781) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
782where
783 F: FnMut(WasmBuildBatchProgressEvent),
784{
785 build_wasm_canisters_cached_batch_with_progress_internal(
786 specs,
787 batch_config,
788 progress_config,
789 Some(session),
790 observer,
791 )
792}
793
794fn build_wasm_canisters_cached_batch_with_progress_internal<F>(
795 specs: &[LabeledWasmBuildSpec],
796 batch_config: WasmBuildBatchConfig,
797 progress_config: WasmBuildProgressConfig,
798 session: Option<&mut WasmBuildSessionState>,
799 mut observer: F,
800) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
801where
802 F: FnMut(WasmBuildBatchProgressEvent),
803{
804 validate_batch_labels(specs)?;
805 if session
806 .as_deref()
807 .is_some_and(WasmBuildSessionState::is_invalidated)
808 {
809 return Err(WasmBuildBatchContractError::SourceLeaseInvalidated);
810 }
811 let count = specs.len();
812 let build_specs = specs
813 .iter()
814 .map(|labeled| labeled.spec.clone())
815 .collect::<Vec<_>>();
816 let mut resolver = session.map_or_else(
817 || WasmBuildBatchInputResolver::new(&build_specs),
818 |session| WasmBuildBatchInputResolver::with_session(&build_specs, session),
819 );
820 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
821 let label = specs[index].label.clone();
822 observer(WasmBuildBatchProgressEvent::BuildStarted {
823 index,
824 label: label.clone(),
825 total: count,
826 });
827 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
828 spec,
829 index,
830 &mut resolver,
831 progress_config,
832 |event| {
833 observer(WasmBuildBatchProgressEvent::BuildProgress {
834 index,
835 label: label.clone(),
836 event,
837 });
838 },
839 );
840 observer(match &attempt.result {
841 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
842 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
843 });
844 attempt
845 });
846 report.input_resolution = resolver.metrics();
847 Ok(report)
848}
849
850fn build_wasm_batch<F>(
851 specs: &[LabeledWasmBuildSpec],
852 config: WasmBuildBatchConfig,
853 mut build: F,
854) -> WasmBuildBatchReport
855where
856 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
857{
858 let started = Instant::now();
859 let mut entries = Vec::with_capacity(specs.len());
860 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
861 for (index, labeled) in specs.iter().enumerate() {
862 let entry_started = Instant::now();
863 let spec = &labeled.spec;
864 if config.shared_incremental_maintenance.is_some()
865 && spec.shared_incremental_target_maintenance().is_some()
866 {
867 let elapsed = entry_started.elapsed();
868 let attempt =
869 WasmBuildBatchAttempt::invalid_spec(batch_maintenance_ownership_error(), elapsed);
870 entries.push(WasmBuildBatchEntry {
871 index,
872 label: labeled.label.clone(),
873 result: attempt.result,
874 failure: Some(WasmBuildFailureDetails {
875 phase: attempt
876 .failure_phase
877 .expect("invalid batch entry must retain its failure phase"),
878 timings: attempt
879 .failure_timings
880 .expect("invalid batch entry must retain its failure timings"),
881 }),
882 entry_elapsed: elapsed,
883 });
884 continue;
885 }
886 let configured = maintenance.prepare_spec(spec);
887 let attempt = build(configured.as_ref().unwrap_or(spec), index);
888 let failure = attempt
889 .failure_phase
890 .zip(attempt.failure_timings)
891 .map(|(phase, timings)| WasmBuildFailureDetails { phase, timings });
892 entries.push(WasmBuildBatchEntry {
893 index,
894 label: labeled.label.clone(),
895 result: attempt.result,
896 failure,
897 entry_elapsed: entry_started.elapsed(),
898 });
899 }
900 WasmBuildBatchReport {
901 entries,
902 input_resolution: WasmBuildBatchInputMetrics::default(),
903 total: started.elapsed(),
904 }
905}
906
907fn validate_batch_labels(
908 specs: &[LabeledWasmBuildSpec],
909) -> Result<(), WasmBuildBatchContractError> {
910 let mut labels = HashMap::with_capacity(specs.len());
911 for (index, labeled) in specs.iter().enumerate() {
912 if labeled.label.is_empty() {
913 return Err(WasmBuildBatchContractError::EmptyLabel { index });
914 }
915 if let Some(first_index) = labels.get(labeled.label.as_str()) {
916 return Err(WasmBuildBatchContractError::DuplicateLabel {
917 label: labeled.label.clone(),
918 first_index: *first_index,
919 duplicate_index: index,
920 });
921 }
922 labels.insert(labeled.label.as_str(), index);
923 }
924 Ok(())
925}
926
927struct BatchMaintenanceTracker {
928 config: Option<SharedIncrementalTargetMaintenanceConfig>,
929 configured_targets: HashSet<PathBuf>,
930}
931
932impl BatchMaintenanceTracker {
933 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
934 Self {
935 config,
936 configured_targets: HashSet::new(),
937 }
938 }
939
940 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
941 let config = self.config?;
942 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
943 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
944 return None;
945 };
946 if !self.configured_targets.insert(target_dir.clone()) {
947 return None;
948 }
949 Some(
950 spec.clone()
951 .with_shared_incremental_target_maintenance(config),
952 )
953 }
954}
955
956fn batch_maintenance_ownership_error() -> WasmBuildError {
957 WasmBuildError::InvalidSpec {
958 message:
959 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
960 .to_owned(),
961 }
962}
963
964impl std::fmt::Display for WasmBuildBatchContractError {
965 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
966 match self {
967 Self::EmptyLabel { index } => {
968 write!(formatter, "Wasm batch label at index {index} is empty")
969 }
970 Self::DuplicateLabel {
971 label,
972 first_index,
973 duplicate_index,
974 } => write!(
975 formatter,
976 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
977 ),
978 Self::SourceLeaseInvalidated => formatter.write_str(
979 "Wasm build session source lease was invalidated by a detected input mutation",
980 ),
981 }
982 }
983}
984
985impl std::error::Error for WasmBuildBatchContractError {}
986
987#[cfg(test)]
988mod tests;