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