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 perform_scheduled_cache_maintenance, prune_direct_child_directories,
21 record_cache_entry_use as record_entry_use, record_cache_maintenance,
22 remove_path_if_present,
23 },
24 digest::{
25 InputDigest, InputHasher, copy_file_atomic, digest_bytes, digest_file,
26 digest_labeled_paths, 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<WasmBuildCachePrunePolicy>,
64 prune_interval: Option<Duration>,
65 shared_incremental_maintenance_config: Option<SharedIncrementalTargetMaintenanceConfig>,
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69struct SharedIncrementalTargetMaintenanceConfig {
70 policy: SharedIncrementalTargetPrunePolicy,
71 minimum_interval: Duration,
72}
73
74#[non_exhaustive]
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub enum WasmBuildCacheMode {
78 Isolated,
80 SharedIncremental {
82 target_dir: PathBuf,
84 },
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
89pub enum WasmBuildOutcome {
90 Built(WasmBuildRecord),
92 Reused(WasmBuildRecord),
94}
95
96#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct WasmBuildRecord {
99 fingerprint: InputDigest,
100 input_digest: InputDigest,
101 artifacts: Vec<PathBuf>,
102 timings: WasmBuildTimings,
103 maintenance: Option<WasmBuildCacheMaintenance>,
104 shared_incremental_maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
105}
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct WasmBuildTimings {
110 lock_wait: Duration,
111 shared_incremental_lock_wait: Option<Duration>,
112 input_resolution: WasmInputResolutionTimings,
113 cargo_build: Option<Duration>,
114 cache_maintenance: Option<Duration>,
115 total: Duration,
116}
117
118#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
120pub struct WasmInputResolutionTimings {
121 tool_identity: Duration,
122 cargo_metadata: Duration,
123 input_discovery: Duration,
124 content_hashing: Duration,
125 total: Duration,
126}
127
128#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct CargoBuildInput {
131 label: PathBuf,
132 path: PathBuf,
133}
134
135#[derive(Clone, Debug, Eq, PartialEq)]
140pub struct ResolvedCargoBuildInputs {
141 fingerprint: InputDigest,
142 input_digest: InputDigest,
143 inputs: Vec<CargoBuildInput>,
144 exclusions: Vec<PathBuf>,
145 timings: WasmInputResolutionTimings,
146}
147
148#[derive(Clone, Debug, Eq, PartialEq)]
150pub struct SharedIncrementalTargetInspection {
151 target_dir: PathBuf,
152 logical_size_bytes: u64,
153 last_used: SystemTime,
154 lock_wait: Duration,
155}
156
157#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
165pub struct SharedIncrementalTargetPrunePolicy {
166 max_age: Option<Duration>,
167 max_size_bytes: Option<u64>,
168}
169
170#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct SharedIncrementalTargetMaintenance {
173 target_dir: PathBuf,
174 logical_size_bytes_before: u64,
175 logical_size_bytes_after: u64,
176 last_used_before: SystemTime,
177 cleared: bool,
178 lock_wait: Duration,
179 maintenance: Duration,
180}
181
182#[non_exhaustive]
184#[derive(Clone, Debug, Eq, PartialEq)]
185pub enum SharedIncrementalTargetMaintenanceOutcome {
186 Missing {
188 target_dir: PathBuf,
190 },
191 Skipped {
193 target_dir: PathBuf,
195 lock_wait: Duration,
197 schedule_check: Duration,
199 },
200 Performed {
202 maintenance: SharedIncrementalTargetMaintenance,
204 schedule_check: Duration,
206 },
207}
208
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub struct WasmBuildProgressConfig {
212 heartbeat_interval: Option<Duration>,
213 emit_cargo_output: bool,
214}
215
216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218pub enum WasmBuildOutputStream {
219 Stdout,
221 Stderr,
223}
224
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub enum WasmBuildProgressOutcome {
228 Built,
230 Reused,
232}
233
234#[non_exhaustive]
236#[derive(Clone, Debug, Eq, PartialEq)]
237pub enum WasmBuildProgressEvent {
238 Started,
240 InputsResolved {
242 fingerprint: InputDigest,
244 input_digest: InputDigest,
246 elapsed: Duration,
248 },
249 CacheMiss {
251 fingerprint: InputDigest,
253 },
254 CacheHit {
256 fingerprint: InputDigest,
258 },
259 SharedTargetLockStarted {
261 target_dir: PathBuf,
263 },
264 SharedTargetLockAcquired {
266 target_dir: PathBuf,
268 wait: Duration,
270 },
271 SharedTargetMaintenanceStarted {
273 target_dir: PathBuf,
275 },
276 SharedTargetMaintenanceFinished {
278 outcome: SharedIncrementalTargetMaintenanceOutcome,
280 },
281 CargoStarted {
283 target_dir: PathBuf,
285 },
286 CargoOutput {
288 stream: WasmBuildOutputStream,
290 bytes: Vec<u8>,
292 },
293 CargoHeartbeat {
295 elapsed: Duration,
297 },
298 CargoFinished {
300 success: bool,
302 code: Option<i32>,
304 elapsed: Duration,
306 },
307 Finished {
309 outcome: WasmBuildProgressOutcome,
311 fingerprint: InputDigest,
313 elapsed: Duration,
315 },
316}
317
318impl Default for WasmBuildProgressConfig {
319 fn default() -> Self {
320 Self {
321 heartbeat_interval: Some(Duration::from_secs(10)),
322 emit_cargo_output: true,
323 }
324 }
325}
326
327impl WasmBuildProgressConfig {
328 #[must_use]
330 pub fn new() -> Self {
331 Self::default()
332 }
333
334 #[must_use]
338 pub const fn with_heartbeat_interval(mut self, interval: Duration) -> Self {
339 self.heartbeat_interval = Some(interval);
340 self
341 }
342
343 #[must_use]
345 pub const fn without_heartbeats(mut self) -> Self {
346 self.heartbeat_interval = None;
347 self
348 }
349
350 #[must_use]
354 pub const fn with_cargo_output(mut self, emit: bool) -> Self {
355 self.emit_cargo_output = emit;
356 self
357 }
358
359 #[must_use]
361 pub const fn heartbeat_interval(self) -> Option<Duration> {
362 self.heartbeat_interval
363 }
364
365 #[must_use]
367 pub const fn emits_cargo_output(self) -> bool {
368 self.emit_cargo_output
369 }
370}
371
372struct ProgressReporter<'a> {
373 config: WasmBuildProgressConfig,
374 observer: Option<&'a mut dyn FnMut(WasmBuildProgressEvent)>,
375}
376
377impl ProgressReporter<'_> {
378 fn silent() -> Self {
379 Self {
380 config: WasmBuildProgressConfig {
381 heartbeat_interval: None,
382 emit_cargo_output: false,
383 },
384 observer: None,
385 }
386 }
387
388 fn emit(&mut self, event: WasmBuildProgressEvent) {
389 if let Some(observer) = &mut self.observer {
390 observer(event);
391 }
392 }
393
394 const fn is_observed(&self) -> bool {
395 self.observer.is_some()
396 }
397}
398
399pub type WasmBuildCachePrunePolicy = ArtifactCachePrunePolicy;
401
402pub type WasmBuildCachePruneReport = ArtifactCachePruneReport;
404
405pub type WasmBuildCacheMaintenance = ArtifactCacheMaintenance;
407
408#[non_exhaustive]
410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
411pub enum WasmBuildPhase {
412 CargoMetadata,
414 CargoIdentity,
416 RustcIdentity,
418 CargoBuild,
420}
421
422#[non_exhaustive]
424#[derive(Debug)]
425pub enum WasmBuildError {
426 InvalidSpec { message: String },
428 Io {
430 operation: &'static str,
431 path: PathBuf,
432 source: io::Error,
433 },
434 CommandSpawn {
436 phase: WasmBuildPhase,
437 program: OsString,
438 source: io::Error,
439 },
440 CommandFailed {
442 phase: WasmBuildPhase,
443 status: ExitStatus,
444 stdout: String,
445 stderr: String,
446 },
447 InvalidMetadata { message: String },
449 InvalidCargoConfiguration { path: PathBuf, message: String },
451 MissingArtifacts { paths: Vec<PathBuf> },
453 InputsChangedDuringBuild {
455 before: InputDigest,
456 after: InputDigest,
457 },
458 FailedBuildCleanup {
460 build_error: Box<Self>,
461 path: PathBuf,
462 source: io::Error,
463 },
464}
465
466impl WasmBuildSpec {
467 #[must_use]
472 pub fn new(
473 workspace_root: &Path,
474 target_dir: &Path,
475 packages: &[&str],
476 profile_target_dir: &str,
477 ) -> Self {
478 Self {
479 workspace_root: workspace_root.to_owned(),
480 target_dir: target_dir.to_owned(),
481 packages: packages
482 .iter()
483 .map(|package| (*package).to_owned())
484 .collect(),
485 profile_target_dir: profile_target_dir.to_owned(),
486 cargo_profile_args: Vec::new(),
487 extra_env: BTreeMap::new(),
488 inherited_env: BTreeSet::new(),
489 additional_inputs: Vec::new(),
490 target: DEFAULT_TARGET.to_owned(),
491 cargo_program: std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()),
492 rustc_program: std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()),
493 cache_mode: WasmBuildCacheMode::Isolated,
494 prune_policy: None,
495 prune_interval: None,
496 shared_incremental_maintenance_config: None,
497 }
498 }
499
500 #[must_use]
502 pub fn with_cargo_profile_args(mut self, arguments: &[&str]) -> Self {
503 self.cargo_profile_args = arguments.iter().map(OsString::from).collect();
504 self
505 }
506
507 #[must_use]
509 pub fn with_cargo_profile_args_os<I, S>(mut self, arguments: I) -> Self
510 where
511 I: IntoIterator<Item = S>,
512 S: Into<OsString>,
513 {
514 self.cargo_profile_args = arguments.into_iter().map(Into::into).collect();
515 self
516 }
517
518 #[must_use]
520 pub fn with_extra_env(mut self, environment: &[(&str, &str)]) -> Self {
521 self.extra_env = environment
522 .iter()
523 .map(|(key, value)| (OsString::from(key), OsString::from(value)))
524 .collect();
525 self
526 }
527
528 #[must_use]
530 pub fn with_extra_env_os<I, K, V>(mut self, environment: I) -> Self
531 where
532 I: IntoIterator<Item = (K, V)>,
533 K: Into<OsString>,
534 V: Into<OsString>,
535 {
536 self.extra_env = environment
537 .into_iter()
538 .map(|(key, value)| (key.into(), value.into()))
539 .collect();
540 self
541 }
542
543 #[must_use]
548 pub fn with_inherited_env(mut self, names: &[&str]) -> Self {
549 self.inherited_env.extend(names.iter().map(OsString::from));
550 self
551 }
552
553 #[must_use]
555 pub fn with_inherited_env_os<I, S>(mut self, names: I) -> Self
556 where
557 I: IntoIterator<Item = S>,
558 S: Into<OsString>,
559 {
560 self.inherited_env.extend(names.into_iter().map(Into::into));
561 self
562 }
563
564 #[must_use]
569 pub fn with_additional_inputs(mut self, paths: &[&str]) -> Self {
570 self.additional_inputs
571 .extend(paths.iter().map(PathBuf::from));
572 self
573 }
574
575 #[must_use]
577 pub fn with_additional_input_paths<I, P>(mut self, paths: I) -> Self
578 where
579 I: IntoIterator<Item = P>,
580 P: Into<PathBuf>,
581 {
582 self.additional_inputs
583 .extend(paths.into_iter().map(Into::into));
584 self
585 }
586
587 #[must_use]
589 pub fn with_target(mut self, target: &str) -> Self {
590 target.clone_into(&mut self.target);
591 self
592 }
593
594 #[must_use]
596 pub fn with_cargo_program(mut self, program: impl Into<OsString>) -> Self {
597 self.cargo_program = program.into();
598 self
599 }
600
601 #[must_use]
603 pub fn with_rustc_program(mut self, program: impl Into<OsString>) -> Self {
604 self.rustc_program = program.into();
605 self
606 }
607
608 #[must_use]
614 pub fn with_shared_incremental_target(mut self, target_dir: impl Into<PathBuf>) -> Self {
615 self.cache_mode = WasmBuildCacheMode::SharedIncremental {
616 target_dir: target_dir.into(),
617 };
618 self
619 }
620
621 #[must_use]
632 pub const fn with_shared_incremental_target_maintenance_at_most_every(
633 mut self,
634 policy: SharedIncrementalTargetPrunePolicy,
635 minimum_interval: Duration,
636 ) -> Self {
637 self.shared_incremental_maintenance_config =
638 Some(SharedIncrementalTargetMaintenanceConfig {
639 policy,
640 minimum_interval,
641 });
642 self
643 }
644
645 #[must_use]
651 pub const fn with_prune_policy(mut self, policy: WasmBuildCachePrunePolicy) -> Self {
652 self.prune_policy = Some(policy);
653 self.prune_interval = None;
654 self
655 }
656
657 #[must_use]
664 pub const fn with_prune_policy_at_most_every(
665 mut self,
666 policy: WasmBuildCachePrunePolicy,
667 minimum_interval: Duration,
668 ) -> Self {
669 self.prune_policy = Some(policy);
670 self.prune_interval = Some(minimum_interval);
671 self
672 }
673
674 #[must_use]
676 pub fn workspace_root(&self) -> &Path {
677 &self.workspace_root
678 }
679
680 #[must_use]
682 pub fn target_dir(&self) -> &Path {
683 &self.target_dir
684 }
685
686 #[must_use]
688 pub fn packages(&self) -> &[String] {
689 &self.packages
690 }
691
692 #[must_use]
694 pub const fn cache_mode(&self) -> &WasmBuildCacheMode {
695 &self.cache_mode
696 }
697}
698
699impl WasmBuildOutcome {
700 #[must_use]
702 pub const fn record(&self) -> &WasmBuildRecord {
703 match self {
704 Self::Built(record) | Self::Reused(record) => record,
705 }
706 }
707
708 #[must_use]
710 pub const fn is_reused(&self) -> bool {
711 matches!(self, Self::Reused(_))
712 }
713}
714
715impl WasmBuildRecord {
716 #[must_use]
718 pub const fn fingerprint(&self) -> InputDigest {
719 self.fingerprint
720 }
721
722 #[must_use]
724 pub const fn input_digest(&self) -> InputDigest {
725 self.input_digest
726 }
727
728 #[must_use]
730 pub fn artifacts(&self) -> &[PathBuf] {
731 &self.artifacts
732 }
733
734 #[must_use]
736 pub const fn timings(&self) -> WasmBuildTimings {
737 self.timings
738 }
739
740 #[must_use]
742 pub const fn maintenance(&self) -> Option<&WasmBuildCacheMaintenance> {
743 self.maintenance.as_ref()
744 }
745
746 #[must_use]
748 pub const fn shared_incremental_maintenance(
749 &self,
750 ) -> Option<&SharedIncrementalTargetMaintenanceOutcome> {
751 self.shared_incremental_maintenance.as_ref()
752 }
753}
754
755impl WasmBuildTimings {
756 #[must_use]
758 pub const fn lock_wait(self) -> Duration {
759 self.lock_wait
760 }
761
762 #[must_use]
764 pub const fn shared_incremental_lock_wait(self) -> Option<Duration> {
765 self.shared_incremental_lock_wait
766 }
767
768 #[must_use]
770 pub const fn input_resolution(self) -> Duration {
771 self.input_resolution.total
772 }
773
774 #[must_use]
776 pub const fn input_resolution_detail(self) -> WasmInputResolutionTimings {
777 self.input_resolution
778 }
779
780 #[must_use]
782 pub const fn cargo_build(self) -> Option<Duration> {
783 self.cargo_build
784 }
785
786 #[must_use]
788 pub const fn cache_maintenance(self) -> Option<Duration> {
789 self.cache_maintenance
790 }
791
792 #[must_use]
794 pub const fn total(self) -> Duration {
795 self.total
796 }
797}
798
799impl WasmInputResolutionTimings {
800 #[must_use]
802 pub const fn tool_identity(self) -> Duration {
803 self.tool_identity
804 }
805
806 #[must_use]
808 pub const fn cargo_metadata(self) -> Duration {
809 self.cargo_metadata
810 }
811
812 #[must_use]
814 pub const fn input_discovery(self) -> Duration {
815 self.input_discovery
816 }
817
818 #[must_use]
820 pub const fn content_hashing(self) -> Duration {
821 self.content_hashing
822 }
823
824 #[must_use]
826 pub const fn total(self) -> Duration {
827 self.total
828 }
829
830 const fn include(&mut self, other: Self) {
831 self.tool_identity = self.tool_identity.saturating_add(other.tool_identity);
832 self.cargo_metadata = self.cargo_metadata.saturating_add(other.cargo_metadata);
833 self.input_discovery = self.input_discovery.saturating_add(other.input_discovery);
834 self.content_hashing = self.content_hashing.saturating_add(other.content_hashing);
835 self.total = self.total.saturating_add(other.total);
836 }
837}
838
839impl CargoBuildInput {
840 #[must_use]
842 pub fn label(&self) -> &Path {
843 &self.label
844 }
845
846 #[must_use]
848 pub fn path(&self) -> &Path {
849 &self.path
850 }
851}
852
853impl ResolvedCargoBuildInputs {
854 #[must_use]
856 pub const fn fingerprint(&self) -> InputDigest {
857 self.fingerprint
858 }
859
860 #[must_use]
862 pub const fn input_digest(&self) -> InputDigest {
863 self.input_digest
864 }
865
866 #[must_use]
868 pub fn inputs(&self) -> &[CargoBuildInput] {
869 &self.inputs
870 }
871
872 #[must_use]
877 pub fn exclusions(&self) -> &[PathBuf] {
878 &self.exclusions
879 }
880
881 #[must_use]
883 pub const fn timings(&self) -> WasmInputResolutionTimings {
884 self.timings
885 }
886
887 pub fn is_current(&self, spec: &WasmBuildSpec) -> Result<bool, WasmBuildError> {
889 resolve_cargo_build_inputs(spec).map(|current| current.fingerprint == self.fingerprint)
890 }
891
892 pub fn is_content_current(&self) -> Result<bool, WasmBuildError> {
899 self.current_input_digest()
900 .map(|current| current == self.input_digest)
901 }
902
903 pub(super) fn current_input_digest(&self) -> Result<InputDigest, WasmBuildError> {
904 let inputs = self
905 .inputs
906 .iter()
907 .map(|input| (input.label.clone(), input.path.clone()))
908 .collect::<Vec<_>>();
909 digest_labeled_paths("wasm-source-inputs-v1", &inputs, &self.exclusions).map_err(|source| {
910 WasmBuildError::Io {
911 operation: "rehash resolved Cargo build inputs",
912 path: self
913 .inputs
914 .first()
915 .map_or_else(PathBuf::new, |input| input.path.clone()),
916 source,
917 }
918 })
919 }
920}
921
922impl SharedIncrementalTargetInspection {
923 #[must_use]
925 pub fn target_dir(&self) -> &Path {
926 &self.target_dir
927 }
928
929 #[must_use]
931 pub const fn logical_size_bytes(&self) -> u64 {
932 self.logical_size_bytes
933 }
934
935 #[must_use]
937 pub const fn last_used(&self) -> SystemTime {
938 self.last_used
939 }
940
941 #[must_use]
943 pub const fn lock_wait(&self) -> Duration {
944 self.lock_wait
945 }
946}
947
948impl SharedIncrementalTargetPrunePolicy {
949 #[must_use]
951 pub const fn new() -> Self {
952 Self {
953 max_age: None,
954 max_size_bytes: None,
955 }
956 }
957
958 #[must_use]
960 pub const fn with_max_age(mut self, max_age: Duration) -> Self {
961 self.max_age = Some(max_age);
962 self
963 }
964
965 #[must_use]
967 pub const fn with_max_size_bytes(mut self, bytes: u64) -> Self {
968 self.max_size_bytes = Some(bytes);
969 self
970 }
971
972 #[must_use]
974 pub const fn max_age(self) -> Option<Duration> {
975 self.max_age
976 }
977
978 #[must_use]
980 pub const fn max_size_bytes(self) -> Option<u64> {
981 self.max_size_bytes
982 }
983
984 fn maintenance_identity(self) -> String {
985 format!(
986 "age={:?};size={:?}",
987 self.max_age.map(|duration| duration.as_nanos()),
988 self.max_size_bytes
989 )
990 }
991}
992
993impl SharedIncrementalTargetMaintenance {
994 #[must_use]
996 pub fn target_dir(&self) -> &Path {
997 &self.target_dir
998 }
999
1000 #[must_use]
1002 pub const fn logical_size_bytes_before(&self) -> u64 {
1003 self.logical_size_bytes_before
1004 }
1005
1006 #[must_use]
1008 pub const fn logical_size_bytes_after(&self) -> u64 {
1009 self.logical_size_bytes_after
1010 }
1011
1012 #[must_use]
1014 pub const fn last_used_before(&self) -> SystemTime {
1015 self.last_used_before
1016 }
1017
1018 #[must_use]
1020 pub const fn was_cleared(&self) -> bool {
1021 self.cleared
1022 }
1023
1024 #[must_use]
1026 pub const fn lock_wait(&self) -> Duration {
1027 self.lock_wait
1028 }
1029
1030 #[must_use]
1032 pub const fn maintenance(&self) -> Duration {
1033 self.maintenance
1034 }
1035}
1036
1037impl std::fmt::Display for SharedIncrementalTargetMaintenance {
1038 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1039 write!(
1040 formatter,
1041 "target={} action={} bytes={}=>{} lock={:?} maintenance={:?}",
1042 self.target_dir.display(),
1043 if self.cleared { "cleared" } else { "retained" },
1044 self.logical_size_bytes_before,
1045 self.logical_size_bytes_after,
1046 self.lock_wait,
1047 self.maintenance,
1048 )
1049 }
1050}
1051
1052impl SharedIncrementalTargetMaintenanceOutcome {
1053 #[must_use]
1055 pub fn target_dir(&self) -> &Path {
1056 match self {
1057 Self::Missing { target_dir } | Self::Skipped { target_dir, .. } => target_dir,
1058 Self::Performed { maintenance, .. } => maintenance.target_dir(),
1059 }
1060 }
1061
1062 #[must_use]
1064 pub const fn maintenance(&self) -> Option<&SharedIncrementalTargetMaintenance> {
1065 match self {
1066 Self::Performed { maintenance, .. } => Some(maintenance),
1067 Self::Missing { .. } | Self::Skipped { .. } => None,
1068 }
1069 }
1070
1071 #[must_use]
1073 pub const fn was_performed(&self) -> bool {
1074 matches!(self, Self::Performed { .. })
1075 }
1076
1077 #[must_use]
1079 pub const fn lock_wait(&self) -> Option<Duration> {
1080 match self {
1081 Self::Missing { .. } => None,
1082 Self::Skipped { lock_wait, .. } => Some(*lock_wait),
1083 Self::Performed { maintenance, .. } => Some(maintenance.lock_wait()),
1084 }
1085 }
1086
1087 #[must_use]
1089 pub const fn schedule_check(&self) -> Option<Duration> {
1090 match self {
1091 Self::Missing { .. } => None,
1092 Self::Skipped { schedule_check, .. } | Self::Performed { schedule_check, .. } => {
1093 Some(*schedule_check)
1094 }
1095 }
1096 }
1097}
1098
1099impl std::fmt::Display for SharedIncrementalTargetMaintenanceOutcome {
1100 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1101 match self {
1102 Self::Missing { target_dir } => {
1103 write!(formatter, "target={} action=missing", target_dir.display())
1104 }
1105 Self::Skipped {
1106 target_dir,
1107 lock_wait,
1108 schedule_check,
1109 } => write!(
1110 formatter,
1111 "target={} action=skipped lock={lock_wait:?} schedule={schedule_check:?}",
1112 target_dir.display(),
1113 ),
1114 Self::Performed {
1115 maintenance,
1116 schedule_check,
1117 } => write!(formatter, "{maintenance} schedule={schedule_check:?}"),
1118 }
1119 }
1120}
1121
1122impl std::fmt::Display for WasmBuildTimings {
1123 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1124 write!(
1125 formatter,
1126 "total={:?} lock={:?} shared_lock={:?} inputs={:?} cargo={:?} maintenance={:?}",
1127 self.total,
1128 self.lock_wait,
1129 self.shared_incremental_lock_wait,
1130 self.input_resolution.total,
1131 self.cargo_build,
1132 self.cache_maintenance,
1133 )
1134 }
1135}
1136
1137impl std::fmt::Display for WasmBuildOutcome {
1138 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1139 let state = if self.is_reused() { "reused" } else { "built" };
1140 write!(
1141 formatter,
1142 "{state} fingerprint={} artifacts={} {}",
1143 self.record().fingerprint,
1144 self.record().artifacts.len(),
1145 self.record().timings,
1146 )?;
1147 if let Some(maintenance) = self.record().shared_incremental_maintenance() {
1148 write!(formatter, " shared_maintenance=({maintenance})")?;
1149 }
1150 Ok(())
1151 }
1152}
1153
1154pub fn resolve_cargo_build_inputs(
1159 spec: &WasmBuildSpec,
1160) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
1161 validate_spec(spec)?;
1162 build_fingerprint(spec)
1163}
1164
1165pub fn inspect_shared_incremental_target(
1170 spec: &WasmBuildSpec,
1171) -> Result<Option<SharedIncrementalTargetInspection>, WasmBuildError> {
1172 if !shared_incremental_target_exists(spec, "inspect shared incremental Cargo target")? {
1173 return Ok(None);
1174 }
1175
1176 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
1177 let logical_size_bytes =
1178 directory_logical_size(&canonical).map_err(|source| WasmBuildError::Io {
1179 operation: "measure shared incremental Cargo target",
1180 path: canonical.clone(),
1181 source,
1182 })?;
1183 let last_used = cache_entry_last_used(&canonical).map_err(|source| WasmBuildError::Io {
1184 operation: "read shared incremental Cargo target use time",
1185 path: canonical.clone(),
1186 source,
1187 })?;
1188 Ok(Some(SharedIncrementalTargetInspection {
1189 target_dir: canonical,
1190 logical_size_bytes,
1191 last_used,
1192 lock_wait,
1193 }))
1194}
1195
1196pub fn maintain_shared_incremental_target(
1210 spec: &WasmBuildSpec,
1211 policy: SharedIncrementalTargetPrunePolicy,
1212) -> Result<Option<SharedIncrementalTargetMaintenance>, WasmBuildError> {
1213 if !shared_incremental_target_exists(
1214 spec,
1215 "inspect shared incremental Cargo target before maintenance",
1216 )? {
1217 return Ok(None);
1218 }
1219
1220 let _ = resolve_cargo_build_inputs(spec)?;
1224 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
1225 maintain_shared_incremental_target_locked(&canonical, policy, lock_wait).map(Some)
1226}
1227
1228pub fn maintain_shared_incremental_target_at_most_every(
1242 spec: &WasmBuildSpec,
1243 policy: SharedIncrementalTargetPrunePolicy,
1244 minimum_interval: Duration,
1245) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
1246 let target_dir =
1247 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
1248 message: "shared incremental target is not configured".to_owned(),
1249 })?;
1250 if !shared_incremental_target_exists(
1251 spec,
1252 "inspect shared incremental Cargo target before scheduled maintenance",
1253 )? {
1254 return Ok(SharedIncrementalTargetMaintenanceOutcome::Missing { target_dir });
1255 }
1256
1257 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
1258 let schedule = schedule_shared_incremental_target_maintenance(
1259 &canonical,
1260 policy,
1261 minimum_interval,
1262 lock_wait,
1263 )?;
1264 let schedule = match schedule {
1265 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => return Ok(outcome),
1266 SharedIncrementalTargetMaintenanceSchedule::Due(due) => due,
1267 };
1268
1269 let _ = resolve_cargo_build_inputs(spec)?;
1272 perform_due_shared_incremental_target_maintenance(&canonical, policy, lock_wait, schedule)
1273}
1274
1275enum SharedIncrementalTargetMaintenanceSchedule {
1276 Skipped(SharedIncrementalTargetMaintenanceOutcome),
1277 Due(DueSharedIncrementalTargetMaintenance),
1278}
1279
1280struct DueSharedIncrementalTargetMaintenance {
1281 schedule_root: PathBuf,
1282 maintenance_identity: String,
1283 schedule_check: Duration,
1284}
1285
1286fn schedule_shared_incremental_target_maintenance(
1287 canonical: &Path,
1288 policy: SharedIncrementalTargetPrunePolicy,
1289 minimum_interval: Duration,
1290 lock_wait: Duration,
1291) -> Result<SharedIncrementalTargetMaintenanceSchedule, WasmBuildError> {
1292 let schedule_root = canonical.join(".ic-testkit");
1293 let maintenance_identity = policy.maintenance_identity();
1294 let schedule_started = Instant::now();
1295 let due = cache_maintenance_due(
1296 &schedule_root,
1297 Some(minimum_interval),
1298 &maintenance_identity,
1299 )
1300 .map_err(wasm_cache_fs_error)?;
1301 let schedule_check = schedule_started.elapsed();
1302 if !due {
1303 return Ok(SharedIncrementalTargetMaintenanceSchedule::Skipped(
1304 SharedIncrementalTargetMaintenanceOutcome::Skipped {
1305 target_dir: canonical.to_owned(),
1306 lock_wait,
1307 schedule_check,
1308 },
1309 ));
1310 }
1311 Ok(SharedIncrementalTargetMaintenanceSchedule::Due(
1312 DueSharedIncrementalTargetMaintenance {
1313 schedule_root,
1314 maintenance_identity,
1315 schedule_check,
1316 },
1317 ))
1318}
1319
1320fn perform_due_shared_incremental_target_maintenance(
1321 canonical: &Path,
1322 policy: SharedIncrementalTargetPrunePolicy,
1323 lock_wait: Duration,
1324 due: DueSharedIncrementalTargetMaintenance,
1325) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
1326 let DueSharedIncrementalTargetMaintenance {
1327 schedule_root,
1328 maintenance_identity,
1329 schedule_check,
1330 } = due;
1331 let maintenance = maintain_shared_incremental_target_locked(canonical, policy, lock_wait)?;
1332 record_cache_maintenance(&schedule_root, &maintenance_identity).map_err(wasm_cache_fs_error)?;
1333 Ok(SharedIncrementalTargetMaintenanceOutcome::Performed {
1334 maintenance,
1335 schedule_check,
1336 })
1337}
1338
1339fn maintain_shared_incremental_target_locked(
1340 canonical: &Path,
1341 policy: SharedIncrementalTargetPrunePolicy,
1342 lock_wait: Duration,
1343) -> Result<SharedIncrementalTargetMaintenance, WasmBuildError> {
1344 let started = Instant::now();
1345 let logical_size_bytes_before =
1346 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
1347 operation: "measure shared incremental Cargo target before maintenance",
1348 path: canonical.to_owned(),
1349 source,
1350 })?;
1351 let last_used_before =
1352 cache_entry_last_used(canonical).map_err(|source| WasmBuildError::Io {
1353 operation: "read shared incremental Cargo target use time before maintenance",
1354 path: canonical.to_owned(),
1355 source,
1356 })?;
1357 let expired = policy.max_age.is_some_and(|max_age| {
1358 SystemTime::now()
1359 .duration_since(last_used_before)
1360 .is_ok_and(|age| age > max_age)
1361 });
1362 let oversized = policy
1363 .max_size_bytes
1364 .is_some_and(|max_size_bytes| logical_size_bytes_before > max_size_bytes);
1365 let cleared = expired || oversized;
1366 if cleared {
1367 clear_shared_incremental_target_contents(canonical)?;
1368 record_cache_entry_use(canonical)?;
1369 }
1370 let logical_size_bytes_after = if cleared {
1371 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
1372 operation: "measure shared incremental Cargo target after maintenance",
1373 path: canonical.to_owned(),
1374 source,
1375 })?
1376 } else {
1377 logical_size_bytes_before
1378 };
1379 Ok(SharedIncrementalTargetMaintenance {
1380 target_dir: canonical.to_owned(),
1381 logical_size_bytes_before,
1382 logical_size_bytes_after,
1383 last_used_before,
1384 cleared,
1385 lock_wait,
1386 maintenance: started.elapsed(),
1387 })
1388}
1389
1390fn clear_shared_incremental_target_contents(target_dir: &Path) -> Result<(), WasmBuildError> {
1391 let entries = fs::read_dir(target_dir).map_err(|source| WasmBuildError::Io {
1392 operation: "read shared incremental Cargo target for maintenance",
1393 path: target_dir.to_owned(),
1394 source,
1395 })?;
1396 for entry in entries {
1397 let path = entry
1398 .map_err(|source| WasmBuildError::Io {
1399 operation: "read shared incremental Cargo target entry for maintenance",
1400 path: target_dir.to_owned(),
1401 source,
1402 })?
1403 .path();
1404 let preserved = path
1405 .file_name()
1406 .is_some_and(|name| name == ".ic-testkit" || name == "CACHEDIR.TAG");
1407 if !preserved {
1408 remove_path_if_present(&path).map_err(|source| WasmBuildError::Io {
1409 operation: "clear shared incremental Cargo target entry",
1410 path,
1411 source,
1412 })?;
1413 }
1414 }
1415 Ok(())
1416}
1417
1418pub fn build_wasm_canisters_cached(
1425 spec: &WasmBuildSpec,
1426) -> Result<WasmBuildOutcome, WasmBuildError> {
1427 build_wasm_canisters_cached_internal(spec, &mut ProgressReporter::silent())
1428}
1429
1430pub fn build_wasm_canisters_cached_with_progress<F>(
1438 spec: &WasmBuildSpec,
1439 config: WasmBuildProgressConfig,
1440 mut observer: F,
1441) -> Result<WasmBuildOutcome, WasmBuildError>
1442where
1443 F: FnMut(WasmBuildProgressEvent),
1444{
1445 if config.heartbeat_interval == Some(Duration::ZERO) {
1446 return Err(WasmBuildError::InvalidSpec {
1447 message: "Wasm build progress heartbeat interval must be greater than zero".to_owned(),
1448 });
1449 }
1450 build_wasm_canisters_cached_internal(
1451 spec,
1452 &mut ProgressReporter {
1453 config,
1454 observer: Some(&mut observer),
1455 },
1456 )
1457}
1458
1459fn build_wasm_canisters_cached_internal(
1460 spec: &WasmBuildSpec,
1461 progress: &mut ProgressReporter<'_>,
1462) -> Result<WasmBuildOutcome, WasmBuildError> {
1463 let total_started = Instant::now();
1464 validate_spec(spec)?;
1465 progress.emit(WasmBuildProgressEvent::Started);
1466 if spec.shared_incremental_maintenance_config.is_some() {
1467 let outcome = build_wasm_canisters_cached_with_scheduled_shared_maintenance(
1468 spec,
1469 total_started,
1470 progress,
1471 )?;
1472 emit_finished_progress(&outcome, progress);
1473 return Ok(outcome);
1474 }
1475 let (cache_lock, first_lock_wait) = lock_wasm_build_cache(&spec.target_dir)?;
1476 ensure_cache_directory_tag(&spec.target_dir)?;
1477
1478 let resolved = resolve_inputs_with_progress(spec, progress)?;
1479 if let Some(outcome) = try_reuse_wasm_artifacts(
1480 spec,
1481 &resolved,
1482 first_lock_wait,
1483 &SharedIncrementalAcquisitionContext::default(),
1484 total_started,
1485 )? {
1486 emit_finished_progress(&outcome, progress);
1487 return Ok(outcome);
1488 }
1489 progress.emit(WasmBuildProgressEvent::CacheMiss {
1490 fingerprint: resolved.fingerprint,
1491 });
1492
1493 let outcome = match &spec.cache_mode {
1494 WasmBuildCacheMode::Isolated => {
1495 let cache_entry = cache_entry_directory(spec, resolved.fingerprint);
1496 build_wasm_cache_miss(
1497 spec,
1498 resolved,
1499 first_lock_wait,
1500 SharedIncrementalAcquisitionContext::default(),
1501 cache_entry,
1502 total_started,
1503 progress,
1504 )
1505 }
1506 WasmBuildCacheMode::SharedIncremental { .. } => {
1507 drop(cache_lock);
1508 let configured_target = shared_incremental_target(spec)
1509 .expect("shared cache mode must resolve a shared Cargo target");
1510 progress.emit(WasmBuildProgressEvent::SharedTargetLockStarted {
1511 target_dir: configured_target,
1512 });
1513 let (shared_lock, shared_lock_wait, shared_target) =
1514 lock_shared_incremental_target(spec)?;
1515 progress.emit(WasmBuildProgressEvent::SharedTargetLockAcquired {
1516 target_dir: shared_target.clone(),
1517 wait: shared_lock_wait,
1518 });
1519 let (_cache_lock, second_lock_wait) = lock_wasm_build_cache(&spec.target_dir)?;
1520 ensure_cache_directory_tag(&spec.target_dir)?;
1521
1522 let mut current = resolve_inputs_with_progress(spec, progress)?;
1523 current.timings.include(resolved.timings);
1524 let lock_wait = first_lock_wait.saturating_add(second_lock_wait);
1525 let shared_incremental = SharedIncrementalAcquisitionContext {
1526 lock_wait: Some(shared_lock_wait),
1527 maintenance: None,
1528 };
1529 if let Some(outcome) = try_reuse_wasm_artifacts(
1530 spec,
1531 ¤t,
1532 lock_wait,
1533 &shared_incremental,
1534 total_started,
1535 )? {
1536 emit_finished_progress(&outcome, progress);
1537 return Ok(outcome);
1538 }
1539
1540 let outcome = build_wasm_cache_miss(
1541 spec,
1542 current,
1543 lock_wait,
1544 shared_incremental,
1545 shared_target,
1546 total_started,
1547 progress,
1548 );
1549 drop(shared_lock);
1550 outcome
1551 }
1552 }?;
1553 emit_finished_progress(&outcome, progress);
1554 Ok(outcome)
1555}
1556
1557fn build_wasm_canisters_cached_with_scheduled_shared_maintenance(
1558 spec: &WasmBuildSpec,
1559 total_started: Instant,
1560 progress: &mut ProgressReporter<'_>,
1561) -> Result<WasmBuildOutcome, WasmBuildError> {
1562 let configured_target = shared_incremental_target(spec)
1563 .expect("validated scheduled maintenance must have a shared Cargo target");
1564 progress.emit(WasmBuildProgressEvent::SharedTargetLockStarted {
1565 target_dir: configured_target,
1566 });
1567 let (_shared_lock, shared_lock_wait, shared_target) = lock_shared_incremental_target(spec)?;
1568 progress.emit(WasmBuildProgressEvent::SharedTargetLockAcquired {
1569 target_dir: shared_target.clone(),
1570 wait: shared_lock_wait,
1571 });
1572 let (_cache_lock, lock_wait) = lock_wasm_build_cache(&spec.target_dir)?;
1573 ensure_cache_directory_tag(&spec.target_dir)?;
1574
1575 let resolved = resolve_inputs_with_progress(spec, progress)?;
1578 let shared_maintenance = perform_configured_shared_incremental_target_maintenance(
1579 spec,
1580 &shared_target,
1581 shared_lock_wait,
1582 progress,
1583 )?;
1584 let shared_incremental = SharedIncrementalAcquisitionContext {
1585 lock_wait: Some(shared_lock_wait),
1586 maintenance: Some(shared_maintenance),
1587 };
1588 if let Some(outcome) = try_reuse_wasm_artifacts(
1589 spec,
1590 &resolved,
1591 lock_wait,
1592 &shared_incremental,
1593 total_started,
1594 )? {
1595 return Ok(outcome);
1596 }
1597 progress.emit(WasmBuildProgressEvent::CacheMiss {
1598 fingerprint: resolved.fingerprint,
1599 });
1600 build_wasm_cache_miss(
1601 spec,
1602 resolved,
1603 lock_wait,
1604 shared_incremental,
1605 shared_target,
1606 total_started,
1607 progress,
1608 )
1609}
1610
1611fn perform_configured_shared_incremental_target_maintenance(
1612 spec: &WasmBuildSpec,
1613 shared_target: &Path,
1614 lock_wait: Duration,
1615 progress: &mut ProgressReporter<'_>,
1616) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
1617 let config = spec
1618 .shared_incremental_maintenance_config
1619 .expect("configured shared-target maintenance must have settings");
1620 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceStarted {
1621 target_dir: shared_target.to_owned(),
1622 });
1623 let schedule = schedule_shared_incremental_target_maintenance(
1624 shared_target,
1625 config.policy,
1626 config.minimum_interval,
1627 lock_wait,
1628 )?;
1629 let outcome = match schedule {
1630 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => outcome,
1631 SharedIncrementalTargetMaintenanceSchedule::Due(due) => {
1632 perform_due_shared_incremental_target_maintenance(
1633 shared_target,
1634 config.policy,
1635 lock_wait,
1636 due,
1637 )?
1638 }
1639 };
1640 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceFinished {
1641 outcome: outcome.clone(),
1642 });
1643 Ok(outcome)
1644}
1645
1646fn resolve_inputs_with_progress(
1647 spec: &WasmBuildSpec,
1648 progress: &mut ProgressReporter<'_>,
1649) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
1650 let resolved = build_fingerprint(spec)?;
1651 progress.emit(WasmBuildProgressEvent::InputsResolved {
1652 fingerprint: resolved.fingerprint,
1653 input_digest: resolved.input_digest,
1654 elapsed: resolved.timings.total,
1655 });
1656 Ok(resolved)
1657}
1658
1659fn emit_finished_progress(outcome: &WasmBuildOutcome, progress: &mut ProgressReporter<'_>) {
1660 let state = if outcome.is_reused() {
1661 progress.emit(WasmBuildProgressEvent::CacheHit {
1662 fingerprint: outcome.record().fingerprint,
1663 });
1664 WasmBuildProgressOutcome::Reused
1665 } else {
1666 WasmBuildProgressOutcome::Built
1667 };
1668 progress.emit(WasmBuildProgressEvent::Finished {
1669 outcome: state,
1670 fingerprint: outcome.record().fingerprint,
1671 elapsed: outcome.record().timings.total,
1672 });
1673}
1674
1675#[derive(Clone, Debug, Default)]
1676struct SharedIncrementalAcquisitionContext {
1677 lock_wait: Option<Duration>,
1678 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
1679}
1680
1681fn try_reuse_wasm_artifacts(
1682 spec: &WasmBuildSpec,
1683 resolved: &ResolvedCargoBuildInputs,
1684 lock_wait: Duration,
1685 shared_incremental: &SharedIncrementalAcquisitionContext,
1686 total_started: Instant,
1687) -> Result<Option<WasmBuildOutcome>, WasmBuildError> {
1688 let fingerprint = resolved.fingerprint;
1689 let artifacts = expected_artifacts(spec, &spec.target_dir);
1690 let cache_entry = cache_entry_directory(spec, fingerprint);
1691 if artifact_set_matches(&artifacts, fingerprint) {
1692 record_cache_entry_use_if_present(&cache_entry)?;
1693 return Ok(Some(WasmBuildOutcome::Reused(complete_build_record(
1694 spec,
1695 BuildRecordInput {
1696 fingerprint,
1697 input_digest: resolved.input_digest,
1698 artifacts,
1699 lock_wait,
1700 shared_incremental: shared_incremental.clone(),
1701 input_resolution: resolved.timings,
1702 cargo_build: None,
1703 active_entry: &cache_entry,
1704 },
1705 total_started,
1706 ))));
1707 }
1708
1709 let cached_artifacts = expected_artifacts(spec, &cache_entry);
1710 if !artifact_set_matches(&cached_artifacts, fingerprint) {
1711 return Ok(None);
1712 }
1713 materialize_artifacts(&cached_artifacts, &artifacts, fingerprint)?;
1714 record_cache_entry_use(&cache_entry)?;
1715 Ok(Some(WasmBuildOutcome::Reused(complete_build_record(
1716 spec,
1717 BuildRecordInput {
1718 fingerprint,
1719 input_digest: resolved.input_digest,
1720 artifacts,
1721 lock_wait,
1722 shared_incremental: shared_incremental.clone(),
1723 input_resolution: resolved.timings,
1724 cargo_build: None,
1725 active_entry: &cache_entry,
1726 },
1727 total_started,
1728 ))))
1729}
1730
1731fn build_wasm_cache_miss(
1732 spec: &WasmBuildSpec,
1733 resolved: ResolvedCargoBuildInputs,
1734 lock_wait: Duration,
1735 shared_incremental: SharedIncrementalAcquisitionContext,
1736 cargo_target_dir: PathBuf,
1737 total_started: Instant,
1738 progress: &mut ProgressReporter<'_>,
1739) -> Result<WasmBuildOutcome, WasmBuildError> {
1740 let fingerprint = resolved.fingerprint;
1741 let mut input_resolution = resolved.timings;
1742 let artifacts = expected_artifacts(spec, &spec.target_dir);
1743 let cache_entry = cache_entry_directory(spec, fingerprint);
1744 remove_directory_if_present(&cache_entry)?;
1745 create_dir_all(
1746 &cache_entry,
1747 "create content-addressed Cargo target directory",
1748 )?;
1749 let incomplete_directory = IncompleteBuildDirectory::new(cache_entry.clone());
1750 let build_result = (|| {
1751 if matches!(
1752 spec.cache_mode,
1753 WasmBuildCacheMode::SharedIncremental { .. }
1754 ) {
1755 record_cache_entry_use(&cargo_target_dir)?;
1756 }
1757 let build_started = Instant::now();
1758 run_cargo_build(spec, &cargo_target_dir, progress)?;
1759 let cargo_build = build_started.elapsed();
1760 let built_artifacts = expected_artifacts(spec, &cargo_target_dir);
1761 let missing = missing_artifacts(&built_artifacts);
1762 if !missing.is_empty() {
1763 return Err(WasmBuildError::MissingArtifacts { paths: missing });
1764 }
1765
1766 let verified = resolve_inputs_with_progress(spec, progress)?;
1767 input_resolution.include(verified.timings);
1768 if fingerprint != verified.fingerprint {
1769 return Err(WasmBuildError::InputsChangedDuringBuild {
1770 before: fingerprint,
1771 after: verified.fingerprint,
1772 });
1773 }
1774
1775 let cached_artifacts = expected_artifacts(spec, &cache_entry);
1776 if cargo_target_dir != cache_entry {
1777 copy_wasm_artifacts(&built_artifacts, &cached_artifacts)?;
1778 }
1779 publish_artifact_stamps(&cached_artifacts, fingerprint)?;
1780 materialize_artifacts(&cached_artifacts, &artifacts, fingerprint)?;
1781 record_cache_entry_use(&cache_entry)?;
1782
1783 Ok(WasmBuildOutcome::Built(complete_build_record(
1784 spec,
1785 BuildRecordInput {
1786 fingerprint,
1787 input_digest: resolved.input_digest,
1788 artifacts,
1789 lock_wait,
1790 shared_incremental,
1791 input_resolution,
1792 cargo_build: Some(cargo_build),
1793 active_entry: &cache_entry,
1794 },
1795 total_started,
1796 )))
1797 })();
1798 finish_fingerprint_build(build_result, incomplete_directory)
1799}
1800
1801pub fn prune_wasm_build_cache(
1809 target_dir: &Path,
1810 policy: WasmBuildCachePrunePolicy,
1811) -> Result<WasmBuildCachePruneReport, WasmBuildError> {
1812 let (_lock_file, _) = lock_wasm_build_cache(target_dir)?;
1813 ensure_cache_directory_tag(target_dir)?;
1814
1815 prune_wasm_build_cache_locked(target_dir, policy, None)
1816}
1817
1818struct BuildRecordInput<'a> {
1819 fingerprint: InputDigest,
1820 input_digest: InputDigest,
1821 artifacts: Vec<PathBuf>,
1822 lock_wait: Duration,
1823 shared_incremental: SharedIncrementalAcquisitionContext,
1824 input_resolution: WasmInputResolutionTimings,
1825 cargo_build: Option<Duration>,
1826 active_entry: &'a Path,
1827}
1828
1829fn complete_build_record(
1830 spec: &WasmBuildSpec,
1831 input: BuildRecordInput<'_>,
1832 total_started: Instant,
1833) -> WasmBuildRecord {
1834 let (maintenance, cache_maintenance) = spec.prune_policy.map_or((None, None), |policy| {
1835 let cache_root = spec.target_dir.join(".ic-testkit/wasm-targets");
1836 let identity = policy.maintenance_identity();
1837 perform_scheduled_cache_maintenance(&cache_root, spec.prune_interval, &identity, || {
1838 prune_wasm_build_cache_locked(&spec.target_dir, policy, Some(input.active_entry))
1839 .map_err(|error| error.to_string())
1840 })
1841 });
1842 WasmBuildRecord {
1843 fingerprint: input.fingerprint,
1844 input_digest: input.input_digest,
1845 artifacts: input.artifacts,
1846 timings: WasmBuildTimings {
1847 lock_wait: input.lock_wait,
1848 shared_incremental_lock_wait: input.shared_incremental.lock_wait,
1849 input_resolution: input.input_resolution,
1850 cargo_build: input.cargo_build,
1851 cache_maintenance,
1852 total: total_started.elapsed(),
1853 },
1854 maintenance,
1855 shared_incremental_maintenance: input.shared_incremental.maintenance,
1856 }
1857}
1858
1859fn prune_wasm_build_cache_locked(
1860 target_dir: &Path,
1861 policy: WasmBuildCachePrunePolicy,
1862 protected_entry: Option<&Path>,
1863) -> Result<WasmBuildCachePruneReport, WasmBuildError> {
1864 let cache_root = target_dir.join(".ic-testkit/wasm-targets");
1865 prune_direct_child_directories(&cache_root, policy, protected_entry, is_sha256_directory)
1866 .map_err(wasm_cache_fs_error)
1867}
1868
1869struct IncompleteBuildDirectory {
1870 path: PathBuf,
1871 armed: bool,
1872}
1873
1874impl IncompleteBuildDirectory {
1875 const fn new(path: PathBuf) -> Self {
1876 Self { path, armed: true }
1877 }
1878
1879 fn preserve(mut self) {
1880 self.armed = false;
1881 }
1882
1883 fn cleanup(mut self) -> io::Result<()> {
1884 let result = remove_path_if_present(&self.path);
1885 if result.is_ok() {
1886 self.armed = false;
1887 }
1888 result
1889 }
1890}
1891
1892impl Drop for IncompleteBuildDirectory {
1893 fn drop(&mut self) {
1894 if self.armed {
1895 let _ = remove_path_if_present(&self.path);
1896 }
1897 }
1898}
1899
1900fn finish_fingerprint_build(
1901 result: Result<WasmBuildOutcome, WasmBuildError>,
1902 incomplete_directory: IncompleteBuildDirectory,
1903) -> Result<WasmBuildOutcome, WasmBuildError> {
1904 match result {
1905 Ok(outcome) => {
1906 incomplete_directory.preserve();
1907 Ok(outcome)
1908 }
1909 Err(build_error) => {
1910 let path = incomplete_directory.path.clone();
1911 match incomplete_directory.cleanup() {
1912 Ok(()) => Err(build_error),
1913 Err(source) => Err(WasmBuildError::FailedBuildCleanup {
1914 build_error: Box::new(build_error),
1915 path,
1916 source,
1917 }),
1918 }
1919 }
1920 }
1921}
1922
1923fn lock_wasm_build_cache(target_dir: &Path) -> Result<(File, Duration), WasmBuildError> {
1924 create_dir_all(target_dir, "create Cargo target directory")?;
1925 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
1926 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)
1927}
1928
1929fn lock_shared_incremental_target(
1930 spec: &WasmBuildSpec,
1931) -> Result<(File, Duration, PathBuf), WasmBuildError> {
1932 let target_dir =
1933 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
1934 message: "shared incremental target is not configured".to_owned(),
1935 })?;
1936 create_dir_all(
1937 &target_dir,
1938 "create shared incremental Cargo target directory",
1939 )?;
1940 ensure_cache_tag(&target_dir).map_err(wasm_cache_fs_error)?;
1941 let canonical = target_dir
1942 .canonicalize()
1943 .map_err(|source| WasmBuildError::Io {
1944 operation: "resolve shared incremental Cargo target directory",
1945 path: target_dir.clone(),
1946 source,
1947 })?;
1948 let lock_path = canonical.join(".ic-testkit/wasm-incremental.lock");
1949 let (lock, wait) = lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)?;
1950 Ok((lock, wait, canonical))
1951}
1952
1953fn ensure_cache_directory_tag(target_dir: &Path) -> Result<(), WasmBuildError> {
1954 ensure_cache_tag(target_dir).map_err(wasm_cache_fs_error)
1955}
1956
1957fn record_cache_entry_use_if_present(path: &Path) -> Result<(), WasmBuildError> {
1958 if path.is_dir() {
1959 record_cache_entry_use(path)?;
1960 }
1961 Ok(())
1962}
1963
1964fn record_cache_entry_use(path: &Path) -> Result<(), WasmBuildError> {
1965 record_entry_use(path).map_err(wasm_cache_fs_error)
1966}
1967
1968fn wasm_cache_fs_error(error: CacheFsError) -> WasmBuildError {
1969 WasmBuildError::Io {
1970 operation: error.operation,
1971 path: error.path,
1972 source: error.source,
1973 }
1974}
1975
1976fn validate_spec(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
1977 if spec.packages.is_empty() {
1978 return Err(WasmBuildError::InvalidSpec {
1979 message: "at least one Cargo package is required".to_owned(),
1980 });
1981 }
1982 if spec.profile_target_dir.is_empty() {
1983 return Err(WasmBuildError::InvalidSpec {
1984 message: "Cargo profile target directory must not be empty".to_owned(),
1985 });
1986 }
1987 if spec.target.is_empty() {
1988 return Err(WasmBuildError::InvalidSpec {
1989 message: "Cargo compilation target must not be empty".to_owned(),
1990 });
1991 }
1992 if matches!(
1993 &spec.cache_mode,
1994 WasmBuildCacheMode::SharedIncremental { target_dir } if target_dir.as_os_str().is_empty()
1995 ) {
1996 return Err(WasmBuildError::InvalidSpec {
1997 message: "shared incremental Cargo target directory must not be empty".to_owned(),
1998 });
1999 }
2000 if spec.shared_incremental_maintenance_config.is_some()
2001 && !matches!(
2002 spec.cache_mode,
2003 WasmBuildCacheMode::SharedIncremental { .. }
2004 )
2005 {
2006 return Err(WasmBuildError::InvalidSpec {
2007 message:
2008 "scheduled shared-target maintenance requires a shared incremental Cargo target"
2009 .to_owned(),
2010 });
2011 }
2012 Ok(())
2013}
2014
2015fn build_fingerprint(spec: &WasmBuildSpec) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2016 let total_started = Instant::now();
2017 let tool_started = Instant::now();
2018 let cargo_identity = command_identity(
2019 spec,
2020 WasmBuildPhase::CargoIdentity,
2021 &spec.cargo_program,
2022 &["--version", "--verbose"],
2023 )?;
2024 let rustc_program = spec
2025 .extra_env
2026 .get(OsStr::new("RUSTC"))
2027 .unwrap_or(&spec.rustc_program);
2028 let rustc_identity =
2029 command_identity(spec, WasmBuildPhase::RustcIdentity, rustc_program, &["-vV"])?;
2030 let tool_identity = tool_started.elapsed();
2031
2032 let metadata_started = Instant::now();
2033 let metadata = cargo_metadata(spec)?;
2034 let cargo_metadata = metadata_started.elapsed();
2035
2036 let discovery_started = Instant::now();
2037 let inputs = resolve_local_inputs(spec, &metadata)?;
2038 validate_shared_incremental_target_boundary(spec, &inputs)?;
2039 let exclusions = source_exclusions(spec, &inputs);
2040 let input_discovery = discovery_started.elapsed();
2041
2042 let hashing_started = Instant::now();
2043 let input_digest = digest_labeled_paths("wasm-source-inputs-v1", &inputs, &exclusions)
2044 .map_err(|source| WasmBuildError::Io {
2045 operation: "hash Wasm build inputs",
2046 path: spec.workspace_root.clone(),
2047 source,
2048 })?;
2049 let content_hashing = hashing_started.elapsed();
2050
2051 let mut hasher = InputHasher::new(CACHE_FORMAT_VERSION);
2052 let mut packages = spec.packages.clone();
2053 packages.sort();
2054 packages.dedup();
2055 for package in packages {
2056 hasher.field("package", package.as_bytes());
2057 }
2058 hasher.field("target", spec.target.as_bytes());
2059 hasher.field("profile-target-dir", spec.profile_target_dir.as_bytes());
2060 for argument in &spec.cargo_profile_args {
2061 hasher.field("cargo-argument", &os_bytes(argument));
2062 }
2063 for (key, value) in effective_environment(spec) {
2064 hasher.field("environment-key", &os_bytes(&key));
2065 if let Some(value) = value {
2066 hasher.field("environment-value", &os_bytes(&value));
2067 } else {
2068 hasher.field("environment-unset", b"");
2069 }
2070 }
2071 hasher.field("cargo-identity", &cargo_identity);
2072 hasher.field("rustc-identity", &rustc_identity);
2073 hasher.field("source-input-digest", input_digest.as_bytes());
2074 Ok(ResolvedCargoBuildInputs {
2075 fingerprint: hasher.finish(),
2076 input_digest,
2077 inputs: inputs
2078 .into_iter()
2079 .map(|(label, path)| CargoBuildInput { label, path })
2080 .collect(),
2081 exclusions,
2082 timings: WasmInputResolutionTimings {
2083 tool_identity,
2084 cargo_metadata,
2085 input_discovery,
2086 content_hashing,
2087 total: total_started.elapsed(),
2088 },
2089 })
2090}
2091
2092fn command_identity(
2093 spec: &WasmBuildSpec,
2094 phase: WasmBuildPhase,
2095 program: &OsStr,
2096 arguments: &[&str],
2097) -> Result<Vec<u8>, WasmBuildError> {
2098 let mut command = Command::new(program);
2099 command.current_dir(&spec.workspace_root).args(arguments);
2100 apply_command_environment(&mut command, spec);
2101 let output = command
2102 .output()
2103 .map_err(|source| WasmBuildError::CommandSpawn {
2104 phase,
2105 program: program.to_owned(),
2106 source,
2107 })?;
2108 ensure_command_success(phase, output).map(|output| {
2109 let mut identity = output.stdout;
2110 identity.extend_from_slice(&output.stderr);
2111 identity
2112 })
2113}
2114
2115fn cargo_metadata(spec: &WasmBuildSpec) -> Result<Value, WasmBuildError> {
2116 let mut command = Command::new(&spec.cargo_program);
2117 command
2118 .current_dir(&spec.workspace_root)
2119 .args(["metadata", "--format-version", "1"]);
2120 for argument in metadata_arguments(&spec.cargo_profile_args) {
2121 command.arg(argument);
2122 }
2123 apply_command_environment(&mut command, spec);
2124 let output = command
2125 .output()
2126 .map_err(|source| WasmBuildError::CommandSpawn {
2127 phase: WasmBuildPhase::CargoMetadata,
2128 program: spec.cargo_program.clone(),
2129 source,
2130 })?;
2131 let output = ensure_command_success(WasmBuildPhase::CargoMetadata, output)?;
2132 serde_json::from_slice(&output.stdout).map_err(|error| WasmBuildError::InvalidMetadata {
2133 message: format!("Cargo metadata was not valid JSON: {error}"),
2134 })
2135}
2136
2137fn metadata_arguments(arguments: &[OsString]) -> Vec<OsString> {
2138 let mut selected = Vec::new();
2139 let mut arguments = arguments.iter();
2140 while let Some(argument) = arguments.next() {
2141 let argument_text = argument.to_string_lossy();
2142 match argument_text.as_ref() {
2143 "--all-features" | "--no-default-features" | "--locked" | "--offline" | "--frozen" => {
2144 selected.push(argument.clone());
2145 }
2146 "--features" | "-F" | "--filter-platform" => {
2147 selected.push(argument.clone());
2148 if let Some(value) = arguments.next() {
2149 selected.push(value.clone());
2150 }
2151 }
2152 _ if argument_text.starts_with("--features=")
2153 || argument_text.starts_with("--filter-platform=") =>
2154 {
2155 selected.push(argument.clone());
2156 }
2157 _ => {}
2158 }
2159 }
2160 selected
2161}
2162
2163#[derive(Clone)]
2164struct MetadataPackage {
2165 id: String,
2166 name: String,
2167 version: String,
2168 manifest_path: PathBuf,
2169 is_local: bool,
2170}
2171
2172fn resolve_local_inputs(
2173 spec: &WasmBuildSpec,
2174 metadata: &Value,
2175) -> Result<Vec<(PathBuf, PathBuf)>, WasmBuildError> {
2176 let packages = metadata_packages(metadata)?;
2177 let mut selected_ids = selected_package_ids(spec, metadata, &packages)?;
2178 let dependencies = metadata_dependencies(metadata)?;
2179 let mut closure = BTreeSet::new();
2180 while let Some(id) = selected_ids.pop_front() {
2181 if !closure.insert(id.clone()) {
2182 continue;
2183 }
2184 if let Some(deps) = dependencies.get(&id) {
2185 selected_ids.extend(deps.iter().cloned());
2186 }
2187 }
2188
2189 let workspace_root = metadata
2190 .get("workspace_root")
2191 .and_then(Value::as_str)
2192 .map_or_else(|| spec.workspace_root.clone(), PathBuf::from);
2193 let mut inputs = workspace_configuration_inputs(spec, &workspace_root)?;
2194 append_package_inputs(&mut inputs, &packages, closure, &workspace_root)?;
2195 append_additional_inputs(&mut inputs, spec, &workspace_root);
2196 Ok(inputs)
2197}
2198
2199fn metadata_packages(metadata: &Value) -> Result<HashMap<String, MetadataPackage>, WasmBuildError> {
2200 let packages_value = metadata
2201 .get("packages")
2202 .and_then(Value::as_array)
2203 .ok_or_else(|| invalid_metadata("Cargo metadata has no package array"))?;
2204 let mut packages = HashMap::new();
2205 for value in packages_value {
2206 let package = MetadataPackage {
2207 id: required_string(value, "id")?,
2208 name: required_string(value, "name")?,
2209 version: required_string(value, "version")?,
2210 manifest_path: PathBuf::from(required_string(value, "manifest_path")?),
2211 is_local: value.get("source").is_some_and(Value::is_null),
2212 };
2213 packages.insert(package.id.clone(), package);
2214 }
2215 Ok(packages)
2216}
2217
2218fn selected_package_ids(
2219 spec: &WasmBuildSpec,
2220 metadata: &Value,
2221 packages: &HashMap<String, MetadataPackage>,
2222) -> Result<VecDeque<String>, WasmBuildError> {
2223 let workspace_members = metadata
2224 .get("workspace_members")
2225 .and_then(Value::as_array)
2226 .ok_or_else(|| invalid_metadata("Cargo metadata has no workspace member array"))?
2227 .iter()
2228 .filter_map(Value::as_str)
2229 .collect::<HashSet<_>>();
2230 let mut selected_ids = VecDeque::new();
2231 for requested in &spec.packages {
2232 let matches = packages
2233 .values()
2234 .filter(|package| {
2235 package.name == *requested && workspace_members.contains(package.id.as_str())
2236 })
2237 .map(|package| package.id.clone())
2238 .collect::<Vec<_>>();
2239 match matches.as_slice() {
2240 [id] => selected_ids.push_back(id.clone()),
2241 [] => {
2242 return Err(WasmBuildError::InvalidSpec {
2243 message: format!("Cargo workspace contains no package named `{requested}`"),
2244 });
2245 }
2246 _ => {
2247 return Err(WasmBuildError::InvalidSpec {
2248 message: format!("Cargo workspace package name `{requested}` is ambiguous"),
2249 });
2250 }
2251 }
2252 }
2253 Ok(selected_ids)
2254}
2255
2256fn metadata_dependencies(metadata: &Value) -> Result<HashMap<String, Vec<String>>, WasmBuildError> {
2257 let mut dependencies = HashMap::<String, Vec<String>>::new();
2258 let nodes = metadata
2259 .pointer("/resolve/nodes")
2260 .and_then(Value::as_array)
2261 .ok_or_else(|| invalid_metadata("Cargo metadata has no resolved dependency nodes"))?;
2262 for node in nodes {
2263 let id = required_string(node, "id")?;
2264 let deps = node
2265 .get("deps")
2266 .and_then(Value::as_array)
2267 .ok_or_else(|| invalid_metadata("Cargo metadata dependency node has no deps array"))?
2268 .iter()
2269 .map(|dependency| required_string(dependency, "pkg"))
2270 .collect::<Result<Vec<_>, _>>()?;
2271 dependencies.insert(id, deps);
2272 }
2273 Ok(dependencies)
2274}
2275
2276fn workspace_configuration_inputs(
2277 spec: &WasmBuildSpec,
2278 workspace_root: &Path,
2279) -> Result<Vec<(PathBuf, PathBuf)>, WasmBuildError> {
2280 let mut inputs = Vec::new();
2281 add_if_present(
2282 &mut inputs,
2283 "workspace/Cargo.toml",
2284 workspace_root.join("Cargo.toml"),
2285 );
2286 add_if_present(
2287 &mut inputs,
2288 "workspace/Cargo.lock",
2289 workspace_root.join("Cargo.lock"),
2290 );
2291 add_if_present(
2292 &mut inputs,
2293 "workspace/rust-toolchain.toml",
2294 workspace_root.join("rust-toolchain.toml"),
2295 );
2296 add_if_present(
2297 &mut inputs,
2298 "workspace/rust-toolchain",
2299 workspace_root.join("rust-toolchain"),
2300 );
2301 append_cargo_configuration_inputs(&mut inputs, spec, workspace_root)?;
2302 Ok(inputs)
2303}
2304
2305fn append_cargo_configuration_inputs(
2306 inputs: &mut Vec<(PathBuf, PathBuf)>,
2307 spec: &WasmBuildSpec,
2308 workspace_root: &Path,
2309) -> Result<(), WasmBuildError> {
2310 let invocation_root =
2311 spec.workspace_root
2312 .canonicalize()
2313 .map_err(|source| WasmBuildError::Io {
2314 operation: "resolve Cargo invocation directory",
2315 path: spec.workspace_root.clone(),
2316 source,
2317 })?;
2318 let canonical_workspace =
2319 workspace_root
2320 .canonicalize()
2321 .map_err(|source| WasmBuildError::Io {
2322 operation: "resolve Cargo workspace directory",
2323 path: workspace_root.to_owned(),
2324 source,
2325 })?;
2326
2327 let mut roots = invocation_root
2328 .ancestors()
2329 .filter_map(|directory| effective_cargo_config(&directory.join(".cargo")))
2330 .collect::<Vec<_>>();
2331 if let Some(cargo_home) = effective_cargo_home(spec, &invocation_root)
2332 && let Some(config) = effective_cargo_config(&cargo_home)
2333 {
2334 roots.push(config);
2335 }
2336
2337 let mut visited = BTreeSet::new();
2338 for config in roots {
2339 append_cargo_configuration_tree(
2340 inputs,
2341 &config,
2342 &canonical_workspace,
2343 &mut visited,
2344 false,
2345 )?;
2346 }
2347 Ok(())
2348}
2349
2350fn effective_cargo_config(directory: &Path) -> Option<PathBuf> {
2351 let extensionless = directory.join("config");
2352 if extensionless.exists() {
2353 return Some(extensionless);
2354 }
2355 let toml = directory.join("config.toml");
2356 toml.exists().then_some(toml)
2357}
2358
2359fn effective_cargo_home(spec: &WasmBuildSpec, invocation_root: &Path) -> Option<PathBuf> {
2360 if let Some(cargo_home) = command_environment_value(spec, "CARGO_HOME") {
2361 let cargo_home = PathBuf::from(cargo_home);
2362 return Some(if cargo_home.is_absolute() {
2363 cargo_home
2364 } else {
2365 invocation_root.join(cargo_home)
2366 });
2367 }
2368
2369 default_home_directory(spec).map(|home| {
2370 let home = if home.is_absolute() {
2371 home
2372 } else {
2373 invocation_root.join(home)
2374 };
2375 home.join(".cargo")
2376 })
2377}
2378
2379#[cfg(windows)]
2380fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
2381 command_environment_value(spec, "USERPROFILE")
2382 .or_else(|| command_environment_value(spec, "HOME"))
2383 .map(PathBuf::from)
2384}
2385
2386#[cfg(not(windows))]
2387fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
2388 command_environment_value(spec, "HOME").map(PathBuf::from)
2389}
2390
2391fn command_environment_value(spec: &WasmBuildSpec, name: &str) -> Option<OsString> {
2392 spec.extra_env
2393 .get(OsStr::new(name))
2394 .cloned()
2395 .or_else(|| std::env::var_os(name))
2396}
2397
2398fn append_cargo_configuration_tree(
2399 inputs: &mut Vec<(PathBuf, PathBuf)>,
2400 config: &Path,
2401 workspace_root: &Path,
2402 visited: &mut BTreeSet<PathBuf>,
2403 optional: bool,
2404) -> Result<(), WasmBuildError> {
2405 let canonical = match config.canonicalize() {
2406 Ok(canonical) => canonical,
2407 Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
2408 Err(source) => {
2409 return Err(WasmBuildError::Io {
2410 operation: "resolve Cargo configuration",
2411 path: config.to_owned(),
2412 source,
2413 });
2414 }
2415 };
2416 if !visited.insert(canonical.clone()) {
2417 return Ok(());
2418 }
2419
2420 let contents = fs::read_to_string(&canonical).map_err(|source| WasmBuildError::Io {
2421 operation: "read Cargo configuration",
2422 path: canonical.clone(),
2423 source,
2424 })?;
2425 let configuration = toml::from_str::<TomlValue>(&contents).map_err(|error| {
2426 WasmBuildError::InvalidCargoConfiguration {
2427 path: canonical.clone(),
2428 message: error.to_string(),
2429 }
2430 })?;
2431 inputs.push((
2432 cargo_configuration_label(&canonical, workspace_root),
2433 canonical.clone(),
2434 ));
2435
2436 let Some(include) = configuration.get("include") else {
2437 return Ok(());
2438 };
2439 let parent = canonical
2440 .parent()
2441 .ok_or_else(|| WasmBuildError::InvalidCargoConfiguration {
2442 path: canonical.clone(),
2443 message: "configuration path has no parent directory".to_owned(),
2444 })?;
2445 for (included, optional) in cargo_configuration_includes(include, &canonical)? {
2446 let included = if included.is_absolute() {
2447 included
2448 } else {
2449 parent.join(included)
2450 };
2451 append_cargo_configuration_tree(inputs, &included, workspace_root, visited, optional)?;
2452 }
2453 Ok(())
2454}
2455
2456fn cargo_configuration_includes(
2457 include: &TomlValue,
2458 config: &Path,
2459) -> Result<Vec<(PathBuf, bool)>, WasmBuildError> {
2460 let values = match include {
2461 TomlValue::Array(values) => values.as_slice(),
2462 value => std::slice::from_ref(value),
2463 };
2464 values
2465 .iter()
2466 .map(|value| match value {
2467 TomlValue::String(path) => Ok((PathBuf::from(path), false)),
2468 TomlValue::Table(table) => {
2469 let path = table
2470 .get("path")
2471 .and_then(TomlValue::as_str)
2472 .ok_or_else(|| {
2473 invalid_cargo_configuration(
2474 config,
2475 "Cargo configuration include table requires a string `path`",
2476 )
2477 })?;
2478 let optional = table
2479 .get("optional")
2480 .map(|value| {
2481 value.as_bool().ok_or_else(|| {
2482 invalid_cargo_configuration(
2483 config,
2484 "Cargo configuration include `optional` must be a boolean",
2485 )
2486 })
2487 })
2488 .transpose()?
2489 .unwrap_or(false);
2490 Ok((PathBuf::from(path), optional))
2491 }
2492 _ => Err(invalid_cargo_configuration(
2493 config,
2494 "Cargo configuration `include` must contain paths or include tables",
2495 )),
2496 })
2497 .collect()
2498}
2499
2500fn cargo_configuration_label(config: &Path, workspace_root: &Path) -> PathBuf {
2501 if let Ok(relative) = config.strip_prefix(workspace_root) {
2502 return PathBuf::from("cargo-config/workspace").join(relative);
2503 }
2504 let location = digest_bytes("cargo-config-location-v1", &os_bytes(config.as_os_str()));
2505 PathBuf::from("cargo-config/external").join(location.to_hex())
2506}
2507
2508fn invalid_cargo_configuration(path: &Path, message: &str) -> WasmBuildError {
2509 WasmBuildError::InvalidCargoConfiguration {
2510 path: path.to_owned(),
2511 message: message.to_owned(),
2512 }
2513}
2514
2515fn append_package_inputs(
2516 inputs: &mut Vec<(PathBuf, PathBuf)>,
2517 packages: &HashMap<String, MetadataPackage>,
2518 closure: BTreeSet<String>,
2519 workspace_root: &Path,
2520) -> Result<(), WasmBuildError> {
2521 for id in closure {
2522 let Some(package) = packages.get(&id) else {
2523 return Err(invalid_metadata(&format!(
2524 "resolved package `{id}` is missing"
2525 )));
2526 };
2527 if !package.is_local {
2528 continue;
2529 }
2530 let root = package.manifest_path.parent().ok_or_else(|| {
2531 invalid_metadata(&format!(
2532 "package `{}` manifest has no parent",
2533 package.name
2534 ))
2535 })?;
2536 let relative_manifest = package
2537 .manifest_path
2538 .strip_prefix(workspace_root)
2539 .unwrap_or(&package.manifest_path);
2540 let label = PathBuf::from(format!("package/{}@{}", package.name, package.version))
2541 .join(relative_manifest.parent().unwrap_or_else(|| Path::new(".")));
2542 inputs.push((label, root.to_owned()));
2543 }
2544 Ok(())
2545}
2546
2547fn append_additional_inputs(
2548 inputs: &mut Vec<(PathBuf, PathBuf)>,
2549 spec: &WasmBuildSpec,
2550 workspace_root: &Path,
2551) {
2552 for additional in &spec.additional_inputs {
2553 let path = if additional.is_absolute() {
2554 additional.clone()
2555 } else {
2556 workspace_root.join(additional)
2557 };
2558 inputs.push((PathBuf::from("additional").join(additional), path));
2559 }
2560}
2561
2562fn source_exclusions(spec: &WasmBuildSpec, inputs: &[(PathBuf, PathBuf)]) -> Vec<PathBuf> {
2563 let mut exclusions = vec![
2564 spec.target_dir.clone(),
2565 spec.workspace_root.join("target"),
2566 spec.workspace_root.join(".git"),
2567 ];
2568 if let Some(shared_target) = shared_incremental_target(spec) {
2569 exclusions.push(shared_target);
2570 }
2571 for (_, path) in inputs {
2572 if path.is_dir() {
2573 exclusions.push(path.join("target"));
2574 exclusions.push(path.join(".git"));
2575 }
2576 }
2577 exclusions
2578}
2579
2580fn validate_shared_incremental_target_boundary(
2581 spec: &WasmBuildSpec,
2582 inputs: &[(PathBuf, PathBuf)],
2583) -> Result<(), WasmBuildError> {
2584 let Some(shared_target) = shared_incremental_target(spec) else {
2585 return Ok(());
2586 };
2587 let shared_target =
2588 canonicalize_allow_missing(&shared_target).map_err(|source| WasmBuildError::Io {
2589 operation: "resolve shared incremental Cargo target boundary",
2590 path: shared_target.clone(),
2591 source,
2592 })?;
2593 let resolved_inputs = inputs
2594 .iter()
2595 .map(|(_, input)| {
2596 let canonical = input.canonicalize().map_err(|source| WasmBuildError::Io {
2597 operation: "resolve Cargo input boundary",
2598 path: input.clone(),
2599 source,
2600 })?;
2601 let metadata = fs::metadata(&canonical).map_err(|source| WasmBuildError::Io {
2602 operation: "inspect Cargo input boundary",
2603 path: canonical.clone(),
2604 source,
2605 })?;
2606 Ok((canonical, metadata.is_dir()))
2607 })
2608 .collect::<Result<Vec<_>, WasmBuildError>>()?;
2609 let safe_generated_roots = std::iter::once(spec.target_dir.clone())
2610 .chain(std::iter::once(spec.workspace_root.join("target")))
2611 .chain(
2612 inputs
2613 .iter()
2614 .filter(|(_, path)| path.is_dir())
2615 .map(|(_, path)| path.join("target")),
2616 )
2617 .filter_map(|path| canonicalize_allow_missing(&path).ok())
2618 .filter(|root| {
2619 !resolved_inputs
2620 .iter()
2621 .any(|(input, _is_directory)| input.starts_with(root))
2622 })
2623 .collect::<Vec<_>>();
2624 if safe_generated_roots
2625 .iter()
2626 .any(|root| shared_target.starts_with(root))
2627 {
2628 return Ok(());
2629 }
2630
2631 for (input, is_directory) in resolved_inputs {
2632 if shared_target == input
2633 || (is_directory && shared_target.starts_with(&input))
2634 || input.starts_with(&shared_target)
2635 {
2636 return Err(WasmBuildError::InvalidSpec {
2637 message: format!(
2638 "shared incremental target {} must not overlap exact Cargo inputs unless it is inside a generated target directory",
2639 shared_target.display()
2640 ),
2641 });
2642 }
2643 }
2644 Ok(())
2645}
2646
2647fn canonicalize_allow_missing(path: &Path) -> io::Result<PathBuf> {
2648 let absolute = if path.is_absolute() {
2649 path.to_owned()
2650 } else {
2651 std::env::current_dir()?.join(path)
2652 };
2653 let mut unresolved = Vec::<OsString>::new();
2654 let mut existing = absolute.as_path();
2655 loop {
2656 match existing.canonicalize() {
2657 Ok(mut canonical) => {
2658 for component in unresolved.into_iter().rev() {
2659 canonical.push(component);
2660 }
2661 return Ok(canonical);
2662 }
2663 Err(error) if error.kind() == io::ErrorKind::NotFound => {
2664 let Some(name) = existing.file_name() else {
2665 return Err(error);
2666 };
2667 unresolved.push(name.to_owned());
2668 existing = existing.parent().ok_or(error)?;
2669 }
2670 Err(error) => return Err(error),
2671 }
2672 }
2673}
2674
2675fn shared_incremental_target(spec: &WasmBuildSpec) -> Option<PathBuf> {
2676 let WasmBuildCacheMode::SharedIncremental { target_dir } = &spec.cache_mode else {
2677 return None;
2678 };
2679 Some(if target_dir.is_absolute() {
2680 target_dir.clone()
2681 } else {
2682 spec.workspace_root.join(target_dir)
2683 })
2684}
2685
2686fn shared_incremental_target_exists(
2687 spec: &WasmBuildSpec,
2688 operation: &'static str,
2689) -> Result<bool, WasmBuildError> {
2690 let target_dir =
2691 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
2692 message: "shared incremental target is not configured".to_owned(),
2693 })?;
2694 match fs::symlink_metadata(&target_dir) {
2695 Ok(metadata) if metadata.is_dir() => Ok(true),
2696 Ok(_) => Err(WasmBuildError::InvalidSpec {
2697 message: format!(
2698 "shared incremental Cargo target {} must be a directory",
2699 target_dir.display()
2700 ),
2701 }),
2702 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
2703 Err(source) => Err(WasmBuildError::Io {
2704 operation,
2705 path: target_dir,
2706 source,
2707 }),
2708 }
2709}
2710
2711fn effective_environment(spec: &WasmBuildSpec) -> BTreeMap<OsString, Option<OsString>> {
2712 let mut names = spec.inherited_env.clone();
2713 names.extend(AUTOMATIC_ENVIRONMENT.iter().map(OsString::from));
2714 let mut environment = names
2715 .into_iter()
2716 .map(|name| {
2717 let value = std::env::var_os(&name);
2718 (name, value)
2719 })
2720 .collect::<BTreeMap<_, _>>();
2721 for (key, value) in &spec.extra_env {
2722 environment.insert(key.clone(), Some(value.clone()));
2723 }
2724 environment
2725}
2726
2727fn apply_command_environment(command: &mut Command, spec: &WasmBuildSpec) {
2728 for (key, value) in &spec.extra_env {
2729 command.env(key, value);
2730 }
2731}
2732
2733fn run_cargo_build(
2734 spec: &WasmBuildSpec,
2735 build_target_dir: &Path,
2736 progress: &mut ProgressReporter<'_>,
2737) -> Result<(), WasmBuildError> {
2738 let mut command = Command::new(&spec.cargo_program);
2739 command
2740 .current_dir(&spec.workspace_root)
2741 .env("CARGO_TARGET_DIR", build_target_dir)
2742 .args(["build", "--target", &spec.target])
2743 .args(&spec.cargo_profile_args);
2744 apply_command_environment(&mut command, spec);
2745 for package in &spec.packages {
2746 command.args(["-p", package]);
2747 }
2748
2749 if !progress.is_observed() {
2750 let output = command
2751 .output()
2752 .map_err(|source| WasmBuildError::CommandSpawn {
2753 phase: WasmBuildPhase::CargoBuild,
2754 program: spec.cargo_program.clone(),
2755 source,
2756 })?;
2757 return ensure_command_success(WasmBuildPhase::CargoBuild, output).map(|_| ());
2758 }
2759
2760 run_observed_cargo_build(spec, build_target_dir, command, progress)
2761}
2762
2763fn run_observed_cargo_build(
2764 spec: &WasmBuildSpec,
2765 build_target_dir: &Path,
2766 mut command: Command,
2767 progress: &mut ProgressReporter<'_>,
2768) -> Result<(), WasmBuildError> {
2769 command.stdout(Stdio::piped()).stderr(Stdio::piped());
2770 let started = Instant::now();
2771 let child = command
2772 .spawn()
2773 .map_err(|source| WasmBuildError::CommandSpawn {
2774 phase: WasmBuildPhase::CargoBuild,
2775 program: spec.cargo_program.clone(),
2776 source,
2777 })?;
2778 let mut child = ObservedChild::new(child);
2779 progress.emit(WasmBuildProgressEvent::CargoStarted {
2780 target_dir: build_target_dir.to_owned(),
2781 });
2782
2783 let stdout = child
2784 .child_mut()
2785 .stdout
2786 .take()
2787 .expect("Cargo stdout must be piped");
2788 let stderr = child
2789 .child_mut()
2790 .stderr
2791 .take()
2792 .expect("Cargo stderr must be piped");
2793 let (sender, chunks) = mpsc::channel();
2794 let stdout_sender = sender.clone();
2795 let stdout_reader = thread::spawn(move || {
2796 read_process_output(stdout, WasmBuildOutputStream::Stdout, stdout_sender)
2797 });
2798 let stderr_reader =
2799 thread::spawn(move || read_process_output(stderr, WasmBuildOutputStream::Stderr, sender));
2800
2801 let captured = capture_observed_cargo_output(chunks, progress, started);
2802
2803 let status = child.wait().map_err(|source| WasmBuildError::Io {
2804 operation: "wait for observed cargo build",
2805 path: PathBuf::from(&spec.cargo_program),
2806 source,
2807 })?;
2808 join_output_reader(
2809 stdout_reader,
2810 "read observed cargo stdout",
2811 &spec.cargo_program,
2812 )?;
2813 join_output_reader(
2814 stderr_reader,
2815 "read observed cargo stderr",
2816 &spec.cargo_program,
2817 )?;
2818 let elapsed = started.elapsed();
2819 progress.emit(WasmBuildProgressEvent::CargoFinished {
2820 success: status.success(),
2821 code: status.code(),
2822 elapsed,
2823 });
2824
2825 ensure_command_success(
2826 WasmBuildPhase::CargoBuild,
2827 Output {
2828 status,
2829 stdout: captured.stdout,
2830 stderr: captured.stderr,
2831 },
2832 )
2833 .map(|_| ())
2834}
2835
2836struct CapturedProcessOutput {
2837 stdout: Vec<u8>,
2838 stderr: Vec<u8>,
2839}
2840
2841fn capture_observed_cargo_output(
2842 chunks: mpsc::Receiver<ProcessOutputChunk>,
2843 progress: &mut ProgressReporter<'_>,
2844 started: Instant,
2845) -> CapturedProcessOutput {
2846 let mut stdout = Vec::new();
2847 let mut stderr = Vec::new();
2848 let mut last_emitted = Instant::now();
2849 loop {
2850 let message = match progress.config.heartbeat_interval {
2851 Some(interval) => {
2852 let quiet_for = last_emitted.elapsed();
2853 if quiet_for >= interval {
2854 progress.emit(WasmBuildProgressEvent::CargoHeartbeat {
2855 elapsed: started.elapsed(),
2856 });
2857 last_emitted = Instant::now();
2858 None
2859 } else {
2860 match chunks.recv_timeout(interval.saturating_sub(quiet_for)) {
2861 Ok(chunk) => Some(chunk),
2862 Err(RecvTimeoutError::Timeout) => {
2863 progress.emit(WasmBuildProgressEvent::CargoHeartbeat {
2864 elapsed: started.elapsed(),
2865 });
2866 last_emitted = Instant::now();
2867 None
2868 }
2869 Err(RecvTimeoutError::Disconnected) => break,
2870 }
2871 }
2872 }
2873 None => match chunks.recv() {
2874 Ok(chunk) => Some(chunk),
2875 Err(_) => break,
2876 },
2877 };
2878 let Some(chunk) = message else {
2879 continue;
2880 };
2881 match chunk.stream {
2882 WasmBuildOutputStream::Stdout => stdout.extend_from_slice(&chunk.bytes),
2883 WasmBuildOutputStream::Stderr => stderr.extend_from_slice(&chunk.bytes),
2884 }
2885 if progress.config.emit_cargo_output {
2886 progress.emit(WasmBuildProgressEvent::CargoOutput {
2887 stream: chunk.stream,
2888 bytes: chunk.bytes,
2889 });
2890 last_emitted = Instant::now();
2891 }
2892 }
2893 CapturedProcessOutput { stdout, stderr }
2894}
2895
2896#[derive(Debug)]
2897struct ProcessOutputChunk {
2898 stream: WasmBuildOutputStream,
2899 bytes: Vec<u8>,
2900}
2901
2902fn read_process_output<R: io::Read>(
2903 mut reader: R,
2904 stream: WasmBuildOutputStream,
2905 sender: mpsc::Sender<ProcessOutputChunk>,
2906) -> io::Result<()> {
2907 let mut buffer = [0_u8; 8 * 1024];
2908 loop {
2909 let count = reader.read(&mut buffer)?;
2910 if count == 0 {
2911 return Ok(());
2912 }
2913 if sender
2914 .send(ProcessOutputChunk {
2915 stream,
2916 bytes: buffer[..count].to_vec(),
2917 })
2918 .is_err()
2919 {
2920 return Ok(());
2921 }
2922 }
2923}
2924
2925fn join_output_reader(
2926 reader: thread::JoinHandle<io::Result<()>>,
2927 operation: &'static str,
2928 cargo_program: &OsStr,
2929) -> Result<(), WasmBuildError> {
2930 let result = reader.join().map_err(|_| WasmBuildError::Io {
2931 operation,
2932 path: PathBuf::from(cargo_program),
2933 source: io::Error::other("Cargo output reader panicked"),
2934 })?;
2935 result.map_err(|source| WasmBuildError::Io {
2936 operation,
2937 path: PathBuf::from(cargo_program),
2938 source,
2939 })
2940}
2941
2942struct ObservedChild(Option<Child>);
2943
2944impl ObservedChild {
2945 const fn new(child: Child) -> Self {
2946 Self(Some(child))
2947 }
2948
2949 const fn child_mut(&mut self) -> &mut Child {
2950 self.0.as_mut().expect("observed child must be present")
2951 }
2952
2953 fn wait(&mut self) -> io::Result<ExitStatus> {
2954 let status = self.child_mut().wait()?;
2955 self.0.take();
2956 Ok(status)
2957 }
2958}
2959
2960impl Drop for ObservedChild {
2961 fn drop(&mut self) {
2962 if let Some(mut child) = self.0.take() {
2963 let _ = child.kill();
2964 let _ = child.wait();
2965 }
2966 }
2967}
2968
2969fn ensure_command_success(phase: WasmBuildPhase, output: Output) -> Result<Output, WasmBuildError> {
2970 if output.status.success() {
2971 return Ok(output);
2972 }
2973 Err(WasmBuildError::CommandFailed {
2974 phase,
2975 status: output.status,
2976 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
2977 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
2978 })
2979}
2980
2981fn expected_artifacts(spec: &WasmBuildSpec, target_dir: &Path) -> Vec<PathBuf> {
2982 let mut packages = spec.packages.iter().map(String::as_str).collect::<Vec<_>>();
2983 packages.sort_unstable();
2984 packages.dedup();
2985 packages
2986 .into_iter()
2987 .map(|package| {
2988 if spec.target == DEFAULT_TARGET {
2989 wasm_path(target_dir, package, &spec.profile_target_dir)
2990 } else {
2991 target_dir
2992 .join(&spec.target)
2993 .join(&spec.profile_target_dir)
2994 .join(format!("{package}.wasm"))
2995 }
2996 })
2997 .collect()
2998}
2999
3000fn cache_entry_directory(spec: &WasmBuildSpec, fingerprint: InputDigest) -> PathBuf {
3001 spec.target_dir
3002 .join(".ic-testkit/wasm-targets")
3003 .join(fingerprint.to_hex())
3004}
3005
3006fn artifact_set_matches(artifacts: &[PathBuf], fingerprint: InputDigest) -> bool {
3007 artifacts.iter().all(|path| {
3008 fs::metadata(path).is_ok_and(|metadata| metadata.is_file() && metadata.len() > 0)
3009 && cache_stamp_matches(path, fingerprint)
3010 })
3011}
3012
3013fn missing_artifacts(artifacts: &[PathBuf]) -> Vec<PathBuf> {
3014 artifacts
3015 .iter()
3016 .filter(|path| {
3017 fs::metadata(path).map_or(true, |metadata| !metadata.is_file() || metadata.len() == 0)
3018 })
3019 .cloned()
3020 .collect()
3021}
3022
3023fn cache_stamp_matches(artifact: &Path, fingerprint: InputDigest) -> bool {
3024 let stamp_path = artifact_stamp_path(artifact);
3025 let Ok(expected) = artifact_stamp_contents(artifact, fingerprint) else {
3026 return false;
3027 };
3028 fs::read_to_string(stamp_path).is_ok_and(|stamp| stamp == expected)
3029}
3030
3031fn artifact_stamp_path(artifact: &Path) -> PathBuf {
3032 let mut name = artifact
3033 .file_name()
3034 .map_or_else(|| OsString::from("artifact"), OsString::from);
3035 name.push(".ic-testkit-build");
3036 artifact.with_file_name(name)
3037}
3038
3039fn artifact_stamp_contents(artifact: &Path, fingerprint: InputDigest) -> io::Result<String> {
3040 let (_, artifact_digest) = digest_file("wasm-artifact-v1", artifact)?;
3041 Ok(format!(
3042 "{CACHE_FORMAT_VERSION}\nbuild-sha256:{fingerprint}\nartifact-sha256:{artifact_digest}\n"
3043 ))
3044}
3045
3046fn publish_artifact_stamps(
3047 artifacts: &[PathBuf],
3048 fingerprint: InputDigest,
3049) -> Result<(), WasmBuildError> {
3050 for artifact in artifacts {
3051 let stamp_path = artifact_stamp_path(artifact);
3052 let stamp = artifact_stamp_contents(artifact, fingerprint).map_err(|source| {
3053 WasmBuildError::Io {
3054 operation: "hash built Wasm artifact",
3055 path: artifact.clone(),
3056 source,
3057 }
3058 })?;
3059 write_atomic(&stamp_path, stamp.as_bytes()).map_err(|source| WasmBuildError::Io {
3060 operation: "publish Wasm build stamp",
3061 path: stamp_path,
3062 source,
3063 })?;
3064 }
3065 Ok(())
3066}
3067
3068fn materialize_artifacts(
3069 cached_artifacts: &[PathBuf],
3070 artifacts: &[PathBuf],
3071 fingerprint: InputDigest,
3072) -> Result<(), WasmBuildError> {
3073 for (cached, artifact) in cached_artifacts.iter().zip(artifacts) {
3074 copy_file_atomic(cached, artifact).map_err(|source| WasmBuildError::Io {
3075 operation: "publish Wasm artifact",
3076 path: artifact.clone(),
3077 source,
3078 })?;
3079 }
3080 publish_artifact_stamps(artifacts, fingerprint)
3081}
3082
3083fn copy_wasm_artifacts(
3084 source_artifacts: &[PathBuf],
3085 cached_artifacts: &[PathBuf],
3086) -> Result<(), WasmBuildError> {
3087 for (source, cached) in source_artifacts.iter().zip(cached_artifacts) {
3088 copy_file_atomic(source, cached).map_err(|source_error| WasmBuildError::Io {
3089 operation: "cache shared-incremental Wasm artifact",
3090 path: cached.clone(),
3091 source: source_error,
3092 })?;
3093 }
3094 Ok(())
3095}
3096
3097fn remove_directory_if_present(path: &Path) -> Result<(), WasmBuildError> {
3098 remove_path_if_present(path).map_err(|source| WasmBuildError::Io {
3099 operation: "remove incomplete content-addressed Cargo target directory",
3100 path: path.to_owned(),
3101 source,
3102 })
3103}
3104
3105fn create_dir_all(path: &Path, operation: &'static str) -> Result<(), WasmBuildError> {
3106 fs::create_dir_all(path).map_err(|source| WasmBuildError::Io {
3107 operation,
3108 path: path.to_owned(),
3109 source,
3110 })
3111}
3112
3113fn add_if_present(inputs: &mut Vec<(PathBuf, PathBuf)>, label: &str, path: PathBuf) {
3114 if path.exists() {
3115 inputs.push((PathBuf::from(label), path));
3116 }
3117}
3118
3119fn required_string(value: &Value, field: &str) -> Result<String, WasmBuildError> {
3120 value
3121 .get(field)
3122 .and_then(Value::as_str)
3123 .map(str::to_owned)
3124 .ok_or_else(|| invalid_metadata(&format!("Cargo metadata field `{field}` is missing")))
3125}
3126
3127fn invalid_metadata(message: &str) -> WasmBuildError {
3128 WasmBuildError::InvalidMetadata {
3129 message: message.to_owned(),
3130 }
3131}
3132
3133impl std::fmt::Display for WasmBuildPhase {
3134 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3135 formatter.write_str(match self {
3136 Self::CargoMetadata => "cargo metadata",
3137 Self::CargoIdentity => "Cargo identity",
3138 Self::RustcIdentity => "Rust compiler identity",
3139 Self::CargoBuild => "cargo build",
3140 })
3141 }
3142}
3143
3144impl std::fmt::Display for WasmBuildError {
3145 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3146 match self {
3147 Self::InvalidSpec { message } => {
3148 write!(formatter, "invalid Wasm build spec: {message}")
3149 }
3150 Self::Io {
3151 operation,
3152 path,
3153 source,
3154 } => write!(
3155 formatter,
3156 "failed to {operation} at {}: {source}",
3157 path.display()
3158 ),
3159 Self::CommandSpawn {
3160 phase,
3161 program,
3162 source,
3163 } => write!(
3164 formatter,
3165 "failed to launch {phase} using `{}`: {source}",
3166 program.to_string_lossy(),
3167 ),
3168 Self::CommandFailed {
3169 phase,
3170 status,
3171 stdout,
3172 stderr,
3173 } => write!(
3174 formatter,
3175 "{phase} failed with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}",
3176 ),
3177 Self::InvalidMetadata { message } => {
3178 write!(formatter, "invalid Cargo metadata: {message}")
3179 }
3180 Self::InvalidCargoConfiguration { path, message } => write!(
3181 formatter,
3182 "invalid Cargo configuration at {}: {message}",
3183 path.display(),
3184 ),
3185 Self::MissingArtifacts { paths } => write!(
3186 formatter,
3187 "cargo build succeeded without producing: {}",
3188 paths
3189 .iter()
3190 .map(|path| path.display().to_string())
3191 .collect::<Vec<_>>()
3192 .join(", "),
3193 ),
3194 Self::InputsChangedDuringBuild { before, after } => write!(
3195 formatter,
3196 "Wasm build inputs changed while Cargo was running: {before} -> {after}",
3197 ),
3198 Self::FailedBuildCleanup {
3199 build_error,
3200 path,
3201 source,
3202 } => write!(
3203 formatter,
3204 "Wasm build failed ({build_error}) and its incomplete target directory at {} could not be removed: {source}",
3205 path.display(),
3206 ),
3207 }
3208 }
3209}
3210
3211impl std::error::Error for WasmBuildError {
3212 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3213 match self {
3214 Self::Io { source, .. }
3215 | Self::CommandSpawn { source, .. }
3216 | Self::FailedBuildCleanup { source, .. } => Some(source),
3217 _ => None,
3218 }
3219 }
3220}
3221
3222#[cfg(test)]
3223mod tests;