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