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