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