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]
163 pub fn new<Guard: ?Sized>(_source_write_guard: &'guard Guard) -> Self {
164 Self {
165 state: WasmBuildSessionState::new(),
166 _source_guard: PhantomData,
167 }
168 }
169
170 pub fn build_batch(
172 &mut self,
173 specs: &[LabeledWasmBuildSpec],
174 config: WasmBuildBatchConfig,
175 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
176 build_wasm_canisters_cached_batch_with_session(specs, config, &mut self.state)
177 }
178
179 pub fn build_batch_with_progress<F>(
181 &mut self,
182 specs: &[LabeledWasmBuildSpec],
183 batch_config: WasmBuildBatchConfig,
184 progress_config: WasmBuildProgressConfig,
185 observer: F,
186 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
187 where
188 F: FnMut(WasmBuildBatchProgressEvent),
189 {
190 build_wasm_canisters_cached_batch_with_session_and_progress(
191 specs,
192 batch_config,
193 progress_config,
194 &mut self.state,
195 observer,
196 )
197 }
198
199 #[must_use]
201 pub const fn metrics(&self) -> WasmBuildSessionMetrics {
202 WasmBuildSessionMetrics {
203 snapshots: self.state.snapshot_count(),
204 snapshot_reuses: self.state.snapshot_reuses(),
205 invalidated: self.state.is_invalidated(),
206 }
207 }
208}
209
210impl WasmBuildSessionMetrics {
211 #[must_use]
213 pub const fn snapshots(self) -> usize {
214 self.snapshots
215 }
216
217 #[must_use]
219 pub const fn snapshot_reuses(self) -> usize {
220 self.snapshot_reuses
221 }
222
223 #[must_use]
225 pub const fn is_invalidated(self) -> bool {
226 self.invalidated
227 }
228}
229
230impl WasmBuildBatchEntry {
231 #[must_use]
233 pub const fn index(&self) -> usize {
234 self.index
235 }
236
237 #[must_use]
239 pub fn label(&self) -> &str {
240 &self.label
241 }
242
243 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
245 self.result.as_ref()
246 }
247
248 #[must_use]
250 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
251 self.result.as_ref().ok()
252 }
253
254 #[must_use]
256 pub fn error(&self) -> Option<&WasmBuildError> {
257 self.result.as_ref().err()
258 }
259
260 #[must_use]
262 pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
263 self.failure
264 }
265
266 #[must_use]
268 pub const fn entry_elapsed(&self) -> Duration {
269 self.entry_elapsed
270 }
271
272 #[must_use]
274 pub const fn is_success(&self) -> bool {
275 self.result.is_ok()
276 }
277
278 pub fn into_parts(
280 self,
281 ) -> (
282 usize,
283 String,
284 Result<WasmBuildOutcome, WasmBuildError>,
285 Option<WasmBuildFailureDetails>,
286 Duration,
287 ) {
288 (
289 self.index,
290 self.label,
291 self.result,
292 self.failure,
293 self.entry_elapsed,
294 )
295 }
296}
297
298impl WasmBuildFailureDetails {
299 #[must_use]
301 pub const fn phase(self) -> WasmBuildFailurePhase {
302 self.phase
303 }
304
305 #[must_use]
307 pub const fn timings(self) -> WasmBuildFailureTimings {
308 self.timings
309 }
310}
311
312impl<'a> WasmBuildBatchOutcomeEntry<'a> {
313 #[must_use]
315 pub const fn index(self) -> usize {
316 self.index
317 }
318
319 #[must_use]
321 pub const fn label(self) -> &'a str {
322 self.label
323 }
324
325 #[must_use]
327 pub const fn outcome(self) -> &'a WasmBuildOutcome {
328 self.outcome
329 }
330
331 #[must_use]
333 pub const fn entry_elapsed(self) -> Duration {
334 self.entry_elapsed
335 }
336}
337
338impl<'a> WasmBuildBatchFailure<'a> {
339 #[must_use]
341 pub const fn index(self) -> usize {
342 self.index
343 }
344
345 #[must_use]
347 pub const fn label(self) -> &'a str {
348 self.label
349 }
350
351 #[must_use]
353 pub const fn error(self) -> &'a WasmBuildError {
354 self.error
355 }
356
357 #[must_use]
359 pub const fn phase(self) -> WasmBuildFailurePhase {
360 self.details.phase
361 }
362
363 #[must_use]
365 pub const fn timings(self) -> WasmBuildFailureTimings {
366 self.details.timings
367 }
368
369 #[must_use]
371 pub const fn entry_elapsed(self) -> Duration {
372 self.entry_elapsed
373 }
374}
375
376impl<'a> WasmBuildBatchMaintenanceEntry<'a> {
377 #[must_use]
379 pub const fn index(self) -> usize {
380 self.index
381 }
382
383 #[must_use]
385 pub const fn label(self) -> &'a str {
386 self.label
387 }
388
389 #[must_use]
391 pub const fn outcome(self) -> &'a SharedIncrementalTargetMaintenanceOutcome {
392 self.outcome
393 }
394}
395
396#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
398pub struct WasmBuildBatchMetrics {
399 specifications: usize,
400 succeeded: usize,
401 failed: usize,
402 built: usize,
403 reused: usize,
404 input_resolution_runs: usize,
405 input_resolution_reuses: usize,
406 input_resolution_session_reuses: usize,
407 successful_timings: WasmBuildTimings,
408 total: Duration,
409}
410
411#[non_exhaustive]
413#[derive(Clone, Debug, Eq, PartialEq)]
414pub enum WasmBuildBatchProgressEvent {
415 BuildStarted {
417 index: usize,
419 label: String,
421 total: usize,
423 },
424 BuildProgress {
426 index: usize,
428 label: String,
430 event: WasmBuildProgressEvent,
432 },
433 BuildFinished {
435 index: usize,
437 label: String,
439 },
440 BuildFailed {
442 index: usize,
444 label: String,
446 },
447}
448
449impl WasmBuildBatchReport {
450 #[must_use]
452 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
453 &self.entries
454 }
455
456 #[must_use]
458 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
459 self.entries
460 }
461
462 pub fn outcomes(&self) -> impl Iterator<Item = WasmBuildBatchOutcomeEntry<'_>> {
464 self.entries.iter().filter_map(|entry| {
465 entry.outcome().map(|outcome| WasmBuildBatchOutcomeEntry {
466 index: entry.index,
467 label: &entry.label,
468 outcome,
469 entry_elapsed: entry.entry_elapsed,
470 })
471 })
472 }
473
474 pub fn failures(&self) -> impl Iterator<Item = WasmBuildBatchFailure<'_>> {
476 self.entries.iter().filter_map(|entry| {
477 entry.error().map(|error| WasmBuildBatchFailure {
478 index: entry.index,
479 label: &entry.label,
480 error,
481 details: entry
482 .failure
483 .expect("failed Wasm batch entry must retain failure details"),
484 entry_elapsed: entry.entry_elapsed,
485 })
486 })
487 }
488
489 pub fn shared_incremental_maintenance_outcomes(
494 &self,
495 ) -> impl Iterator<Item = WasmBuildBatchMaintenanceEntry<'_>> {
496 self.outcomes().filter_map(|entry| {
497 entry
498 .outcome
499 .record()
500 .shared_incremental_maintenance()
501 .map(|outcome| WasmBuildBatchMaintenanceEntry {
502 index: entry.index,
503 label: entry.label,
504 outcome,
505 })
506 })
507 }
508
509 #[must_use]
511 pub const fn total(&self) -> Duration {
512 self.total
513 }
514
515 #[must_use]
517 pub fn is_success(&self) -> bool {
518 self.entries.iter().all(WasmBuildBatchEntry::is_success)
519 }
520
521 #[must_use]
523 pub fn metrics(&self) -> WasmBuildBatchMetrics {
524 let mut metrics = WasmBuildBatchMetrics {
525 specifications: self.entries.len(),
526 input_resolution_runs: self.input_resolution.runs,
527 input_resolution_reuses: self.input_resolution.reuses,
528 input_resolution_session_reuses: self.input_resolution.session_reuses,
529 total: self.total,
530 ..WasmBuildBatchMetrics::default()
531 };
532 for entry in &self.entries {
533 match &entry.result {
534 Ok(outcome) => {
535 metrics.succeeded += 1;
536 if outcome.is_reused() {
537 metrics.reused += 1;
538 } else {
539 metrics.built += 1;
540 }
541 metrics.successful_timings = metrics
542 .successful_timings
543 .saturating_add(outcome.record().timings());
544 }
545 Err(_) => metrics.failed += 1,
546 }
547 }
548 metrics
549 }
550}
551
552impl WasmBuildBatchMetrics {
553 #[must_use]
555 pub const fn specifications(self) -> usize {
556 self.specifications
557 }
558
559 #[must_use]
561 pub const fn succeeded(self) -> usize {
562 self.succeeded
563 }
564
565 #[must_use]
567 pub const fn failed(self) -> usize {
568 self.failed
569 }
570
571 #[must_use]
573 pub const fn built(self) -> usize {
574 self.built
575 }
576
577 #[must_use]
579 pub const fn reused(self) -> usize {
580 self.reused
581 }
582
583 #[must_use]
585 pub const fn input_resolution_runs(self) -> usize {
586 self.input_resolution_runs
587 }
588
589 #[must_use]
591 pub const fn input_resolution_reuses(self) -> usize {
592 self.input_resolution_reuses
593 }
594
595 #[must_use]
597 pub const fn input_resolution_session_reuses(self) -> usize {
598 self.input_resolution_session_reuses
599 }
600
601 #[must_use]
603 pub const fn successful_timings(self) -> WasmBuildTimings {
604 self.successful_timings
605 }
606
607 #[must_use]
609 pub const fn total(self) -> Duration {
610 self.total
611 }
612}
613
614impl WasmBuildBatchConfig {
615 #[must_use]
617 pub const fn new() -> Self {
618 Self {
619 shared_incremental_maintenance: None,
620 }
621 }
622
623 #[must_use]
625 pub const fn with_shared_incremental_target_maintenance(
626 mut self,
627 config: SharedIncrementalTargetMaintenanceConfig,
628 ) -> Self {
629 self.shared_incremental_maintenance = Some(config);
630 self
631 }
632
633 #[must_use]
635 pub const fn with_shared_incremental_target_maintenance_at_most_every(
636 self,
637 policy: SharedIncrementalTargetPrunePolicy,
638 minimum_interval: Duration,
639 ) -> Self {
640 self.with_shared_incremental_target_maintenance(
641 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
642 )
643 }
644
645 #[must_use]
647 pub const fn shared_incremental_target_maintenance(
648 self,
649 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
650 self.shared_incremental_maintenance
651 }
652}
653
654impl std::fmt::Display for WasmBuildBatchReport {
655 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656 let metrics = self.metrics();
657 write!(
658 formatter,
659 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} successful_timings=({}) total={:?}",
660 metrics.specifications(),
661 metrics.succeeded(),
662 metrics.failed(),
663 metrics.built(),
664 metrics.reused(),
665 metrics.input_resolution_runs(),
666 metrics.input_resolution_reuses(),
667 metrics.input_resolution_session_reuses(),
668 metrics.successful_timings(),
669 metrics.total(),
670 )
671 }
672}
673
674pub fn build_wasm_canisters_cached_batch(
681 specs: &[LabeledWasmBuildSpec],
682) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
683 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
684}
685
686pub fn build_wasm_canisters_cached_batch_with_config(
693 specs: &[LabeledWasmBuildSpec],
694 config: WasmBuildBatchConfig,
695) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
696 build_wasm_canisters_cached_batch_internal(specs, config, None)
697}
698
699fn build_wasm_canisters_cached_batch_with_session(
700 specs: &[LabeledWasmBuildSpec],
701 config: WasmBuildBatchConfig,
702 session: &mut WasmBuildSessionState,
703) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
704 build_wasm_canisters_cached_batch_internal(specs, config, Some(session))
705}
706
707fn build_wasm_canisters_cached_batch_internal(
708 specs: &[LabeledWasmBuildSpec],
709 config: WasmBuildBatchConfig,
710 session: Option<&mut WasmBuildSessionState>,
711) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
712 validate_batch_labels(specs)?;
713 if session
714 .as_deref()
715 .is_some_and(WasmBuildSessionState::is_invalidated)
716 {
717 return Err(WasmBuildBatchContractError::SourceLeaseInvalidated);
718 }
719 let build_specs = specs
720 .iter()
721 .map(|labeled| labeled.spec.clone())
722 .collect::<Vec<_>>();
723 let mut resolver = session.map_or_else(
724 || WasmBuildBatchInputResolver::new(&build_specs),
725 |session| WasmBuildBatchInputResolver::with_session(&build_specs, session),
726 );
727 let mut report = build_wasm_batch(specs, config, |spec, index| {
728 build_wasm_canisters_cached_in_batch(spec, index, &mut resolver)
729 });
730 report.input_resolution = resolver.metrics();
731 Ok(report)
732}
733
734pub fn build_wasm_canisters_cached_batch_with_progress<F>(
740 specs: &[LabeledWasmBuildSpec],
741 config: WasmBuildProgressConfig,
742 observer: F,
743) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
744where
745 F: FnMut(WasmBuildBatchProgressEvent),
746{
747 build_wasm_canisters_cached_batch_with_config_and_progress(
748 specs,
749 WasmBuildBatchConfig::new(),
750 config,
751 observer,
752 )
753}
754
755pub fn build_wasm_canisters_cached_batch_with_config_and_progress<F>(
757 specs: &[LabeledWasmBuildSpec],
758 batch_config: WasmBuildBatchConfig,
759 progress_config: WasmBuildProgressConfig,
760 observer: F,
761) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
762where
763 F: FnMut(WasmBuildBatchProgressEvent),
764{
765 build_wasm_canisters_cached_batch_with_progress_internal(
766 specs,
767 batch_config,
768 progress_config,
769 None,
770 observer,
771 )
772}
773
774fn build_wasm_canisters_cached_batch_with_session_and_progress<F>(
775 specs: &[LabeledWasmBuildSpec],
776 batch_config: WasmBuildBatchConfig,
777 progress_config: WasmBuildProgressConfig,
778 session: &mut WasmBuildSessionState,
779 observer: F,
780) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
781where
782 F: FnMut(WasmBuildBatchProgressEvent),
783{
784 build_wasm_canisters_cached_batch_with_progress_internal(
785 specs,
786 batch_config,
787 progress_config,
788 Some(session),
789 observer,
790 )
791}
792
793fn build_wasm_canisters_cached_batch_with_progress_internal<F>(
794 specs: &[LabeledWasmBuildSpec],
795 batch_config: WasmBuildBatchConfig,
796 progress_config: WasmBuildProgressConfig,
797 session: Option<&mut WasmBuildSessionState>,
798 mut observer: F,
799) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
800where
801 F: FnMut(WasmBuildBatchProgressEvent),
802{
803 validate_batch_labels(specs)?;
804 if session
805 .as_deref()
806 .is_some_and(WasmBuildSessionState::is_invalidated)
807 {
808 return Err(WasmBuildBatchContractError::SourceLeaseInvalidated);
809 }
810 let count = specs.len();
811 let build_specs = specs
812 .iter()
813 .map(|labeled| labeled.spec.clone())
814 .collect::<Vec<_>>();
815 let mut resolver = session.map_or_else(
816 || WasmBuildBatchInputResolver::new(&build_specs),
817 |session| WasmBuildBatchInputResolver::with_session(&build_specs, session),
818 );
819 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
820 let label = specs[index].label.clone();
821 observer(WasmBuildBatchProgressEvent::BuildStarted {
822 index,
823 label: label.clone(),
824 total: count,
825 });
826 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
827 spec,
828 index,
829 &mut resolver,
830 progress_config,
831 |event| {
832 observer(WasmBuildBatchProgressEvent::BuildProgress {
833 index,
834 label: label.clone(),
835 event,
836 });
837 },
838 );
839 observer(match &attempt.result {
840 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
841 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
842 });
843 attempt
844 });
845 report.input_resolution = resolver.metrics();
846 Ok(report)
847}
848
849fn build_wasm_batch<F>(
850 specs: &[LabeledWasmBuildSpec],
851 config: WasmBuildBatchConfig,
852 mut build: F,
853) -> WasmBuildBatchReport
854where
855 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
856{
857 let started = Instant::now();
858 let mut entries = Vec::with_capacity(specs.len());
859 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
860 for (index, labeled) in specs.iter().enumerate() {
861 let entry_started = Instant::now();
862 let spec = &labeled.spec;
863 if config.shared_incremental_maintenance.is_some()
864 && spec.shared_incremental_target_maintenance().is_some()
865 {
866 let elapsed = entry_started.elapsed();
867 let attempt =
868 WasmBuildBatchAttempt::invalid_spec(batch_maintenance_ownership_error(), elapsed);
869 entries.push(WasmBuildBatchEntry {
870 index,
871 label: labeled.label.clone(),
872 result: attempt.result,
873 failure: Some(WasmBuildFailureDetails {
874 phase: attempt
875 .failure_phase
876 .expect("invalid batch entry must retain its failure phase"),
877 timings: attempt
878 .failure_timings
879 .expect("invalid batch entry must retain its failure timings"),
880 }),
881 entry_elapsed: elapsed,
882 });
883 continue;
884 }
885 let configured = maintenance.prepare_spec(spec);
886 let attempt = build(configured.as_ref().unwrap_or(spec), index);
887 let failure = attempt
888 .failure_phase
889 .zip(attempt.failure_timings)
890 .map(|(phase, timings)| WasmBuildFailureDetails { phase, timings });
891 entries.push(WasmBuildBatchEntry {
892 index,
893 label: labeled.label.clone(),
894 result: attempt.result,
895 failure,
896 entry_elapsed: entry_started.elapsed(),
897 });
898 }
899 WasmBuildBatchReport {
900 entries,
901 input_resolution: WasmBuildBatchInputMetrics::default(),
902 total: started.elapsed(),
903 }
904}
905
906fn validate_batch_labels(
907 specs: &[LabeledWasmBuildSpec],
908) -> Result<(), WasmBuildBatchContractError> {
909 let mut labels = HashMap::with_capacity(specs.len());
910 for (index, labeled) in specs.iter().enumerate() {
911 if labeled.label.is_empty() {
912 return Err(WasmBuildBatchContractError::EmptyLabel { index });
913 }
914 if let Some(first_index) = labels.get(labeled.label.as_str()) {
915 return Err(WasmBuildBatchContractError::DuplicateLabel {
916 label: labeled.label.clone(),
917 first_index: *first_index,
918 duplicate_index: index,
919 });
920 }
921 labels.insert(labeled.label.as_str(), index);
922 }
923 Ok(())
924}
925
926struct BatchMaintenanceTracker {
927 config: Option<SharedIncrementalTargetMaintenanceConfig>,
928 configured_targets: HashSet<PathBuf>,
929}
930
931impl BatchMaintenanceTracker {
932 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
933 Self {
934 config,
935 configured_targets: HashSet::new(),
936 }
937 }
938
939 fn prepare_spec(&mut self, spec: &WasmBuildSpec) -> Option<WasmBuildSpec> {
940 let config = self.config?;
941 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
942 let WasmBuildCacheMode::SharedIncremental { target_dir } = spec.cache_mode() else {
943 return None;
944 };
945 if !self.configured_targets.insert(target_dir.clone()) {
946 return None;
947 }
948 Some(
949 spec.clone()
950 .with_shared_incremental_target_maintenance(config),
951 )
952 }
953}
954
955fn batch_maintenance_ownership_error() -> WasmBuildError {
956 WasmBuildError::InvalidSpec {
957 message:
958 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
959 .to_owned(),
960 }
961}
962
963impl std::fmt::Display for WasmBuildBatchContractError {
964 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965 match self {
966 Self::EmptyLabel { index } => {
967 write!(formatter, "Wasm batch label at index {index} is empty")
968 }
969 Self::DuplicateLabel {
970 label,
971 first_index,
972 duplicate_index,
973 } => write!(
974 formatter,
975 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
976 ),
977 Self::SourceLeaseInvalidated => formatter.write_str(
978 "Wasm build session source lease was invalidated by a detected input mutation",
979 ),
980 }
981 }
982}
983
984impl std::error::Error for WasmBuildBatchContractError {}
985
986#[cfg(test)]
987mod tests;