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> {
1459 resolve_cargo_build_inputs(spec).map(|current| current.fingerprint == self.fingerprint)
1460 }
1461
1462 pub fn is_content_current(&self) -> Result<bool, WasmBuildError> {
1469 self.current_validation_digest()
1470 .map(|current| current == self.validation_digest)
1471 }
1472
1473 pub(super) fn current_validation_digest(&self) -> Result<InputDigest, WasmBuildError> {
1474 digest_labeled_paths_composable(
1475 "wasm-source-inputs-v1",
1476 self.inputs
1477 .iter()
1478 .map(|input| (input.label.as_path(), input.path.as_path())),
1479 &self.exclusions,
1480 &mut LabeledPathDigestCache::default(),
1481 )
1482 .map_err(|source| WasmBuildError::Io {
1483 operation: "rehash resolved Cargo build inputs",
1484 path: self
1485 .inputs
1486 .first()
1487 .map_or_else(PathBuf::new, |input| input.path.clone()),
1488 source,
1489 })
1490 }
1491}
1492
1493impl WasmBuildSessionState {
1494 pub(super) fn new() -> Self {
1495 Self {
1496 snapshots: Vec::new(),
1497 digest_cache: LabeledPathDigestCache::default(),
1498 snapshot_reuses: 0,
1499 invalidated: false,
1500 }
1501 }
1502
1503 pub(super) const fn snapshot_count(&self) -> usize {
1504 self.snapshots.len()
1505 }
1506
1507 pub(super) const fn snapshot_reuses(&self) -> usize {
1508 self.snapshot_reuses
1509 }
1510
1511 pub(super) const fn is_invalidated(&self) -> bool {
1512 self.invalidated
1513 }
1514
1515 fn reuse(&mut self, spec: &WasmBuildSpec) -> Option<ResolvedCargoBuildInputs> {
1516 let (_, snapshot) = self
1517 .snapshots
1518 .iter()
1519 .find(|(candidate, _)| candidate == spec)?;
1520 self.snapshot_reuses = self.snapshot_reuses.saturating_add(1);
1521 Some(snapshot.clone())
1522 }
1523
1524 fn remember(&mut self, spec: &WasmBuildSpec, resolved: &ResolvedCargoBuildInputs) {
1525 if self
1526 .snapshots
1527 .iter()
1528 .any(|(candidate, _)| candidate == spec)
1529 {
1530 return;
1531 }
1532 let mut snapshot = resolved.clone();
1533 snapshot.timings = WasmInputResolutionTimings::default();
1534 self.snapshots.push((spec.clone(), snapshot));
1535 }
1536
1537 fn invalidate(&mut self) {
1538 self.snapshots.clear();
1539 self.digest_cache = LabeledPathDigestCache::default();
1540 self.invalidated = true;
1541 }
1542}
1543
1544impl WasmBuildInputSnapshotState {
1545 pub(super) fn prepare(specs: &[WasmBuildSpec]) -> Result<Self, WasmBuildError> {
1546 for spec in specs {
1547 validate_spec(spec)?;
1548 }
1549 let mut resolver = WasmBuildBatchInputResolver::new(specs, None);
1550 let mut snapshots = Vec::with_capacity(specs.len());
1551 let mut preparation_timings = WasmInputResolutionTimings::default();
1552 let mut progress = ProgressReporter::silent();
1553 for (index, spec) in specs.iter().enumerate() {
1554 let mut prepared_input = resolver.resolve(index, &mut progress)?;
1555 preparation_timings.include(prepared_input.timings);
1556 prepared_input.timings = WasmInputResolutionTimings::default();
1557 snapshots.push((spec.clone(), prepared_input));
1558 }
1559 Ok(Self {
1560 snapshots,
1561 preparation_metrics: resolver.metrics(),
1562 preparation_timings,
1563 reader_reuses: AtomicUsize::new(0),
1564 invalidation: Arc::new(RwLock::new(false)),
1565 })
1566 }
1567
1568 pub(super) fn contains(&self, spec: &WasmBuildSpec) -> bool {
1569 self.snapshots
1570 .iter()
1571 .any(|(candidate, _)| candidate == spec)
1572 }
1573
1574 fn reuse(&self, spec: &WasmBuildSpec) -> Option<ResolvedCargoBuildInputs> {
1575 let (_, snapshot) = self
1576 .snapshots
1577 .iter()
1578 .find(|(candidate, _)| candidate == spec)?;
1579 let mut current = self.reader_reuses.load(Ordering::Relaxed);
1580 loop {
1581 match self.reader_reuses.compare_exchange_weak(
1582 current,
1583 current.saturating_add(1),
1584 Ordering::Relaxed,
1585 Ordering::Relaxed,
1586 ) {
1587 Ok(_) => break,
1588 Err(observed) => current = observed,
1589 }
1590 }
1591 Some(snapshot.clone())
1592 }
1593
1594 pub(super) const fn specification_count(&self) -> usize {
1595 self.snapshots.len()
1596 }
1597
1598 pub(super) const fn preparation_metrics(&self) -> WasmBuildBatchInputMetrics {
1599 self.preparation_metrics
1600 }
1601
1602 pub(super) const fn preparation_timings(&self) -> WasmInputResolutionTimings {
1603 self.preparation_timings
1604 }
1605
1606 pub(super) fn reader_reuses(&self) -> usize {
1607 self.reader_reuses.load(Ordering::Relaxed)
1608 }
1609
1610 pub(super) fn is_invalidated(&self) -> bool {
1611 *self
1612 .invalidation
1613 .read()
1614 .unwrap_or_else(std::sync::PoisonError::into_inner)
1615 }
1616
1617 fn invalidate(&self) {
1618 *self
1619 .invalidation
1620 .write()
1621 .unwrap_or_else(std::sync::PoisonError::into_inner) = true;
1622 }
1623
1624 fn invalidation(&self) -> Arc<RwLock<bool>> {
1625 Arc::clone(&self.invalidation)
1626 }
1627}
1628
1629impl<'a, 'session> WasmBuildBatchInputResolver<'a, 'session> {
1630 pub(super) fn new(
1631 specs: impl IntoIterator<Item = &'a WasmBuildSpec>,
1632 mut reuse: Option<WasmBuildInputReuse<'session>>,
1633 ) -> Self {
1634 let specs = specs.into_iter().collect::<Vec<_>>();
1635 let mut keys = Vec::<BatchResolutionKey>::new();
1636 let mut groups = Vec::<BatchResolutionGroup>::new();
1637 let mut group_by_index = Vec::with_capacity(specs.len());
1638 for (index, spec) in specs.iter().copied().enumerate() {
1639 let key = BatchResolutionKey::for_spec(spec);
1640 let group = keys
1641 .iter()
1642 .position(|candidate| *candidate == key)
1643 .unwrap_or_else(|| {
1644 keys.push(key);
1645 groups.push(BatchResolutionGroup {
1646 indexes: Vec::new(),
1647 });
1648 groups.len() - 1
1649 });
1650 groups[group].indexes.push(index);
1651 group_by_index.push(group);
1652 }
1653 let mut metrics = WasmBuildBatchInputMetrics::default();
1654 let resolved = specs
1655 .iter()
1656 .map(|spec| match reuse.as_mut() {
1657 Some(WasmBuildInputReuse::Session(session)) => {
1658 let reused = session.reuse(spec);
1659 if reused.is_some() {
1660 metrics.session_reuses = metrics.session_reuses.saturating_add(1);
1661 }
1662 reused.map(Ok)
1663 }
1664 Some(WasmBuildInputReuse::Snapshot(snapshot)) => {
1665 let reused = snapshot
1666 .reuse(spec)
1667 .expect("prepared input snapshot must contain every reader specification");
1668 metrics.prepared_reuses = metrics.prepared_reuses.saturating_add(1);
1669 Some(Ok(reused))
1670 }
1671 None => None,
1672 })
1673 .collect();
1674 Self {
1675 specs,
1676 groups,
1677 group_by_index,
1678 resolved,
1679 reuse,
1680 metrics,
1681 }
1682 }
1683
1684 pub(super) const fn metrics(&self) -> WasmBuildBatchInputMetrics {
1685 self.metrics
1686 }
1687
1688 pub(super) fn invalidate_source_lease(&mut self) {
1689 match self.reuse.as_mut() {
1690 Some(WasmBuildInputReuse::Session(session)) => {
1691 session.invalidate();
1692 for resolved in &mut self.resolved {
1696 *resolved = None;
1697 }
1698 self.reuse = None;
1699 }
1700 Some(WasmBuildInputReuse::Snapshot(snapshot)) => snapshot.invalidate(),
1701 None => {}
1702 }
1703 }
1704
1705 pub(super) const fn assumes_sources_immutable(&self) -> bool {
1706 self.reuse.is_some()
1707 }
1708
1709 pub(super) fn prepared_invalidation(&self) -> Option<Arc<RwLock<bool>>> {
1710 match self.reuse.as_ref() {
1711 Some(WasmBuildInputReuse::Snapshot(snapshot)) => Some(snapshot.invalidation()),
1712 _ => None,
1713 }
1714 }
1715
1716 fn resolve(
1717 &mut self,
1718 index: usize,
1719 progress: &mut ProgressReporter<'_>,
1720 ) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
1721 if self.resolved[index].is_none() {
1722 self.resolve_group(index, progress)?;
1723 }
1724 self.resolved[index]
1725 .take()
1726 .expect("resolved batch input must be populated")
1727 .map_err(|(phase, error)| {
1728 progress.begin_phase(phase);
1729 error
1730 })
1731 }
1732
1733 fn resolve_group(
1734 &mut self,
1735 active_index: usize,
1736 progress: &mut ProgressReporter<'_>,
1737 ) -> Result<(), WasmBuildError> {
1738 let total_started = Instant::now();
1739 let group = self.group_by_index[active_index];
1740 let active = self.specs[active_index];
1741
1742 let (cargo_identity, rustc_identity, tool_identity) =
1743 resolve_tool_identity(active, progress)?;
1744
1745 let metadata_started = Instant::now();
1746 let metadata = progress.run_phase(WasmBuildProgressPhase::CargoMetadata, || {
1747 cargo_metadata(active)
1748 })?;
1749 let cargo_metadata = metadata_started.elapsed();
1750
1751 let (discovered, input_discovery) = self.discover_group_inputs(group, &metadata, progress);
1752
1753 let hashing_started = Instant::now();
1754 let mut batch_digest_cache = LabeledPathDigestCache::default();
1755 let digest_cache = match self.reuse.as_mut() {
1756 Some(WasmBuildInputReuse::Session(session)) => &mut session.digest_cache,
1757 _ => &mut batch_digest_cache,
1758 };
1759 let workspace_root = &active.workspace_root;
1760 let resolved_inputs = progress.run_phase(WasmBuildProgressPhase::ContentHashing, || {
1761 discovered
1762 .into_iter()
1763 .map(|(index, inputs, exclusions)| {
1764 let result = digest_resolved_local_inputs(
1765 &inputs,
1766 &exclusions,
1767 digest_cache,
1768 workspace_root,
1769 "hash batched Wasm build inputs",
1770 "hash batched semantic Wasm build inputs",
1771 )
1772 .map(|(input_digest, validation_digest)| {
1773 (
1774 inputs.validation_inputs,
1775 exclusions,
1776 input_digest,
1777 validation_digest,
1778 inputs.wasm_artifact_error,
1779 )
1780 });
1781 (index, result)
1782 })
1783 .collect::<Vec<_>>()
1784 });
1785 let content_hashing = hashing_started.elapsed();
1786 let timings = WasmInputResolutionTimings {
1787 tool_identity,
1788 cargo_metadata,
1789 input_discovery,
1790 content_hashing,
1791 total: total_started.elapsed(),
1792 };
1793 let resolved_count = resolved_inputs
1794 .iter()
1795 .filter(|(_, result)| result.is_ok())
1796 .count();
1797 self.metrics.runs += usize::from(resolved_count > 0);
1798 self.metrics.reuses += resolved_count.saturating_sub(1);
1799 let timing_index = resolved_inputs
1800 .iter()
1801 .find(|(index, result)| *index == active_index && result.is_ok())
1802 .or_else(|| resolved_inputs.iter().find(|(_, result)| result.is_ok()))
1803 .map(|(index, _)| *index);
1804 for (index, result) in resolved_inputs {
1805 let (inputs, exclusions, input_digest, validation_digest, wasm_artifact_error) =
1806 match result {
1807 Ok(resolved) => resolved,
1808 Err(error) => {
1809 self.resolved[index] =
1810 Some(Err((WasmBuildFailurePhase::ContentHashing, error)));
1811 continue;
1812 }
1813 };
1814 let spec = self.specs[index];
1815 let resolved = ResolvedCargoBuildInputs {
1816 fingerprint: finish_build_fingerprint(
1817 spec,
1818 &cargo_identity,
1819 &rustc_identity,
1820 input_digest,
1821 ),
1822 input_digest,
1823 validation_digest,
1824 inputs: inputs
1825 .into_iter()
1826 .map(|(label, path)| CargoBuildInput { label, path })
1827 .collect(),
1828 exclusions: exclusions.into(),
1829 wasm_artifact_error,
1830 timings: if Some(index) == timing_index {
1831 timings
1832 } else {
1833 WasmInputResolutionTimings::default()
1834 },
1835 };
1836 if let Some(WasmBuildInputReuse::Session(session)) = self.reuse.as_mut() {
1837 session.remember(spec, &resolved);
1838 }
1839 self.resolved[index] = Some(Ok(resolved));
1840 }
1841 Ok(())
1842 }
1843
1844 fn discover_group_inputs(
1845 &mut self,
1846 group: usize,
1847 metadata: &Value,
1848 progress: &mut ProgressReporter<'_>,
1849 ) -> (Vec<(usize, ResolvedLocalInputs, Vec<PathBuf>)>, Duration) {
1850 let started = Instant::now();
1851 let pending = self.groups[group].indexes.iter().copied().filter(|index| {
1852 self.resolved[*index].is_none() && validate_spec(self.specs[*index]).is_ok()
1853 });
1854 let results = progress.run_phase(WasmBuildProgressPhase::InputDiscovery, || {
1855 let parsed = ParsedCargoMetadata::parse(metadata);
1856 pending
1857 .map(|index| {
1858 let spec = self.specs[index];
1859 let result = (|| {
1860 let parsed = parsed
1861 .as_ref()
1862 .map_err(|message| invalid_metadata(message))?;
1863 resolve_local_inputs(spec, parsed)
1864 })();
1865 (index, result)
1866 })
1867 .collect::<Vec<_>>()
1868 });
1869 let mut discovered = Vec::new();
1870 for (index, result) in results {
1871 match result {
1872 Ok((inputs, exclusions)) => discovered.push((index, inputs, exclusions)),
1873 Err(error) => {
1874 self.resolved[index] =
1875 Some(Err((WasmBuildFailurePhase::InputDiscovery, error)));
1876 }
1877 }
1878 }
1879 (discovered, started.elapsed())
1880 }
1881}
1882
1883fn resolve_tool_identity(
1884 spec: &WasmBuildSpec,
1885 progress: &mut ProgressReporter<'_>,
1886) -> Result<(Vec<u8>, Vec<u8>, Duration), WasmBuildError> {
1887 let started = Instant::now();
1888 let cargo_identity = progress.run_phase(WasmBuildProgressPhase::CargoIdentity, || {
1889 command_identity(
1890 spec,
1891 WasmBuildPhase::CargoIdentity,
1892 &spec.cargo_program,
1893 &["--version", "--verbose"],
1894 )
1895 })?;
1896 let rustc_program = spec
1897 .extra_env
1898 .get(OsStr::new("RUSTC"))
1899 .unwrap_or(&spec.rustc_program);
1900 let rustc_identity = progress.run_phase(WasmBuildProgressPhase::RustcIdentity, || {
1901 command_identity(spec, WasmBuildPhase::RustcIdentity, rustc_program, &["-vV"])
1902 })?;
1903 Ok((cargo_identity, rustc_identity, started.elapsed()))
1904}
1905
1906impl<'a> BatchResolutionKey<'a> {
1907 fn for_spec(spec: &'a WasmBuildSpec) -> Self {
1908 Self {
1909 workspace_root: &spec.workspace_root,
1910 cargo_program: &spec.cargo_program,
1911 rustc_program: spec
1912 .extra_env
1913 .get(OsStr::new("RUSTC"))
1914 .unwrap_or(&spec.rustc_program),
1915 metadata_arguments: metadata_arguments(&spec.cargo_profile_args),
1916 environment: effective_environment(spec),
1917 }
1918 }
1919}
1920
1921impl SharedIncrementalTargetInspection {
1922 #[must_use]
1924 pub fn target_dir(&self) -> &Path {
1925 &self.target_dir
1926 }
1927
1928 #[must_use]
1930 pub const fn logical_size_bytes(&self) -> u64 {
1931 self.logical_size_bytes
1932 }
1933
1934 #[must_use]
1936 pub const fn last_used(&self) -> SystemTime {
1937 self.last_used
1938 }
1939
1940 #[must_use]
1942 pub const fn lock_wait(&self) -> Duration {
1943 self.lock_wait
1944 }
1945}
1946
1947impl SharedIncrementalTargetPrunePolicy {
1948 #[must_use]
1950 pub const fn new() -> Self {
1951 Self {
1952 max_age: None,
1953 max_size_bytes: None,
1954 }
1955 }
1956
1957 #[must_use]
1959 pub const fn with_max_age(mut self, max_age: Duration) -> Self {
1960 self.max_age = Some(max_age);
1961 self
1962 }
1963
1964 #[must_use]
1966 pub const fn with_max_size_bytes(mut self, bytes: u64) -> Self {
1967 self.max_size_bytes = Some(bytes);
1968 self
1969 }
1970
1971 #[must_use]
1973 pub const fn max_age(self) -> Option<Duration> {
1974 self.max_age
1975 }
1976
1977 #[must_use]
1979 pub const fn max_size_bytes(self) -> Option<u64> {
1980 self.max_size_bytes
1981 }
1982
1983 fn maintenance_identity(self) -> String {
1984 format!(
1985 "age={:?};size={:?}",
1986 self.max_age.map(|duration| duration.as_nanos()),
1987 self.max_size_bytes
1988 )
1989 }
1990}
1991
1992impl SharedIncrementalTargetMaintenanceConfig {
1993 #[must_use]
1995 pub const fn new(
1996 policy: SharedIncrementalTargetPrunePolicy,
1997 minimum_interval: Duration,
1998 ) -> Self {
1999 Self {
2000 policy,
2001 minimum_interval,
2002 failure_mode: SharedIncrementalTargetMaintenanceFailureMode::Strict,
2003 }
2004 }
2005
2006 #[must_use]
2008 pub const fn with_failure_mode(
2009 mut self,
2010 failure_mode: SharedIncrementalTargetMaintenanceFailureMode,
2011 ) -> Self {
2012 self.failure_mode = failure_mode;
2013 self
2014 }
2015
2016 #[must_use]
2018 pub const fn policy(self) -> SharedIncrementalTargetPrunePolicy {
2019 self.policy
2020 }
2021
2022 #[must_use]
2024 pub const fn minimum_interval(self) -> Duration {
2025 self.minimum_interval
2026 }
2027
2028 #[must_use]
2030 pub const fn failure_mode(self) -> SharedIncrementalTargetMaintenanceFailureMode {
2031 self.failure_mode
2032 }
2033}
2034
2035impl SharedIncrementalTargetMaintenance {
2036 #[must_use]
2038 pub fn target_dir(&self) -> &Path {
2039 &self.target_dir
2040 }
2041
2042 #[must_use]
2044 pub const fn logical_size_bytes_before(&self) -> u64 {
2045 self.logical_size_bytes_before
2046 }
2047
2048 #[must_use]
2050 pub const fn logical_size_bytes_after(&self) -> u64 {
2051 self.logical_size_bytes_after
2052 }
2053
2054 #[must_use]
2056 pub const fn last_used_before(&self) -> SystemTime {
2057 self.last_used_before
2058 }
2059
2060 #[must_use]
2062 pub const fn was_cleared(&self) -> bool {
2063 self.cleared
2064 }
2065
2066 #[must_use]
2068 pub const fn lock_wait(&self) -> Duration {
2069 self.lock_wait
2070 }
2071
2072 #[must_use]
2074 pub const fn maintenance(&self) -> Duration {
2075 self.maintenance
2076 }
2077}
2078
2079impl std::fmt::Display for SharedIncrementalTargetMaintenance {
2080 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2081 write!(
2082 formatter,
2083 "target={} action={} bytes={}=>{} lock={:?} maintenance={:?}",
2084 self.target_dir.display(),
2085 if self.cleared { "cleared" } else { "retained" },
2086 self.logical_size_bytes_before,
2087 self.logical_size_bytes_after,
2088 self.lock_wait,
2089 self.maintenance,
2090 )
2091 }
2092}
2093
2094impl SharedIncrementalTargetMaintenanceOutcome {
2095 #[must_use]
2097 pub fn target_dir(&self) -> &Path {
2098 match self {
2099 Self::Missing { target_dir }
2100 | Self::Skipped { target_dir, .. }
2101 | Self::Failed { target_dir, .. } => target_dir,
2102 Self::Performed { maintenance, .. } => maintenance.target_dir(),
2103 }
2104 }
2105
2106 #[must_use]
2108 pub const fn maintenance(&self) -> Option<&SharedIncrementalTargetMaintenance> {
2109 match self {
2110 Self::Performed { maintenance, .. } => Some(maintenance),
2111 Self::Missing { .. } | Self::Skipped { .. } | Self::Failed { .. } => None,
2112 }
2113 }
2114
2115 #[must_use]
2117 pub const fn was_performed(&self) -> bool {
2118 matches!(self, Self::Performed { .. })
2119 }
2120
2121 #[must_use]
2123 pub const fn lock_wait(&self) -> Option<Duration> {
2124 match self {
2125 Self::Missing { .. } => None,
2126 Self::Skipped { lock_wait, .. } | Self::Failed { lock_wait, .. } => Some(*lock_wait),
2127 Self::Performed { maintenance, .. } => Some(maintenance.lock_wait()),
2128 }
2129 }
2130
2131 #[must_use]
2133 pub const fn schedule_check(&self) -> Option<Duration> {
2134 match self {
2135 Self::Missing { .. } | Self::Failed { .. } => None,
2136 Self::Skipped { schedule_check, .. } | Self::Performed { schedule_check, .. } => {
2137 Some(*schedule_check)
2138 }
2139 }
2140 }
2141
2142 #[must_use]
2144 pub fn failure_message(&self) -> Option<&str> {
2145 match self {
2146 Self::Failed { message, .. } => Some(message),
2147 Self::Missing { .. } | Self::Skipped { .. } | Self::Performed { .. } => None,
2148 }
2149 }
2150}
2151
2152impl std::fmt::Display for SharedIncrementalTargetMaintenanceOutcome {
2153 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2154 match self {
2155 Self::Missing { target_dir } => {
2156 write!(formatter, "target={} action=missing", target_dir.display())
2157 }
2158 Self::Skipped {
2159 target_dir,
2160 lock_wait,
2161 schedule_check,
2162 } => write!(
2163 formatter,
2164 "target={} action=skipped lock={lock_wait:?} schedule={schedule_check:?}",
2165 target_dir.display(),
2166 ),
2167 Self::Performed {
2168 maintenance,
2169 schedule_check,
2170 } => write!(formatter, "{maintenance} schedule={schedule_check:?}"),
2171 Self::Failed {
2172 target_dir,
2173 lock_wait,
2174 message,
2175 } => write!(
2176 formatter,
2177 "target={} action=failed lock={lock_wait:?} error={message}",
2178 target_dir.display(),
2179 ),
2180 }
2181 }
2182}
2183
2184impl std::fmt::Display for WasmBuildTimings {
2185 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2186 write!(
2187 formatter,
2188 "total={:?} lock={:?} shared_lock={:?} inputs={:?} cargo={:?} maintenance={:?}",
2189 self.total,
2190 self.lock_wait,
2191 self.shared_incremental_lock_wait,
2192 self.input_resolution.total,
2193 self.cargo_build,
2194 self.cache_maintenance,
2195 )
2196 }
2197}
2198
2199impl std::fmt::Display for WasmBuildOutcome {
2200 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2201 let state = if self.is_reused() { "reused" } else { "built" };
2202 write!(
2203 formatter,
2204 "{state} fingerprint={} artifacts={} {}",
2205 self.record().fingerprint,
2206 self.record().artifacts.len(),
2207 self.record().timings,
2208 )?;
2209 if let Some(maintenance) = self.record().shared_incremental_maintenance() {
2210 write!(formatter, " shared_maintenance=({maintenance})")?;
2211 }
2212 Ok(())
2213 }
2214}
2215
2216pub fn resolve_cargo_build_inputs(
2226 spec: &WasmBuildSpec,
2227) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2228 validate_spec(spec)?;
2229 build_fingerprint(spec)
2230}
2231
2232pub fn inspect_shared_incremental_target(
2237 spec: &WasmBuildSpec,
2238) -> Result<Option<SharedIncrementalTargetInspection>, WasmBuildError> {
2239 if !shared_incremental_target_exists(spec, "inspect shared incremental Cargo target")? {
2240 return Ok(None);
2241 }
2242
2243 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2244 let logical_size_bytes =
2245 directory_logical_size(&canonical).map_err(|source| WasmBuildError::Io {
2246 operation: "measure shared incremental Cargo target",
2247 path: canonical.clone(),
2248 source,
2249 })?;
2250 let last_used = cache_entry_last_used(&canonical).map_err(|source| WasmBuildError::Io {
2251 operation: "read shared incremental Cargo target use time",
2252 path: canonical.clone(),
2253 source,
2254 })?;
2255 Ok(Some(SharedIncrementalTargetInspection {
2256 target_dir: canonical,
2257 logical_size_bytes,
2258 last_used,
2259 lock_wait,
2260 }))
2261}
2262
2263pub fn maintain_shared_incremental_target(
2277 spec: &WasmBuildSpec,
2278 policy: SharedIncrementalTargetPrunePolicy,
2279) -> Result<Option<SharedIncrementalTargetMaintenance>, WasmBuildError> {
2280 if !shared_incremental_target_exists(
2281 spec,
2282 "inspect shared incremental Cargo target before maintenance",
2283 )? {
2284 return Ok(None);
2285 }
2286
2287 let _ = resolve_cargo_build_inputs(spec)?;
2291 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2292 maintain_shared_incremental_target_locked(&canonical, policy, lock_wait).map(Some)
2293}
2294
2295pub fn maintain_shared_incremental_target_at_most_every(
2309 spec: &WasmBuildSpec,
2310 policy: SharedIncrementalTargetPrunePolicy,
2311 minimum_interval: Duration,
2312) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2313 let target_dir =
2314 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
2315 message: "shared incremental target is not configured".to_owned(),
2316 })?;
2317 if !shared_incremental_target_exists(
2318 spec,
2319 "inspect shared incremental Cargo target before scheduled maintenance",
2320 )? {
2321 return Ok(SharedIncrementalTargetMaintenanceOutcome::Missing { target_dir });
2322 }
2323
2324 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2325 let schedule = schedule_shared_incremental_target_maintenance(
2326 &canonical,
2327 policy,
2328 minimum_interval,
2329 lock_wait,
2330 )?;
2331 let schedule = match schedule {
2332 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => return Ok(outcome),
2333 SharedIncrementalTargetMaintenanceSchedule::Due(due) => due,
2334 };
2335
2336 let _ = resolve_cargo_build_inputs(spec)?;
2339 perform_due_shared_incremental_target_maintenance(&canonical, policy, lock_wait, schedule)
2340}
2341
2342enum SharedIncrementalTargetMaintenanceSchedule {
2343 Skipped(SharedIncrementalTargetMaintenanceOutcome),
2344 Due(DueSharedIncrementalTargetMaintenance),
2345}
2346
2347struct DueSharedIncrementalTargetMaintenance {
2348 schedule_root: PathBuf,
2349 maintenance_identity: String,
2350 schedule_check: Duration,
2351}
2352
2353fn schedule_shared_incremental_target_maintenance(
2354 canonical: &Path,
2355 policy: SharedIncrementalTargetPrunePolicy,
2356 minimum_interval: Duration,
2357 lock_wait: Duration,
2358) -> Result<SharedIncrementalTargetMaintenanceSchedule, WasmBuildError> {
2359 let schedule_root = canonical.join(".ic-testkit");
2360 let maintenance_identity = policy.maintenance_identity();
2361 let schedule_started = Instant::now();
2362 let due = cache_maintenance_due(
2363 &schedule_root,
2364 Some(minimum_interval),
2365 &maintenance_identity,
2366 )
2367 .map_err(wasm_cache_fs_error)?;
2368 let schedule_check = schedule_started.elapsed();
2369 if !due {
2370 return Ok(SharedIncrementalTargetMaintenanceSchedule::Skipped(
2371 SharedIncrementalTargetMaintenanceOutcome::Skipped {
2372 target_dir: canonical.to_owned(),
2373 lock_wait,
2374 schedule_check,
2375 },
2376 ));
2377 }
2378 Ok(SharedIncrementalTargetMaintenanceSchedule::Due(
2379 DueSharedIncrementalTargetMaintenance {
2380 schedule_root,
2381 maintenance_identity,
2382 schedule_check,
2383 },
2384 ))
2385}
2386
2387fn perform_due_shared_incremental_target_maintenance(
2388 canonical: &Path,
2389 policy: SharedIncrementalTargetPrunePolicy,
2390 lock_wait: Duration,
2391 due: DueSharedIncrementalTargetMaintenance,
2392) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2393 let DueSharedIncrementalTargetMaintenance {
2394 schedule_root,
2395 maintenance_identity,
2396 schedule_check,
2397 } = due;
2398 let maintenance = maintain_shared_incremental_target_locked(canonical, policy, lock_wait)?;
2399 record_cache_maintenance(&schedule_root, &maintenance_identity).map_err(wasm_cache_fs_error)?;
2400 Ok(SharedIncrementalTargetMaintenanceOutcome::Performed {
2401 maintenance,
2402 schedule_check,
2403 })
2404}
2405
2406fn maintain_shared_incremental_target_locked(
2407 canonical: &Path,
2408 policy: SharedIncrementalTargetPrunePolicy,
2409 lock_wait: Duration,
2410) -> Result<SharedIncrementalTargetMaintenance, WasmBuildError> {
2411 let started = Instant::now();
2412 let logical_size_bytes_before =
2413 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
2414 operation: "measure shared incremental Cargo target before maintenance",
2415 path: canonical.to_owned(),
2416 source,
2417 })?;
2418 let last_used_before =
2419 cache_entry_last_used(canonical).map_err(|source| WasmBuildError::Io {
2420 operation: "read shared incremental Cargo target use time before maintenance",
2421 path: canonical.to_owned(),
2422 source,
2423 })?;
2424 let expired = policy.max_age.is_some_and(|max_age| {
2425 SystemTime::now()
2426 .duration_since(last_used_before)
2427 .is_ok_and(|age| age > max_age)
2428 });
2429 let oversized = policy
2430 .max_size_bytes
2431 .is_some_and(|max_size_bytes| logical_size_bytes_before > max_size_bytes);
2432 let cleared = expired || oversized;
2433 if cleared {
2434 clear_shared_incremental_target_contents(canonical)?;
2435 record_cache_entry_use(canonical)?;
2436 }
2437 let logical_size_bytes_after = if cleared {
2438 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
2439 operation: "measure shared incremental Cargo target after maintenance",
2440 path: canonical.to_owned(),
2441 source,
2442 })?
2443 } else {
2444 logical_size_bytes_before
2445 };
2446 Ok(SharedIncrementalTargetMaintenance {
2447 target_dir: canonical.to_owned(),
2448 logical_size_bytes_before,
2449 logical_size_bytes_after,
2450 last_used_before,
2451 cleared,
2452 lock_wait,
2453 maintenance: started.elapsed(),
2454 })
2455}
2456
2457fn clear_shared_incremental_target_contents(target_dir: &Path) -> Result<(), WasmBuildError> {
2458 let entries = fs::read_dir(target_dir).map_err(|source| WasmBuildError::Io {
2459 operation: "read shared incremental Cargo target for maintenance",
2460 path: target_dir.to_owned(),
2461 source,
2462 })?;
2463 for entry in entries {
2464 let path = entry
2465 .map_err(|source| WasmBuildError::Io {
2466 operation: "read shared incremental Cargo target entry for maintenance",
2467 path: target_dir.to_owned(),
2468 source,
2469 })?
2470 .path();
2471 let preserved = path
2472 .file_name()
2473 .is_some_and(|name| name == ".ic-testkit" || name == "CACHEDIR.TAG");
2474 if !preserved {
2475 remove_path_if_present(&path).map_err(|source| WasmBuildError::Io {
2476 operation: "clear shared incremental Cargo target entry",
2477 path,
2478 source,
2479 })?;
2480 }
2481 }
2482 Ok(())
2483}
2484
2485pub fn build_wasm_canisters_cached(
2503 spec: &WasmBuildSpec,
2504) -> Result<WasmBuildOutcome, WasmBuildError> {
2505 build_wasm_canisters_cached_internal(spec, &mut ProgressReporter::silent(), None)
2506}
2507
2508pub(super) fn build_wasm_canisters_cached_in_batch(
2509 spec: &WasmBuildSpec,
2510 index: usize,
2511 resolver: &mut WasmBuildBatchInputResolver<'_, '_>,
2512) -> WasmBuildBatchAttempt {
2513 let started = Instant::now();
2514 let mut progress = ProgressReporter::silent();
2515 let result = build_wasm_canisters_cached_internal(spec, &mut progress, Some((resolver, index)));
2516 if result
2517 .as_ref()
2518 .is_err_and(WasmBuildError::indicates_input_change)
2519 {
2520 resolver.invalidate_source_lease();
2521 }
2522 batch_result(result, &progress, started.elapsed())
2523}
2524
2525pub fn build_wasm_canisters_cached_with_progress<F>(
2538 spec: &WasmBuildSpec,
2539 config: WasmBuildProgressConfig,
2540 mut observer: F,
2541) -> Result<WasmBuildOutcome, WasmBuildError>
2542where
2543 F: FnMut(WasmBuildProgressEvent),
2544{
2545 if config.heartbeat_interval == Some(Duration::ZERO) {
2546 return Err(WasmBuildError::InvalidSpec {
2547 message: "Wasm build progress heartbeat interval must be greater than zero".to_owned(),
2548 });
2549 }
2550 build_wasm_canisters_cached_internal(
2551 spec,
2552 &mut ProgressReporter::observed(config, &mut observer),
2553 None,
2554 )
2555}
2556
2557pub(super) fn build_wasm_canisters_cached_in_batch_with_progress<F>(
2558 spec: &WasmBuildSpec,
2559 index: usize,
2560 resolver: &mut WasmBuildBatchInputResolver<'_, '_>,
2561 config: WasmBuildProgressConfig,
2562 mut observer: F,
2563) -> WasmBuildBatchAttempt
2564where
2565 F: FnMut(WasmBuildProgressEvent),
2566{
2567 if config.heartbeat_interval == Some(Duration::ZERO) {
2568 return WasmBuildBatchAttempt {
2569 result: Err((
2570 WasmBuildError::InvalidSpec {
2571 message: "Wasm build progress heartbeat interval must be greater than zero"
2572 .to_owned(),
2573 },
2574 WasmBuildFailureDetails::specification(Duration::ZERO),
2575 )),
2576 };
2577 }
2578 let started = Instant::now();
2579 let mut progress = ProgressReporter::observed(config, &mut observer);
2580 let result = build_wasm_canisters_cached_internal(spec, &mut progress, Some((resolver, index)));
2581 if result
2582 .as_ref()
2583 .is_err_and(WasmBuildError::indicates_input_change)
2584 {
2585 resolver.invalidate_source_lease();
2586 }
2587 batch_result(result, &progress, started.elapsed())
2588}
2589
2590fn batch_result(
2591 result: Result<WasmBuildOutcome, WasmBuildError>,
2592 progress: &ProgressReporter<'_>,
2593 total: Duration,
2594) -> WasmBuildBatchAttempt {
2595 WasmBuildBatchAttempt {
2596 result: result.map_err(|error| {
2597 let details = progress.failure_details(&error, total);
2598 (error, details)
2599 }),
2600 }
2601}
2602
2603fn batch_source_assumptions(
2604 batch_resolution: Option<&(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2605) -> (bool, Option<Arc<RwLock<bool>>>) {
2606 batch_resolution.map_or((false, None), |(resolver, _)| {
2607 (
2608 resolver.assumes_sources_immutable(),
2609 resolver.prepared_invalidation(),
2610 )
2611 })
2612}
2613
2614fn build_wasm_canisters_cached_internal(
2615 spec: &WasmBuildSpec,
2616 progress: &mut ProgressReporter<'_>,
2617 mut batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2618) -> Result<WasmBuildOutcome, WasmBuildError> {
2619 let total_started = Instant::now();
2620 validate_spec(spec)?;
2621 let (assumes_sources_immutable, prepared_invalidation) =
2622 batch_source_assumptions(batch_resolution.as_ref());
2623 progress.emit(WasmBuildProgressEvent::Started);
2624 if spec.shared_incremental_maintenance_config.is_some() {
2625 let outcome = build_wasm_canisters_cached_with_scheduled_shared_maintenance(
2626 spec,
2627 total_started,
2628 progress,
2629 batch_resolution.take(),
2630 )?;
2631 emit_finished_progress(&outcome, progress);
2632 return Ok(outcome);
2633 }
2634 let (cache_lock, first_lock_wait) =
2635 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2636 ensure_cache_directory_tag(&spec.target_dir)?;
2637
2638 let resolved = resolve_initial_inputs(spec, batch_resolution.take(), progress)?;
2639 let isolated_acquisition =
2640 WasmAcquisitionContext::isolated(prepared_invalidation.clone(), assumes_sources_immutable);
2641 if let Some(outcome) = try_reuse_wasm_artifacts(
2642 spec,
2643 &resolved,
2644 first_lock_wait,
2645 &isolated_acquisition,
2646 total_started,
2647 progress,
2648 )? {
2649 emit_finished_progress(&outcome, progress);
2650 return Ok(outcome);
2651 }
2652 progress.emit(WasmBuildProgressEvent::CacheMiss {
2653 fingerprint: resolved.fingerprint,
2654 });
2655
2656 let outcome = match &spec.cache_mode {
2657 WasmBuildCacheMode::Isolated => {
2658 let cache_entry = cache_entry_directory(spec, resolved.fingerprint);
2659 build_wasm_cache_miss(
2660 spec,
2661 resolved,
2662 first_lock_wait,
2663 isolated_acquisition,
2664 cache_entry,
2665 total_started,
2666 progress,
2667 )
2668 }
2669 WasmBuildCacheMode::SharedIncremental { .. } => {
2670 drop(cache_lock);
2671 build_wasm_with_shared_incremental(
2672 spec,
2673 resolved,
2674 first_lock_wait,
2675 assumes_sources_immutable,
2676 prepared_invalidation,
2677 total_started,
2678 progress,
2679 )
2680 }
2681 }?;
2682 emit_finished_progress(&outcome, progress);
2683 Ok(outcome)
2684}
2685
2686fn build_wasm_with_shared_incremental(
2687 spec: &WasmBuildSpec,
2688 resolved: ResolvedCargoBuildInputs,
2689 first_lock_wait: Duration,
2690 assumes_sources_immutable: bool,
2691 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2692 total_started: Instant,
2693 progress: &mut ProgressReporter<'_>,
2694) -> Result<WasmBuildOutcome, WasmBuildError> {
2695 let (shared_lock, shared_lock_wait, shared_target) =
2696 lock_shared_incremental_target_with_progress(spec, progress)?;
2697 let (_cache_lock, second_lock_wait) =
2698 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2699 ensure_cache_directory_tag(&spec.target_dir)?;
2700
2701 let current = if assumes_sources_immutable {
2702 resolved
2703 } else {
2704 let mut current = resolve_inputs_with_progress(spec, progress)?;
2705 current.timings.include(resolved.timings);
2706 current
2707 };
2708 let lock_wait = first_lock_wait.saturating_add(second_lock_wait);
2709 let shared_incremental = WasmAcquisitionContext::shared(
2710 shared_lock_wait,
2711 None,
2712 prepared_invalidation,
2713 assumes_sources_immutable,
2714 );
2715 if let Some(outcome) = try_reuse_wasm_artifacts(
2716 spec,
2717 ¤t,
2718 lock_wait,
2719 &shared_incremental,
2720 total_started,
2721 progress,
2722 )? {
2723 return Ok(outcome);
2724 }
2725
2726 let outcome = build_wasm_cache_miss(
2727 spec,
2728 current,
2729 lock_wait,
2730 shared_incremental,
2731 shared_target,
2732 total_started,
2733 progress,
2734 );
2735 drop(shared_lock);
2736 outcome
2737}
2738
2739fn build_wasm_canisters_cached_with_scheduled_shared_maintenance(
2740 spec: &WasmBuildSpec,
2741 total_started: Instant,
2742 progress: &mut ProgressReporter<'_>,
2743 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2744) -> Result<WasmBuildOutcome, WasmBuildError> {
2745 let (assumes_sources_immutable, prepared_invalidation) =
2746 batch_source_assumptions(batch_resolution.as_ref());
2747 let (_shared_lock, shared_lock_wait, shared_target) =
2748 lock_shared_incremental_target_with_progress(spec, progress)?;
2749 let (_cache_lock, lock_wait) = lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2750 ensure_cache_directory_tag(&spec.target_dir)?;
2751
2752 let resolved = resolve_initial_inputs(spec, batch_resolution, progress)?;
2755 let shared_maintenance = perform_configured_shared_incremental_target_maintenance(
2756 spec,
2757 &shared_target,
2758 shared_lock_wait,
2759 progress,
2760 )?;
2761 let shared_incremental = WasmAcquisitionContext::shared(
2762 shared_lock_wait,
2763 Some(shared_maintenance),
2764 prepared_invalidation,
2765 assumes_sources_immutable,
2766 );
2767 if let Some(outcome) = try_reuse_wasm_artifacts(
2768 spec,
2769 &resolved,
2770 lock_wait,
2771 &shared_incremental,
2772 total_started,
2773 progress,
2774 )? {
2775 return Ok(outcome);
2776 }
2777 progress.emit(WasmBuildProgressEvent::CacheMiss {
2778 fingerprint: resolved.fingerprint,
2779 });
2780 build_wasm_cache_miss(
2781 spec,
2782 resolved,
2783 lock_wait,
2784 shared_incremental,
2785 shared_target,
2786 total_started,
2787 progress,
2788 )
2789}
2790
2791fn perform_configured_shared_incremental_target_maintenance(
2792 spec: &WasmBuildSpec,
2793 shared_target: &Path,
2794 lock_wait: Duration,
2795 progress: &mut ProgressReporter<'_>,
2796) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2797 let config = spec
2798 .shared_incremental_maintenance_config
2799 .expect("configured shared-target maintenance must have settings");
2800 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceStarted {
2801 target_dir: shared_target.to_owned(),
2802 });
2803 let result = progress.run_phase(WasmBuildProgressPhase::SharedTargetMaintenance, || {
2804 let schedule = schedule_shared_incremental_target_maintenance(
2805 shared_target,
2806 config.policy,
2807 config.minimum_interval,
2808 lock_wait,
2809 )?;
2810 match schedule {
2811 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => Ok(outcome),
2812 SharedIncrementalTargetMaintenanceSchedule::Due(due) => {
2813 perform_due_shared_incremental_target_maintenance(
2814 shared_target,
2815 config.policy,
2816 lock_wait,
2817 due,
2818 )
2819 }
2820 }
2821 });
2822 let outcome = integrated_shared_maintenance_result(config, shared_target, lock_wait, result)?;
2823 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceFinished {
2824 outcome: outcome.clone(),
2825 });
2826 Ok(outcome)
2827}
2828
2829fn integrated_shared_maintenance_result(
2830 config: SharedIncrementalTargetMaintenanceConfig,
2831 shared_target: &Path,
2832 lock_wait: Duration,
2833 result: Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError>,
2834) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2835 match result {
2836 Ok(outcome) => Ok(outcome),
2837 Err(error)
2838 if config.failure_mode == SharedIncrementalTargetMaintenanceFailureMode::BestEffort =>
2839 {
2840 Ok(SharedIncrementalTargetMaintenanceOutcome::Failed {
2841 target_dir: shared_target.to_owned(),
2842 lock_wait,
2843 message: error.to_string(),
2844 })
2845 }
2846 Err(error) => Err(error),
2847 }
2848}
2849
2850fn resolve_inputs_with_progress(
2851 spec: &WasmBuildSpec,
2852 progress: &mut ProgressReporter<'_>,
2853) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2854 let resolved = build_fingerprint_with_progress(spec, progress)?;
2855 validate_wasm_library_outputs(&resolved, progress)?;
2856 progress.emit(WasmBuildProgressEvent::InputsResolved {
2857 fingerprint: resolved.fingerprint,
2858 input_digest: resolved.input_digest,
2859 elapsed: resolved.timings.total,
2860 });
2861 Ok(resolved)
2862}
2863
2864fn resolve_initial_inputs(
2865 spec: &WasmBuildSpec,
2866 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2867 progress: &mut ProgressReporter<'_>,
2868) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2869 let resolved = if let Some((resolver, index)) = batch_resolution {
2870 resolver.resolve(index, progress)?
2871 } else {
2872 build_fingerprint_with_progress(spec, progress)?
2873 };
2874 validate_wasm_library_outputs(&resolved, progress)?;
2875 progress.emit(WasmBuildProgressEvent::InputsResolved {
2876 fingerprint: resolved.fingerprint,
2877 input_digest: resolved.input_digest,
2878 elapsed: resolved.timings.total,
2879 });
2880 Ok(resolved)
2881}
2882
2883fn validate_wasm_library_outputs(
2884 resolved: &ResolvedCargoBuildInputs,
2885 progress: &mut ProgressReporter<'_>,
2886) -> Result<(), WasmBuildError> {
2887 if let Some(message) = &resolved.wasm_artifact_error {
2888 progress.begin_phase(WasmBuildFailurePhase::InputDiscovery);
2889 return Err(WasmBuildError::InvalidSpec {
2890 message: message.clone(),
2891 });
2892 }
2893 Ok(())
2894}
2895
2896fn emit_finished_progress(outcome: &WasmBuildOutcome, progress: &mut ProgressReporter<'_>) {
2897 let state = if outcome.is_reused() {
2898 progress.emit(WasmBuildProgressEvent::CacheHit {
2899 fingerprint: outcome.record().fingerprint,
2900 });
2901 WasmBuildProgressOutcome::Reused
2902 } else {
2903 WasmBuildProgressOutcome::Built
2904 };
2905 progress.emit(WasmBuildProgressEvent::Finished {
2906 outcome: state,
2907 fingerprint: outcome.record().fingerprint,
2908 elapsed: outcome.record().timings.total,
2909 });
2910}
2911
2912#[derive(Clone, Debug, Default)]
2913struct WasmAcquisitionContext {
2914 assumes_sources_immutable: bool,
2915 lock_wait: Option<Duration>,
2916 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
2917 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2918}
2919
2920impl WasmAcquisitionContext {
2921 fn isolated(
2922 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2923 assumes_sources_immutable: bool,
2924 ) -> Self {
2925 Self {
2926 assumes_sources_immutable,
2927 prepared_invalidation,
2928 ..Self::default()
2929 }
2930 }
2931
2932 const fn shared(
2933 lock_wait: Duration,
2934 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
2935 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2936 assumes_sources_immutable: bool,
2937 ) -> Self {
2938 Self {
2939 assumes_sources_immutable,
2940 lock_wait: Some(lock_wait),
2941 maintenance,
2942 prepared_invalidation,
2943 }
2944 }
2945
2946 fn lock_prepared_publication(
2947 &self,
2948 ) -> Result<Option<std::sync::RwLockReadGuard<'_, bool>>, WasmBuildError> {
2949 let guard = self.prepared_invalidation.as_deref().map(|invalidation| {
2950 invalidation
2951 .read()
2952 .unwrap_or_else(std::sync::PoisonError::into_inner)
2953 });
2954 if guard.as_deref().is_some_and(|invalidated| *invalidated) {
2955 return Err(WasmBuildError::PreparedInputSnapshotInvalidated);
2956 }
2957 Ok(guard)
2958 }
2959}
2960
2961fn try_reuse_wasm_artifacts(
2962 spec: &WasmBuildSpec,
2963 resolved: &ResolvedCargoBuildInputs,
2964 lock_wait: Duration,
2965 shared_incremental: &WasmAcquisitionContext,
2966 total_started: Instant,
2967 progress: &mut ProgressReporter<'_>,
2968) -> Result<Option<WasmBuildOutcome>, WasmBuildError> {
2969 let _publication_guard = shared_incremental.lock_prepared_publication()?;
2970 let fingerprint = resolved.fingerprint;
2971 let artifacts = expected_artifacts(spec, &spec.target_dir);
2972 let cache_entry = cache_entry_directory(spec, fingerprint);
2973 let cached_artifacts = expected_artifacts(spec, &cache_entry);
2974 let cached_info = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2975 validated_artifact_set(&cached_artifacts, fingerprint)
2976 });
2977 let artifact_info = if let Some(info) = cached_info {
2978 info
2979 } else {
2980 let public_is_current = progress
2983 .run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2984 validated_artifact_set(&artifacts, fingerprint).is_some()
2985 });
2986 if !public_is_current {
2987 return Ok(None);
2988 }
2989 reconstruct_exact_cache_entry(spec, &artifacts, &cache_entry, fingerprint, progress)?
2990 };
2991 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
2992 materialize_artifacts(
2993 &cached_artifacts,
2994 &artifacts,
2995 fingerprint,
2996 &artifact_info,
2997 true,
2998 )?;
2999 record_cache_entry_use(&cache_entry)
3000 })?;
3001 let input_resolution = validate_reused_inputs(spec, resolved, shared_incremental, progress)?;
3002 Ok(Some(WasmBuildOutcome::Reused(complete_build_record(
3003 spec,
3004 BuildRecordInput {
3005 fingerprint,
3006 input_digest: resolved.input_digest,
3007 lock_wait,
3008 shared_incremental: shared_incremental.clone(),
3009 input_resolution,
3010 cargo_build: None,
3011 active_entry: &cache_entry,
3012 },
3013 total_started,
3014 progress,
3015 )?)))
3016}
3017
3018fn validate_reused_inputs(
3019 spec: &WasmBuildSpec,
3020 resolved: &ResolvedCargoBuildInputs,
3021 context: &WasmAcquisitionContext,
3022 progress: &mut ProgressReporter<'_>,
3023) -> Result<WasmInputResolutionTimings, WasmBuildError> {
3024 let mut timings = resolved.timings;
3025 if !context.assumes_sources_immutable {
3026 let verified = verify_resolved_inputs(spec, resolved, progress)?;
3027 timings.include(verified.timings);
3028 }
3029 Ok(timings)
3030}
3031
3032fn verify_resolved_inputs(
3033 spec: &WasmBuildSpec,
3034 resolved: &ResolvedCargoBuildInputs,
3035 progress: &mut ProgressReporter<'_>,
3036) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3037 let verified = resolve_inputs_with_progress(spec, progress)?;
3038 for (before, after) in [
3039 (resolved.validation_digest, verified.validation_digest),
3040 (resolved.fingerprint, verified.fingerprint),
3041 ] {
3042 if before != after {
3043 return Err(WasmBuildError::InputsChangedDuringAcquisition { before, after });
3044 }
3045 }
3046 Ok(verified)
3047}
3048
3049fn reconstruct_exact_cache_entry(
3050 spec: &WasmBuildSpec,
3051 artifacts: &[PathBuf],
3052 cache_entry: &Path,
3053 fingerprint: InputDigest,
3054 progress: &mut ProgressReporter<'_>,
3055) -> Result<Vec<FileDigest>, WasmBuildError> {
3056 let cached_artifacts = expected_artifacts(spec, cache_entry);
3057 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3058 remove_unretained_entry(cache_entry).map_err(wasm_cache_fs_error)?;
3059 create_dir_all(
3060 cache_entry,
3061 "create content-addressed Cargo target directory",
3062 )
3063 })?;
3064 let incomplete = IncompleteBuildDirectory::new(cache_entry.to_owned());
3065 let result = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3066 copy_wasm_artifacts(artifacts, &cached_artifacts)?;
3067 let missing = missing_artifacts(&cached_artifacts);
3068 if !missing.is_empty() {
3069 return Err(WasmBuildError::MissingArtifacts { paths: missing });
3070 }
3071 publish_artifact_stamps(&cached_artifacts, fingerprint)
3074 });
3075 finish_fingerprint_build(result, incomplete, progress)
3076}
3077
3078fn build_wasm_cache_miss(
3079 spec: &WasmBuildSpec,
3080 resolved: ResolvedCargoBuildInputs,
3081 lock_wait: Duration,
3082 shared_incremental: WasmAcquisitionContext,
3083 cargo_target_dir: PathBuf,
3084 total_started: Instant,
3085 progress: &mut ProgressReporter<'_>,
3086) -> Result<WasmBuildOutcome, WasmBuildError> {
3087 let fingerprint = resolved.fingerprint;
3088 let mut input_resolution = resolved.timings;
3089 let artifacts = expected_artifacts(spec, &spec.target_dir);
3090 let cache_entry = cache_entry_directory(spec, fingerprint);
3091 let preparation_started = Instant::now();
3092 progress.begin_phase(WasmBuildFailurePhase::ArtifactPublication);
3093 let preparation_result = (|| {
3094 remove_unretained_entry(&cache_entry).map_err(wasm_cache_fs_error)?;
3095 create_dir_all(
3096 &cache_entry,
3097 "create content-addressed Cargo target directory",
3098 )
3099 })();
3100 progress.record_phase(
3101 WasmBuildFailurePhase::ArtifactPublication,
3102 preparation_started.elapsed(),
3103 );
3104 preparation_result?;
3105 let incomplete_directory = IncompleteBuildDirectory::new(cache_entry.clone());
3106 let build_result = (|| {
3107 if matches!(
3108 spec.cache_mode,
3109 WasmBuildCacheMode::SharedIncremental { .. }
3110 ) {
3111 record_cache_entry_use(&cargo_target_dir)?;
3112 }
3113 let build_started = Instant::now();
3114 progress.begin_phase(WasmBuildFailurePhase::CargoBuild);
3115 let cargo_result = run_cargo_build(spec, &cargo_target_dir, progress);
3116 let cargo_build = build_started.elapsed();
3117 progress.record_phase(WasmBuildFailurePhase::CargoBuild, cargo_build);
3118 cargo_result?;
3119 let built_artifacts = expected_artifacts(spec, &cargo_target_dir);
3120 let validation_started = Instant::now();
3121 progress.begin_phase(WasmBuildFailurePhase::ArtifactPublication);
3122 let missing = missing_artifacts(&built_artifacts);
3123 progress.record_phase(
3124 WasmBuildFailurePhase::ArtifactPublication,
3125 validation_started.elapsed(),
3126 );
3127 if !missing.is_empty() {
3128 return Err(WasmBuildError::MissingArtifacts { paths: missing });
3129 }
3130
3131 let verified = verify_resolved_inputs(spec, &resolved, progress)?;
3132 input_resolution.include(verified.timings);
3133
3134 let publication_guard = shared_incremental.lock_prepared_publication()?;
3139
3140 let cached_artifacts = expected_artifacts(spec, &cache_entry);
3141 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3142 if cargo_target_dir != cache_entry {
3143 copy_wasm_artifacts(&built_artifacts, &cached_artifacts)?;
3144 }
3145 let info = publish_artifact_stamps(&cached_artifacts, fingerprint)?;
3146 materialize_artifacts(&cached_artifacts, &artifacts, fingerprint, &info, false)?;
3147 record_cache_entry_use(&cache_entry)
3148 })?;
3149 drop(publication_guard);
3150
3151 Ok(WasmBuildOutcome::Built(complete_build_record(
3152 spec,
3153 BuildRecordInput {
3154 fingerprint,
3155 input_digest: resolved.input_digest,
3156 lock_wait,
3157 shared_incremental,
3158 input_resolution,
3159 cargo_build: Some(cargo_build),
3160 active_entry: &cache_entry,
3161 },
3162 total_started,
3163 progress,
3164 )?))
3165 })();
3166 finish_fingerprint_build(build_result, incomplete_directory, progress)
3167}
3168
3169pub fn prune_wasm_build_cache(
3178 target_dir: &Path,
3179 policy: ArtifactCachePrunePolicy,
3180) -> Result<ArtifactCachePruneReport, WasmBuildError> {
3181 let (_lock_file, _) = lock_wasm_build_cache(target_dir)?;
3182 ensure_cache_directory_tag(target_dir)?;
3183
3184 prune_wasm_build_cache_locked(target_dir, policy, None)
3185}
3186
3187struct BuildRecordInput<'a> {
3188 fingerprint: InputDigest,
3189 input_digest: InputDigest,
3190 lock_wait: Duration,
3191 shared_incremental: WasmAcquisitionContext,
3192 input_resolution: WasmInputResolutionTimings,
3193 cargo_build: Option<Duration>,
3194 active_entry: &'a Path,
3195}
3196
3197fn complete_build_record(
3198 spec: &WasmBuildSpec,
3199 input: BuildRecordInput<'_>,
3200 total_started: Instant,
3201 progress: &mut ProgressReporter<'_>,
3202) -> Result<WasmBuildRecord, WasmBuildError> {
3203 let retention = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3204 RetainedCacheEntry::acquire(input.active_entry).map_err(wasm_cache_fs_error)
3205 })?;
3206 let (maintenance, cache_maintenance) = spec.prune_policy.map_or((None, None), |policy| {
3207 progress.run_phase(WasmBuildProgressPhase::ExactCacheMaintenance, || {
3208 let cache_root = spec.target_dir.join(".ic-testkit/wasm-targets");
3209 let identity = policy.maintenance_identity();
3210 perform_scheduled_cache_maintenance(&cache_root, spec.prune_interval, &identity, || {
3211 prune_wasm_build_cache_locked(&spec.target_dir, policy, Some(input.active_entry))
3212 .map_err(|error| error.to_string())
3213 })
3214 })
3215 });
3216 Ok(WasmBuildRecord {
3217 fingerprint: input.fingerprint,
3218 input_digest: input.input_digest,
3219 artifacts: expected_artifacts(spec, input.active_entry),
3220 retention,
3221 timings: WasmBuildTimings {
3222 lock_wait: input.lock_wait,
3223 shared_incremental_lock_wait: input.shared_incremental.lock_wait,
3224 input_resolution: input.input_resolution,
3225 cargo_build: input.cargo_build,
3226 cache_maintenance,
3227 total: total_started.elapsed(),
3228 },
3229 maintenance,
3230 shared_incremental_maintenance: input.shared_incremental.maintenance,
3231 })
3232}
3233
3234fn prune_wasm_build_cache_locked(
3235 target_dir: &Path,
3236 policy: ArtifactCachePrunePolicy,
3237 protected_entry: Option<&Path>,
3238) -> Result<ArtifactCachePruneReport, WasmBuildError> {
3239 let cache_root = target_dir.join(".ic-testkit/wasm-targets");
3240 prune_direct_child_directories(&cache_root, policy, protected_entry, is_sha256_directory)
3241 .map_err(wasm_cache_fs_error)
3242}
3243
3244struct IncompleteBuildDirectory {
3245 path: PathBuf,
3246 armed: bool,
3247}
3248
3249impl IncompleteBuildDirectory {
3250 const fn new(path: PathBuf) -> Self {
3251 Self { path, armed: true }
3252 }
3253
3254 fn preserve(mut self) {
3255 self.armed = false;
3256 }
3257
3258 fn cleanup(mut self) -> io::Result<()> {
3259 let result = remove_path_if_present(&self.path);
3260 if result.is_ok() {
3261 self.armed = false;
3262 }
3263 result
3264 }
3265}
3266
3267impl Drop for IncompleteBuildDirectory {
3268 fn drop(&mut self) {
3269 if self.armed {
3270 let _ = remove_path_if_present(&self.path);
3271 }
3272 }
3273}
3274
3275fn finish_fingerprint_build<T>(
3276 result: Result<T, WasmBuildError>,
3277 incomplete_directory: IncompleteBuildDirectory,
3278 progress: &mut ProgressReporter<'_>,
3279) -> Result<T, WasmBuildError> {
3280 match result {
3281 Ok(outcome) => {
3282 incomplete_directory.preserve();
3283 Ok(outcome)
3284 }
3285 Err(build_error) => Err(cleanup_failed_fingerprint_build(
3286 build_error,
3287 incomplete_directory,
3288 progress,
3289 )),
3290 }
3291}
3292
3293fn cleanup_failed_fingerprint_build(
3294 build_error: WasmBuildError,
3295 incomplete_directory: IncompleteBuildDirectory,
3296 progress: &mut ProgressReporter<'_>,
3297) -> WasmBuildError {
3298 let path = incomplete_directory.path.clone();
3299 let primary_phase = progress.failure_phase;
3300 let cleanup_started = Instant::now();
3301 let cleanup = incomplete_directory.cleanup();
3302 progress.record_phase(WasmBuildFailurePhase::Cleanup, cleanup_started.elapsed());
3303 match cleanup {
3304 Ok(()) => {
3305 progress.failure_phase = primary_phase;
3306 build_error
3307 }
3308 Err(source) => WasmBuildError::FailedBuildCleanup {
3309 build_error: Box::new(build_error),
3310 path,
3311 source,
3312 },
3313 }
3314}
3315
3316fn lock_wasm_build_cache(target_dir: &Path) -> Result<(File, Duration), WasmBuildError> {
3317 create_dir_all(target_dir, "create Cargo target directory")?;
3318 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
3319 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)
3320}
3321
3322fn lock_wasm_build_cache_with_progress(
3323 target_dir: &Path,
3324 progress: &mut ProgressReporter<'_>,
3325) -> Result<(File, Duration), WasmBuildError> {
3326 progress.begin_phase(WasmBuildFailurePhase::ExactCacheCoordination);
3327 create_dir_all(target_dir, "create Cargo target directory")?;
3328 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
3329 lock_cache_file_with_progress(&lock_path, WasmBuildProgressPhase::ExactCacheLock, progress)
3330}
3331
3332fn lock_shared_incremental_target(
3333 spec: &WasmBuildSpec,
3334) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3335 lock_shared_incremental_target_internal(spec, None)
3336}
3337
3338fn lock_shared_incremental_target_with_progress(
3339 spec: &WasmBuildSpec,
3340 progress: &mut ProgressReporter<'_>,
3341) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3342 let target_dir = shared_incremental_target(spec)
3343 .expect("validated shared acquisition must have a shared Cargo target");
3344 progress.emit(WasmBuildProgressEvent::SharedTargetLockStarted { target_dir });
3345 let (lock, wait, canonical) = lock_shared_incremental_target_internal(spec, Some(progress))?;
3346 progress.emit(WasmBuildProgressEvent::SharedTargetLockAcquired {
3347 target_dir: canonical.clone(),
3348 wait,
3349 });
3350 Ok((lock, wait, canonical))
3351}
3352
3353fn lock_shared_incremental_target_internal(
3354 spec: &WasmBuildSpec,
3355 mut progress: Option<&mut ProgressReporter<'_>>,
3356) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3357 if let Some(progress) = progress.as_deref_mut() {
3358 progress.begin_phase(WasmBuildFailurePhase::SharedTargetCoordination);
3359 }
3360 let target_dir =
3361 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
3362 message: "shared incremental target is not configured".to_owned(),
3363 })?;
3364 create_dir_all(
3365 &target_dir,
3366 "create shared incremental Cargo target directory",
3367 )?;
3368 ensure_cache_tag(&target_dir).map_err(wasm_cache_fs_error)?;
3369 let canonical = target_dir
3370 .canonicalize()
3371 .map_err(|source| WasmBuildError::Io {
3372 operation: "resolve shared incremental Cargo target directory",
3373 path: target_dir.clone(),
3374 source,
3375 })?;
3376 let lock_path = canonical.join(".ic-testkit/wasm-incremental.lock");
3377 let (lock, wait) = if let Some(progress) = progress {
3378 lock_cache_file_with_progress(
3379 &lock_path,
3380 WasmBuildProgressPhase::SharedTargetLock,
3381 progress,
3382 )?
3383 } else {
3384 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)?
3385 };
3386 Ok((lock, wait, canonical))
3387}
3388
3389fn lock_cache_file_with_progress(
3390 lock_path: &Path,
3391 phase: WasmBuildProgressPhase,
3392 progress: &mut ProgressReporter<'_>,
3393) -> Result<(File, Duration), WasmBuildError> {
3394 let failure_phase = progress_failure_phase(phase);
3395 let started = Instant::now();
3396 progress.begin_phase(failure_phase);
3397 let result = if !progress.is_observed() || progress.config.heartbeat_interval.is_none() {
3398 lock_cache_file(lock_path).map_err(wasm_cache_fs_error)
3399 } else {
3400 let heartbeat_interval = progress
3401 .config
3402 .heartbeat_interval
3403 .expect("observed cache lock must have a heartbeat interval");
3404 lock_cache_file_with_wait_observer(lock_path, heartbeat_interval, |elapsed| {
3405 progress.emit_heartbeat_if_due(phase, elapsed);
3406 })
3407 .map_err(wasm_cache_fs_error)
3408 };
3409 progress.record_phase(failure_phase, started.elapsed());
3410 result
3411}
3412
3413fn ensure_cache_directory_tag(target_dir: &Path) -> Result<(), WasmBuildError> {
3414 ensure_cache_tag(target_dir).map_err(wasm_cache_fs_error)
3415}
3416
3417fn record_cache_entry_use(path: &Path) -> Result<(), WasmBuildError> {
3418 record_entry_use(path).map_err(wasm_cache_fs_error)
3419}
3420
3421fn wasm_cache_fs_error(error: CacheFsError) -> WasmBuildError {
3422 WasmBuildError::Io {
3423 operation: error.operation,
3424 path: error.path,
3425 source: error.source,
3426 }
3427}
3428
3429fn validate_spec(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3430 if spec.packages.is_empty() {
3431 return Err(WasmBuildError::InvalidSpec {
3432 message: "at least one Cargo package is required".to_owned(),
3433 });
3434 }
3435 let mut profile_components = Path::new(&spec.profile_target_dir).components();
3436 if !matches!(
3437 (profile_components.next(), profile_components.next()),
3438 (Some(Component::Normal(_)), None)
3439 ) {
3440 return Err(WasmBuildError::InvalidSpec {
3441 message: "Cargo profile target directory must be one normal path component".to_owned(),
3442 });
3443 }
3444 if spec.target.is_empty() {
3445 return Err(WasmBuildError::InvalidSpec {
3446 message: "Cargo compilation target must not be empty".to_owned(),
3447 });
3448 }
3449 if spec.cargo_profile_args.iter().any(|argument| {
3450 matches!(argument.to_str(), Some("--help" | "--unit-graph"))
3451 || matches!(short_cargo_option(argument), Some((b'h', _)))
3452 }) {
3453 return Err(WasmBuildError::InvalidSpec {
3454 message:
3455 "Cargo help and build-graph flags exit without building and cannot be used for Wasm acquisition"
3456 .to_owned(),
3457 });
3458 }
3459 if spec.extra_env.contains_key(OsStr::new("CARGO_TARGET_DIR"))
3460 || spec.cargo_profile_args.iter().any(|argument| {
3461 argument == OsStr::new("--target-dir")
3462 || argument.as_encoded_bytes().starts_with(b"--target-dir=")
3463 })
3464 {
3465 return Err(WasmBuildError::InvalidSpec {
3466 message: "Cargo target directories are owned by the build specification; use target_dir or with_shared_incremental_target instead of command overrides".to_owned(),
3467 });
3468 }
3469 validate_cargo_target_selection(spec)?;
3470 if spec.cargo_profile_args.iter().any(|argument| {
3471 let option = argument
3472 .as_encoded_bytes()
3473 .split(|byte| *byte == b'=')
3474 .next()
3475 .unwrap_or_default();
3476 matches!(
3477 option,
3478 b"--manifest-path"
3479 | b"--target"
3480 | b"--package"
3481 | b"--workspace"
3482 | b"--all"
3483 | b"--exclude"
3484 | b"--config"
3485 ) || matches!(short_cargo_option(argument), Some((b'm' | b'p' | b'C', _)))
3486 }) {
3487 return Err(WasmBuildError::InvalidSpec {
3488 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(),
3489 });
3490 }
3491 validate_cargo_profile(spec)?;
3492 if matches!(
3493 &spec.cache_mode,
3494 WasmBuildCacheMode::SharedIncremental { target_dir } if target_dir.as_os_str().is_empty()
3495 ) {
3496 return Err(WasmBuildError::InvalidSpec {
3497 message: "shared incremental Cargo target directory must not be empty".to_owned(),
3498 });
3499 }
3500 if spec.shared_incremental_maintenance_config.is_some()
3501 && !matches!(
3502 spec.cache_mode,
3503 WasmBuildCacheMode::SharedIncremental { .. }
3504 )
3505 {
3506 return Err(WasmBuildError::InvalidSpec {
3507 message:
3508 "scheduled shared-target maintenance requires a shared incremental Cargo target"
3509 .to_owned(),
3510 });
3511 }
3512 Ok(())
3513}
3514
3515fn validate_cargo_target_selection(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3516 if spec.cargo_profile_args.iter().any(|argument| {
3517 let option = argument
3518 .as_encoded_bytes()
3519 .split(|byte| *byte == b'=')
3520 .next()
3521 .unwrap_or_default();
3522 matches!(
3523 option,
3524 b"--bin"
3525 | b"--bins"
3526 | b"--example"
3527 | b"--examples"
3528 | b"--test"
3529 | b"--tests"
3530 | b"--bench"
3531 | b"--benches"
3532 | b"--all-targets"
3533 )
3534 }) {
3535 return Err(WasmBuildError::InvalidSpec {
3536 message: "Wasm acquisition builds canister libraries only; remove other Cargo target selectors".to_owned(),
3537 });
3538 }
3539 Ok(())
3540}
3541
3542fn validate_cargo_profile(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3543 let mut selected = None;
3544 let mut arguments = spec.cargo_profile_args.iter();
3545 while let Some(argument) = arguments.next() {
3546 let profile = if argument == "--profile" {
3547 Some(
3548 arguments
3549 .next()
3550 .and_then(|value| value.to_str())
3551 .ok_or_else(|| WasmBuildError::InvalidSpec {
3552 message: "Cargo --profile requires a UTF-8 profile name".to_owned(),
3553 })?,
3554 )
3555 } else if let Some(profile) = argument.to_str().and_then(|s| s.strip_prefix("--profile=")) {
3556 Some(profile)
3557 } else {
3558 let bytes = argument.as_encoded_bytes();
3559 let short_option = short_cargo_option(argument);
3562 let flags_end = short_option.map_or(bytes.len(), |(_, index)| index);
3563 let release = argument == "--release"
3564 || (bytes.starts_with(b"-")
3565 && !bytes.starts_with(b"--")
3566 && bytes[..flags_end].contains(&b'r'));
3567 if matches!(short_option, Some((_, index)) if index + 1 == bytes.len()) {
3568 arguments.next();
3569 }
3570 release.then_some("release")
3571 };
3572 if let Some(profile) = profile
3573 && (profile.is_empty() || selected.replace(profile).is_some())
3574 {
3575 return Err(WasmBuildError::InvalidSpec {
3576 message: "select one nonempty Cargo profile".to_owned(),
3577 });
3578 }
3579 }
3580 let profile = selected.unwrap_or("dev");
3581 let expected = match profile {
3582 "dev" | "test" => "debug",
3583 "bench" => "release",
3584 custom => custom,
3585 };
3586 if spec.profile_target_dir != expected {
3587 return Err(WasmBuildError::InvalidSpec {
3588 message: format!(
3589 "Cargo profile {profile:?} writes to {expected:?}, but profile target directory is {:?}; select matching Cargo profile arguments and output directory",
3590 spec.profile_target_dir,
3591 ),
3592 });
3593 }
3594 Ok(())
3595}
3596
3597fn build_fingerprint(spec: &WasmBuildSpec) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3598 build_fingerprint_with_progress(spec, &mut ProgressReporter::silent())
3599}
3600
3601fn build_fingerprint_with_progress(
3602 spec: &WasmBuildSpec,
3603 progress: &mut ProgressReporter<'_>,
3604) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3605 let total_started = Instant::now();
3606 let (cargo_identity, rustc_identity, tool_identity) = resolve_tool_identity(spec, progress)?;
3607
3608 let metadata_started = Instant::now();
3609 let metadata = progress.run_phase(WasmBuildProgressPhase::CargoMetadata, || {
3610 cargo_metadata(spec)
3611 })?;
3612 let cargo_metadata = metadata_started.elapsed();
3613
3614 let discovery_started = Instant::now();
3615 let (inputs, exclusions) =
3616 progress.run_phase(WasmBuildProgressPhase::InputDiscovery, || {
3617 let parsed = ParsedCargoMetadata::parse(&metadata)
3618 .map_err(|message| invalid_metadata(&message))?;
3619 resolve_local_inputs(spec, &parsed)
3620 })?;
3621 let input_discovery = discovery_started.elapsed();
3622
3623 let hashing_started = Instant::now();
3624 let (input_digest, validation_digest) =
3625 progress.run_phase(WasmBuildProgressPhase::ContentHashing, || {
3626 let mut cache = LabeledPathDigestCache::default();
3627 digest_resolved_local_inputs(
3628 &inputs,
3629 &exclusions,
3630 &mut cache,
3631 &spec.workspace_root,
3632 "hash Wasm build inputs",
3633 "hash semantic Wasm build inputs",
3634 )
3635 })?;
3636 let content_hashing = hashing_started.elapsed();
3637
3638 let fingerprint =
3639 finish_build_fingerprint(spec, &cargo_identity, &rustc_identity, input_digest);
3640 Ok(ResolvedCargoBuildInputs {
3641 fingerprint,
3642 input_digest,
3643 validation_digest,
3644 inputs: inputs
3645 .validation_inputs
3646 .into_iter()
3647 .map(|(label, path)| CargoBuildInput { label, path })
3648 .collect(),
3649 exclusions: exclusions.into(),
3650 wasm_artifact_error: inputs.wasm_artifact_error,
3651 timings: WasmInputResolutionTimings {
3652 tool_identity,
3653 cargo_metadata,
3654 input_discovery,
3655 content_hashing,
3656 total: total_started.elapsed(),
3657 },
3658 })
3659}
3660
3661fn finish_build_fingerprint(
3662 spec: &WasmBuildSpec,
3663 cargo_identity: &[u8],
3664 rustc_identity: &[u8],
3665 input_digest: InputDigest,
3666) -> InputDigest {
3667 let mut hasher = InputHasher::new(CACHE_FORMAT_VERSION);
3668 for package in &spec.packages {
3669 hasher.field("package", package.as_bytes());
3670 }
3671 hasher.field("target", spec.target.as_bytes());
3672 hasher.field("cargo-target-selection", b"lib");
3673 hasher.field("profile-target-dir", spec.profile_target_dir.as_bytes());
3674 for argument in &spec.cargo_profile_args {
3675 hasher.field("cargo-argument", &os_bytes(argument));
3676 }
3677 for (key, value) in effective_environment(spec) {
3678 hasher.field("environment-key", &os_bytes(&key));
3679 if let Some(value) = value {
3680 hasher.field("environment-value", &os_bytes(&value));
3681 } else {
3682 hasher.field("environment-unset", b"");
3683 }
3684 }
3685 hasher.field("cargo-identity", cargo_identity);
3686 hasher.field("rustc-identity", rustc_identity);
3687 hasher.field("source-input-digest", input_digest.as_bytes());
3688 hasher.finish()
3689}
3690
3691fn command_identity(
3692 spec: &WasmBuildSpec,
3693 phase: WasmBuildPhase,
3694 program: &OsStr,
3695 arguments: &[&str],
3696) -> Result<Vec<u8>, WasmBuildError> {
3697 let mut command = Command::new(program);
3698 command.current_dir(&spec.workspace_root).args(arguments);
3699 apply_command_environment(&mut command, spec);
3700 let output = capture_probe_output(&mut command, phase, TOOL_IDENTITY_BYTES)?;
3701 ensure_command_success(phase, output).map(|output| {
3702 let mut identity = output.stdout;
3703 identity.extend_from_slice(&output.stderr);
3704 identity
3705 })
3706}
3707
3708fn cargo_metadata(spec: &WasmBuildSpec) -> Result<Value, WasmBuildError> {
3709 let mut command = Command::new(&spec.cargo_program);
3710 command
3711 .current_dir(&spec.workspace_root)
3712 .args(["metadata", "--format-version", "1"]);
3713 for argument in metadata_arguments(&spec.cargo_profile_args) {
3714 command.arg(argument);
3715 }
3716 apply_command_environment(&mut command, spec);
3717 let output = capture_probe_output(
3718 &mut command,
3719 WasmBuildPhase::CargoMetadata,
3720 CARGO_METADATA_BYTES,
3721 )?;
3722 let output = ensure_command_success(WasmBuildPhase::CargoMetadata, output)?;
3723 serde_json::from_slice(&output.stdout).map_err(|error| WasmBuildError::InvalidMetadata {
3724 message: format!("Cargo metadata was not valid JSON: {error}"),
3725 })
3726}
3727
3728fn capture_probe_output(
3729 command: &mut Command,
3730 phase: WasmBuildPhase,
3731 stdout_bytes: usize,
3732) -> Result<CompletedExecution, WasmBuildError> {
3733 let result = capture_command(
3734 command,
3735 OutputLimits {
3736 stdout: OutputLimit::Terminate(stdout_bytes),
3737 stderr: OutputLimit::Terminate(TOOL_IDENTITY_BYTES),
3738 timeout: None,
3739 },
3740 )
3741 .and_then(CompletedExecution::require_complete);
3742 let evidence = match result {
3743 Ok(evidence) => evidence,
3744 Err(ToolError::Execution(error))
3745 if matches!(
3746 error.failure,
3747 ExecutionFailure::Io {
3748 operation: ExecutionOperation::Spawn,
3749 ..
3750 }
3751 ) && error.cleanup.is_none() =>
3752 {
3753 let ExecutionFailure::Io { source, .. } = error.failure else {
3754 unreachable!()
3755 };
3756 return Err(WasmBuildError::CommandSpawn {
3757 phase,
3758 program: command.get_program().to_owned(),
3759 source,
3760 });
3761 }
3762 Err(error) => {
3763 return Err(WasmBuildError::Io {
3764 operation: "capture complete Cargo/tool probe output",
3765 path: PathBuf::from(command.get_program()),
3766 source: io::Error::other(error),
3767 });
3768 }
3769 };
3770 Ok(evidence)
3771}
3772
3773fn metadata_arguments(arguments: &[OsString]) -> Vec<OsString> {
3774 let mut selected = Vec::new();
3775 let mut arguments = arguments.iter();
3776 while let Some(argument) = arguments.next() {
3777 if let Some((b'F', index)) = short_cargo_option(argument) {
3778 selected.push(argument.to_str().map_or_else(
3783 || argument.clone(),
3784 |text| OsString::from(format!("-F{}", &text[index + 1..])),
3785 ));
3786 if index + 1 == argument.as_encoded_bytes().len()
3787 && let Some(value) = arguments.next()
3788 {
3789 selected.push(value.clone());
3790 }
3791 continue;
3792 }
3793 let argument_text = argument.to_string_lossy();
3794 match argument_text.as_ref() {
3795 "--all-features" | "--no-default-features" | "--locked" | "--offline" | "--frozen" => {
3796 selected.push(argument.clone());
3797 }
3798 "--features" | "--filter-platform" => {
3799 selected.push(argument.clone());
3800 if let Some(value) = arguments.next() {
3801 selected.push(value.clone());
3802 }
3803 }
3804 _ if argument_text.starts_with("--features=")
3805 || argument_text.starts_with("--filter-platform=") =>
3806 {
3807 selected.push(argument.clone());
3808 }
3809 _ => {}
3810 }
3811 }
3812 selected
3813}
3814
3815fn short_cargo_option(argument: &OsStr) -> Option<(u8, usize)> {
3819 let bytes = argument.as_encoded_bytes();
3820 if !bytes.starts_with(b"-") || bytes.starts_with(b"--") {
3821 return None;
3822 }
3823 for (index, byte) in bytes.iter().copied().enumerate().skip(1) {
3824 match byte {
3825 b'v' | b'q' | b'r' => {}
3826 b'h' | b'F' | b'j' | b'Z' | b'm' | b'p' | b'C' => return Some((byte, index)),
3827 _ => return None,
3828 }
3829 }
3830 None
3831}
3832
3833struct MetadataPackage<'a> {
3834 id: &'a str,
3835 name: &'a str,
3836 version: &'a str,
3837 manifest_path: PathBuf,
3838 is_local: bool,
3839 source: Option<&'a str>,
3840 semantic_fields: Vec<(&'static str, Option<String>)>,
3841 cdylib_name: Option<&'a str>,
3842}
3843
3844struct ParsedCargoMetadata<'a> {
3847 packages: HashMap<&'a str, MetadataPackage<'a>>,
3848 workspace_members: HashSet<&'a str>,
3849 nodes: HashMap<&'a str, MetadataNode<'a>>,
3850 workspace_root: Option<&'a str>,
3851}
3852
3853struct MetadataNode<'a> {
3854 dependencies: Vec<&'a str>,
3855 value: &'a Value,
3856}
3857
3858impl<'a> ParsedCargoMetadata<'a> {
3859 fn parse(metadata: &'a Value) -> Result<Self, String> {
3860 let packages = metadata_packages(metadata)?;
3861 let workspace_members = metadata
3862 .get("workspace_members")
3863 .and_then(Value::as_array)
3864 .ok_or_else(|| "Cargo metadata has no workspace member array".to_owned())?
3865 .iter()
3866 .filter_map(Value::as_str)
3867 .collect();
3868 let values = metadata
3869 .pointer("/resolve/nodes")
3870 .and_then(Value::as_array)
3871 .ok_or_else(|| "Cargo metadata has no resolved dependency nodes".to_owned())?;
3872 let mut nodes = HashMap::new();
3873 for value in values {
3874 let id = required_string(value, "id")?;
3875 let dependencies = value
3876 .get("deps")
3877 .and_then(Value::as_array)
3878 .ok_or_else(|| "Cargo metadata dependency node has no deps array".to_owned())?
3879 .iter()
3880 .map(|dependency| required_string(dependency, "pkg"))
3881 .collect::<Result<_, _>>()?;
3882 nodes.insert(
3883 id,
3884 MetadataNode {
3885 dependencies,
3886 value,
3887 },
3888 );
3889 }
3890 Ok(Self {
3891 packages,
3892 workspace_members,
3893 nodes,
3894 workspace_root: metadata.get("workspace_root").and_then(Value::as_str),
3895 })
3896 }
3897}
3898
3899const SEMANTIC_PACKAGE_FIELDS: &[&str] = &[
3900 "authors",
3901 "default_run",
3902 "description",
3903 "documentation",
3904 "edition",
3905 "homepage",
3906 "license",
3907 "license_file",
3908 "links",
3909 "metadata",
3910 "name",
3911 "readme",
3912 "repository",
3913 "rust_version",
3914 "version",
3915];
3916
3917struct LockedPackageIdentity {
3918 name: String,
3919 version: String,
3920 source: String,
3921 checksum: Option<String>,
3922}
3923
3924fn resolve_local_inputs(
3925 spec: &WasmBuildSpec,
3926 metadata: &ParsedCargoMetadata<'_>,
3927) -> Result<(ResolvedLocalInputs, Vec<PathBuf>), WasmBuildError> {
3928 let mut selected_ids = selected_package_ids(spec, metadata)?;
3929 let wasm_artifact_error = selected_ids.iter().find_map(|id| {
3932 let package = &metadata.packages[id];
3933 (package.cdylib_name != Some(package.name)).then(|| {
3934 format!(
3935 "Cargo package `{}` must declare a cdylib library named `{}` to produce its expected Wasm artifact",
3936 package.name, package.name,
3937 )
3938 })
3939 });
3940 let mut closure = BTreeSet::new();
3941 while let Some(id) = selected_ids.pop_front() {
3942 if !closure.insert(id) {
3943 continue;
3944 }
3945 if let Some(node) = metadata.nodes.get(id) {
3946 selected_ids.extend(node.dependencies.iter().copied());
3947 }
3948 }
3949
3950 let workspace_root = metadata
3951 .workspace_root
3952 .map_or_else(|| spec.workspace_root.clone(), PathBuf::from);
3953 let workspace_projection = semantic_workspace_projection(metadata, &closure, &workspace_root)?;
3954 let mut validation_inputs = workspace_configuration_inputs(spec, &workspace_root)?;
3955 append_package_inputs(
3956 &mut validation_inputs,
3957 &metadata.packages,
3958 closure,
3959 &workspace_root,
3960 )?;
3961 append_additional_inputs(&mut validation_inputs, spec, &workspace_root);
3962 validate_shared_incremental_target_boundary(spec, &validation_inputs)?;
3963 let exclusions = source_exclusions(spec, &validation_inputs);
3964 Ok((
3965 ResolvedLocalInputs {
3966 validation_inputs,
3967 workspace_projection,
3968 wasm_artifact_error,
3969 },
3970 exclusions,
3971 ))
3972}
3973
3974fn metadata_packages(metadata: &Value) -> Result<HashMap<&str, MetadataPackage<'_>>, String> {
3975 let packages_value = metadata
3976 .get("packages")
3977 .and_then(Value::as_array)
3978 .ok_or_else(|| "Cargo metadata has no package array".to_owned())?;
3979 let mut packages = HashMap::new();
3980 for value in packages_value {
3981 let source = optional_string(value, "source")?;
3982 let package = MetadataPackage {
3983 id: required_string(value, "id")?,
3984 name: required_string(value, "name")?,
3985 version: required_string(value, "version")?,
3986 manifest_path: PathBuf::from(required_string(value, "manifest_path")?),
3987 is_local: value.get("source").is_some_and(Value::is_null),
3988 source,
3989 semantic_fields: SEMANTIC_PACKAGE_FIELDS
3990 .iter()
3991 .map(|field| (*field, value.get(*field).map(Value::to_string)))
3992 .collect(),
3993 cdylib_name: value
3994 .get("targets")
3995 .and_then(Value::as_array)
3996 .and_then(|targets| {
3997 targets.iter().find(|target| {
3998 target
3999 .get("kind")
4000 .and_then(Value::as_array)
4001 .is_some_and(|kinds| kinds.iter().any(|kind| kind == "cdylib"))
4002 })
4003 })
4004 .and_then(|target| target.get("name"))
4005 .and_then(Value::as_str),
4006 };
4007 packages.insert(package.id, package);
4008 }
4009 Ok(packages)
4010}
4011
4012fn selected_package_ids<'a>(
4013 spec: &WasmBuildSpec,
4014 metadata: &ParsedCargoMetadata<'a>,
4015) -> Result<VecDeque<&'a str>, WasmBuildError> {
4016 let mut selected_ids = VecDeque::new();
4017 for requested in &spec.packages {
4018 let mut matches = metadata
4019 .packages
4020 .values()
4021 .filter(|package| {
4022 package.name == *requested && metadata.workspace_members.contains(package.id)
4023 })
4024 .map(|package| package.id);
4025 match (matches.next(), matches.next()) {
4026 (Some(id), None) => selected_ids.push_back(id),
4027 (None, _) => {
4028 return Err(WasmBuildError::InvalidSpec {
4029 message: format!("Cargo workspace contains no package named `{requested}`"),
4030 });
4031 }
4032 _ => {
4033 return Err(WasmBuildError::InvalidSpec {
4034 message: format!("Cargo workspace package name `{requested}` is ambiguous"),
4035 });
4036 }
4037 }
4038 }
4039 Ok(selected_ids)
4040}
4041
4042fn semantic_workspace_projection(
4043 metadata: &ParsedCargoMetadata<'_>,
4044 closure: &BTreeSet<&str>,
4045 workspace_root: &Path,
4046) -> Result<Option<InputDigest>, WasmBuildError> {
4047 let locked_packages = locked_package_identities(workspace_root)?;
4050 let mut identities = HashMap::new();
4051 for &id in closure {
4052 let package = metadata
4053 .packages
4054 .get(id)
4055 .ok_or_else(|| invalid_metadata(&format!("resolved package `{id}` is missing")))?;
4056 let Some(identity) = semantic_package_identity(package, workspace_root, &locked_packages)
4057 else {
4058 return Ok(None);
4059 };
4060 identities.insert(id, identity);
4061 }
4062
4063 let mut projected_packages = closure
4064 .iter()
4065 .map(|&id| {
4066 let package = metadata
4067 .packages
4068 .get(id)
4069 .expect("selected package closure was validated above");
4070 let identity = identities[id];
4071 let node = metadata.nodes.get(id).ok_or_else(|| {
4072 invalid_metadata(&format!("resolved package `{id}` has no dependency node"))
4073 })?;
4074 let projection = semantic_package_projection(package, node.value, &identities)?;
4075 Ok::<_, WasmBuildError>((identity, projection))
4076 })
4077 .collect::<Result<Vec<_>, _>>()?;
4078 projected_packages.sort_by_key(|(identity, _)| *identity);
4079
4080 let root_manifest = workspace_root.join("Cargo.toml");
4081 let root_contents =
4082 fs::read_to_string(&root_manifest).map_err(|source| WasmBuildError::Io {
4083 operation: "read workspace manifest for semantic projection",
4084 path: root_manifest.clone(),
4085 source,
4086 })?;
4087 let root = toml::from_str::<TomlValue>(&root_contents).map_err(|error| {
4088 invalid_metadata(&format!(
4089 "workspace manifest could not be projected as TOML: {error}"
4090 ))
4091 })?;
4092
4093 let mut hasher = InputHasher::new("wasm-semantic-workspace-projection-v1");
4094 for (identity, projection) in projected_packages {
4095 hasher.field("package-identity", identity.as_bytes());
4096 hasher.field("package-projection", projection.as_bytes());
4097 }
4098 hash_toml_setting(&mut hasher, "cargo-features", root.get("cargo-features"));
4099 hash_toml_setting(&mut hasher, "profile", root.get("profile"));
4100 let workspace = root.get("workspace").and_then(TomlValue::as_table);
4101 hash_toml_setting(
4102 &mut hasher,
4103 "workspace-resolver",
4104 workspace.and_then(|table| table.get("resolver")),
4105 );
4106 hash_toml_setting(
4107 &mut hasher,
4108 "workspace-lints",
4109 workspace.and_then(|table| table.get("lints")),
4110 );
4111 Ok(Some(hasher.finish()))
4112}
4113
4114fn locked_package_identities(
4115 workspace_root: &Path,
4116) -> Result<Vec<LockedPackageIdentity>, WasmBuildError> {
4117 let lockfile = workspace_root.join("Cargo.lock");
4118 let contents = match fs::read_to_string(&lockfile) {
4119 Ok(contents) => contents,
4120 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
4121 Err(source) => {
4122 return Err(WasmBuildError::Io {
4123 operation: "read Cargo lockfile for semantic projection",
4124 path: lockfile,
4125 source,
4126 });
4127 }
4128 };
4129 let lock = toml::from_str::<TomlValue>(&contents).map_err(|error| {
4130 invalid_metadata(&format!(
4131 "Cargo lockfile could not be projected as TOML: {error}"
4132 ))
4133 })?;
4134 let Some(packages) = lock.get("package").and_then(TomlValue::as_array) else {
4135 return Ok(Vec::new());
4136 };
4137 packages
4138 .iter()
4139 .filter_map(|package| {
4140 let Some(table) = package.as_table() else {
4141 return Some(Err(invalid_metadata(
4142 "Cargo lockfile package entry is not a table",
4143 )));
4144 };
4145 let source = table.get("source")?.as_str().map(str::to_owned);
4146 Some(
4147 source
4148 .ok_or_else(|| {
4149 invalid_metadata("Cargo lockfile package source is not a string")
4150 })
4151 .and_then(|source| {
4152 Ok(LockedPackageIdentity {
4153 name: required_toml_string(table, "name", "Cargo lockfile package")?,
4154 version: required_toml_string(
4155 table,
4156 "version",
4157 "Cargo lockfile package",
4158 )?,
4159 source,
4160 checksum: optional_toml_string(
4161 table,
4162 "checksum",
4163 "Cargo lockfile package",
4164 )?,
4165 })
4166 }),
4167 )
4168 })
4169 .collect()
4170}
4171
4172fn required_toml_string(
4173 table: &toml::Table,
4174 field: &str,
4175 context: &str,
4176) -> Result<String, WasmBuildError> {
4177 table
4178 .get(field)
4179 .and_then(TomlValue::as_str)
4180 .map(str::to_owned)
4181 .ok_or_else(|| invalid_metadata(&format!("{context} `{field}` is missing or not a string")))
4182}
4183
4184fn optional_toml_string(
4185 table: &toml::Table,
4186 field: &str,
4187 context: &str,
4188) -> Result<Option<String>, WasmBuildError> {
4189 match table.get(field) {
4190 None => Ok(None),
4191 Some(TomlValue::String(value)) => Ok(Some(value.clone())),
4192 Some(_) => Err(invalid_metadata(&format!(
4193 "{context} `{field}` is not a string"
4194 ))),
4195 }
4196}
4197
4198fn semantic_package_identity(
4199 package: &MetadataPackage<'_>,
4200 workspace_root: &Path,
4201 locked_packages: &[LockedPackageIdentity],
4202) -> Option<InputDigest> {
4203 let mut hasher = InputHasher::new("wasm-semantic-package-identity-v1");
4204 hasher.field("name", package.name.as_bytes());
4205 hasher.field("version", package.version.as_bytes());
4206 if package.is_local {
4207 let manifest = package.manifest_path.strip_prefix(workspace_root).ok()?;
4208 let package_root = package.manifest_path.parent()?;
4209 if package_root == workspace_root {
4210 return None;
4211 }
4212 hasher.field("local-manifest", &os_bytes(manifest.as_os_str()));
4213 } else {
4214 let metadata_source = package.source?;
4215 let locked = locked_packages.iter().find(|locked| {
4216 locked.name == package.name
4217 && locked.version == package.version
4218 && locked.source == metadata_source
4219 })?;
4220 match locked.source.as_str() {
4221 source if source.starts_with("registry+") && locked.checksum.is_some() => {}
4222 source if source.starts_with("git+") && source.contains('#') => {}
4223 _ => return None,
4224 }
4225 hasher.field("external-package-id", package.id.as_bytes());
4226 hasher.field("external-source", locked.source.as_bytes());
4227 hasher.field(
4228 "external-checksum",
4229 locked.checksum.as_deref().unwrap_or_default().as_bytes(),
4230 );
4231 }
4232 Some(hasher.finish())
4233}
4234
4235fn semantic_package_projection(
4236 package: &MetadataPackage<'_>,
4237 node: &Value,
4238 identities: &HashMap<&str, InputDigest>,
4239) -> Result<InputDigest, WasmBuildError> {
4240 let mut hasher = InputHasher::new("wasm-semantic-package-projection-v1");
4244 for (field, value) in &package.semantic_fields {
4245 hasher.field("package-field-name", field.as_bytes());
4246 match value {
4247 Some(value) => hasher.field("package-field-value", value.as_bytes()),
4248 None => hasher.field("package-field-missing", b""),
4249 }
4250 }
4251
4252 let mut features = node
4253 .get("features")
4254 .and_then(Value::as_array)
4255 .ok_or_else(|| invalid_metadata("Cargo metadata dependency node has no features array"))?
4256 .iter()
4257 .map(|feature| {
4258 feature.as_str().ok_or_else(|| {
4259 invalid_metadata("Cargo metadata dependency feature is not a string")
4260 })
4261 })
4262 .collect::<Result<Vec<_>, _>>()?;
4263 features.sort_unstable();
4264 for feature in features {
4265 hasher.field("enabled-feature", feature.as_bytes());
4266 }
4267
4268 let mut dependencies = node
4269 .get("deps")
4270 .and_then(Value::as_array)
4271 .ok_or_else(|| invalid_metadata("Cargo metadata dependency node has no deps array"))?
4272 .iter()
4273 .map(|dependency| {
4274 let name = required_string(dependency, "name")
4275 .map_err(|message| invalid_metadata(&message))?;
4276 let package_id =
4277 required_string(dependency, "pkg").map_err(|message| invalid_metadata(&message))?;
4278 let identity = identities.get(package_id).copied().ok_or_else(|| {
4279 invalid_metadata(&format!(
4280 "dependency `{package_id}` is outside the selected package closure"
4281 ))
4282 })?;
4283 let kinds = dependency
4284 .get("dep_kinds")
4285 .ok_or_else(|| invalid_metadata("Cargo metadata dependency has no kind array"))?
4286 .to_string();
4287 Ok::<_, WasmBuildError>((name, identity, kinds))
4288 })
4289 .collect::<Result<Vec<_>, _>>()?;
4290 dependencies.sort();
4291 for (name, identity, kinds) in dependencies {
4292 hasher.field("dependency-name", name.as_bytes());
4293 hasher.field("dependency-identity", identity.as_bytes());
4294 hasher.field("dependency-kinds", kinds.as_bytes());
4295 }
4296 Ok(hasher.finish())
4297}
4298
4299fn hash_toml_setting(hasher: &mut InputHasher, label: &str, value: Option<&TomlValue>) {
4300 hasher.field("workspace-setting-name", label.as_bytes());
4301 match value {
4302 Some(value) => hasher.field("workspace-setting-value", value.to_string().as_bytes()),
4303 None => hasher.field("workspace-setting-missing", b""),
4304 }
4305}
4306
4307fn is_broad_workspace_input(label: &Path) -> bool {
4308 label == Path::new("workspace/Cargo.toml") || label == Path::new("workspace/Cargo.lock")
4309}
4310
4311fn digest_resolved_local_inputs(
4312 inputs: &ResolvedLocalInputs,
4313 exclusions: &[PathBuf],
4314 cache: &mut LabeledPathDigestCache,
4315 error_path: &Path,
4316 validation_operation: &'static str,
4317 semantic_operation: &'static str,
4318) -> Result<(InputDigest, InputDigest), WasmBuildError> {
4319 let validation_digest = digest_labeled_paths_composable(
4320 "wasm-source-inputs-v1",
4321 inputs
4322 .validation_inputs
4323 .iter()
4324 .map(|(label, path)| (label.as_path(), path.as_path())),
4325 exclusions,
4326 cache,
4327 )
4328 .map_err(|source| WasmBuildError::Io {
4329 operation: validation_operation,
4330 path: error_path.to_owned(),
4331 source,
4332 })?;
4333 let input_digest = semantic_input_digest(inputs, validation_digest, exclusions, cache)
4334 .map_err(|source| WasmBuildError::Io {
4335 operation: semantic_operation,
4336 path: error_path.to_owned(),
4337 source,
4338 })?;
4339 Ok((input_digest, validation_digest))
4340}
4341
4342fn semantic_input_digest(
4343 inputs: &ResolvedLocalInputs,
4344 validation_digest: InputDigest,
4345 exclusions: &[PathBuf],
4346 cache: &mut LabeledPathDigestCache,
4347) -> io::Result<InputDigest> {
4348 let Some(workspace) = inputs.workspace_projection else {
4349 return Ok(validation_digest);
4350 };
4351 let path_digest = digest_labeled_paths_composable(
4352 "wasm-source-inputs-v1",
4353 inputs
4354 .validation_inputs
4355 .iter()
4356 .filter(|(label, _)| !is_broad_workspace_input(label))
4357 .map(|(label, path)| (label.as_path(), path.as_path())),
4358 exclusions,
4359 cache,
4360 )?;
4361 let mut hasher = InputHasher::new("wasm-semantic-source-inputs-v1");
4362 hasher.field("path-input-digest", path_digest.as_bytes());
4363 hasher.field("workspace-projection", workspace.as_bytes());
4364 Ok(hasher.finish())
4365}
4366
4367fn workspace_configuration_inputs(
4368 spec: &WasmBuildSpec,
4369 workspace_root: &Path,
4370) -> Result<Vec<(PathBuf, PathBuf)>, WasmBuildError> {
4371 let mut inputs = Vec::new();
4372 add_if_present(
4373 &mut inputs,
4374 "workspace/Cargo.toml",
4375 workspace_root.join("Cargo.toml"),
4376 );
4377 add_if_present(
4378 &mut inputs,
4379 "workspace/Cargo.lock",
4380 workspace_root.join("Cargo.lock"),
4381 );
4382 add_if_present(
4383 &mut inputs,
4384 "workspace/rust-toolchain.toml",
4385 workspace_root.join("rust-toolchain.toml"),
4386 );
4387 add_if_present(
4388 &mut inputs,
4389 "workspace/rust-toolchain",
4390 workspace_root.join("rust-toolchain"),
4391 );
4392 append_cargo_configuration_inputs(&mut inputs, spec, workspace_root)?;
4393 Ok(inputs)
4394}
4395
4396fn append_cargo_configuration_inputs(
4397 inputs: &mut Vec<(PathBuf, PathBuf)>,
4398 spec: &WasmBuildSpec,
4399 workspace_root: &Path,
4400) -> Result<(), WasmBuildError> {
4401 let invocation_root =
4402 spec.workspace_root
4403 .canonicalize()
4404 .map_err(|source| WasmBuildError::Io {
4405 operation: "resolve Cargo invocation directory",
4406 path: spec.workspace_root.clone(),
4407 source,
4408 })?;
4409 let canonical_workspace =
4410 workspace_root
4411 .canonicalize()
4412 .map_err(|source| WasmBuildError::Io {
4413 operation: "resolve Cargo workspace directory",
4414 path: workspace_root.to_owned(),
4415 source,
4416 })?;
4417
4418 let mut roots = invocation_root
4419 .ancestors()
4420 .filter_map(|directory| effective_cargo_config(&directory.join(".cargo")))
4421 .collect::<Vec<_>>();
4422 if let Some(cargo_home) = effective_cargo_home(spec, &invocation_root)
4423 && let Some(config) = effective_cargo_config(&cargo_home)
4424 {
4425 roots.push(config);
4426 }
4427
4428 let mut active = BTreeSet::new();
4429 for config in roots {
4430 append_cargo_configuration_tree(inputs, &config, &canonical_workspace, &mut active, false)?;
4431 }
4432 Ok(())
4433}
4434
4435fn effective_cargo_config(directory: &Path) -> Option<PathBuf> {
4436 let extensionless = directory.join("config");
4437 if extensionless.exists() {
4438 return Some(extensionless);
4439 }
4440 let toml = directory.join("config.toml");
4441 toml.exists().then_some(toml)
4442}
4443
4444fn effective_cargo_home(spec: &WasmBuildSpec, invocation_root: &Path) -> Option<PathBuf> {
4445 if let Some(cargo_home) = command_environment_value(spec, "CARGO_HOME") {
4446 let cargo_home = PathBuf::from(cargo_home);
4447 return Some(if cargo_home.is_absolute() {
4448 cargo_home
4449 } else {
4450 invocation_root.join(cargo_home)
4451 });
4452 }
4453
4454 default_home_directory(spec).map(|home| {
4455 let home = if home.is_absolute() {
4456 home
4457 } else {
4458 invocation_root.join(home)
4459 };
4460 home.join(".cargo")
4461 })
4462}
4463
4464#[cfg(windows)]
4465fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
4466 command_environment_value(spec, "USERPROFILE")
4467 .or_else(|| command_environment_value(spec, "HOME"))
4468 .map(PathBuf::from)
4469}
4470
4471#[cfg(not(windows))]
4472fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
4473 command_environment_value(spec, "HOME").map(PathBuf::from)
4474}
4475
4476fn command_environment_value(spec: &WasmBuildSpec, name: &str) -> Option<OsString> {
4477 spec.extra_env
4478 .get(OsStr::new(name))
4479 .cloned()
4480 .or_else(|| std::env::var_os(name))
4481}
4482
4483fn append_cargo_configuration_tree(
4484 inputs: &mut Vec<(PathBuf, PathBuf)>,
4485 config: &Path,
4486 workspace_root: &Path,
4487 active: &mut BTreeSet<PathBuf>,
4488 optional: bool,
4489) -> Result<(), WasmBuildError> {
4490 let contents = match fs::read_to_string(config) {
4491 Ok(contents) => contents,
4492 Err(error) if optional && error.kind() == io::ErrorKind::NotFound => return Ok(()),
4493 Err(source) => {
4494 return Err(WasmBuildError::Io {
4495 operation: "read Cargo configuration",
4496 path: config.to_owned(),
4497 source,
4498 });
4499 }
4500 };
4501 let parent = config
4502 .parent()
4503 .ok_or_else(|| WasmBuildError::InvalidCargoConfiguration {
4504 path: config.to_owned(),
4505 message: "configuration path has no parent directory".to_owned(),
4506 })?;
4507 let location = parent
4510 .canonicalize()
4511 .map_err(|source| WasmBuildError::Io {
4512 operation: "resolve Cargo configuration directory",
4513 path: parent.to_owned(),
4514 source,
4515 })?
4516 .join(config.file_name().ok_or_else(|| {
4517 invalid_cargo_configuration(config, "configuration path has no file name")
4518 })?);
4519 if !active.insert(location.clone()) {
4522 return Ok(());
4523 }
4524 let configuration = toml::from_str::<TomlValue>(&contents).map_err(|error| {
4525 WasmBuildError::InvalidCargoConfiguration {
4526 path: config.to_owned(),
4527 message: error.to_string(),
4528 }
4529 })?;
4530 if !inputs.iter().any(|(_, path)| path == config) {
4533 inputs.push((
4534 cargo_configuration_label(&location, workspace_root),
4535 config.to_owned(),
4536 ));
4537 }
4538 if let Some(include) = configuration.get("include") {
4539 for (included, optional) in cargo_configuration_includes(include, config)? {
4540 let included = if included.is_absolute() {
4541 included
4542 } else {
4543 parent.join(included)
4544 };
4545 append_cargo_configuration_tree(inputs, &included, workspace_root, active, optional)?;
4546 }
4547 }
4548 active.remove(&location);
4549 Ok(())
4550}
4551
4552fn cargo_configuration_includes(
4553 include: &TomlValue,
4554 config: &Path,
4555) -> Result<Vec<(PathBuf, bool)>, WasmBuildError> {
4556 let values = match include {
4557 TomlValue::Array(values) => values.as_slice(),
4558 value => std::slice::from_ref(value),
4559 };
4560 values
4561 .iter()
4562 .map(|value| match value {
4563 TomlValue::String(path) => Ok((PathBuf::from(path), false)),
4564 TomlValue::Table(table) => {
4565 let path = table
4566 .get("path")
4567 .and_then(TomlValue::as_str)
4568 .ok_or_else(|| {
4569 invalid_cargo_configuration(
4570 config,
4571 "Cargo configuration include table requires a string `path`",
4572 )
4573 })?;
4574 let optional = table
4575 .get("optional")
4576 .map(|value| {
4577 value.as_bool().ok_or_else(|| {
4578 invalid_cargo_configuration(
4579 config,
4580 "Cargo configuration include `optional` must be a boolean",
4581 )
4582 })
4583 })
4584 .transpose()?
4585 .unwrap_or(false);
4586 Ok((PathBuf::from(path), optional))
4587 }
4588 _ => Err(invalid_cargo_configuration(
4589 config,
4590 "Cargo configuration `include` must contain paths or include tables",
4591 )),
4592 })
4593 .collect()
4594}
4595
4596fn cargo_configuration_label(config: &Path, workspace_root: &Path) -> PathBuf {
4597 if let Ok(relative) = config.strip_prefix(workspace_root) {
4598 return PathBuf::from("cargo-config/workspace").join(relative);
4599 }
4600 let location = digest_bytes("cargo-config-location-v1", &os_bytes(config.as_os_str()));
4601 PathBuf::from("cargo-config/external").join(location.to_hex())
4602}
4603
4604fn invalid_cargo_configuration(path: &Path, message: &str) -> WasmBuildError {
4605 WasmBuildError::InvalidCargoConfiguration {
4606 path: path.to_owned(),
4607 message: message.to_owned(),
4608 }
4609}
4610
4611fn append_package_inputs(
4612 inputs: &mut Vec<(PathBuf, PathBuf)>,
4613 packages: &HashMap<&str, MetadataPackage<'_>>,
4614 closure: BTreeSet<&str>,
4615 workspace_root: &Path,
4616) -> Result<(), WasmBuildError> {
4617 for id in closure {
4618 let Some(package) = packages.get(id) else {
4619 return Err(invalid_metadata(&format!(
4620 "resolved package `{id}` is missing"
4621 )));
4622 };
4623 if !package.is_local {
4624 continue;
4625 }
4626 let root = package.manifest_path.parent().ok_or_else(|| {
4627 invalid_metadata(&format!(
4628 "package `{}` manifest has no parent",
4629 package.name
4630 ))
4631 })?;
4632 let relative_manifest = package
4633 .manifest_path
4634 .strip_prefix(workspace_root)
4635 .unwrap_or(&package.manifest_path);
4636 let label = PathBuf::from(format!("package/{}@{}", package.name, package.version))
4637 .join(relative_manifest.parent().unwrap_or_else(|| Path::new(".")));
4638 inputs.push((label, root.to_owned()));
4639 }
4640 Ok(())
4641}
4642
4643fn append_additional_inputs(
4644 inputs: &mut Vec<(PathBuf, PathBuf)>,
4645 spec: &WasmBuildSpec,
4646 workspace_root: &Path,
4647) {
4648 for additional in &spec.additional_inputs {
4649 let path = if additional.is_absolute() {
4650 additional.clone()
4651 } else {
4652 workspace_root.join(additional)
4653 };
4654 inputs.push((PathBuf::from("additional").join(additional), path));
4655 }
4656}
4657
4658fn source_exclusions(spec: &WasmBuildSpec, inputs: &[(PathBuf, PathBuf)]) -> Vec<PathBuf> {
4659 let mut exclusions = vec![
4660 spec.target_dir.clone(),
4661 spec.workspace_root.join("target"),
4662 spec.workspace_root.join(".git"),
4663 ];
4664 if let Some(shared_target) = shared_incremental_target(spec) {
4665 exclusions.push(shared_target);
4666 }
4667 for (_, path) in inputs {
4668 if path.is_dir() {
4669 exclusions.push(path.join("target"));
4670 exclusions.push(path.join(".git"));
4671 }
4672 }
4673 exclusions
4674}
4675
4676fn validate_shared_incremental_target_boundary(
4677 spec: &WasmBuildSpec,
4678 inputs: &[(PathBuf, PathBuf)],
4679) -> Result<(), WasmBuildError> {
4680 let Some(shared_target) = shared_incremental_target(spec) else {
4681 return Ok(());
4682 };
4683 let shared_target =
4684 canonicalize_allow_missing(&shared_target).map_err(|source| WasmBuildError::Io {
4685 operation: "resolve shared incremental Cargo target boundary",
4686 path: shared_target.clone(),
4687 source,
4688 })?;
4689 let exact_entries = spec.target_dir.join(".ic-testkit/wasm-targets");
4690 let exact_entries =
4691 canonicalize_allow_missing(&exact_entries).map_err(|source| WasmBuildError::Io {
4692 operation: "resolve exact Wasm cache boundary",
4693 path: exact_entries,
4694 source,
4695 })?;
4696 if shared_target.starts_with(&exact_entries)
4699 || (exact_entries.starts_with(&shared_target)
4700 && !exact_entries.starts_with(shared_target.join(".ic-testkit")))
4701 {
4702 return Err(WasmBuildError::InvalidSpec {
4703 message: "shared incremental target must not overlap removable exact Wasm cache state"
4704 .to_owned(),
4705 });
4706 }
4707 let resolved_inputs = inputs
4708 .iter()
4709 .map(|(_, input)| {
4710 let canonical = input.canonicalize().map_err(|source| WasmBuildError::Io {
4711 operation: "resolve Cargo input boundary",
4712 path: input.clone(),
4713 source,
4714 })?;
4715 let metadata = fs::metadata(&canonical).map_err(|source| WasmBuildError::Io {
4716 operation: "inspect Cargo input boundary",
4717 path: canonical.clone(),
4718 source,
4719 })?;
4720 Ok((canonical, metadata.is_dir()))
4721 })
4722 .collect::<Result<Vec<_>, WasmBuildError>>()?;
4723 let safe_generated_roots = std::iter::once(spec.target_dir.clone())
4724 .chain(std::iter::once(spec.workspace_root.join("target")))
4725 .chain(
4726 inputs
4727 .iter()
4728 .filter(|(_, path)| path.is_dir())
4729 .map(|(_, path)| path.join("target")),
4730 )
4731 .filter_map(|path| canonicalize_allow_missing(&path).ok())
4732 .filter(|root| {
4733 !resolved_inputs
4734 .iter()
4735 .any(|(input, _is_directory)| input.starts_with(root))
4736 })
4737 .collect::<Vec<_>>();
4738 if safe_generated_roots
4739 .iter()
4740 .any(|root| shared_target.starts_with(root))
4741 {
4742 return Ok(());
4743 }
4744
4745 for (input, is_directory) in resolved_inputs {
4746 if shared_target == input
4747 || (is_directory && shared_target.starts_with(&input))
4748 || input.starts_with(&shared_target)
4749 {
4750 return Err(WasmBuildError::InvalidSpec {
4751 message: format!(
4752 "shared incremental target {} must not overlap exact Cargo inputs unless it is inside a generated target directory",
4753 shared_target.display()
4754 ),
4755 });
4756 }
4757 }
4758 Ok(())
4759}
4760
4761pub(super) fn shared_incremental_target(spec: &WasmBuildSpec) -> Option<PathBuf> {
4762 let WasmBuildCacheMode::SharedIncremental { target_dir } = &spec.cache_mode else {
4763 return None;
4764 };
4765 Some(if target_dir.is_absolute() {
4766 target_dir.clone()
4767 } else {
4768 spec.workspace_root.join(target_dir)
4769 })
4770}
4771
4772fn shared_incremental_target_exists(
4773 spec: &WasmBuildSpec,
4774 operation: &'static str,
4775) -> Result<bool, WasmBuildError> {
4776 let target_dir =
4777 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
4778 message: "shared incremental target is not configured".to_owned(),
4779 })?;
4780 match fs::symlink_metadata(&target_dir) {
4781 Ok(metadata) if metadata.is_dir() => Ok(true),
4782 Ok(_) => Err(WasmBuildError::InvalidSpec {
4783 message: format!(
4784 "shared incremental Cargo target {} must be a directory",
4785 target_dir.display()
4786 ),
4787 }),
4788 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
4789 Err(source) => Err(WasmBuildError::Io {
4790 operation,
4791 path: target_dir,
4792 source,
4793 }),
4794 }
4795}
4796
4797fn effective_environment(spec: &WasmBuildSpec) -> BTreeMap<OsString, Option<OsString>> {
4798 let mut names = spec.inherited_env.clone();
4799 names.extend(AUTOMATIC_ENVIRONMENT.iter().map(OsString::from));
4800 let mut environment = names
4801 .into_iter()
4802 .map(|name| {
4803 let value = std::env::var_os(&name);
4804 (name, value)
4805 })
4806 .collect::<BTreeMap<_, _>>();
4807 for (key, value) in &spec.extra_env {
4808 environment.insert(key.clone(), Some(value.clone()));
4809 }
4810 environment
4811}
4812
4813fn apply_command_environment(command: &mut Command, spec: &WasmBuildSpec) {
4814 for (key, value) in &spec.extra_env {
4815 command.env(key, value);
4816 }
4817}
4818
4819fn run_cargo_build(
4820 spec: &WasmBuildSpec,
4821 build_target_dir: &Path,
4822 progress: &mut ProgressReporter<'_>,
4823) -> Result<(), WasmBuildError> {
4824 let absolute_target_dir =
4825 canonicalize_allow_missing(build_target_dir).map_err(|source| WasmBuildError::Io {
4826 operation: "resolve Cargo build target directory",
4827 path: build_target_dir.to_owned(),
4828 source,
4829 })?;
4830 let mut command = Command::new(&spec.cargo_program);
4831 command
4832 .current_dir(&spec.workspace_root)
4833 .env("CARGO_TARGET_DIR", absolute_target_dir)
4834 .args(["build", "--lib", "--target", &spec.target])
4835 .args(
4836 spec.cargo_profile_args
4837 .iter()
4838 .filter(|argument| argument.as_os_str() != OsStr::new("--lib")),
4839 );
4840 apply_command_environment(&mut command, spec);
4841 for package in &spec.packages {
4842 command.args(["-p", package]);
4843 }
4844
4845 communicate_cargo_build(spec, build_target_dir, command, progress)
4846}
4847
4848fn communicate_cargo_build(
4849 spec: &WasmBuildSpec,
4850 build_target_dir: &Path,
4851 mut command: Command,
4852 progress: &mut ProgressReporter<'_>,
4853) -> Result<(), WasmBuildError> {
4854 command.stdout(Stdio::piped()).stderr(Stdio::piped());
4857 let started = Instant::now();
4858 let observed = progress.is_observed();
4859 if !observed {
4860 command.stdin(Stdio::null());
4862 }
4863 let mut child = if observed {
4864 OwnedChild::spawn(&mut command)
4865 } else {
4866 OwnedChild::spawn_direct(&mut command)
4867 }
4868 .map_err(|source| WasmBuildError::CommandSpawn {
4869 phase: WasmBuildPhase::CargoBuild,
4870 program: spec.cargo_program.clone(),
4871 source,
4872 })?;
4873 progress.emit(WasmBuildProgressEvent::CargoStarted {
4874 target_dir: build_target_dir.to_owned(),
4875 });
4876 let progress = RefCell::new(progress);
4877 let result = communicate_child_with_observer(
4878 &mut child,
4879 None,
4880 OutputLimits {
4881 stdout: OutputLimit::Truncate(CARGO_DIAGNOSTIC_BYTES),
4884 stderr: OutputLimit::Truncate(CARGO_DIAGNOSTIC_BYTES),
4885 timeout: None,
4886 },
4887 SuccessfulExit::Cleanup,
4888 || {
4889 progress
4890 .borrow_mut()
4891 .emit_heartbeat_if_due(WasmBuildProgressPhase::CargoBuild, started.elapsed());
4892 false
4893 },
4894 |stream, bytes| {
4895 let mut progress = progress.borrow_mut();
4896 if progress.config.emit_cargo_output {
4897 progress.emit(WasmBuildProgressEvent::CargoOutput {
4898 stream: match stream {
4899 OutputStream::Stdout => WasmBuildOutputStream::Stdout,
4900 OutputStream::Stderr => WasmBuildOutputStream::Stderr,
4901 },
4902 bytes: bytes.to_vec(),
4903 });
4904 }
4905 Ok(())
4906 },
4907 );
4908 let status = match &result {
4909 Ok(evidence) => Some(evidence.status),
4910 Err(error) => error.evidence().and_then(|evidence| evidence.status),
4911 };
4912 if let Some(status) = status {
4913 progress
4914 .borrow_mut()
4915 .emit(WasmBuildProgressEvent::CargoFinished {
4916 success: status.success(),
4917 code: status.code(),
4918 elapsed: started.elapsed(),
4919 });
4920 }
4921 let evidence = match result {
4922 Ok(evidence) => evidence,
4923 Err(error) => {
4924 return Err(WasmBuildError::Io {
4925 operation: "communicate with cargo build",
4926 path: PathBuf::from(&spec.cargo_program),
4927 source: io::Error::other(error),
4929 });
4930 }
4931 };
4932 let status = evidence.status;
4933 if status.success() {
4934 return Ok(());
4935 }
4936 Err(WasmBuildError::CommandFailed {
4937 phase: WasmBuildPhase::CargoBuild,
4938 status,
4939 stdout: cargo_diagnostic(&evidence.stdout, evidence.stdout_truncated),
4940 stderr: cargo_diagnostic(&evidence.stderr, evidence.stderr_truncated),
4941 })
4942}
4943
4944fn cargo_diagnostic(bytes: &[u8], truncated: bool) -> String {
4945 let mut text = String::from_utf8_lossy(bytes).into_owned();
4946 if truncated {
4947 text.push_str("\n[diagnostic truncated after 1 MiB; enable Cargo output events for the complete stream]\n");
4948 }
4949 text
4950}
4951
4952fn ensure_command_success(
4953 phase: WasmBuildPhase,
4954 output: CompletedExecution,
4955) -> Result<CompletedExecution, WasmBuildError> {
4956 if output.status.success() {
4957 return Ok(output);
4958 }
4959 Err(WasmBuildError::CommandFailed {
4960 phase,
4961 status: output.status,
4962 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
4963 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
4964 })
4965}
4966
4967fn expected_artifacts(spec: &WasmBuildSpec, target_dir: &Path) -> Vec<PathBuf> {
4968 spec.packages
4969 .iter()
4970 .map(|package| {
4971 target_dir
4972 .join(&spec.target)
4973 .join(&spec.profile_target_dir)
4974 .join(format!("{package}.wasm"))
4975 })
4976 .collect()
4977}
4978
4979fn cache_entry_directory(spec: &WasmBuildSpec, fingerprint: InputDigest) -> PathBuf {
4980 spec.target_dir
4981 .join(".ic-testkit/wasm-targets")
4982 .join(fingerprint.to_hex())
4983}
4984
4985fn validated_artifact_set(
4986 artifacts: &[PathBuf],
4987 fingerprint: InputDigest,
4988) -> Option<Vec<FileDigest>> {
4989 let header = artifact_stamp_header(fingerprint);
4990 let stamp_len = artifact_stamp_contents(fingerprint, fingerprint).len();
4993 artifacts
4994 .iter()
4995 .map(|artifact| {
4996 let metadata = fs::metadata(artifact).ok()?;
4997 if !metadata.is_file() || metadata.len() == 0 {
4998 return None;
4999 }
5000 let stamp = read_stamp_with_limit(&artifact_stamp_path(artifact), stamp_len).ok()??;
5001 if stamp.len() != stamp_len || !stamp.starts_with(&header) {
5004 return None;
5005 }
5006 let info = digest_file("wasm-artifact-v1", artifact).ok()?;
5007 (stamp == artifact_stamp_contents(fingerprint, info.digest)).then_some(info)
5008 })
5009 .collect()
5010}
5011
5012fn missing_artifacts(artifacts: &[PathBuf]) -> Vec<PathBuf> {
5013 artifacts
5014 .iter()
5015 .filter(|path| {
5016 fs::metadata(path).map_or(true, |metadata| !metadata.is_file() || metadata.len() == 0)
5017 })
5018 .cloned()
5019 .collect()
5020}
5021
5022fn artifact_stamp_path(artifact: &Path) -> PathBuf {
5023 let mut name = artifact
5024 .file_name()
5025 .map_or_else(|| OsString::from("artifact"), OsString::from);
5026 name.push(".ic-testkit-build");
5027 artifact.with_file_name(name)
5028}
5029
5030fn artifact_stamp_header(fingerprint: InputDigest) -> String {
5031 format!("{CACHE_FORMAT_VERSION}\nbuild-sha256:{fingerprint}\n")
5032}
5033
5034fn artifact_stamp_contents(fingerprint: InputDigest, artifact_digest: InputDigest) -> String {
5035 format!(
5036 "{}artifact-sha256:{artifact_digest}\n",
5037 artifact_stamp_header(fingerprint),
5038 )
5039}
5040
5041fn write_artifact_stamp(
5042 artifact: &Path,
5043 fingerprint: InputDigest,
5044 info: &FileDigest,
5045 preserve_matching: bool,
5046) -> Result<(), WasmBuildError> {
5047 let stamp_path = artifact_stamp_path(artifact);
5048 let stamp = artifact_stamp_contents(fingerprint, info.digest);
5049 if preserve_matching && destination_matches_bytes(&stamp_path, stamp.as_bytes()) {
5050 return Ok(());
5051 }
5052 ic_host_fs::durable::write_bytes(&stamp_path, stamp.as_bytes()).map_err(|source| {
5053 WasmBuildError::Io {
5054 operation: "publish Wasm build stamp",
5055 path: stamp_path,
5056 source: io::Error::other(source),
5057 }
5058 })
5059}
5060
5061fn publish_artifact_stamps(
5062 artifacts: &[PathBuf],
5063 fingerprint: InputDigest,
5064) -> Result<Vec<FileDigest>, WasmBuildError> {
5065 let mut info = Vec::with_capacity(artifacts.len());
5066 for artifact in artifacts {
5067 let digest =
5068 digest_file("wasm-artifact-v1", artifact).map_err(|source| WasmBuildError::Io {
5069 operation: "hash built Wasm artifact",
5070 path: artifact.clone(),
5071 source,
5072 })?;
5073 write_artifact_stamp(artifact, fingerprint, &digest, false)?;
5074 info.push(digest);
5075 }
5076 Ok(info)
5077}
5078
5079fn materialize_artifacts(
5080 cached_artifacts: &[PathBuf],
5081 artifacts: &[PathBuf],
5082 fingerprint: InputDigest,
5083 artifact_info: &[FileDigest],
5084 preserve_matching: bool,
5085) -> Result<(), WasmBuildError> {
5086 for ((cached, artifact), info) in cached_artifacts.iter().zip(artifacts).zip(artifact_info) {
5087 if preserve_matching && destination_matches_digest("wasm-artifact-v1", artifact, info) {
5088 continue;
5089 }
5090 copy_file_atomic(cached, artifact).map_err(|source| WasmBuildError::Io {
5091 operation: "publish Wasm artifact",
5092 path: artifact.clone(),
5093 source,
5094 })?;
5095 }
5096 for (artifact, info) in artifacts.iter().zip(artifact_info) {
5099 write_artifact_stamp(artifact, fingerprint, info, preserve_matching)?;
5100 }
5101 Ok(())
5102}
5103
5104fn copy_wasm_artifacts(
5105 source_artifacts: &[PathBuf],
5106 cached_artifacts: &[PathBuf],
5107) -> Result<(), WasmBuildError> {
5108 for (source, cached) in source_artifacts.iter().zip(cached_artifacts) {
5109 copy_file_atomic(source, cached).map_err(|source_error| WasmBuildError::Io {
5110 operation: "cache shared-incremental Wasm artifact",
5111 path: cached.clone(),
5112 source: source_error,
5113 })?;
5114 }
5115 Ok(())
5116}
5117
5118fn create_dir_all(path: &Path, operation: &'static str) -> Result<(), WasmBuildError> {
5119 fs::create_dir_all(path).map_err(|source| WasmBuildError::Io {
5120 operation,
5121 path: path.to_owned(),
5122 source,
5123 })
5124}
5125
5126fn add_if_present(inputs: &mut Vec<(PathBuf, PathBuf)>, label: &str, path: PathBuf) {
5127 if path.exists() {
5128 inputs.push((PathBuf::from(label), path));
5129 }
5130}
5131
5132fn required_string<'a>(value: &'a Value, field: &str) -> Result<&'a str, String> {
5133 value
5134 .get(field)
5135 .and_then(Value::as_str)
5136 .ok_or_else(|| format!("Cargo metadata field `{field}` is missing"))
5137}
5138
5139fn optional_string<'a>(value: &'a Value, field: &str) -> Result<Option<&'a str>, String> {
5140 match value.get(field) {
5141 None | Some(Value::Null) => Ok(None),
5142 Some(Value::String(value)) => Ok(Some(value)),
5143 Some(_) => Err(format!(
5144 "Cargo metadata field `{field}` is not a string or null"
5145 )),
5146 }
5147}
5148
5149fn invalid_metadata(message: &str) -> WasmBuildError {
5150 WasmBuildError::InvalidMetadata {
5151 message: message.to_owned(),
5152 }
5153}
5154
5155impl WasmBuildError {
5156 fn indicates_input_change(&self) -> bool {
5157 match self {
5158 Self::InputsChangedDuringAcquisition { .. } => true,
5159 Self::FailedBuildCleanup { build_error, .. } => build_error.indicates_input_change(),
5160 _ => false,
5161 }
5162 }
5163}
5164
5165impl std::fmt::Display for WasmBuildPhase {
5166 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5167 formatter.write_str(match self {
5168 Self::CargoMetadata => "cargo metadata",
5169 Self::CargoIdentity => "Cargo identity",
5170 Self::RustcIdentity => "Rust compiler identity",
5171 Self::CargoBuild => "cargo build",
5172 })
5173 }
5174}
5175
5176impl std::fmt::Display for WasmBuildProgressPhase {
5177 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5178 formatter.write_str(match self {
5179 Self::ExactCacheLock => "exact cache lock",
5180 Self::CargoIdentity => "Cargo identity",
5181 Self::RustcIdentity => "Rust compiler identity",
5182 Self::CargoMetadata => "Cargo metadata",
5183 Self::InputDiscovery => "input discovery",
5184 Self::ContentHashing => "content hashing",
5185 Self::SharedTargetLock => "shared target lock",
5186 Self::SharedTargetMaintenance => "shared target maintenance",
5187 Self::CargoBuild => "Cargo build",
5188 Self::ArtifactPublication => "artifact publication",
5189 Self::ExactCacheMaintenance => "exact cache maintenance",
5190 })
5191 }
5192}
5193
5194impl std::fmt::Display for WasmBuildError {
5195 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5196 match self {
5197 Self::InvalidSpec { message } => {
5198 write!(formatter, "invalid Wasm build spec: {message}")
5199 }
5200 Self::Io {
5201 operation,
5202 path,
5203 source,
5204 } => write!(
5205 formatter,
5206 "failed to {operation} at {}: {source}",
5207 path.display()
5208 ),
5209 Self::CommandSpawn {
5210 phase,
5211 program,
5212 source,
5213 } => write!(
5214 formatter,
5215 "failed to launch {phase} using `{}`: {source}",
5216 program.to_string_lossy(),
5217 ),
5218 Self::CommandFailed {
5219 phase,
5220 status,
5221 stdout,
5222 stderr,
5223 } => write!(
5224 formatter,
5225 "{phase} failed with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}",
5226 ),
5227 Self::InvalidMetadata { message } => {
5228 write!(formatter, "invalid Cargo metadata: {message}")
5229 }
5230 Self::InvalidCargoConfiguration { path, message } => write!(
5231 formatter,
5232 "invalid Cargo configuration at {}: {message}",
5233 path.display(),
5234 ),
5235 Self::MissingArtifacts { paths } => write!(
5236 formatter,
5237 "cargo build succeeded without producing: {}",
5238 paths
5239 .iter()
5240 .map(|path| path.display().to_string())
5241 .collect::<Vec<_>>()
5242 .join(", "),
5243 ),
5244 Self::InputsChangedDuringAcquisition { before, after } => write!(
5245 formatter,
5246 "Wasm inputs changed during artifact acquisition: {before} -> {after}",
5247 ),
5248 Self::PreparedInputSnapshotInvalidated => formatter.write_str(
5249 "the prepared Wasm input snapshot was invalidated before artifact publication",
5250 ),
5251 Self::FailedBuildCleanup {
5252 build_error,
5253 path,
5254 source,
5255 } => write!(
5256 formatter,
5257 "Wasm build failed ({build_error}) and its incomplete target directory at {} could not be removed: {source}",
5258 path.display(),
5259 ),
5260 }
5261 }
5262}
5263
5264impl std::error::Error for WasmBuildError {
5265 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
5266 match self {
5267 Self::Io { source, .. }
5268 | Self::CommandSpawn { source, .. }
5269 | Self::FailedBuildCleanup { source, .. } => Some(source),
5270 _ => None,
5271 }
5272 }
5273}
5274
5275#[cfg(test)]
5276mod tests;