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