1use crate::cargo::{
2 messages::{MessageDecoder, artifact_matches},
3 observation::schema,
4 selection::{CargoSelection, SelectionFlag},
5};
6use ic_host_process::{
7 child::{CleanupPolicy, OwnedChild},
8 tool::{
9 CompletedExecution, ExecutionError, ExecutionEvidence, ExecutionFailure,
10 ExecutionOperation, OutputLimit, OutputLimits, OutputStream, SuccessfulExit, ToolError,
11 capture_command, communicate_child, communicate_child_with_observer,
12 },
13};
14use serde_json::Value;
15use std::{
16 cell::RefCell,
17 collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
18 ffi::{OsStr, OsString},
19 fs::{self, File},
20 io,
21 path::{Component, Path, PathBuf},
22 process::{Command, ExitStatus, Stdio},
23 sync::{
24 Arc, RwLock,
25 atomic::{AtomicUsize, Ordering},
26 mpsc::{self, RecvTimeoutError},
27 },
28 thread,
29 time::{Duration, Instant, SystemTime},
30};
31use toml::Value as TomlValue;
32
33mod acquisition;
34use acquisition::prepare_exact_cache_entry;
35
36use super::{
37 cache_fs::{
38 ArtifactCacheMaintenance, ArtifactCachePrunePolicy, ArtifactCachePruneReport, CacheFsError,
39 RetainedCacheEntry, cache_entry_last_used, cache_maintenance_due,
40 canonicalize_allow_missing, directory_logical_size,
41 ensure_cache_directory_tag as ensure_cache_tag, is_sha256_directory, lock_cache_file,
42 lock_cache_file_with_wait_observer, perform_scheduled_cache_maintenance,
43 prune_direct_child_directories, record_cache_entry_use as record_entry_use,
44 record_cache_maintenance, remove_path_if_present, remove_unretained_entry,
45 },
46 digest::{
47 FileDigest, InputDigest, InputHasher, LabeledPathDigestCache, copy_file_atomic,
48 destination_matches_bytes, digest_bytes, digest_file, digest_labeled_paths_composable,
49 materialize_verified_output, os_bytes, read_stamp_with_limit,
50 },
51 staging::OwnedDirectory,
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: Vec<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 pending = self.groups[group].indexes.iter().copied().filter(|index| {
1984 self.resolved[*index].is_none() && validate_spec(self.specs[*index]).is_ok()
1985 });
1986 let results = 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 pending
1993 .map(|index| {
1994 let spec = self.specs[index];
1995 let result = (|| {
1996 let parsed = parsed
1997 .as_ref()
1998 .map_err(|message| invalid_metadata(message))?;
1999 resolve_local_inputs(spec, parsed)
2000 })();
2001 (index, result)
2002 })
2003 .collect::<Vec<_>>()
2004 });
2005 let mut discovered = Vec::new();
2006 for (index, result) in results {
2007 match result {
2008 Ok((inputs, exclusions)) => discovered.push((index, inputs, exclusions)),
2009 Err(error) => {
2010 self.resolved[index] =
2011 Some(Err((WasmBuildFailurePhase::InputDiscovery, error)));
2012 }
2013 }
2014 }
2015 (discovered, started.elapsed())
2016 }
2017}
2018
2019fn resolve_tool_identity(
2020 spec: &WasmBuildSpec,
2021 progress: &mut ProgressReporter<'_>,
2022) -> Result<(Vec<u8>, Vec<u8>, Duration), WasmBuildError> {
2023 resolve_tool_identity_with_policy(spec, progress, &mut None)
2024}
2025
2026fn resolve_tool_identity_with_policy(
2027 spec: &WasmBuildSpec,
2028 progress: &mut ProgressReporter<'_>,
2029 policy: &mut Option<&mut CargoInputProbePolicy<'_>>,
2030) -> Result<(Vec<u8>, Vec<u8>, Duration), WasmBuildError> {
2031 let started = Instant::now();
2032 let cargo_identity = progress.run_phase(WasmBuildProgressPhase::CargoIdentity, || {
2033 command_identity_with_policy(
2034 spec,
2035 WasmBuildPhase::CargoIdentity,
2036 &spec.cargo_program,
2037 &["--version", "--verbose"],
2038 policy,
2039 )
2040 })?;
2041 let rustc_program = spec
2042 .extra_env
2043 .get(OsStr::new("RUSTC"))
2044 .unwrap_or(&spec.rustc_program);
2045 let rustc_identity = progress.run_phase(WasmBuildProgressPhase::RustcIdentity, || {
2046 command_identity_with_policy(
2047 spec,
2048 WasmBuildPhase::RustcIdentity,
2049 rustc_program,
2050 &["-vV"],
2051 policy,
2052 )
2053 })?;
2054 Ok((cargo_identity, rustc_identity, started.elapsed()))
2055}
2056
2057impl<'a> BatchResolutionKey<'a> {
2058 fn for_spec(spec: &'a WasmBuildSpec) -> Self {
2059 Self {
2060 workspace_root: &spec.workspace_root,
2061 cargo_program: &spec.cargo_program,
2062 rustc_program: spec
2063 .extra_env
2064 .get(OsStr::new("RUSTC"))
2065 .unwrap_or(&spec.rustc_program),
2066 metadata_arguments: &spec.cargo_selection.metadata_arguments,
2067 environment: effective_environment(spec),
2068 }
2069 }
2070}
2071
2072impl SharedIncrementalTargetInspection {
2073 #[must_use]
2075 pub fn target_dir(&self) -> &Path {
2076 &self.target_dir
2077 }
2078
2079 #[must_use]
2081 pub const fn logical_size_bytes(&self) -> u64 {
2082 self.logical_size_bytes
2083 }
2084
2085 #[must_use]
2087 pub const fn last_used(&self) -> SystemTime {
2088 self.last_used
2089 }
2090
2091 #[must_use]
2093 pub const fn lock_wait(&self) -> Duration {
2094 self.lock_wait
2095 }
2096}
2097
2098impl SharedIncrementalTargetPrunePolicy {
2099 #[must_use]
2101 pub const fn new() -> Self {
2102 Self {
2103 max_age: None,
2104 max_size_bytes: None,
2105 }
2106 }
2107
2108 #[must_use]
2110 pub const fn with_max_age(mut self, max_age: Duration) -> Self {
2111 self.max_age = Some(max_age);
2112 self
2113 }
2114
2115 #[must_use]
2117 pub const fn with_max_size_bytes(mut self, bytes: u64) -> Self {
2118 self.max_size_bytes = Some(bytes);
2119 self
2120 }
2121
2122 #[must_use]
2124 pub const fn max_age(self) -> Option<Duration> {
2125 self.max_age
2126 }
2127
2128 #[must_use]
2130 pub const fn max_size_bytes(self) -> Option<u64> {
2131 self.max_size_bytes
2132 }
2133
2134 fn maintenance_identity(self) -> String {
2135 format!(
2136 "age={:?};size={:?}",
2137 self.max_age.map(|duration| duration.as_nanos()),
2138 self.max_size_bytes
2139 )
2140 }
2141}
2142
2143impl SharedIncrementalTargetMaintenanceConfig {
2144 #[must_use]
2146 pub const fn new(
2147 policy: SharedIncrementalTargetPrunePolicy,
2148 minimum_interval: Duration,
2149 ) -> Self {
2150 Self {
2151 policy,
2152 minimum_interval,
2153 failure_mode: SharedIncrementalTargetMaintenanceFailureMode::Strict,
2154 }
2155 }
2156
2157 #[must_use]
2159 pub const fn with_failure_mode(
2160 mut self,
2161 failure_mode: SharedIncrementalTargetMaintenanceFailureMode,
2162 ) -> Self {
2163 self.failure_mode = failure_mode;
2164 self
2165 }
2166
2167 #[must_use]
2169 pub const fn policy(self) -> SharedIncrementalTargetPrunePolicy {
2170 self.policy
2171 }
2172
2173 #[must_use]
2175 pub const fn minimum_interval(self) -> Duration {
2176 self.minimum_interval
2177 }
2178
2179 #[must_use]
2181 pub const fn failure_mode(self) -> SharedIncrementalTargetMaintenanceFailureMode {
2182 self.failure_mode
2183 }
2184}
2185
2186impl SharedIncrementalTargetMaintenance {
2187 #[must_use]
2189 pub fn target_dir(&self) -> &Path {
2190 &self.target_dir
2191 }
2192
2193 #[must_use]
2195 pub const fn logical_size_bytes_before(&self) -> u64 {
2196 self.logical_size_bytes_before
2197 }
2198
2199 #[must_use]
2201 pub const fn logical_size_bytes_after(&self) -> u64 {
2202 self.logical_size_bytes_after
2203 }
2204
2205 #[must_use]
2207 pub const fn last_used_before(&self) -> SystemTime {
2208 self.last_used_before
2209 }
2210
2211 #[must_use]
2213 pub const fn was_cleared(&self) -> bool {
2214 self.cleared
2215 }
2216
2217 #[must_use]
2219 pub const fn lock_wait(&self) -> Duration {
2220 self.lock_wait
2221 }
2222
2223 #[must_use]
2225 pub const fn maintenance(&self) -> Duration {
2226 self.maintenance
2227 }
2228}
2229
2230impl std::fmt::Display for SharedIncrementalTargetMaintenance {
2231 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2232 write!(
2233 formatter,
2234 "target={} action={} bytes={}=>{} lock={:?} maintenance={:?}",
2235 self.target_dir.display(),
2236 if self.cleared { "cleared" } else { "retained" },
2237 self.logical_size_bytes_before,
2238 self.logical_size_bytes_after,
2239 self.lock_wait,
2240 self.maintenance,
2241 )
2242 }
2243}
2244
2245impl SharedIncrementalTargetMaintenanceOutcome {
2246 #[must_use]
2248 pub fn target_dir(&self) -> &Path {
2249 match self {
2250 Self::Missing { target_dir }
2251 | Self::Skipped { target_dir, .. }
2252 | Self::Failed { target_dir, .. } => target_dir,
2253 Self::Performed { maintenance, .. } => maintenance.target_dir(),
2254 }
2255 }
2256
2257 #[must_use]
2259 pub const fn maintenance(&self) -> Option<&SharedIncrementalTargetMaintenance> {
2260 match self {
2261 Self::Performed { maintenance, .. } => Some(maintenance),
2262 Self::Missing { .. } | Self::Skipped { .. } | Self::Failed { .. } => None,
2263 }
2264 }
2265
2266 #[must_use]
2268 pub const fn was_performed(&self) -> bool {
2269 matches!(self, Self::Performed { .. })
2270 }
2271
2272 #[must_use]
2274 pub const fn lock_wait(&self) -> Option<Duration> {
2275 match self {
2276 Self::Missing { .. } => None,
2277 Self::Skipped { lock_wait, .. } | Self::Failed { lock_wait, .. } => Some(*lock_wait),
2278 Self::Performed { maintenance, .. } => Some(maintenance.lock_wait()),
2279 }
2280 }
2281
2282 #[must_use]
2284 pub const fn schedule_check(&self) -> Option<Duration> {
2285 match self {
2286 Self::Missing { .. } | Self::Failed { .. } => None,
2287 Self::Skipped { schedule_check, .. } | Self::Performed { schedule_check, .. } => {
2288 Some(*schedule_check)
2289 }
2290 }
2291 }
2292
2293 #[must_use]
2295 pub fn failure_message(&self) -> Option<&str> {
2296 match self {
2297 Self::Failed { message, .. } => Some(message),
2298 Self::Missing { .. } | Self::Skipped { .. } | Self::Performed { .. } => None,
2299 }
2300 }
2301}
2302
2303impl std::fmt::Display for SharedIncrementalTargetMaintenanceOutcome {
2304 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2305 match self {
2306 Self::Missing { target_dir } => {
2307 write!(formatter, "target={} action=missing", target_dir.display())
2308 }
2309 Self::Skipped {
2310 target_dir,
2311 lock_wait,
2312 schedule_check,
2313 } => write!(
2314 formatter,
2315 "target={} action=skipped lock={lock_wait:?} schedule={schedule_check:?}",
2316 target_dir.display(),
2317 ),
2318 Self::Performed {
2319 maintenance,
2320 schedule_check,
2321 } => write!(formatter, "{maintenance} schedule={schedule_check:?}"),
2322 Self::Failed {
2323 target_dir,
2324 lock_wait,
2325 message,
2326 } => write!(
2327 formatter,
2328 "target={} action=failed lock={lock_wait:?} error={message}",
2329 target_dir.display(),
2330 ),
2331 }
2332 }
2333}
2334
2335impl std::fmt::Display for WasmBuildTimings {
2336 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2337 write!(
2338 formatter,
2339 "total={:?} lock={:?} shared_lock={:?} inputs={:?} cargo={:?} maintenance={:?}",
2340 self.total,
2341 self.lock_wait,
2342 self.shared_incremental_lock_wait,
2343 self.input_resolution.total,
2344 self.cargo_build,
2345 self.cache_maintenance,
2346 )
2347 }
2348}
2349
2350impl std::fmt::Display for WasmBuildOutcome {
2351 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2352 let state = if self.is_reused() { "reused" } else { "built" };
2353 write!(
2354 formatter,
2355 "{state} fingerprint={} artifacts={} {}",
2356 self.record().fingerprint,
2357 self.record().artifacts.len(),
2358 self.record().timings,
2359 )?;
2360 if let Some(maintenance) = self.record().shared_incremental_maintenance() {
2361 write!(formatter, " shared_maintenance=({maintenance})")?;
2362 }
2363 Ok(())
2364 }
2365}
2366
2367pub fn resolve_cargo_build_inputs(
2381 spec: &WasmBuildSpec,
2382) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2383 validate_spec(spec)?;
2384 build_fingerprint(spec)
2385}
2386
2387pub fn resolve_cargo_build_inputs_with_probe_policy(
2399 spec: &WasmBuildSpec,
2400 policy: &mut CargoInputProbePolicy<'_>,
2401) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
2402 validate_spec(spec)?;
2403 build_fingerprint_with_policy(spec, &mut ProgressReporter::silent(), &mut Some(policy))
2404}
2405
2406pub fn inspect_shared_incremental_target(
2414 spec: &WasmBuildSpec,
2415) -> Result<Option<SharedIncrementalTargetInspection>, WasmBuildError> {
2416 if !shared_incremental_target_exists(spec, "inspect shared incremental Cargo target")? {
2417 return Ok(None);
2418 }
2419
2420 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2421 let logical_size_bytes =
2422 directory_logical_size(&canonical).map_err(|source| WasmBuildError::Io {
2423 operation: "measure shared incremental Cargo target",
2424 path: canonical.clone(),
2425 source,
2426 })?;
2427 let last_used = cache_entry_last_used(&canonical).map_err(|source| WasmBuildError::Io {
2428 operation: "read shared incremental Cargo target use time",
2429 path: canonical.clone(),
2430 source,
2431 })?;
2432 Ok(Some(SharedIncrementalTargetInspection {
2433 target_dir: canonical,
2434 logical_size_bytes,
2435 last_used,
2436 lock_wait,
2437 }))
2438}
2439
2440pub fn maintain_shared_incremental_target(
2458 spec: &WasmBuildSpec,
2459 policy: SharedIncrementalTargetPrunePolicy,
2460) -> Result<Option<SharedIncrementalTargetMaintenance>, WasmBuildError> {
2461 if !shared_incremental_target_exists(
2462 spec,
2463 "inspect shared incremental Cargo target before maintenance",
2464 )? {
2465 return Ok(None);
2466 }
2467
2468 let _ = resolve_cargo_build_inputs(spec)?;
2472 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2473 maintain_shared_incremental_target_locked(&canonical, policy, lock_wait).map(Some)
2474}
2475
2476pub fn maintain_shared_incremental_target_at_most_every(
2494 spec: &WasmBuildSpec,
2495 policy: SharedIncrementalTargetPrunePolicy,
2496 minimum_interval: Duration,
2497) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2498 let target_dir =
2499 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
2500 message: "shared incremental target is not configured".to_owned(),
2501 })?;
2502 if !shared_incremental_target_exists(
2503 spec,
2504 "inspect shared incremental Cargo target before scheduled maintenance",
2505 )? {
2506 return Ok(SharedIncrementalTargetMaintenanceOutcome::Missing { target_dir });
2507 }
2508
2509 let (_lock, lock_wait, canonical) = lock_shared_incremental_target(spec)?;
2510 let schedule = schedule_shared_incremental_target_maintenance(
2511 &canonical,
2512 policy,
2513 minimum_interval,
2514 lock_wait,
2515 )?;
2516 let schedule = match schedule {
2517 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => return Ok(outcome),
2518 SharedIncrementalTargetMaintenanceSchedule::Due(due) => due,
2519 };
2520
2521 let _ = resolve_cargo_build_inputs(spec)?;
2524 perform_due_shared_incremental_target_maintenance(&canonical, policy, lock_wait, schedule)
2525}
2526
2527enum SharedIncrementalTargetMaintenanceSchedule {
2528 Skipped(SharedIncrementalTargetMaintenanceOutcome),
2529 Due(DueSharedIncrementalTargetMaintenance),
2530}
2531
2532struct DueSharedIncrementalTargetMaintenance {
2533 schedule_root: PathBuf,
2534 maintenance_identity: String,
2535 schedule_check: Duration,
2536}
2537
2538fn schedule_shared_incremental_target_maintenance(
2539 canonical: &Path,
2540 policy: SharedIncrementalTargetPrunePolicy,
2541 minimum_interval: Duration,
2542 lock_wait: Duration,
2543) -> Result<SharedIncrementalTargetMaintenanceSchedule, WasmBuildError> {
2544 let schedule_root = canonical.join(".ic-testkit");
2545 let maintenance_identity = policy.maintenance_identity();
2546 let schedule_started = Instant::now();
2547 let due = cache_maintenance_due(
2548 &schedule_root,
2549 Some(minimum_interval),
2550 &maintenance_identity,
2551 )
2552 .map_err(wasm_cache_fs_error)?;
2553 let schedule_check = schedule_started.elapsed();
2554 if !due {
2555 return Ok(SharedIncrementalTargetMaintenanceSchedule::Skipped(
2556 SharedIncrementalTargetMaintenanceOutcome::Skipped {
2557 target_dir: canonical.to_owned(),
2558 lock_wait,
2559 schedule_check,
2560 },
2561 ));
2562 }
2563 Ok(SharedIncrementalTargetMaintenanceSchedule::Due(
2564 DueSharedIncrementalTargetMaintenance {
2565 schedule_root,
2566 maintenance_identity,
2567 schedule_check,
2568 },
2569 ))
2570}
2571
2572fn perform_due_shared_incremental_target_maintenance(
2573 canonical: &Path,
2574 policy: SharedIncrementalTargetPrunePolicy,
2575 lock_wait: Duration,
2576 due: DueSharedIncrementalTargetMaintenance,
2577) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2578 let DueSharedIncrementalTargetMaintenance {
2579 schedule_root,
2580 maintenance_identity,
2581 schedule_check,
2582 } = due;
2583 let maintenance = maintain_shared_incremental_target_locked(canonical, policy, lock_wait)?;
2584 record_cache_maintenance(&schedule_root, &maintenance_identity).map_err(wasm_cache_fs_error)?;
2585 Ok(SharedIncrementalTargetMaintenanceOutcome::Performed {
2586 maintenance,
2587 schedule_check,
2588 })
2589}
2590
2591fn maintain_shared_incremental_target_locked(
2592 canonical: &Path,
2593 policy: SharedIncrementalTargetPrunePolicy,
2594 lock_wait: Duration,
2595) -> Result<SharedIncrementalTargetMaintenance, WasmBuildError> {
2596 let started = Instant::now();
2597 let logical_size_bytes_before =
2598 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
2599 operation: "measure shared incremental Cargo target before maintenance",
2600 path: canonical.to_owned(),
2601 source,
2602 })?;
2603 let last_used_before =
2604 cache_entry_last_used(canonical).map_err(|source| WasmBuildError::Io {
2605 operation: "read shared incremental Cargo target use time before maintenance",
2606 path: canonical.to_owned(),
2607 source,
2608 })?;
2609 let expired = policy.max_age.is_some_and(|max_age| {
2610 SystemTime::now()
2611 .duration_since(last_used_before)
2612 .is_ok_and(|age| age > max_age)
2613 });
2614 let oversized = policy
2615 .max_size_bytes
2616 .is_some_and(|max_size_bytes| logical_size_bytes_before > max_size_bytes);
2617 let cleared = expired || oversized;
2618 if cleared {
2619 clear_shared_incremental_target_contents(canonical)?;
2620 record_cache_entry_use(canonical)?;
2621 }
2622 let logical_size_bytes_after = if cleared {
2623 directory_logical_size(canonical).map_err(|source| WasmBuildError::Io {
2624 operation: "measure shared incremental Cargo target after maintenance",
2625 path: canonical.to_owned(),
2626 source,
2627 })?
2628 } else {
2629 logical_size_bytes_before
2630 };
2631 Ok(SharedIncrementalTargetMaintenance {
2632 target_dir: canonical.to_owned(),
2633 logical_size_bytes_before,
2634 logical_size_bytes_after,
2635 last_used_before,
2636 cleared,
2637 lock_wait,
2638 maintenance: started.elapsed(),
2639 })
2640}
2641
2642fn clear_shared_incremental_target_contents(target_dir: &Path) -> Result<(), WasmBuildError> {
2643 let entries = fs::read_dir(target_dir).map_err(|source| WasmBuildError::Io {
2644 operation: "read shared incremental Cargo target for maintenance",
2645 path: target_dir.to_owned(),
2646 source,
2647 })?;
2648 for entry in entries {
2649 let path = entry
2650 .map_err(|source| WasmBuildError::Io {
2651 operation: "read shared incremental Cargo target entry for maintenance",
2652 path: target_dir.to_owned(),
2653 source,
2654 })?
2655 .path();
2656 let preserved = path
2657 .file_name()
2658 .is_some_and(|name| name == ".ic-testkit" || name == "CACHEDIR.TAG");
2659 if !preserved {
2660 remove_path_if_present(&path).map_err(|source| WasmBuildError::Io {
2661 operation: "clear shared incremental Cargo target entry",
2662 path,
2663 source,
2664 })?;
2665 }
2666 }
2667 Ok(())
2668}
2669
2670pub fn build_wasm_canisters_cached(
2692 spec: &WasmBuildSpec,
2693) -> Result<WasmBuildOutcome, WasmBuildError> {
2694 build_wasm_canisters_cached_internal(spec, &mut ProgressReporter::silent(), None)
2695}
2696
2697pub(super) fn build_wasm_canisters_cached_in_batch(
2698 spec: &WasmBuildSpec,
2699 index: usize,
2700 resolver: &mut WasmBuildBatchInputResolver<'_, '_>,
2701) -> WasmBuildBatchAttempt {
2702 let started = Instant::now();
2703 let mut progress = ProgressReporter::silent();
2704 let result = build_wasm_canisters_cached_internal(spec, &mut progress, Some((resolver, index)));
2705 if result
2706 .as_ref()
2707 .is_err_and(WasmBuildError::indicates_input_change)
2708 {
2709 resolver.invalidate_source_lease();
2710 }
2711 batch_result(result, &progress, started.elapsed())
2712}
2713
2714pub fn build_wasm_canisters_cached_with_progress<F>(
2731 spec: &WasmBuildSpec,
2732 config: WasmBuildProgressConfig,
2733 mut observer: F,
2734) -> Result<WasmBuildOutcome, WasmBuildError>
2735where
2736 F: FnMut(WasmBuildProgressEvent),
2737{
2738 if config.heartbeat_interval == Some(Duration::ZERO) {
2739 return Err(WasmBuildError::InvalidSpec {
2740 message: "Wasm build progress heartbeat interval must be greater than zero".to_owned(),
2741 });
2742 }
2743 build_wasm_canisters_cached_internal(
2744 spec,
2745 &mut ProgressReporter::observed(config, &mut observer),
2746 None,
2747 )
2748}
2749
2750pub(super) fn build_wasm_canisters_cached_in_batch_with_progress<F>(
2751 spec: &WasmBuildSpec,
2752 index: usize,
2753 resolver: &mut WasmBuildBatchInputResolver<'_, '_>,
2754 config: WasmBuildProgressConfig,
2755 mut observer: F,
2756) -> WasmBuildBatchAttempt
2757where
2758 F: FnMut(WasmBuildProgressEvent),
2759{
2760 if config.heartbeat_interval == Some(Duration::ZERO) {
2761 return WasmBuildBatchAttempt {
2762 result: Err((
2763 WasmBuildError::InvalidSpec {
2764 message: "Wasm build progress heartbeat interval must be greater than zero"
2765 .to_owned(),
2766 },
2767 WasmBuildFailureDetails::specification(Duration::ZERO),
2768 )),
2769 };
2770 }
2771 let started = Instant::now();
2772 let mut progress = ProgressReporter::observed(config, &mut observer);
2773 let result = build_wasm_canisters_cached_internal(spec, &mut progress, Some((resolver, index)));
2774 if result
2775 .as_ref()
2776 .is_err_and(WasmBuildError::indicates_input_change)
2777 {
2778 resolver.invalidate_source_lease();
2779 }
2780 batch_result(result, &progress, started.elapsed())
2781}
2782
2783fn batch_result(
2784 result: Result<WasmBuildOutcome, WasmBuildError>,
2785 progress: &ProgressReporter<'_>,
2786 total: Duration,
2787) -> WasmBuildBatchAttempt {
2788 WasmBuildBatchAttempt {
2789 result: result.map_err(|error| {
2790 let details = progress.failure_details(&error, total);
2791 (error, details)
2792 }),
2793 }
2794}
2795
2796fn batch_source_assumptions(
2797 batch_resolution: Option<&(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2798) -> (bool, Option<Arc<RwLock<bool>>>) {
2799 batch_resolution.map_or((false, None), |(resolver, _)| {
2800 (
2801 resolver.assumes_sources_immutable(),
2802 resolver.prepared_invalidation(),
2803 )
2804 })
2805}
2806
2807fn build_wasm_canisters_cached_internal(
2808 spec: &WasmBuildSpec,
2809 progress: &mut ProgressReporter<'_>,
2810 mut batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2811) -> Result<WasmBuildOutcome, WasmBuildError> {
2812 let total_started = Instant::now();
2813 validate_spec(spec)?;
2814 let (assumes_sources_immutable, prepared_invalidation) =
2815 batch_source_assumptions(batch_resolution.as_ref());
2816 progress.emit(WasmBuildProgressEvent::Started);
2817 if spec.shared_incremental_maintenance_config.is_some() {
2818 let outcome = build_wasm_canisters_cached_with_scheduled_shared_maintenance(
2819 spec,
2820 total_started,
2821 progress,
2822 batch_resolution.take(),
2823 )?;
2824 emit_finished_progress(&outcome, progress);
2825 return Ok(outcome);
2826 }
2827 let (cache_lock, first_lock_wait) =
2828 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2829 ensure_cache_directory_tag(&spec.target_dir)?;
2830
2831 let resolved = resolve_initial_inputs(spec, batch_resolution.take(), progress)?;
2832 let isolated_acquisition =
2833 WasmAcquisitionContext::isolated(prepared_invalidation.clone(), assumes_sources_immutable);
2834 if let Some(outcome) = try_reuse_wasm_artifacts(
2835 spec,
2836 &resolved,
2837 first_lock_wait,
2838 &isolated_acquisition,
2839 total_started,
2840 progress,
2841 )? {
2842 emit_finished_progress(&outcome, progress);
2843 return Ok(outcome);
2844 }
2845 progress.emit(WasmBuildProgressEvent::CacheMiss {
2846 fingerprint: resolved.fingerprint,
2847 });
2848
2849 let outcome = match &spec.cache_mode {
2850 WasmBuildCacheMode::Isolated => {
2851 let cache_entry = cache_entry_directory(spec, resolved.fingerprint);
2852 build_wasm_cache_miss(
2853 spec,
2854 &resolved,
2855 first_lock_wait,
2856 isolated_acquisition,
2857 &cache_entry,
2858 total_started,
2859 progress,
2860 )
2861 }
2862 WasmBuildCacheMode::SharedIncremental { .. } => {
2863 drop(cache_lock);
2864 build_wasm_with_shared_incremental(
2865 spec,
2866 resolved,
2867 first_lock_wait,
2868 assumes_sources_immutable,
2869 prepared_invalidation,
2870 total_started,
2871 progress,
2872 )
2873 }
2874 }?;
2875 emit_finished_progress(&outcome, progress);
2876 Ok(outcome)
2877}
2878
2879fn build_wasm_with_shared_incremental(
2880 spec: &WasmBuildSpec,
2881 resolved: ResolvedCargoBuildInputs,
2882 first_lock_wait: Duration,
2883 assumes_sources_immutable: bool,
2884 prepared_invalidation: Option<Arc<RwLock<bool>>>,
2885 total_started: Instant,
2886 progress: &mut ProgressReporter<'_>,
2887) -> Result<WasmBuildOutcome, WasmBuildError> {
2888 let (shared_lock, shared_lock_wait, shared_target) =
2889 lock_shared_incremental_target_with_progress(spec, progress)?;
2890 let (_cache_lock, second_lock_wait) =
2891 lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2892 ensure_cache_directory_tag(&spec.target_dir)?;
2893
2894 let current = if assumes_sources_immutable {
2895 resolved
2896 } else {
2897 let mut current = resolve_initial_inputs(spec, None, progress)?;
2898 current.timings.include(resolved.timings);
2899 current
2900 };
2901 let lock_wait = first_lock_wait.saturating_add(second_lock_wait);
2902 let shared_incremental = WasmAcquisitionContext::shared(
2903 shared_lock_wait,
2904 None,
2905 prepared_invalidation,
2906 assumes_sources_immutable,
2907 );
2908 if let Some(outcome) = try_reuse_wasm_artifacts(
2909 spec,
2910 ¤t,
2911 lock_wait,
2912 &shared_incremental,
2913 total_started,
2914 progress,
2915 )? {
2916 return Ok(outcome);
2917 }
2918
2919 let outcome = build_wasm_cache_miss(
2920 spec,
2921 ¤t,
2922 lock_wait,
2923 shared_incremental,
2924 &shared_target,
2925 total_started,
2926 progress,
2927 );
2928 drop(shared_lock);
2929 outcome
2930}
2931
2932fn build_wasm_canisters_cached_with_scheduled_shared_maintenance(
2933 spec: &WasmBuildSpec,
2934 total_started: Instant,
2935 progress: &mut ProgressReporter<'_>,
2936 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
2937) -> Result<WasmBuildOutcome, WasmBuildError> {
2938 let (assumes_sources_immutable, prepared_invalidation) =
2939 batch_source_assumptions(batch_resolution.as_ref());
2940 let (_shared_lock, shared_lock_wait, shared_target) =
2941 lock_shared_incremental_target_with_progress(spec, progress)?;
2942 let (_cache_lock, lock_wait) = lock_wasm_build_cache_with_progress(&spec.target_dir, progress)?;
2943 ensure_cache_directory_tag(&spec.target_dir)?;
2944
2945 let resolved = resolve_initial_inputs(spec, batch_resolution, progress)?;
2948 let shared_maintenance = perform_configured_shared_incremental_target_maintenance(
2949 spec,
2950 &shared_target,
2951 shared_lock_wait,
2952 progress,
2953 )?;
2954 let shared_incremental = WasmAcquisitionContext::shared(
2955 shared_lock_wait,
2956 Some(shared_maintenance),
2957 prepared_invalidation,
2958 assumes_sources_immutable,
2959 );
2960 if let Some(outcome) = try_reuse_wasm_artifacts(
2961 spec,
2962 &resolved,
2963 lock_wait,
2964 &shared_incremental,
2965 total_started,
2966 progress,
2967 )? {
2968 return Ok(outcome);
2969 }
2970 progress.emit(WasmBuildProgressEvent::CacheMiss {
2971 fingerprint: resolved.fingerprint,
2972 });
2973 build_wasm_cache_miss(
2974 spec,
2975 &resolved,
2976 lock_wait,
2977 shared_incremental,
2978 &shared_target,
2979 total_started,
2980 progress,
2981 )
2982}
2983
2984fn perform_configured_shared_incremental_target_maintenance(
2985 spec: &WasmBuildSpec,
2986 shared_target: &Path,
2987 lock_wait: Duration,
2988 progress: &mut ProgressReporter<'_>,
2989) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
2990 let config = spec
2991 .shared_incremental_maintenance_config
2992 .expect("configured shared-target maintenance must have settings");
2993 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceStarted {
2994 target_dir: shared_target.to_owned(),
2995 });
2996 let result = progress.run_phase(WasmBuildProgressPhase::SharedTargetMaintenance, || {
2997 let schedule = schedule_shared_incremental_target_maintenance(
2998 shared_target,
2999 config.policy,
3000 config.minimum_interval,
3001 lock_wait,
3002 )?;
3003 match schedule {
3004 SharedIncrementalTargetMaintenanceSchedule::Skipped(outcome) => Ok(outcome),
3005 SharedIncrementalTargetMaintenanceSchedule::Due(due) => {
3006 perform_due_shared_incremental_target_maintenance(
3007 shared_target,
3008 config.policy,
3009 lock_wait,
3010 due,
3011 )
3012 }
3013 }
3014 });
3015 let outcome = integrated_shared_maintenance_result(config, shared_target, lock_wait, result)?;
3016 progress.emit(WasmBuildProgressEvent::SharedTargetMaintenanceFinished {
3017 outcome: outcome.clone(),
3018 });
3019 Ok(outcome)
3020}
3021
3022fn integrated_shared_maintenance_result(
3023 config: SharedIncrementalTargetMaintenanceConfig,
3024 shared_target: &Path,
3025 lock_wait: Duration,
3026 result: Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError>,
3027) -> Result<SharedIncrementalTargetMaintenanceOutcome, WasmBuildError> {
3028 match result {
3029 Ok(outcome) => Ok(outcome),
3030 Err(error)
3031 if config.failure_mode == SharedIncrementalTargetMaintenanceFailureMode::BestEffort =>
3032 {
3033 Ok(SharedIncrementalTargetMaintenanceOutcome::Failed {
3034 target_dir: shared_target.to_owned(),
3035 lock_wait,
3036 message: error.to_string(),
3037 })
3038 }
3039 Err(error) => Err(error),
3040 }
3041}
3042
3043fn resolve_initial_inputs(
3044 spec: &WasmBuildSpec,
3045 batch_resolution: Option<(&mut WasmBuildBatchInputResolver<'_, '_>, usize)>,
3046 progress: &mut ProgressReporter<'_>,
3047) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3048 let resolved = if let Some((resolver, index)) = batch_resolution {
3049 resolver.resolve(index, progress)?
3050 } else {
3051 build_fingerprint_with_progress(spec, progress)?
3052 };
3053 validate_wasm_library_outputs(&resolved, progress)?;
3054 progress.emit(WasmBuildProgressEvent::InputsResolved {
3055 fingerprint: resolved.fingerprint,
3056 input_digest: resolved.input_digest,
3057 elapsed: resolved.timings.total,
3058 });
3059 Ok(resolved)
3060}
3061
3062fn validate_wasm_library_outputs(
3063 resolved: &ResolvedCargoBuildInputs,
3064 progress: &mut ProgressReporter<'_>,
3065) -> Result<(), WasmBuildError> {
3066 if let Some(message) = &resolved.wasm_artifact_error {
3067 progress.begin_phase(WasmBuildFailurePhase::InputDiscovery);
3068 return Err(WasmBuildError::InvalidSpec {
3069 message: message.clone(),
3070 });
3071 }
3072 Ok(())
3073}
3074
3075fn emit_finished_progress(outcome: &WasmBuildOutcome, progress: &mut ProgressReporter<'_>) {
3076 let state = if outcome.is_reused() {
3077 progress.emit(WasmBuildProgressEvent::CacheHit {
3078 fingerprint: outcome.record().fingerprint,
3079 });
3080 WasmBuildProgressOutcome::Reused
3081 } else {
3082 WasmBuildProgressOutcome::Built
3083 };
3084 progress.emit(WasmBuildProgressEvent::Finished {
3085 outcome: state,
3086 fingerprint: outcome.record().fingerprint,
3087 elapsed: outcome.record().timings.total,
3088 });
3089}
3090
3091#[derive(Clone, Debug, Default)]
3092struct WasmAcquisitionContext {
3093 assumes_sources_immutable: bool,
3094 lock_wait: Option<Duration>,
3095 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
3096 prepared_invalidation: Option<Arc<RwLock<bool>>>,
3097}
3098
3099impl WasmAcquisitionContext {
3100 fn isolated(
3101 prepared_invalidation: Option<Arc<RwLock<bool>>>,
3102 assumes_sources_immutable: bool,
3103 ) -> Self {
3104 Self {
3105 assumes_sources_immutable,
3106 prepared_invalidation,
3107 ..Self::default()
3108 }
3109 }
3110
3111 const fn shared(
3112 lock_wait: Duration,
3113 maintenance: Option<SharedIncrementalTargetMaintenanceOutcome>,
3114 prepared_invalidation: Option<Arc<RwLock<bool>>>,
3115 assumes_sources_immutable: bool,
3116 ) -> Self {
3117 Self {
3118 assumes_sources_immutable,
3119 lock_wait: Some(lock_wait),
3120 maintenance,
3121 prepared_invalidation,
3122 }
3123 }
3124
3125 fn lock_prepared_publication(
3126 &self,
3127 ) -> Result<Option<std::sync::RwLockReadGuard<'_, bool>>, WasmBuildError> {
3128 let guard = self.prepared_invalidation.as_deref().map(|invalidation| {
3129 invalidation
3130 .read()
3131 .unwrap_or_else(std::sync::PoisonError::into_inner)
3132 });
3133 if guard.as_deref().is_some_and(|invalidated| *invalidated) {
3134 return Err(WasmBuildError::PreparedInputSnapshotInvalidated);
3135 }
3136 Ok(guard)
3137 }
3138}
3139
3140fn try_reuse_wasm_artifacts(
3141 spec: &WasmBuildSpec,
3142 resolved: &ResolvedCargoBuildInputs,
3143 lock_wait: Duration,
3144 shared_incremental: &WasmAcquisitionContext,
3145 total_started: Instant,
3146 progress: &mut ProgressReporter<'_>,
3147) -> Result<Option<WasmBuildOutcome>, WasmBuildError> {
3148 let _publication_guard = shared_incremental.lock_prepared_publication()?;
3149 let fingerprint = resolved.fingerprint;
3150 let artifacts = expected_artifacts(spec, &spec.target_dir, &resolved.wasm_targets);
3151 let cache_entry = cache_entry_directory(spec, fingerprint);
3152 let cached_artifacts = expected_artifacts(spec, &cache_entry, &resolved.wasm_targets);
3153 let cached_info = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3154 validated_artifact_set(&cached_artifacts, fingerprint)
3155 });
3156 let artifact_info = if let Some(info) = cached_info {
3157 info
3158 } else {
3159 let public_is_current = progress
3162 .run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3163 validated_artifact_set(&artifacts, fingerprint).is_some()
3164 });
3165 if !public_is_current {
3166 return Ok(None);
3167 }
3168 reconstruct_exact_cache_entry(
3169 spec,
3170 &artifacts,
3171 &cache_entry,
3172 fingerprint,
3173 &resolved.wasm_targets,
3174 progress,
3175 )?
3176 };
3177 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3178 materialize_artifacts(
3179 &cached_artifacts,
3180 &artifacts,
3181 fingerprint,
3182 &artifact_info,
3183 true,
3184 )?;
3185 record_cache_entry_use(&cache_entry)
3186 })?;
3187 let input_resolution = validate_reused_inputs(spec, resolved, shared_incremental, progress)?;
3188 Ok(Some(WasmBuildOutcome::Reused(complete_build_record(
3189 spec,
3190 BuildRecordInput {
3191 fingerprint,
3192 input_digest: resolved.input_digest,
3193 lock_wait,
3194 shared_incremental: shared_incremental.clone(),
3195 input_resolution,
3196 cargo_build: None,
3197 active_entry: &cache_entry,
3198 targets: &resolved.wasm_targets,
3199 },
3200 total_started,
3201 progress,
3202 )?)))
3203}
3204
3205fn validate_reused_inputs(
3206 spec: &WasmBuildSpec,
3207 resolved: &ResolvedCargoBuildInputs,
3208 context: &WasmAcquisitionContext,
3209 progress: &mut ProgressReporter<'_>,
3210) -> Result<WasmInputResolutionTimings, WasmBuildError> {
3211 let mut timings = resolved.timings;
3212 if !context.assumes_sources_immutable {
3213 let verified = verify_resolved_inputs(spec, resolved, progress)?;
3214 timings.include(verified.timings);
3215 }
3216 Ok(timings)
3217}
3218
3219fn verify_resolved_inputs(
3220 spec: &WasmBuildSpec,
3221 resolved: &ResolvedCargoBuildInputs,
3222 progress: &mut ProgressReporter<'_>,
3223) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3224 let verified = resolve_initial_inputs(spec, None, progress)?;
3225 for (before, after) in [
3226 (resolved.validation_digest, verified.validation_digest),
3227 (resolved.fingerprint, verified.fingerprint),
3228 ] {
3229 if before != after {
3230 return Err(WasmBuildError::InputsChangedDuringAcquisition { before, after });
3231 }
3232 }
3233 Ok(verified)
3234}
3235
3236fn reconstruct_exact_cache_entry(
3237 spec: &WasmBuildSpec,
3238 artifacts: &[PathBuf],
3239 cache_entry: &Path,
3240 fingerprint: InputDigest,
3241 targets: &[WasmArtifactTarget],
3242 progress: &mut ProgressReporter<'_>,
3243) -> Result<Vec<FileDigest>, WasmBuildError> {
3244 let cached_artifacts = expected_artifacts(spec, cache_entry, targets);
3245 let incomplete = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3246 prepare_exact_cache_entry(cache_entry)
3247 })?;
3248 let result = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3249 copy_wasm_artifacts(artifacts, &cached_artifacts)?;
3250 let missing = missing_artifacts(&cached_artifacts);
3251 if !missing.is_empty() {
3252 return Err(WasmBuildError::MissingArtifacts { paths: missing });
3253 }
3254 publish_artifact_stamps(&cached_artifacts, fingerprint)
3257 });
3258 finish_fingerprint_build(result, incomplete, progress)
3259}
3260
3261fn build_wasm_cache_miss(
3262 spec: &WasmBuildSpec,
3263 resolved: &ResolvedCargoBuildInputs,
3264 lock_wait: Duration,
3265 shared_incremental: WasmAcquisitionContext,
3266 cargo_target_dir: &Path,
3267 total_started: Instant,
3268 progress: &mut ProgressReporter<'_>,
3269) -> Result<WasmBuildOutcome, WasmBuildError> {
3270 let fingerprint = resolved.fingerprint;
3271 let mut input_resolution = resolved.timings;
3272 let artifacts = expected_artifacts(spec, &spec.target_dir, &resolved.wasm_targets);
3273 let cache_entry = cache_entry_directory(spec, fingerprint);
3274 let preparation_started = Instant::now();
3275 progress.begin_phase(WasmBuildFailurePhase::ArtifactPublication);
3276 let preparation_result = prepare_exact_cache_entry(&cache_entry);
3277 progress.record_phase(
3278 WasmBuildFailurePhase::ArtifactPublication,
3279 preparation_started.elapsed(),
3280 );
3281 let incomplete_directory = preparation_result?;
3282 let build_result = (|| {
3283 if matches!(
3284 spec.cache_mode,
3285 WasmBuildCacheMode::SharedIncremental { .. }
3286 ) {
3287 record_cache_entry_use(cargo_target_dir)?;
3288 }
3289 let build_started = Instant::now();
3290 progress.begin_phase(WasmBuildFailurePhase::CargoBuild);
3291 let cargo_result =
3292 run_cargo_build(spec, cargo_target_dir, &resolved.wasm_targets, progress);
3293 let cargo_build = build_started.elapsed();
3294 progress.record_phase(WasmBuildFailurePhase::CargoBuild, cargo_build);
3295 let built_artifacts = cargo_result?;
3296 let validation_started = Instant::now();
3297 progress.begin_phase(WasmBuildFailurePhase::ArtifactPublication);
3298 let missing = missing_artifacts(&built_artifacts);
3299 progress.record_phase(
3300 WasmBuildFailurePhase::ArtifactPublication,
3301 validation_started.elapsed(),
3302 );
3303 if !missing.is_empty() {
3304 return Err(WasmBuildError::MissingArtifacts { paths: missing });
3305 }
3306
3307 let verified = verify_resolved_inputs(spec, resolved, progress)?;
3308 input_resolution.include(verified.timings);
3309
3310 let publication_guard = shared_incremental.lock_prepared_publication()?;
3315
3316 let cached_artifacts = expected_artifacts(spec, &cache_entry, &resolved.wasm_targets);
3317 progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3318 if built_artifacts != cached_artifacts {
3319 copy_wasm_artifacts(&built_artifacts, &cached_artifacts)?;
3320 }
3321 let info = publish_artifact_stamps(&cached_artifacts, fingerprint)?;
3322 materialize_artifacts(&cached_artifacts, &artifacts, fingerprint, &info, false)?;
3323 record_cache_entry_use(&cache_entry)
3324 })?;
3325 drop(publication_guard);
3326
3327 Ok(WasmBuildOutcome::Built(complete_build_record(
3328 spec,
3329 BuildRecordInput {
3330 fingerprint,
3331 input_digest: resolved.input_digest,
3332 lock_wait,
3333 shared_incremental,
3334 input_resolution,
3335 cargo_build: Some(cargo_build),
3336 active_entry: &cache_entry,
3337 targets: &resolved.wasm_targets,
3338 },
3339 total_started,
3340 progress,
3341 )?))
3342 })();
3343 finish_fingerprint_build(build_result, incomplete_directory, progress)
3344}
3345
3346pub fn prune_wasm_build_cache(
3358 target_dir: &Path,
3359 policy: ArtifactCachePrunePolicy,
3360) -> Result<ArtifactCachePruneReport, WasmBuildError> {
3361 let (_lock_file, _) = lock_wasm_build_cache(target_dir)?;
3362 ensure_cache_directory_tag(target_dir)?;
3363
3364 prune_wasm_build_cache_locked(target_dir, policy, None)
3365}
3366
3367struct BuildRecordInput<'a> {
3368 fingerprint: InputDigest,
3369 input_digest: InputDigest,
3370 lock_wait: Duration,
3371 shared_incremental: WasmAcquisitionContext,
3372 input_resolution: WasmInputResolutionTimings,
3373 cargo_build: Option<Duration>,
3374 active_entry: &'a Path,
3375 targets: &'a [WasmArtifactTarget],
3376}
3377
3378fn complete_build_record(
3379 spec: &WasmBuildSpec,
3380 input: BuildRecordInput<'_>,
3381 total_started: Instant,
3382 progress: &mut ProgressReporter<'_>,
3383) -> Result<WasmBuildRecord, WasmBuildError> {
3384 let retention = progress.run_phase(WasmBuildProgressPhase::ArtifactPublication, || {
3385 RetainedCacheEntry::acquire(input.active_entry).map_err(wasm_cache_fs_error)
3386 })?;
3387 let (maintenance, cache_maintenance) = spec.prune_policy.map_or((None, None), |policy| {
3388 progress.run_phase(WasmBuildProgressPhase::ExactCacheMaintenance, || {
3389 let cache_root = spec.target_dir.join(".ic-testkit/wasm-targets");
3390 let identity = policy.maintenance_identity();
3391 perform_scheduled_cache_maintenance(&cache_root, spec.prune_interval, &identity, || {
3392 prune_wasm_build_cache_locked(&spec.target_dir, policy, Some(input.active_entry))
3393 .map_err(|error| error.to_string())
3394 })
3395 })
3396 });
3397 Ok(WasmBuildRecord {
3398 fingerprint: input.fingerprint,
3399 input_digest: input.input_digest,
3400 artifacts: expected_artifacts(spec, input.active_entry, input.targets),
3401 retention,
3402 timings: WasmBuildTimings {
3403 lock_wait: input.lock_wait,
3404 shared_incremental_lock_wait: input.shared_incremental.lock_wait,
3405 input_resolution: input.input_resolution,
3406 cargo_build: input.cargo_build,
3407 cache_maintenance,
3408 total: total_started.elapsed(),
3409 },
3410 maintenance,
3411 shared_incremental_maintenance: input.shared_incremental.maintenance,
3412 })
3413}
3414
3415fn prune_wasm_build_cache_locked(
3416 target_dir: &Path,
3417 policy: ArtifactCachePrunePolicy,
3418 protected_entry: Option<&Path>,
3419) -> Result<ArtifactCachePruneReport, WasmBuildError> {
3420 let cache_root = target_dir.join(".ic-testkit/wasm-targets");
3421 prune_direct_child_directories(&cache_root, policy, protected_entry, is_sha256_directory)
3422 .map_err(wasm_cache_fs_error)
3423}
3424
3425fn finish_fingerprint_build<T>(
3426 result: Result<T, WasmBuildError>,
3427 incomplete_directory: OwnedDirectory,
3428 progress: &mut ProgressReporter<'_>,
3429) -> Result<T, WasmBuildError> {
3430 match result {
3431 Ok(outcome) => {
3432 incomplete_directory.preserve();
3433 Ok(outcome)
3434 }
3435 Err(build_error) => Err(cleanup_failed_fingerprint_build(
3436 build_error,
3437 incomplete_directory,
3438 progress,
3439 )),
3440 }
3441}
3442
3443fn cleanup_failed_fingerprint_build(
3444 build_error: WasmBuildError,
3445 incomplete_directory: OwnedDirectory,
3446 progress: &mut ProgressReporter<'_>,
3447) -> WasmBuildError {
3448 let path = incomplete_directory.path().to_owned();
3449 let primary_phase = progress.failure_phase;
3450 let cleanup_started = Instant::now();
3451 let cleanup = incomplete_directory.cleanup();
3452 progress.record_phase(WasmBuildFailurePhase::Cleanup, cleanup_started.elapsed());
3453 match cleanup {
3454 Ok(()) => {
3455 progress.failure_phase = primary_phase;
3456 build_error
3457 }
3458 Err(source) => WasmBuildError::FailedBuildCleanup {
3459 build_error: Box::new(build_error),
3460 path,
3461 source,
3462 },
3463 }
3464}
3465
3466fn lock_wasm_build_cache(target_dir: &Path) -> Result<(File, Duration), WasmBuildError> {
3467 create_dir_all(target_dir, "create Cargo target directory")?;
3468 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
3469 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)
3470}
3471
3472fn lock_wasm_build_cache_with_progress(
3473 target_dir: &Path,
3474 progress: &mut ProgressReporter<'_>,
3475) -> Result<(File, Duration), WasmBuildError> {
3476 progress.begin_phase(WasmBuildFailurePhase::ExactCacheCoordination);
3477 create_dir_all(target_dir, "create Cargo target directory")?;
3478 let lock_path = target_dir.join(".ic-testkit/wasm-build.lock");
3479 lock_cache_file_with_progress(&lock_path, WasmBuildProgressPhase::ExactCacheLock, progress)
3480}
3481
3482fn lock_shared_incremental_target(
3483 spec: &WasmBuildSpec,
3484) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3485 lock_shared_incremental_target_internal(spec, None)
3486}
3487
3488fn lock_shared_incremental_target_with_progress(
3489 spec: &WasmBuildSpec,
3490 progress: &mut ProgressReporter<'_>,
3491) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3492 let target_dir = shared_incremental_target(spec)
3493 .expect("validated shared acquisition must have a shared Cargo target");
3494 progress.emit(WasmBuildProgressEvent::SharedTargetLockStarted { target_dir });
3495 let (lock, wait, canonical) = lock_shared_incremental_target_internal(spec, Some(progress))?;
3496 progress.emit(WasmBuildProgressEvent::SharedTargetLockAcquired {
3497 target_dir: canonical.clone(),
3498 wait,
3499 });
3500 Ok((lock, wait, canonical))
3501}
3502
3503fn lock_shared_incremental_target_internal(
3504 spec: &WasmBuildSpec,
3505 mut progress: Option<&mut ProgressReporter<'_>>,
3506) -> Result<(File, Duration, PathBuf), WasmBuildError> {
3507 if let Some(progress) = progress.as_deref_mut() {
3508 progress.begin_phase(WasmBuildFailurePhase::SharedTargetCoordination);
3509 }
3510 let target_dir =
3511 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
3512 message: "shared incremental target is not configured".to_owned(),
3513 })?;
3514 create_dir_all(
3515 &target_dir,
3516 "create shared incremental Cargo target directory",
3517 )?;
3518 ensure_cache_tag(&target_dir).map_err(wasm_cache_fs_error)?;
3519 let canonical = target_dir
3520 .canonicalize()
3521 .map_err(|source| WasmBuildError::Io {
3522 operation: "resolve shared incremental Cargo target directory",
3523 path: target_dir.clone(),
3524 source,
3525 })?;
3526 let lock_path = canonical.join(".ic-testkit/wasm-incremental.lock");
3527 let (lock, wait) = if let Some(progress) = progress {
3528 lock_cache_file_with_progress(
3529 &lock_path,
3530 WasmBuildProgressPhase::SharedTargetLock,
3531 progress,
3532 )?
3533 } else {
3534 lock_cache_file(&lock_path).map_err(wasm_cache_fs_error)?
3535 };
3536 Ok((lock, wait, canonical))
3537}
3538
3539fn lock_cache_file_with_progress(
3540 lock_path: &Path,
3541 phase: WasmBuildProgressPhase,
3542 progress: &mut ProgressReporter<'_>,
3543) -> Result<(File, Duration), WasmBuildError> {
3544 let failure_phase = progress_failure_phase(phase);
3545 let started = Instant::now();
3546 progress.begin_phase(failure_phase);
3547 let result = if !progress.is_observed() || progress.config.heartbeat_interval.is_none() {
3548 lock_cache_file(lock_path).map_err(wasm_cache_fs_error)
3549 } else {
3550 let heartbeat_interval = progress
3551 .config
3552 .heartbeat_interval
3553 .expect("observed cache lock must have a heartbeat interval");
3554 lock_cache_file_with_wait_observer(lock_path, heartbeat_interval, |elapsed| {
3555 progress.emit_heartbeat_if_due(phase, elapsed);
3556 })
3557 .map_err(wasm_cache_fs_error)
3558 };
3559 progress.record_phase(failure_phase, started.elapsed());
3560 result
3561}
3562
3563fn ensure_cache_directory_tag(target_dir: &Path) -> Result<(), WasmBuildError> {
3564 ensure_cache_tag(target_dir).map_err(wasm_cache_fs_error)
3565}
3566
3567fn record_cache_entry_use(path: &Path) -> Result<(), WasmBuildError> {
3568 record_entry_use(path).map_err(wasm_cache_fs_error)
3569}
3570
3571fn wasm_cache_fs_error(error: CacheFsError) -> WasmBuildError {
3572 WasmBuildError::Io {
3573 operation: error.operation,
3574 path: error.path,
3575 source: error.source,
3576 }
3577}
3578
3579fn validate_spec(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3580 if spec.cargo_message_bytes == 0 {
3581 return Err(WasmBuildError::InvalidSpec {
3582 message: "Cargo message allowance must be nonzero".to_owned(),
3583 });
3584 }
3585 if spec.packages.is_empty() {
3586 return Err(WasmBuildError::InvalidSpec {
3587 message: "at least one Cargo package is required".to_owned(),
3588 });
3589 }
3590 if spec.target.is_empty() {
3591 return Err(WasmBuildError::InvalidSpec {
3592 message: "Cargo compilation target must not be empty".to_owned(),
3593 });
3594 }
3595 if spec
3596 .cargo_selection
3597 .flags
3598 .contains(&SelectionFlag::NonBuilding)
3599 {
3600 return Err(WasmBuildError::InvalidSpec {
3601 message:
3602 "Cargo help and build-graph flags exit without building and cannot be used for Wasm acquisition"
3603 .to_owned(),
3604 });
3605 }
3606 if spec.extra_env.contains_key(OsStr::new("CARGO_TARGET_DIR"))
3607 || spec
3608 .cargo_selection
3609 .flags
3610 .contains(&SelectionFlag::TargetDirectory)
3611 {
3612 return Err(WasmBuildError::InvalidSpec {
3613 message: "Cargo target directories are owned by the build specification; use target_dir or with_shared_incremental_target instead of command overrides".to_owned(),
3614 });
3615 }
3616 validate_cargo_target_selection(spec)?;
3617 if spec
3618 .cargo_selection
3619 .flags
3620 .contains(&SelectionFlag::InputSelection)
3621 {
3622 return Err(WasmBuildError::InvalidSpec {
3623 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(),
3624 });
3625 }
3626 spec.cargo_selection
3627 .profile_target_dir()
3628 .map_err(|message| WasmBuildError::InvalidSpec {
3629 message: message.to_owned(),
3630 })?;
3631 if matches!(
3632 &spec.cache_mode,
3633 WasmBuildCacheMode::SharedIncremental { target_dir } if target_dir.as_os_str().is_empty()
3634 ) {
3635 return Err(WasmBuildError::InvalidSpec {
3636 message: "shared incremental Cargo target directory must not be empty".to_owned(),
3637 });
3638 }
3639 if spec.shared_incremental_maintenance_config.is_some()
3640 && !matches!(
3641 spec.cache_mode,
3642 WasmBuildCacheMode::SharedIncremental { .. }
3643 )
3644 {
3645 return Err(WasmBuildError::InvalidSpec {
3646 message:
3647 "scheduled shared-target maintenance requires a shared incremental Cargo target"
3648 .to_owned(),
3649 });
3650 }
3651 Ok(())
3652}
3653
3654fn validate_cargo_target_selection(spec: &WasmBuildSpec) -> Result<(), WasmBuildError> {
3655 if spec
3656 .cargo_selection
3657 .flags
3658 .contains(&SelectionFlag::MessageFormat)
3659 {
3660 return Err(WasmBuildError::InvalidSpec {
3661 message: "Cargo message format is owned by artifact selection".to_owned(),
3662 });
3663 }
3664 if let Some(example) = &spec.example {
3665 let mut components = Path::new(example).components();
3666 if spec.packages.len() != 1
3667 || !matches!(
3668 (components.next(), components.next()),
3669 (Some(Component::Normal(_)), None)
3670 )
3671 || spec.cargo_selection.flags.contains(&SelectionFlag::Library)
3672 {
3673 return Err(WasmBuildError::InvalidSpec {
3674 message: "a named Cargo example requires one package, a normal target name and no --lib override".to_owned(),
3675 });
3676 }
3677 }
3678 if spec.packages.iter().any(|package| {
3679 let mut components = Path::new(package).components();
3680 !matches!(
3681 (components.next(), components.next()),
3682 (Some(Component::Normal(_)), None)
3683 )
3684 }) {
3685 return Err(WasmBuildError::InvalidSpec {
3686 message: "Cargo package names must be normal path components".to_owned(),
3687 });
3688 }
3689 if spec
3690 .cargo_selection
3691 .flags
3692 .contains(&SelectionFlag::TargetSelection)
3693 {
3694 return Err(WasmBuildError::InvalidSpec {
3695 message: "Cargo target selection is owned by the build specification; use with_example for a named cdylib example".to_owned(),
3696 });
3697 }
3698 Ok(())
3699}
3700
3701fn build_fingerprint(spec: &WasmBuildSpec) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3702 build_fingerprint_with_progress(spec, &mut ProgressReporter::silent())
3703}
3704
3705fn build_fingerprint_with_progress(
3706 spec: &WasmBuildSpec,
3707 progress: &mut ProgressReporter<'_>,
3708) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3709 build_fingerprint_with_policy(spec, progress, &mut None)
3710}
3711
3712fn build_fingerprint_with_policy(
3713 spec: &WasmBuildSpec,
3714 progress: &mut ProgressReporter<'_>,
3715 policy: &mut Option<&mut CargoInputProbePolicy<'_>>,
3716) -> Result<ResolvedCargoBuildInputs, WasmBuildError> {
3717 let total_started = Instant::now();
3718 let (cargo_identity, rustc_identity, tool_identity) =
3719 resolve_tool_identity_with_policy(spec, progress, policy)?;
3720
3721 let metadata_started = Instant::now();
3722 let metadata = progress.run_phase(WasmBuildProgressPhase::CargoMetadata, || {
3723 cargo_metadata_with_policy(spec, policy)
3724 })?;
3725 let cargo_metadata = metadata_started.elapsed();
3726
3727 let discovery_started = Instant::now();
3728 let (inputs, exclusions) =
3729 progress.run_phase(WasmBuildProgressPhase::InputDiscovery, || {
3730 let canonical = crate::cargo::observation::decode_metadata(&metadata)
3731 .map_err(|error| invalid_metadata(&error.to_string()))?;
3732 let parsed = ParsedCargoMetadata::parse(&canonical, &metadata)
3733 .map_err(|message| invalid_metadata(&message))?;
3734 resolve_local_inputs(spec, &parsed)
3735 })?;
3736 let input_discovery = discovery_started.elapsed();
3737
3738 let hashing_started = Instant::now();
3739 let (input_digest, validation_digest) =
3740 progress.run_phase(WasmBuildProgressPhase::ContentHashing, || {
3741 let mut cache = LabeledPathDigestCache::default();
3742 digest_resolved_local_inputs(
3743 &inputs,
3744 &exclusions,
3745 &mut cache,
3746 &spec.workspace_root,
3747 "hash Wasm build inputs",
3748 "hash semantic Wasm build inputs",
3749 )
3750 })?;
3751 let content_hashing = hashing_started.elapsed();
3752
3753 let fingerprint =
3754 finish_build_fingerprint(spec, &cargo_identity, &rustc_identity, input_digest);
3755 Ok(ResolvedCargoBuildInputs::assemble(
3756 spec,
3757 inputs,
3758 exclusions,
3759 input_digest,
3760 validation_digest,
3761 fingerprint,
3762 || WasmInputResolutionTimings {
3763 tool_identity,
3764 cargo_metadata,
3765 input_discovery,
3766 content_hashing,
3767 total: total_started.elapsed(),
3768 },
3769 ))
3770}
3771
3772fn cargo_build_inputs(inputs: Vec<(PathBuf, PathBuf)>) -> Arc<[CargoBuildInput]> {
3773 inputs
3774 .into_iter()
3775 .map(|(label, path)| CargoBuildInput { label, path })
3776 .collect()
3777}
3778
3779fn finish_build_fingerprint(
3780 spec: &WasmBuildSpec,
3781 cargo_identity: &[u8],
3782 rustc_identity: &[u8],
3783 input_digest: InputDigest,
3784) -> InputDigest {
3785 let mut hasher = InputHasher::new(CACHE_FORMAT_VERSION);
3786 for package in &spec.packages {
3787 hasher.field("package", package.as_bytes());
3788 }
3789 hasher.field("target", spec.target.as_bytes());
3790 if let Some(example) = &spec.example {
3791 hasher.field("cargo-target-selection", b"example");
3792 hasher.field("cargo-example", example.as_bytes());
3793 } else {
3794 hasher.field("cargo-target-selection", b"lib");
3795 }
3796 hasher.field(
3797 "profile-target-dir",
3798 spec.cargo_selection
3799 .profile_target_dir()
3800 .expect("Cargo profile was validated before fingerprinting")
3801 .as_bytes(),
3802 );
3803 for argument in &spec.cargo_selection.arguments {
3804 hasher.field("cargo-argument", &os_bytes(argument));
3805 }
3806 for (key, value) in effective_environment(spec) {
3807 hasher.field("environment-key", &os_bytes(&key));
3808 if let Some(value) = value {
3809 hasher.field("environment-value", &os_bytes(&value));
3810 } else {
3811 hasher.field("environment-unset", b"");
3812 }
3813 }
3814 hasher.field("cargo-identity", cargo_identity);
3815 hasher.field("rustc-identity", rustc_identity);
3816 hasher.field("source-input-digest", input_digest.as_bytes());
3817 hasher.finish()
3818}
3819
3820#[cfg(test)]
3821fn command_identity(
3822 spec: &WasmBuildSpec,
3823 phase: WasmBuildPhase,
3824 program: &OsStr,
3825 arguments: &[&str],
3826) -> Result<Vec<u8>, WasmBuildError> {
3827 command_identity_with_policy(spec, phase, program, arguments, &mut None)
3828}
3829
3830fn command_identity_with_policy(
3831 spec: &WasmBuildSpec,
3832 phase: WasmBuildPhase,
3833 program: &OsStr,
3834 arguments: &[&str],
3835 policy: &mut Option<&mut CargoInputProbePolicy<'_>>,
3836) -> Result<Vec<u8>, WasmBuildError> {
3837 let mut command = Command::new(program);
3838 command.current_dir(&spec.workspace_root).args(arguments);
3839 apply_command_environment(&mut command, spec);
3840 let output =
3841 capture_probe_output_with_policy(&mut command, phase, TOOL_IDENTITY_BYTES, policy)?;
3842 ensure_command_success(phase, output).map(|output| {
3843 let mut identity = output.stdout;
3844 identity.extend_from_slice(&output.stderr);
3845 identity
3846 })
3847}
3848
3849fn cargo_metadata(spec: &WasmBuildSpec) -> Result<Value, WasmBuildError> {
3850 cargo_metadata_with_policy(spec, &mut None)
3851}
3852
3853fn cargo_metadata_with_policy(
3854 spec: &WasmBuildSpec,
3855 policy: &mut Option<&mut CargoInputProbePolicy<'_>>,
3856) -> Result<Value, WasmBuildError> {
3857 let mut command = Command::new(&spec.cargo_program);
3858 command
3859 .current_dir(&spec.workspace_root)
3860 .args(["metadata", "--format-version", "1"]);
3861 for argument in &spec.cargo_selection.metadata_arguments {
3862 command.arg(argument);
3863 }
3864 apply_command_environment(&mut command, spec);
3865 let output = capture_probe_output_with_policy(
3866 &mut command,
3867 WasmBuildPhase::CargoMetadata,
3868 CARGO_METADATA_BYTES,
3869 policy,
3870 )?;
3871 let output = ensure_command_success(WasmBuildPhase::CargoMetadata, output)?;
3872 serde_json::from_slice(&output.stdout).map_err(|error| WasmBuildError::InvalidMetadata {
3873 message: format!("Cargo metadata was not valid JSON: {error}"),
3874 })
3875}
3876
3877#[cfg(test)]
3878fn capture_probe_output(
3879 command: &mut Command,
3880 phase: WasmBuildPhase,
3881 stdout_bytes: usize,
3882) -> Result<CompletedExecution, WasmBuildError> {
3883 capture_probe_output_with_policy(command, phase, stdout_bytes, &mut None)
3884}
3885
3886fn capture_probe_output_with_policy(
3887 command: &mut Command,
3888 phase: WasmBuildPhase,
3889 stdout_bytes: usize,
3890 policy: &mut Option<&mut CargoInputProbePolicy<'_>>,
3891) -> Result<CompletedExecution, WasmBuildError> {
3892 let limits = OutputLimits {
3893 stdout: OutputLimit::Terminate(stdout_bytes),
3894 stderr: OutputLimit::Terminate(TOOL_IDENTITY_BYTES),
3895 timeout: None,
3896 };
3897 let result = if let Some(policy) = policy {
3898 capture_controlled_probe(command, limits, policy)
3899 } else {
3900 capture_command(command, limits)
3901 }
3902 .and_then(CompletedExecution::require_complete);
3903 let evidence = match result {
3904 Ok(evidence) => evidence,
3905 Err(ToolError::Execution(error))
3906 if matches!(
3907 error.failure,
3908 ExecutionFailure::Io {
3909 operation: ExecutionOperation::Spawn,
3910 ..
3911 }
3912 ) && error.cleanup.is_none() =>
3913 {
3914 let ExecutionFailure::Io { source, .. } = error.failure else {
3915 unreachable!()
3916 };
3917 return Err(WasmBuildError::CommandSpawn {
3918 phase,
3919 program: command.get_program().to_owned(),
3920 source,
3921 });
3922 }
3923 Err(error) => {
3924 return Err(WasmBuildError::Io {
3925 operation: match phase {
3926 WasmBuildPhase::CargoIdentity => "capture complete Cargo identity probe output",
3927 WasmBuildPhase::RustcIdentity => "capture complete rustc identity probe output",
3928 _ => "capture complete Cargo metadata probe output",
3929 },
3930 path: PathBuf::from(command.get_program()),
3931 source: io::Error::other(error),
3932 });
3933 }
3934 };
3935 Ok(evidence)
3936}
3937
3938fn capture_controlled_probe(
3939 command: &mut Command,
3940 mut limits: OutputLimits,
3941 policy: &mut CargoInputProbePolicy<'_>,
3942) -> Result<CompletedExecution, ToolError> {
3943 let failure = if (policy.cancelled)() {
3944 Some(ExecutionFailure::Cancelled)
3945 } else if policy
3946 .deadline
3947 .is_some_and(|deadline| deadline <= Instant::now())
3948 {
3949 Some(ExecutionFailure::TimedOut)
3950 } else {
3951 None
3952 };
3953 if let Some(failure) = failure {
3954 return Err(ToolError::Execution(Box::new(ExecutionError {
3955 failure,
3956 evidence: ExecutionEvidence::default(),
3957 cleanup: None,
3958 })));
3959 }
3960 command
3961 .stdin(Stdio::null())
3962 .stdout(Stdio::piped())
3963 .stderr(Stdio::piped());
3964 let mut child = OwnedChild::spawn(command, policy.cleanup).map_err(|source| {
3965 ToolError::Execution(Box::new(ExecutionError {
3966 failure: ExecutionFailure::Io {
3967 operation: ExecutionOperation::Spawn,
3968 source,
3969 },
3970 evidence: ExecutionEvidence::default(),
3971 cleanup: None,
3972 }))
3973 })?;
3974 limits.timeout = policy.deadline.map(|deadline| {
3976 deadline
3977 .saturating_duration_since(Instant::now())
3978 .max(Duration::from_nanos(1))
3979 });
3980 communicate_child(
3981 &mut child,
3982 None,
3983 limits,
3984 SuccessfulExit::Cleanup,
3985 &mut *policy.cancelled,
3986 )
3987}
3988
3989struct MetadataPackage<'a> {
3992 package: &'a crate::cargo::observation::schema::Package,
3993 original: &'a Value,
3994}
3995
3996struct ParsedCargoMetadata<'a> {
3999 packages: HashMap<&'a str, MetadataPackage<'a>>,
4000 workspace_packages: HashMap<&'a str, Option<&'a str>>,
4001 nodes: HashMap<&'a str, MetadataNode<'a>>,
4002 workspace_root: &'a Path,
4003}
4004
4005struct MetadataNode<'a> {
4006 node: &'a crate::cargo::observation::schema::Node,
4007 value: &'a Value,
4008}
4009
4010impl<'a> ParsedCargoMetadata<'a> {
4011 fn parse(
4012 metadata: &'a crate::cargo::observation::schema::Metadata,
4013 original: &'a Value,
4014 ) -> Result<Self, String> {
4015 let packages = metadata_packages(metadata, original);
4016 let workspace_members: HashSet<_> = metadata
4017 .workspace_members
4018 .iter()
4019 .map(|id| id.repr.as_str())
4020 .collect();
4021 let mut workspace_packages = HashMap::new();
4022 for package in packages
4023 .values()
4024 .filter(|package| workspace_members.contains(package.package.id.repr.as_str()))
4025 {
4026 workspace_packages
4027 .entry(package.package.name.as_str())
4028 .and_modify(|id| *id = None)
4029 .or_insert(Some(package.package.id.repr.as_str()));
4030 }
4031 let resolve = metadata
4032 .resolve
4033 .as_ref()
4034 .ok_or_else(|| "Cargo metadata has no resolved dependency nodes".to_owned())?;
4035 let nodes = resolve
4036 .nodes
4037 .iter()
4038 .enumerate()
4039 .map(|(index, node)| {
4040 (
4041 node.id.repr.as_str(),
4042 MetadataNode {
4043 node,
4044 value: &original["resolve"]["nodes"][index],
4045 },
4046 )
4047 })
4048 .collect();
4049 Ok(Self {
4050 packages,
4051 workspace_packages,
4052 nodes,
4053 workspace_root: metadata.workspace_root.as_std_path(),
4054 })
4055 }
4056}
4057
4058const SEMANTIC_PACKAGE_FIELDS: &[&str] = &[
4059 "authors",
4060 "default_run",
4061 "description",
4062 "documentation",
4063 "edition",
4064 "homepage",
4065 "license",
4066 "license_file",
4067 "links",
4068 "metadata",
4069 "name",
4070 "readme",
4071 "repository",
4072 "rust_version",
4073 "version",
4074];
4075
4076struct LockedPackageIdentity {
4077 name: String,
4078 version: String,
4079 source: String,
4080 checksum: Option<String>,
4081}
4082
4083type LockedPackageIndex<'a> = HashMap<(&'a str, &'a str, &'a str), &'a LockedPackageIdentity>;
4084
4085fn index_locked_packages(packages: &[LockedPackageIdentity]) -> LockedPackageIndex<'_> {
4086 let mut index = HashMap::with_capacity(packages.len());
4087 for package in packages {
4088 index
4090 .entry((
4091 package.name.as_str(),
4092 package.version.as_str(),
4093 package.source.as_str(),
4094 ))
4095 .or_insert(package);
4096 }
4097 index
4098}
4099
4100fn resolve_local_inputs(
4101 spec: &WasmBuildSpec,
4102 metadata: &ParsedCargoMetadata<'_>,
4103) -> Result<(ResolvedLocalInputs, Vec<PathBuf>), WasmBuildError> {
4104 let mut selected_ids = selected_package_ids(spec, metadata)?;
4105 let mut artifact_names = BTreeSet::new();
4108 let mut wasm_targets = Vec::new();
4109 let wasm_artifact_error = selected_ids.iter().find_map(|id| {
4110 let package = metadata.packages[id].package;
4111 let mut matching = package.targets.iter().filter(|target| {
4112 target.crate_types.contains(&schema::CrateType::CDyLib)
4113 && spec.example.as_ref().map_or_else(
4114 || target.is_cdylib(),
4115 |example| target.name == *example && target.is_example(),
4116 )
4117 });
4118 let Some(target) = matching.next() else {
4119 return Some(format!(
4120 "Cargo package `{}` must declare the selected cdylib target",
4121 package.name
4122 ));
4123 };
4124 let mut components = Path::new(&target.name).components();
4125 if matching.next().is_some()
4126 || !matches!(
4127 (components.next(), components.next()),
4128 (Some(Component::Normal(_)), None)
4129 )
4130 {
4131 return Some(format!(
4132 "Cargo package `{}` has an ambiguous or unsafe cdylib target",
4133 package.name
4134 ));
4135 }
4136 let cache_name = target.name.replace('-', "_");
4137 if !artifact_names.insert(cache_name.clone()) {
4138 return Some(format!(
4139 "selected Cargo packages share the Wasm artifact name `{cache_name}`"
4140 ));
4141 }
4142 wasm_targets.push(WasmArtifactTarget {
4143 package_id: package.id.repr.clone(),
4144 name: target.name.clone(),
4145 kind: if spec.example.is_some() {
4146 schema::TargetKind::Example
4147 } else {
4148 schema::TargetKind::CDyLib
4149 },
4150 });
4151 None
4152 });
4153 let mut closure = BTreeSet::new();
4154 while let Some(id) = selected_ids.pop_front() {
4155 if !closure.insert(id) {
4156 continue;
4157 }
4158 if let Some(node) = metadata.nodes.get(id) {
4159 selected_ids.extend(
4160 node.node
4161 .deps
4162 .iter()
4163 .map(|dependency| dependency.pkg.repr.as_str()),
4164 );
4165 }
4166 }
4167
4168 let workspace_root = metadata.workspace_root;
4169 let workspace_projection = semantic_workspace_projection(metadata, &closure, workspace_root)?;
4170 let mut validation_inputs = workspace_configuration_inputs(spec, workspace_root)?;
4171 append_package_inputs(
4172 &mut validation_inputs,
4173 &metadata.packages,
4174 closure,
4175 workspace_root,
4176 )?;
4177 append_additional_inputs(&mut validation_inputs, spec, workspace_root);
4178 validate_shared_incremental_target_boundary(spec, &validation_inputs)?;
4179 let exclusions = source_exclusions(spec, &validation_inputs);
4180 Ok((
4181 ResolvedLocalInputs {
4182 validation_inputs,
4183 workspace_projection,
4184 wasm_artifact_error,
4185 wasm_targets,
4186 },
4187 exclusions,
4188 ))
4189}
4190
4191fn metadata_packages<'a>(
4192 metadata: &'a crate::cargo::observation::schema::Metadata,
4193 original: &'a Value,
4194) -> HashMap<&'a str, MetadataPackage<'a>> {
4195 metadata
4196 .packages
4197 .iter()
4198 .enumerate()
4199 .map(|(index, package)| {
4200 (
4201 package.id.repr.as_str(),
4202 MetadataPackage {
4203 package,
4204 original: &original["packages"][index],
4205 },
4206 )
4207 })
4208 .collect()
4209}
4210
4211fn selected_package_ids<'a>(
4212 spec: &WasmBuildSpec,
4213 metadata: &ParsedCargoMetadata<'a>,
4214) -> Result<VecDeque<&'a str>, WasmBuildError> {
4215 let mut selected_ids = VecDeque::new();
4216 for requested in &spec.packages {
4217 match metadata.workspace_packages.get(requested.as_str()) {
4218 Some(Some(id)) => selected_ids.push_back(*id),
4219 None => {
4220 return Err(WasmBuildError::InvalidSpec {
4221 message: format!("Cargo workspace contains no package named `{requested}`"),
4222 });
4223 }
4224 Some(None) => {
4225 return Err(WasmBuildError::InvalidSpec {
4226 message: format!("Cargo workspace package name `{requested}` is ambiguous"),
4227 });
4228 }
4229 }
4230 }
4231 Ok(selected_ids)
4232}
4233
4234fn semantic_workspace_projection(
4235 metadata: &ParsedCargoMetadata<'_>,
4236 closure: &BTreeSet<&str>,
4237 workspace_root: &Path,
4238) -> Result<Option<InputDigest>, WasmBuildError> {
4239 let locked_packages = locked_package_identities(workspace_root)?;
4242 let locked_index = index_locked_packages(&locked_packages);
4243 let mut identities = HashMap::new();
4244 for &id in closure {
4245 let package = metadata
4246 .packages
4247 .get(id)
4248 .ok_or_else(|| invalid_metadata(&format!("resolved package `{id}` is missing")))?;
4249 let Some(identity) = semantic_package_identity(package, workspace_root, &locked_index)
4250 else {
4251 return Ok(None);
4252 };
4253 identities.insert(id, identity);
4254 }
4255
4256 let mut projected_packages = closure
4257 .iter()
4258 .map(|&id| {
4259 let package = metadata
4260 .packages
4261 .get(id)
4262 .expect("selected package closure was validated above");
4263 let identity = identities[id];
4264 let node = metadata.nodes.get(id).ok_or_else(|| {
4265 invalid_metadata(&format!("resolved package `{id}` has no dependency node"))
4266 })?;
4267 let projection = semantic_package_projection(package, node, &identities)?;
4268 Ok::<_, WasmBuildError>((identity, projection))
4269 })
4270 .collect::<Result<Vec<_>, _>>()?;
4271 projected_packages.sort_by_key(|(identity, _)| *identity);
4272
4273 let root_manifest = workspace_root.join("Cargo.toml");
4274 let root_contents =
4275 fs::read_to_string(&root_manifest).map_err(|source| WasmBuildError::Io {
4276 operation: "read workspace manifest for semantic projection",
4277 path: root_manifest.clone(),
4278 source,
4279 })?;
4280 let root = toml::from_str::<TomlValue>(&root_contents).map_err(|error| {
4281 invalid_metadata(&format!(
4282 "workspace manifest could not be projected as TOML: {error}"
4283 ))
4284 })?;
4285
4286 let mut hasher = InputHasher::new("wasm-semantic-workspace-projection-v1");
4287 for (identity, projection) in projected_packages {
4288 hasher.field("package-identity", identity.as_bytes());
4289 hasher.field("package-projection", projection.as_bytes());
4290 }
4291 hash_toml_setting(&mut hasher, "cargo-features", root.get("cargo-features"));
4292 hash_toml_setting(&mut hasher, "profile", root.get("profile"));
4293 let workspace = root.get("workspace").and_then(TomlValue::as_table);
4294 hash_toml_setting(
4295 &mut hasher,
4296 "workspace-resolver",
4297 workspace.and_then(|table| table.get("resolver")),
4298 );
4299 hash_toml_setting(
4300 &mut hasher,
4301 "workspace-lints",
4302 workspace.and_then(|table| table.get("lints")),
4303 );
4304 Ok(Some(hasher.finish()))
4305}
4306
4307fn locked_package_identities(
4308 workspace_root: &Path,
4309) -> Result<Vec<LockedPackageIdentity>, WasmBuildError> {
4310 use crate::cargo::lockfile::{external_package, optional_string, required_string};
4311 let lockfile = workspace_root.join("Cargo.lock");
4312 let contents = match fs::read_to_string(&lockfile) {
4313 Ok(contents) => contents,
4314 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
4315 Err(source) => {
4316 return Err(WasmBuildError::Io {
4317 operation: "read Cargo lockfile for semantic projection",
4318 path: lockfile,
4319 source,
4320 });
4321 }
4322 };
4323 let lock = toml::from_str::<TomlValue>(&contents).map_err(|error| {
4324 invalid_metadata(&format!(
4325 "Cargo lockfile could not be projected as TOML: {error}"
4326 ))
4327 })?;
4328 let Some(packages) = lock.get("package").and_then(TomlValue::as_array) else {
4329 return Ok(Vec::new());
4330 };
4331 packages
4332 .iter()
4333 .filter_map(|package| {
4334 let table = match external_package(package) {
4335 Ok(Some(table)) => table,
4336 Ok(None) => return None,
4337 Err(message) => {
4338 return Some(Err(invalid_metadata(&format!("Cargo lockfile {message}"))));
4339 }
4340 };
4341 let text = |field| {
4342 required_string(table, field)
4343 .map(str::to_owned)
4344 .map_err(|_| {
4345 invalid_metadata(&format!(
4346 "Cargo lockfile package `{field}` is missing or not a string"
4347 ))
4348 })
4349 };
4350 let source = required_string(table, "source").map(str::to_owned);
4351 Some(
4352 source
4353 .map_err(|_| invalid_metadata("Cargo lockfile package source is not a string"))
4354 .and_then(|source| {
4355 Ok(LockedPackageIdentity {
4356 name: text("name")?,
4357 version: text("version")?,
4358 source,
4359 checksum: optional_string(table, "checksum")
4360 .map_err(|_| {
4361 invalid_metadata(
4362 "Cargo lockfile package `checksum` is not a string",
4363 )
4364 })?
4365 .map(str::to_owned),
4366 })
4367 }),
4368 )
4369 })
4370 .collect()
4371}
4372
4373fn semantic_package_identity(
4374 package: &MetadataPackage<'_>,
4375 workspace_root: &Path,
4376 locked_packages: &LockedPackageIndex<'_>,
4377) -> Option<InputDigest> {
4378 let package = package.package;
4379 let version = package.version.to_string();
4380 let mut hasher = InputHasher::new("wasm-semantic-package-identity-v1");
4381 hasher.field("name", package.name.as_bytes());
4382 hasher.field("version", version.as_bytes());
4383 if package.source.is_none() {
4384 let manifest = package.manifest_path.strip_prefix(workspace_root).ok()?;
4385 let package_root = package.manifest_path.parent()?;
4386 if package_root == workspace_root {
4387 return None;
4388 }
4389 hasher.field("local-manifest", &os_bytes(manifest.as_os_str()));
4390 } else {
4391 let metadata_source = &package.source.as_ref()?.repr;
4392 let locked = locked_packages.get(&(
4393 package.name.as_str(),
4394 version.as_str(),
4395 metadata_source.as_str(),
4396 ))?;
4397 match locked.source.as_str() {
4398 source if source.starts_with("registry+") && locked.checksum.is_some() => {}
4399 source if source.starts_with("git+") && source.contains('#') => {}
4400 _ => return None,
4401 }
4402 hasher.field("external-package-id", package.id.repr.as_bytes());
4403 hasher.field("external-source", locked.source.as_bytes());
4404 hasher.field(
4405 "external-checksum",
4406 locked.checksum.as_deref().unwrap_or_default().as_bytes(),
4407 );
4408 }
4409 Some(hasher.finish())
4410}
4411
4412fn semantic_package_projection(
4413 package: &MetadataPackage<'_>,
4414 node: &MetadataNode<'_>,
4415 identities: &HashMap<&str, InputDigest>,
4416) -> Result<InputDigest, WasmBuildError> {
4417 let mut hasher = InputHasher::new("wasm-semantic-package-projection-v1");
4421 let mut serialized = Vec::new();
4422 for field in SEMANTIC_PACKAGE_FIELDS {
4423 hasher.field("package-field-name", field.as_bytes());
4424 match package.original.get(*field) {
4425 Some(value) => {
4426 serialized.clear();
4427 serde_json::to_writer(&mut serialized, value)
4428 .expect("serializing JSON values into a Vec cannot fail");
4429 hasher.field("package-field-value", &serialized);
4430 }
4431 None => hasher.field("package-field-missing", b""),
4432 }
4433 }
4434 drop(serialized);
4435
4436 let mut features = node
4437 .node
4438 .features
4439 .iter()
4440 .map(|feature| feature.as_str())
4441 .collect::<Vec<_>>();
4442 features.sort_unstable();
4443 for feature in features {
4444 hasher.field("enabled-feature", feature.as_bytes());
4445 }
4446
4447 let mut dependencies = node
4448 .node
4449 .deps
4450 .iter()
4451 .enumerate()
4452 .map(|(index, dependency)| {
4453 let name = dependency.name.as_str();
4454 let package_id = dependency.pkg.repr.as_str();
4455 let identity = identities.get(package_id).copied().ok_or_else(|| {
4456 invalid_metadata(&format!(
4457 "dependency `{package_id}` is outside the selected package closure"
4458 ))
4459 })?;
4460 let kinds = node.value["deps"][index]
4463 .get("dep_kinds")
4464 .ok_or_else(|| invalid_metadata("Cargo metadata dependency has no kind array"))?
4465 .to_string();
4466 Ok::<_, WasmBuildError>((name, identity, kinds))
4467 })
4468 .collect::<Result<Vec<_>, _>>()?;
4469 dependencies.sort();
4470 for (name, identity, kinds) in dependencies {
4471 hasher.field("dependency-name", name.as_bytes());
4472 hasher.field("dependency-identity", identity.as_bytes());
4473 hasher.field("dependency-kinds", kinds.as_bytes());
4474 }
4475 Ok(hasher.finish())
4476}
4477
4478fn hash_toml_setting(hasher: &mut InputHasher, label: &str, value: Option<&TomlValue>) {
4479 hasher.field("workspace-setting-name", label.as_bytes());
4480 match value {
4481 Some(value) => hasher.field("workspace-setting-value", value.to_string().as_bytes()),
4482 None => hasher.field("workspace-setting-missing", b""),
4483 }
4484}
4485
4486fn is_broad_workspace_input(label: &Path) -> bool {
4487 label == Path::new("workspace/Cargo.toml") || label == Path::new("workspace/Cargo.lock")
4488}
4489
4490fn digest_resolved_local_inputs(
4491 inputs: &ResolvedLocalInputs,
4492 exclusions: &[PathBuf],
4493 cache: &mut LabeledPathDigestCache,
4494 error_path: &Path,
4495 validation_operation: &'static str,
4496 semantic_operation: &'static str,
4497) -> Result<(InputDigest, InputDigest), WasmBuildError> {
4498 let validation_digest = digest_labeled_paths_composable(
4499 "wasm-source-inputs-v1",
4500 inputs
4501 .validation_inputs
4502 .iter()
4503 .map(|(label, path)| (label.as_path(), path.as_path())),
4504 exclusions,
4505 cache,
4506 )
4507 .map_err(|source| WasmBuildError::Io {
4508 operation: validation_operation,
4509 path: error_path.to_owned(),
4510 source,
4511 })?;
4512 let input_digest = semantic_input_digest(inputs, validation_digest, exclusions, cache)
4513 .map_err(|source| WasmBuildError::Io {
4514 operation: semantic_operation,
4515 path: error_path.to_owned(),
4516 source,
4517 })?;
4518 Ok((input_digest, validation_digest))
4519}
4520
4521fn semantic_input_digest(
4522 inputs: &ResolvedLocalInputs,
4523 validation_digest: InputDigest,
4524 exclusions: &[PathBuf],
4525 cache: &mut LabeledPathDigestCache,
4526) -> io::Result<InputDigest> {
4527 let Some(workspace) = inputs.workspace_projection else {
4528 return Ok(validation_digest);
4529 };
4530 let path_digest = digest_labeled_paths_composable(
4531 "wasm-source-inputs-v1",
4532 inputs
4533 .validation_inputs
4534 .iter()
4535 .filter(|(label, _)| !is_broad_workspace_input(label))
4536 .map(|(label, path)| (label.as_path(), path.as_path())),
4537 exclusions,
4538 cache,
4539 )?;
4540 let mut hasher = InputHasher::new("wasm-semantic-source-inputs-v1");
4541 hasher.field("path-input-digest", path_digest.as_bytes());
4542 hasher.field("workspace-projection", workspace.as_bytes());
4543 Ok(hasher.finish())
4544}
4545
4546fn workspace_configuration_inputs(
4547 spec: &WasmBuildSpec,
4548 workspace_root: &Path,
4549) -> Result<Vec<(PathBuf, PathBuf)>, WasmBuildError> {
4550 let mut inputs = Vec::new();
4551 add_if_present(
4552 &mut inputs,
4553 "workspace/Cargo.toml",
4554 workspace_root.join("Cargo.toml"),
4555 );
4556 add_if_present(
4557 &mut inputs,
4558 "workspace/Cargo.lock",
4559 workspace_root.join("Cargo.lock"),
4560 );
4561 add_if_present(
4562 &mut inputs,
4563 "workspace/rust-toolchain.toml",
4564 workspace_root.join("rust-toolchain.toml"),
4565 );
4566 add_if_present(
4567 &mut inputs,
4568 "workspace/rust-toolchain",
4569 workspace_root.join("rust-toolchain"),
4570 );
4571 append_cargo_configuration_inputs(&mut inputs, spec, workspace_root)?;
4572 Ok(inputs)
4573}
4574
4575fn append_cargo_configuration_inputs(
4576 inputs: &mut Vec<(PathBuf, PathBuf)>,
4577 spec: &WasmBuildSpec,
4578 workspace_root: &Path,
4579) -> Result<(), WasmBuildError> {
4580 let invocation_root =
4581 spec.workspace_root
4582 .canonicalize()
4583 .map_err(|source| WasmBuildError::Io {
4584 operation: "resolve Cargo invocation directory",
4585 path: spec.workspace_root.clone(),
4586 source,
4587 })?;
4588 let canonical_workspace =
4589 workspace_root
4590 .canonicalize()
4591 .map_err(|source| WasmBuildError::Io {
4592 operation: "resolve Cargo workspace directory",
4593 path: workspace_root.to_owned(),
4594 source,
4595 })?;
4596
4597 let cargo_home = command_environment_value(spec, "CARGO_HOME");
4598 let default_home = default_home_directory(spec);
4599 let directories = crate::cargo::environment::configuration_directories(
4600 &invocation_root,
4601 cargo_home.as_deref(),
4602 default_home.as_deref().map(Path::as_os_str),
4603 )
4604 .map_err(|source| WasmBuildError::Io {
4605 operation: "resolve Cargo configuration directories",
4606 path: invocation_root.clone(),
4607 source,
4608 })?;
4609 let roots: Vec<_> = directories
4610 .iter()
4611 .filter_map(|directory| crate::cargo::environment::preferred_configuration(directory))
4612 .collect();
4613 let observed =
4614 crate::cargo::environment::observe_configurations(&roots).map_err(|error| match error {
4615 crate::cargo::environment::ConfigurationError::Io {
4616 operation,
4617 path,
4618 source,
4619 } => WasmBuildError::Io {
4620 operation,
4621 path,
4622 source,
4623 },
4624 crate::cargo::environment::ConfigurationError::Invalid { path, message } => {
4625 WasmBuildError::InvalidCargoConfiguration { path, message }
4626 }
4627 })?;
4628 for config in observed {
4629 if !inputs.iter().any(|(_, path)| path == &config.path) {
4630 inputs.push((
4631 cargo_configuration_label(&config.location, &canonical_workspace),
4632 config.path,
4633 ));
4634 }
4635 }
4636 Ok(())
4637}
4638
4639#[cfg(windows)]
4640fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
4641 command_environment_value(spec, "USERPROFILE")
4642 .or_else(|| command_environment_value(spec, "HOME"))
4643 .map(PathBuf::from)
4644}
4645
4646#[cfg(not(windows))]
4647fn default_home_directory(spec: &WasmBuildSpec) -> Option<PathBuf> {
4648 command_environment_value(spec, "HOME").map(PathBuf::from)
4649}
4650
4651fn command_environment_value(spec: &WasmBuildSpec, name: &str) -> Option<OsString> {
4652 spec.extra_env
4653 .get(OsStr::new(name))
4654 .cloned()
4655 .or_else(|| std::env::var_os(name))
4656}
4657
4658fn cargo_configuration_label(config: &Path, workspace_root: &Path) -> PathBuf {
4659 if let Ok(relative) = config.strip_prefix(workspace_root) {
4660 return PathBuf::from("cargo-config/workspace").join(relative);
4661 }
4662 let location = digest_bytes("cargo-config-location-v1", &os_bytes(config.as_os_str()));
4663 PathBuf::from("cargo-config/external").join(location.to_hex())
4664}
4665
4666fn append_package_inputs(
4667 inputs: &mut Vec<(PathBuf, PathBuf)>,
4668 packages: &HashMap<&str, MetadataPackage<'_>>,
4669 closure: BTreeSet<&str>,
4670 workspace_root: &Path,
4671) -> Result<(), WasmBuildError> {
4672 for id in closure {
4673 let Some(package) = packages.get(id) else {
4674 return Err(invalid_metadata(&format!(
4675 "resolved package `{id}` is missing"
4676 )));
4677 };
4678 let package = package.package;
4679 if package.source.is_some() {
4680 continue;
4681 }
4682 let root = package.manifest_path.parent().ok_or_else(|| {
4683 invalid_metadata(&format!(
4684 "package `{}` manifest has no parent",
4685 package.name
4686 ))
4687 })?;
4688 let relative_manifest = package
4689 .manifest_path
4690 .as_std_path()
4691 .strip_prefix(workspace_root)
4692 .unwrap_or(package.manifest_path.as_std_path());
4693 let label = PathBuf::from(format!("package/{}@{}", package.name, package.version))
4694 .join(relative_manifest.parent().unwrap_or_else(|| Path::new(".")));
4695 inputs.push((label, root.as_std_path().to_owned()));
4696 }
4697 Ok(())
4698}
4699
4700fn append_additional_inputs(
4701 inputs: &mut Vec<(PathBuf, PathBuf)>,
4702 spec: &WasmBuildSpec,
4703 workspace_root: &Path,
4704) {
4705 for additional in &spec.additional_inputs {
4706 let path = if additional.is_absolute() {
4707 additional.clone()
4708 } else {
4709 workspace_root.join(additional)
4710 };
4711 inputs.push((PathBuf::from("additional").join(additional), path));
4712 }
4713}
4714
4715fn source_exclusions(spec: &WasmBuildSpec, inputs: &[(PathBuf, PathBuf)]) -> Vec<PathBuf> {
4716 let mut exclusions = vec![
4717 spec.target_dir.clone(),
4718 spec.workspace_root.join("target"),
4719 spec.workspace_root.join(".git"),
4720 ];
4721 if let Some(shared_target) = shared_incremental_target(spec) {
4722 exclusions.push(shared_target);
4723 }
4724 for (_, path) in inputs {
4725 if path.is_dir() {
4726 exclusions.push(path.join("target"));
4727 exclusions.push(path.join(".git"));
4728 }
4729 }
4730 exclusions
4731}
4732
4733fn validate_shared_incremental_target_boundary(
4734 spec: &WasmBuildSpec,
4735 inputs: &[(PathBuf, PathBuf)],
4736) -> Result<(), WasmBuildError> {
4737 let Some(shared_target) = shared_incremental_target(spec) else {
4738 return Ok(());
4739 };
4740 let shared_target =
4741 canonicalize_allow_missing(&shared_target).map_err(|source| WasmBuildError::Io {
4742 operation: "resolve shared incremental Cargo target boundary",
4743 path: shared_target.clone(),
4744 source,
4745 })?;
4746 let exact_entries = spec.target_dir.join(".ic-testkit/wasm-targets");
4747 let exact_entries =
4748 canonicalize_allow_missing(&exact_entries).map_err(|source| WasmBuildError::Io {
4749 operation: "resolve exact Wasm cache boundary",
4750 path: exact_entries,
4751 source,
4752 })?;
4753 if shared_target.starts_with(&exact_entries)
4756 || (exact_entries.starts_with(&shared_target)
4757 && !exact_entries.starts_with(shared_target.join(".ic-testkit")))
4758 {
4759 return Err(WasmBuildError::InvalidSpec {
4760 message: "shared incremental target must not overlap removable exact Wasm cache state"
4761 .to_owned(),
4762 });
4763 }
4764 let resolved_inputs = inputs
4765 .iter()
4766 .map(|(_, input)| {
4767 let canonical = input.canonicalize().map_err(|source| WasmBuildError::Io {
4768 operation: "resolve Cargo input boundary",
4769 path: input.clone(),
4770 source,
4771 })?;
4772 let metadata = fs::metadata(&canonical).map_err(|source| WasmBuildError::Io {
4773 operation: "inspect Cargo input boundary",
4774 path: canonical.clone(),
4775 source,
4776 })?;
4777 Ok((canonical, metadata.is_dir()))
4778 })
4779 .collect::<Result<Vec<_>, WasmBuildError>>()?;
4780 let has_safe_generated_root = std::iter::once(spec.target_dir.clone())
4781 .chain(std::iter::once(spec.workspace_root.join("target")))
4782 .chain(
4783 inputs
4784 .iter()
4785 .filter(|(_, path)| path.is_dir())
4786 .map(|(_, path)| path.join("target")),
4787 )
4788 .filter_map(|path| canonicalize_allow_missing(&path).ok())
4789 .filter(|root| {
4790 !resolved_inputs
4791 .iter()
4792 .any(|(input, _is_directory)| input.starts_with(root))
4793 })
4794 .fold(false, |safe, root| safe | shared_target.starts_with(root));
4796 if has_safe_generated_root {
4797 return Ok(());
4798 }
4799
4800 for (input, is_directory) in resolved_inputs {
4801 if shared_target == input
4802 || (is_directory && shared_target.starts_with(&input))
4803 || input.starts_with(&shared_target)
4804 {
4805 return Err(WasmBuildError::InvalidSpec {
4806 message: format!(
4807 "shared incremental target {} must not overlap exact Cargo inputs unless it is inside a generated target directory",
4808 shared_target.display()
4809 ),
4810 });
4811 }
4812 }
4813 Ok(())
4814}
4815
4816pub(super) fn shared_incremental_target(spec: &WasmBuildSpec) -> Option<PathBuf> {
4817 let WasmBuildCacheMode::SharedIncremental { target_dir } = &spec.cache_mode else {
4818 return None;
4819 };
4820 Some(if target_dir.is_absolute() {
4821 target_dir.clone()
4822 } else {
4823 spec.workspace_root.join(target_dir)
4824 })
4825}
4826
4827fn shared_incremental_target_exists(
4828 spec: &WasmBuildSpec,
4829 operation: &'static str,
4830) -> Result<bool, WasmBuildError> {
4831 let target_dir =
4832 shared_incremental_target(spec).ok_or_else(|| WasmBuildError::InvalidSpec {
4833 message: "shared incremental target is not configured".to_owned(),
4834 })?;
4835 match fs::symlink_metadata(&target_dir) {
4836 Ok(metadata) if metadata.is_dir() => Ok(true),
4837 Ok(_) => Err(WasmBuildError::InvalidSpec {
4838 message: format!(
4839 "shared incremental Cargo target {} must be a directory",
4840 target_dir.display()
4841 ),
4842 }),
4843 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
4844 Err(source) => Err(WasmBuildError::Io {
4845 operation,
4846 path: target_dir,
4847 source,
4848 }),
4849 }
4850}
4851
4852fn effective_environment(spec: &WasmBuildSpec) -> BTreeMap<OsString, Option<OsString>> {
4853 let mut names = spec.inherited_env.clone();
4854 names.extend(AUTOMATIC_ENVIRONMENT.iter().map(OsString::from));
4855 let mut environment = names
4856 .into_iter()
4857 .map(|name| {
4858 let value = std::env::var_os(&name);
4859 (name, value)
4860 })
4861 .collect::<BTreeMap<_, _>>();
4862 for (key, value) in &spec.extra_env {
4863 environment.insert(key.clone(), Some(value.clone()));
4864 }
4865 environment
4866}
4867
4868fn apply_command_environment(command: &mut Command, spec: &WasmBuildSpec) {
4869 for (key, value) in &spec.extra_env {
4870 command.env(key, value);
4871 }
4872}
4873
4874fn run_cargo_build(
4875 spec: &WasmBuildSpec,
4876 build_target_dir: &Path,
4877 targets: &[WasmArtifactTarget],
4878 progress: &mut ProgressReporter<'_>,
4879) -> Result<Vec<PathBuf>, WasmBuildError> {
4880 let command = cargo_build_command(spec, build_target_dir)?;
4881 communicate_cargo_build(spec, build_target_dir, targets, command, progress)
4882}
4883
4884fn cargo_build_command(
4885 spec: &WasmBuildSpec,
4886 build_target_dir: &Path,
4887) -> Result<Command, WasmBuildError> {
4888 let absolute_target_dir =
4889 canonicalize_allow_missing(build_target_dir).map_err(|source| WasmBuildError::Io {
4890 operation: "resolve Cargo build target directory",
4891 path: build_target_dir.to_owned(),
4892 source,
4893 })?;
4894 let mut command = Command::new(&spec.cargo_program);
4895 command
4896 .current_dir(&spec.workspace_root)
4897 .env("CARGO_TARGET_DIR", absolute_target_dir)
4898 .args(["build", "--message-format=json"]);
4899 if let Some(example) = &spec.example {
4900 command.args(["--example", example]);
4901 } else {
4902 command.arg("--lib");
4903 }
4904 command.args(["--target", &spec.target]).args(
4905 spec.cargo_selection
4906 .arguments
4907 .iter()
4908 .filter(|argument| argument.as_os_str() != OsStr::new("--lib")),
4909 );
4910 apply_command_environment(&mut command, spec);
4911 for package in &spec.packages {
4912 command.args(["-p", package]);
4913 }
4914
4915 Ok(command)
4916}
4917
4918fn communicate_cargo_build(
4919 spec: &WasmBuildSpec,
4920 build_target_dir: &Path,
4921 targets: &[WasmArtifactTarget],
4922 mut command: Command,
4923 progress: &mut ProgressReporter<'_>,
4924) -> Result<Vec<PathBuf>, WasmBuildError> {
4925 command.stdout(Stdio::piped()).stderr(Stdio::piped());
4928 let started = Instant::now();
4929 let observed = progress.is_observed();
4930 if !observed {
4931 command.stdin(Stdio::null());
4933 }
4934 let mut child = if observed {
4935 OwnedChild::spawn(&mut command, CleanupPolicy::KillAndWait)
4936 } else {
4937 OwnedChild::spawn_direct(&mut command)
4938 }
4939 .map_err(|source| WasmBuildError::CommandSpawn {
4940 phase: WasmBuildPhase::CargoBuild,
4941 program: spec.cargo_program.clone(),
4942 source,
4943 })?;
4944 progress.emit(WasmBuildProgressEvent::CargoStarted {
4945 target_dir: build_target_dir.to_owned(),
4946 });
4947 let progress = RefCell::new(progress);
4948 let mut decoder = MessageDecoder::new(spec.cargo_message_bytes, true);
4949 let mut artifacts = vec![None; targets.len()];
4950 let mut protocol_error = None;
4951 let mut collect = |message| collect_wasm_artifact(message, targets, &mut artifacts);
4952 let result = communicate_child_with_observer(
4953 &mut child,
4954 None,
4955 OutputLimits {
4956 stdout: OutputLimit::Truncate(CARGO_DIAGNOSTIC_BYTES),
4959 stderr: OutputLimit::Truncate(CARGO_DIAGNOSTIC_BYTES),
4960 timeout: None,
4961 },
4962 SuccessfulExit::Cleanup,
4963 || {
4964 progress
4965 .borrow_mut()
4966 .emit_heartbeat_if_due(WasmBuildProgressPhase::CargoBuild, started.elapsed());
4967 false
4968 },
4969 |stream, bytes| {
4970 if stream == OutputStream::Stdout && protocol_error.is_none() {
4973 protocol_error = decoder.feed(bytes, &mut collect).err();
4974 }
4975 let mut progress = progress.borrow_mut();
4976 if progress.config.emit_cargo_output {
4977 progress.emit(WasmBuildProgressEvent::CargoOutput {
4978 stream: match stream {
4979 OutputStream::Stdout => WasmBuildOutputStream::Stdout,
4980 OutputStream::Stderr => WasmBuildOutputStream::Stderr,
4981 },
4982 bytes: bytes.to_vec(),
4983 });
4984 }
4985 Ok(())
4986 },
4987 );
4988 let status = match &result {
4989 Ok(evidence) => Some(evidence.status),
4990 Err(error) => error.evidence().and_then(|evidence| evidence.status),
4991 };
4992 if let Some(status) = status {
4993 progress
4994 .borrow_mut()
4995 .emit(WasmBuildProgressEvent::CargoFinished {
4996 success: status.success(),
4997 code: status.code(),
4998 elapsed: started.elapsed(),
4999 });
5000 }
5001 let evidence = result.map_err(|error| WasmBuildError::Io {
5002 operation: "communicate with cargo build",
5003 path: PathBuf::from(&spec.cargo_program),
5004 source: io::Error::other(error),
5006 })?;
5007 let status = evidence.status;
5008 if status.success() {
5009 protocol_error
5010 .map_or_else(|| decoder.finish(&mut collect), Err)
5011 .map_err(|source| WasmBuildError::Io {
5012 operation: "decode Cargo build artifacts",
5013 path: PathBuf::from(&spec.cargo_program),
5014 source,
5015 })?;
5016 return artifacts
5017 .into_iter()
5018 .collect::<Option<Vec<_>>>()
5019 .ok_or_else(|| WasmBuildError::MissingArtifacts {
5020 paths: expected_artifacts(spec, build_target_dir, targets),
5021 });
5022 }
5023 Err(WasmBuildError::CommandFailed {
5024 phase: WasmBuildPhase::CargoBuild,
5025 status,
5026 stdout: cargo_diagnostic(&evidence.stdout, evidence.stdout_truncated),
5027 stderr: cargo_diagnostic(&evidence.stderr, evidence.stderr_truncated),
5028 })
5029}
5030
5031fn collect_wasm_artifact(
5032 message: schema::Message,
5033 targets: &[WasmArtifactTarget],
5034 artifacts: &mut [Option<PathBuf>],
5035) -> io::Result<()> {
5036 let schema::Message::CompilerArtifact(artifact) = message else {
5037 return Ok(());
5038 };
5039 let Some((_, selected)) = targets.iter().zip(artifacts).find(|(target, _)| {
5041 artifact_matches(&artifact, &target.package_id, &target.name, &target.kind)
5042 }) else {
5043 return Ok(());
5044 };
5045 if !artifact
5046 .target
5047 .crate_types
5048 .contains(&schema::CrateType::CDyLib)
5049 {
5050 return Err(io::Error::new(
5051 io::ErrorKind::InvalidData,
5052 "Cargo reported a non-cdylib Wasm target",
5053 ));
5054 }
5055 let mut matching = artifact
5056 .filenames
5057 .into_iter()
5058 .filter(|path| path.extension() == Some("wasm"));
5059 let path = matching
5060 .next()
5061 .filter(|path| path.is_absolute())
5062 .ok_or_else(|| {
5063 io::Error::new(
5064 io::ErrorKind::InvalidData,
5065 "Cargo omitted absolute Wasm artifact path",
5066 )
5067 })?;
5068 if matching.next().is_some() || selected.replace(path.into_std_path_buf()).is_some() {
5069 return Err(io::Error::new(
5070 io::ErrorKind::InvalidData,
5071 "Cargo reported ambiguous Wasm artifacts",
5072 ));
5073 }
5074 Ok(())
5075}
5076
5077fn cargo_diagnostic(bytes: &[u8], truncated: bool) -> String {
5078 let mut text = String::from_utf8_lossy(bytes).into_owned();
5079 if truncated {
5080 text.push_str("\n[diagnostic truncated after 1 MiB; enable Cargo output events for the complete stream]\n");
5081 }
5082 text
5083}
5084
5085fn ensure_command_success(
5086 phase: WasmBuildPhase,
5087 output: CompletedExecution,
5088) -> Result<CompletedExecution, WasmBuildError> {
5089 if output.status.success() {
5090 return Ok(output);
5091 }
5092 Err(WasmBuildError::CommandFailed {
5093 phase,
5094 status: output.status,
5095 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
5096 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
5097 })
5098}
5099
5100fn expected_artifacts(
5101 spec: &WasmBuildSpec,
5102 target_dir: &Path,
5103 targets: &[WasmArtifactTarget],
5104) -> Vec<PathBuf> {
5105 let profile = target_dir.join(&spec.target).join(
5106 spec.cargo_selection
5107 .profile_target_dir()
5108 .expect("Cargo profile was validated before artifact discovery"),
5109 );
5110 let directory = if spec.example.is_some() {
5111 profile.join("examples")
5112 } else {
5113 profile
5114 };
5115 targets
5116 .iter()
5117 .map(|target| directory.join(format!("{}.wasm", target.name.replace('-', "_"))))
5118 .collect()
5119}
5120
5121fn cache_entry_directory(spec: &WasmBuildSpec, fingerprint: InputDigest) -> PathBuf {
5122 spec.target_dir
5123 .join(".ic-testkit/wasm-targets")
5124 .join(fingerprint.to_hex())
5125}
5126
5127fn validated_artifact_set(
5128 artifacts: &[PathBuf],
5129 fingerprint: InputDigest,
5130) -> Option<Vec<FileDigest>> {
5131 let header = artifact_stamp_header(fingerprint);
5132 let stamp_len =
5134 header.len() + ARTIFACT_STAMP_DIGEST_LABEL.len() + fingerprint.as_bytes().len() * 2 + 1;
5135 artifacts
5136 .iter()
5137 .map(|artifact| {
5138 let metadata = fs::metadata(artifact).ok()?;
5139 if !metadata.is_file() || metadata.len() == 0 {
5140 return None;
5141 }
5142 let stamp = read_stamp_with_limit(&artifact_stamp_path(artifact), stamp_len).ok()??;
5143 if stamp.len() != stamp_len || !stamp.starts_with(&header) {
5146 return None;
5147 }
5148 let info = digest_file("wasm-artifact-v1", artifact).ok()?;
5149 (stamp == artifact_stamp_contents(fingerprint, info.digest)).then_some(info)
5150 })
5151 .collect()
5152}
5153
5154fn missing_artifacts(artifacts: &[PathBuf]) -> Vec<PathBuf> {
5155 artifacts
5156 .iter()
5157 .filter(|path| {
5158 fs::metadata(path).map_or(true, |metadata| !metadata.is_file() || metadata.len() == 0)
5159 })
5160 .cloned()
5161 .collect()
5162}
5163
5164fn artifact_stamp_path(artifact: &Path) -> PathBuf {
5165 let mut name = artifact
5166 .file_name()
5167 .map_or_else(|| OsString::from("artifact"), OsString::from);
5168 name.push(".ic-testkit-build");
5169 artifact.with_file_name(name)
5170}
5171
5172fn artifact_stamp_header(fingerprint: InputDigest) -> String {
5173 format!("{CACHE_FORMAT_VERSION}\nbuild-sha256:{fingerprint}\n")
5174}
5175
5176fn artifact_stamp_contents(fingerprint: InputDigest, artifact_digest: InputDigest) -> String {
5177 format!(
5178 "{}{ARTIFACT_STAMP_DIGEST_LABEL}{artifact_digest}\n",
5179 artifact_stamp_header(fingerprint),
5180 )
5181}
5182
5183fn write_artifact_stamp(
5184 artifact: &Path,
5185 fingerprint: InputDigest,
5186 info: &FileDigest,
5187 preserve_matching: bool,
5188) -> Result<(), WasmBuildError> {
5189 let stamp_path = artifact_stamp_path(artifact);
5190 let stamp = artifact_stamp_contents(fingerprint, info.digest);
5191 if preserve_matching && destination_matches_bytes(&stamp_path, stamp.as_bytes()) {
5192 return Ok(());
5193 }
5194 ic_host_fs::durable::write_bytes(
5195 &stamp_path,
5196 ic_host_fs::durable::WriteOptions {
5197 mode: ic_host_fs::durable::PublicationMode::Replace,
5198 permissions: 0o666,
5199 },
5200 stamp.as_bytes(),
5201 )
5202 .map_err(|source| WasmBuildError::Io {
5203 operation: "publish Wasm build stamp",
5204 path: stamp_path,
5205 source: io::Error::other(source),
5206 })
5207}
5208
5209fn publish_artifact_stamps(
5210 artifacts: &[PathBuf],
5211 fingerprint: InputDigest,
5212) -> Result<Vec<FileDigest>, WasmBuildError> {
5213 let mut info = Vec::with_capacity(artifacts.len());
5214 for artifact in artifacts {
5215 let digest =
5216 digest_file("wasm-artifact-v1", artifact).map_err(|source| WasmBuildError::Io {
5217 operation: "hash built Wasm artifact",
5218 path: artifact.clone(),
5219 source,
5220 })?;
5221 write_artifact_stamp(artifact, fingerprint, &digest, false)?;
5222 info.push(digest);
5223 }
5224 Ok(info)
5225}
5226
5227fn materialize_artifacts(
5228 cached_artifacts: &[PathBuf],
5229 artifacts: &[PathBuf],
5230 fingerprint: InputDigest,
5231 artifact_info: &[FileDigest],
5232 preserve_matching: bool,
5233) -> Result<(), WasmBuildError> {
5234 for ((cached, artifact), info) in cached_artifacts.iter().zip(artifacts).zip(artifact_info) {
5235 materialize_verified_output(
5236 "wasm-artifact-v1",
5237 cached,
5238 artifact,
5239 preserve_matching.then_some(info),
5240 )
5241 .map_err(|source| WasmBuildError::Io {
5242 operation: "publish Wasm artifact",
5243 path: artifact.clone(),
5244 source,
5245 })?;
5246 }
5247 for (artifact, info) in artifacts.iter().zip(artifact_info) {
5250 write_artifact_stamp(artifact, fingerprint, info, preserve_matching)?;
5251 }
5252 Ok(())
5253}
5254
5255fn copy_wasm_artifacts(
5256 source_artifacts: &[PathBuf],
5257 cached_artifacts: &[PathBuf],
5258) -> Result<(), WasmBuildError> {
5259 for (source, cached) in source_artifacts.iter().zip(cached_artifacts) {
5260 if source == cached {
5261 continue;
5262 }
5263 copy_file_atomic(source, cached).map_err(|source_error| WasmBuildError::Io {
5264 operation: "cache shared-incremental Wasm artifact",
5265 path: cached.clone(),
5266 source: source_error,
5267 })?;
5268 }
5269 Ok(())
5270}
5271
5272fn create_dir_all(path: &Path, operation: &'static str) -> Result<(), WasmBuildError> {
5273 fs::create_dir_all(path).map_err(|source| WasmBuildError::Io {
5274 operation,
5275 path: path.to_owned(),
5276 source,
5277 })
5278}
5279
5280fn add_if_present(inputs: &mut Vec<(PathBuf, PathBuf)>, label: &str, path: PathBuf) {
5281 if path.exists() {
5282 inputs.push((PathBuf::from(label), path));
5283 }
5284}
5285
5286fn invalid_metadata(message: &str) -> WasmBuildError {
5287 WasmBuildError::InvalidMetadata {
5288 message: message.to_owned(),
5289 }
5290}
5291
5292impl WasmBuildError {
5293 fn indicates_input_change(&self) -> bool {
5294 match self {
5295 Self::InputsChangedDuringAcquisition { .. } => true,
5296 Self::FailedBuildCleanup { build_error, .. } => build_error.indicates_input_change(),
5297 _ => false,
5298 }
5299 }
5300}
5301
5302impl std::fmt::Display for WasmBuildPhase {
5303 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5304 formatter.write_str(match self {
5305 Self::CargoMetadata => "cargo metadata",
5306 Self::CargoIdentity => "Cargo identity",
5307 Self::RustcIdentity => "Rust compiler identity",
5308 Self::CargoBuild => "cargo build",
5309 })
5310 }
5311}
5312
5313impl std::fmt::Display for WasmBuildProgressPhase {
5314 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5315 formatter.write_str(match self {
5316 Self::ExactCacheLock => "exact cache lock",
5317 Self::CargoIdentity => "Cargo identity",
5318 Self::RustcIdentity => "Rust compiler identity",
5319 Self::CargoMetadata => "Cargo metadata",
5320 Self::InputDiscovery => "input discovery",
5321 Self::ContentHashing => "content hashing",
5322 Self::SharedTargetLock => "shared target lock",
5323 Self::SharedTargetMaintenance => "shared target maintenance",
5324 Self::CargoBuild => "Cargo build",
5325 Self::ArtifactPublication => "artifact publication",
5326 Self::ExactCacheMaintenance => "exact cache maintenance",
5327 })
5328 }
5329}
5330
5331impl std::fmt::Display for WasmBuildError {
5332 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5333 match self {
5334 Self::InvalidSpec { message } => {
5335 write!(formatter, "invalid Wasm build spec: {message}")
5336 }
5337 Self::Io {
5338 operation,
5339 path,
5340 source,
5341 } => write!(
5342 formatter,
5343 "failed to {operation} at {}: {source}",
5344 path.display()
5345 ),
5346 Self::CommandSpawn {
5347 phase,
5348 program,
5349 source,
5350 } => write!(
5351 formatter,
5352 "failed to launch {phase} using `{}`: {source}",
5353 program.to_string_lossy(),
5354 ),
5355 Self::CommandFailed {
5356 phase,
5357 status,
5358 stdout,
5359 stderr,
5360 } => write!(
5361 formatter,
5362 "{phase} failed with {status}\nstdout:\n{stdout}\nstderr:\n{stderr}",
5363 ),
5364 Self::InvalidMetadata { message } => {
5365 write!(formatter, "invalid Cargo metadata: {message}")
5366 }
5367 Self::InvalidCargoConfiguration { path, message } => write!(
5368 formatter,
5369 "invalid Cargo configuration at {}: {message}",
5370 path.display(),
5371 ),
5372 Self::MissingArtifacts { paths } => {
5373 formatter.write_str("cargo build succeeded without producing: ")?;
5374 for (index, path) in paths.iter().enumerate() {
5375 if index != 0 {
5376 formatter.write_str(", ")?;
5377 }
5378 write!(formatter, "{}", path.display())?;
5379 }
5380 Ok(())
5381 }
5382 Self::InputsChangedDuringAcquisition { before, after } => write!(
5383 formatter,
5384 "Wasm inputs changed during artifact acquisition: {before} -> {after}",
5385 ),
5386 Self::PreparedInputSnapshotInvalidated => formatter.write_str(
5387 "the prepared Wasm input snapshot was invalidated before artifact publication",
5388 ),
5389 Self::FailedBuildCleanup {
5390 build_error,
5391 path,
5392 source,
5393 } => write!(
5394 formatter,
5395 "Wasm build failed ({build_error}) and its incomplete target directory at {} could not be removed: {source}",
5396 path.display(),
5397 ),
5398 }
5399 }
5400}
5401
5402impl std::error::Error for WasmBuildError {
5403 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
5404 match self {
5405 Self::Io { source, .. }
5406 | Self::CommandSpawn { source, .. }
5407 | Self::FailedBuildCleanup { source, .. } => Some(source),
5408 _ => None,
5409 }
5410 }
5411}
5412
5413#[cfg(test)]
5414mod tests;