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, WasmBuildInputReuse,
15 WasmBuildInputSnapshotState, WasmBuildOutcome, WasmBuildProgressConfig, WasmBuildProgressEvent,
16 WasmBuildSessionState, WasmBuildSpec, WasmBuildTimings, WasmInputResolutionTimings,
17 build_wasm_canisters_cached_in_batch, build_wasm_canisters_cached_in_batch_with_progress,
18 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#[non_exhaustive]
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub enum WasmBuildBatchContractError {
98 EmptyLabel {
100 index: usize,
102 },
103 DuplicateLabel {
105 label: String,
107 first_index: usize,
109 duplicate_index: usize,
111 },
112 SourceLeaseInvalidated,
114 SpecificationNotPrepared {
116 index: usize,
118 label: String,
120 },
121}
122
123impl LabeledWasmBuildSpec {
124 #[must_use]
126 pub fn new(label: impl Into<String>, spec: WasmBuildSpec) -> Self {
127 Self {
128 label: label.into(),
129 spec,
130 }
131 }
132
133 #[must_use]
135 pub fn label(&self) -> &str {
136 &self.label
137 }
138
139 #[must_use]
141 pub const fn spec(&self) -> &WasmBuildSpec {
142 &self.spec
143 }
144
145 #[must_use]
147 pub fn into_parts(self) -> (String, WasmBuildSpec) {
148 (self.label, self.spec)
149 }
150}
151
152impl<'guard> WasmBuildSession<'guard> {
153 #[must_use]
162 pub fn assume_sources_immutable<Guard: ?Sized>(_source_write_guard: &'guard Guard) -> Self {
163 Self {
164 state: WasmBuildSessionState::new(),
165 _source_guard: PhantomData,
166 }
167 }
168
169 pub fn build_batch(
175 &mut self,
176 specs: &[LabeledWasmBuildSpec],
177 config: WasmBuildBatchConfig,
178 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
179 run_wasm_batch(
180 specs,
181 config,
182 Some(WasmBuildInputReuse::Session(&mut self.state)),
183 None,
184 )
185 }
186
187 pub fn build_batch_with_progress<F>(
193 &mut self,
194 specs: &[LabeledWasmBuildSpec],
195 batch_config: WasmBuildBatchConfig,
196 progress_config: WasmBuildProgressConfig,
197 mut observer: F,
198 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
199 where
200 F: FnMut(WasmBuildBatchProgressEvent),
201 {
202 run_wasm_batch(
203 specs,
204 batch_config,
205 Some(WasmBuildInputReuse::Session(&mut self.state)),
206 Some((progress_config, &mut observer)),
207 )
208 }
209
210 #[must_use]
212 pub const fn metrics(&self) -> WasmBuildSessionMetrics {
213 WasmBuildSessionMetrics {
214 snapshots: self.state.snapshot_count(),
215 snapshot_reuses: self.state.snapshot_reuses(),
216 invalidated: self.state.is_invalidated(),
217 }
218 }
219}
220
221impl WasmBuildSessionMetrics {
222 #[must_use]
224 pub const fn snapshots(self) -> usize {
225 self.snapshots
226 }
227
228 #[must_use]
230 pub const fn snapshot_reuses(self) -> usize {
231 self.snapshot_reuses
232 }
233
234 #[must_use]
236 pub const fn is_invalidated(self) -> bool {
237 self.invalidated
238 }
239}
240
241impl<'guard> WasmBuildInputSnapshot<'guard> {
242 pub fn prepare_assuming_sources_immutable<Guard: ?Sized>(
252 _source_write_guard: &'guard Guard,
253 specs: &[WasmBuildSpec],
254 ) -> Result<Self, WasmBuildError> {
255 Ok(Self {
256 state: WasmBuildInputSnapshotState::prepare(specs)?,
257 _source_guard: PhantomData,
258 })
259 }
260
261 pub fn build_batch(
270 &self,
271 specs: &[LabeledWasmBuildSpec],
272 config: WasmBuildBatchConfig,
273 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
274 run_wasm_batch(
275 specs,
276 config,
277 Some(WasmBuildInputReuse::Snapshot(&self.state)),
278 None,
279 )
280 }
281
282 pub fn build_batch_with_progress<F>(
290 &self,
291 specs: &[LabeledWasmBuildSpec],
292 batch_config: WasmBuildBatchConfig,
293 progress_config: WasmBuildProgressConfig,
294 mut observer: F,
295 ) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
296 where
297 F: FnMut(WasmBuildBatchProgressEvent),
298 {
299 run_wasm_batch(
300 specs,
301 batch_config,
302 Some(WasmBuildInputReuse::Snapshot(&self.state)),
303 Some((progress_config, &mut observer)),
304 )
305 }
306
307 #[must_use]
309 pub fn metrics(&self) -> WasmBuildInputSnapshotMetrics {
310 let preparation = self.state.preparation_metrics();
311 WasmBuildInputSnapshotMetrics {
312 specifications: self.state.specification_count(),
313 input_resolution_runs: preparation.runs,
314 input_resolution_reuses: preparation.reuses,
315 input_resolution_timings: self.state.preparation_timings(),
316 reader_reuses: self.state.reader_reuses(),
317 invalidated: self.state.is_invalidated(),
318 }
319 }
320}
321
322impl WasmBuildInputSnapshotMetrics {
323 #[must_use]
325 pub const fn specifications(self) -> usize {
326 self.specifications
327 }
328
329 #[must_use]
331 pub const fn input_resolution_runs(self) -> usize {
332 self.input_resolution_runs
333 }
334
335 #[must_use]
337 pub const fn input_resolution_reuses(self) -> usize {
338 self.input_resolution_reuses
339 }
340
341 #[must_use]
343 pub const fn input_resolution_timings(self) -> WasmInputResolutionTimings {
344 self.input_resolution_timings
345 }
346
347 #[must_use]
349 pub const fn reader_reuses(self) -> usize {
350 self.reader_reuses
351 }
352
353 #[must_use]
355 pub const fn is_invalidated(self) -> bool {
356 self.invalidated
357 }
358}
359
360impl WasmBuildBatchEntry {
361 #[must_use]
363 pub const fn index(&self) -> usize {
364 self.index
365 }
366
367 #[must_use]
369 pub fn label(&self) -> &str {
370 &self.label
371 }
372
373 pub const fn result(&self) -> Result<&WasmBuildOutcome, &WasmBuildError> {
378 match &self.result {
379 Ok(outcome) => Ok(outcome),
380 Err((error, _)) => Err(error),
381 }
382 }
383
384 #[must_use]
386 pub fn outcome(&self) -> Option<&WasmBuildOutcome> {
387 self.result.as_ref().ok()
388 }
389
390 #[must_use]
392 pub fn error(&self) -> Option<&WasmBuildError> {
393 self.result().err()
394 }
395
396 #[must_use]
398 pub const fn failure_details(&self) -> Option<WasmBuildFailureDetails> {
399 match &self.result {
400 Ok(_) => None,
401 Err((_, details)) => Some(*details),
402 }
403 }
404
405 #[must_use]
407 pub const fn entry_elapsed(&self) -> Duration {
408 self.entry_elapsed
409 }
410
411 #[must_use]
413 pub const fn is_success(&self) -> bool {
414 self.result.is_ok()
415 }
416
417 pub fn into_parts(
419 self,
420 ) -> (
421 usize,
422 String,
423 Result<WasmBuildOutcome, WasmBuildError>,
424 Option<WasmBuildFailureDetails>,
425 Duration,
426 ) {
427 let (result, failure) = match self.result {
428 Ok(outcome) => (Ok(outcome), None),
429 Err((error, details)) => (Err(error), Some(details)),
430 };
431 (self.index, self.label, result, failure, self.entry_elapsed)
432 }
433}
434
435#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
437pub struct WasmBuildBatchMetrics {
438 specifications: usize,
439 built: usize,
440 reused: usize,
441 input_resolution_runs: usize,
442 input_resolution_reuses: usize,
443 input_resolution_session_reuses: usize,
444 input_resolution_prepared_reuses: usize,
445 successful_timings: WasmBuildTimings,
446 total: Duration,
447}
448
449#[non_exhaustive]
451#[derive(Clone, Debug, Eq, PartialEq)]
452pub enum WasmBuildBatchProgressEvent {
453 BuildStarted {
455 index: usize,
457 label: String,
459 total: usize,
461 },
462 BuildProgress {
464 index: usize,
466 label: String,
468 event: WasmBuildProgressEvent,
470 },
471 BuildFinished {
473 index: usize,
475 label: String,
477 },
478 BuildFailed {
480 index: usize,
482 label: String,
484 },
485}
486
487impl WasmBuildBatchReport {
488 #[must_use]
490 pub fn entries(&self) -> &[WasmBuildBatchEntry] {
491 &self.entries
492 }
493
494 #[must_use]
496 pub fn into_entries(self) -> Vec<WasmBuildBatchEntry> {
497 self.entries
498 }
499
500 pub fn outcomes(&self) -> impl Iterator<Item = &WasmBuildBatchEntry> {
502 self.entries.iter().filter(|entry| entry.is_success())
503 }
504
505 pub fn failures(&self) -> impl Iterator<Item = &WasmBuildBatchEntry> {
507 self.entries.iter().filter(|entry| !entry.is_success())
508 }
509
510 pub fn shared_incremental_maintenance_outcomes(
515 &self,
516 ) -> impl Iterator<
517 Item = (
518 &WasmBuildBatchEntry,
519 &SharedIncrementalTargetMaintenanceOutcome,
520 ),
521 > {
522 self.outcomes().filter_map(|entry| {
523 entry
524 .outcome()?
525 .record()
526 .shared_incremental_maintenance()
527 .map(|outcome| (entry, outcome))
528 })
529 }
530
531 #[must_use]
533 pub const fn total(&self) -> Duration {
534 self.total
535 }
536
537 #[must_use]
539 pub fn is_success(&self) -> bool {
540 self.entries.iter().all(WasmBuildBatchEntry::is_success)
541 }
542
543 #[must_use]
545 pub fn metrics(&self) -> WasmBuildBatchMetrics {
546 let mut metrics = WasmBuildBatchMetrics {
547 specifications: self.entries.len(),
548 input_resolution_runs: self.input_resolution.runs,
549 input_resolution_reuses: self.input_resolution.reuses,
550 input_resolution_session_reuses: self.input_resolution.session_reuses,
551 input_resolution_prepared_reuses: self.input_resolution.prepared_reuses,
552 total: self.total,
553 ..WasmBuildBatchMetrics::default()
554 };
555 for outcome in self.entries.iter().filter_map(WasmBuildBatchEntry::outcome) {
556 if outcome.is_reused() {
557 metrics.reused += 1;
558 } else {
559 metrics.built += 1;
560 }
561 metrics.successful_timings = metrics
562 .successful_timings
563 .saturating_add(outcome.record().timings());
564 }
565 metrics
566 }
567}
568
569impl WasmBuildBatchMetrics {
570 #[must_use]
572 pub const fn specifications(self) -> usize {
573 self.specifications
574 }
575
576 #[must_use]
578 pub const fn succeeded(self) -> usize {
579 self.built + self.reused
580 }
581
582 #[must_use]
584 pub const fn failed(self) -> usize {
585 self.specifications - self.succeeded()
586 }
587
588 #[must_use]
590 pub const fn built(self) -> usize {
591 self.built
592 }
593
594 #[must_use]
596 pub const fn reused(self) -> usize {
597 self.reused
598 }
599
600 #[must_use]
602 pub const fn input_resolution_runs(self) -> usize {
603 self.input_resolution_runs
604 }
605
606 #[must_use]
608 pub const fn input_resolution_reuses(self) -> usize {
609 self.input_resolution_reuses
610 }
611
612 #[must_use]
614 pub const fn input_resolution_session_reuses(self) -> usize {
615 self.input_resolution_session_reuses
616 }
617
618 #[must_use]
620 pub const fn input_resolution_prepared_reuses(self) -> usize {
621 self.input_resolution_prepared_reuses
622 }
623
624 #[must_use]
626 pub const fn successful_timings(self) -> WasmBuildTimings {
627 self.successful_timings
628 }
629
630 #[must_use]
632 pub const fn total(self) -> Duration {
633 self.total
634 }
635}
636
637impl WasmBuildBatchConfig {
638 #[must_use]
640 pub const fn new() -> Self {
641 Self {
642 shared_incremental_maintenance: None,
643 }
644 }
645
646 #[must_use]
648 pub const fn with_shared_incremental_target_maintenance(
649 mut self,
650 config: SharedIncrementalTargetMaintenanceConfig,
651 ) -> Self {
652 self.shared_incremental_maintenance = Some(config);
653 self
654 }
655
656 #[must_use]
658 pub const fn with_shared_incremental_target_maintenance_at_most_every(
659 self,
660 policy: SharedIncrementalTargetPrunePolicy,
661 minimum_interval: Duration,
662 ) -> Self {
663 self.with_shared_incremental_target_maintenance(
664 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
665 )
666 }
667
668 #[must_use]
670 pub const fn shared_incremental_target_maintenance(
671 self,
672 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
673 self.shared_incremental_maintenance
674 }
675}
676
677impl std::fmt::Display for WasmBuildBatchReport {
678 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679 let metrics = self.metrics();
680 write!(
681 formatter,
682 "builds={} succeeded={} failed={} built={} reused={} input_resolution_runs={} input_resolution_reuses={} input_resolution_session_reuses={} input_resolution_prepared_reuses={} successful_timings=({}) total={:?}",
683 metrics.specifications(),
684 metrics.succeeded(),
685 metrics.failed(),
686 metrics.built(),
687 metrics.reused(),
688 metrics.input_resolution_runs(),
689 metrics.input_resolution_reuses(),
690 metrics.input_resolution_session_reuses(),
691 metrics.input_resolution_prepared_reuses(),
692 metrics.successful_timings(),
693 metrics.total(),
694 )
695 }
696}
697
698pub fn build_wasm_canisters_cached_batch(
709 specs: &[LabeledWasmBuildSpec],
710) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
711 build_wasm_canisters_cached_batch_with_config(specs, WasmBuildBatchConfig::new())
712}
713
714pub fn build_wasm_canisters_cached_batch_with_config(
725 specs: &[LabeledWasmBuildSpec],
726 config: WasmBuildBatchConfig,
727) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
728 run_wasm_batch(specs, config, None, None)
729}
730
731pub 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>(
762 specs: &[LabeledWasmBuildSpec],
763 batch_config: WasmBuildBatchConfig,
764 progress_config: WasmBuildProgressConfig,
765 mut observer: F,
766) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError>
767where
768 F: FnMut(WasmBuildBatchProgressEvent),
769{
770 run_wasm_batch(
771 specs,
772 batch_config,
773 None,
774 Some((progress_config, &mut observer)),
775 )
776}
777
778fn run_wasm_batch(
779 specs: &[LabeledWasmBuildSpec],
780 batch_config: WasmBuildBatchConfig,
781 reuse: Option<WasmBuildInputReuse<'_>>,
782 mut observation: Option<(
783 WasmBuildProgressConfig,
784 &mut dyn FnMut(WasmBuildBatchProgressEvent),
785 )>,
786) -> Result<WasmBuildBatchReport, WasmBuildBatchContractError> {
787 validate_batch_labels(specs)?;
788 validate_input_reuse(specs, reuse.as_ref())?;
789 let count = specs.len();
790 let mut resolver =
791 WasmBuildBatchInputResolver::new(specs.iter().map(LabeledWasmBuildSpec::spec), reuse);
792 let mut report = build_wasm_batch(specs, batch_config, |spec, index| {
793 let Some((progress_config, observer)) = observation.as_mut() else {
794 return build_wasm_canisters_cached_in_batch(spec, index, &mut resolver);
795 };
796 let label = specs[index].label.clone();
797 observer(WasmBuildBatchProgressEvent::BuildStarted {
798 index,
799 label: label.clone(),
800 total: count,
801 });
802 let attempt = build_wasm_canisters_cached_in_batch_with_progress(
803 spec,
804 index,
805 &mut resolver,
806 *progress_config,
807 |event| {
808 observer(WasmBuildBatchProgressEvent::BuildProgress {
809 index,
810 label: label.clone(),
811 event,
812 });
813 },
814 );
815 observer(match &attempt.result {
816 Ok(_) => WasmBuildBatchProgressEvent::BuildFinished { index, label },
817 Err(_) => WasmBuildBatchProgressEvent::BuildFailed { index, label },
818 });
819 attempt
820 });
821 report.input_resolution = resolver.metrics();
822 Ok(report)
823}
824
825fn build_wasm_batch<F>(
826 specs: &[LabeledWasmBuildSpec],
827 config: WasmBuildBatchConfig,
828 mut build: F,
829) -> WasmBuildBatchReport
830where
831 F: FnMut(&WasmBuildSpec, usize) -> WasmBuildBatchAttempt,
832{
833 let started = Instant::now();
834 let mut entries = Vec::with_capacity(specs.len());
835 let mut maintenance = BatchMaintenanceTracker::new(config.shared_incremental_maintenance);
836 for (index, labeled) in specs.iter().enumerate() {
837 let entry_started = Instant::now();
838 let spec = &labeled.spec;
839 let result = if config.shared_incremental_maintenance.is_some()
840 && spec.shared_incremental_target_maintenance().is_some()
841 {
842 Err((
843 batch_maintenance_ownership_error(),
844 WasmBuildFailureDetails::specification(entry_started.elapsed()),
845 ))
846 } else {
847 match maintenance.prepare_spec(spec) {
848 Ok(configured) => build(configured.as_ref().unwrap_or(spec), index).result,
849 Err(error) => Err((
850 error,
851 WasmBuildFailureDetails::specification(entry_started.elapsed()),
852 )),
853 }
854 };
855 entries.push(WasmBuildBatchEntry {
856 index,
857 label: labeled.label.clone(),
858 result,
859 entry_elapsed: entry_started.elapsed(),
860 });
861 }
862 WasmBuildBatchReport {
863 entries,
864 input_resolution: WasmBuildBatchInputMetrics::default(),
865 total: started.elapsed(),
866 }
867}
868
869fn validate_batch_labels(
870 specs: &[LabeledWasmBuildSpec],
871) -> Result<(), WasmBuildBatchContractError> {
872 validate_labels(specs.iter().map(|labeled| labeled.label.as_str())).map_err(|error| match error
873 {
874 BatchLabelError::Empty { index } => WasmBuildBatchContractError::EmptyLabel { index },
875 BatchLabelError::Duplicate {
876 label,
877 first_index,
878 duplicate_index,
879 } => WasmBuildBatchContractError::DuplicateLabel {
880 label,
881 first_index,
882 duplicate_index,
883 },
884 })
885}
886
887fn validate_input_reuse(
888 specs: &[LabeledWasmBuildSpec],
889 reuse: Option<&WasmBuildInputReuse<'_>>,
890) -> Result<(), WasmBuildBatchContractError> {
891 match reuse {
892 Some(WasmBuildInputReuse::Session(session)) if session.is_invalidated() => {
893 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
894 }
895 Some(WasmBuildInputReuse::Snapshot(snapshot)) if snapshot.is_invalidated() => {
896 Err(WasmBuildBatchContractError::SourceLeaseInvalidated)
897 }
898 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
899 for (index, labeled) in specs.iter().enumerate() {
900 if !snapshot.contains(&labeled.spec) {
901 return Err(WasmBuildBatchContractError::SpecificationNotPrepared {
902 index,
903 label: labeled.label.clone(),
904 });
905 }
906 }
907 Ok(())
908 }
909 _ => Ok(()),
910 }
911}
912
913struct BatchMaintenanceTracker {
914 config: Option<SharedIncrementalTargetMaintenanceConfig>,
915 configured_targets: HashSet<PathBuf>,
916}
917
918impl BatchMaintenanceTracker {
919 fn new(config: Option<SharedIncrementalTargetMaintenanceConfig>) -> Self {
920 Self {
921 config,
922 configured_targets: HashSet::new(),
923 }
924 }
925
926 fn prepare_spec(
927 &mut self,
928 spec: &WasmBuildSpec,
929 ) -> Result<Option<WasmBuildSpec>, WasmBuildError> {
930 let Some(config) = self.config else {
931 return Ok(None);
932 };
933 debug_assert!(spec.shared_incremental_target_maintenance().is_none());
934 let Some(target_dir) = shared_incremental_target(spec) else {
935 return Ok(None);
936 };
937 let canonical =
938 canonicalize_allow_missing(&target_dir).map_err(|source| WasmBuildError::Io {
939 operation: "resolve batch shared incremental target",
940 path: target_dir,
941 source,
942 })?;
943 if !self.configured_targets.insert(canonical) {
944 return Ok(None);
945 }
946 Ok(Some(
947 spec.clone()
948 .with_shared_incremental_target_maintenance(config),
949 ))
950 }
951}
952
953fn batch_maintenance_ownership_error() -> WasmBuildError {
954 WasmBuildError::InvalidSpec {
955 message:
956 "batch-owned shared-target maintenance cannot be combined with per-spec maintenance"
957 .to_owned(),
958 }
959}
960
961impl std::fmt::Display for WasmBuildBatchContractError {
962 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
963 match self {
964 Self::EmptyLabel { index } => {
965 write!(formatter, "Wasm batch label at index {index} is empty")
966 }
967 Self::DuplicateLabel {
968 label,
969 first_index,
970 duplicate_index,
971 } => write!(
972 formatter,
973 "Wasm batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
974 ),
975 Self::SourceLeaseInvalidated => formatter
976 .write_str("Wasm build source lease was invalidated by a detected input mutation"),
977 Self::SpecificationNotPrepared { index, label } => write!(
978 formatter,
979 "Wasm batch entry {label:?} at index {index} was not declared when the input snapshot was prepared",
980 ),
981 }
982 }
983}
984
985impl std::error::Error for WasmBuildBatchContractError {}
986
987#[cfg(test)]
988mod tests;