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