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