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::{Path, PathBuf},
8 process::{Child, Command, ExitStatus, Output, Stdio},
9 sync::mpsc::{self, RecvTimeoutError},
10 thread,
11 time::{Duration, Instant, SystemTime},
12};
13use toml::Value as TomlValue;
14
15use super::{
16 cache_fs::{
17 ArtifactCacheMaintenance, ArtifactCachePrunePolicy, ArtifactCachePruneReport, CacheFsError,
18 cache_entry_last_used, cache_maintenance_due, directory_logical_size,
19 ensure_cache_directory_tag as ensure_cache_tag, is_sha256_directory, lock_cache_file,
20 lock_cache_file_with_wait_observer, perform_scheduled_cache_maintenance,
21 prune_direct_child_directories, record_cache_entry_use as record_entry_use,
22 record_cache_maintenance, remove_path_if_present,
23 },
24 digest::{
25 InputDigest, InputHasher, LabeledPathDigestCache, copy_file_atomic, digest_bytes,
26 digest_file, digest_labeled_paths_composable, os_bytes, write_atomic,
27 },
28 wasm::wasm_path,
29};
30
31const CACHE_FORMAT_VERSION: &str = "ic-testkit-wasm-build-v1";
32const DEFAULT_TARGET: &str = "wasm32-unknown-unknown";
33const AUTOMATIC_ENVIRONMENT: &[&str] = &[
34 "CARGO_BUILD_RUSTC",
35 "CARGO_ENCODED_RUSTFLAGS",
36 "RUSTC",
37 "RUSTC_WRAPPER",
38 "RUSTC_WORKSPACE_WRAPPER",
39 "RUSTFLAGS",
40 "RUSTUP_TOOLCHAIN",
41];
42
43#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct WasmBuildSpec {
51 workspace_root: PathBuf,
52 target_dir: PathBuf,
53 packages: Vec<String>,
54 profile_target_dir: String,
55 cargo_profile_args: Vec<OsString>,
56 extra_env: BTreeMap<OsString, OsString>,
57 inherited_env: BTreeSet<OsString>,
58 additional_inputs: Vec<PathBuf>,
59 target: String,
60 cargo_program: OsString,
61 rustc_program: OsString,
62 cache_mode: WasmBuildCacheMode,
63 prune_policy: Option<ArtifactCachePrunePolicy>,
64 prune_interval: Option<Duration>,
65 shared_incremental_maintenance_config: Option<SharedIncrementalTargetMaintenanceConfig>,
66}
67
68#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub enum SharedIncrementalTargetMaintenanceFailureMode {
71 #[default]
73 Strict,
74 BestEffort,
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub struct SharedIncrementalTargetMaintenanceConfig {
81 policy: SharedIncrementalTargetPrunePolicy,
82 minimum_interval: Duration,
83 failure_mode: SharedIncrementalTargetMaintenanceFailureMode,
84}
85
86#[non_exhaustive]
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub enum WasmBuildCacheMode {
90 Isolated,
92 SharedIncremental {
94 target_dir: PathBuf,
96 },
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
101pub enum WasmBuildOutcome {
102 Built(WasmBuildRecord),
104 Reused(WasmBuildRecord),
106}
107
108#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct WasmBuildRecord {
111 fingerprint: InputDigest,
112 input_digest: InputDigest,
113 exact_cache_path: PathBuf,
114 artifacts: Vec<PathBuf>,
115 timings: WasmBuildTimings,
116 maintenance: Option<ArtifactCacheMaintenance>,
117 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
118}
119
120#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
122pub struct WasmBuildTimings {
123 lock_wait: Duration,
124 shared_incremental_lock_wait: Option<Duration>,
125 input_resolution: WasmInputResolutionTimings,
126 cargo_build: Option<Duration>,
127 cache_maintenance: Option<Duration>,
128 total: Duration,
129}
130
131#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
133pub struct WasmInputResolutionTimings {
134 tool_identity: Duration,
135 cargo_metadata: Duration,
136 input_discovery: Duration,
137 content_hashing: Duration,
138 total: Duration,
139}
140
141#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct CargoBuildInput {
144 label: PathBuf,
145 path: PathBuf,
146}
147
148#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct ResolvedCargoBuildInputs {
154 fingerprint: InputDigest,
155 input_digest: InputDigest,
156 inputs: Vec<CargoBuildInput>,
157 exclusions: Vec<PathBuf>,
158 timings: WasmInputResolutionTimings,
159}
160
161pub(super) struct WasmBuildBatchInputResolver<'a> {
162 specs: &'a [WasmBuildSpec],
163 groups: Vec<BatchResolutionGroup>,
164 group_by_index: Vec<usize>,
165 resolved: Vec<Option<Result<ResolvedCargoBuildInputs, WasmBuildError>>>,
166 metrics: WasmBuildBatchInputMetrics,
167}
168
169struct BatchResolutionGroup {
170 indexes: Vec<usize>,
171}
172
173#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
174pub(super) struct WasmBuildBatchInputMetrics {
175 pub(super) runs: usize,
176 pub(super) reuses: usize,
177}
178
179#[derive(Eq, PartialEq)]
180struct BatchResolutionKey {
181 workspace_root: PathBuf,
182 cargo_program: OsString,
183 rustc_program: OsString,
184 metadata_arguments: Vec<OsString>,
185 environment: BTreeMap<OsString, Option<OsString>>,
186}
187
188#[derive(Clone, Debug, Eq, PartialEq)]
190pub struct SharedIncrementalTargetInspection {
191 target_dir: PathBuf,
192 logical_size_bytes: u64,
193 last_used: SystemTime,
194 lock_wait: Duration,
195}
196
197#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
205pub struct SharedIncrementalTargetPrunePolicy {
206 max_age: Option<Duration>,
207 max_size_bytes: Option<u64>,
208}
209
210#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct SharedIncrementalTargetMaintenance {
213 target_dir: PathBuf,
214 logical_size_bytes_before: u64,
215 logical_size_bytes_after: u64,
216 last_used_before: SystemTime,
217 cleared: bool,
218 lock_wait: Duration,
219 maintenance: Duration,
220}
221
222#[non_exhaustive]
224#[derive(Clone, Debug, Eq, PartialEq)]
225pub enum SharedIncrementalTargetMaintenanceOutcome {
226 Missing {
228 target_dir: PathBuf,
230 },
231 Skipped {
233 target_dir: PathBuf,
235 lock_wait: Duration,
237 schedule_check: Duration,
239 },
240 Performed {
242 maintenance: SharedIncrementalTargetMaintenance,
244 schedule_check: Duration,
246 },
247 Failed {
249 target_dir: PathBuf,
251 lock_wait: Duration,
253 message: String,
255 },
256}
257
258#[derive(Clone, Copy, Debug, Eq, PartialEq)]
260pub struct WasmBuildProgressConfig {
261 heartbeat_interval: Option<Duration>,
262 emit_cargo_output: bool,
263}
264
265#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub enum WasmBuildOutputStream {
268 Stdout,
270 Stderr,
272}
273
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
276pub enum WasmBuildProgressOutcome {
277 Built,
279 Reused,
281}
282
283#[non_exhaustive]
285#[derive(Clone, Copy, Debug, Eq, PartialEq)]
286pub enum WasmBuildProgressPhase {
287 ExactCacheLock,
289 CargoIdentity,
291 RustcIdentity,
293 CargoMetadata,
295 InputDiscovery,
297 ContentHashing,
299 SharedTargetLock,
301 SharedTargetMaintenance,
303 CargoBuild,
305 ArtifactPublication,
307 ExactCacheMaintenance,
309}
310
311#[non_exhaustive]
313#[derive(Clone, Debug, Eq, PartialEq)]
314pub enum WasmBuildProgressEvent {
315 Started,
317 InputsResolved {
319 fingerprint: InputDigest,
321 input_digest: InputDigest,
323 elapsed: Duration,
325 },
326 CacheMiss {
328 fingerprint: InputDigest,
330 },
331 CacheHit {
333 fingerprint: InputDigest,
335 },
336 SharedTargetLockStarted {
338 target_dir: PathBuf,
340 },
341 SharedTargetLockAcquired {
343 target_dir: PathBuf,
345 wait: Duration,
347 },
348 SharedTargetMaintenanceStarted {
350 target_dir: PathBuf,
352 },
353 SharedTargetMaintenanceFinished {
355 outcome: SharedIncrementalTargetMaintenanceOutcome,
357 },
358 CargoStarted {
360 target_dir: PathBuf,
362 },
363 CargoOutput {
365 stream: WasmBuildOutputStream,
367 bytes: Vec<u8>,
369 },
370 Heartbeat {
372 phase: WasmBuildProgressPhase,
374 elapsed: Duration,
376 },
377 CargoFinished {
379 success: bool,
381 code: Option<i32>,
383 elapsed: Duration,
385 },
386 Finished {
388 outcome: WasmBuildProgressOutcome,
390 fingerprint: InputDigest,
392 elapsed: Duration,
394 },
395}
396
397impl Default for WasmBuildProgressConfig {
398 fn default() -> Self {
399 Self {
400 heartbeat_interval: Some(Duration::from_secs(10)),
401 emit_cargo_output: true,
402 }
403 }
404}
405
406impl WasmBuildProgressConfig {
407 #[must_use]
409 pub fn new() -> Self {
410 Self::default()
411 }
412
413 #[must_use]
417 pub const fn with_heartbeat_interval(mut self, interval: Duration) -> Self {
418 self.heartbeat_interval = Some(interval);
419 self
420 }
421
422 #[must_use]
424 pub const fn without_heartbeats(mut self) -> Self {
425 self.heartbeat_interval = None;
426 self
427 }
428
429 #[must_use]
433 pub const fn with_cargo_output(mut self, emit: bool) -> Self {
434 self.emit_cargo_output = emit;
435 self
436 }
437
438 #[must_use]
440 pub const fn heartbeat_interval(self) -> Option<Duration> {
441 self.heartbeat_interval
442 }
443
444 #[must_use]
446 pub const fn emits_cargo_output(self) -> bool {
447 self.emit_cargo_output
448 }
449}
450
451struct ProgressReporter<'a> {
452 config: WasmBuildProgressConfig,
453 observer: Option<&'a mut dyn FnMut(WasmBuildProgressEvent)>,
454 last_event: Instant,
455}
456
457impl ProgressReporter<'_> {
458 fn silent() -> Self {
459 Self {
460 config: WasmBuildProgressConfig {
461 heartbeat_interval: None,
462 emit_cargo_output: false,
463 },
464 observer: None,
465 last_event: Instant::now(),
466 }
467 }
468
469 fn observed(
470 config: WasmBuildProgressConfig,
471 observer: &'_ mut dyn FnMut(WasmBuildProgressEvent),
472 ) -> ProgressReporter<'_> {
473 ProgressReporter {
474 config,
475 observer: Some(observer),
476 last_event: Instant::now(),
477 }
478 }
479
480 fn emit(&mut self, event: WasmBuildProgressEvent) {
481 if let Some(observer) = &mut self.observer {
482 observer(event);
483 self.last_event = Instant::now();
484 }
485 }
486
487 const fn is_observed(&self) -> bool {
488 self.observer.is_some()
489 }
490
491 fn heartbeat_due_in(&self) -> Option<Duration> {
492 self.config
493 .heartbeat_interval
494 .map(|interval| interval.saturating_sub(self.last_event.elapsed()))
495 }
496
497 fn emit_heartbeat(&mut self, phase: WasmBuildProgressPhase, elapsed: Duration) {
498 self.emit(WasmBuildProgressEvent::Heartbeat { phase, elapsed });
499 }
500
501 fn emit_heartbeat_if_due(&mut self, phase: WasmBuildProgressPhase, elapsed: Duration) {
502 if self.heartbeat_due_in() == Some(Duration::ZERO) {
503 self.emit_heartbeat(phase, elapsed);
504 }
505 }
506
507 fn run_phase<T, F>(&mut self, phase: WasmBuildProgressPhase, operation: F) -> T
508 where
509 T: Send,
510 F: FnOnce() -> T + Send,
511 {
512 if !self.is_observed() || self.config.heartbeat_interval.is_none() {
513 return operation();
514 }
515
516 let started = Instant::now();
517 thread::scope(|scope| {
518 let (finished, completion) = mpsc::sync_channel(0);
519 let worker = scope.spawn(move || {
520 let result = operation();
521 let _ = finished.send(());
522 result
523 });
524 loop {
525 let wait = self
526 .heartbeat_due_in()
527 .expect("observed phase must have a heartbeat interval");
528 match completion.recv_timeout(wait) {
529 Ok(()) | Err(RecvTimeoutError::Disconnected) => {
530 return worker
531 .join()
532 .unwrap_or_else(|panic| std::panic::resume_unwind(panic));
533 }
534 Err(RecvTimeoutError::Timeout) => self.emit_heartbeat(phase, started.elapsed()),
535 }
536 }
537 })
538 }
539}
540
541#[non_exhaustive]
543#[derive(Clone, Copy, Debug, Eq, PartialEq)]
544pub enum WasmBuildPhase {
545 CargoMetadata,
547 CargoIdentity,
549 RustcIdentity,
551 CargoBuild,
553}
554
555#[non_exhaustive]
557#[derive(Debug)]
558pub enum WasmBuildError {
559 InvalidSpec { message: String },
561 Io {
563 operation: &'static str,
564 path: PathBuf,
565 source: io::Error,
566 },
567 CommandSpawn {
569 phase: WasmBuildPhase,
570 program: OsString,
571 source: io::Error,
572 },
573 CommandFailed {
575 phase: WasmBuildPhase,
576 status: ExitStatus,
577 stdout: String,
578 stderr: String,
579 },
580 InvalidMetadata { message: String },
582 InvalidCargoConfiguration { path: PathBuf, message: String },
584 MissingArtifacts { paths: Vec<PathBuf> },
586 InputsChangedDuringBuild {
588 before: InputDigest,
589 after: InputDigest,
590 },
591 FailedBuildCleanup {
593 build_error: Box<Self>,
594 path: PathBuf,
595 source: io::Error,
596 },
597}
598
599impl WasmBuildSpec {
600 #[must_use]
605 pub fn new(
606 workspace_root: &Path,
607 target_dir: &Path,
608 packages: &[&str],
609 profile_target_dir: &str,
610 ) -> Self {
611 Self {
612 workspace_root: workspace_root.to_owned(),
613 target_dir: target_dir.to_owned(),
614 packages: packages
615 .iter()
616 .map(|package| (*package).to_owned())
617 .collect(),
618 profile_target_dir: profile_target_dir.to_owned(),
619 cargo_profile_args: Vec::new(),
620 extra_env: BTreeMap::new(),
621 inherited_env: BTreeSet::new(),
622 additional_inputs: Vec::new(),
623 target: DEFAULT_TARGET.to_owned(),
624 cargo_program: std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()),
625 rustc_program: std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()),
626 cache_mode: WasmBuildCacheMode::Isolated,
627 prune_policy: None,
628 prune_interval: None,
629 shared_incremental_maintenance_config: None,
630 }
631 }
632
633 #[must_use]
635 pub fn with_cargo_profile_args<I, S>(mut self, arguments: I) -> Self
636 where
637 I: IntoIterator<Item = S>,
638 S: AsRef<OsStr>,
639 {
640 self.cargo_profile_args = arguments
641 .into_iter()
642 .map(|argument| argument.as_ref().to_owned())
643 .collect();
644 self
645 }
646
647 #[must_use]
649 pub fn with_extra_env<I, K, V>(mut self, environment: I) -> Self
650 where
651 I: IntoIterator<Item = (K, V)>,
652 K: Into<OsString>,
653 V: Into<OsString>,
654 {
655 self.extra_env = environment
656 .into_iter()
657 .map(|(key, value)| (key.into(), value.into()))
658 .collect();
659 self
660 }
661
662 #[must_use]
667 pub fn with_inherited_env<I, S>(mut self, names: I) -> Self
668 where
669 I: IntoIterator<Item = S>,
670 S: Into<OsString>,
671 {
672 self.inherited_env.extend(names.into_iter().map(Into::into));
673 self
674 }
675
676 #[must_use]
681 pub fn with_additional_inputs<I, P>(mut self, paths: I) -> Self
682 where
683 I: IntoIterator<Item = P>,
684 P: Into<PathBuf>,
685 {
686 self.additional_inputs
687 .extend(paths.into_iter().map(Into::into));
688 self
689 }
690
691 #[must_use]
693 pub fn with_target(mut self, target: &str) -> Self {
694 target.clone_into(&mut self.target);
695 self
696 }
697
698 #[must_use]
700 pub fn with_cargo_program(mut self, program: impl Into<OsString>) -> Self {
701 self.cargo_program = program.into();
702 self
703 }
704
705 #[must_use]
707 pub fn with_rustc_program(mut self, program: impl Into<OsString>) -> Self {
708 self.rustc_program = program.into();
709 self
710 }
711
712 #[must_use]
718 pub fn with_shared_incremental_target(mut self, target_dir: impl Into<PathBuf>) -> Self {
719 self.cache_mode = WasmBuildCacheMode::SharedIncremental {
720 target_dir: target_dir.into(),
721 };
722 self
723 }
724
725 #[must_use]
736 pub const fn with_shared_incremental_target_maintenance_at_most_every(
737 mut self,
738 policy: SharedIncrementalTargetPrunePolicy,
739 minimum_interval: Duration,
740 ) -> Self {
741 self.shared_incremental_maintenance_config = Some(
742 SharedIncrementalTargetMaintenanceConfig::new(policy, minimum_interval),
743 );
744 self
745 }
746
747 #[must_use]
753 pub const fn with_shared_incremental_target_maintenance(
754 mut self,
755 config: SharedIncrementalTargetMaintenanceConfig,
756 ) -> Self {
757 self.shared_incremental_maintenance_config = Some(config);
758 self
759 }
760
761 #[must_use]
767 pub const fn with_prune_policy(mut self, policy: ArtifactCachePrunePolicy) -> Self {
768 self.prune_policy = Some(policy);
769 self.prune_interval = None;
770 self
771 }
772
773 #[must_use]
780 pub const fn with_prune_policy_at_most_every(
781 mut self,
782 policy: ArtifactCachePrunePolicy,
783 minimum_interval: Duration,
784 ) -> Self {
785 self.prune_policy = Some(policy);
786 self.prune_interval = Some(minimum_interval);
787 self
788 }
789
790 #[must_use]
792 pub fn workspace_root(&self) -> &Path {
793 &self.workspace_root
794 }
795
796 #[must_use]
798 pub fn target_dir(&self) -> &Path {
799 &self.target_dir
800 }
801
802 #[must_use]
804 pub fn packages(&self) -> &[String] {
805 &self.packages
806 }
807
808 #[must_use]
810 pub const fn cache_mode(&self) -> &WasmBuildCacheMode {
811 &self.cache_mode
812 }
813
814 #[must_use]
816 pub const fn prune_policy(&self) -> Option<ArtifactCachePrunePolicy> {
817 self.prune_policy
818 }
819
820 #[must_use]
822 pub const fn prune_interval(&self) -> Option<Duration> {
823 self.prune_interval
824 }
825
826 #[must_use]
828 pub const fn shared_incremental_target_maintenance(
829 &self,
830 ) -> Option<SharedIncrementalTargetMaintenanceConfig> {
831 self.shared_incremental_maintenance_config
832 }
833}
834
835impl WasmBuildOutcome {
836 #[must_use]
838 pub const fn record(&self) -> &WasmBuildRecord {
839 match self {
840 Self::Built(record) | Self::Reused(record) => record,
841 }
842 }
843
844 #[must_use]
846 pub const fn is_reused(&self) -> bool {
847 matches!(self, Self::Reused(_))
848 }
849}
850
851impl WasmBuildRecord {
852 #[must_use]
854 pub const fn fingerprint(&self) -> InputDigest {
855 self.fingerprint
856 }
857
858 #[must_use]
860 pub const fn input_digest(&self) -> InputDigest {
861 self.input_digest
862 }
863
864 #[must_use]
870 pub fn exact_cache_path(&self) -> &Path {
871 &self.exact_cache_path
872 }
873
874 #[must_use]
876 pub fn artifacts(&self) -> &[PathBuf] {
877 &self.artifacts
878 }
879
880 #[must_use]
882 pub const fn timings(&self) -> WasmBuildTimings {
883 self.timings
884 }
885
886 #[must_use]
888 pub const fn maintenance(&self) -> Option<&ArtifactCacheMaintenance> {
889 self.maintenance.as_ref()
890 }
891
892 #[must_use]
894 pub const fn shared_incremental_maintenance(
895 &self,
896 ) -> Option<&SharedIncrementalTargetMaintenanceOutcome> {
897 self.shared_incremental_maintenance.as_ref()
898 }
899}
900
901impl WasmBuildTimings {
902 #[must_use]
904 pub const fn lock_wait(self) -> Duration {
905 self.lock_wait
906 }
907
908 #[must_use]
910 pub const fn shared_incremental_lock_wait(self) -> Option<Duration> {
911 self.shared_incremental_lock_wait
912 }
913
914 #[must_use]
916 pub const fn input_resolution(self) -> WasmInputResolutionTimings {
917 self.input_resolution
918 }
919
920 #[must_use]
922 pub const fn cargo_build(self) -> Option<Duration> {
923 self.cargo_build
924 }
925
926 #[must_use]
928 pub const fn cache_maintenance(self) -> Option<Duration> {
929 self.cache_maintenance
930 }
931
932 #[must_use]
934 pub const fn total(self) -> Duration {
935 self.total
936 }
937
938 pub(super) const fn saturating_add(self, other: Self) -> Self {
939 let mut input_resolution = self.input_resolution;
940 input_resolution.include(other.input_resolution);
941 Self {
942 lock_wait: self.lock_wait.saturating_add(other.lock_wait),
943 shared_incremental_lock_wait: sum_optional_duration(
944 self.shared_incremental_lock_wait,
945 other.shared_incremental_lock_wait,
946 ),
947 input_resolution,
948 cargo_build: sum_optional_duration(self.cargo_build, other.cargo_build),
949 cache_maintenance: sum_optional_duration(
950 self.cache_maintenance,
951 other.cache_maintenance,
952 ),
953 total: self.total.saturating_add(other.total),
954 }
955 }
956}
957
958impl WasmInputResolutionTimings {
959 #[must_use]
961 pub const fn tool_identity(self) -> Duration {
962 self.tool_identity
963 }
964
965 #[must_use]
967 pub const fn cargo_metadata(self) -> Duration {
968 self.cargo_metadata
969 }
970
971 #[must_use]
973 pub const fn input_discovery(self) -> Duration {
974 self.input_discovery
975 }
976
977 #[must_use]
979 pub const fn content_hashing(self) -> Duration {
980 self.content_hashing
981 }
982
983 #[must_use]
985 pub const fn total(self) -> Duration {
986 self.total
987 }
988
989 const fn include(&mut self, other: Self) {
990 self.tool_identity = self.tool_identity.saturating_add(other.tool_identity);
991 self.cargo_metadata = self.cargo_metadata.saturating_add(other.cargo_metadata);
992 self.input_discovery = self.input_discovery.saturating_add(other.input_discovery);
993 self.content_hashing = self.content_hashing.saturating_add(other.content_hashing);
994 self.total = self.total.saturating_add(other.total);
995 }
996}
997
998const fn sum_optional_duration(
999 left: Option<Duration>,
1000 right: Option<Duration>,
1001) -> Option<Duration> {
1002 match (left, right) {
1003 (None, None) => None,
1004 (Some(duration), None) | (None, Some(duration)) => Some(duration),
1005 (Some(left), Some(right)) => Some(left.saturating_add(right)),
1006 }
1007}
1008
1009impl CargoBuildInput {
1010 #[must_use]
1012 pub fn label(&self) -> &Path {
1013 &self.label
1014 }
1015
1016 #[must_use]
1018 pub fn path(&self) -> &Path {
1019 &self.path
1020 }
1021}
1022
1023impl ResolvedCargoBuildInputs {
1024 #[must_use]
1026 pub const fn fingerprint(&self) -> InputDigest {
1027 self.fingerprint
1028 }
1029
1030 #[must_use]
1032 pub const fn input_digest(&self) -> InputDigest {
1033 self.input_digest
1034 }
1035
1036 #[must_use]
1038 pub fn inputs(&self) -> &[CargoBuildInput] {
1039 &self.inputs
1040 }
1041
1042 #[must_use]
1047 pub fn exclusions(&self) -> &[PathBuf] {
1048 &self.exclusions
1049 }
1050
1051 #[must_use]
1053 pub const fn timings(&self) -> WasmInputResolutionTimings {
1054 self.timings
1055 }
1056
1057 pub fn is_current(&self, spec: &WasmBuildSpec) -> Result<bool, WasmBuildError> {
1059 resolve_cargo_build_inputs(spec).map(|current| current.fingerprint == self.fingerprint)
1060 }
1061
1062 pub fn is_content_current(&self) -> Result<bool, WasmBuildError> {
1069 self.current_input_digest()
1070 .map(|current| current == self.input_digest)
1071 }
1072
1073 pub(super) fn current_input_digest(&self) -> Result<InputDigest, WasmBuildError> {
1074 let inputs = self
1075 .inputs
1076 .iter()
1077 .map(|input| (input.label.clone(), input.path.clone()))
1078 .collect::<Vec<_>>();
1079 digest_labeled_paths_composable(
1080 "wasm-source-inputs-v1",
1081 &inputs,
1082 &self.exclusions,
1083 &mut LabeledPathDigestCache::default(),
1084 )
1085 .map_err(|source| WasmBuildError::Io {
1086 operation: "rehash resolved Cargo build inputs",
1087 path: self
1088 .inputs
1089 .first()
1090 .map_or_else(PathBuf::new, |input| input.path.clone()),
1091 source,
1092 })
1093 }
1094}
1095
1096impl<'a> WasmBuildBatchInputResolver<'a> {
1097 pub(super) fn new(specs: &'a [WasmBuildSpec]) -> Self {
1098 let mut keys = Vec::<BatchResolutionKey>::new();
1099 let mut groups = Vec::<BatchResolutionGroup>::new();
1100 let mut group_by_index = Vec::with_capacity(specs.len());
1101 for (index, spec) in specs.iter().enumerate() {
1102 let key = BatchResolutionKey::for_spec(spec);
1103 let group = keys
1104 .iter()
1105 .position(|candidate| *candidate == key)
1106 .unwrap_or_else(|| {
1107 keys.push(key);
1108 groups.push(BatchResolutionGroup {
1109 indexes: Vec::new(),
1110 });
1111 groups.len() - 1
1112 });
1113 groups[group].indexes.push(index);
1114 group_by_index.push(group);
1115 }
1116 Self {
1117 specs,
1118 groups,
1119 group_by_index,
1120 resolved: std::iter::repeat_with(|| None).take(specs.len()).collect(),
1121 metrics: WasmBuildBatchInputMetrics::default(),
1122 }
1123 }
1124
1125 pub(super) const fn metrics(&self) -> WasmBuildBatchInputMetrics {
1126 self.metrics
1127 }
1128
1129 fn resolve(
1130 &mut self,
1131 index: usize,
1132 progress: &mut ProgressReporter<'_>,
1133 ) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
1134 if self.resolved[index].is_none() {
1135 self.resolve_group(index, progress)?;
1136 }
1137 self.resolved[index]
1138 .take()
1139 .expect("resolved batch input must be populated")
1140 }
1141
1142 fn resolve_group(
1143 &mut self,
1144 active_index: usize,
1145 progress: &mut ProgressReporter<'_>,
1146 ) -> Result<(), WasmBuildError> {
1147 let total_started = Instant::now();
1148 let indexes = self.groups[self.group_by_index[active_index]]
1149 .indexes
1150 .clone();
1151 let active = &self.specs[active_index];
1152
1153 let (cargo_identity, rustc_identity, tool_identity) =
1154 resolve_batch_tool_identity(active, progress)?;
1155
1156 let metadata_started = Instant::now();
1157 let metadata = progress.run_phase(WasmBuildProgressPhase::CargoMetadata, || {
1158 cargo_metadata(active)
1159 })?;
1160 let cargo_metadata = metadata_started.elapsed();
1161
1162 let discovery_started = Instant::now();
1163 let mut discovered = Vec::new();
1164 for index in indexes {
1165 if self.resolved[index].is_some() || validate_spec(&self.specs[index]).is_err() {
1166 continue;
1167 }
1168 let spec = &self.specs[index];
1169 let result = (|| {
1170 let inputs = resolve_local_inputs(spec, &metadata)?;
1171 validate_shared_incremental_target_boundary(spec, &inputs)?;
1172 let exclusions = source_exclusions(spec, &inputs);
1173 Ok::<_, WasmBuildError>((inputs, exclusions))
1174 })();
1175 match result {
1176 Ok((inputs, exclusions)) => discovered.push((index, inputs, exclusions)),
1177 Err(error) => self.resolved[index] = Some(Err(error)),
1178 }
1179 }
1180 let input_discovery = discovery_started.elapsed();
1181
1182 let hashing_started = Instant::now();
1183 let resolved_inputs = progress.run_phase(WasmBuildProgressPhase::ContentHashing, || {
1184 let mut cache = LabeledPathDigestCache::default();
1185 discovered
1186 .into_iter()
1187 .map(|(index, inputs, exclusions)| {
1188 let input_digest = digest_labeled_paths_composable(
1189 "wasm-source-inputs-v1",
1190 &inputs,
1191 &exclusions,
1192 &mut cache,
1193 )
1194 .map_err(|source| WasmBuildError::Io {
1195 operation: "hash batched Wasm build inputs",
1196 path: active.workspace_root.clone(),
1197 source,
1198 })?;
1199 Ok::<_, WasmBuildError>((index, inputs, exclusions, input_digest))
1200 })
1201 .collect::<Result<Vec<_>, _>>()
1202 })?;
1203 let content_hashing = hashing_started.elapsed();
1204 let timings = WasmInputResolutionTimings {
1205 tool_identity,
1206 cargo_metadata,
1207 input_discovery,
1208 content_hashing,
1209 total: total_started.elapsed(),
1210 };
1211 let resolved_count = resolved_inputs.len();
1212 if resolved_count > 0 {
1213 self.metrics.runs += 1;
1214 self.metrics.reuses += resolved_count.saturating_sub(1);
1215 }
1216 let timing_index = resolved_inputs
1217 .iter()
1218 .any(|(index, ..)| *index == active_index)
1219 .then_some(active_index)
1220 .or_else(|| resolved_inputs.first().map(|(index, ..)| *index));
1221 for (index, inputs, exclusions, input_digest) in resolved_inputs {
1222 let spec = &self.specs[index];
1223 self.resolved[index] = Some(Ok(ResolvedCargoBuildInputs {
1224 fingerprint: finish_build_fingerprint(
1225 spec,
1226 &cargo_identity,
1227 &rustc_identity,
1228 input_digest,
1229 ),
1230 input_digest,
1231 inputs: inputs
1232 .into_iter()
1233 .map(|(label, path)| CargoBuildInput { label, path })
1234 .collect(),
1235 exclusions,
1236 timings: if Some(index) == timing_index {
1237 timings
1238 } else {
1239 WasmInputResolutionTimings::default()
1240 },
1241 }));
1242 }
1243 Ok(())
1244 }
1245}
1246
1247fn resolve_batch_tool_identity(
1248 spec: &WasmBuildSpec,
1249 progress: &mut ProgressReporter<'_>,
1250) -> Result<(Vec<u8>, Vec<u8>, Duration), WasmBuildError> {
1251 let started = Instant::now();
1252 let cargo_identity = progress.run_phase(WasmBuildProgressPhase::CargoIdentity, || {
1253 command_identity(
1254 spec,
1255 WasmBuildPhase::CargoIdentity,
1256 &spec.cargo_program,
1257 &["--version", "--verbose"],
1258 )
1259 })?;
1260 let rustc_program = spec
1261 .extra_env
1262 .get(OsStr::new("RUSTC"))
1263 .unwrap_or(&spec.rustc_program);
1264 let rustc_identity = progress.run_phase(WasmBuildProgressPhase::RustcIdentity, || {
1265 command_identity(spec, WasmBuildPhase::RustcIdentity, rustc_program, &["-vV"])
1266 })?;
1267 Ok((cargo_identity, rustc_identity, started.elapsed()))
1268}
1269
1270impl BatchResolutionKey {
1271 fn for_spec(spec: &WasmBuildSpec) -> Self {
1272 Self {
1273 workspace_root: spec.workspace_root.clone(),
1274 cargo_program: spec.cargo_program.clone(),
1275 rustc_program: spec
1276 .extra_env
1277 .get(OsStr::new("RUSTC"))
1278 .unwrap_or(&spec.rustc_program)
1279 .clone(),
1280 metadata_arguments: metadata_arguments(&spec.cargo_profile_args),
1281 environment: effective_environment(spec),
1282 }
1283 }
1284}
1285
1286impl SharedIncrementalTargetInspection {
1287 #[must_use]
1289 pub fn target_dir(&self) -> &Path {
1290 &self.target_dir
1291 }
1292
1293 #[must_use]
1295 pub const fn logical_size_bytes(&self) -> u64 {
1296 self.logical_size_bytes
1297 }
1298
1299 #[must_use]
1301 pub const fn last_used(&self) -> SystemTime {
1302 self.last_used
1303 }
1304
1305 #[must_use]
1307 pub const fn lock_wait(&self) -> Duration {
1308 self.lock_wait
1309 }
1310}
1311
1312impl SharedIncrementalTargetPrunePolicy {
1313 #[must_use]
1315 pub const fn new() -> Self {
1316 Self {
1317 max_age: None,
1318 max_size_bytes: None,
1319 }
1320 }
1321
1322 #[must_use]
1324 pub const fn with_max_age(mut self, max_age: Duration) -> Self {
1325 self.max_age = Some(max_age);
1326 self
1327 }
1328
1329 #[must_use]
1331 pub const fn with_max_size_bytes(mut self, bytes: u64) -> Self {
1332 self.max_size_bytes = Some(bytes);
1333 self
1334 }
1335
1336 #[must_use]
1338 pub const fn max_age(self) -> Option<Duration> {
1339 self.max_age
1340 }
1341
1342 #[must_use]
1344 pub const fn max_size_bytes(self) -> Option<u64> {
1345 self.max_size_bytes
1346 }
1347
1348 fn maintenance_identity(self) -> String {
1349 format!(
1350 "age={:?};size={:?}",
1351 self.max_age.map(|duration| duration.as_nanos()),
1352 self.max_size_bytes
1353 )
1354 }
1355}
1356
1357impl SharedIncrementalTargetMaintenanceConfig {
1358 #[must_use]
1360 pub const fn new(
1361 policy: SharedIncrementalTargetPrunePolicy,
1362 minimum_interval: Duration,
1363 ) -> Self {
1364 Self {
1365 policy,
1366 minimum_interval,
1367 failure_mode: SharedIncrementalTargetMaintenanceFailureMode::Strict,
1368 }
1369 }
1370
1371 #[must_use]
1373 pub const fn with_failure_mode(
1374 mut self,
1375 failure_mode: SharedIncrementalTargetMaintenanceFailureMode,
1376 ) -> Self {
1377 self.failure_mode = failure_mode;
1378 self
1379 }
1380
1381 #[must_use]
1383 pub const fn policy(self) -> SharedIncrementalTargetPrunePolicy {
1384 self.policy
1385 }
1386
1387 #[must_use]
1389 pub const fn minimum_interval(self) -> Duration {
1390 self.minimum_interval
1391 }
1392
1393 #[must_use]
1395 pub const fn failure_mode(self) -> SharedIncrementalTargetMaintenanceFailureMode {
1396 self.failure_mode
1397 }
1398}
1399
1400impl SharedIncrementalTargetMaintenance {
1401 #[must_use]
1403 pub fn target_dir(&self) -> &Path {
1404 &self.target_dir
1405 }
1406
1407 #[must_use]
1409 pub const fn logical_size_bytes_before(&self) -> u64 {
1410 self.logical_size_bytes_before
1411 }
1412
1413 #[must_use]
1415 pub const fn logical_size_bytes_after(&self) -> u64 {
1416 self.logical_size_bytes_after
1417 }
1418
1419 #[must_use]
1421 pub const fn last_used_before(&self) -> SystemTime {
1422 self.last_used_before
1423 }
1424
1425 #[must_use]
1427 pub const fn was_cleared(&self) -> bool {
1428 self.cleared
1429 }
1430
1431 #[must_use]
1433 pub const fn lock_wait(&self) -> Duration {
1434 self.lock_wait
1435 }
1436
1437 #[must_use]
1439 pub const fn maintenance(&self) -> Duration {
1440 self.maintenance
1441 }
1442}
1443
1444impl std::fmt::Display for SharedIncrementalTargetMaintenance {
1445 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1446 write!(
1447 formatter,
1448 "target={} action={} bytes={}=>{} lock={:?} maintenance={:?}",
1449 self.target_dir.display(),
1450 if self.cleared { "cleared" } else { "retained" },
1451 self.logical_size_bytes_before,
1452 self.logical_size_bytes_after,
1453 self.lock_wait,
1454 self.maintenance,
1455 )
1456 }
1457}
1458
1459impl SharedIncrementalTargetMaintenanceOutcome {
1460 #[must_use]
1462 pub fn target_dir(&self) -> &Path {
1463 match self {
1464 Self::Missing { target_dir }
1465 | Self::Skipped { target_dir, .. }
1466 | Self::Failed { target_dir, .. } => target_dir,
1467 Self::Performed { maintenance, .. } => maintenance.target_dir(),
1468 }
1469 }
1470
1471 #[must_use]
1473 pub const fn maintenance(&self) -> Option<&SharedIncrementalTargetMaintenance> {
1474 match self {
1475 Self::Performed { maintenance, .. } => Some(maintenance),
1476 Self::Missing { .. } | Self::Skipped { .. } | Self::Failed { .. } => None,
1477 }
1478 }
1479
1480 #[must_use]
1482 pub const fn was_performed(&self) -> bool {
1483 matches!(self, Self::Performed { .. })
1484 }
1485
1486 #[must_use]
1488 pub const fn lock_wait(&self) -> Option<Duration> {
1489 match self {
1490 Self::Missing { .. } => None,
1491 Self::Skipped { lock_wait, .. } | Self::Failed { lock_wait, .. } => Some(*lock_wait),
1492 Self::Performed { maintenance, .. } => Some(maintenance.lock_wait()),
1493 }
1494 }
1495
1496 #[must_use]
1498 pub const fn schedule_check(&self) -> Option<Duration> {
1499 match self {
1500 Self::Missing { .. } | Self::Failed { .. } => None,
1501 Self::Skipped { schedule_check, .. } | Self::Performed { schedule_check, .. } => {
1502 Some(*schedule_check)
1503 }
1504 }
1505 }
1506
1507 #[must_use]
1509 pub fn failure_message(&self) -> Option<&str> {
1510 match self {
1511 Self::Failed { message, .. } => Some(message),
1512 Self::Missing { .. } | Self::Skipped { .. } | Self::Performed { .. } => None,
1513 }
1514 }
1515}
1516
1517impl std::fmt::Display for SharedIncrementalTargetMaintenanceOutcome {
1518 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1519 match self {
1520 Self::Missing { target_dir } => {
1521 write!(formatter, "target={} action=missing", target_dir.display())
1522 }
1523 Self::Skipped {
1524 target_dir,
1525 lock_wait,
1526 schedule_check,
1527 } => write!(
1528 formatter,
1529 "target={} action=skipped lock={lock_wait:?} schedule={schedule_check:?}",
1530 target_dir.display(),
1531 ),
1532 Self::Performed {
1533 maintenance,
1534 schedule_check,
1535 } => write!(formatter, "{maintenance} schedule={schedule_check:?}"),
1536 Self::Failed {
1537 target_dir,
1538 lock_wait,
1539 message,
1540 } => write!(
1541 formatter,
1542 "target={} action=failed lock={lock_wait:?} error={message}",
1543 target_dir.display(),
1544 ),
1545 }
1546 }
1547}
1548
1549impl std::fmt::Display for WasmBuildTimings {
1550 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1551 write!(
1552 formatter,
1553 "total={:?} lock={:?} shared_lock={:?} inputs={:?} cargo={:?} maintenance={:?}",
1554 self.total,
1555 self.lock_wait,
1556 self.shared_incremental_lock_wait,
1557 self.input_resolution.total,
1558 self.cargo_build,
1559 self.cache_maintenance,
1560 )
1561 }
1562}
1563
1564impl std::fmt::Display for WasmBuildOutcome {
1565 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1566 let state = if self.is_reused() { "reused" } else { "built" };
1567 write!(
1568 formatter,
1569 "{state} fingerprint={} artifacts={} {}",
1570 self.record().fingerprint,
1571 self.record().artifacts.len(),
1572 self.record().timings,
1573 )?;
1574 if let Some(maintenance) = self.record().shared_incremental_maintenance() {
1575 write!(formatter, " shared_maintenance=({maintenance})")?;
1576 }
1577 Ok(())
1578 }
1579}
1580
1581pub fn resolve_cargo_build_inputs(
1586 spec: &WasmBuildSpec,
1587) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
1588 validate_spec(spec)?;
1589 build_fingerprint(spec)
1590}
1591
1592pub fn inspect_shared_incremental_target(
1597 spec: &WasmBuildSpec,
1598) -> Result<Option<SharedIncrementalTargetInspection>, WasmBuildError> {
1599 if !shared_incremental_target_exists(spec, "inspect shared incremental Cargo target")? {
1600 return Ok(None);
1601 }
1602
1603 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
1604 let logical_size_bytes =
1605 directory_logical_size(&canonical).map_err(|source| WasmBuildError::Io {
1606 operation: "measure shared incremental Cargo target",
1607 path: canonical.clone(),
1608 source,
1609 })?;
1610 let last_used = cache_entry_last_used(&canonical).map_err(|source| WasmBuildError::Io {
1611 operation: "read shared incremental Cargo target use time",
1612 path: canonical.clone(),
1613 source,
1614 })?;
1615 Ok(Some(SharedIncrementalTargetInspection {
1616 target_dir: canonical,
1617 logical_size_bytes,
1618 last_used,
1619 lock_wait,
1620 }))
1621}
1622
1623pub fn maintain_shared_incremental_target(
1637 spec: &WasmBuildSpec,
1638 policy: SharedIncrementalTargetPrunePolicy,
1639) -> Result<Option<SharedIncrementalTargetMaintenance>, WasmBuildError> {
1640 if !shared_incremental_target_exists(
1641 spec,
1642 "inspect shared incremental Cargo target before maintenance",
1643 )? {
1644 return Ok(None);
1645 }
1646
1647 let _ = resolve_cargo_build_inputs(spec)?;
1651 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
1652 maintain_shared_incremental_target_locked(&canonical, policy, lock_wait).map(Some)
1653}
1654
1655pub fn maintain_shared_incremental_target_at_most_every(
1669 spec: &WasmBuildSpec,
1670 policy: SharedIncrementalTargetPrunePolicy,
1671 minimum_interval: Duration,
1672) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
1673 let target_dir =
1674 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
1675 message: "shared incremental target is not configured".to_owned(),
1676 })?;
1677 if !shared_incremental_target_exists(
1678 spec,
1679 "inspect shared incremental Cargo target before scheduled maintenance",
1680 )? {
1681 return Ok(SharedIncrementalTargetMaintenanceOutcome::Missing { target_dir });
1682 }
1683
1684 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
1685 let schedule = schedule_shared_incremental_target_maintenance(
1686 &canonical,
1687 policy,
1688 minimum_interval,
1689 lock_wait,
1690 )?;
1691 let schedule = match schedule {
1692 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => return Ok(outcome),
1693 SharedIncrementalTargetMaintenanceSchedule::Due(due) => due,
1694 };
1695
1696 let _ = resolve_cargo_build_inputs(spec)?;
1699 perform_due_shared_incremental_target_maintenance(&canonical, policy, lock_wait, schedule)
1700}
1701
1702enum SharedIncrementalTargetMaintenanceSchedule {
1703 Skipped(SharedIncrementalTargetMaintenanceOutcome),
1704 Due(DueSharedIncrementalTargetMaintenance),
1705}
1706
1707struct DueSharedIncrementalTargetMaintenance {
1708 schedule_root: PathBuf,
1709 maintenance_identity: String,
1710 schedule_check: Duration,
1711}
1712
1713fn schedule_shared_incremental_target_maintenance(
1714 canonical: &Path,
1715 policy: SharedIncrementalTargetPrunePolicy,
1716 minimum_interval: Duration,
1717 lock_wait: Duration,
1718) -> Result<SharedIncrementalTargetMaintenanceSchedule, WasmBuildError> {
1719 let schedule_root = canonical.join(".ic-testkit");
1720 let maintenance_identity = policy.maintenance_identity();
1721 let schedule_started = Instant::now();
1722 let due = cache_maintenance_due(
1723 &schedule_root,
1724 Some(minimum_interval),
1725 &maintenance_identity,
1726 )
1727 .map_err(wasm_cache_fs_error)?;
1728 let schedule_check = schedule_started.elapsed();
1729 if !due {
1730 return Ok(SharedIncrementalTargetMaintenanceSchedule::Skipped(
1731 SharedIncrementalTargetMaintenanceOutcome::Skipped {
1732 target_dir: canonical.to_owned(),
1733 lock_wait,
1734 schedule_check,
1735 },
1736 ));
1737 }
1738 Ok(SharedIncrementalTargetMaintenanceSchedule::Due(
1739 DueSharedIncrementalTargetMaintenance {
1740 schedule_root,
1741 maintenance_identity,
1742 schedule_check,
1743 },
1744 ))
1745}
1746
1747fn perform_due_shared_incremental_target_maintenance(
1748 canonical: &Path,
1749 policy: SharedIncrementalTargetPrunePolicy,
1750 lock_wait: Duration,
1751 due: DueSharedIncrementalTargetMaintenance,
1752) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
1753 let DueSharedIncrementalTargetMaintenance {
1754 schedule_root,
1755 maintenance_identity,
1756 schedule_check,
1757 } = due;
1758 let maintenance = maintain_shared_incremental_target_locked(canonical, policy, lock_wait)?;
1759 record_cache_maintenance(&schedule_root, &maintenance_identity).map_err(wasm_cache_fs_error)?;
1760 Ok(SharedIncrementalTargetMaintenanceOutcome::Performed {
1761 maintenance,
1762 schedule_check,
1763 })
1764}
1765
1766fn maintain_shared_incremental_target_locked(
1767 canonical: &Path,
1768 policy: SharedIncrementalTargetPrunePolicy,
1769 lock_wait: Duration,
1770) -> Result<SharedIncrementalTargetMaintenance, WasmBuildError> {
1771 let started = Instant::now();
1772 let logical_size_bytes_before =
1773 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
1774 operation: "measure shared incremental Cargo target before maintenance",
1775 path: canonical.to_owned(),
1776 source,
1777 })?;
1778 let last_used_before =
1779 cache_entry_last_used(canonical).map_err(|source| WasmBuildError::Io {
1780 operation: "read shared incremental Cargo target use time before maintenance",
1781 path: canonical.to_owned(),
1782 source,
1783 })?;
1784 let expired = policy.max_age.is_some_and(|max_age| {
1785 SystemTime::now()
1786 .duration_since(last_used_before)
1787 .is_ok_and(|age| age > max_age)
1788 });
1789 let oversized = policy
1790 .max_size_bytes
1791 .is_some_and(|max_size_bytes| logical_size_bytes_before > max_size_bytes);
1792 let cleared = expired || oversized;
1793 if cleared {
1794 clear_shared_incremental_target_contents(canonical)?;
1795 record_cache_entry_use(canonical)?;
1796 }
1797 let logical_size_bytes_after = if cleared {
1798 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
1799 operation: "measure shared incremental Cargo target after maintenance",
1800 path: canonical.to_owned(),
1801 source,
1802 })?
1803 } else {
1804 logical_size_bytes_before
1805 };
1806 Ok(SharedIncrementalTargetMaintenance {
1807 target_dir: canonical.to_owned(),
1808 logical_size_bytes_before,
1809 logical_size_bytes_after,
1810 last_used_before,
1811 cleared,
1812 lock_wait,
1813 maintenance: started.elapsed(),
1814 })
1815}
1816
1817fn clear_shared_incremental_target_contents(target_dir: &Path) -> Result<(), WasmBuildError> {
1818 let entries = fs::read_dir(target_dir).map_err(|source| WasmBuildError::Io {
1819 operation: "read shared incremental Cargo target for maintenance",
1820 path: target_dir.to_owned(),
1821 source,
1822 })?;
1823 for entry in entries {
1824 let path = entry
1825 .map_err(|source| WasmBuildError::Io {
1826 operation: "read shared incremental Cargo target entry for maintenance",
1827 path: target_dir.to_owned(),
1828 source,
1829 })?
1830 .path();
1831 let preserved = path
1832 .file_name()
1833 .is_some_and(|name| name == ".ic-testkit" || name == "CACHEDIR.TAG");
1834 if !preserved {
1835 remove_path_if_present(&path).map_err(|source| WasmBuildError::Io {
1836 operation: "clear shared incremental Cargo target entry",
1837 path,
1838 source,
1839 })?;
1840 }
1841 }
1842 Ok(())
1843}
1844
1845pub fn build_wasm_canisters_cached(
1852 spec: &WasmBuildSpec,
1853) -> Result<WasmBuildOutcome, WasmBuildError> {
1854 build_wasm_canisters_cached_internal(spec, &mut ProgressReporter::silent(), None)
1855}
1856
1857pub(super) fn build_wasm_canisters_cached_in_batch(
1858 spec: &WasmBuildSpec,
1859 index: usize,
1860 resolver: &mut WasmBuildBatchInputResolver<'_>,
1861) -> Result<WasmBuildOutcome, WasmBuildError> {
1862 build_wasm_canisters_cached_internal(
1863 spec,
1864 &mut ProgressReporter::silent(),
1865 Some((resolver, index)),
1866 )
1867}
1868
1869pub fn build_wasm_canisters_cached_with_progress<F>(
1878 spec: &WasmBuildSpec,
1879 config: WasmBuildProgressConfig,
1880 mut observer: F,
1881) -> Result<WasmBuildOutcome, WasmBuildError>
1882where
1883 F: FnMut(WasmBuildProgressEvent),
1884{
1885 if config.heartbeat_interval == Some(Duration::ZERO) {
1886 return Err(WasmBuildError::InvalidSpec {
1887 message: "Wasm build progress heartbeat interval must be greater than zero".to_owned(),
1888 });
1889 }
1890 build_wasm_canisters_cached_internal(
1891 spec,
1892 &mut ProgressReporter::observed(config, &mut observer),
1893 None,
1894 )
1895}
1896
1897pub(super) fn build_wasm_canisters_cached_in_batch_with_progress<F>(
1898 spec: &WasmBuildSpec,
1899 index: usize,
1900 resolver: &mut WasmBuildBatchInputResolver<'_>,
1901 config: WasmBuildProgressConfig,
1902 mut observer: F,
1903) -> Result<WasmBuildOutcome, WasmBuildError>
1904where
1905 F: FnMut(WasmBuildProgressEvent),
1906{
1907 if config.heartbeat_interval == Some(Duration::ZERO) {
1908 return Err(WasmBuildError::InvalidSpec {
1909 message: "Wasm build progress heartbeat interval must be greater than zero".to_owned(),
1910 });
1911 }
1912 build_wasm_canisters_cached_internal(
1913 spec,
1914 &mut ProgressReporter::observed(config, &mut observer),
1915 Some((resolver, index)),
1916 )
1917}
1918
1919fn build_wasm_canisters_cached_internal(
1920 spec: &WasmBuildSpec,
1921 progress: &mut ProgressReporter<'_>,
1922 mut batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_>, usize)>,
1923) -> Result<WasmBuildOutcome, WasmBuildError> {
1924 let total_started = Instant::now();
1925 validate_spec(spec)?;
1926 progress.emit(WasmBuildProgressEvent::Started);
1927 if spec.shared_incremental_maintenance_config.is_some() {
1928 let outcome = build_wasm_canisters_cached_with_scheduled_shared_maintenance(
1929 spec,
1930 total_started,
1931 progress,
1932 batch_resolution.take(),
1933 )?;
1934 emit_finished_progress(&outcome, progress);
1935 return Ok(outcome);
1936 }
1937 let (cache_lock, first_lock_wait) =
1938 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
1939 ensure_cache_directory_tag(&spec.target_dir)?;
1940
1941 let resolved = resolve_initial_inputs(spec, batch_resolution.take(), progress)?;
1942 if let Some(outcome) = try_reuse_wasm_artifacts(
1943 spec,
1944 &resolved,
1945 first_lock_wait,
1946 &SharedIncrementalAcquisitionContext::default(),
1947 total_started,
1948 progress,
1949 )? {
1950 emit_finished_progress(&outcome, progress);
1951 return Ok(outcome);
1952 }
1953 progress.emit(WasmBuildProgressEvent::CacheMiss {
1954 fingerprint: resolved.fingerprint,
1955 });
1956
1957 let outcome = match &spec.cache_mode {
1958 WasmBuildCacheMode::Isolated => {
1959 let cache_entry = cache_entry_directory(spec, resolved.fingerprint);
1960 build_wasm_cache_miss(
1961 spec,
1962 resolved,
1963 first_lock_wait,
1964 SharedIncrementalAcquisitionContext::default(),
1965 cache_entry,
1966 total_started,
1967 progress,
1968 )
1969 }
1970 WasmBuildCacheMode::SharedIncremental { .. } => {
1971 drop(cache_lock);
1972 let configured_target = shared_incremental_target(spec)
1973 .expect("shared cache mode must resolve a shared Cargo target");
1974 progress.emit(WasmBuildProgressEvent::SharedTargetLockStarted {
1975 target_dir: configured_target,
1976 });
1977 let (shared_lock, shared_lock_wait, shared_target) =
1978 lock_shared_incremental_target_with_progress(spec, progress)?;
1979 progress.emit(WasmBuildProgressEvent::SharedTargetLockAcquired {
1980 target_dir: shared_target.clone(),
1981 wait: shared_lock_wait,
1982 });
1983 let (_cache_lock, second_lock_wait) =
1984 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
1985 ensure_cache_directory_tag(&spec.target_dir)?;
1986
1987 let mut current = resolve_inputs_with_progress(spec, progress)?;
1988 current.timings.include(resolved.timings);
1989 let lock_wait = first_lock_wait.saturating_add(second_lock_wait);
1990 let shared_incremental = SharedIncrementalAcquisitionContext {
1991 lock_wait: Some(shared_lock_wait),
1992 maintenance: None,
1993 };
1994 if let Some(outcome) = try_reuse_wasm_artifacts(
1995 spec,
1996 ¤t,
1997 lock_wait,
1998 &shared_incremental,
1999 total_started,
2000 progress,
2001 )? {
2002 emit_finished_progress(&outcome, progress);
2003 return Ok(outcome);
2004 }
2005
2006 let outcome = build_wasm_cache_miss(
2007 spec,
2008 current,
2009 lock_wait,
2010 shared_incremental,
2011 shared_target,
2012 total_started,
2013 progress,
2014 );
2015 drop(shared_lock);
2016 outcome
2017 }
2018 }?;
2019 emit_finished_progress(&outcome, progress);
2020 Ok(outcome)
2021}
2022
2023fn build_wasm_canisters_cached_with_scheduled_shared_maintenance(
2024 spec: &WasmBuildSpec,
2025 total_started: Instant,
2026 progress: &mut ProgressReporter<'_>,
2027 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_>, usize)>,
2028) -> Result<WasmBuildOutcome, WasmBuildError> {
2029 let configured_target = shared_incremental_target(spec)
2030 .expect("validated scheduled maintenance must have a shared Cargo target");
2031 progress.emit(WasmBuildProgressEvent::SharedTargetLockStarted {
2032 target_dir: configured_target,
2033 });
2034 let (_shared_lock, shared_lock_wait, shared_target) =
2035 lock_shared_incremental_target_with_progress(spec, progress)?;
2036 progress.emit(WasmBuildProgressEvent::SharedTargetLockAcquired {
2037 target_dir: shared_target.clone(),
2038 wait: shared_lock_wait,
2039 });
2040 let (_cache_lock, lock_wait) = lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2041 ensure_cache_directory_tag(&spec.target_dir)?;
2042
2043 let resolved = resolve_initial_inputs(spec, batch_resolution, progress)?;
2046 let shared_maintenance = perform_configured_shared_incremental_target_maintenance(
2047 spec,
2048 &shared_target,
2049 shared_lock_wait,
2050 progress,
2051 )?;
2052 let shared_incremental = SharedIncrementalAcquisitionContext {
2053 lock_wait: Some(shared_lock_wait),
2054 maintenance: Some(shared_maintenance),
2055 };
2056 if let Some(outcome) = try_reuse_wasm_artifacts(
2057 spec,
2058 &resolved,
2059 lock_wait,
2060 &shared_incremental,
2061 total_started,
2062 progress,
2063 )? {
2064 return Ok(outcome);
2065 }
2066 progress.emit(WasmBuildProgressEvent::CacheMiss {
2067 fingerprint: resolved.fingerprint,
2068 });
2069 build_wasm_cache_miss(
2070 spec,
2071 resolved,
2072 lock_wait,
2073 shared_incremental,
2074 shared_target,
2075 total_started,
2076 progress,
2077 )
2078}
2079
2080fn perform_configured_shared_incremental_target_maintenance(
2081 spec: &WasmBuildSpec,
2082 shared_target: &Path,
2083 lock_wait: Duration,
2084 progress: &mut ProgressReporter<'_>,
2085) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2086 let config = spec
2087 .shared_incremental_maintenance_config
2088 .expect("configured shared-target maintenance must have settings");
2089 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceStarted {
2090 target_dir: shared_target.to_owned(),
2091 });
2092 let result = progress.run_phase(WasmBuildProgressPhase::SharedTargetMaintenance, || {
2093 let schedule = schedule_shared_incremental_target_maintenance(
2094 shared_target,
2095 config.policy,
2096 config.minimum_interval,
2097 lock_wait,
2098 )?;
2099 match schedule {
2100 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => Ok(outcome),
2101 SharedIncrementalTargetMaintenanceSchedule::Due(due) => {
2102 perform_due_shared_incremental_target_maintenance(
2103 shared_target,
2104 config.policy,
2105 lock_wait,
2106 due,
2107 )
2108 }
2109 }
2110 });
2111 let outcome = integrated_shared_maintenance_result(config, shared_target, lock_wait, result)?;
2112 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceFinished {
2113 outcome: outcome.clone(),
2114 });
2115 Ok(outcome)
2116}
2117
2118fn integrated_shared_maintenance_result(
2119 config: SharedIncrementalTargetMaintenanceConfig,
2120 shared_target: &Path,
2121 lock_wait: Duration,
2122 result: Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError>,
2123) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2124 match result {
2125 Ok(outcome) => Ok(outcome),
2126 Err(error)
2127 if config.failure_mode == SharedIncrementalTargetMaintenanceFailureMode::BestEffort =>
2128 {
2129 Ok(SharedIncrementalTargetMaintenanceOutcome::Failed {
2130 target_dir: shared_target.to_owned(),
2131 lock_wait,
2132 message: error.to_string(),
2133 })
2134 }
2135 Err(error) => Err(error),
2136 }
2137}
2138
2139fn resolve_inputs_with_progress(
2140 spec: &WasmBuildSpec,
2141 progress: &mut ProgressReporter<'_>,
2142) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2143 let resolved = build_fingerprint_with_progress(spec, progress)?;
2144 progress.emit(WasmBuildProgressEvent::InputsResolved {
2145 fingerprint: resolved.fingerprint,
2146 input_digest: resolved.input_digest,
2147 elapsed: resolved.timings.total,
2148 });
2149 Ok(resolved)
2150}
2151
2152fn resolve_initial_inputs(
2153 spec: &WasmBuildSpec,
2154 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_>, usize)>,
2155 progress: &mut ProgressReporter<'_>,
2156) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2157 let resolved = if let Some((resolver, index)) = batch_resolution {
2158 resolver.resolve(index, progress)?
2159 } else {
2160 build_fingerprint_with_progress(spec, progress)?
2161 };
2162 progress.emit(WasmBuildProgressEvent::InputsResolved {
2163 fingerprint: resolved.fingerprint,
2164 input_digest: resolved.input_digest,
2165 elapsed: resolved.timings.total,
2166 });
2167 Ok(resolved)
2168}
2169
2170fn emit_finished_progress(outcome: &WasmBuildOutcome, progress: &mut ProgressReporter<'_>) {
2171 let state = if outcome.is_reused() {
2172 progress.emit(WasmBuildProgressEvent::CacheHit {
2173 fingerprint: outcome.record().fingerprint,
2174 });
2175 WasmBuildProgressOutcome::Reused
2176 } else {
2177 WasmBuildProgressOutcome::Built
2178 };
2179 progress.emit(WasmBuildProgressEvent::Finished {
2180 outcome: state,
2181 fingerprint: outcome.record().fingerprint,
2182 elapsed: outcome.record().timings.total,
2183 });
2184}
2185
2186#[derive(Clone, Debug, Default)]
2187struct SharedIncrementalAcquisitionContext {
2188 lock_wait: Option<Duration>,
2189 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
2190}
2191
2192fn try_reuse_wasm_artifacts(
2193 spec: &WasmBuildSpec,
2194 resolved: &ResolvedCargoBuildInputs,
2195 lock_wait: Duration,
2196 shared_incremental: &SharedIncrementalAcquisitionContext,
2197 total_started: Instant,
2198 progress: &mut ProgressReporter<'_>,
2199) -> Result<Option<WasmBuildOutcome>, WasmBuildError> {
2200 let fingerprint = resolved.fingerprint;
2201 let artifacts = expected_artifacts(spec, &spec.target_dir);
2202 let cache_entry = cache_entry_directory(spec, fingerprint);
2203 let artifacts_match = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2204 artifact_set_matches(&artifacts, fingerprint)
2205 });
2206 if artifacts_match {
2207 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2208 ensure_exact_cache_entry(spec, &artifacts, &cache_entry, fingerprint)
2209 })?;
2210 return Ok(Some(WasmBuildOutcome::Reused(complete_build_record(
2211 spec,
2212 BuildRecordInput {
2213 fingerprint,
2214 input_digest: resolved.input_digest,
2215 artifacts,
2216 lock_wait,
2217 shared_incremental: shared_incremental.clone(),
2218 input_resolution: resolved.timings,
2219 cargo_build: None,
2220 active_entry: &cache_entry,
2221 },
2222 total_started,
2223 progress,
2224 ))));
2225 }
2226
2227 let cached_artifacts = expected_artifacts(spec, &cache_entry);
2228 let cached_artifacts_match = progress
2229 .run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2230 artifact_set_matches(&cached_artifacts, fingerprint)
2231 });
2232 if !cached_artifacts_match {
2233 return Ok(None);
2234 }
2235 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2236 materialize_artifacts(&cached_artifacts, &artifacts, fingerprint)?;
2237 record_cache_entry_use(&cache_entry)
2238 })?;
2239 Ok(Some(WasmBuildOutcome::Reused(complete_build_record(
2240 spec,
2241 BuildRecordInput {
2242 fingerprint,
2243 input_digest: resolved.input_digest,
2244 artifacts,
2245 lock_wait,
2246 shared_incremental: shared_incremental.clone(),
2247 input_resolution: resolved.timings,
2248 cargo_build: None,
2249 active_entry: &cache_entry,
2250 },
2251 total_started,
2252 progress,
2253 ))))
2254}
2255
2256fn ensure_exact_cache_entry(
2257 spec: &WasmBuildSpec,
2258 artifacts: &[PathBuf],
2259 cache_entry: &Path,
2260 fingerprint: InputDigest,
2261) -> Result<(), WasmBuildError> {
2262 let cached_artifacts = expected_artifacts(spec, cache_entry);
2263 if artifact_set_matches(&cached_artifacts, fingerprint) {
2264 return record_cache_entry_use(cache_entry);
2265 }
2266 remove_directory_if_present(cache_entry)?;
2267 create_dir_all(
2268 cache_entry,
2269 "create content-addressed Cargo target directory",
2270 )?;
2271 let incomplete = IncompleteBuildDirectory::new(cache_entry.to_owned());
2272 let result = (|| {
2273 copy_wasm_artifacts(artifacts, &cached_artifacts)?;
2274 publish_artifact_stamps(&cached_artifacts, fingerprint)?;
2275 record_cache_entry_use(cache_entry)
2276 })();
2277 match result {
2278 Ok(()) => {
2279 incomplete.preserve();
2280 Ok(())
2281 }
2282 Err(build_error) => {
2283 let path = incomplete.path.clone();
2284 match incomplete.cleanup() {
2285 Ok(()) => Err(build_error),
2286 Err(source) => Err(WasmBuildError::FailedBuildCleanup {
2287 build_error: Box::new(build_error),
2288 path,
2289 source,
2290 }),
2291 }
2292 }
2293 }
2294}
2295
2296fn build_wasm_cache_miss(
2297 spec: &WasmBuildSpec,
2298 resolved: ResolvedCargoBuildInputs,
2299 lock_wait: Duration,
2300 shared_incremental: SharedIncrementalAcquisitionContext,
2301 cargo_target_dir: PathBuf,
2302 total_started: Instant,
2303 progress: &mut ProgressReporter<'_>,
2304) -> Result<WasmBuildOutcome, WasmBuildError> {
2305 let fingerprint = resolved.fingerprint;
2306 let mut input_resolution = resolved.timings;
2307 let artifacts = expected_artifacts(spec, &spec.target_dir);
2308 let cache_entry = cache_entry_directory(spec, fingerprint);
2309 remove_directory_if_present(&cache_entry)?;
2310 create_dir_all(
2311 &cache_entry,
2312 "create content-addressed Cargo target directory",
2313 )?;
2314 let incomplete_directory = IncompleteBuildDirectory::new(cache_entry.clone());
2315 let build_result = (|| {
2316 if matches!(
2317 spec.cache_mode,
2318 WasmBuildCacheMode::SharedIncremental { .. }
2319 ) {
2320 record_cache_entry_use(&cargo_target_dir)?;
2321 }
2322 let build_started = Instant::now();
2323 run_cargo_build(spec, &cargo_target_dir, progress)?;
2324 let cargo_build = build_started.elapsed();
2325 let built_artifacts = expected_artifacts(spec, &cargo_target_dir);
2326 let missing = missing_artifacts(&built_artifacts);
2327 if !missing.is_empty() {
2328 return Err(WasmBuildError::MissingArtifacts { paths: missing });
2329 }
2330
2331 let verified = resolve_inputs_with_progress(spec, progress)?;
2332 input_resolution.include(verified.timings);
2333 if fingerprint != verified.fingerprint {
2334 return Err(WasmBuildError::InputsChangedDuringBuild {
2335 before: fingerprint,
2336 after: verified.fingerprint,
2337 });
2338 }
2339
2340 let cached_artifacts = expected_artifacts(spec, &cache_entry);
2341 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2342 if cargo_target_dir != cache_entry {
2343 copy_wasm_artifacts(&built_artifacts, &cached_artifacts)?;
2344 }
2345 publish_artifact_stamps(&cached_artifacts, fingerprint)?;
2346 materialize_artifacts(&cached_artifacts, &artifacts, fingerprint)?;
2347 record_cache_entry_use(&cache_entry)
2348 })?;
2349
2350 Ok(WasmBuildOutcome::Built(complete_build_record(
2351 spec,
2352 BuildRecordInput {
2353 fingerprint,
2354 input_digest: resolved.input_digest,
2355 artifacts,
2356 lock_wait,
2357 shared_incremental,
2358 input_resolution,
2359 cargo_build: Some(cargo_build),
2360 active_entry: &cache_entry,
2361 },
2362 total_started,
2363 progress,
2364 )))
2365 })();
2366 finish_fingerprint_build(build_result, incomplete_directory)
2367}
2368
2369pub fn prune_wasm_build_cache(
2377 target_dir: &Path,
2378 policy: ArtifactCachePrunePolicy,
2379) -> Result<ArtifactCachePruneReport, WasmBuildError> {
2380 let (_lock_file, _) = lock_wasm_build_cache(target_dir)?;
2381 ensure_cache_directory_tag(target_dir)?;
2382
2383 prune_wasm_build_cache_locked(target_dir, policy, None)
2384}
2385
2386struct BuildRecordInput<'a> {
2387 fingerprint: InputDigest,
2388 input_digest: InputDigest,
2389 artifacts: Vec<PathBuf>,
2390 lock_wait: Duration,
2391 shared_incremental: SharedIncrementalAcquisitionContext,
2392 input_resolution: WasmInputResolutionTimings,
2393 cargo_build: Option<Duration>,
2394 active_entry: &'a Path,
2395}
2396
2397fn complete_build_record(
2398 spec: &WasmBuildSpec,
2399 input: BuildRecordInput<'_>,
2400 total_started: Instant,
2401 progress: &mut ProgressReporter<'_>,
2402) -> WasmBuildRecord {
2403 let (maintenance, cache_maintenance) = spec.prune_policy.map_or((None, None), |policy| {
2404 progress.run_phase(WasmBuildProgressPhase::ExactCacheMaintenance, || {
2405 let cache_root = spec.target_dir.join(".ic-testkit/wasm-targets");
2406 let identity = policy.maintenance_identity();
2407 perform_scheduled_cache_maintenance(&cache_root, spec.prune_interval, &identity, || {
2408 prune_wasm_build_cache_locked(&spec.target_dir, policy, Some(input.active_entry))
2409 .map_err(|error| error.to_string())
2410 })
2411 })
2412 });
2413 WasmBuildRecord {
2414 fingerprint: input.fingerprint,
2415 input_digest: input.input_digest,
2416 exact_cache_path: input.active_entry.to_owned(),
2417 artifacts: input.artifacts,
2418 timings: WasmBuildTimings {
2419 lock_wait: input.lock_wait,
2420 shared_incremental_lock_wait: input.shared_incremental.lock_wait,
2421 input_resolution: input.input_resolution,
2422 cargo_build: input.cargo_build,
2423 cache_maintenance,
2424 total: total_started.elapsed(),
2425 },
2426 maintenance,
2427 shared_incremental_maintenance: input.shared_incremental.maintenance,
2428 }
2429}
2430
2431fn prune_wasm_build_cache_locked(
2432 target_dir: &Path,
2433 policy: ArtifactCachePrunePolicy,
2434 protected_entry: Option<&Path>,
2435) -> Result<ArtifactCachePruneReport, WasmBuildError> {
2436 let cache_root = target_dir.join(".ic-testkit/wasm-targets");
2437 prune_direct_child_directories(&cache_root, policy, protected_entry, is_sha256_directory)
2438 .map_err(wasm_cache_fs_error)
2439}
2440
2441struct IncompleteBuildDirectory {
2442 path: PathBuf,
2443 armed: bool,
2444}
2445
2446impl IncompleteBuildDirectory {
2447 const fn new(path: PathBuf) -> Self {
2448 Self { path, armed: true }
2449 }
2450
2451 fn preserve(mut self) {
2452 self.armed = false;
2453 }
2454
2455 fn cleanup(mut self) -> io::Result<()> {
2456 let result = remove_path_if_present(&self.path);
2457 if result.is_ok() {
2458 self.armed = false;
2459 }
2460 result
2461 }
2462}
2463
2464impl Drop for IncompleteBuildDirectory {
2465 fn drop(&mut self) {
2466 if self.armed {
2467 let _ = remove_path_if_present(&self.path);
2468 }
2469 }
2470}
2471
2472fn finish_fingerprint_build(
2473 result: Result<WasmBuildOutcome, WasmBuildError>,
2474 incomplete_directory: IncompleteBuildDirectory,
2475) -> Result<WasmBuildOutcome, WasmBuildError> {
2476 match result {
2477 Ok(outcome) => {
2478 incomplete_directory.preserve();
2479 Ok(outcome)
2480 }
2481 Err(build_error) => {
2482 let path = incomplete_directory.path.clone();
2483 match incomplete_directory.cleanup() {
2484 Ok(()) => Err(build_error),
2485 Err(source) => Err(WasmBuildError::FailedBuildCleanup {
2486 build_error: Box::new(build_error),
2487 path,
2488 source,
2489 }),
2490 }
2491 }
2492 }
2493}
2494
2495fn lock_wasm_build_cache(target_dir: &Path) -> Result<(File, Duration), WasmBuildError> {
2496 create_dir_all(target_dir, "create Cargo target directory")?;
2497 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
2498 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)
2499}
2500
2501fn lock_wasm_build_cache_with_progress(
2502 target_dir: &Path,
2503 progress: &mut ProgressReporter<'_>,
2504) -> Result<(File, Duration), WasmBuildError> {
2505 create_dir_all(target_dir, "create Cargo target directory")?;
2506 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
2507 lock_cache_file_with_progress(&lock_path, WasmBuildProgressPhase::ExactCacheLock, progress)
2508}
2509
2510fn lock_shared_incremental_target(
2511 spec: &WasmBuildSpec,
2512) -> Result<(File, Duration, PathBuf), WasmBuildError> {
2513 lock_shared_incremental_target_internal(spec, None)
2514}
2515
2516fn lock_shared_incremental_target_with_progress(
2517 spec: &WasmBuildSpec,
2518 progress: &mut ProgressReporter<'_>,
2519) -> Result<(File, Duration, PathBuf), WasmBuildError> {
2520 lock_shared_incremental_target_internal(spec, Some(progress))
2521}
2522
2523fn lock_shared_incremental_target_internal(
2524 spec: &WasmBuildSpec,
2525 progress: Option<&mut ProgressReporter<'_>>,
2526) -> Result<(File, Duration, PathBuf), WasmBuildError> {
2527 let target_dir =
2528 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
2529 message: "shared incremental target is not configured".to_owned(),
2530 })?;
2531 create_dir_all(
2532 &target_dir,
2533 "create shared incremental Cargo target directory",
2534 )?;
2535 ensure_cache_tag(&target_dir).map_err(wasm_cache_fs_error)?;
2536 let canonical = target_dir
2537 .canonicalize()
2538 .map_err(|source| WasmBuildError::Io {
2539 operation: "resolve shared incremental Cargo target directory",
2540 path: target_dir.clone(),
2541 source,
2542 })?;
2543 let lock_path = canonical.join(".ic-testkit/wasm-incremental.lock");
2544 let (lock, wait) = if let Some(progress) = progress {
2545 lock_cache_file_with_progress(
2546 &lock_path,
2547 WasmBuildProgressPhase::SharedTargetLock,
2548 progress,
2549 )?
2550 } else {
2551 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)?
2552 };
2553 Ok((lock, wait, canonical))
2554}
2555
2556fn lock_cache_file_with_progress(
2557 lock_path: &Path,
2558 phase: WasmBuildProgressPhase,
2559 progress: &mut ProgressReporter<'_>,
2560) -> Result<(File, Duration), WasmBuildError> {
2561 if !progress.is_observed() || progress.config.heartbeat_interval.is_none() {
2562 return lock_cache_file(lock_path).map_err(wasm_cache_fs_error);
2563 }
2564 let heartbeat_interval = progress
2565 .config
2566 .heartbeat_interval
2567 .expect("observed cache lock must have a heartbeat interval");
2568 lock_cache_file_with_wait_observer(lock_path, heartbeat_interval, |elapsed| {
2569 progress.emit_heartbeat_if_due(phase, elapsed);
2570 })
2571 .map_err(wasm_cache_fs_error)
2572}
2573
2574fn ensure_cache_directory_tag(target_dir: &Path) -> Result<(), WasmBuildError> {
2575 ensure_cache_tag(target_dir).map_err(wasm_cache_fs_error)
2576}
2577
2578fn record_cache_entry_use(path: &Path) -> Result<(), WasmBuildError> {
2579 record_entry_use(path).map_err(wasm_cache_fs_error)
2580}
2581
2582fn wasm_cache_fs_error(error: CacheFsError) -> WasmBuildError {
2583 WasmBuildError::Io {
2584 operation: error.operation,
2585 path: error.path,
2586 source: error.source,
2587 }
2588}
2589
2590fn validate_spec(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
2591 if spec.packages.is_empty() {
2592 return Err(WasmBuildError::InvalidSpec {
2593 message: "at least one Cargo package is required".to_owned(),
2594 });
2595 }
2596 if spec.profile_target_dir.is_empty() {
2597 return Err(WasmBuildError::InvalidSpec {
2598 message: "Cargo profile target directory must not be empty".to_owned(),
2599 });
2600 }
2601 if spec.target.is_empty() {
2602 return Err(WasmBuildError::InvalidSpec {
2603 message: "Cargo compilation target must not be empty".to_owned(),
2604 });
2605 }
2606 if matches!(
2607 &spec.cache_mode,
2608 WasmBuildCacheMode::SharedIncremental { target_dir } if target_dir.as_os_str().is_empty()
2609 ) {
2610 return Err(WasmBuildError::InvalidSpec {
2611 message: "shared incremental Cargo target directory must not be empty".to_owned(),
2612 });
2613 }
2614 if spec.shared_incremental_maintenance_config.is_some()
2615 && !matches!(
2616 spec.cache_mode,
2617 WasmBuildCacheMode::SharedIncremental { .. }
2618 )
2619 {
2620 return Err(WasmBuildError::InvalidSpec {
2621 message:
2622 "scheduled shared-target maintenance requires a shared incremental Cargo target"
2623 .to_owned(),
2624 });
2625 }
2626 Ok(())
2627}
2628
2629fn build_fingerprint(spec: &WasmBuildSpec) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2630 build_fingerprint_with_progress(spec, &mut ProgressReporter::silent())
2631}
2632
2633fn build_fingerprint_with_progress(
2634 spec: &WasmBuildSpec,
2635 progress: &mut ProgressReporter<'_>,
2636) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2637 let total_started = Instant::now();
2638 let tool_started = Instant::now();
2639 let cargo_identity = progress.run_phase(WasmBuildProgressPhase::CargoIdentity, || {
2640 command_identity(
2641 spec,
2642 WasmBuildPhase::CargoIdentity,
2643 &spec.cargo_program,
2644 &["--version", "--verbose"],
2645 )
2646 })?;
2647 let rustc_program = spec
2648 .extra_env
2649 .get(OsStr::new("RUSTC"))
2650 .unwrap_or(&spec.rustc_program);
2651 let rustc_identity = progress.run_phase(WasmBuildProgressPhase::RustcIdentity, || {
2652 command_identity(spec, WasmBuildPhase::RustcIdentity, rustc_program, &["-vV"])
2653 })?;
2654 let tool_identity = tool_started.elapsed();
2655
2656 let metadata_started = Instant::now();
2657 let metadata = progress.run_phase(WasmBuildProgressPhase::CargoMetadata, || {
2658 cargo_metadata(spec)
2659 })?;
2660 let cargo_metadata = metadata_started.elapsed();
2661
2662 let discovery_started = Instant::now();
2663 let (inputs, exclusions) =
2664 progress.run_phase(WasmBuildProgressPhase::InputDiscovery, || {
2665 let inputs = resolve_local_inputs(spec, &metadata)?;
2666 validate_shared_incremental_target_boundary(spec, &inputs)?;
2667 let exclusions = source_exclusions(spec, &inputs);
2668 Ok::<_, WasmBuildError>((inputs, exclusions))
2669 })?;
2670 let input_discovery = discovery_started.elapsed();
2671
2672 let hashing_started = Instant::now();
2673 let input_digest = progress.run_phase(WasmBuildProgressPhase::ContentHashing, || {
2674 digest_labeled_paths_composable(
2675 "wasm-source-inputs-v1",
2676 &inputs,
2677 &exclusions,
2678 &mut LabeledPathDigestCache::default(),
2679 )
2680 .map_err(|source| WasmBuildError::Io {
2681 operation: "hash Wasm build inputs",
2682 path: spec.workspace_root.clone(),
2683 source,
2684 })
2685 })?;
2686 let content_hashing = hashing_started.elapsed();
2687
2688 let fingerprint =
2689 finish_build_fingerprint(spec, &cargo_identity, &rustc_identity, input_digest);
2690 Ok(ResolvedCargoBuildInputs {
2691 fingerprint,
2692 input_digest,
2693 inputs: inputs
2694 .into_iter()
2695 .map(|(label, path)| CargoBuildInput { label, path })
2696 .collect(),
2697 exclusions,
2698 timings: WasmInputResolutionTimings {
2699 tool_identity,
2700 cargo_metadata,
2701 input_discovery,
2702 content_hashing,
2703 total: total_started.elapsed(),
2704 },
2705 })
2706}
2707
2708fn finish_build_fingerprint(
2709 spec: &WasmBuildSpec,
2710 cargo_identity: &[u8],
2711 rustc_identity: &[u8],
2712 input_digest: InputDigest,
2713) -> InputDigest {
2714 let mut hasher = InputHasher::new(CACHE_FORMAT_VERSION);
2715 let mut packages = spec.packages.clone();
2716 packages.sort();
2717 packages.dedup();
2718 for package in packages {
2719 hasher.field("package", package.as_bytes());
2720 }
2721 hasher.field("target", spec.target.as_bytes());
2722 hasher.field("profile-target-dir", spec.profile_target_dir.as_bytes());
2723 for argument in &spec.cargo_profile_args {
2724 hasher.field("cargo-argument", &os_bytes(argument));
2725 }
2726 for (key, value) in effective_environment(spec) {
2727 hasher.field("environment-key", &os_bytes(&key));
2728 if let Some(value) = value {
2729 hasher.field("environment-value", &os_bytes(&value));
2730 } else {
2731 hasher.field("environment-unset", b"");
2732 }
2733 }
2734 hasher.field("cargo-identity", cargo_identity);
2735 hasher.field("rustc-identity", rustc_identity);
2736 hasher.field("source-input-digest", input_digest.as_bytes());
2737 hasher.finish()
2738}
2739
2740fn command_identity(
2741 spec: &WasmBuildSpec,
2742 phase: WasmBuildPhase,
2743 program: &OsStr,
2744 arguments: &[&str],
2745) -> Result<Vec<u8>, WasmBuildError> {
2746 let mut command = Command::new(program);
2747 command.current_dir(&spec.workspace_root).args(arguments);
2748 apply_command_environment(&mut command, spec);
2749 let output = command
2750 .output()
2751 .map_err(|source| WasmBuildError::CommandSpawn {
2752 phase,
2753 program: program.to_owned(),
2754 source,
2755 })?;
2756 ensure_command_success(phase, output).map(|output| {
2757 let mut identity = output.stdout;
2758 identity.extend_from_slice(&output.stderr);
2759 identity
2760 })
2761}
2762
2763fn cargo_metadata(spec: &WasmBuildSpec) -> Result<Value, WasmBuildError> {
2764 let mut command = Command::new(&spec.cargo_program);
2765 command
2766 .current_dir(&spec.workspace_root)
2767 .args(["metadata", "--format-version", "1"]);
2768 for argument in metadata_arguments(&spec.cargo_profile_args) {
2769 command.arg(argument);
2770 }
2771 apply_command_environment(&mut command, spec);
2772 let output = command
2773 .output()
2774 .map_err(|source| WasmBuildError::CommandSpawn {
2775 phase: WasmBuildPhase::CargoMetadata,
2776 program: spec.cargo_program.clone(),
2777 source,
2778 })?;
2779 let output = ensure_command_success(WasmBuildPhase::CargoMetadata, output)?;
2780 serde_json::from_slice(&output.stdout).map_err(|error| WasmBuildError::InvalidMetadata {
2781 message: format!("Cargo metadata was not valid JSON: {error}"),
2782 })
2783}
2784
2785fn metadata_arguments(arguments: &[OsString]) -> Vec<OsString> {
2786 let mut selected = Vec::new();
2787 let mut arguments = arguments.iter();
2788 while let Some(argument) = arguments.next() {
2789 let argument_text = argument.to_string_lossy();
2790 match argument_text.as_ref() {
2791 "--all-features" | "--no-default-features" | "--locked" | "--offline" | "--frozen" => {
2792 selected.push(argument.clone());
2793 }
2794 "--features" | "-F" | "--filter-platform" => {
2795 selected.push(argument.clone());
2796 if let Some(value) = arguments.next() {
2797 selected.push(value.clone());
2798 }
2799 }
2800 _ if argument_text.starts_with("--features=")
2801 || argument_text.starts_with("--filter-platform=") =>
2802 {
2803 selected.push(argument.clone());
2804 }
2805 _ => {}
2806 }
2807 }
2808 selected
2809}
2810
2811#[derive(Clone)]
2812struct MetadataPackage {
2813 id: String,
2814 name: String,
2815 version: String,
2816 manifest_path: PathBuf,
2817 is_local: bool,
2818}
2819
2820fn resolve_local_inputs(
2821 spec: &WasmBuildSpec,
2822 metadata: &Value,
2823) -> Result<Vec<(PathBuf, PathBuf)>, WasmBuildError> {
2824 let packages = metadata_packages(metadata)?;
2825 let mut selected_ids = selected_package_ids(spec, metadata, &packages)?;
2826 let dependencies = metadata_dependencies(metadata)?;
2827 let mut closure = BTreeSet::new();
2828 while let Some(id) = selected_ids.pop_front() {
2829 if !closure.insert(id.clone()) {
2830 continue;
2831 }
2832 if let Some(deps) = dependencies.get(&id) {
2833 selected_ids.extend(deps.iter().cloned());
2834 }
2835 }
2836
2837 let workspace_root = metadata
2838 .get("workspace_root")
2839 .and_then(Value::as_str)
2840 .map_or_else(|| spec.workspace_root.clone(), PathBuf::from);
2841 let mut inputs = workspace_configuration_inputs(spec, &workspace_root)?;
2842 append_package_inputs(&mut inputs, &packages, closure, &workspace_root)?;
2843 append_additional_inputs(&mut inputs, spec, &workspace_root);
2844 Ok(inputs)
2845}
2846
2847fn metadata_packages(metadata: &Value) -> Result<HashMap<String, MetadataPackage>, WasmBuildError> {
2848 let packages_value = metadata
2849 .get("packages")
2850 .and_then(Value::as_array)
2851 .ok_or_else(|| invalid_metadata("Cargo metadata has no package array"))?;
2852 let mut packages = HashMap::new();
2853 for value in packages_value {
2854 let package = MetadataPackage {
2855 id: required_string(value, "id")?,
2856 name: required_string(value, "name")?,
2857 version: required_string(value, "version")?,
2858 manifest_path: PathBuf::from(required_string(value, "manifest_path")?),
2859 is_local: value.get("source").is_some_and(Value::is_null),
2860 };
2861 packages.insert(package.id.clone(), package);
2862 }
2863 Ok(packages)
2864}
2865
2866fn selected_package_ids(
2867 spec: &WasmBuildSpec,
2868 metadata: &Value,
2869 packages: &HashMap<String, MetadataPackage>,
2870) -> Result<VecDeque<String>, WasmBuildError> {
2871 let workspace_members = metadata
2872 .get("workspace_members")
2873 .and_then(Value::as_array)
2874 .ok_or_else(|| invalid_metadata("Cargo metadata has no workspace member array"))?
2875 .iter()
2876 .filter_map(Value::as_str)
2877 .collect::<HashSet<_>>();
2878 let mut selected_ids = VecDeque::new();
2879 for requested in &spec.packages {
2880 let matches = packages
2881 .values()
2882 .filter(|package| {
2883 package.name == *requested && workspace_members.contains(package.id.as_str())
2884 })
2885 .map(|package| package.id.clone())
2886 .collect::<Vec<_>>();
2887 match matches.as_slice() {
2888 [id] => selected_ids.push_back(id.clone()),
2889 [] => {
2890 return Err(WasmBuildError::InvalidSpec {
2891 message: format!("Cargo workspace contains no package named `{requested}`"),
2892 });
2893 }
2894 _ => {
2895 return Err(WasmBuildError::InvalidSpec {
2896 message: format!("Cargo workspace package name `{requested}` is ambiguous"),
2897 });
2898 }
2899 }
2900 }
2901 Ok(selected_ids)
2902}
2903
2904fn metadata_dependencies(metadata: &Value) -> Result<HashMap<String, Vec<String>>, WasmBuildError> {
2905 let mut dependencies = HashMap::<String, Vec<String>>::new();
2906 let nodes = metadata
2907 .pointer("/resolve/nodes")
2908 .and_then(Value::as_array)
2909 .ok_or_else(|| invalid_metadata("Cargo metadata has no resolved dependency nodes"))?;
2910 for node in nodes {
2911 let id = required_string(node, "id")?;
2912 let deps = node
2913 .get("deps")
2914 .and_then(Value::as_array)
2915 .ok_or_else(|| invalid_metadata("Cargo metadata dependency node has no deps array"))?
2916 .iter()
2917 .map(|dependency| required_string(dependency, "pkg"))
2918 .collect::<Result<Vec<_>, _>>()?;
2919 dependencies.insert(id, deps);
2920 }
2921 Ok(dependencies)
2922}
2923
2924fn workspace_configuration_inputs(
2925 spec: &WasmBuildSpec,
2926 workspace_root: &Path,
2927) -> Result<Vec<(PathBuf, PathBuf)>, WasmBuildError> {
2928 let mut inputs = Vec::new();
2929 add_if_present(
2930 &mut inputs,
2931 "workspace/Cargo.toml",
2932 workspace_root.join("Cargo.toml"),
2933 );
2934 add_if_present(
2935 &mut inputs,
2936 "workspace/Cargo.lock",
2937 workspace_root.join("Cargo.lock"),
2938 );
2939 add_if_present(
2940 &mut inputs,
2941 "workspace/rust-toolchain.toml",
2942 workspace_root.join("rust-toolchain.toml"),
2943 );
2944 add_if_present(
2945 &mut inputs,
2946 "workspace/rust-toolchain",
2947 workspace_root.join("rust-toolchain"),
2948 );
2949 append_cargo_configuration_inputs(&mut inputs, spec, workspace_root)?;
2950 Ok(inputs)
2951}
2952
2953fn append_cargo_configuration_inputs(
2954 inputs: &mut Vec<(PathBuf, PathBuf)>,
2955 spec: &WasmBuildSpec,
2956 workspace_root: &Path,
2957) -> Result<(), WasmBuildError> {
2958 let invocation_root =
2959 spec.workspace_root
2960 .canonicalize()
2961 .map_err(|source| WasmBuildError::Io {
2962 operation: "resolve Cargo invocation directory",
2963 path: spec.workspace_root.clone(),
2964 source,
2965 })?;
2966 let canonical_workspace =
2967 workspace_root
2968 .canonicalize()
2969 .map_err(|source| WasmBuildError::Io {
2970 operation: "resolve Cargo workspace directory",
2971 path: workspace_root.to_owned(),
2972 source,
2973 })?;
2974
2975 let mut roots = invocation_root
2976 .ancestors()
2977 .filter_map(|directory| effective_cargo_config(&directory.join(".cargo")))
2978 .collect::<Vec<_>>();
2979 if let Some(cargo_home) = effective_cargo_home(spec, &invocation_root)
2980 && let Some(config) = effective_cargo_config(&cargo_home)
2981 {
2982 roots.push(config);
2983 }
2984
2985 let mut visited = BTreeSet::new();
2986 for config in roots {
2987 append_cargo_configuration_tree(
2988 inputs,
2989 &config,
2990 &canonical_workspace,
2991 &mut visited,
2992 false,
2993 )?;
2994 }
2995 Ok(())
2996}
2997
2998fn effective_cargo_config(directory: &Path) -> Option<PathBuf> {
2999 let extensionless = directory.join("config");
3000 if extensionless.exists() {
3001 return Some(extensionless);
3002 }
3003 let toml = directory.join("config.toml");
3004 toml.exists().then_some(toml)
3005}
3006
3007fn effective_cargo_home(spec: &WasmBuildSpec, invocation_root: &Path) -> Option<PathBuf> {
3008 if let Some(cargo_home) = command_environment_value(spec, "CARGO_HOME") {
3009 let cargo_home = PathBuf::from(cargo_home);
3010 return Some(if cargo_home.is_absolute() {
3011 cargo_home
3012 } else {
3013 invocation_root.join(cargo_home)
3014 });
3015 }
3016
3017 default_home_directory(spec).map(|home| {
3018 let home = if home.is_absolute() {
3019 home
3020 } else {
3021 invocation_root.join(home)
3022 };
3023 home.join(".cargo")
3024 })
3025}
3026
3027#[cfg(windows)]
3028fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
3029 command_environment_value(spec, "USERPROFILE")
3030 .or_else(|| command_environment_value(spec, "HOME"))
3031 .map(PathBuf::from)
3032}
3033
3034#[cfg(not(windows))]
3035fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
3036 command_environment_value(spec, "HOME").map(PathBuf::from)
3037}
3038
3039fn command_environment_value(spec: &WasmBuildSpec, name: &str) -> Option<OsString> {
3040 spec.extra_env
3041 .get(OsStr::new(name))
3042 .cloned()
3043 .or_else(|| std::env::var_os(name))
3044}
3045
3046fn append_cargo_configuration_tree(
3047 inputs: &mut Vec<(PathBuf, PathBuf)>,
3048 config: &Path,
3049 workspace_root: &Path,
3050 visited: &mut BTreeSet<PathBuf>,
3051 optional: bool,
3052) -> Result<(), WasmBuildError> {
3053 let canonical = match config.canonicalize() {
3054 Ok(canonical) => canonical,
3055 Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
3056 Err(source) => {
3057 return Err(WasmBuildError::Io {
3058 operation: "resolve Cargo configuration",
3059 path: config.to_owned(),
3060 source,
3061 });
3062 }
3063 };
3064 if !visited.insert(canonical.clone()) {
3065 return Ok(());
3066 }
3067
3068 let contents = fs::read_to_string(&canonical).map_err(|source| WasmBuildError::Io {
3069 operation: "read Cargo configuration",
3070 path: canonical.clone(),
3071 source,
3072 })?;
3073 let configuration = toml::from_str::<TomlValue>(&contents).map_err(|error| {
3074 WasmBuildError::InvalidCargoConfiguration {
3075 path: canonical.clone(),
3076 message: error.to_string(),
3077 }
3078 })?;
3079 inputs.push((
3080 cargo_configuration_label(&canonical, workspace_root),
3081 canonical.clone(),
3082 ));
3083
3084 let Some(include) = configuration.get("include") else {
3085 return Ok(());
3086 };
3087 let parent = canonical
3088 .parent()
3089 .ok_or_else(|| WasmBuildError::InvalidCargoConfiguration {
3090 path: canonical.clone(),
3091 message: "configuration path has no parent directory".to_owned(),
3092 })?;
3093 for (included, optional) in cargo_configuration_includes(include, &canonical)? {
3094 let included = if included.is_absolute() {
3095 included
3096 } else {
3097 parent.join(included)
3098 };
3099 append_cargo_configuration_tree(inputs, &included, workspace_root, visited, optional)?;
3100 }
3101 Ok(())
3102}
3103
3104fn cargo_configuration_includes(
3105 include: &TomlValue,
3106 config: &Path,
3107) -> Result<Vec<(PathBuf, bool)>, WasmBuildError> {
3108 let values = match include {
3109 TomlValue::Array(values) => values.as_slice(),
3110 value => std::slice::from_ref(value),
3111 };
3112 values
3113 .iter()
3114 .map(|value| match value {
3115 TomlValue::String(path) => Ok((PathBuf::from(path), false)),
3116 TomlValue::Table(table) => {
3117 let path = table
3118 .get("path")
3119 .and_then(TomlValue::as_str)
3120 .ok_or_else(|| {
3121 invalid_cargo_configuration(
3122 config,
3123 "Cargo configuration include table requires a string `path`",
3124 )
3125 })?;
3126 let optional = table
3127 .get("optional")
3128 .map(|value| {
3129 value.as_bool().ok_or_else(|| {
3130 invalid_cargo_configuration(
3131 config,
3132 "Cargo configuration include `optional` must be a boolean",
3133 )
3134 })
3135 })
3136 .transpose()?
3137 .unwrap_or(false);
3138 Ok((PathBuf::from(path), optional))
3139 }
3140 _ => Err(invalid_cargo_configuration(
3141 config,
3142 "Cargo configuration `include` must contain paths or include tables",
3143 )),
3144 })
3145 .collect()
3146}
3147
3148fn cargo_configuration_label(config: &Path, workspace_root: &Path) -> PathBuf {
3149 if let Ok(relative) = config.strip_prefix(workspace_root) {
3150 return PathBuf::from("cargo-config/workspace").join(relative);
3151 }
3152 let location = digest_bytes("cargo-config-location-v1", &os_bytes(config.as_os_str()));
3153 PathBuf::from("cargo-config/external").join(location.to_hex())
3154}
3155
3156fn invalid_cargo_configuration(path: &Path, message: &str) -> WasmBuildError {
3157 WasmBuildError::InvalidCargoConfiguration {
3158 path: path.to_owned(),
3159 message: message.to_owned(),
3160 }
3161}
3162
3163fn append_package_inputs(
3164 inputs: &mut Vec<(PathBuf, PathBuf)>,
3165 packages: &HashMap<String, MetadataPackage>,
3166 closure: BTreeSet<String>,
3167 workspace_root: &Path,
3168) -> Result<(), WasmBuildError> {
3169 for id in closure {
3170 let Some(package) = packages.get(&id) else {
3171 return Err(invalid_metadata(&format!(
3172 "resolved package `{id}` is missing"
3173 )));
3174 };
3175 if !package.is_local {
3176 continue;
3177 }
3178 let root = package.manifest_path.parent().ok_or_else(|| {
3179 invalid_metadata(&format!(
3180 "package `{}` manifest has no parent",
3181 package.name
3182 ))
3183 })?;
3184 let relative_manifest = package
3185 .manifest_path
3186 .strip_prefix(workspace_root)
3187 .unwrap_or(&package.manifest_path);
3188 let label = PathBuf::from(format!("package/{}@{}", package.name, package.version))
3189 .join(relative_manifest.parent().unwrap_or_else(|| Path::new(".")));
3190 inputs.push((label, root.to_owned()));
3191 }
3192 Ok(())
3193}
3194
3195fn append_additional_inputs(
3196 inputs: &mut Vec<(PathBuf, PathBuf)>,
3197 spec: &WasmBuildSpec,
3198 workspace_root: &Path,
3199) {
3200 for additional in &spec.additional_inputs {
3201 let path = if additional.is_absolute() {
3202 additional.clone()
3203 } else {
3204 workspace_root.join(additional)
3205 };
3206 inputs.push((PathBuf::from("additional").join(additional), path));
3207 }
3208}
3209
3210fn source_exclusions(spec: &WasmBuildSpec, inputs: &[(PathBuf, PathBuf)]) -> Vec<PathBuf> {
3211 let mut exclusions = vec![
3212 spec.target_dir.clone(),
3213 spec.workspace_root.join("target"),
3214 spec.workspace_root.join(".git"),
3215 ];
3216 if let Some(shared_target) = shared_incremental_target(spec) {
3217 exclusions.push(shared_target);
3218 }
3219 for (_, path) in inputs {
3220 if path.is_dir() {
3221 exclusions.push(path.join("target"));
3222 exclusions.push(path.join(".git"));
3223 }
3224 }
3225 exclusions
3226}
3227
3228fn validate_shared_incremental_target_boundary(
3229 spec: &WasmBuildSpec,
3230 inputs: &[(PathBuf, PathBuf)],
3231) -> Result<(), WasmBuildError> {
3232 let Some(shared_target) = shared_incremental_target(spec) else {
3233 return Ok(());
3234 };
3235 let shared_target =
3236 canonicalize_allow_missing(&shared_target).map_err(|source| WasmBuildError::Io {
3237 operation: "resolve shared incremental Cargo target boundary",
3238 path: shared_target.clone(),
3239 source,
3240 })?;
3241 let resolved_inputs = inputs
3242 .iter()
3243 .map(|(_, input)| {
3244 let canonical = input.canonicalize().map_err(|source| WasmBuildError::Io {
3245 operation: "resolve Cargo input boundary",
3246 path: input.clone(),
3247 source,
3248 })?;
3249 let metadata = fs::metadata(&canonical).map_err(|source| WasmBuildError::Io {
3250 operation: "inspect Cargo input boundary",
3251 path: canonical.clone(),
3252 source,
3253 })?;
3254 Ok((canonical, metadata.is_dir()))
3255 })
3256 .collect::<Result<Vec<_>, WasmBuildError>>()?;
3257 let safe_generated_roots = std::iter::once(spec.target_dir.clone())
3258 .chain(std::iter::once(spec.workspace_root.join("target")))
3259 .chain(
3260 inputs
3261 .iter()
3262 .filter(|(_, path)| path.is_dir())
3263 .map(|(_, path)| path.join("target")),
3264 )
3265 .filter_map(|path| canonicalize_allow_missing(&path).ok())
3266 .filter(|root| {
3267 !resolved_inputs
3268 .iter()
3269 .any(|(input, _is_directory)| input.starts_with(root))
3270 })
3271 .collect::<Vec<_>>();
3272 if safe_generated_roots
3273 .iter()
3274 .any(|root| shared_target.starts_with(root))
3275 {
3276 return Ok(());
3277 }
3278
3279 for (input, is_directory) in resolved_inputs {
3280 if shared_target == input
3281 || (is_directory && shared_target.starts_with(&input))
3282 || input.starts_with(&shared_target)
3283 {
3284 return Err(WasmBuildError::InvalidSpec {
3285 message: format!(
3286 "shared incremental target {} must not overlap exact Cargo inputs unless it is inside a generated target directory",
3287 shared_target.display()
3288 ),
3289 });
3290 }
3291 }
3292 Ok(())
3293}
3294
3295fn canonicalize_allow_missing(path: &Path) -> io::Result<PathBuf> {
3296 let absolute = if path.is_absolute() {
3297 path.to_owned()
3298 } else {
3299 std::env::current_dir()?.join(path)
3300 };
3301 let mut unresolved = Vec::<OsString>::new();
3302 let mut existing = absolute.as_path();
3303 loop {
3304 match existing.canonicalize() {
3305 Ok(mut canonical) => {
3306 for component in unresolved.into_iter().rev() {
3307 canonical.push(component);
3308 }
3309 return Ok(canonical);
3310 }
3311 Err(error) if error.kind() == io::ErrorKind::NotFound => {
3312 let Some(name) = existing.file_name() else {
3313 return Err(error);
3314 };
3315 unresolved.push(name.to_owned());
3316 existing = existing.parent().ok_or(error)?;
3317 }
3318 Err(error) => return Err(error),
3319 }
3320 }
3321}
3322
3323fn shared_incremental_target(spec: &WasmBuildSpec) -> Option<PathBuf> {
3324 let WasmBuildCacheMode::SharedIncremental { target_dir } = &spec.cache_mode else {
3325 return None;
3326 };
3327 Some(if target_dir.is_absolute() {
3328 target_dir.clone()
3329 } else {
3330 spec.workspace_root.join(target_dir)
3331 })
3332}
3333
3334fn shared_incremental_target_exists(
3335 spec: &WasmBuildSpec,
3336 operation: &'static str,
3337) -> Result<bool, WasmBuildError> {
3338 let target_dir =
3339 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
3340 message: "shared incremental target is not configured".to_owned(),
3341 })?;
3342 match fs::symlink_metadata(&target_dir) {
3343 Ok(metadata) if metadata.is_dir() => Ok(true),
3344 Ok(_) => Err(WasmBuildError::InvalidSpec {
3345 message: format!(
3346 "shared incremental Cargo target {} must be a directory",
3347 target_dir.display()
3348 ),
3349 }),
3350 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
3351 Err(source) => Err(WasmBuildError::Io {
3352 operation,
3353 path: target_dir,
3354 source,
3355 }),
3356 }
3357}
3358
3359fn effective_environment(spec: &WasmBuildSpec) -> BTreeMap<OsString, Option<OsString>> {
3360 let mut names = spec.inherited_env.clone();
3361 names.extend(AUTOMATIC_ENVIRONMENT.iter().map(OsString::from));
3362 let mut environment = names
3363 .into_iter()
3364 .map(|name| {
3365 let value = std::env::var_os(&name);
3366 (name, value)
3367 })
3368 .collect::<BTreeMap<_, _>>();
3369 for (key, value) in &spec.extra_env {
3370 environment.insert(key.clone(), Some(value.clone()));
3371 }
3372 environment
3373}
3374
3375fn apply_command_environment(command: &mut Command, spec: &WasmBuildSpec) {
3376 for (key, value) in &spec.extra_env {
3377 command.env(key, value);
3378 }
3379}
3380
3381fn run_cargo_build(
3382 spec: &WasmBuildSpec,
3383 build_target_dir: &Path,
3384 progress: &mut ProgressReporter<'_>,
3385) -> Result<(), WasmBuildError> {
3386 let mut command = Command::new(&spec.cargo_program);
3387 command
3388 .current_dir(&spec.workspace_root)
3389 .env("CARGO_TARGET_DIR", build_target_dir)
3390 .args(["build", "--target", &spec.target])
3391 .args(&spec.cargo_profile_args);
3392 apply_command_environment(&mut command, spec);
3393 for package in &spec.packages {
3394 command.args(["-p", package]);
3395 }
3396
3397 if !progress.is_observed() {
3398 let output = command
3399 .output()
3400 .map_err(|source| WasmBuildError::CommandSpawn {
3401 phase: WasmBuildPhase::CargoBuild,
3402 program: spec.cargo_program.clone(),
3403 source,
3404 })?;
3405 return ensure_command_success(WasmBuildPhase::CargoBuild, output).map(|_| ());
3406 }
3407
3408 run_observed_cargo_build(spec, build_target_dir, command, progress)
3409}
3410
3411fn run_observed_cargo_build(
3412 spec: &WasmBuildSpec,
3413 build_target_dir: &Path,
3414 mut command: Command,
3415 progress: &mut ProgressReporter<'_>,
3416) -> Result<(), WasmBuildError> {
3417 command.stdout(Stdio::piped()).stderr(Stdio::piped());
3418 let started = Instant::now();
3419 let child = command
3420 .spawn()
3421 .map_err(|source| WasmBuildError::CommandSpawn {
3422 phase: WasmBuildPhase::CargoBuild,
3423 program: spec.cargo_program.clone(),
3424 source,
3425 })?;
3426 let mut child = ObservedChild::new(child);
3427 progress.emit(WasmBuildProgressEvent::CargoStarted {
3428 target_dir: build_target_dir.to_owned(),
3429 });
3430
3431 let stdout = child
3432 .child_mut()
3433 .stdout
3434 .take()
3435 .expect("Cargo stdout must be piped");
3436 let stderr = child
3437 .child_mut()
3438 .stderr
3439 .take()
3440 .expect("Cargo stderr must be piped");
3441 let (sender, chunks) = mpsc::channel();
3442 let stdout_sender = sender.clone();
3443 let stdout_reader = thread::spawn(move || {
3444 read_process_output(stdout, WasmBuildOutputStream::Stdout, stdout_sender)
3445 });
3446 let stderr_reader =
3447 thread::spawn(move || read_process_output(stderr, WasmBuildOutputStream::Stderr, sender));
3448
3449 let captured = capture_observed_cargo_output(chunks, progress, started);
3450
3451 let status = child.wait().map_err(|source| WasmBuildError::Io {
3452 operation: "wait for observed cargo build",
3453 path: PathBuf::from(&spec.cargo_program),
3454 source,
3455 })?;
3456 join_output_reader(
3457 stdout_reader,
3458 "read observed cargo stdout",
3459 &spec.cargo_program,
3460 )?;
3461 join_output_reader(
3462 stderr_reader,
3463 "read observed cargo stderr",
3464 &spec.cargo_program,
3465 )?;
3466 let elapsed = started.elapsed();
3467 progress.emit(WasmBuildProgressEvent::CargoFinished {
3468 success: status.success(),
3469 code: status.code(),
3470 elapsed,
3471 });
3472
3473 ensure_command_success(
3474 WasmBuildPhase::CargoBuild,
3475 Output {
3476 status,
3477 stdout: captured.stdout,
3478 stderr: captured.stderr,
3479 },
3480 )
3481 .map(|_| ())
3482}
3483
3484struct CapturedProcessOutput {
3485 stdout: Vec<u8>,
3486 stderr: Vec<u8>,
3487}
3488
3489fn capture_observed_cargo_output(
3490 chunks: mpsc::Receiver<ProcessOutputChunk>,
3491 progress: &mut ProgressReporter<'_>,
3492 started: Instant,
3493) -> CapturedProcessOutput {
3494 let mut stdout = Vec::new();
3495 let mut stderr = Vec::new();
3496 loop {
3497 let message = match progress.heartbeat_due_in() {
3498 Some(wait) => match chunks.recv_timeout(wait) {
3499 Ok(chunk) => Some(chunk),
3500 Err(RecvTimeoutError::Timeout) => {
3501 progress.emit_heartbeat(WasmBuildProgressPhase::CargoBuild, started.elapsed());
3502 None
3503 }
3504 Err(RecvTimeoutError::Disconnected) => break,
3505 },
3506 None => match chunks.recv() {
3507 Ok(chunk) => Some(chunk),
3508 Err(_) => break,
3509 },
3510 };
3511 let Some(chunk) = message else {
3512 continue;
3513 };
3514 match chunk.stream {
3515 WasmBuildOutputStream::Stdout => stdout.extend_from_slice(&chunk.bytes),
3516 WasmBuildOutputStream::Stderr => stderr.extend_from_slice(&chunk.bytes),
3517 }
3518 if progress.config.emit_cargo_output {
3519 progress.emit(WasmBuildProgressEvent::CargoOutput {
3520 stream: chunk.stream,
3521 bytes: chunk.bytes,
3522 });
3523 }
3524 }
3525 CapturedProcessOutput { stdout, stderr }
3526}
3527
3528#[derive(Debug)]
3529struct ProcessOutputChunk {
3530 stream: WasmBuildOutputStream,
3531 bytes: Vec<u8>,
3532}
3533
3534fn read_process_output<R: io::Read>(
3535 mut reader: R,
3536 stream: WasmBuildOutputStream,
3537 sender: mpsc::Sender<ProcessOutputChunk>,
3538) -> io::Result<()> {
3539 let mut buffer = [0_u8; 8 * 1024];
3540 loop {
3541 let count = reader.read(&mut buffer)?;
3542 if count == 0 {
3543 return Ok(());
3544 }
3545 if sender
3546 .send(ProcessOutputChunk {
3547 stream,
3548 bytes: buffer[..count].to_vec(),
3549 })
3550 .is_err()
3551 {
3552 return Ok(());
3553 }
3554 }
3555}
3556
3557fn join_output_reader(
3558 reader: thread::JoinHandle<io::Result<()>>,
3559 operation: &'static str,
3560 cargo_program: &OsStr,
3561) -> Result<(), WasmBuildError> {
3562 let result = reader.join().map_err(|_| WasmBuildError::Io {
3563 operation,
3564 path: PathBuf::from(cargo_program),
3565 source: io::Error::other("Cargo output reader panicked"),
3566 })?;
3567 result.map_err(|source| WasmBuildError::Io {
3568 operation,
3569 path: PathBuf::from(cargo_program),
3570 source,
3571 })
3572}
3573
3574struct ObservedChild(Option<Child>);
3575
3576impl ObservedChild {
3577 const fn new(child: Child) -> Self {
3578 Self(Some(child))
3579 }
3580
3581 const fn child_mut(&mut self) -> &mut Child {
3582 self.0.as_mut().expect("observed child must be present")
3583 }
3584
3585 fn wait(&mut self) -> io::Result<ExitStatus> {
3586 let status = self.child_mut().wait()?;
3587 self.0.take();
3588 Ok(status)
3589 }
3590}
3591
3592impl Drop for ObservedChild {
3593 fn drop(&mut self) {
3594 if let Some(mut child) = self.0.take() {
3595 let _ = child.kill();
3596 let _ = child.wait();
3597 }
3598 }
3599}
3600
3601fn ensure_command_success(phase: WasmBuildPhase, output: Output) -> Result<Output, WasmBuildError> {
3602 if output.status.success() {
3603 return Ok(output);
3604 }
3605 Err(WasmBuildError::CommandFailed {
3606 phase,
3607 status: output.status,
3608 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
3609 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
3610 })
3611}
3612
3613fn expected_artifacts(spec: &WasmBuildSpec, target_dir: &Path) -> Vec<PathBuf> {
3614 let mut packages = spec.packages.iter().map(String::as_str).collect::<Vec<_>>();
3615 packages.sort_unstable();
3616 packages.dedup();
3617 packages
3618 .into_iter()
3619 .map(|package| {
3620 if spec.target == DEFAULT_TARGET {
3621 wasm_path(target_dir, package, &spec.profile_target_dir)
3622 } else {
3623 target_dir
3624 .join(&spec.target)
3625 .join(&spec.profile_target_dir)
3626 .join(format!("{package}.wasm"))
3627 }
3628 })
3629 .collect()
3630}
3631
3632fn cache_entry_directory(spec: &WasmBuildSpec, fingerprint: InputDigest) -> PathBuf {
3633 spec.target_dir
3634 .join(".ic-testkit/wasm-targets")
3635 .join(fingerprint.to_hex())
3636}
3637
3638fn artifact_set_matches(artifacts: &[PathBuf], fingerprint: InputDigest) -> bool {
3639 artifacts.iter().all(|path| {
3640 fs::metadata(path).is_ok_and(|metadata| metadata.is_file() && metadata.len() > 0)
3641 && cache_stamp_matches(path, fingerprint)
3642 })
3643}
3644
3645fn missing_artifacts(artifacts: &[PathBuf]) -> Vec<PathBuf> {
3646 artifacts
3647 .iter()
3648 .filter(|path| {
3649 fs::metadata(path).map_or(true, |metadata| !metadata.is_file() || metadata.len() == 0)
3650 })
3651 .cloned()
3652 .collect()
3653}
3654
3655fn cache_stamp_matches(artifact: &Path, fingerprint: InputDigest) -> bool {
3656 let stamp_path = artifact_stamp_path(artifact);
3657 let Ok(expected) = artifact_stamp_contents(artifact, fingerprint) else {
3658 return false;
3659 };
3660 fs::read_to_string(stamp_path).is_ok_and(|stamp| stamp == expected)
3661}
3662
3663fn artifact_stamp_path(artifact: &Path) -> PathBuf {
3664 let mut name = artifact
3665 .file_name()
3666 .map_or_else(|| OsString::from("artifact"), OsString::from);
3667 name.push(".ic-testkit-build");
3668 artifact.with_file_name(name)
3669}
3670
3671fn artifact_stamp_contents(artifact: &Path, fingerprint: InputDigest) -> io::Result<String> {
3672 let (_, artifact_digest) = digest_file("wasm-artifact-v1", artifact)?;
3673 Ok(format!(
3674 "{CACHE_FORMAT_VERSION}\nbuild-sha256:{fingerprint}\nartifact-sha256:{artifact_digest}\n"
3675 ))
3676}
3677
3678fn publish_artifact_stamps(
3679 artifacts: &[PathBuf],
3680 fingerprint: InputDigest,
3681) -> Result<(), WasmBuildError> {
3682 for artifact in artifacts {
3683 let stamp_path = artifact_stamp_path(artifact);
3684 let stamp = artifact_stamp_contents(artifact, fingerprint).map_err(|source| {
3685 WasmBuildError::Io {
3686 operation: "hash built Wasm artifact",
3687 path: artifact.clone(),
3688 source,
3689 }
3690 })?;
3691 write_atomic(&stamp_path, stamp.as_bytes()).map_err(|source| WasmBuildError::Io {
3692 operation: "publish Wasm build stamp",
3693 path: stamp_path,
3694 source,
3695 })?;
3696 }
3697 Ok(())
3698}
3699
3700fn materialize_artifacts(
3701 cached_artifacts: &[PathBuf],
3702 artifacts: &[PathBuf],
3703 fingerprint: InputDigest,
3704) -> Result<(), WasmBuildError> {
3705 for (cached, artifact) in cached_artifacts.iter().zip(artifacts) {
3706 copy_file_atomic(cached, artifact).map_err(|source| WasmBuildError::Io {
3707 operation: "publish Wasm artifact",
3708 path: artifact.clone(),
3709 source,
3710 })?;
3711 }
3712 publish_artifact_stamps(artifacts, fingerprint)
3713}
3714
3715fn copy_wasm_artifacts(
3716 source_artifacts: &[PathBuf],
3717 cached_artifacts: &[PathBuf],
3718) -> Result<(), WasmBuildError> {
3719 for (source, cached) in source_artifacts.iter().zip(cached_artifacts) {
3720 copy_file_atomic(source, cached).map_err(|source_error| WasmBuildError::Io {
3721 operation: "cache shared-incremental Wasm artifact",
3722 path: cached.clone(),
3723 source: source_error,
3724 })?;
3725 }
3726 Ok(())
3727}
3728
3729fn remove_directory_if_present(path: &Path) -> Result<(), WasmBuildError> {
3730 remove_path_if_present(path).map_err(|source| WasmBuildError::Io {
3731 operation: "remove incomplete content-addressed Cargo target directory",
3732 path: path.to_owned(),
3733 source,
3734 })
3735}
3736
3737fn create_dir_all(path: &Path, operation: &'static str) -> Result<(), WasmBuildError> {
3738 fs::create_dir_all(path).map_err(|source| WasmBuildError::Io {
3739 operation,
3740 path: path.to_owned(),
3741 source,
3742 })
3743}
3744
3745fn add_if_present(inputs: &mut Vec<(PathBuf, PathBuf)>, label: &str, path: PathBuf) {
3746 if path.exists() {
3747 inputs.push((PathBuf::from(label), path));
3748 }
3749}
3750
3751fn required_string(value: &Value, field: &str) -> Result<String, WasmBuildError> {
3752 value
3753 .get(field)
3754 .and_then(Value::as_str)
3755 .map(str::to_owned)
3756 .ok_or_else(|| invalid_metadata(&format!("Cargo metadata field `{field}` is missing")))
3757}
3758
3759fn invalid_metadata(message: &str) -> WasmBuildError {
3760 WasmBuildError::InvalidMetadata {
3761 message: message.to_owned(),
3762 }
3763}
3764
3765impl std::fmt::Display for WasmBuildPhase {
3766 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3767 formatter.write_str(match self {
3768 Self::CargoMetadata => "cargo metadata",
3769 Self::CargoIdentity => "Cargo identity",
3770 Self::RustcIdentity => "Rust compiler identity",
3771 Self::CargoBuild => "cargo build",
3772 })
3773 }
3774}
3775
3776impl std::fmt::Display for WasmBuildProgressPhase {
3777 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3778 formatter.write_str(match self {
3779 Self::ExactCacheLock => "exact cache lock",
3780 Self::CargoIdentity => "Cargo identity",
3781 Self::RustcIdentity => "Rust compiler identity",
3782 Self::CargoMetadata => "Cargo metadata",
3783 Self::InputDiscovery => "input discovery",
3784 Self::ContentHashing => "content hashing",
3785 Self::SharedTargetLock => "shared target lock",
3786 Self::SharedTargetMaintenance => "shared target maintenance",
3787 Self::CargoBuild => "Cargo build",
3788 Self::ArtifactPublication => "artifact publication",
3789 Self::ExactCacheMaintenance => "exact cache maintenance",
3790 })
3791 }
3792}
3793
3794impl std::fmt::Display for WasmBuildError {
3795 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3796 match self {
3797 Self::InvalidSpec { message } => {
3798 write!(formatter, "invalid Wasm build spec: {message}")
3799 }
3800 Self::Io {
3801 operation,
3802 path,
3803 source,
3804 } => write!(
3805 formatter,
3806 "failed to {operation} at {}: {source}",
3807 path.display()
3808 ),
3809 Self::CommandSpawn {
3810 phase,
3811 program,
3812 source,
3813 } => write!(
3814 formatter,
3815 "failed to launch {phase} using `{}`: {source}",
3816 program.to_string_lossy(),
3817 ),
3818 Self::CommandFailed {
3819 phase,
3820 status,
3821 stdout,
3822 stderr,
3823 } => write!(
3824 formatter,
3825 "{phase} failed with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}",
3826 ),
3827 Self::InvalidMetadata { message } => {
3828 write!(formatter, "invalid Cargo metadata: {message}")
3829 }
3830 Self::InvalidCargoConfiguration { path, message } => write!(
3831 formatter,
3832 "invalid Cargo configuration at {}: {message}",
3833 path.display(),
3834 ),
3835 Self::MissingArtifacts { paths } => write!(
3836 formatter,
3837 "cargo build succeeded without producing: {}",
3838 paths
3839 .iter()
3840 .map(|path| path.display().to_string())
3841 .collect::<Vec<_>>()
3842 .join(", "),
3843 ),
3844 Self::InputsChangedDuringBuild { before, after } => write!(
3845 formatter,
3846 "Wasm build inputs changed while Cargo was running: {before} -> {after}",
3847 ),
3848 Self::FailedBuildCleanup {
3849 build_error,
3850 path,
3851 source,
3852 } => write!(
3853 formatter,
3854 "Wasm build failed ({build_error}) and its incomplete target directory at {} could not be removed: {source}",
3855 path.display(),
3856 ),
3857 }
3858 }
3859}
3860
3861impl std::error::Error for WasmBuildError {
3862 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3863 match self {
3864 Self::Io { source, .. }
3865 | Self::CommandSpawn { source, .. }
3866 | Self::FailedBuildCleanup { source, .. } => Some(source),
3867 _ => None,
3868 }
3869 }
3870}
3871
3872#[cfg(test)]
3873mod tests;