1use serde_json::Value;
2use std::{
3 collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
4 ffi::{OsStr, OsString},
5 fs::{self, File},
6 io,
7 path::{Component, Path, PathBuf},
8 process::{Child, Command, ExitStatus, Output, Stdio},
9 sync::{
10 Arc, RwLock,
11 atomic::{AtomicUsize, Ordering},
12 mpsc::{self, RecvTimeoutError},
13 },
14 thread,
15 time::{Duration, Instant, SystemTime},
16};
17use toml::Value as TomlValue;
18
19use crate::timing::saturating_add_optional_duration;
20
21use super::{
22 cache_fs::{
23 ArtifactCacheMaintenance, ArtifactCachePrunePolicy, ArtifactCachePruneReport, CacheFsError,
24 RetainedCacheEntry, cache_entry_last_used, cache_maintenance_due,
25 canonicalize_allow_missing, directory_logical_size,
26 ensure_cache_directory_tag as ensure_cache_tag, is_sha256_directory, lock_cache_file,
27 lock_cache_file_with_wait_observer, perform_scheduled_cache_maintenance,
28 prune_direct_child_directories, record_cache_entry_use as record_entry_use,
29 record_cache_maintenance, remove_path_if_present, remove_unretained_entry,
30 },
31 digest::{
32 FileDigest, InputDigest, InputHasher, LabeledPathDigestCache, copy_file_atomic,
33 destination_matches_bytes, destination_matches_digest, digest_bytes, digest_file,
34 digest_labeled_paths_composable, os_bytes, read_stamp_with_limit, write_atomic,
35 },
36};
37
38const CACHE_FORMAT_VERSION: &str = "ic-testkit-wasm-build-v1";
39const DEFAULT_TARGET: &str = "wasm32-unknown-unknown";
40const AUTOMATIC_ENVIRONMENT: &[&str] = &[
41 "CARGO_BUILD_RUSTC",
42 "CARGO_ENCODED_RUSTFLAGS",
43 "RUSTC",
44 "RUSTC_WRAPPER",
45 "RUSTC_WORKSPACE_WRAPPER",
46 "RUSTFLAGS",
47 "RUSTUP_TOOLCHAIN",
48];
49
50#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct WasmBuildSpec {
59 workspace_root: PathBuf,
60 target_dir: PathBuf,
61 packages: Vec<String>,
62 profile_target_dir: String,
63 cargo_profile_args: Vec<OsString>,
64 extra_env: BTreeMap<OsString, OsString>,
65 inherited_env: BTreeSet<OsString>,
66 additional_inputs: Vec<PathBuf>,
67 target: String,
68 cargo_program: OsString,
69 rustc_program: OsString,
70 cache_mode: WasmBuildCacheMode,
71 prune_policy: Option<ArtifactCachePrunePolicy>,
72 prune_interval: Option<Duration>,
73 shared_incremental_maintenance_config: Option<SharedIncrementalTargetMaintenanceConfig>,
74}
75
76#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
78pub enum SharedIncrementalTargetMaintenanceFailureMode {
79 #[default]
81 Strict,
82 BestEffort,
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub struct SharedIncrementalTargetMaintenanceConfig {
89 policy: SharedIncrementalTargetPrunePolicy,
90 minimum_interval: Duration,
91 failure_mode: SharedIncrementalTargetMaintenanceFailureMode,
92}
93
94#[non_exhaustive]
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub enum WasmBuildCacheMode {
98 Isolated,
100 SharedIncremental {
102 target_dir: PathBuf,
104 },
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
109pub enum WasmBuildOutcome {
110 Built(WasmBuildRecord),
112 Reused(WasmBuildRecord),
114}
115
116#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct WasmBuildRecord {
119 fingerprint: InputDigest,
120 input_digest: InputDigest,
121 retention: RetainedCacheEntry,
122 artifacts: Vec<PathBuf>,
123 timings: WasmBuildTimings,
124 maintenance: Option<ArtifactCacheMaintenance>,
125 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
126}
127
128#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
130pub struct WasmBuildTimings {
131 lock_wait: Duration,
132 shared_incremental_lock_wait: Option<Duration>,
133 input_resolution: WasmInputResolutionTimings,
134 cargo_build: Option<Duration>,
135 cache_maintenance: Option<Duration>,
136 total: Duration,
137}
138
139#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
141pub struct WasmInputResolutionTimings {
142 tool_identity: Duration,
143 cargo_metadata: Duration,
144 input_discovery: Duration,
145 content_hashing: Duration,
146 total: Duration,
147}
148
149#[non_exhaustive]
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub enum WasmBuildFailurePhase {
153 Specification,
155 ExactCacheCoordination,
157 ToolIdentity,
159 CargoMetadata,
161 InputDiscovery,
163 ContentHashing,
165 SharedTargetCoordination,
167 SharedTargetMaintenance,
169 CargoBuild,
171 ArtifactPublication,
173 ExactCacheMaintenance,
175 Cleanup,
177}
178
179#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
181pub struct WasmBuildFailureTimings {
182 exact_cache_coordination: Duration,
183 shared_target_coordination: Option<Duration>,
184 input_resolution: WasmInputResolutionTimings,
185 shared_target_maintenance: Option<Duration>,
186 cargo_build: Option<Duration>,
187 artifact_publication: Option<Duration>,
188 exact_cache_maintenance: Option<Duration>,
189 cleanup: Option<Duration>,
190 total: Duration,
191}
192
193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub struct WasmBuildFailureDetails {
196 phase: WasmBuildFailurePhase,
197 timings: WasmBuildFailureTimings,
198}
199
200#[derive(Clone, Debug, Eq, PartialEq)]
202pub struct CargoBuildInput {
203 label: PathBuf,
204 path: PathBuf,
205}
206
207#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct ResolvedCargoBuildInputs {
215 fingerprint: InputDigest,
216 input_digest: InputDigest,
217 validation_digest: InputDigest,
218 inputs: Arc<[CargoBuildInput]>,
219 exclusions: Arc<[PathBuf]>,
220 timings: WasmInputResolutionTimings,
221}
222
223pub(super) enum WasmBuildInputReuse<'reuse> {
224 Session(&'reuse mut WasmBuildSessionState),
225 Snapshot(&'reuse WasmBuildInputSnapshotState),
226}
227
228pub(super) struct WasmBuildBatchInputResolver<'a, 'session> {
229 specs: Vec<&'a WasmBuildSpec>,
230 groups: Vec<BatchResolutionGroup>,
231 group_by_index: Vec<usize>,
232 resolved:
233 Vec<Option<Result<ResolvedCargoBuildInputs, (WasmBuildFailurePhase, WasmBuildError)>>>,
234 reuse: Option<WasmBuildInputReuse<'session>>,
235 metrics: WasmBuildBatchInputMetrics,
236}
237
238pub(super) struct WasmBuildSessionState {
239 snapshots: Vec<(WasmBuildSpec, ResolvedCargoBuildInputs)>,
240 digest_cache: LabeledPathDigestCache,
241 snapshot_reuses: usize,
242 invalidated: bool,
243}
244
245pub(super) struct WasmBuildInputSnapshotState {
246 snapshots: Vec<(WasmBuildSpec, ResolvedCargoBuildInputs)>,
247 preparation_metrics: WasmBuildBatchInputMetrics,
248 preparation_timings: WasmInputResolutionTimings,
249 reader_reuses: AtomicUsize,
250 invalidation: Arc<RwLock<bool>>,
251}
252
253pub(super) struct WasmBuildBatchAttempt {
255 pub(super) result: Result<WasmBuildOutcome, (WasmBuildError, WasmBuildFailureDetails)>,
256}
257
258struct BatchResolutionGroup {
259 indexes: Vec<usize>,
260}
261
262struct ResolvedLocalInputs {
263 validation_inputs: Vec<(PathBuf, PathBuf)>,
264 workspace_projection: Option<InputDigest>,
265}
266
267#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
268pub(super) struct WasmBuildBatchInputMetrics {
269 pub(super) runs: usize,
270 pub(super) reuses: usize,
271 pub(super) session_reuses: usize,
272 pub(super) prepared_reuses: usize,
273}
274
275#[derive(Eq, PartialEq)]
276struct BatchResolutionKey<'a> {
277 workspace_root: &'a Path,
278 cargo_program: &'a OsStr,
279 rustc_program: &'a OsStr,
280 metadata_arguments: Vec<OsString>,
281 environment: BTreeMap<OsString, Option<OsString>>,
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
286pub struct SharedIncrementalTargetInspection {
287 target_dir: PathBuf,
288 logical_size_bytes: u64,
289 last_used: SystemTime,
290 lock_wait: Duration,
291}
292
293#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
301pub struct SharedIncrementalTargetPrunePolicy {
302 max_age: Option<Duration>,
303 max_size_bytes: Option<u64>,
304}
305
306#[derive(Clone, Debug, Eq, PartialEq)]
308pub struct SharedIncrementalTargetMaintenance {
309 target_dir: PathBuf,
310 logical_size_bytes_before: u64,
311 logical_size_bytes_after: u64,
312 last_used_before: SystemTime,
313 cleared: bool,
314 lock_wait: Duration,
315 maintenance: Duration,
316}
317
318#[non_exhaustive]
320#[derive(Clone, Debug, Eq, PartialEq)]
321pub enum SharedIncrementalTargetMaintenanceOutcome {
322 Missing {
324 target_dir: PathBuf,
326 },
327 Skipped {
329 target_dir: PathBuf,
331 lock_wait: Duration,
333 schedule_check: Duration,
335 },
336 Performed {
338 maintenance: SharedIncrementalTargetMaintenance,
340 schedule_check: Duration,
342 },
343 Failed {
345 target_dir: PathBuf,
347 lock_wait: Duration,
349 message: String,
351 },
352}
353
354#[derive(Clone, Copy, Debug, Eq, PartialEq)]
356pub struct WasmBuildProgressConfig {
357 heartbeat_interval: Option<Duration>,
358 emit_cargo_output: bool,
359}
360
361#[derive(Clone, Copy, Debug, Eq, PartialEq)]
363pub enum WasmBuildOutputStream {
364 Stdout,
366 Stderr,
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq)]
372pub enum WasmBuildProgressOutcome {
373 Built,
375 Reused,
377}
378
379#[non_exhaustive]
381#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub enum WasmBuildProgressPhase {
383 ExactCacheLock,
385 CargoIdentity,
387 RustcIdentity,
389 CargoMetadata,
391 InputDiscovery,
393 ContentHashing,
395 SharedTargetLock,
397 SharedTargetMaintenance,
399 CargoBuild,
401 ArtifactPublication,
403 ExactCacheMaintenance,
405}
406
407#[non_exhaustive]
409#[derive(Clone, Debug, Eq, PartialEq)]
410pub enum WasmBuildProgressEvent {
411 Started,
413 InputsResolved {
415 fingerprint: InputDigest,
417 input_digest: InputDigest,
419 elapsed: Duration,
421 },
422 CacheMiss {
424 fingerprint: InputDigest,
426 },
427 CacheHit {
429 fingerprint: InputDigest,
431 },
432 SharedTargetLockStarted {
434 target_dir: PathBuf,
436 },
437 SharedTargetLockAcquired {
439 target_dir: PathBuf,
441 wait: Duration,
443 },
444 SharedTargetMaintenanceStarted {
446 target_dir: PathBuf,
448 },
449 SharedTargetMaintenanceFinished {
451 outcome: SharedIncrementalTargetMaintenanceOutcome,
453 },
454 CargoStarted {
456 target_dir: PathBuf,
458 },
459 CargoOutput {
461 stream: WasmBuildOutputStream,
463 bytes: Vec<u8>,
465 },
466 Heartbeat {
468 phase: WasmBuildProgressPhase,
470 elapsed: Duration,
472 },
473 CargoFinished {
475 success: bool,
477 code: Option<i32>,
479 elapsed: Duration,
481 },
482 Finished {
484 outcome: WasmBuildProgressOutcome,
486 fingerprint: InputDigest,
488 elapsed: Duration,
490 },
491}
492
493impl Default for WasmBuildProgressConfig {
494 fn default() -> Self {
495 Self {
496 heartbeat_interval: Some(Duration::from_secs(10)),
497 emit_cargo_output: true,
498 }
499 }
500}
501
502impl WasmBuildProgressConfig {
503 #[must_use]
505 pub fn new() -> Self {
506 Self::default()
507 }
508
509 #[must_use]
513 pub const fn with_heartbeat_interval(mut self, interval: Duration) -> Self {
514 self.heartbeat_interval = Some(interval);
515 self
516 }
517
518 #[must_use]
520 pub const fn without_heartbeats(mut self) -> Self {
521 self.heartbeat_interval = None;
522 self
523 }
524
525 #[must_use]
531 pub const fn with_cargo_output(mut self, emit: bool) -> Self {
532 self.emit_cargo_output = emit;
533 self
534 }
535
536 #[must_use]
538 pub const fn heartbeat_interval(self) -> Option<Duration> {
539 self.heartbeat_interval
540 }
541
542 #[must_use]
544 pub const fn emits_cargo_output(self) -> bool {
545 self.emit_cargo_output
546 }
547}
548
549struct ProgressReporter<'a> {
550 config: WasmBuildProgressConfig,
551 observer: Option<&'a mut dyn FnMut(WasmBuildProgressEvent)>,
552 last_event: Instant,
553 failure_phase: Option<WasmBuildFailurePhase>,
554 failure_timings: WasmBuildFailureTimings,
555}
556
557impl ProgressReporter<'_> {
558 fn silent() -> Self {
559 Self {
560 config: WasmBuildProgressConfig {
561 heartbeat_interval: None,
562 emit_cargo_output: false,
563 },
564 observer: None,
565 last_event: Instant::now(),
566 failure_phase: None,
567 failure_timings: WasmBuildFailureTimings::default(),
568 }
569 }
570
571 fn observed(
572 config: WasmBuildProgressConfig,
573 observer: &'_ mut dyn FnMut(WasmBuildProgressEvent),
574 ) -> ProgressReporter<'_> {
575 ProgressReporter {
576 config,
577 observer: Some(observer),
578 last_event: Instant::now(),
579 failure_phase: None,
580 failure_timings: WasmBuildFailureTimings::default(),
581 }
582 }
583
584 fn emit(&mut self, event: WasmBuildProgressEvent) {
585 if let Some(observer) = &mut self.observer {
586 observer(event);
587 self.last_event = Instant::now();
588 }
589 }
590
591 const fn is_observed(&self) -> bool {
592 self.observer.is_some()
593 }
594
595 fn heartbeat_due_in(&self) -> Option<Duration> {
596 self.config
597 .heartbeat_interval
598 .map(|interval| interval.saturating_sub(self.last_event.elapsed()))
599 }
600
601 fn emit_heartbeat(&mut self, phase: WasmBuildProgressPhase, elapsed: Duration) {
602 self.emit(WasmBuildProgressEvent::Heartbeat { phase, elapsed });
603 }
604
605 fn emit_heartbeat_if_due(&mut self, phase: WasmBuildProgressPhase, elapsed: Duration) {
606 if self.heartbeat_due_in() == Some(Duration::ZERO) {
607 self.emit_heartbeat(phase, elapsed);
608 }
609 }
610
611 fn run_phase<T, F>(&mut self, phase: WasmBuildProgressPhase, operation: F) -> T
612 where
613 T: Send,
614 F: FnOnce() -> T + Send,
615 {
616 let started = Instant::now();
617 self.begin_phase(progress_failure_phase(phase));
618 let result = if !self.is_observed() || self.config.heartbeat_interval.is_none() {
619 operation()
620 } else {
621 thread::scope(|scope| {
622 let (finished, completion) = mpsc::sync_channel(0);
623 let worker = scope.spawn(move || {
624 let result = operation();
625 let _ = finished.send(());
626 result
627 });
628 loop {
629 let wait = self
630 .heartbeat_due_in()
631 .expect("observed phase must have a heartbeat interval");
632 match completion.recv_timeout(wait) {
633 Ok(()) | Err(RecvTimeoutError::Disconnected) => {
634 return worker
635 .join()
636 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
637 }
638 Err(RecvTimeoutError::Timeout) => {
639 self.emit_heartbeat(phase, started.elapsed());
640 }
641 }
642 }
643 })
644 };
645 self.record_phase(progress_failure_phase(phase), started.elapsed());
646 result
647 }
648
649 const fn begin_phase(&mut self, phase: WasmBuildFailurePhase) {
650 self.failure_phase = Some(phase);
651 }
652
653 fn record_phase(&mut self, phase: WasmBuildFailurePhase, elapsed: Duration) {
654 self.failure_phase = Some(phase);
655 let timings = &mut self.failure_timings;
656 match phase {
657 WasmBuildFailurePhase::Specification => {}
658 WasmBuildFailurePhase::ExactCacheCoordination => {
659 timings.exact_cache_coordination =
660 timings.exact_cache_coordination.saturating_add(elapsed);
661 }
662 WasmBuildFailurePhase::ToolIdentity => {
663 timings.input_resolution.tool_identity = timings
664 .input_resolution
665 .tool_identity
666 .saturating_add(elapsed);
667 timings.input_resolution.total =
668 timings.input_resolution.total.saturating_add(elapsed);
669 }
670 WasmBuildFailurePhase::CargoMetadata => {
671 timings.input_resolution.cargo_metadata = timings
672 .input_resolution
673 .cargo_metadata
674 .saturating_add(elapsed);
675 timings.input_resolution.total =
676 timings.input_resolution.total.saturating_add(elapsed);
677 }
678 WasmBuildFailurePhase::InputDiscovery => {
679 timings.input_resolution.input_discovery = timings
680 .input_resolution
681 .input_discovery
682 .saturating_add(elapsed);
683 timings.input_resolution.total =
684 timings.input_resolution.total.saturating_add(elapsed);
685 }
686 WasmBuildFailurePhase::ContentHashing => {
687 timings.input_resolution.content_hashing = timings
688 .input_resolution
689 .content_hashing
690 .saturating_add(elapsed);
691 timings.input_resolution.total =
692 timings.input_resolution.total.saturating_add(elapsed);
693 }
694 WasmBuildFailurePhase::SharedTargetCoordination => {
695 timings.shared_target_coordination = Some(
696 timings
697 .shared_target_coordination
698 .unwrap_or_default()
699 .saturating_add(elapsed),
700 );
701 }
702 WasmBuildFailurePhase::SharedTargetMaintenance => {
703 timings.shared_target_maintenance = Some(
704 timings
705 .shared_target_maintenance
706 .unwrap_or_default()
707 .saturating_add(elapsed),
708 );
709 }
710 WasmBuildFailurePhase::CargoBuild => {
711 timings.cargo_build = Some(
712 timings
713 .cargo_build
714 .unwrap_or_default()
715 .saturating_add(elapsed),
716 );
717 }
718 WasmBuildFailurePhase::ArtifactPublication => {
719 timings.artifact_publication = Some(
720 timings
721 .artifact_publication
722 .unwrap_or_default()
723 .saturating_add(elapsed),
724 );
725 }
726 WasmBuildFailurePhase::ExactCacheMaintenance => {
727 timings.exact_cache_maintenance = Some(
728 timings
729 .exact_cache_maintenance
730 .unwrap_or_default()
731 .saturating_add(elapsed),
732 );
733 }
734 WasmBuildFailurePhase::Cleanup => {
735 timings.cleanup = Some(timings.cleanup.unwrap_or_default().saturating_add(elapsed));
736 }
737 }
738 }
739
740 fn failure_details(&self, error: &WasmBuildError, total: Duration) -> WasmBuildFailureDetails {
741 let phase = self
742 .failure_phase
743 .unwrap_or_else(|| classify_unobserved_failure(error));
744 let mut timings = self.failure_timings;
745 timings.total = total;
746 WasmBuildFailureDetails { phase, timings }
747 }
748}
749
750const fn progress_failure_phase(phase: WasmBuildProgressPhase) -> WasmBuildFailurePhase {
751 match phase {
752 WasmBuildProgressPhase::ExactCacheLock => WasmBuildFailurePhase::ExactCacheCoordination,
753 WasmBuildProgressPhase::CargoIdentity | WasmBuildProgressPhase::RustcIdentity => {
754 WasmBuildFailurePhase::ToolIdentity
755 }
756 WasmBuildProgressPhase::CargoMetadata => WasmBuildFailurePhase::CargoMetadata,
757 WasmBuildProgressPhase::InputDiscovery => WasmBuildFailurePhase::InputDiscovery,
758 WasmBuildProgressPhase::ContentHashing => WasmBuildFailurePhase::ContentHashing,
759 WasmBuildProgressPhase::SharedTargetLock => WasmBuildFailurePhase::SharedTargetCoordination,
760 WasmBuildProgressPhase::SharedTargetMaintenance => {
761 WasmBuildFailurePhase::SharedTargetMaintenance
762 }
763 WasmBuildProgressPhase::CargoBuild => WasmBuildFailurePhase::CargoBuild,
764 WasmBuildProgressPhase::ArtifactPublication => WasmBuildFailurePhase::ArtifactPublication,
765 WasmBuildProgressPhase::ExactCacheMaintenance => {
766 WasmBuildFailurePhase::ExactCacheMaintenance
767 }
768 }
769}
770
771const fn classify_unobserved_failure(error: &WasmBuildError) -> WasmBuildFailurePhase {
772 match error {
773 WasmBuildError::InvalidSpec { .. } => WasmBuildFailurePhase::Specification,
774 WasmBuildError::CommandSpawn { phase, .. }
775 | WasmBuildError::CommandFailed { phase, .. } => match phase {
776 WasmBuildPhase::CargoIdentity | WasmBuildPhase::RustcIdentity => {
777 WasmBuildFailurePhase::ToolIdentity
778 }
779 WasmBuildPhase::CargoMetadata => WasmBuildFailurePhase::CargoMetadata,
780 WasmBuildPhase::CargoBuild => WasmBuildFailurePhase::CargoBuild,
781 },
782 WasmBuildError::InvalidMetadata { .. } => WasmBuildFailurePhase::CargoMetadata,
783 WasmBuildError::InvalidCargoConfiguration { .. } => WasmBuildFailurePhase::InputDiscovery,
784 WasmBuildError::MissingArtifacts { .. } => WasmBuildFailurePhase::ArtifactPublication,
785 WasmBuildError::InputsChangedDuringAcquisition { .. } => {
786 WasmBuildFailurePhase::ContentHashing
787 }
788 WasmBuildError::PreparedInputSnapshotInvalidated => {
789 WasmBuildFailurePhase::ArtifactPublication
790 }
791 WasmBuildError::FailedBuildCleanup { .. } => WasmBuildFailurePhase::Cleanup,
792 WasmBuildError::Io { .. } => WasmBuildFailurePhase::ExactCacheCoordination,
793 }
794}
795
796#[non_exhaustive]
798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
799pub enum WasmBuildPhase {
800 CargoMetadata,
802 CargoIdentity,
804 RustcIdentity,
806 CargoBuild,
808}
809
810#[non_exhaustive]
812#[derive(Debug)]
813pub enum WasmBuildError {
814 InvalidSpec { message: String },
816 Io {
818 operation: &'static str,
819 path: PathBuf,
820 source: io::Error,
821 },
822 CommandSpawn {
824 phase: WasmBuildPhase,
825 program: OsString,
826 source: io::Error,
827 },
828 CommandFailed {
830 phase: WasmBuildPhase,
831 status: ExitStatus,
832 stdout: String,
833 stderr: String,
834 },
835 InvalidMetadata { message: String },
837 InvalidCargoConfiguration { path: PathBuf, message: String },
839 MissingArtifacts { paths: Vec<PathBuf> },
841 InputsChangedDuringAcquisition {
843 before: InputDigest,
844 after: InputDigest,
845 },
846 PreparedInputSnapshotInvalidated,
848 FailedBuildCleanup {
850 build_error: Box<Self>,
851 path: PathBuf,
852 source: io::Error,
853 },
854}
855
856impl WasmBuildSpec {
857 #[must_use]
871 pub fn new(
872 workspace_root: &Path,
873 target_dir: &Path,
874 packages: &[&str],
875 profile_target_dir: &str,
876 ) -> Self {
877 let mut packages = packages
878 .iter()
879 .map(|package| (*package).to_owned())
880 .collect::<Vec<_>>();
881 packages.sort();
882 packages.dedup();
883 Self {
884 workspace_root: workspace_root.to_owned(),
885 target_dir: target_dir.to_owned(),
886 packages,
887 profile_target_dir: profile_target_dir.to_owned(),
888 cargo_profile_args: Vec::new(),
889 extra_env: BTreeMap::new(),
890 inherited_env: BTreeSet::new(),
891 additional_inputs: Vec::new(),
892 target: DEFAULT_TARGET.to_owned(),
893 cargo_program: std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()),
894 rustc_program: std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()),
895 cache_mode: WasmBuildCacheMode::Isolated,
896 prune_policy: None,
897 prune_interval: None,
898 shared_incremental_maintenance_config: None,
899 }
900 }
901
902 #[must_use]
916 pub fn with_cargo_profile_args<I, S>(mut self, arguments: I) -> Self
917 where
918 I: IntoIterator<Item = S>,
919 S: AsRef<OsStr>,
920 {
921 self.cargo_profile_args = arguments
922 .into_iter()
923 .map(|argument| argument.as_ref().to_owned())
924 .collect();
925 self
926 }
927
928 #[must_use]
933 pub fn with_extra_env<I, K, V>(mut self, environment: I) -> Self
934 where
935 I: IntoIterator<Item = (K, V)>,
936 K: Into<OsString>,
937 V: Into<OsString>,
938 {
939 self.extra_env = environment
940 .into_iter()
941 .map(|(key, value)| (key.into(), value.into()))
942 .collect();
943 self
944 }
945
946 #[must_use]
951 pub fn with_inherited_env<I, S>(mut self, names: I) -> Self
952 where
953 I: IntoIterator<Item = S>,
954 S: Into<OsString>,
955 {
956 self.inherited_env.extend(names.into_iter().map(Into::into));
957 self
958 }
959
960 #[must_use]
965 pub fn with_additional_inputs<I, P>(mut self, paths: I) -> Self
966 where
967 I: IntoIterator<Item = P>,
968 P: Into<PathBuf>,
969 {
970 self.additional_inputs
971 .extend(paths.into_iter().map(Into::into));
972 self
973 }
974
975 #[must_use]
977 pub fn with_target(mut self, target: &str) -> Self {
978 target.clone_into(&mut self.target);
979 self
980 }
981
982 #[must_use]
984 pub fn with_cargo_program(mut self, program: impl Into<OsString>) -> Self {
985 self.cargo_program = program.into();
986 self
987 }
988
989 #[must_use]
991 pub fn with_rustc_program(mut self, program: impl Into<OsString>) -> Self {
992 self.rustc_program = program.into();
993 self
994 }
995
996 #[must_use]
1002 pub fn with_shared_incremental_target(mut self, target_dir: impl Into<PathBuf>) -> Self {
1003 self.cache_mode = WasmBuildCacheMode::SharedIncremental {
1004 target_dir: target_dir.into(),
1005 };
1006 self
1007 }
1008
1009 #[must_use]
1020 pub const fn with_shared_incremental_target_maintenance_at_most_every(
1021 mut self,
1022 policy: SharedIncrementalTargetPrunePolicy,
1023 minimum_interval: Duration,
1024 ) -> Self {
1025 self.shared_incremental_maintenance_config = Some(
1026 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
1027 );
1028 self
1029 }
1030
1031 #[must_use]
1037 pub const fn with_shared_incremental_target_maintenance(
1038 mut self,
1039 config: SharedIncrementalTargetMaintenanceConfig,
1040 ) -> Self {
1041 self.shared_incremental_maintenance_config = Some(config);
1042 self
1043 }
1044
1045 #[must_use]
1051 pub const fn with_prune_policy(mut self, policy: ArtifactCachePrunePolicy) -> Self {
1052 self.prune_policy = Some(policy);
1053 self.prune_interval = None;
1054 self
1055 }
1056
1057 #[must_use]
1064 pub const fn with_prune_policy_at_most_every(
1065 mut self,
1066 policy: ArtifactCachePrunePolicy,
1067 minimum_interval: Duration,
1068 ) -> Self {
1069 self.prune_policy = Some(policy);
1070 self.prune_interval = Some(minimum_interval);
1071 self
1072 }
1073
1074 #[must_use]
1076 pub fn workspace_root(&self) -> &Path {
1077 &self.workspace_root
1078 }
1079
1080 #[must_use]
1082 pub fn target_dir(&self) -> &Path {
1083 &self.target_dir
1084 }
1085
1086 #[must_use]
1088 pub fn packages(&self) -> &[String] {
1089 &self.packages
1090 }
1091
1092 #[must_use]
1094 pub const fn cache_mode(&self) -> &WasmBuildCacheMode {
1095 &self.cache_mode
1096 }
1097
1098 #[must_use]
1100 pub const fn prune_policy(&self) -> Option<ArtifactCachePrunePolicy> {
1101 self.prune_policy
1102 }
1103
1104 #[must_use]
1106 pub const fn prune_interval(&self) -> Option<Duration> {
1107 self.prune_interval
1108 }
1109
1110 #[must_use]
1112 pub const fn shared_incremental_target_maintenance(
1113 &self,
1114 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
1115 self.shared_incremental_maintenance_config
1116 }
1117}
1118
1119impl WasmBuildOutcome {
1120 #[must_use]
1122 pub const fn record(&self) -> &WasmBuildRecord {
1123 match self {
1124 Self::Built(record) | Self::Reused(record) => record,
1125 }
1126 }
1127
1128 #[must_use]
1130 pub const fn is_reused(&self) -> bool {
1131 matches!(self, Self::Reused(_))
1132 }
1133}
1134
1135impl WasmBuildRecord {
1136 #[must_use]
1138 pub const fn fingerprint(&self) -> InputDigest {
1139 self.fingerprint
1140 }
1141
1142 #[must_use]
1144 pub const fn input_digest(&self) -> InputDigest {
1145 self.input_digest
1146 }
1147
1148 #[must_use]
1155 pub fn exact_cache_path(&self) -> &Path {
1156 self.retention.path()
1157 }
1158
1159 #[must_use]
1164 pub fn artifacts(&self) -> &[PathBuf] {
1165 &self.artifacts
1166 }
1167
1168 #[must_use]
1170 pub const fn timings(&self) -> WasmBuildTimings {
1171 self.timings
1172 }
1173
1174 #[must_use]
1176 pub const fn maintenance(&self) -> Option<&ArtifactCacheMaintenance> {
1177 self.maintenance.as_ref()
1178 }
1179
1180 #[must_use]
1182 pub const fn shared_incremental_maintenance(
1183 &self,
1184 ) -> Option<&SharedIncrementalTargetMaintenanceOutcome> {
1185 self.shared_incremental_maintenance.as_ref()
1186 }
1187}
1188
1189impl WasmBuildTimings {
1190 #[must_use]
1192 pub const fn lock_wait(self) -> Duration {
1193 self.lock_wait
1194 }
1195
1196 #[must_use]
1198 pub const fn shared_incremental_lock_wait(self) -> Option<Duration> {
1199 self.shared_incremental_lock_wait
1200 }
1201
1202 #[must_use]
1204 pub const fn input_resolution(self) -> WasmInputResolutionTimings {
1205 self.input_resolution
1206 }
1207
1208 #[must_use]
1210 pub const fn cargo_build(self) -> Option<Duration> {
1211 self.cargo_build
1212 }
1213
1214 #[must_use]
1216 pub const fn cache_maintenance(self) -> Option<Duration> {
1217 self.cache_maintenance
1218 }
1219
1220 #[must_use]
1222 pub const fn total(self) -> Duration {
1223 self.total
1224 }
1225
1226 pub(super) const fn saturating_add(self, other: Self) -> Self {
1227 let mut input_resolution = self.input_resolution;
1228 input_resolution.include(other.input_resolution);
1229 Self {
1230 lock_wait: self.lock_wait.saturating_add(other.lock_wait),
1231 shared_incremental_lock_wait: saturating_add_optional_duration(
1232 self.shared_incremental_lock_wait,
1233 other.shared_incremental_lock_wait,
1234 ),
1235 input_resolution,
1236 cargo_build: saturating_add_optional_duration(self.cargo_build, other.cargo_build),
1237 cache_maintenance: saturating_add_optional_duration(
1238 self.cache_maintenance,
1239 other.cache_maintenance,
1240 ),
1241 total: self.total.saturating_add(other.total),
1242 }
1243 }
1244}
1245
1246impl WasmInputResolutionTimings {
1247 #[must_use]
1249 pub const fn tool_identity(self) -> Duration {
1250 self.tool_identity
1251 }
1252
1253 #[must_use]
1255 pub const fn cargo_metadata(self) -> Duration {
1256 self.cargo_metadata
1257 }
1258
1259 #[must_use]
1261 pub const fn input_discovery(self) -> Duration {
1262 self.input_discovery
1263 }
1264
1265 #[must_use]
1267 pub const fn content_hashing(self) -> Duration {
1268 self.content_hashing
1269 }
1270
1271 #[must_use]
1273 pub const fn total(self) -> Duration {
1274 self.total
1275 }
1276
1277 const fn include(&mut self, other: Self) {
1278 self.tool_identity = self.tool_identity.saturating_add(other.tool_identity);
1279 self.cargo_metadata = self.cargo_metadata.saturating_add(other.cargo_metadata);
1280 self.input_discovery = self.input_discovery.saturating_add(other.input_discovery);
1281 self.content_hashing = self.content_hashing.saturating_add(other.content_hashing);
1282 self.total = self.total.saturating_add(other.total);
1283 }
1284}
1285
1286impl WasmBuildFailureDetails {
1287 pub(super) fn specification(total: Duration) -> Self {
1288 Self {
1289 phase: WasmBuildFailurePhase::Specification,
1290 timings: WasmBuildFailureTimings {
1291 total,
1292 ..WasmBuildFailureTimings::default()
1293 },
1294 }
1295 }
1296
1297 #[must_use]
1299 pub const fn phase(self) -> WasmBuildFailurePhase {
1300 self.phase
1301 }
1302
1303 #[must_use]
1305 pub const fn timings(self) -> WasmBuildFailureTimings {
1306 self.timings
1307 }
1308}
1309
1310impl WasmBuildFailureTimings {
1311 #[must_use]
1313 pub const fn exact_cache_coordination(self) -> Duration {
1314 self.exact_cache_coordination
1315 }
1316
1317 #[must_use]
1319 pub const fn shared_target_coordination(self) -> Option<Duration> {
1320 self.shared_target_coordination
1321 }
1322
1323 #[must_use]
1325 pub const fn input_resolution(self) -> WasmInputResolutionTimings {
1326 self.input_resolution
1327 }
1328
1329 #[must_use]
1331 pub const fn shared_target_maintenance(self) -> Option<Duration> {
1332 self.shared_target_maintenance
1333 }
1334
1335 #[must_use]
1337 pub const fn cargo_build(self) -> Option<Duration> {
1338 self.cargo_build
1339 }
1340
1341 #[must_use]
1343 pub const fn artifact_publication(self) -> Option<Duration> {
1344 self.artifact_publication
1345 }
1346
1347 #[must_use]
1349 pub const fn exact_cache_maintenance(self) -> Option<Duration> {
1350 self.exact_cache_maintenance
1351 }
1352
1353 #[must_use]
1355 pub const fn cleanup(self) -> Option<Duration> {
1356 self.cleanup
1357 }
1358
1359 #[must_use]
1361 pub const fn total(self) -> Duration {
1362 self.total
1363 }
1364}
1365
1366impl CargoBuildInput {
1367 #[must_use]
1369 pub fn label(&self) -> &Path {
1370 &self.label
1371 }
1372
1373 #[must_use]
1378 pub fn path(&self) -> &Path {
1379 &self.path
1380 }
1381}
1382
1383impl ResolvedCargoBuildInputs {
1384 #[must_use]
1386 pub const fn fingerprint(&self) -> InputDigest {
1387 self.fingerprint
1388 }
1389
1390 #[must_use]
1395 pub const fn input_digest(&self) -> InputDigest {
1396 self.input_digest
1397 }
1398
1399 #[must_use]
1404 pub const fn validation_digest(&self) -> InputDigest {
1405 self.validation_digest
1406 }
1407
1408 #[must_use]
1410 pub fn inputs(&self) -> &[CargoBuildInput] {
1411 &self.inputs
1412 }
1413
1414 #[must_use]
1419 pub fn exclusions(&self) -> &[PathBuf] {
1420 &self.exclusions
1421 }
1422
1423 #[must_use]
1425 pub const fn timings(&self) -> WasmInputResolutionTimings {
1426 self.timings
1427 }
1428
1429 pub fn is_current(&self, spec: &WasmBuildSpec) -> Result<bool, WasmBuildError> {
1431 resolve_cargo_build_inputs(spec).map(|current| current.fingerprint == self.fingerprint)
1432 }
1433
1434 pub fn is_content_current(&self) -> Result<bool, WasmBuildError> {
1441 self.current_validation_digest()
1442 .map(|current| current == self.validation_digest)
1443 }
1444
1445 pub(super) fn current_validation_digest(&self) -> Result<InputDigest, WasmBuildError> {
1446 digest_labeled_paths_composable(
1447 "wasm-source-inputs-v1",
1448 self.inputs
1449 .iter()
1450 .map(|input| (input.label.as_path(), input.path.as_path())),
1451 &self.exclusions,
1452 &mut LabeledPathDigestCache::default(),
1453 )
1454 .map_err(|source| WasmBuildError::Io {
1455 operation: "rehash resolved Cargo build inputs",
1456 path: self
1457 .inputs
1458 .first()
1459 .map_or_else(PathBuf::new, |input| input.path.clone()),
1460 source,
1461 })
1462 }
1463}
1464
1465impl WasmBuildSessionState {
1466 pub(super) fn new() -> Self {
1467 Self {
1468 snapshots: Vec::new(),
1469 digest_cache: LabeledPathDigestCache::default(),
1470 snapshot_reuses: 0,
1471 invalidated: false,
1472 }
1473 }
1474
1475 pub(super) const fn snapshot_count(&self) -> usize {
1476 self.snapshots.len()
1477 }
1478
1479 pub(super) const fn snapshot_reuses(&self) -> usize {
1480 self.snapshot_reuses
1481 }
1482
1483 pub(super) const fn is_invalidated(&self) -> bool {
1484 self.invalidated
1485 }
1486
1487 fn reuse(&mut self, spec: &WasmBuildSpec) -> Option<ResolvedCargoBuildInputs> {
1488 let (_, snapshot) = self
1489 .snapshots
1490 .iter()
1491 .find(|(candidate, _)| candidate == spec)?;
1492 self.snapshot_reuses = self.snapshot_reuses.saturating_add(1);
1493 Some(snapshot.clone())
1494 }
1495
1496 fn remember(&mut self, spec: &WasmBuildSpec, resolved: &ResolvedCargoBuildInputs) {
1497 if self
1498 .snapshots
1499 .iter()
1500 .any(|(candidate, _)| candidate == spec)
1501 {
1502 return;
1503 }
1504 let mut snapshot = resolved.clone();
1505 snapshot.timings = WasmInputResolutionTimings::default();
1506 self.snapshots.push((spec.clone(), snapshot));
1507 }
1508
1509 fn invalidate(&mut self) {
1510 self.snapshots.clear();
1511 self.digest_cache = LabeledPathDigestCache::default();
1512 self.invalidated = true;
1513 }
1514}
1515
1516impl WasmBuildInputSnapshotState {
1517 pub(super) fn prepare(specs: &[WasmBuildSpec]) -> Result<Self, WasmBuildError> {
1518 for spec in specs {
1519 validate_spec(spec)?;
1520 }
1521 let mut resolver = WasmBuildBatchInputResolver::new(specs, None);
1522 let mut snapshots = Vec::with_capacity(specs.len());
1523 let mut preparation_timings = WasmInputResolutionTimings::default();
1524 let mut progress = ProgressReporter::silent();
1525 for (index, spec) in specs.iter().enumerate() {
1526 let mut prepared_input = resolver.resolve(index, &mut progress)?;
1527 preparation_timings.include(prepared_input.timings);
1528 prepared_input.timings = WasmInputResolutionTimings::default();
1529 snapshots.push((spec.clone(), prepared_input));
1530 }
1531 Ok(Self {
1532 snapshots,
1533 preparation_metrics: resolver.metrics(),
1534 preparation_timings,
1535 reader_reuses: AtomicUsize::new(0),
1536 invalidation: Arc::new(RwLock::new(false)),
1537 })
1538 }
1539
1540 pub(super) fn contains(&self, spec: &WasmBuildSpec) -> bool {
1541 self.snapshots
1542 .iter()
1543 .any(|(candidate, _)| candidate == spec)
1544 }
1545
1546 fn reuse(&self, spec: &WasmBuildSpec) -> Option<ResolvedCargoBuildInputs> {
1547 let (_, snapshot) = self
1548 .snapshots
1549 .iter()
1550 .find(|(candidate, _)| candidate == spec)?;
1551 let mut current = self.reader_reuses.load(Ordering::Relaxed);
1552 loop {
1553 match self.reader_reuses.compare_exchange_weak(
1554 current,
1555 current.saturating_add(1),
1556 Ordering::Relaxed,
1557 Ordering::Relaxed,
1558 ) {
1559 Ok(_) => break,
1560 Err(observed) => current = observed,
1561 }
1562 }
1563 Some(snapshot.clone())
1564 }
1565
1566 pub(super) const fn specification_count(&self) -> usize {
1567 self.snapshots.len()
1568 }
1569
1570 pub(super) const fn preparation_metrics(&self) -> WasmBuildBatchInputMetrics {
1571 self.preparation_metrics
1572 }
1573
1574 pub(super) const fn preparation_timings(&self) -> WasmInputResolutionTimings {
1575 self.preparation_timings
1576 }
1577
1578 pub(super) fn reader_reuses(&self) -> usize {
1579 self.reader_reuses.load(Ordering::Relaxed)
1580 }
1581
1582 pub(super) fn is_invalidated(&self) -> bool {
1583 *self
1584 .invalidation
1585 .read()
1586 .unwrap_or_else(std::sync::PoisonError::into_inner)
1587 }
1588
1589 fn invalidate(&self) {
1590 *self
1591 .invalidation
1592 .write()
1593 .unwrap_or_else(std::sync::PoisonError::into_inner) = true;
1594 }
1595
1596 fn invalidation(&self) -> Arc<RwLock<bool>> {
1597 Arc::clone(&self.invalidation)
1598 }
1599}
1600
1601impl<'a, 'session> WasmBuildBatchInputResolver<'a, 'session> {
1602 pub(super) fn new(
1603 specs: impl IntoIterator<Item = &'a WasmBuildSpec>,
1604 mut reuse: Option<WasmBuildInputReuse<'session>>,
1605 ) -> Self {
1606 let specs = specs.into_iter().collect::<Vec<_>>();
1607 let mut keys = Vec::<BatchResolutionKey>::new();
1608 let mut groups = Vec::<BatchResolutionGroup>::new();
1609 let mut group_by_index = Vec::with_capacity(specs.len());
1610 for (index, spec) in specs.iter().copied().enumerate() {
1611 let key = BatchResolutionKey::for_spec(spec);
1612 let group = keys
1613 .iter()
1614 .position(|candidate| *candidate == key)
1615 .unwrap_or_else(|| {
1616 keys.push(key);
1617 groups.push(BatchResolutionGroup {
1618 indexes: Vec::new(),
1619 });
1620 groups.len() - 1
1621 });
1622 groups[group].indexes.push(index);
1623 group_by_index.push(group);
1624 }
1625 let mut metrics = WasmBuildBatchInputMetrics::default();
1626 let resolved = specs
1627 .iter()
1628 .map(|spec| match reuse.as_mut() {
1629 Some(WasmBuildInputReuse::Session(session)) => {
1630 let reused = session.reuse(spec);
1631 if reused.is_some() {
1632 metrics.session_reuses = metrics.session_reuses.saturating_add(1);
1633 }
1634 reused.map(Ok)
1635 }
1636 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
1637 let reused = snapshot
1638 .reuse(spec)
1639 .expect("prepared input snapshot must contain every reader specification");
1640 metrics.prepared_reuses = metrics.prepared_reuses.saturating_add(1);
1641 Some(Ok(reused))
1642 }
1643 None => None,
1644 })
1645 .collect();
1646 Self {
1647 specs,
1648 groups,
1649 group_by_index,
1650 resolved,
1651 reuse,
1652 metrics,
1653 }
1654 }
1655
1656 pub(super) const fn metrics(&self) -> WasmBuildBatchInputMetrics {
1657 self.metrics
1658 }
1659
1660 pub(super) fn invalidate_source_lease(&mut self) {
1661 match self.reuse.as_mut() {
1662 Some(WasmBuildInputReuse::Session(session)) => {
1663 session.invalidate();
1664 for resolved in &mut self.resolved {
1668 *resolved = None;
1669 }
1670 self.reuse = None;
1671 }
1672 Some(WasmBuildInputReuse::Snapshot(snapshot)) => snapshot.invalidate(),
1673 None => {}
1674 }
1675 }
1676
1677 pub(super) const fn assumes_sources_immutable(&self) -> bool {
1678 self.reuse.is_some()
1679 }
1680
1681 pub(super) fn prepared_invalidation(&self) -> Option<Arc<RwLock<bool>>> {
1682 match self.reuse.as_ref() {
1683 Some(WasmBuildInputReuse::Snapshot(snapshot)) => Some(snapshot.invalidation()),
1684 _ => None,
1685 }
1686 }
1687
1688 fn resolve(
1689 &mut self,
1690 index: usize,
1691 progress: &mut ProgressReporter<'_>,
1692 ) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
1693 if self.resolved[index].is_none() {
1694 self.resolve_group(index, progress)?;
1695 }
1696 self.resolved[index]
1697 .take()
1698 .expect("resolved batch input must be populated")
1699 .map_err(|(phase, error)| {
1700 progress.begin_phase(phase);
1701 error
1702 })
1703 }
1704
1705 fn resolve_group(
1706 &mut self,
1707 active_index: usize,
1708 progress: &mut ProgressReporter<'_>,
1709 ) -> Result<(), WasmBuildError> {
1710 let total_started = Instant::now();
1711 let group = self.group_by_index[active_index];
1712 let active = self.specs[active_index];
1713
1714 let (cargo_identity, rustc_identity, tool_identity) =
1715 resolve_tool_identity(active, progress)?;
1716
1717 let metadata_started = Instant::now();
1718 let metadata = progress.run_phase(WasmBuildProgressPhase::CargoMetadata, || {
1719 cargo_metadata(active)
1720 })?;
1721 let cargo_metadata = metadata_started.elapsed();
1722
1723 let (discovered, input_discovery) = self.discover_group_inputs(group, &metadata, progress);
1724
1725 let hashing_started = Instant::now();
1726 let mut batch_digest_cache = LabeledPathDigestCache::default();
1727 let digest_cache = match self.reuse.as_mut() {
1728 Some(WasmBuildInputReuse::Session(session)) => &mut session.digest_cache,
1729 _ => &mut batch_digest_cache,
1730 };
1731 let workspace_root = &active.workspace_root;
1732 let resolved_inputs = progress.run_phase(WasmBuildProgressPhase::ContentHashing, || {
1733 discovered
1734 .into_iter()
1735 .map(|(index, inputs, exclusions)| {
1736 let result = digest_resolved_local_inputs(
1737 &inputs,
1738 &exclusions,
1739 digest_cache,
1740 workspace_root,
1741 "hash batched Wasm build inputs",
1742 "hash batched semantic Wasm build inputs",
1743 )
1744 .map(|(input_digest, validation_digest)| {
1745 (
1746 inputs.validation_inputs,
1747 exclusions,
1748 input_digest,
1749 validation_digest,
1750 )
1751 });
1752 (index, result)
1753 })
1754 .collect::<Vec<_>>()
1755 });
1756 let content_hashing = hashing_started.elapsed();
1757 let timings = WasmInputResolutionTimings {
1758 tool_identity,
1759 cargo_metadata,
1760 input_discovery,
1761 content_hashing,
1762 total: total_started.elapsed(),
1763 };
1764 let resolved_count = resolved_inputs
1765 .iter()
1766 .filter(|(_, result)| result.is_ok())
1767 .count();
1768 self.metrics.runs += usize::from(resolved_count > 0);
1769 self.metrics.reuses += resolved_count.saturating_sub(1);
1770 let timing_index = resolved_inputs
1771 .iter()
1772 .find(|(index, result)| *index == active_index && result.is_ok())
1773 .or_else(|| resolved_inputs.iter().find(|(_, result)| result.is_ok()))
1774 .map(|(index, _)| *index);
1775 for (index, result) in resolved_inputs {
1776 let (inputs, exclusions, input_digest, validation_digest) = match result {
1777 Ok(resolved) => resolved,
1778 Err(error) => {
1779 self.resolved[index] =
1780 Some(Err((WasmBuildFailurePhase::ContentHashing, error)));
1781 continue;
1782 }
1783 };
1784 let spec = self.specs[index];
1785 let resolved = ResolvedCargoBuildInputs {
1786 fingerprint: finish_build_fingerprint(
1787 spec,
1788 &cargo_identity,
1789 &rustc_identity,
1790 input_digest,
1791 ),
1792 input_digest,
1793 validation_digest,
1794 inputs: inputs
1795 .into_iter()
1796 .map(|(label, path)| CargoBuildInput { label, path })
1797 .collect(),
1798 exclusions: exclusions.into(),
1799 timings: if Some(index) == timing_index {
1800 timings
1801 } else {
1802 WasmInputResolutionTimings::default()
1803 },
1804 };
1805 if let Some(WasmBuildInputReuse::Session(session)) = self.reuse.as_mut() {
1806 session.remember(spec, &resolved);
1807 }
1808 self.resolved[index] = Some(Ok(resolved));
1809 }
1810 Ok(())
1811 }
1812
1813 fn discover_group_inputs(
1814 &mut self,
1815 group: usize,
1816 metadata: &Value,
1817 progress: &mut ProgressReporter<'_>,
1818 ) -> (Vec<(usize, ResolvedLocalInputs, Vec<PathBuf>)>, Duration) {
1819 let started = Instant::now();
1820 let pending = self.groups[group].indexes.iter().copied().filter(|index| {
1821 self.resolved[*index].is_none() && validate_spec(self.specs[*index]).is_ok()
1822 });
1823 let results = progress.run_phase(WasmBuildProgressPhase::InputDiscovery, || {
1824 let parsed = ParsedCargoMetadata::parse(metadata);
1825 pending
1826 .map(|index| {
1827 let spec = self.specs[index];
1828 let result = (|| {
1829 let parsed = parsed
1830 .as_ref()
1831 .map_err(|message| invalid_metadata(message))?;
1832 resolve_local_inputs(spec, parsed)
1833 })();
1834 (index, result)
1835 })
1836 .collect::<Vec<_>>()
1837 });
1838 let mut discovered = Vec::new();
1839 for (index, result) in results {
1840 match result {
1841 Ok((inputs, exclusions)) => discovered.push((index, inputs, exclusions)),
1842 Err(error) => {
1843 self.resolved[index] =
1844 Some(Err((WasmBuildFailurePhase::InputDiscovery, error)));
1845 }
1846 }
1847 }
1848 (discovered, started.elapsed())
1849 }
1850}
1851
1852fn resolve_tool_identity(
1853 spec: &WasmBuildSpec,
1854 progress: &mut ProgressReporter<'_>,
1855) -> Result<(Vec<u8>, Vec<u8>, Duration), WasmBuildError> {
1856 let started = Instant::now();
1857 let cargo_identity = progress.run_phase(WasmBuildProgressPhase::CargoIdentity, || {
1858 command_identity(
1859 spec,
1860 WasmBuildPhase::CargoIdentity,
1861 &spec.cargo_program,
1862 &["--version", "--verbose"],
1863 )
1864 })?;
1865 let rustc_program = spec
1866 .extra_env
1867 .get(OsStr::new("RUSTC"))
1868 .unwrap_or(&spec.rustc_program);
1869 let rustc_identity = progress.run_phase(WasmBuildProgressPhase::RustcIdentity, || {
1870 command_identity(spec, WasmBuildPhase::RustcIdentity, rustc_program, &["-vV"])
1871 })?;
1872 Ok((cargo_identity, rustc_identity, started.elapsed()))
1873}
1874
1875impl<'a> BatchResolutionKey<'a> {
1876 fn for_spec(spec: &'a WasmBuildSpec) -> Self {
1877 Self {
1878 workspace_root: &spec.workspace_root,
1879 cargo_program: &spec.cargo_program,
1880 rustc_program: spec
1881 .extra_env
1882 .get(OsStr::new("RUSTC"))
1883 .unwrap_or(&spec.rustc_program),
1884 metadata_arguments: metadata_arguments(&spec.cargo_profile_args),
1885 environment: effective_environment(spec),
1886 }
1887 }
1888}
1889
1890impl SharedIncrementalTargetInspection {
1891 #[must_use]
1893 pub fn target_dir(&self) -> &Path {
1894 &self.target_dir
1895 }
1896
1897 #[must_use]
1899 pub const fn logical_size_bytes(&self) -> u64 {
1900 self.logical_size_bytes
1901 }
1902
1903 #[must_use]
1905 pub const fn last_used(&self) -> SystemTime {
1906 self.last_used
1907 }
1908
1909 #[must_use]
1911 pub const fn lock_wait(&self) -> Duration {
1912 self.lock_wait
1913 }
1914}
1915
1916impl SharedIncrementalTargetPrunePolicy {
1917 #[must_use]
1919 pub const fn new() -> Self {
1920 Self {
1921 max_age: None,
1922 max_size_bytes: None,
1923 }
1924 }
1925
1926 #[must_use]
1928 pub const fn with_max_age(mut self, max_age: Duration) -> Self {
1929 self.max_age = Some(max_age);
1930 self
1931 }
1932
1933 #[must_use]
1935 pub const fn with_max_size_bytes(mut self, bytes: u64) -> Self {
1936 self.max_size_bytes = Some(bytes);
1937 self
1938 }
1939
1940 #[must_use]
1942 pub const fn max_age(self) -> Option<Duration> {
1943 self.max_age
1944 }
1945
1946 #[must_use]
1948 pub const fn max_size_bytes(self) -> Option<u64> {
1949 self.max_size_bytes
1950 }
1951
1952 fn maintenance_identity(self) -> String {
1953 format!(
1954 "age={:?};size={:?}",
1955 self.max_age.map(|duration| duration.as_nanos()),
1956 self.max_size_bytes
1957 )
1958 }
1959}
1960
1961impl SharedIncrementalTargetMaintenanceConfig {
1962 #[must_use]
1964 pub const fn new(
1965 policy: SharedIncrementalTargetPrunePolicy,
1966 minimum_interval: Duration,
1967 ) -> Self {
1968 Self {
1969 policy,
1970 minimum_interval,
1971 failure_mode: SharedIncrementalTargetMaintenanceFailureMode::Strict,
1972 }
1973 }
1974
1975 #[must_use]
1977 pub const fn with_failure_mode(
1978 mut self,
1979 failure_mode: SharedIncrementalTargetMaintenanceFailureMode,
1980 ) -> Self {
1981 self.failure_mode = failure_mode;
1982 self
1983 }
1984
1985 #[must_use]
1987 pub const fn policy(self) -> SharedIncrementalTargetPrunePolicy {
1988 self.policy
1989 }
1990
1991 #[must_use]
1993 pub const fn minimum_interval(self) -> Duration {
1994 self.minimum_interval
1995 }
1996
1997 #[must_use]
1999 pub const fn failure_mode(self) -> SharedIncrementalTargetMaintenanceFailureMode {
2000 self.failure_mode
2001 }
2002}
2003
2004impl SharedIncrementalTargetMaintenance {
2005 #[must_use]
2007 pub fn target_dir(&self) -> &Path {
2008 &self.target_dir
2009 }
2010
2011 #[must_use]
2013 pub const fn logical_size_bytes_before(&self) -> u64 {
2014 self.logical_size_bytes_before
2015 }
2016
2017 #[must_use]
2019 pub const fn logical_size_bytes_after(&self) -> u64 {
2020 self.logical_size_bytes_after
2021 }
2022
2023 #[must_use]
2025 pub const fn last_used_before(&self) -> SystemTime {
2026 self.last_used_before
2027 }
2028
2029 #[must_use]
2031 pub const fn was_cleared(&self) -> bool {
2032 self.cleared
2033 }
2034
2035 #[must_use]
2037 pub const fn lock_wait(&self) -> Duration {
2038 self.lock_wait
2039 }
2040
2041 #[must_use]
2043 pub const fn maintenance(&self) -> Duration {
2044 self.maintenance
2045 }
2046}
2047
2048impl std::fmt::Display for SharedIncrementalTargetMaintenance {
2049 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2050 write!(
2051 formatter,
2052 "target={} action={} bytes={}=>{} lock={:?} maintenance={:?}",
2053 self.target_dir.display(),
2054 if self.cleared { "cleared" } else { "retained" },
2055 self.logical_size_bytes_before,
2056 self.logical_size_bytes_after,
2057 self.lock_wait,
2058 self.maintenance,
2059 )
2060 }
2061}
2062
2063impl SharedIncrementalTargetMaintenanceOutcome {
2064 #[must_use]
2066 pub fn target_dir(&self) -> &Path {
2067 match self {
2068 Self::Missing { target_dir }
2069 | Self::Skipped { target_dir, .. }
2070 | Self::Failed { target_dir, .. } => target_dir,
2071 Self::Performed { maintenance, .. } => maintenance.target_dir(),
2072 }
2073 }
2074
2075 #[must_use]
2077 pub const fn maintenance(&self) -> Option<&SharedIncrementalTargetMaintenance> {
2078 match self {
2079 Self::Performed { maintenance, .. } => Some(maintenance),
2080 Self::Missing { .. } | Self::Skipped { .. } | Self::Failed { .. } => None,
2081 }
2082 }
2083
2084 #[must_use]
2086 pub const fn was_performed(&self) -> bool {
2087 matches!(self, Self::Performed { .. })
2088 }
2089
2090 #[must_use]
2092 pub const fn lock_wait(&self) -> Option<Duration> {
2093 match self {
2094 Self::Missing { .. } => None,
2095 Self::Skipped { lock_wait, .. } | Self::Failed { lock_wait, .. } => Some(*lock_wait),
2096 Self::Performed { maintenance, .. } => Some(maintenance.lock_wait()),
2097 }
2098 }
2099
2100 #[must_use]
2102 pub const fn schedule_check(&self) -> Option<Duration> {
2103 match self {
2104 Self::Missing { .. } | Self::Failed { .. } => None,
2105 Self::Skipped { schedule_check, .. } | Self::Performed { schedule_check, .. } => {
2106 Some(*schedule_check)
2107 }
2108 }
2109 }
2110
2111 #[must_use]
2113 pub fn failure_message(&self) -> Option<&str> {
2114 match self {
2115 Self::Failed { message, .. } => Some(message),
2116 Self::Missing { .. } | Self::Skipped { .. } | Self::Performed { .. } => None,
2117 }
2118 }
2119}
2120
2121impl std::fmt::Display for SharedIncrementalTargetMaintenanceOutcome {
2122 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2123 match self {
2124 Self::Missing { target_dir } => {
2125 write!(formatter, "target={} action=missing", target_dir.display())
2126 }
2127 Self::Skipped {
2128 target_dir,
2129 lock_wait,
2130 schedule_check,
2131 } => write!(
2132 formatter,
2133 "target={} action=skipped lock={lock_wait:?} schedule={schedule_check:?}",
2134 target_dir.display(),
2135 ),
2136 Self::Performed {
2137 maintenance,
2138 schedule_check,
2139 } => write!(formatter, "{maintenance} schedule={schedule_check:?}"),
2140 Self::Failed {
2141 target_dir,
2142 lock_wait,
2143 message,
2144 } => write!(
2145 formatter,
2146 "target={} action=failed lock={lock_wait:?} error={message}",
2147 target_dir.display(),
2148 ),
2149 }
2150 }
2151}
2152
2153impl std::fmt::Display for WasmBuildTimings {
2154 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2155 write!(
2156 formatter,
2157 "total={:?} lock={:?} shared_lock={:?} inputs={:?} cargo={:?} maintenance={:?}",
2158 self.total,
2159 self.lock_wait,
2160 self.shared_incremental_lock_wait,
2161 self.input_resolution.total,
2162 self.cargo_build,
2163 self.cache_maintenance,
2164 )
2165 }
2166}
2167
2168impl std::fmt::Display for WasmBuildOutcome {
2169 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2170 let state = if self.is_reused() { "reused" } else { "built" };
2171 write!(
2172 formatter,
2173 "{state} fingerprint={} artifacts={} {}",
2174 self.record().fingerprint,
2175 self.record().artifacts.len(),
2176 self.record().timings,
2177 )?;
2178 if let Some(maintenance) = self.record().shared_incremental_maintenance() {
2179 write!(formatter, " shared_maintenance=({maintenance})")?;
2180 }
2181 Ok(())
2182 }
2183}
2184
2185pub fn resolve_cargo_build_inputs(
2190 spec: &WasmBuildSpec,
2191) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2192 validate_spec(spec)?;
2193 build_fingerprint(spec)
2194}
2195
2196pub fn inspect_shared_incremental_target(
2201 spec: &WasmBuildSpec,
2202) -> Result<Option<SharedIncrementalTargetInspection>, WasmBuildError> {
2203 if !shared_incremental_target_exists(spec, "inspect shared incremental Cargo target")? {
2204 return Ok(None);
2205 }
2206
2207 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2208 let logical_size_bytes =
2209 directory_logical_size(&canonical).map_err(|source| WasmBuildError::Io {
2210 operation: "measure shared incremental Cargo target",
2211 path: canonical.clone(),
2212 source,
2213 })?;
2214 let last_used = cache_entry_last_used(&canonical).map_err(|source| WasmBuildError::Io {
2215 operation: "read shared incremental Cargo target use time",
2216 path: canonical.clone(),
2217 source,
2218 })?;
2219 Ok(Some(SharedIncrementalTargetInspection {
2220 target_dir: canonical,
2221 logical_size_bytes,
2222 last_used,
2223 lock_wait,
2224 }))
2225}
2226
2227pub fn maintain_shared_incremental_target(
2241 spec: &WasmBuildSpec,
2242 policy: SharedIncrementalTargetPrunePolicy,
2243) -> Result<Option<SharedIncrementalTargetMaintenance>, WasmBuildError> {
2244 if !shared_incremental_target_exists(
2245 spec,
2246 "inspect shared incremental Cargo target before maintenance",
2247 )? {
2248 return Ok(None);
2249 }
2250
2251 let _ = resolve_cargo_build_inputs(spec)?;
2255 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2256 maintain_shared_incremental_target_locked(&canonical, policy, lock_wait).map(Some)
2257}
2258
2259pub fn maintain_shared_incremental_target_at_most_every(
2273 spec: &WasmBuildSpec,
2274 policy: SharedIncrementalTargetPrunePolicy,
2275 minimum_interval: Duration,
2276) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2277 let target_dir =
2278 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
2279 message: "shared incremental target is not configured".to_owned(),
2280 })?;
2281 if !shared_incremental_target_exists(
2282 spec,
2283 "inspect shared incremental Cargo target before scheduled maintenance",
2284 )? {
2285 return Ok(SharedIncrementalTargetMaintenanceOutcome::Missing { target_dir });
2286 }
2287
2288 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2289 let schedule = schedule_shared_incremental_target_maintenance(
2290 &canonical,
2291 policy,
2292 minimum_interval,
2293 lock_wait,
2294 )?;
2295 let schedule = match schedule {
2296 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => return Ok(outcome),
2297 SharedIncrementalTargetMaintenanceSchedule::Due(due) => due,
2298 };
2299
2300 let _ = resolve_cargo_build_inputs(spec)?;
2303 perform_due_shared_incremental_target_maintenance(&canonical, policy, lock_wait, schedule)
2304}
2305
2306enum SharedIncrementalTargetMaintenanceSchedule {
2307 Skipped(SharedIncrementalTargetMaintenanceOutcome),
2308 Due(DueSharedIncrementalTargetMaintenance),
2309}
2310
2311struct DueSharedIncrementalTargetMaintenance {
2312 schedule_root: PathBuf,
2313 maintenance_identity: String,
2314 schedule_check: Duration,
2315}
2316
2317fn schedule_shared_incremental_target_maintenance(
2318 canonical: &Path,
2319 policy: SharedIncrementalTargetPrunePolicy,
2320 minimum_interval: Duration,
2321 lock_wait: Duration,
2322) -> Result<SharedIncrementalTargetMaintenanceSchedule, WasmBuildError> {
2323 let schedule_root = canonical.join(".ic-testkit");
2324 let maintenance_identity = policy.maintenance_identity();
2325 let schedule_started = Instant::now();
2326 let due = cache_maintenance_due(
2327 &schedule_root,
2328 Some(minimum_interval),
2329 &maintenance_identity,
2330 )
2331 .map_err(wasm_cache_fs_error)?;
2332 let schedule_check = schedule_started.elapsed();
2333 if !due {
2334 return Ok(SharedIncrementalTargetMaintenanceSchedule::Skipped(
2335 SharedIncrementalTargetMaintenanceOutcome::Skipped {
2336 target_dir: canonical.to_owned(),
2337 lock_wait,
2338 schedule_check,
2339 },
2340 ));
2341 }
2342 Ok(SharedIncrementalTargetMaintenanceSchedule::Due(
2343 DueSharedIncrementalTargetMaintenance {
2344 schedule_root,
2345 maintenance_identity,
2346 schedule_check,
2347 },
2348 ))
2349}
2350
2351fn perform_due_shared_incremental_target_maintenance(
2352 canonical: &Path,
2353 policy: SharedIncrementalTargetPrunePolicy,
2354 lock_wait: Duration,
2355 due: DueSharedIncrementalTargetMaintenance,
2356) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2357 let DueSharedIncrementalTargetMaintenance {
2358 schedule_root,
2359 maintenance_identity,
2360 schedule_check,
2361 } = due;
2362 let maintenance = maintain_shared_incremental_target_locked(canonical, policy, lock_wait)?;
2363 record_cache_maintenance(&schedule_root, &maintenance_identity).map_err(wasm_cache_fs_error)?;
2364 Ok(SharedIncrementalTargetMaintenanceOutcome::Performed {
2365 maintenance,
2366 schedule_check,
2367 })
2368}
2369
2370fn maintain_shared_incremental_target_locked(
2371 canonical: &Path,
2372 policy: SharedIncrementalTargetPrunePolicy,
2373 lock_wait: Duration,
2374) -> Result<SharedIncrementalTargetMaintenance, WasmBuildError> {
2375 let started = Instant::now();
2376 let logical_size_bytes_before =
2377 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
2378 operation: "measure shared incremental Cargo target before maintenance",
2379 path: canonical.to_owned(),
2380 source,
2381 })?;
2382 let last_used_before =
2383 cache_entry_last_used(canonical).map_err(|source| WasmBuildError::Io {
2384 operation: "read shared incremental Cargo target use time before maintenance",
2385 path: canonical.to_owned(),
2386 source,
2387 })?;
2388 let expired = policy.max_age.is_some_and(|max_age| {
2389 SystemTime::now()
2390 .duration_since(last_used_before)
2391 .is_ok_and(|age| age > max_age)
2392 });
2393 let oversized = policy
2394 .max_size_bytes
2395 .is_some_and(|max_size_bytes| logical_size_bytes_before > max_size_bytes);
2396 let cleared = expired || oversized;
2397 if cleared {
2398 clear_shared_incremental_target_contents(canonical)?;
2399 record_cache_entry_use(canonical)?;
2400 }
2401 let logical_size_bytes_after = if cleared {
2402 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
2403 operation: "measure shared incremental Cargo target after maintenance",
2404 path: canonical.to_owned(),
2405 source,
2406 })?
2407 } else {
2408 logical_size_bytes_before
2409 };
2410 Ok(SharedIncrementalTargetMaintenance {
2411 target_dir: canonical.to_owned(),
2412 logical_size_bytes_before,
2413 logical_size_bytes_after,
2414 last_used_before,
2415 cleared,
2416 lock_wait,
2417 maintenance: started.elapsed(),
2418 })
2419}
2420
2421fn clear_shared_incremental_target_contents(target_dir: &Path) -> Result<(), WasmBuildError> {
2422 let entries = fs::read_dir(target_dir).map_err(|source| WasmBuildError::Io {
2423 operation: "read shared incremental Cargo target for maintenance",
2424 path: target_dir.to_owned(),
2425 source,
2426 })?;
2427 for entry in entries {
2428 let path = entry
2429 .map_err(|source| WasmBuildError::Io {
2430 operation: "read shared incremental Cargo target entry for maintenance",
2431 path: target_dir.to_owned(),
2432 source,
2433 })?
2434 .path();
2435 let preserved = path
2436 .file_name()
2437 .is_some_and(|name| name == ".ic-testkit" || name == "CACHEDIR.TAG");
2438 if !preserved {
2439 remove_path_if_present(&path).map_err(|source| WasmBuildError::Io {
2440 operation: "clear shared incremental Cargo target entry",
2441 path,
2442 source,
2443 })?;
2444 }
2445 }
2446 Ok(())
2447}
2448
2449pub fn build_wasm_canisters_cached(
2467 spec: &WasmBuildSpec,
2468) -> Result<WasmBuildOutcome, WasmBuildError> {
2469 build_wasm_canisters_cached_internal(spec, &mut ProgressReporter::silent(), None)
2470}
2471
2472pub(super) fn build_wasm_canisters_cached_in_batch(
2473 spec: &WasmBuildSpec,
2474 index: usize,
2475 resolver: &mut WasmBuildBatchInputResolver<'_, '_>,
2476) -> WasmBuildBatchAttempt {
2477 let started = Instant::now();
2478 let mut progress = ProgressReporter::silent();
2479 let result = build_wasm_canisters_cached_internal(spec, &mut progress, Some((resolver, index)));
2480 if result
2481 .as_ref()
2482 .is_err_and(WasmBuildError::indicates_input_change)
2483 {
2484 resolver.invalidate_source_lease();
2485 }
2486 batch_result(result, &progress, started.elapsed())
2487}
2488
2489pub fn build_wasm_canisters_cached_with_progress<F>(
2498 spec: &WasmBuildSpec,
2499 config: WasmBuildProgressConfig,
2500 mut observer: F,
2501) -> Result<WasmBuildOutcome, WasmBuildError>
2502where
2503 F: FnMut(WasmBuildProgressEvent),
2504{
2505 if config.heartbeat_interval == Some(Duration::ZERO) {
2506 return Err(WasmBuildError::InvalidSpec {
2507 message: "Wasm build progress heartbeat interval must be greater than zero".to_owned(),
2508 });
2509 }
2510 build_wasm_canisters_cached_internal(
2511 spec,
2512 &mut ProgressReporter::observed(config, &mut observer),
2513 None,
2514 )
2515}
2516
2517pub(super) fn build_wasm_canisters_cached_in_batch_with_progress<F>(
2518 spec: &WasmBuildSpec,
2519 index: usize,
2520 resolver: &mut WasmBuildBatchInputResolver<'_, '_>,
2521 config: WasmBuildProgressConfig,
2522 mut observer: F,
2523) -> WasmBuildBatchAttempt
2524where
2525 F: FnMut(WasmBuildProgressEvent),
2526{
2527 if config.heartbeat_interval == Some(Duration::ZERO) {
2528 return WasmBuildBatchAttempt {
2529 result: Err((
2530 WasmBuildError::InvalidSpec {
2531 message: "Wasm build progress heartbeat interval must be greater than zero"
2532 .to_owned(),
2533 },
2534 WasmBuildFailureDetails::specification(Duration::ZERO),
2535 )),
2536 };
2537 }
2538 let started = Instant::now();
2539 let mut progress = ProgressReporter::observed(config, &mut observer);
2540 let result = build_wasm_canisters_cached_internal(spec, &mut progress, Some((resolver, index)));
2541 if result
2542 .as_ref()
2543 .is_err_and(WasmBuildError::indicates_input_change)
2544 {
2545 resolver.invalidate_source_lease();
2546 }
2547 batch_result(result, &progress, started.elapsed())
2548}
2549
2550fn batch_result(
2551 result: Result<WasmBuildOutcome, WasmBuildError>,
2552 progress: &ProgressReporter<'_>,
2553 total: Duration,
2554) -> WasmBuildBatchAttempt {
2555 WasmBuildBatchAttempt {
2556 result: result.map_err(|error| {
2557 let details = progress.failure_details(&error, total);
2558 (error, details)
2559 }),
2560 }
2561}
2562
2563fn batch_source_assumptions(
2564 batch_resolution: Option<&(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2565) -> (bool, Option<Arc<RwLock<bool>>>) {
2566 batch_resolution.map_or((false, None), |(resolver, _)| {
2567 (
2568 resolver.assumes_sources_immutable(),
2569 resolver.prepared_invalidation(),
2570 )
2571 })
2572}
2573
2574fn build_wasm_canisters_cached_internal(
2575 spec: &WasmBuildSpec,
2576 progress: &mut ProgressReporter<'_>,
2577 mut batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2578) -> Result<WasmBuildOutcome, WasmBuildError> {
2579 let total_started = Instant::now();
2580 validate_spec(spec)?;
2581 let (assumes_sources_immutable, prepared_invalidation) =
2582 batch_source_assumptions(batch_resolution.as_ref());
2583 progress.emit(WasmBuildProgressEvent::Started);
2584 if spec.shared_incremental_maintenance_config.is_some() {
2585 let outcome = build_wasm_canisters_cached_with_scheduled_shared_maintenance(
2586 spec,
2587 total_started,
2588 progress,
2589 batch_resolution.take(),
2590 )?;
2591 emit_finished_progress(&outcome, progress);
2592 return Ok(outcome);
2593 }
2594 let (cache_lock, first_lock_wait) =
2595 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2596 ensure_cache_directory_tag(&spec.target_dir)?;
2597
2598 let resolved = resolve_initial_inputs(spec, batch_resolution.take(), progress)?;
2599 let isolated_acquisition =
2600 WasmAcquisitionContext::isolated(prepared_invalidation.clone(), assumes_sources_immutable);
2601 if let Some(outcome) = try_reuse_wasm_artifacts(
2602 spec,
2603 &resolved,
2604 first_lock_wait,
2605 &isolated_acquisition,
2606 total_started,
2607 progress,
2608 )? {
2609 emit_finished_progress(&outcome, progress);
2610 return Ok(outcome);
2611 }
2612 progress.emit(WasmBuildProgressEvent::CacheMiss {
2613 fingerprint: resolved.fingerprint,
2614 });
2615
2616 let outcome = match &spec.cache_mode {
2617 WasmBuildCacheMode::Isolated => {
2618 let cache_entry = cache_entry_directory(spec, resolved.fingerprint);
2619 build_wasm_cache_miss(
2620 spec,
2621 resolved,
2622 first_lock_wait,
2623 isolated_acquisition,
2624 cache_entry,
2625 total_started,
2626 progress,
2627 )
2628 }
2629 WasmBuildCacheMode::SharedIncremental { .. } => {
2630 drop(cache_lock);
2631 build_wasm_with_shared_incremental(
2632 spec,
2633 resolved,
2634 first_lock_wait,
2635 assumes_sources_immutable,
2636 prepared_invalidation,
2637 total_started,
2638 progress,
2639 )
2640 }
2641 }?;
2642 emit_finished_progress(&outcome, progress);
2643 Ok(outcome)
2644}
2645
2646fn build_wasm_with_shared_incremental(
2647 spec: &WasmBuildSpec,
2648 resolved: ResolvedCargoBuildInputs,
2649 first_lock_wait: Duration,
2650 assumes_sources_immutable: bool,
2651 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2652 total_started: Instant,
2653 progress: &mut ProgressReporter<'_>,
2654) -> Result<WasmBuildOutcome, WasmBuildError> {
2655 let (shared_lock, shared_lock_wait, shared_target) =
2656 lock_shared_incremental_target_with_progress(spec, progress)?;
2657 let (_cache_lock, second_lock_wait) =
2658 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2659 ensure_cache_directory_tag(&spec.target_dir)?;
2660
2661 let current = if assumes_sources_immutable {
2662 resolved
2663 } else {
2664 let mut current = resolve_inputs_with_progress(spec, progress)?;
2665 current.timings.include(resolved.timings);
2666 current
2667 };
2668 let lock_wait = first_lock_wait.saturating_add(second_lock_wait);
2669 let shared_incremental = WasmAcquisitionContext::shared(
2670 shared_lock_wait,
2671 None,
2672 prepared_invalidation,
2673 assumes_sources_immutable,
2674 );
2675 if let Some(outcome) = try_reuse_wasm_artifacts(
2676 spec,
2677 ¤t,
2678 lock_wait,
2679 &shared_incremental,
2680 total_started,
2681 progress,
2682 )? {
2683 return Ok(outcome);
2684 }
2685
2686 let outcome = build_wasm_cache_miss(
2687 spec,
2688 current,
2689 lock_wait,
2690 shared_incremental,
2691 shared_target,
2692 total_started,
2693 progress,
2694 );
2695 drop(shared_lock);
2696 outcome
2697}
2698
2699fn build_wasm_canisters_cached_with_scheduled_shared_maintenance(
2700 spec: &WasmBuildSpec,
2701 total_started: Instant,
2702 progress: &mut ProgressReporter<'_>,
2703 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2704) -> Result<WasmBuildOutcome, WasmBuildError> {
2705 let (assumes_sources_immutable, prepared_invalidation) =
2706 batch_source_assumptions(batch_resolution.as_ref());
2707 let (_shared_lock, shared_lock_wait, shared_target) =
2708 lock_shared_incremental_target_with_progress(spec, progress)?;
2709 let (_cache_lock, lock_wait) = lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2710 ensure_cache_directory_tag(&spec.target_dir)?;
2711
2712 let resolved = resolve_initial_inputs(spec, batch_resolution, progress)?;
2715 let shared_maintenance = perform_configured_shared_incremental_target_maintenance(
2716 spec,
2717 &shared_target,
2718 shared_lock_wait,
2719 progress,
2720 )?;
2721 let shared_incremental = WasmAcquisitionContext::shared(
2722 shared_lock_wait,
2723 Some(shared_maintenance),
2724 prepared_invalidation,
2725 assumes_sources_immutable,
2726 );
2727 if let Some(outcome) = try_reuse_wasm_artifacts(
2728 spec,
2729 &resolved,
2730 lock_wait,
2731 &shared_incremental,
2732 total_started,
2733 progress,
2734 )? {
2735 return Ok(outcome);
2736 }
2737 progress.emit(WasmBuildProgressEvent::CacheMiss {
2738 fingerprint: resolved.fingerprint,
2739 });
2740 build_wasm_cache_miss(
2741 spec,
2742 resolved,
2743 lock_wait,
2744 shared_incremental,
2745 shared_target,
2746 total_started,
2747 progress,
2748 )
2749}
2750
2751fn perform_configured_shared_incremental_target_maintenance(
2752 spec: &WasmBuildSpec,
2753 shared_target: &Path,
2754 lock_wait: Duration,
2755 progress: &mut ProgressReporter<'_>,
2756) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2757 let config = spec
2758 .shared_incremental_maintenance_config
2759 .expect("configured shared-target maintenance must have settings");
2760 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceStarted {
2761 target_dir: shared_target.to_owned(),
2762 });
2763 let result = progress.run_phase(WasmBuildProgressPhase::SharedTargetMaintenance, || {
2764 let schedule = schedule_shared_incremental_target_maintenance(
2765 shared_target,
2766 config.policy,
2767 config.minimum_interval,
2768 lock_wait,
2769 )?;
2770 match schedule {
2771 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => Ok(outcome),
2772 SharedIncrementalTargetMaintenanceSchedule::Due(due) => {
2773 perform_due_shared_incremental_target_maintenance(
2774 shared_target,
2775 config.policy,
2776 lock_wait,
2777 due,
2778 )
2779 }
2780 }
2781 });
2782 let outcome = integrated_shared_maintenance_result(config, shared_target, lock_wait, result)?;
2783 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceFinished {
2784 outcome: outcome.clone(),
2785 });
2786 Ok(outcome)
2787}
2788
2789fn integrated_shared_maintenance_result(
2790 config: SharedIncrementalTargetMaintenanceConfig,
2791 shared_target: &Path,
2792 lock_wait: Duration,
2793 result: Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError>,
2794) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2795 match result {
2796 Ok(outcome) => Ok(outcome),
2797 Err(error)
2798 if config.failure_mode == SharedIncrementalTargetMaintenanceFailureMode::BestEffort =>
2799 {
2800 Ok(SharedIncrementalTargetMaintenanceOutcome::Failed {
2801 target_dir: shared_target.to_owned(),
2802 lock_wait,
2803 message: error.to_string(),
2804 })
2805 }
2806 Err(error) => Err(error),
2807 }
2808}
2809
2810fn resolve_inputs_with_progress(
2811 spec: &WasmBuildSpec,
2812 progress: &mut ProgressReporter<'_>,
2813) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2814 let resolved = build_fingerprint_with_progress(spec, progress)?;
2815 progress.emit(WasmBuildProgressEvent::InputsResolved {
2816 fingerprint: resolved.fingerprint,
2817 input_digest: resolved.input_digest,
2818 elapsed: resolved.timings.total,
2819 });
2820 Ok(resolved)
2821}
2822
2823fn resolve_initial_inputs(
2824 spec: &WasmBuildSpec,
2825 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2826 progress: &mut ProgressReporter<'_>,
2827) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2828 let resolved = if let Some((resolver, index)) = batch_resolution {
2829 resolver.resolve(index, progress)?
2830 } else {
2831 build_fingerprint_with_progress(spec, progress)?
2832 };
2833 progress.emit(WasmBuildProgressEvent::InputsResolved {
2834 fingerprint: resolved.fingerprint,
2835 input_digest: resolved.input_digest,
2836 elapsed: resolved.timings.total,
2837 });
2838 Ok(resolved)
2839}
2840
2841fn emit_finished_progress(outcome: &WasmBuildOutcome, progress: &mut ProgressReporter<'_>) {
2842 let state = if outcome.is_reused() {
2843 progress.emit(WasmBuildProgressEvent::CacheHit {
2844 fingerprint: outcome.record().fingerprint,
2845 });
2846 WasmBuildProgressOutcome::Reused
2847 } else {
2848 WasmBuildProgressOutcome::Built
2849 };
2850 progress.emit(WasmBuildProgressEvent::Finished {
2851 outcome: state,
2852 fingerprint: outcome.record().fingerprint,
2853 elapsed: outcome.record().timings.total,
2854 });
2855}
2856
2857#[derive(Clone, Debug, Default)]
2858struct WasmAcquisitionContext {
2859 assumes_sources_immutable: bool,
2860 lock_wait: Option<Duration>,
2861 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
2862 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2863}
2864
2865impl WasmAcquisitionContext {
2866 fn isolated(
2867 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2868 assumes_sources_immutable: bool,
2869 ) -> Self {
2870 Self {
2871 assumes_sources_immutable,
2872 prepared_invalidation,
2873 ..Self::default()
2874 }
2875 }
2876
2877 const fn shared(
2878 lock_wait: Duration,
2879 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
2880 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2881 assumes_sources_immutable: bool,
2882 ) -> Self {
2883 Self {
2884 assumes_sources_immutable,
2885 lock_wait: Some(lock_wait),
2886 maintenance,
2887 prepared_invalidation,
2888 }
2889 }
2890
2891 fn lock_prepared_publication(
2892 &self,
2893 ) -> Result<Option<std::sync::RwLockReadGuard<'_, bool>>, WasmBuildError> {
2894 let guard = self.prepared_invalidation.as_deref().map(|invalidation| {
2895 invalidation
2896 .read()
2897 .unwrap_or_else(std::sync::PoisonError::into_inner)
2898 });
2899 if guard.as_deref().is_some_and(|invalidated| *invalidated) {
2900 return Err(WasmBuildError::PreparedInputSnapshotInvalidated);
2901 }
2902 Ok(guard)
2903 }
2904}
2905
2906fn try_reuse_wasm_artifacts(
2907 spec: &WasmBuildSpec,
2908 resolved: &ResolvedCargoBuildInputs,
2909 lock_wait: Duration,
2910 shared_incremental: &WasmAcquisitionContext,
2911 total_started: Instant,
2912 progress: &mut ProgressReporter<'_>,
2913) -> Result<Option<WasmBuildOutcome>, WasmBuildError> {
2914 let _publication_guard = shared_incremental.lock_prepared_publication()?;
2915 let fingerprint = resolved.fingerprint;
2916 let artifacts = expected_artifacts(spec, &spec.target_dir);
2917 let cache_entry = cache_entry_directory(spec, fingerprint);
2918 let cached_artifacts = expected_artifacts(spec, &cache_entry);
2919 let cached_info = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2920 validated_artifact_set(&cached_artifacts, fingerprint)
2921 });
2922 let artifact_info = if let Some(info) = cached_info {
2923 info
2924 } else {
2925 let public_is_current = progress
2928 .run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2929 validated_artifact_set(&artifacts, fingerprint).is_some()
2930 });
2931 if !public_is_current {
2932 return Ok(None);
2933 }
2934 reconstruct_exact_cache_entry(spec, &artifacts, &cache_entry, fingerprint, progress)?
2935 };
2936 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2937 materialize_artifacts(
2938 &cached_artifacts,
2939 &artifacts,
2940 fingerprint,
2941 &artifact_info,
2942 true,
2943 )?;
2944 record_cache_entry_use(&cache_entry)
2945 })?;
2946 let input_resolution = validate_reused_inputs(spec, resolved, shared_incremental, progress)?;
2947 Ok(Some(WasmBuildOutcome::Reused(complete_build_record(
2948 spec,
2949 BuildRecordInput {
2950 fingerprint,
2951 input_digest: resolved.input_digest,
2952 lock_wait,
2953 shared_incremental: shared_incremental.clone(),
2954 input_resolution,
2955 cargo_build: None,
2956 active_entry: &cache_entry,
2957 },
2958 total_started,
2959 progress,
2960 )?)))
2961}
2962
2963fn validate_reused_inputs(
2964 spec: &WasmBuildSpec,
2965 resolved: &ResolvedCargoBuildInputs,
2966 context: &WasmAcquisitionContext,
2967 progress: &mut ProgressReporter<'_>,
2968) -> Result<WasmInputResolutionTimings, WasmBuildError> {
2969 let mut timings = resolved.timings;
2970 if !context.assumes_sources_immutable {
2971 let verified = verify_resolved_inputs(spec, resolved, progress)?;
2972 timings.include(verified.timings);
2973 }
2974 Ok(timings)
2975}
2976
2977fn verify_resolved_inputs(
2978 spec: &WasmBuildSpec,
2979 resolved: &ResolvedCargoBuildInputs,
2980 progress: &mut ProgressReporter<'_>,
2981) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2982 let verified = resolve_inputs_with_progress(spec, progress)?;
2983 for (before, after) in [
2984 (resolved.validation_digest, verified.validation_digest),
2985 (resolved.fingerprint, verified.fingerprint),
2986 ] {
2987 if before != after {
2988 return Err(WasmBuildError::InputsChangedDuringAcquisition { before, after });
2989 }
2990 }
2991 Ok(verified)
2992}
2993
2994fn reconstruct_exact_cache_entry(
2995 spec: &WasmBuildSpec,
2996 artifacts: &[PathBuf],
2997 cache_entry: &Path,
2998 fingerprint: InputDigest,
2999 progress: &mut ProgressReporter<'_>,
3000) -> Result<Vec<FileDigest>, WasmBuildError> {
3001 let cached_artifacts = expected_artifacts(spec, cache_entry);
3002 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3003 remove_unretained_entry(cache_entry).map_err(wasm_cache_fs_error)?;
3004 create_dir_all(
3005 cache_entry,
3006 "create content-addressed Cargo target directory",
3007 )
3008 })?;
3009 let incomplete = IncompleteBuildDirectory::new(cache_entry.to_owned());
3010 let result = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3011 copy_wasm_artifacts(artifacts, &cached_artifacts)?;
3012 let missing = missing_artifacts(&cached_artifacts);
3013 if !missing.is_empty() {
3014 return Err(WasmBuildError::MissingArtifacts { paths: missing });
3015 }
3016 publish_artifact_stamps(&cached_artifacts, fingerprint)
3019 });
3020 finish_fingerprint_build(result, incomplete, progress)
3021}
3022
3023fn build_wasm_cache_miss(
3024 spec: &WasmBuildSpec,
3025 resolved: ResolvedCargoBuildInputs,
3026 lock_wait: Duration,
3027 shared_incremental: WasmAcquisitionContext,
3028 cargo_target_dir: PathBuf,
3029 total_started: Instant,
3030 progress: &mut ProgressReporter<'_>,
3031) -> Result<WasmBuildOutcome, WasmBuildError> {
3032 let fingerprint = resolved.fingerprint;
3033 let mut input_resolution = resolved.timings;
3034 let artifacts = expected_artifacts(spec, &spec.target_dir);
3035 let cache_entry = cache_entry_directory(spec, fingerprint);
3036 let preparation_started = Instant::now();
3037 progress.begin_phase(WasmBuildFailurePhase::ArtifactPublication);
3038 let preparation_result = (|| {
3039 remove_unretained_entry(&cache_entry).map_err(wasm_cache_fs_error)?;
3040 create_dir_all(
3041 &cache_entry,
3042 "create content-addressed Cargo target directory",
3043 )
3044 })();
3045 progress.record_phase(
3046 WasmBuildFailurePhase::ArtifactPublication,
3047 preparation_started.elapsed(),
3048 );
3049 preparation_result?;
3050 let incomplete_directory = IncompleteBuildDirectory::new(cache_entry.clone());
3051 let build_result = (|| {
3052 if matches!(
3053 spec.cache_mode,
3054 WasmBuildCacheMode::SharedIncremental { .. }
3055 ) {
3056 record_cache_entry_use(&cargo_target_dir)?;
3057 }
3058 let build_started = Instant::now();
3059 progress.begin_phase(WasmBuildFailurePhase::CargoBuild);
3060 let cargo_result = run_cargo_build(spec, &cargo_target_dir, progress);
3061 let cargo_build = build_started.elapsed();
3062 progress.record_phase(WasmBuildFailurePhase::CargoBuild, cargo_build);
3063 cargo_result?;
3064 let built_artifacts = expected_artifacts(spec, &cargo_target_dir);
3065 let validation_started = Instant::now();
3066 progress.begin_phase(WasmBuildFailurePhase::ArtifactPublication);
3067 let missing = missing_artifacts(&built_artifacts);
3068 progress.record_phase(
3069 WasmBuildFailurePhase::ArtifactPublication,
3070 validation_started.elapsed(),
3071 );
3072 if !missing.is_empty() {
3073 return Err(WasmBuildError::MissingArtifacts { paths: missing });
3074 }
3075
3076 let verified = verify_resolved_inputs(spec, &resolved, progress)?;
3077 input_resolution.include(verified.timings);
3078
3079 let publication_guard = shared_incremental.lock_prepared_publication()?;
3084
3085 let cached_artifacts = expected_artifacts(spec, &cache_entry);
3086 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3087 if cargo_target_dir != cache_entry {
3088 copy_wasm_artifacts(&built_artifacts, &cached_artifacts)?;
3089 }
3090 let info = publish_artifact_stamps(&cached_artifacts, fingerprint)?;
3091 materialize_artifacts(&cached_artifacts, &artifacts, fingerprint, &info, false)?;
3092 record_cache_entry_use(&cache_entry)
3093 })?;
3094 drop(publication_guard);
3095
3096 Ok(WasmBuildOutcome::Built(complete_build_record(
3097 spec,
3098 BuildRecordInput {
3099 fingerprint,
3100 input_digest: resolved.input_digest,
3101 lock_wait,
3102 shared_incremental,
3103 input_resolution,
3104 cargo_build: Some(cargo_build),
3105 active_entry: &cache_entry,
3106 },
3107 total_started,
3108 progress,
3109 )?))
3110 })();
3111 finish_fingerprint_build(build_result, incomplete_directory, progress)
3112}
3113
3114pub fn prune_wasm_build_cache(
3123 target_dir: &Path,
3124 policy: ArtifactCachePrunePolicy,
3125) -> Result<ArtifactCachePruneReport, WasmBuildError> {
3126 let (_lock_file, _) = lock_wasm_build_cache(target_dir)?;
3127 ensure_cache_directory_tag(target_dir)?;
3128
3129 prune_wasm_build_cache_locked(target_dir, policy, None)
3130}
3131
3132struct BuildRecordInput<'a> {
3133 fingerprint: InputDigest,
3134 input_digest: InputDigest,
3135 lock_wait: Duration,
3136 shared_incremental: WasmAcquisitionContext,
3137 input_resolution: WasmInputResolutionTimings,
3138 cargo_build: Option<Duration>,
3139 active_entry: &'a Path,
3140}
3141
3142fn complete_build_record(
3143 spec: &WasmBuildSpec,
3144 input: BuildRecordInput<'_>,
3145 total_started: Instant,
3146 progress: &mut ProgressReporter<'_>,
3147) -> Result<WasmBuildRecord, WasmBuildError> {
3148 let retention = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3149 RetainedCacheEntry::acquire(input.active_entry).map_err(wasm_cache_fs_error)
3150 })?;
3151 let (maintenance, cache_maintenance) = spec.prune_policy.map_or((None, None), |policy| {
3152 progress.run_phase(WasmBuildProgressPhase::ExactCacheMaintenance, || {
3153 let cache_root = spec.target_dir.join(".ic-testkit/wasm-targets");
3154 let identity = policy.maintenance_identity();
3155 perform_scheduled_cache_maintenance(&cache_root, spec.prune_interval, &identity, || {
3156 prune_wasm_build_cache_locked(&spec.target_dir, policy, Some(input.active_entry))
3157 .map_err(|error| error.to_string())
3158 })
3159 })
3160 });
3161 Ok(WasmBuildRecord {
3162 fingerprint: input.fingerprint,
3163 input_digest: input.input_digest,
3164 artifacts: expected_artifacts(spec, input.active_entry),
3165 retention,
3166 timings: WasmBuildTimings {
3167 lock_wait: input.lock_wait,
3168 shared_incremental_lock_wait: input.shared_incremental.lock_wait,
3169 input_resolution: input.input_resolution,
3170 cargo_build: input.cargo_build,
3171 cache_maintenance,
3172 total: total_started.elapsed(),
3173 },
3174 maintenance,
3175 shared_incremental_maintenance: input.shared_incremental.maintenance,
3176 })
3177}
3178
3179fn prune_wasm_build_cache_locked(
3180 target_dir: &Path,
3181 policy: ArtifactCachePrunePolicy,
3182 protected_entry: Option<&Path>,
3183) -> Result<ArtifactCachePruneReport, WasmBuildError> {
3184 let cache_root = target_dir.join(".ic-testkit/wasm-targets");
3185 prune_direct_child_directories(&cache_root, policy, protected_entry, is_sha256_directory)
3186 .map_err(wasm_cache_fs_error)
3187}
3188
3189struct IncompleteBuildDirectory {
3190 path: PathBuf,
3191 armed: bool,
3192}
3193
3194impl IncompleteBuildDirectory {
3195 const fn new(path: PathBuf) -> Self {
3196 Self { path, armed: true }
3197 }
3198
3199 fn preserve(mut self) {
3200 self.armed = false;
3201 }
3202
3203 fn cleanup(mut self) -> io::Result<()> {
3204 let result = remove_path_if_present(&self.path);
3205 if result.is_ok() {
3206 self.armed = false;
3207 }
3208 result
3209 }
3210}
3211
3212impl Drop for IncompleteBuildDirectory {
3213 fn drop(&mut self) {
3214 if self.armed {
3215 let _ = remove_path_if_present(&self.path);
3216 }
3217 }
3218}
3219
3220fn finish_fingerprint_build<T>(
3221 result: Result<T, WasmBuildError>,
3222 incomplete_directory: IncompleteBuildDirectory,
3223 progress: &mut ProgressReporter<'_>,
3224) -> Result<T, WasmBuildError> {
3225 match result {
3226 Ok(outcome) => {
3227 incomplete_directory.preserve();
3228 Ok(outcome)
3229 }
3230 Err(build_error) => Err(cleanup_failed_fingerprint_build(
3231 build_error,
3232 incomplete_directory,
3233 progress,
3234 )),
3235 }
3236}
3237
3238fn cleanup_failed_fingerprint_build(
3239 build_error: WasmBuildError,
3240 incomplete_directory: IncompleteBuildDirectory,
3241 progress: &mut ProgressReporter<'_>,
3242) -> WasmBuildError {
3243 let path = incomplete_directory.path.clone();
3244 let primary_phase = progress.failure_phase;
3245 let cleanup_started = Instant::now();
3246 let cleanup = incomplete_directory.cleanup();
3247 progress.record_phase(WasmBuildFailurePhase::Cleanup, cleanup_started.elapsed());
3248 match cleanup {
3249 Ok(()) => {
3250 progress.failure_phase = primary_phase;
3251 build_error
3252 }
3253 Err(source) => WasmBuildError::FailedBuildCleanup {
3254 build_error: Box::new(build_error),
3255 path,
3256 source,
3257 },
3258 }
3259}
3260
3261fn lock_wasm_build_cache(target_dir: &Path) -> Result<(File, Duration), WasmBuildError> {
3262 create_dir_all(target_dir, "create Cargo target directory")?;
3263 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
3264 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)
3265}
3266
3267fn lock_wasm_build_cache_with_progress(
3268 target_dir: &Path,
3269 progress: &mut ProgressReporter<'_>,
3270) -> Result<(File, Duration), WasmBuildError> {
3271 progress.begin_phase(WasmBuildFailurePhase::ExactCacheCoordination);
3272 create_dir_all(target_dir, "create Cargo target directory")?;
3273 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
3274 lock_cache_file_with_progress(&lock_path, WasmBuildProgressPhase::ExactCacheLock, progress)
3275}
3276
3277fn lock_shared_incremental_target(
3278 spec: &WasmBuildSpec,
3279) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3280 lock_shared_incremental_target_internal(spec, None)
3281}
3282
3283fn lock_shared_incremental_target_with_progress(
3284 spec: &WasmBuildSpec,
3285 progress: &mut ProgressReporter<'_>,
3286) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3287 let target_dir = shared_incremental_target(spec)
3288 .expect("validated shared acquisition must have a shared Cargo target");
3289 progress.emit(WasmBuildProgressEvent::SharedTargetLockStarted { target_dir });
3290 let (lock, wait, canonical) = lock_shared_incremental_target_internal(spec, Some(progress))?;
3291 progress.emit(WasmBuildProgressEvent::SharedTargetLockAcquired {
3292 target_dir: canonical.clone(),
3293 wait,
3294 });
3295 Ok((lock, wait, canonical))
3296}
3297
3298fn lock_shared_incremental_target_internal(
3299 spec: &WasmBuildSpec,
3300 mut progress: Option<&mut ProgressReporter<'_>>,
3301) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3302 if let Some(progress) = progress.as_deref_mut() {
3303 progress.begin_phase(WasmBuildFailurePhase::SharedTargetCoordination);
3304 }
3305 let target_dir =
3306 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
3307 message: "shared incremental target is not configured".to_owned(),
3308 })?;
3309 create_dir_all(
3310 &target_dir,
3311 "create shared incremental Cargo target directory",
3312 )?;
3313 ensure_cache_tag(&target_dir).map_err(wasm_cache_fs_error)?;
3314 let canonical = target_dir
3315 .canonicalize()
3316 .map_err(|source| WasmBuildError::Io {
3317 operation: "resolve shared incremental Cargo target directory",
3318 path: target_dir.clone(),
3319 source,
3320 })?;
3321 let lock_path = canonical.join(".ic-testkit/wasm-incremental.lock");
3322 let (lock, wait) = if let Some(progress) = progress {
3323 lock_cache_file_with_progress(
3324 &lock_path,
3325 WasmBuildProgressPhase::SharedTargetLock,
3326 progress,
3327 )?
3328 } else {
3329 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)?
3330 };
3331 Ok((lock, wait, canonical))
3332}
3333
3334fn lock_cache_file_with_progress(
3335 lock_path: &Path,
3336 phase: WasmBuildProgressPhase,
3337 progress: &mut ProgressReporter<'_>,
3338) -> Result<(File, Duration), WasmBuildError> {
3339 let failure_phase = progress_failure_phase(phase);
3340 let started = Instant::now();
3341 progress.begin_phase(failure_phase);
3342 let result = if !progress.is_observed() || progress.config.heartbeat_interval.is_none() {
3343 lock_cache_file(lock_path).map_err(wasm_cache_fs_error)
3344 } else {
3345 let heartbeat_interval = progress
3346 .config
3347 .heartbeat_interval
3348 .expect("observed cache lock must have a heartbeat interval");
3349 lock_cache_file_with_wait_observer(lock_path, heartbeat_interval, |elapsed| {
3350 progress.emit_heartbeat_if_due(phase, elapsed);
3351 })
3352 .map_err(wasm_cache_fs_error)
3353 };
3354 progress.record_phase(failure_phase, started.elapsed());
3355 result
3356}
3357
3358fn ensure_cache_directory_tag(target_dir: &Path) -> Result<(), WasmBuildError> {
3359 ensure_cache_tag(target_dir).map_err(wasm_cache_fs_error)
3360}
3361
3362fn record_cache_entry_use(path: &Path) -> Result<(), WasmBuildError> {
3363 record_entry_use(path).map_err(wasm_cache_fs_error)
3364}
3365
3366fn wasm_cache_fs_error(error: CacheFsError) -> WasmBuildError {
3367 WasmBuildError::Io {
3368 operation: error.operation,
3369 path: error.path,
3370 source: error.source,
3371 }
3372}
3373
3374fn validate_spec(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3375 if spec.packages.is_empty() {
3376 return Err(WasmBuildError::InvalidSpec {
3377 message: "at least one Cargo package is required".to_owned(),
3378 });
3379 }
3380 let mut profile_components = Path::new(&spec.profile_target_dir).components();
3381 if !matches!(
3382 (profile_components.next(), profile_components.next()),
3383 (Some(Component::Normal(_)), None)
3384 ) {
3385 return Err(WasmBuildError::InvalidSpec {
3386 message: "Cargo profile target directory must be one normal path component".to_owned(),
3387 });
3388 }
3389 if spec.target.is_empty() {
3390 return Err(WasmBuildError::InvalidSpec {
3391 message: "Cargo compilation target must not be empty".to_owned(),
3392 });
3393 }
3394 if spec.cargo_profile_args.iter().any(|argument| {
3395 matches!(argument.to_str(), Some("--help" | "--unit-graph"))
3396 || matches!(short_cargo_option(argument), Some((b'h', _)))
3397 }) {
3398 return Err(WasmBuildError::InvalidSpec {
3399 message:
3400 "Cargo help and build-graph flags exit without building and cannot be used for Wasm acquisition"
3401 .to_owned(),
3402 });
3403 }
3404 if spec.extra_env.contains_key(OsStr::new("CARGO_TARGET_DIR"))
3405 || spec.cargo_profile_args.iter().any(|argument| {
3406 argument == OsStr::new("--target-dir")
3407 || argument.as_encoded_bytes().starts_with(b"--target-dir=")
3408 })
3409 {
3410 return Err(WasmBuildError::InvalidSpec {
3411 message: "Cargo target directories are owned by the build specification; use target_dir or with_shared_incremental_target instead of command overrides".to_owned(),
3412 });
3413 }
3414 validate_cargo_target_selection(spec)?;
3415 if spec.cargo_profile_args.iter().any(|argument| {
3416 let option = argument
3417 .as_encoded_bytes()
3418 .split(|byte| *byte == b'=')
3419 .next()
3420 .unwrap_or_default();
3421 matches!(
3422 option,
3423 b"--manifest-path"
3424 | b"--target"
3425 | b"--package"
3426 | b"--workspace"
3427 | b"--all"
3428 | b"--exclude"
3429 | b"--config"
3430 ) || matches!(short_cargo_option(argument), Some((b'm' | b'p' | b'C', _)))
3431 }) {
3432 return Err(WasmBuildError::InvalidSpec {
3433 message: "Cargo workspace, package, target, and configuration inputs are owned by the build specification; use workspace_root, packages, with_target, and discovered Cargo configuration files or with_extra_env instead of command overrides".to_owned(),
3434 });
3435 }
3436 validate_cargo_profile(spec)?;
3437 if matches!(
3438 &spec.cache_mode,
3439 WasmBuildCacheMode::SharedIncremental { target_dir } if target_dir.as_os_str().is_empty()
3440 ) {
3441 return Err(WasmBuildError::InvalidSpec {
3442 message: "shared incremental Cargo target directory must not be empty".to_owned(),
3443 });
3444 }
3445 if spec.shared_incremental_maintenance_config.is_some()
3446 && !matches!(
3447 spec.cache_mode,
3448 WasmBuildCacheMode::SharedIncremental { .. }
3449 )
3450 {
3451 return Err(WasmBuildError::InvalidSpec {
3452 message:
3453 "scheduled shared-target maintenance requires a shared incremental Cargo target"
3454 .to_owned(),
3455 });
3456 }
3457 Ok(())
3458}
3459
3460fn validate_cargo_target_selection(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3461 if spec.cargo_profile_args.iter().any(|argument| {
3462 let option = argument
3463 .as_encoded_bytes()
3464 .split(|byte| *byte == b'=')
3465 .next()
3466 .unwrap_or_default();
3467 matches!(
3468 option,
3469 b"--bin"
3470 | b"--bins"
3471 | b"--example"
3472 | b"--examples"
3473 | b"--test"
3474 | b"--tests"
3475 | b"--bench"
3476 | b"--benches"
3477 | b"--all-targets"
3478 )
3479 }) {
3480 return Err(WasmBuildError::InvalidSpec {
3481 message: "Wasm acquisition builds canister libraries only; remove other Cargo target selectors".to_owned(),
3482 });
3483 }
3484 Ok(())
3485}
3486
3487fn validate_cargo_profile(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3488 let mut selected = None;
3489 let mut arguments = spec.cargo_profile_args.iter();
3490 while let Some(argument) = arguments.next() {
3491 let profile = if argument == "--profile" {
3492 Some(
3493 arguments
3494 .next()
3495 .and_then(|value| value.to_str())
3496 .ok_or_else(|| WasmBuildError::InvalidSpec {
3497 message: "Cargo --profile requires a UTF-8 profile name".to_owned(),
3498 })?,
3499 )
3500 } else if let Some(profile) = argument.to_str().and_then(|s| s.strip_prefix("--profile=")) {
3501 Some(profile)
3502 } else {
3503 let bytes = argument.as_encoded_bytes();
3504 let short_option = short_cargo_option(argument);
3507 let flags_end = short_option.map_or(bytes.len(), |(_, index)| index);
3508 let release = argument == "--release"
3509 || (bytes.starts_with(b"-")
3510 && !bytes.starts_with(b"--")
3511 && bytes[..flags_end].contains(&b'r'));
3512 if matches!(short_option, Some((_, index)) if index + 1 == bytes.len()) {
3513 arguments.next();
3514 }
3515 release.then_some("release")
3516 };
3517 if let Some(profile) = profile
3518 && (profile.is_empty() || selected.replace(profile).is_some())
3519 {
3520 return Err(WasmBuildError::InvalidSpec {
3521 message: "select one nonempty Cargo profile".to_owned(),
3522 });
3523 }
3524 }
3525 let profile = selected.unwrap_or("dev");
3526 let expected = match profile {
3527 "dev" | "test" => "debug",
3528 "bench" => "release",
3529 custom => custom,
3530 };
3531 if spec.profile_target_dir != expected {
3532 return Err(WasmBuildError::InvalidSpec {
3533 message: format!(
3534 "Cargo profile {profile:?} writes to {expected:?}, but profile target directory is {:?}; select matching Cargo profile arguments and output directory",
3535 spec.profile_target_dir,
3536 ),
3537 });
3538 }
3539 Ok(())
3540}
3541
3542fn build_fingerprint(spec: &WasmBuildSpec) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3543 build_fingerprint_with_progress(spec, &mut ProgressReporter::silent())
3544}
3545
3546fn build_fingerprint_with_progress(
3547 spec: &WasmBuildSpec,
3548 progress: &mut ProgressReporter<'_>,
3549) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3550 let total_started = Instant::now();
3551 let (cargo_identity, rustc_identity, tool_identity) = resolve_tool_identity(spec, progress)?;
3552
3553 let metadata_started = Instant::now();
3554 let metadata = progress.run_phase(WasmBuildProgressPhase::CargoMetadata, || {
3555 cargo_metadata(spec)
3556 })?;
3557 let cargo_metadata = metadata_started.elapsed();
3558
3559 let discovery_started = Instant::now();
3560 let (inputs, exclusions) =
3561 progress.run_phase(WasmBuildProgressPhase::InputDiscovery, || {
3562 let parsed = ParsedCargoMetadata::parse(&metadata)
3563 .map_err(|message| invalid_metadata(&message))?;
3564 resolve_local_inputs(spec, &parsed)
3565 })?;
3566 let input_discovery = discovery_started.elapsed();
3567
3568 let hashing_started = Instant::now();
3569 let (input_digest, validation_digest) =
3570 progress.run_phase(WasmBuildProgressPhase::ContentHashing, || {
3571 let mut cache = LabeledPathDigestCache::default();
3572 digest_resolved_local_inputs(
3573 &inputs,
3574 &exclusions,
3575 &mut cache,
3576 &spec.workspace_root,
3577 "hash Wasm build inputs",
3578 "hash semantic Wasm build inputs",
3579 )
3580 })?;
3581 let content_hashing = hashing_started.elapsed();
3582
3583 let fingerprint =
3584 finish_build_fingerprint(spec, &cargo_identity, &rustc_identity, input_digest);
3585 Ok(ResolvedCargoBuildInputs {
3586 fingerprint,
3587 input_digest,
3588 validation_digest,
3589 inputs: inputs
3590 .validation_inputs
3591 .into_iter()
3592 .map(|(label, path)| CargoBuildInput { label, path })
3593 .collect(),
3594 exclusions: exclusions.into(),
3595 timings: WasmInputResolutionTimings {
3596 tool_identity,
3597 cargo_metadata,
3598 input_discovery,
3599 content_hashing,
3600 total: total_started.elapsed(),
3601 },
3602 })
3603}
3604
3605fn finish_build_fingerprint(
3606 spec: &WasmBuildSpec,
3607 cargo_identity: &[u8],
3608 rustc_identity: &[u8],
3609 input_digest: InputDigest,
3610) -> InputDigest {
3611 let mut hasher = InputHasher::new(CACHE_FORMAT_VERSION);
3612 for package in &spec.packages {
3613 hasher.field("package", package.as_bytes());
3614 }
3615 hasher.field("target", spec.target.as_bytes());
3616 hasher.field("cargo-target-selection", b"lib");
3617 hasher.field("profile-target-dir", spec.profile_target_dir.as_bytes());
3618 for argument in &spec.cargo_profile_args {
3619 hasher.field("cargo-argument", &os_bytes(argument));
3620 }
3621 for (key, value) in effective_environment(spec) {
3622 hasher.field("environment-key", &os_bytes(&key));
3623 if let Some(value) = value {
3624 hasher.field("environment-value", &os_bytes(&value));
3625 } else {
3626 hasher.field("environment-unset", b"");
3627 }
3628 }
3629 hasher.field("cargo-identity", cargo_identity);
3630 hasher.field("rustc-identity", rustc_identity);
3631 hasher.field("source-input-digest", input_digest.as_bytes());
3632 hasher.finish()
3633}
3634
3635fn command_identity(
3636 spec: &WasmBuildSpec,
3637 phase: WasmBuildPhase,
3638 program: &OsStr,
3639 arguments: &[&str],
3640) -> Result<Vec<u8>, WasmBuildError> {
3641 let mut command = Command::new(program);
3642 command.current_dir(&spec.workspace_root).args(arguments);
3643 apply_command_environment(&mut command, spec);
3644 let output = command
3645 .output()
3646 .map_err(|source| WasmBuildError::CommandSpawn {
3647 phase,
3648 program: program.to_owned(),
3649 source,
3650 })?;
3651 ensure_command_success(phase, output).map(|output| {
3652 let mut identity = output.stdout;
3653 identity.extend_from_slice(&output.stderr);
3654 identity
3655 })
3656}
3657
3658fn cargo_metadata(spec: &WasmBuildSpec) -> Result<Value, WasmBuildError> {
3659 let mut command = Command::new(&spec.cargo_program);
3660 command
3661 .current_dir(&spec.workspace_root)
3662 .args(["metadata", "--format-version", "1"]);
3663 for argument in metadata_arguments(&spec.cargo_profile_args) {
3664 command.arg(argument);
3665 }
3666 apply_command_environment(&mut command, spec);
3667 let output = command
3668 .output()
3669 .map_err(|source| WasmBuildError::CommandSpawn {
3670 phase: WasmBuildPhase::CargoMetadata,
3671 program: spec.cargo_program.clone(),
3672 source,
3673 })?;
3674 let output = ensure_command_success(WasmBuildPhase::CargoMetadata, output)?;
3675 serde_json::from_slice(&output.stdout).map_err(|error| WasmBuildError::InvalidMetadata {
3676 message: format!("Cargo metadata was not valid JSON: {error}"),
3677 })
3678}
3679
3680fn metadata_arguments(arguments: &[OsString]) -> Vec<OsString> {
3681 let mut selected = Vec::new();
3682 let mut arguments = arguments.iter();
3683 while let Some(argument) = arguments.next() {
3684 if let Some((b'F', index)) = short_cargo_option(argument) {
3685 selected.push(argument.to_str().map_or_else(
3690 || argument.clone(),
3691 |text| OsString::from(format!("-F{}", &text[index + 1..])),
3692 ));
3693 if index + 1 == argument.as_encoded_bytes().len()
3694 && let Some(value) = arguments.next()
3695 {
3696 selected.push(value.clone());
3697 }
3698 continue;
3699 }
3700 let argument_text = argument.to_string_lossy();
3701 match argument_text.as_ref() {
3702 "--all-features" | "--no-default-features" | "--locked" | "--offline" | "--frozen" => {
3703 selected.push(argument.clone());
3704 }
3705 "--features" | "--filter-platform" => {
3706 selected.push(argument.clone());
3707 if let Some(value) = arguments.next() {
3708 selected.push(value.clone());
3709 }
3710 }
3711 _ if argument_text.starts_with("--features=")
3712 || argument_text.starts_with("--filter-platform=") =>
3713 {
3714 selected.push(argument.clone());
3715 }
3716 _ => {}
3717 }
3718 }
3719 selected
3720}
3721
3722fn short_cargo_option(argument: &OsStr) -> Option<(u8, usize)> {
3726 let bytes = argument.as_encoded_bytes();
3727 if !bytes.starts_with(b"-") || bytes.starts_with(b"--") {
3728 return None;
3729 }
3730 for (index, byte) in bytes.iter().copied().enumerate().skip(1) {
3731 match byte {
3732 b'v' | b'q' | b'r' => {}
3733 b'h' | b'F' | b'j' | b'Z' | b'm' | b'p' | b'C' => return Some((byte, index)),
3734 _ => return None,
3735 }
3736 }
3737 None
3738}
3739
3740struct MetadataPackage<'a> {
3741 id: &'a str,
3742 name: &'a str,
3743 version: &'a str,
3744 manifest_path: PathBuf,
3745 is_local: bool,
3746 source: Option<&'a str>,
3747 semantic_fields: Vec<(&'static str, Option<String>)>,
3748}
3749
3750struct ParsedCargoMetadata<'a> {
3753 packages: HashMap<&'a str, MetadataPackage<'a>>,
3754 workspace_members: HashSet<&'a str>,
3755 nodes: HashMap<&'a str, MetadataNode<'a>>,
3756 workspace_root: Option<&'a str>,
3757}
3758
3759struct MetadataNode<'a> {
3760 dependencies: Vec<&'a str>,
3761 value: &'a Value,
3762}
3763
3764impl<'a> ParsedCargoMetadata<'a> {
3765 fn parse(metadata: &'a Value) -> Result<Self, String> {
3766 let packages = metadata_packages(metadata)?;
3767 let workspace_members = metadata
3768 .get("workspace_members")
3769 .and_then(Value::as_array)
3770 .ok_or_else(|| "Cargo metadata has no workspace member array".to_owned())?
3771 .iter()
3772 .filter_map(Value::as_str)
3773 .collect();
3774 let values = metadata
3775 .pointer("/resolve/nodes")
3776 .and_then(Value::as_array)
3777 .ok_or_else(|| "Cargo metadata has no resolved dependency nodes".to_owned())?;
3778 let mut nodes = HashMap::new();
3779 for value in values {
3780 let id = required_string(value, "id")?;
3781 let dependencies = value
3782 .get("deps")
3783 .and_then(Value::as_array)
3784 .ok_or_else(|| "Cargo metadata dependency node has no deps array".to_owned())?
3785 .iter()
3786 .map(|dependency| required_string(dependency, "pkg"))
3787 .collect::<Result<_, _>>()?;
3788 nodes.insert(
3789 id,
3790 MetadataNode {
3791 dependencies,
3792 value,
3793 },
3794 );
3795 }
3796 Ok(Self {
3797 packages,
3798 workspace_members,
3799 nodes,
3800 workspace_root: metadata.get("workspace_root").and_then(Value::as_str),
3801 })
3802 }
3803}
3804
3805const SEMANTIC_PACKAGE_FIELDS: &[&str] = &[
3806 "authors",
3807 "default_run",
3808 "description",
3809 "documentation",
3810 "edition",
3811 "homepage",
3812 "license",
3813 "license_file",
3814 "links",
3815 "metadata",
3816 "name",
3817 "readme",
3818 "repository",
3819 "rust_version",
3820 "version",
3821];
3822
3823struct LockedPackageIdentity {
3824 name: String,
3825 version: String,
3826 source: String,
3827 checksum: Option<String>,
3828}
3829
3830fn resolve_local_inputs(
3831 spec: &WasmBuildSpec,
3832 metadata: &ParsedCargoMetadata<'_>,
3833) -> Result<(ResolvedLocalInputs, Vec<PathBuf>), WasmBuildError> {
3834 let mut selected_ids = selected_package_ids(spec, metadata)?;
3835 let mut closure = BTreeSet::new();
3836 while let Some(id) = selected_ids.pop_front() {
3837 if !closure.insert(id) {
3838 continue;
3839 }
3840 if let Some(node) = metadata.nodes.get(id) {
3841 selected_ids.extend(node.dependencies.iter().copied());
3842 }
3843 }
3844
3845 let workspace_root = metadata
3846 .workspace_root
3847 .map_or_else(|| spec.workspace_root.clone(), PathBuf::from);
3848 let workspace_projection = semantic_workspace_projection(metadata, &closure, &workspace_root)?;
3849 let mut validation_inputs = workspace_configuration_inputs(spec, &workspace_root)?;
3850 append_package_inputs(
3851 &mut validation_inputs,
3852 &metadata.packages,
3853 closure,
3854 &workspace_root,
3855 )?;
3856 append_additional_inputs(&mut validation_inputs, spec, &workspace_root);
3857 validate_shared_incremental_target_boundary(spec, &validation_inputs)?;
3858 let exclusions = source_exclusions(spec, &validation_inputs);
3859 Ok((
3860 ResolvedLocalInputs {
3861 validation_inputs,
3862 workspace_projection,
3863 },
3864 exclusions,
3865 ))
3866}
3867
3868fn metadata_packages(metadata: &Value) -> Result<HashMap<&str, MetadataPackage<'_>>, String> {
3869 let packages_value = metadata
3870 .get("packages")
3871 .and_then(Value::as_array)
3872 .ok_or_else(|| "Cargo metadata has no package array".to_owned())?;
3873 let mut packages = HashMap::new();
3874 for value in packages_value {
3875 let source = optional_string(value, "source")?;
3876 let package = MetadataPackage {
3877 id: required_string(value, "id")?,
3878 name: required_string(value, "name")?,
3879 version: required_string(value, "version")?,
3880 manifest_path: PathBuf::from(required_string(value, "manifest_path")?),
3881 is_local: value.get("source").is_some_and(Value::is_null),
3882 source,
3883 semantic_fields: SEMANTIC_PACKAGE_FIELDS
3884 .iter()
3885 .map(|field| (*field, value.get(*field).map(Value::to_string)))
3886 .collect(),
3887 };
3888 packages.insert(package.id, package);
3889 }
3890 Ok(packages)
3891}
3892
3893fn selected_package_ids<'a>(
3894 spec: &WasmBuildSpec,
3895 metadata: &ParsedCargoMetadata<'a>,
3896) -> Result<VecDeque<&'a str>, WasmBuildError> {
3897 let mut selected_ids = VecDeque::new();
3898 for requested in &spec.packages {
3899 let mut matches = metadata
3900 .packages
3901 .values()
3902 .filter(|package| {
3903 package.name == *requested && metadata.workspace_members.contains(package.id)
3904 })
3905 .map(|package| package.id);
3906 match (matches.next(), matches.next()) {
3907 (Some(id), None) => selected_ids.push_back(id),
3908 (None, _) => {
3909 return Err(WasmBuildError::InvalidSpec {
3910 message: format!("Cargo workspace contains no package named `{requested}`"),
3911 });
3912 }
3913 _ => {
3914 return Err(WasmBuildError::InvalidSpec {
3915 message: format!("Cargo workspace package name `{requested}` is ambiguous"),
3916 });
3917 }
3918 }
3919 }
3920 Ok(selected_ids)
3921}
3922
3923fn semantic_workspace_projection(
3924 metadata: &ParsedCargoMetadata<'_>,
3925 closure: &BTreeSet<&str>,
3926 workspace_root: &Path,
3927) -> Result<Option<InputDigest>, WasmBuildError> {
3928 let locked_packages = locked_package_identities(workspace_root)?;
3931 let mut identities = HashMap::new();
3932 for &id in closure {
3933 let package = metadata
3934 .packages
3935 .get(id)
3936 .ok_or_else(|| invalid_metadata(&format!("resolved package `{id}` is missing")))?;
3937 let Some(identity) = semantic_package_identity(package, workspace_root, &locked_packages)
3938 else {
3939 return Ok(None);
3940 };
3941 identities.insert(id, identity);
3942 }
3943
3944 let mut projected_packages = closure
3945 .iter()
3946 .map(|&id| {
3947 let package = metadata
3948 .packages
3949 .get(id)
3950 .expect("selected package closure was validated above");
3951 let identity = identities[id];
3952 let node = metadata.nodes.get(id).ok_or_else(|| {
3953 invalid_metadata(&format!("resolved package `{id}` has no dependency node"))
3954 })?;
3955 let projection = semantic_package_projection(package, node.value, &identities)?;
3956 Ok::<_, WasmBuildError>((identity, projection))
3957 })
3958 .collect::<Result<Vec<_>, _>>()?;
3959 projected_packages.sort_by_key(|(identity, _)| *identity);
3960
3961 let root_manifest = workspace_root.join("Cargo.toml");
3962 let root_contents =
3963 fs::read_to_string(&root_manifest).map_err(|source| WasmBuildError::Io {
3964 operation: "read workspace manifest for semantic projection",
3965 path: root_manifest.clone(),
3966 source,
3967 })?;
3968 let root = toml::from_str::<TomlValue>(&root_contents).map_err(|error| {
3969 invalid_metadata(&format!(
3970 "workspace manifest could not be projected as TOML: {error}"
3971 ))
3972 })?;
3973
3974 let mut hasher = InputHasher::new("wasm-semantic-workspace-projection-v1");
3975 for (identity, projection) in projected_packages {
3976 hasher.field("package-identity", identity.as_bytes());
3977 hasher.field("package-projection", projection.as_bytes());
3978 }
3979 hash_toml_setting(&mut hasher, "cargo-features", root.get("cargo-features"));
3980 hash_toml_setting(&mut hasher, "profile", root.get("profile"));
3981 let workspace = root.get("workspace").and_then(TomlValue::as_table);
3982 hash_toml_setting(
3983 &mut hasher,
3984 "workspace-resolver",
3985 workspace.and_then(|table| table.get("resolver")),
3986 );
3987 hash_toml_setting(
3988 &mut hasher,
3989 "workspace-lints",
3990 workspace.and_then(|table| table.get("lints")),
3991 );
3992 Ok(Some(hasher.finish()))
3993}
3994
3995fn locked_package_identities(
3996 workspace_root: &Path,
3997) -> Result<Vec<LockedPackageIdentity>, WasmBuildError> {
3998 let lockfile = workspace_root.join("Cargo.lock");
3999 let contents = match fs::read_to_string(&lockfile) {
4000 Ok(contents) => contents,
4001 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
4002 Err(source) => {
4003 return Err(WasmBuildError::Io {
4004 operation: "read Cargo lockfile for semantic projection",
4005 path: lockfile,
4006 source,
4007 });
4008 }
4009 };
4010 let lock = toml::from_str::<TomlValue>(&contents).map_err(|error| {
4011 invalid_metadata(&format!(
4012 "Cargo lockfile could not be projected as TOML: {error}"
4013 ))
4014 })?;
4015 let Some(packages) = lock.get("package").and_then(TomlValue::as_array) else {
4016 return Ok(Vec::new());
4017 };
4018 packages
4019 .iter()
4020 .filter_map(|package| {
4021 let Some(table) = package.as_table() else {
4022 return Some(Err(invalid_metadata(
4023 "Cargo lockfile package entry is not a table",
4024 )));
4025 };
4026 let source = table.get("source")?.as_str().map(str::to_owned);
4027 Some(
4028 source
4029 .ok_or_else(|| {
4030 invalid_metadata("Cargo lockfile package source is not a string")
4031 })
4032 .and_then(|source| {
4033 Ok(LockedPackageIdentity {
4034 name: required_toml_string(table, "name", "Cargo lockfile package")?,
4035 version: required_toml_string(
4036 table,
4037 "version",
4038 "Cargo lockfile package",
4039 )?,
4040 source,
4041 checksum: optional_toml_string(
4042 table,
4043 "checksum",
4044 "Cargo lockfile package",
4045 )?,
4046 })
4047 }),
4048 )
4049 })
4050 .collect()
4051}
4052
4053fn required_toml_string(
4054 table: &toml::Table,
4055 field: &str,
4056 context: &str,
4057) -> Result<String, WasmBuildError> {
4058 table
4059 .get(field)
4060 .and_then(TomlValue::as_str)
4061 .map(str::to_owned)
4062 .ok_or_else(|| invalid_metadata(&format!("{context} `{field}` is missing or not a string")))
4063}
4064
4065fn optional_toml_string(
4066 table: &toml::Table,
4067 field: &str,
4068 context: &str,
4069) -> Result<Option<String>, WasmBuildError> {
4070 match table.get(field) {
4071 None => Ok(None),
4072 Some(TomlValue::String(value)) => Ok(Some(value.clone())),
4073 Some(_) => Err(invalid_metadata(&format!(
4074 "{context} `{field}` is not a string"
4075 ))),
4076 }
4077}
4078
4079fn semantic_package_identity(
4080 package: &MetadataPackage<'_>,
4081 workspace_root: &Path,
4082 locked_packages: &[LockedPackageIdentity],
4083) -> Option<InputDigest> {
4084 let mut hasher = InputHasher::new("wasm-semantic-package-identity-v1");
4085 hasher.field("name", package.name.as_bytes());
4086 hasher.field("version", package.version.as_bytes());
4087 if package.is_local {
4088 let manifest = package.manifest_path.strip_prefix(workspace_root).ok()?;
4089 let package_root = package.manifest_path.parent()?;
4090 if package_root == workspace_root {
4091 return None;
4092 }
4093 hasher.field("local-manifest", &os_bytes(manifest.as_os_str()));
4094 } else {
4095 let metadata_source = package.source?;
4096 let locked = locked_packages.iter().find(|locked| {
4097 locked.name == package.name
4098 && locked.version == package.version
4099 && locked.source == metadata_source
4100 })?;
4101 match locked.source.as_str() {
4102 source if source.starts_with("registry+") && locked.checksum.is_some() => {}
4103 source if source.starts_with("git+") && source.contains('#') => {}
4104 _ => return None,
4105 }
4106 hasher.field("external-package-id", package.id.as_bytes());
4107 hasher.field("external-source", locked.source.as_bytes());
4108 hasher.field(
4109 "external-checksum",
4110 locked.checksum.as_deref().unwrap_or_default().as_bytes(),
4111 );
4112 }
4113 Some(hasher.finish())
4114}
4115
4116fn semantic_package_projection(
4117 package: &MetadataPackage<'_>,
4118 node: &Value,
4119 identities: &HashMap<&str, InputDigest>,
4120) -> Result<InputDigest, WasmBuildError> {
4121 let mut hasher = InputHasher::new("wasm-semantic-package-projection-v1");
4125 for (field, value) in &package.semantic_fields {
4126 hasher.field("package-field-name", field.as_bytes());
4127 match value {
4128 Some(value) => hasher.field("package-field-value", value.as_bytes()),
4129 None => hasher.field("package-field-missing", b""),
4130 }
4131 }
4132
4133 let mut features = node
4134 .get("features")
4135 .and_then(Value::as_array)
4136 .ok_or_else(|| invalid_metadata("Cargo metadata dependency node has no features array"))?
4137 .iter()
4138 .map(|feature| {
4139 feature.as_str().ok_or_else(|| {
4140 invalid_metadata("Cargo metadata dependency feature is not a string")
4141 })
4142 })
4143 .collect::<Result<Vec<_>, _>>()?;
4144 features.sort_unstable();
4145 for feature in features {
4146 hasher.field("enabled-feature", feature.as_bytes());
4147 }
4148
4149 let mut dependencies = node
4150 .get("deps")
4151 .and_then(Value::as_array)
4152 .ok_or_else(|| invalid_metadata("Cargo metadata dependency node has no deps array"))?
4153 .iter()
4154 .map(|dependency| {
4155 let name = required_string(dependency, "name")
4156 .map_err(|message| invalid_metadata(&message))?;
4157 let package_id =
4158 required_string(dependency, "pkg").map_err(|message| invalid_metadata(&message))?;
4159 let identity = identities.get(package_id).copied().ok_or_else(|| {
4160 invalid_metadata(&format!(
4161 "dependency `{package_id}` is outside the selected package closure"
4162 ))
4163 })?;
4164 let kinds = dependency
4165 .get("dep_kinds")
4166 .ok_or_else(|| invalid_metadata("Cargo metadata dependency has no kind array"))?
4167 .to_string();
4168 Ok::<_, WasmBuildError>((name, identity, kinds))
4169 })
4170 .collect::<Result<Vec<_>, _>>()?;
4171 dependencies.sort();
4172 for (name, identity, kinds) in dependencies {
4173 hasher.field("dependency-name", name.as_bytes());
4174 hasher.field("dependency-identity", identity.as_bytes());
4175 hasher.field("dependency-kinds", kinds.as_bytes());
4176 }
4177 Ok(hasher.finish())
4178}
4179
4180fn hash_toml_setting(hasher: &mut InputHasher, label: &str, value: Option<&TomlValue>) {
4181 hasher.field("workspace-setting-name", label.as_bytes());
4182 match value {
4183 Some(value) => hasher.field("workspace-setting-value", value.to_string().as_bytes()),
4184 None => hasher.field("workspace-setting-missing", b""),
4185 }
4186}
4187
4188fn is_broad_workspace_input(label: &Path) -> bool {
4189 label == Path::new("workspace/Cargo.toml") || label == Path::new("workspace/Cargo.lock")
4190}
4191
4192fn digest_resolved_local_inputs(
4193 inputs: &ResolvedLocalInputs,
4194 exclusions: &[PathBuf],
4195 cache: &mut LabeledPathDigestCache,
4196 error_path: &Path,
4197 validation_operation: &'static str,
4198 semantic_operation: &'static str,
4199) -> Result<(InputDigest, InputDigest), WasmBuildError> {
4200 let validation_digest = digest_labeled_paths_composable(
4201 "wasm-source-inputs-v1",
4202 inputs
4203 .validation_inputs
4204 .iter()
4205 .map(|(label, path)| (label.as_path(), path.as_path())),
4206 exclusions,
4207 cache,
4208 )
4209 .map_err(|source| WasmBuildError::Io {
4210 operation: validation_operation,
4211 path: error_path.to_owned(),
4212 source,
4213 })?;
4214 let input_digest = semantic_input_digest(inputs, validation_digest, exclusions, cache)
4215 .map_err(|source| WasmBuildError::Io {
4216 operation: semantic_operation,
4217 path: error_path.to_owned(),
4218 source,
4219 })?;
4220 Ok((input_digest, validation_digest))
4221}
4222
4223fn semantic_input_digest(
4224 inputs: &ResolvedLocalInputs,
4225 validation_digest: InputDigest,
4226 exclusions: &[PathBuf],
4227 cache: &mut LabeledPathDigestCache,
4228) -> io::Result<InputDigest> {
4229 let Some(workspace) = inputs.workspace_projection else {
4230 return Ok(validation_digest);
4231 };
4232 let path_digest = digest_labeled_paths_composable(
4233 "wasm-source-inputs-v1",
4234 inputs
4235 .validation_inputs
4236 .iter()
4237 .filter(|(label, _)| !is_broad_workspace_input(label))
4238 .map(|(label, path)| (label.as_path(), path.as_path())),
4239 exclusions,
4240 cache,
4241 )?;
4242 let mut hasher = InputHasher::new("wasm-semantic-source-inputs-v1");
4243 hasher.field("path-input-digest", path_digest.as_bytes());
4244 hasher.field("workspace-projection", workspace.as_bytes());
4245 Ok(hasher.finish())
4246}
4247
4248fn workspace_configuration_inputs(
4249 spec: &WasmBuildSpec,
4250 workspace_root: &Path,
4251) -> Result<Vec<(PathBuf, PathBuf)>, WasmBuildError> {
4252 let mut inputs = Vec::new();
4253 add_if_present(
4254 &mut inputs,
4255 "workspace/Cargo.toml",
4256 workspace_root.join("Cargo.toml"),
4257 );
4258 add_if_present(
4259 &mut inputs,
4260 "workspace/Cargo.lock",
4261 workspace_root.join("Cargo.lock"),
4262 );
4263 add_if_present(
4264 &mut inputs,
4265 "workspace/rust-toolchain.toml",
4266 workspace_root.join("rust-toolchain.toml"),
4267 );
4268 add_if_present(
4269 &mut inputs,
4270 "workspace/rust-toolchain",
4271 workspace_root.join("rust-toolchain"),
4272 );
4273 append_cargo_configuration_inputs(&mut inputs, spec, workspace_root)?;
4274 Ok(inputs)
4275}
4276
4277fn append_cargo_configuration_inputs(
4278 inputs: &mut Vec<(PathBuf, PathBuf)>,
4279 spec: &WasmBuildSpec,
4280 workspace_root: &Path,
4281) -> Result<(), WasmBuildError> {
4282 let invocation_root =
4283 spec.workspace_root
4284 .canonicalize()
4285 .map_err(|source| WasmBuildError::Io {
4286 operation: "resolve Cargo invocation directory",
4287 path: spec.workspace_root.clone(),
4288 source,
4289 })?;
4290 let canonical_workspace =
4291 workspace_root
4292 .canonicalize()
4293 .map_err(|source| WasmBuildError::Io {
4294 operation: "resolve Cargo workspace directory",
4295 path: workspace_root.to_owned(),
4296 source,
4297 })?;
4298
4299 let mut roots = invocation_root
4300 .ancestors()
4301 .filter_map(|directory| effective_cargo_config(&directory.join(".cargo")))
4302 .collect::<Vec<_>>();
4303 if let Some(cargo_home) = effective_cargo_home(spec, &invocation_root)
4304 && let Some(config) = effective_cargo_config(&cargo_home)
4305 {
4306 roots.push(config);
4307 }
4308
4309 let mut active = BTreeSet::new();
4310 for config in roots {
4311 append_cargo_configuration_tree(inputs, &config, &canonical_workspace, &mut active, false)?;
4312 }
4313 Ok(())
4314}
4315
4316fn effective_cargo_config(directory: &Path) -> Option<PathBuf> {
4317 let extensionless = directory.join("config");
4318 if extensionless.exists() {
4319 return Some(extensionless);
4320 }
4321 let toml = directory.join("config.toml");
4322 toml.exists().then_some(toml)
4323}
4324
4325fn effective_cargo_home(spec: &WasmBuildSpec, invocation_root: &Path) -> Option<PathBuf> {
4326 if let Some(cargo_home) = command_environment_value(spec, "CARGO_HOME") {
4327 let cargo_home = PathBuf::from(cargo_home);
4328 return Some(if cargo_home.is_absolute() {
4329 cargo_home
4330 } else {
4331 invocation_root.join(cargo_home)
4332 });
4333 }
4334
4335 default_home_directory(spec).map(|home| {
4336 let home = if home.is_absolute() {
4337 home
4338 } else {
4339 invocation_root.join(home)
4340 };
4341 home.join(".cargo")
4342 })
4343}
4344
4345#[cfg(windows)]
4346fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
4347 command_environment_value(spec, "USERPROFILE")
4348 .or_else(|| command_environment_value(spec, "HOME"))
4349 .map(PathBuf::from)
4350}
4351
4352#[cfg(not(windows))]
4353fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
4354 command_environment_value(spec, "HOME").map(PathBuf::from)
4355}
4356
4357fn command_environment_value(spec: &WasmBuildSpec, name: &str) -> Option<OsString> {
4358 spec.extra_env
4359 .get(OsStr::new(name))
4360 .cloned()
4361 .or_else(|| std::env::var_os(name))
4362}
4363
4364fn append_cargo_configuration_tree(
4365 inputs: &mut Vec<(PathBuf, PathBuf)>,
4366 config: &Path,
4367 workspace_root: &Path,
4368 active: &mut BTreeSet<PathBuf>,
4369 optional: bool,
4370) -> Result<(), WasmBuildError> {
4371 let contents = match fs::read_to_string(config) {
4372 Ok(contents) => contents,
4373 Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
4374 Err(source) => {
4375 return Err(WasmBuildError::Io {
4376 operation: "read Cargo configuration",
4377 path: config.to_owned(),
4378 source,
4379 });
4380 }
4381 };
4382 let parent = config
4383 .parent()
4384 .ok_or_else(|| WasmBuildError::InvalidCargoConfiguration {
4385 path: config.to_owned(),
4386 message: "configuration path has no parent directory".to_owned(),
4387 })?;
4388 let location = parent
4391 .canonicalize()
4392 .map_err(|source| WasmBuildError::Io {
4393 operation: "resolve Cargo configuration directory",
4394 path: parent.to_owned(),
4395 source,
4396 })?
4397 .join(config.file_name().ok_or_else(|| {
4398 invalid_cargo_configuration(config, "configuration path has no file name")
4399 })?);
4400 if !active.insert(location.clone()) {
4403 return Ok(());
4404 }
4405 let configuration = toml::from_str::<TomlValue>(&contents).map_err(|error| {
4406 WasmBuildError::InvalidCargoConfiguration {
4407 path: config.to_owned(),
4408 message: error.to_string(),
4409 }
4410 })?;
4411 if !inputs.iter().any(|(_, path)| path == config) {
4414 inputs.push((
4415 cargo_configuration_label(&location, workspace_root),
4416 config.to_owned(),
4417 ));
4418 }
4419 if let Some(include) = configuration.get("include") {
4420 for (included, optional) in cargo_configuration_includes(include, config)? {
4421 let included = if included.is_absolute() {
4422 included
4423 } else {
4424 parent.join(included)
4425 };
4426 append_cargo_configuration_tree(inputs, &included, workspace_root, active, optional)?;
4427 }
4428 }
4429 active.remove(&location);
4430 Ok(())
4431}
4432
4433fn cargo_configuration_includes(
4434 include: &TomlValue,
4435 config: &Path,
4436) -> Result<Vec<(PathBuf, bool)>, WasmBuildError> {
4437 let values = match include {
4438 TomlValue::Array(values) => values.as_slice(),
4439 value => std::slice::from_ref(value),
4440 };
4441 values
4442 .iter()
4443 .map(|value| match value {
4444 TomlValue::String(path) => Ok((PathBuf::from(path), false)),
4445 TomlValue::Table(table) => {
4446 let path = table
4447 .get("path")
4448 .and_then(TomlValue::as_str)
4449 .ok_or_else(|| {
4450 invalid_cargo_configuration(
4451 config,
4452 "Cargo configuration include table requires a string `path`",
4453 )
4454 })?;
4455 let optional = table
4456 .get("optional")
4457 .map(|value| {
4458 value.as_bool().ok_or_else(|| {
4459 invalid_cargo_configuration(
4460 config,
4461 "Cargo configuration include `optional` must be a boolean",
4462 )
4463 })
4464 })
4465 .transpose()?
4466 .unwrap_or(false);
4467 Ok((PathBuf::from(path), optional))
4468 }
4469 _ => Err(invalid_cargo_configuration(
4470 config,
4471 "Cargo configuration `include` must contain paths or include tables",
4472 )),
4473 })
4474 .collect()
4475}
4476
4477fn cargo_configuration_label(config: &Path, workspace_root: &Path) -> PathBuf {
4478 if let Ok(relative) = config.strip_prefix(workspace_root) {
4479 return PathBuf::from("cargo-config/workspace").join(relative);
4480 }
4481 let location = digest_bytes("cargo-config-location-v1", &os_bytes(config.as_os_str()));
4482 PathBuf::from("cargo-config/external").join(location.to_hex())
4483}
4484
4485fn invalid_cargo_configuration(path: &Path, message: &str) -> WasmBuildError {
4486 WasmBuildError::InvalidCargoConfiguration {
4487 path: path.to_owned(),
4488 message: message.to_owned(),
4489 }
4490}
4491
4492fn append_package_inputs(
4493 inputs: &mut Vec<(PathBuf, PathBuf)>,
4494 packages: &HashMap<&str, MetadataPackage<'_>>,
4495 closure: BTreeSet<&str>,
4496 workspace_root: &Path,
4497) -> Result<(), WasmBuildError> {
4498 for id in closure {
4499 let Some(package) = packages.get(id) else {
4500 return Err(invalid_metadata(&format!(
4501 "resolved package `{id}` is missing"
4502 )));
4503 };
4504 if !package.is_local {
4505 continue;
4506 }
4507 let root = package.manifest_path.parent().ok_or_else(|| {
4508 invalid_metadata(&format!(
4509 "package `{}` manifest has no parent",
4510 package.name
4511 ))
4512 })?;
4513 let relative_manifest = package
4514 .manifest_path
4515 .strip_prefix(workspace_root)
4516 .unwrap_or(&package.manifest_path);
4517 let label = PathBuf::from(format!("package/{}@{}", package.name, package.version))
4518 .join(relative_manifest.parent().unwrap_or_else(|| Path::new(".")));
4519 inputs.push((label, root.to_owned()));
4520 }
4521 Ok(())
4522}
4523
4524fn append_additional_inputs(
4525 inputs: &mut Vec<(PathBuf, PathBuf)>,
4526 spec: &WasmBuildSpec,
4527 workspace_root: &Path,
4528) {
4529 for additional in &spec.additional_inputs {
4530 let path = if additional.is_absolute() {
4531 additional.clone()
4532 } else {
4533 workspace_root.join(additional)
4534 };
4535 inputs.push((PathBuf::from("additional").join(additional), path));
4536 }
4537}
4538
4539fn source_exclusions(spec: &WasmBuildSpec, inputs: &[(PathBuf, PathBuf)]) -> Vec<PathBuf> {
4540 let mut exclusions = vec![
4541 spec.target_dir.clone(),
4542 spec.workspace_root.join("target"),
4543 spec.workspace_root.join(".git"),
4544 ];
4545 if let Some(shared_target) = shared_incremental_target(spec) {
4546 exclusions.push(shared_target);
4547 }
4548 for (_, path) in inputs {
4549 if path.is_dir() {
4550 exclusions.push(path.join("target"));
4551 exclusions.push(path.join(".git"));
4552 }
4553 }
4554 exclusions
4555}
4556
4557fn validate_shared_incremental_target_boundary(
4558 spec: &WasmBuildSpec,
4559 inputs: &[(PathBuf, PathBuf)],
4560) -> Result<(), WasmBuildError> {
4561 let Some(shared_target) = shared_incremental_target(spec) else {
4562 return Ok(());
4563 };
4564 let shared_target =
4565 canonicalize_allow_missing(&shared_target).map_err(|source| WasmBuildError::Io {
4566 operation: "resolve shared incremental Cargo target boundary",
4567 path: shared_target.clone(),
4568 source,
4569 })?;
4570 let exact_entries = spec.target_dir.join(".ic-testkit/wasm-targets");
4571 let exact_entries =
4572 canonicalize_allow_missing(&exact_entries).map_err(|source| WasmBuildError::Io {
4573 operation: "resolve exact Wasm cache boundary",
4574 path: exact_entries,
4575 source,
4576 })?;
4577 if shared_target.starts_with(&exact_entries)
4580 || (exact_entries.starts_with(&shared_target)
4581 && !exact_entries.starts_with(shared_target.join(".ic-testkit")))
4582 {
4583 return Err(WasmBuildError::InvalidSpec {
4584 message: "shared incremental target must not overlap removable exact Wasm cache state"
4585 .to_owned(),
4586 });
4587 }
4588 let resolved_inputs = inputs
4589 .iter()
4590 .map(|(_, input)| {
4591 let canonical = input.canonicalize().map_err(|source| WasmBuildError::Io {
4592 operation: "resolve Cargo input boundary",
4593 path: input.clone(),
4594 source,
4595 })?;
4596 let metadata = fs::metadata(&canonical).map_err(|source| WasmBuildError::Io {
4597 operation: "inspect Cargo input boundary",
4598 path: canonical.clone(),
4599 source,
4600 })?;
4601 Ok((canonical, metadata.is_dir()))
4602 })
4603 .collect::<Result<Vec<_>, WasmBuildError>>()?;
4604 let safe_generated_roots = std::iter::once(spec.target_dir.clone())
4605 .chain(std::iter::once(spec.workspace_root.join("target")))
4606 .chain(
4607 inputs
4608 .iter()
4609 .filter(|(_, path)| path.is_dir())
4610 .map(|(_, path)| path.join("target")),
4611 )
4612 .filter_map(|path| canonicalize_allow_missing(&path).ok())
4613 .filter(|root| {
4614 !resolved_inputs
4615 .iter()
4616 .any(|(input, _is_directory)| input.starts_with(root))
4617 })
4618 .collect::<Vec<_>>();
4619 if safe_generated_roots
4620 .iter()
4621 .any(|root| shared_target.starts_with(root))
4622 {
4623 return Ok(());
4624 }
4625
4626 for (input, is_directory) in resolved_inputs {
4627 if shared_target == input
4628 || (is_directory && shared_target.starts_with(&input))
4629 || input.starts_with(&shared_target)
4630 {
4631 return Err(WasmBuildError::InvalidSpec {
4632 message: format!(
4633 "shared incremental target {} must not overlap exact Cargo inputs unless it is inside a generated target directory",
4634 shared_target.display()
4635 ),
4636 });
4637 }
4638 }
4639 Ok(())
4640}
4641
4642pub(super) fn shared_incremental_target(spec: &WasmBuildSpec) -> Option<PathBuf> {
4643 let WasmBuildCacheMode::SharedIncremental { target_dir } = &spec.cache_mode else {
4644 return None;
4645 };
4646 Some(if target_dir.is_absolute() {
4647 target_dir.clone()
4648 } else {
4649 spec.workspace_root.join(target_dir)
4650 })
4651}
4652
4653fn shared_incremental_target_exists(
4654 spec: &WasmBuildSpec,
4655 operation: &'static str,
4656) -> Result<bool, WasmBuildError> {
4657 let target_dir =
4658 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
4659 message: "shared incremental target is not configured".to_owned(),
4660 })?;
4661 match fs::symlink_metadata(&target_dir) {
4662 Ok(metadata) if metadata.is_dir() => Ok(true),
4663 Ok(_) => Err(WasmBuildError::InvalidSpec {
4664 message: format!(
4665 "shared incremental Cargo target {} must be a directory",
4666 target_dir.display()
4667 ),
4668 }),
4669 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
4670 Err(source) => Err(WasmBuildError::Io {
4671 operation,
4672 path: target_dir,
4673 source,
4674 }),
4675 }
4676}
4677
4678fn effective_environment(spec: &WasmBuildSpec) -> BTreeMap<OsString, Option<OsString>> {
4679 let mut names = spec.inherited_env.clone();
4680 names.extend(AUTOMATIC_ENVIRONMENT.iter().map(OsString::from));
4681 let mut environment = names
4682 .into_iter()
4683 .map(|name| {
4684 let value = std::env::var_os(&name);
4685 (name, value)
4686 })
4687 .collect::<BTreeMap<_, _>>();
4688 for (key, value) in &spec.extra_env {
4689 environment.insert(key.clone(), Some(value.clone()));
4690 }
4691 environment
4692}
4693
4694fn apply_command_environment(command: &mut Command, spec: &WasmBuildSpec) {
4695 for (key, value) in &spec.extra_env {
4696 command.env(key, value);
4697 }
4698}
4699
4700fn run_cargo_build(
4701 spec: &WasmBuildSpec,
4702 build_target_dir: &Path,
4703 progress: &mut ProgressReporter<'_>,
4704) -> Result<(), WasmBuildError> {
4705 let absolute_target_dir =
4706 canonicalize_allow_missing(build_target_dir).map_err(|source| WasmBuildError::Io {
4707 operation: "resolve Cargo build target directory",
4708 path: build_target_dir.to_owned(),
4709 source,
4710 })?;
4711 let mut command = Command::new(&spec.cargo_program);
4712 command
4713 .current_dir(&spec.workspace_root)
4714 .env("CARGO_TARGET_DIR", absolute_target_dir)
4715 .args(["build", "--lib", "--target", &spec.target])
4716 .args(
4717 spec.cargo_profile_args
4718 .iter()
4719 .filter(|argument| argument.as_os_str() != OsStr::new("--lib")),
4720 );
4721 apply_command_environment(&mut command, spec);
4722 for package in &spec.packages {
4723 command.args(["-p", package]);
4724 }
4725
4726 if !progress.is_observed() {
4727 let output = command
4728 .output()
4729 .map_err(|source| WasmBuildError::CommandSpawn {
4730 phase: WasmBuildPhase::CargoBuild,
4731 program: spec.cargo_program.clone(),
4732 source,
4733 })?;
4734 return ensure_command_success(WasmBuildPhase::CargoBuild, output).map(|_| ());
4735 }
4736
4737 run_observed_cargo_build(spec, build_target_dir, command, progress)
4738}
4739
4740fn run_observed_cargo_build(
4741 spec: &WasmBuildSpec,
4742 build_target_dir: &Path,
4743 mut command: Command,
4744 progress: &mut ProgressReporter<'_>,
4745) -> Result<(), WasmBuildError> {
4746 command.stdout(Stdio::piped()).stderr(Stdio::piped());
4747 let started = Instant::now();
4748 let child = command
4749 .spawn()
4750 .map_err(|source| WasmBuildError::CommandSpawn {
4751 phase: WasmBuildPhase::CargoBuild,
4752 program: spec.cargo_program.clone(),
4753 source,
4754 })?;
4755 let mut child = ObservedChild::new(child);
4756 progress.emit(WasmBuildProgressEvent::CargoStarted {
4757 target_dir: build_target_dir.to_owned(),
4758 });
4759
4760 let stdout = child
4761 .child_mut()
4762 .stdout
4763 .take()
4764 .expect("Cargo stdout must be piped");
4765 let stderr = child
4766 .child_mut()
4767 .stderr
4768 .take()
4769 .expect("Cargo stderr must be piped");
4770 let (sender, chunks) = mpsc::sync_channel(8);
4773 let stdout_sender = sender.clone();
4774 let stdout_reader = thread::spawn(move || {
4775 read_process_output(stdout, WasmBuildOutputStream::Stdout, stdout_sender)
4776 });
4777 let stderr_reader =
4778 thread::spawn(move || read_process_output(stderr, WasmBuildOutputStream::Stderr, sender));
4779
4780 let captured = capture_observed_cargo_output(chunks, progress, started);
4781
4782 let status = child.wait().map_err(|source| WasmBuildError::Io {
4783 operation: "wait for observed cargo build",
4784 path: PathBuf::from(&spec.cargo_program),
4785 source,
4786 })?;
4787 join_output_reader(
4788 stdout_reader,
4789 "read observed cargo stdout",
4790 &spec.cargo_program,
4791 )?;
4792 join_output_reader(
4793 stderr_reader,
4794 "read observed cargo stderr",
4795 &spec.cargo_program,
4796 )?;
4797 let elapsed = started.elapsed();
4798 progress.emit(WasmBuildProgressEvent::CargoFinished {
4799 success: status.success(),
4800 code: status.code(),
4801 elapsed,
4802 });
4803
4804 ensure_command_success(
4805 WasmBuildPhase::CargoBuild,
4806 Output {
4807 status,
4808 stdout: captured.stdout,
4809 stderr: captured.stderr,
4810 },
4811 )
4812 .map(|_| ())
4813}
4814
4815struct CapturedProcessOutput {
4816 stdout: Vec<u8>,
4817 stderr: Vec<u8>,
4818}
4819
4820fn capture_observed_cargo_output(
4821 chunks: mpsc::Receiver<ProcessOutputChunk>,
4822 progress: &mut ProgressReporter<'_>,
4823 started: Instant,
4824) -> CapturedProcessOutput {
4825 let mut stdout = Vec::new();
4826 let mut stderr = Vec::new();
4827 loop {
4828 let message = match progress.heartbeat_due_in() {
4829 Some(wait) => match chunks.recv_timeout(wait) {
4830 Ok(chunk) => Some(chunk),
4831 Err(RecvTimeoutError::Timeout) => {
4832 progress.emit_heartbeat(WasmBuildProgressPhase::CargoBuild, started.elapsed());
4833 None
4834 }
4835 Err(RecvTimeoutError::Disconnected) => break,
4836 },
4837 None => match chunks.recv() {
4838 Ok(chunk) => Some(chunk),
4839 Err(_) => break,
4840 },
4841 };
4842 let Some(chunk) = message else {
4843 continue;
4844 };
4845 match chunk.stream {
4846 WasmBuildOutputStream::Stdout => stdout.extend_from_slice(&chunk.bytes),
4847 WasmBuildOutputStream::Stderr => stderr.extend_from_slice(&chunk.bytes),
4848 }
4849 if progress.config.emit_cargo_output {
4850 progress.emit(WasmBuildProgressEvent::CargoOutput {
4851 stream: chunk.stream,
4852 bytes: chunk.bytes,
4853 });
4854 }
4855 }
4856 CapturedProcessOutput { stdout, stderr }
4857}
4858
4859#[derive(Debug)]
4860struct ProcessOutputChunk {
4861 stream: WasmBuildOutputStream,
4862 bytes: Vec<u8>,
4863}
4864
4865fn read_process_output<R: io::Read>(
4866 mut reader: R,
4867 stream: WasmBuildOutputStream,
4868 sender: mpsc::SyncSender<ProcessOutputChunk>,
4869) -> io::Result<()> {
4870 let mut buffer = [0_u8; 8 * 1024];
4871 loop {
4872 let count = match reader.read(&mut buffer) {
4873 Ok(count) => count,
4874 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
4875 Err(error) => return Err(error),
4876 };
4877 if count == 0 {
4878 return Ok(());
4879 }
4880 if sender
4881 .send(ProcessOutputChunk {
4882 stream,
4883 bytes: buffer[..count].to_vec(),
4884 })
4885 .is_err()
4886 {
4887 return Ok(());
4888 }
4889 }
4890}
4891
4892fn join_output_reader(
4893 reader: thread::JoinHandle<io::Result<()>>,
4894 operation: &'static str,
4895 cargo_program: &OsStr,
4896) -> Result<(), WasmBuildError> {
4897 let result = reader.join().map_err(|_| WasmBuildError::Io {
4898 operation,
4899 path: PathBuf::from(cargo_program),
4900 source: io::Error::other("Cargo output reader panicked"),
4901 })?;
4902 result.map_err(|source| WasmBuildError::Io {
4903 operation,
4904 path: PathBuf::from(cargo_program),
4905 source,
4906 })
4907}
4908
4909struct ObservedChild(Option<Child>);
4910
4911impl ObservedChild {
4912 const fn new(child: Child) -> Self {
4913 Self(Some(child))
4914 }
4915
4916 const fn child_mut(&mut self) -> &mut Child {
4917 self.0.as_mut().expect("observed child must be present")
4918 }
4919
4920 fn wait(&mut self) -> io::Result<ExitStatus> {
4921 let status = self.child_mut().wait()?;
4922 self.0.take();
4923 Ok(status)
4924 }
4925}
4926
4927impl Drop for ObservedChild {
4928 fn drop(&mut self) {
4929 if let Some(mut child) = self.0.take() {
4930 let _ = child.kill();
4931 let _ = child.wait();
4932 }
4933 }
4934}
4935
4936fn ensure_command_success(phase: WasmBuildPhase, output: Output) -> Result<Output, WasmBuildError> {
4937 if output.status.success() {
4938 return Ok(output);
4939 }
4940 Err(WasmBuildError::CommandFailed {
4941 phase,
4942 status: output.status,
4943 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
4944 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
4945 })
4946}
4947
4948fn expected_artifacts(spec: &WasmBuildSpec, target_dir: &Path) -> Vec<PathBuf> {
4949 spec.packages
4950 .iter()
4951 .map(|package| {
4952 target_dir
4953 .join(&spec.target)
4954 .join(&spec.profile_target_dir)
4955 .join(format!("{package}.wasm"))
4956 })
4957 .collect()
4958}
4959
4960fn cache_entry_directory(spec: &WasmBuildSpec, fingerprint: InputDigest) -> PathBuf {
4961 spec.target_dir
4962 .join(".ic-testkit/wasm-targets")
4963 .join(fingerprint.to_hex())
4964}
4965
4966fn validated_artifact_set(
4967 artifacts: &[PathBuf],
4968 fingerprint: InputDigest,
4969) -> Option<Vec<FileDigest>> {
4970 let header = artifact_stamp_header(fingerprint);
4971 let stamp_len = artifact_stamp_contents(fingerprint, fingerprint).len();
4974 artifacts
4975 .iter()
4976 .map(|artifact| {
4977 let metadata = fs::metadata(artifact).ok()?;
4978 if !metadata.is_file() || metadata.len() == 0 {
4979 return None;
4980 }
4981 let stamp = read_stamp_with_limit(&artifact_stamp_path(artifact), stamp_len).ok()??;
4982 if stamp.len() != stamp_len || !stamp.starts_with(&header) {
4985 return None;
4986 }
4987 let info = digest_file("wasm-artifact-v1", artifact).ok()?;
4988 (stamp == artifact_stamp_contents(fingerprint, info.digest)).then_some(info)
4989 })
4990 .collect()
4991}
4992
4993fn missing_artifacts(artifacts: &[PathBuf]) -> Vec<PathBuf> {
4994 artifacts
4995 .iter()
4996 .filter(|path| {
4997 fs::metadata(path).map_or(true, |metadata| !metadata.is_file() || metadata.len() == 0)
4998 })
4999 .cloned()
5000 .collect()
5001}
5002
5003fn artifact_stamp_path(artifact: &Path) -> PathBuf {
5004 let mut name = artifact
5005 .file_name()
5006 .map_or_else(|| OsString::from("artifact"), OsString::from);
5007 name.push(".ic-testkit-build");
5008 artifact.with_file_name(name)
5009}
5010
5011fn artifact_stamp_header(fingerprint: InputDigest) -> String {
5012 format!("{CACHE_FORMAT_VERSION}\nbuild-sha256:{fingerprint}\n")
5013}
5014
5015fn artifact_stamp_contents(fingerprint: InputDigest, artifact_digest: InputDigest) -> String {
5016 format!(
5017 "{}artifact-sha256:{artifact_digest}\n",
5018 artifact_stamp_header(fingerprint),
5019 )
5020}
5021
5022fn write_artifact_stamp(
5023 artifact: &Path,
5024 fingerprint: InputDigest,
5025 info: &FileDigest,
5026 preserve_matching: bool,
5027) -> Result<(), WasmBuildError> {
5028 let stamp_path = artifact_stamp_path(artifact);
5029 let stamp = artifact_stamp_contents(fingerprint, info.digest);
5030 if preserve_matching && destination_matches_bytes(&stamp_path, stamp.as_bytes()) {
5031 return Ok(());
5032 }
5033 write_atomic(&stamp_path, stamp.as_bytes()).map_err(|source| WasmBuildError::Io {
5034 operation: "publish Wasm build stamp",
5035 path: stamp_path,
5036 source,
5037 })
5038}
5039
5040fn publish_artifact_stamps(
5041 artifacts: &[PathBuf],
5042 fingerprint: InputDigest,
5043) -> Result<Vec<FileDigest>, WasmBuildError> {
5044 let mut info = Vec::with_capacity(artifacts.len());
5045 for artifact in artifacts {
5046 let digest =
5047 digest_file("wasm-artifact-v1", artifact).map_err(|source| WasmBuildError::Io {
5048 operation: "hash built Wasm artifact",
5049 path: artifact.clone(),
5050 source,
5051 })?;
5052 write_artifact_stamp(artifact, fingerprint, &digest, false)?;
5053 info.push(digest);
5054 }
5055 Ok(info)
5056}
5057
5058fn materialize_artifacts(
5059 cached_artifacts: &[PathBuf],
5060 artifacts: &[PathBuf],
5061 fingerprint: InputDigest,
5062 artifact_info: &[FileDigest],
5063 preserve_matching: bool,
5064) -> Result<(), WasmBuildError> {
5065 for ((cached, artifact), info) in cached_artifacts.iter().zip(artifacts).zip(artifact_info) {
5066 if preserve_matching && destination_matches_digest("wasm-artifact-v1", artifact, info) {
5067 continue;
5068 }
5069 copy_file_atomic(cached, artifact).map_err(|source| WasmBuildError::Io {
5070 operation: "publish Wasm artifact",
5071 path: artifact.clone(),
5072 source,
5073 })?;
5074 }
5075 for (artifact, info) in artifacts.iter().zip(artifact_info) {
5078 write_artifact_stamp(artifact, fingerprint, info, preserve_matching)?;
5079 }
5080 Ok(())
5081}
5082
5083fn copy_wasm_artifacts(
5084 source_artifacts: &[PathBuf],
5085 cached_artifacts: &[PathBuf],
5086) -> Result<(), WasmBuildError> {
5087 for (source, cached) in source_artifacts.iter().zip(cached_artifacts) {
5088 copy_file_atomic(source, cached).map_err(|source_error| WasmBuildError::Io {
5089 operation: "cache shared-incremental Wasm artifact",
5090 path: cached.clone(),
5091 source: source_error,
5092 })?;
5093 }
5094 Ok(())
5095}
5096
5097fn create_dir_all(path: &Path, operation: &'static str) -> Result<(), WasmBuildError> {
5098 fs::create_dir_all(path).map_err(|source| WasmBuildError::Io {
5099 operation,
5100 path: path.to_owned(),
5101 source,
5102 })
5103}
5104
5105fn add_if_present(inputs: &mut Vec<(PathBuf, PathBuf)>, label: &str, path: PathBuf) {
5106 if path.exists() {
5107 inputs.push((PathBuf::from(label), path));
5108 }
5109}
5110
5111fn required_string<'a>(value: &'a Value, field: &str) -> Result<&'a str, String> {
5112 value
5113 .get(field)
5114 .and_then(Value::as_str)
5115 .ok_or_else(|| format!("Cargo metadata field `{field}` is missing"))
5116}
5117
5118fn optional_string<'a>(value: &'a Value, field: &str) -> Result<Option<&'a str>, String> {
5119 match value.get(field) {
5120 None | Some(Value::Null) => Ok(None),
5121 Some(Value::String(value)) => Ok(Some(value)),
5122 Some(_) => Err(format!(
5123 "Cargo metadata field `{field}` is not a string or null"
5124 )),
5125 }
5126}
5127
5128fn invalid_metadata(message: &str) -> WasmBuildError {
5129 WasmBuildError::InvalidMetadata {
5130 message: message.to_owned(),
5131 }
5132}
5133
5134impl WasmBuildError {
5135 fn indicates_input_change(&self) -> bool {
5136 match self {
5137 Self::InputsChangedDuringAcquisition { .. } => true,
5138 Self::FailedBuildCleanup { build_error, .. } => build_error.indicates_input_change(),
5139 _ => false,
5140 }
5141 }
5142}
5143
5144impl std::fmt::Display for WasmBuildPhase {
5145 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5146 formatter.write_str(match self {
5147 Self::CargoMetadata => "cargo metadata",
5148 Self::CargoIdentity => "Cargo identity",
5149 Self::RustcIdentity => "Rust compiler identity",
5150 Self::CargoBuild => "cargo build",
5151 })
5152 }
5153}
5154
5155impl std::fmt::Display for WasmBuildProgressPhase {
5156 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5157 formatter.write_str(match self {
5158 Self::ExactCacheLock => "exact cache lock",
5159 Self::CargoIdentity => "Cargo identity",
5160 Self::RustcIdentity => "Rust compiler identity",
5161 Self::CargoMetadata => "Cargo metadata",
5162 Self::InputDiscovery => "input discovery",
5163 Self::ContentHashing => "content hashing",
5164 Self::SharedTargetLock => "shared target lock",
5165 Self::SharedTargetMaintenance => "shared target maintenance",
5166 Self::CargoBuild => "Cargo build",
5167 Self::ArtifactPublication => "artifact publication",
5168 Self::ExactCacheMaintenance => "exact cache maintenance",
5169 })
5170 }
5171}
5172
5173impl std::fmt::Display for WasmBuildError {
5174 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5175 match self {
5176 Self::InvalidSpec { message } => {
5177 write!(formatter, "invalid Wasm build spec: {message}")
5178 }
5179 Self::Io {
5180 operation,
5181 path,
5182 source,
5183 } => write!(
5184 formatter,
5185 "failed to {operation} at {}: {source}",
5186 path.display()
5187 ),
5188 Self::CommandSpawn {
5189 phase,
5190 program,
5191 source,
5192 } => write!(
5193 formatter,
5194 "failed to launch {phase} using `{}`: {source}",
5195 program.to_string_lossy(),
5196 ),
5197 Self::CommandFailed {
5198 phase,
5199 status,
5200 stdout,
5201 stderr,
5202 } => write!(
5203 formatter,
5204 "{phase} failed with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}",
5205 ),
5206 Self::InvalidMetadata { message } => {
5207 write!(formatter, "invalid Cargo metadata: {message}")
5208 }
5209 Self::InvalidCargoConfiguration { path, message } => write!(
5210 formatter,
5211 "invalid Cargo configuration at {}: {message}",
5212 path.display(),
5213 ),
5214 Self::MissingArtifacts { paths } => write!(
5215 formatter,
5216 "cargo build succeeded without producing: {}",
5217 paths
5218 .iter()
5219 .map(|path| path.display().to_string())
5220 .collect::<Vec<_>>()
5221 .join(", "),
5222 ),
5223 Self::InputsChangedDuringAcquisition { before, after } => write!(
5224 formatter,
5225 "Wasm inputs changed during artifact acquisition: {before} -> {after}",
5226 ),
5227 Self::PreparedInputSnapshotInvalidated => formatter.write_str(
5228 "the prepared Wasm input snapshot was invalidated before artifact publication",
5229 ),
5230 Self::FailedBuildCleanup {
5231 build_error,
5232 path,
5233 source,
5234 } => write!(
5235 formatter,
5236 "Wasm build failed ({build_error}) and its incomplete target directory at {} could not be removed: {source}",
5237 path.display(),
5238 ),
5239 }
5240 }
5241}
5242
5243impl std::error::Error for WasmBuildError {
5244 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
5245 match self {
5246 Self::Io { source, .. }
5247 | Self::CommandSpawn { source, .. }
5248 | Self::FailedBuildCleanup { source, .. } => Some(source),
5249 _ => None,
5250 }
5251 }
5252}
5253
5254#[cfg(test)]
5255mod tests;