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