1use crate::canister_build::{
2 CanisterBuildProfile, build_current_workspace_canister_artifact,
3 current_workspace_build_context_once,
4};
5use crate::deployment_truth::{
6 ArtifactTransportV1, CurrentCliDeploymentExecutor, DeploymentCheckV1,
7 DeploymentCommandResultV1, DeploymentExecutionContextV1, DeploymentExecutionPreflightV1,
8 DeploymentExecutionStatusV1, DeploymentExecutor, DeploymentExecutorCapabilityV1,
9 DeploymentReceiptV1, LocalDeploymentCheckRequest, ObservationStatusV1, SafetyFindingV1,
10 StagingReceiptV1, artifact_gate_phase_receipt, artifact_gate_role_phase_receipts,
11 check_local_deployment, deployment_execution_preflight_from_check,
12 deployment_receipt_from_check_with_status, missing_executor_capabilities, phase_receipt,
13 staging_receipt_evidence, validate_deployment_execution_preflight_for_check,
14};
15use crate::format::wasm_size_label;
16use crate::icp::{self, CANIC_ICP_LOCAL_NETWORK_URL_ENV, CANIC_ICP_LOCAL_ROOT_KEY_ENV};
17use crate::release_set::{
18 LOCAL_ROOT_MIN_READY_CYCLES, RootReleaseSetManifest, configured_fleet_name,
19 configured_install_targets, configured_local_root_create_cycles,
20 emit_root_release_set_manifest_with_config, icp_query_on_network, icp_root,
21 load_root_release_set_manifest, resolve_artifact_root, resume_root_bootstrap,
22 stage_root_release_set, workspace_root,
23};
24use crate::replica_query;
25use crate::response_parse::parse_cycle_balance_response;
26use crate::table::{ColumnAlign, render_separator, render_table, render_table_row, table_widths};
27use canic_core::{
28 cdk::{types::Principal, utils::hash::wasm_hash},
29 protocol,
30};
31use config_selection::resolve_install_config_path;
32use serde_json::Value as JsonValue;
33use std::{
34 env,
35 ffi::OsString,
36 fs,
37 path::{Path, PathBuf},
38 process::Command,
39 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
40};
41
42mod config_selection;
43mod readiness;
44mod state;
45
46pub use config_selection::{
47 current_canic_project_root, discover_canic_config_choices, discover_canic_project_root_from,
48 discover_project_canic_config_choices, project_fleet_roots,
49};
50use readiness::wait_for_root_ready;
51use state::{
52 INSTALL_STATE_SCHEMA_VERSION, validate_fleet_name, validate_network_name, write_install_state,
53};
54pub use state::{
55 InstallState, read_named_fleet_install_state, read_named_fleet_install_state_from_root,
56};
57
58#[cfg(test)]
59mod tests;
60
61#[cfg(test)]
62use config_selection::config_selection_error;
63#[cfg(test)]
64use readiness::{parse_bootstrap_status_value, parse_root_ready_value};
65#[cfg(test)]
66use state::{fleet_install_state_path, read_fleet_install_state};
67
68#[derive(Clone, Debug)]
73pub struct InstallRootOptions {
74 pub root_canister: String,
75 pub root_build_target: String,
76 pub network: String,
77 pub icp_root: Option<PathBuf>,
78 pub build_profile: Option<CanisterBuildProfile>,
79 pub ready_timeout_seconds: u64,
80 pub config_path: Option<String>,
81 pub expected_fleet: Option<String>,
82 pub interactive_config_selection: bool,
83}
84
85#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
90struct InstallTimingSummary {
91 create_canisters: Duration,
92 build_all: Duration,
93 emit_manifest: Duration,
94 install_root: Duration,
95 fund_root: Duration,
96 stage_release_set: Duration,
97 resume_bootstrap: Duration,
98 wait_ready: Duration,
99 finalize_root_funding: Duration,
100}
101
102const CURRENT_INSTALL_REQUIRED_CAPABILITIES: &[DeploymentExecutorCapabilityV1] = &[
103 DeploymentExecutorCapabilityV1::CreateCanister,
104 DeploymentExecutorCapabilityV1::InstallCode,
105 DeploymentExecutorCapabilityV1::Call,
106 DeploymentExecutorCapabilityV1::Query,
107 DeploymentExecutorCapabilityV1::StageArtifact,
108];
109
110pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
112 let project_root = current_canic_project_root()?;
113 let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
114 if !choices.is_empty() {
115 return Ok(choices);
116 }
117
118 if let Ok(icp_root) = icp_root()
119 && icp_root != project_root
120 {
121 return config_selection::discover_workspace_canic_config_choices(&icp_root);
122 }
123
124 Ok(choices)
125}
126
127pub fn install_root(options: InstallRootOptions) -> Result<(), Box<dyn std::error::Error>> {
129 let workspace_root = workspace_root()?;
130 let icp_root = match &options.icp_root {
131 Some(path) => path.canonicalize()?,
132 None => icp_root()?,
133 };
134 let config_path = resolve_install_config_path(
135 &icp_root,
136 options.config_path.as_deref(),
137 options.interactive_config_selection,
138 )?;
139 let _install_env = BuildEnvGuard::apply(&options.network, &config_path, &icp_root);
140 let fleet_name = configured_fleet_name(&config_path)?;
141 validate_expected_fleet_name(options.expected_fleet.as_deref(), &fleet_name, &config_path)?;
142 validate_fleet_name(&fleet_name)?;
143 let total_started_at = Instant::now();
144 let mut timings = InstallTimingSummary::default();
145 let network = options.network.as_str();
146 let execution_context = current_install_execution_context(&workspace_root, &icp_root, network);
147
148 println!("Installing fleet {fleet_name}");
149 println!();
150 let prepared = prepare_install_deployment_truth(
151 &options,
152 &workspace_root,
153 &icp_root,
154 &config_path,
155 &fleet_name,
156 &execution_context,
157 )?;
158 timings.create_canisters = prepared.timings.create_canisters;
159 timings.build_all = prepared.timings.build_all;
160
161 let (manifest_path, emit_manifest_duration) = emit_manifest_with_deployment_truth_receipt(
162 &workspace_root,
163 &icp_root,
164 &options,
165 &config_path,
166 &fleet_name,
167 &prepared.deployment_truth_check,
168 &execution_context,
169 )?;
170 timings.emit_manifest = emit_manifest_duration;
171 let activation_timings = run_root_activation_phases(
172 InstallReceiptScope {
173 icp_root: &icp_root,
174 network,
175 fleet_name: &fleet_name,
176 check: &prepared.deployment_truth_check,
177 execution_context: Some(&execution_context),
178 },
179 &options,
180 &prepared.root_canister_id,
181 &manifest_path,
182 total_started_at,
183 )?;
184 timings.install_root = activation_timings.install_root;
185 timings.fund_root = activation_timings.fund_root;
186 timings.stage_release_set = activation_timings.stage_release_set;
187 timings.resume_bootstrap = activation_timings.resume_bootstrap;
188 timings.wait_ready = activation_timings.wait_ready;
189 timings.finalize_root_funding = activation_timings.finalize_root_funding;
190
191 print_install_timing_summary(&timings, total_started_at.elapsed());
192 let state = build_install_state(
193 &options,
194 &workspace_root,
195 &icp_root,
196 &config_path,
197 &manifest_path,
198 &fleet_name,
199 &prepared.root_canister_id,
200 )?;
201 let state_path = write_install_state_with_deployment_truth_receipt(
202 InstallReceiptScope {
203 icp_root: &icp_root,
204 network,
205 fleet_name: &fleet_name,
206 check: &prepared.deployment_truth_check,
207 execution_context: Some(&execution_context),
208 },
209 &options.network,
210 &state,
211 )?;
212 print_install_result_summary(&options.network, &state.fleet, &state_path);
213 Ok(())
214}
215
216struct PreparedInstallTruth {
217 root_canister_id: String,
218 deployment_truth_check: DeploymentCheckV1,
219 timings: InstallTimingSummary,
220}
221
222struct CurrentInstallTruthInputs {
223 workspace_root: PathBuf,
224 icp_root: PathBuf,
225 config_path: PathBuf,
226 fleet_name: String,
227}
228
229fn prepare_install_deployment_truth(
230 options: &InstallRootOptions,
231 workspace_root: &Path,
232 icp_root: &Path,
233 config_path: &Path,
234 fleet_name: &str,
235 execution_context: &DeploymentExecutionContextV1,
236) -> Result<PreparedInstallTruth, Box<dyn std::error::Error>> {
237 let mut timings = InstallTimingSummary::default();
238 ensure_current_install_executor_capabilities(execution_context)?;
239 ensure_icp_environment_ready(icp_root, &options.network)?;
240 let (root_canister_id, create_phase, create_duration) =
241 resolve_root_canister_with_phase(options, icp_root, config_path)?;
242 timings.create_canisters = create_duration;
243
244 let (build_phase, build_duration) =
245 build_install_targets_with_phase(options, icp_root, config_path)?;
246 timings.build_all = build_duration;
247
248 let deployment_truth_check = run_install_deployment_truth_safety_gate(
249 options,
250 workspace_root,
251 icp_root,
252 config_path,
253 fleet_name,
254 execution_context,
255 )?;
256 let receipt_scope = InstallReceiptScope {
257 icp_root,
258 network: &options.network,
259 fleet_name,
260 check: &deployment_truth_check,
261 execution_context: Some(execution_context),
262 };
263 write_completed_install_phase_receipt(receipt_scope, create_phase)?;
264 write_completed_install_phase_receipt(receipt_scope, build_phase)?;
265
266 Ok(PreparedInstallTruth {
267 root_canister_id,
268 deployment_truth_check,
269 timings,
270 })
271}
272
273fn resolve_root_canister_with_phase(
274 options: &InstallRootOptions,
275 icp_root: &Path,
276 config_path: &Path,
277) -> Result<(String, CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
278 let operation = ResolveRootCanisterOperation::new(
279 icp_root,
280 &options.network,
281 &options.root_canister,
282 config_path,
283 );
284 let started_at = current_unix_timestamp_label()?;
285 let started = Instant::now();
286 let root_canister_id = operation.execute()?;
287 let duration = started.elapsed();
288 let phase = CompletedInstallPhase {
289 phase: "resolve_root_canister",
290 attempted_action: "resolve or create root canister id",
291 started_at,
292 finished_at: Some(current_unix_timestamp_label()?),
293 evidence: operation.evidence(&root_canister_id),
294 role_names: Vec::new(),
295 };
296 Ok((root_canister_id, phase, duration))
297}
298
299fn build_install_targets_with_phase(
300 options: &InstallRootOptions,
301 icp_root: &Path,
302 config_path: &Path,
303) -> Result<(CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
304 let build_targets = configured_install_targets(config_path, &options.root_build_target)?;
305 let operation = BuildInstallTargetsOperation::new(
306 &options.network,
307 build_targets,
308 options.build_profile,
309 config_path,
310 icp_root,
311 );
312 let started_at = current_unix_timestamp_label()?;
313 let started = Instant::now();
314 operation.execute()?;
315 let duration = started.elapsed();
316 let phase = CompletedInstallPhase {
317 phase: "build_artifacts",
318 attempted_action: "build configured install targets",
319 started_at,
320 finished_at: Some(current_unix_timestamp_label()?),
321 evidence: operation.evidence(),
322 role_names: operation.role_names(),
323 };
324 Ok((phase, duration))
325}
326
327fn emit_manifest_with_deployment_truth_receipt(
328 workspace_root: &Path,
329 icp_root: &Path,
330 options: &InstallRootOptions,
331 config_path: &Path,
332 fleet_name: &str,
333 deployment_truth_check: &DeploymentCheckV1,
334 execution_context: &DeploymentExecutionContextV1,
335) -> Result<(PathBuf, Duration), Box<dyn std::error::Error>> {
336 let operation =
337 EmitRootManifestOperation::new(workspace_root, icp_root, &options.network, config_path);
338 let emit_manifest_started_at_label = current_unix_timestamp_label()?;
339 let emit_manifest_started_at = Instant::now();
340 let manifest_path = operation.execute()?;
341 let emit_manifest_duration = emit_manifest_started_at.elapsed();
342 let emit_manifest_receipt = receipt_with_execution_context(
343 install_deployment_truth_phase_receipt(
344 deployment_truth_check,
345 "emit_manifest",
346 emit_manifest_started_at_label,
347 Some(current_unix_timestamp_label()?),
348 "emit root release-set manifest",
349 crate::deployment_truth::ObservationStatusV1::Observed,
350 EmitRootManifestOperation::evidence(&manifest_path),
351 ),
352 execution_context,
353 );
354 let emit_manifest_receipt_path = write_install_deployment_truth_receipt(
355 icp_root,
356 &options.network,
357 fleet_name,
358 &emit_manifest_receipt,
359 )?;
360 println!(
361 "Deployment truth receipt JSON: {}",
362 emit_manifest_receipt_path.display()
363 );
364 Ok((manifest_path, emit_manifest_duration))
365}
366
367fn run_root_activation_phases(
368 receipt_scope: InstallReceiptScope<'_>,
369 options: &InstallRootOptions,
370 root_canister_id: &str,
371 manifest_path: &Path,
372 total_started_at: Instant,
373) -> Result<InstallTimingSummary, Box<dyn std::error::Error>> {
374 let mut timings = InstallTimingSummary::default();
375 let root_wasm = resolve_artifact_root(receipt_scope.icp_root, receipt_scope.network)?
376 .join(&options.root_build_target)
377 .join(format!("{}.wasm", options.root_build_target));
378 let install_operation = InstallRootWasmOperation::new(
379 receipt_scope.icp_root,
380 receipt_scope.network,
381 root_canister_id,
382 root_wasm,
383 );
384 timings.install_root = receipt_scope.run_operation(&install_operation)?;
385 let pre_bootstrap_funding = EnsureRootCyclesOperation::new(
386 receipt_scope.icp_root,
387 receipt_scope.network,
388 root_canister_id,
389 "fund_root_pre_bootstrap",
390 "ensure local root minimum cycles before bootstrap",
391 "pre-bootstrap",
392 );
393 timings.fund_root = receipt_scope.run_operation(&pre_bootstrap_funding)?;
394 let manifest = load_root_release_set_manifest(manifest_path)?;
395 let stage_operation = StageReleaseSetOperation::new(
396 receipt_scope.icp_root,
397 receipt_scope.network,
398 root_canister_id,
399 manifest_path,
400 manifest,
401 );
402 timings.stage_release_set = receipt_scope.run_operation(&stage_operation)?;
403 let resume_operation = ResumeBootstrapOperation::new(receipt_scope.network, root_canister_id);
404 timings.resume_bootstrap = receipt_scope.run_operation(&resume_operation)?;
405 let wait_ready_operation = WaitRootReadyOperation::new(
406 receipt_scope.network,
407 root_canister_id,
408 options.ready_timeout_seconds,
409 );
410 let wait_ready_result = receipt_scope.run_operation(&wait_ready_operation);
411 match wait_ready_result {
412 Ok(duration) => timings.wait_ready = duration,
413 Err(err) => {
414 print_install_timing_summary(&timings, total_started_at.elapsed());
415 return Err(err);
416 }
417 }
418 let post_ready_funding = EnsureRootCyclesOperation::new(
419 receipt_scope.icp_root,
420 receipt_scope.network,
421 root_canister_id,
422 "fund_root_post_ready",
423 "ensure local root minimum cycles after ready",
424 "post-ready",
425 );
426 timings.finalize_root_funding = receipt_scope.run_operation(&post_ready_funding)?;
427 Ok(timings)
428}
429
430#[derive(Clone, Copy)]
431struct InstallReceiptScope<'a> {
432 icp_root: &'a Path,
433 network: &'a str,
434 fleet_name: &'a str,
435 check: &'a DeploymentCheckV1,
436 execution_context: Option<&'a DeploymentExecutionContextV1>,
437}
438
439struct CompletedInstallPhase {
440 phase: &'static str,
441 attempted_action: &'static str,
442 started_at: String,
443 finished_at: Option<String>,
444 evidence: Vec<String>,
445 role_names: Vec<String>,
446}
447
448trait InstallPhaseOperation {
449 fn phase(&self) -> &'static str;
450 fn attempted_action(&self) -> &'static str;
451 fn evidence(&self) -> Vec<String>;
452 fn execute(&self) -> Result<(), Box<dyn std::error::Error>>;
453}
454
455struct ResolveRootCanisterOperation<'a> {
456 icp_root: &'a Path,
457 network: &'a str,
458 root_canister: &'a str,
459 config_path: &'a Path,
460}
461
462impl<'a> ResolveRootCanisterOperation<'a> {
463 const fn new(
464 icp_root: &'a Path,
465 network: &'a str,
466 root_canister: &'a str,
467 config_path: &'a Path,
468 ) -> Self {
469 Self {
470 icp_root,
471 network,
472 root_canister,
473 config_path,
474 }
475 }
476
477 fn evidence(&self, root_canister_id: &str) -> Vec<String> {
478 vec![
479 format!("root_target:{}", self.root_canister),
480 format!("root_canister:{root_canister_id}"),
481 ]
482 }
483
484 fn execute(&self) -> Result<String, Box<dyn std::error::Error>> {
485 ensure_root_canister_id(
486 self.icp_root,
487 self.network,
488 self.root_canister,
489 self.config_path,
490 )
491 }
492}
493
494struct BuildInstallTargetsOperation<'a> {
495 network: &'a str,
496 build_targets: Vec<String>,
497 build_profile: Option<CanisterBuildProfile>,
498 config_path: &'a Path,
499 icp_root: &'a Path,
500}
501
502impl<'a> BuildInstallTargetsOperation<'a> {
503 const fn new(
504 network: &'a str,
505 build_targets: Vec<String>,
506 build_profile: Option<CanisterBuildProfile>,
507 config_path: &'a Path,
508 icp_root: &'a Path,
509 ) -> Self {
510 Self {
511 network,
512 build_targets,
513 build_profile,
514 config_path,
515 icp_root,
516 }
517 }
518
519 fn evidence(&self) -> Vec<String> {
520 self.build_targets
521 .iter()
522 .map(|target| format!("build_target:{target}"))
523 .collect()
524 }
525
526 fn role_names(&self) -> Vec<String> {
527 self.build_targets.clone()
528 }
529
530 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
531 run_canic_build_targets(
532 self.network,
533 &self.build_targets,
534 self.build_profile,
535 self.config_path,
536 self.icp_root,
537 )
538 }
539}
540
541struct EmitRootManifestOperation<'a> {
542 workspace_root: &'a Path,
543 icp_root: &'a Path,
544 network: &'a str,
545 config_path: &'a Path,
546}
547
548impl<'a> EmitRootManifestOperation<'a> {
549 const fn new(
550 workspace_root: &'a Path,
551 icp_root: &'a Path,
552 network: &'a str,
553 config_path: &'a Path,
554 ) -> Self {
555 Self {
556 workspace_root,
557 icp_root,
558 network,
559 config_path,
560 }
561 }
562
563 fn evidence(manifest_path: &Path) -> Vec<String> {
564 vec![format!("manifest_path:{}", manifest_path.display())]
565 }
566
567 fn execute(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
568 emit_root_release_set_manifest_with_config(
569 self.workspace_root,
570 self.icp_root,
571 self.network,
572 self.config_path,
573 )
574 }
575}
576
577struct InstallRootWasmOperation<'a> {
578 icp_root: &'a Path,
579 network: &'a str,
580 root_canister_id: &'a str,
581 root_wasm: PathBuf,
582}
583
584impl<'a> InstallRootWasmOperation<'a> {
585 const fn new(
586 icp_root: &'a Path,
587 network: &'a str,
588 root_canister_id: &'a str,
589 root_wasm: PathBuf,
590 ) -> Self {
591 Self {
592 icp_root,
593 network,
594 root_canister_id,
595 root_wasm,
596 }
597 }
598}
599
600impl InstallPhaseOperation for InstallRootWasmOperation<'_> {
601 fn phase(&self) -> &'static str {
602 "install_root"
603 }
604
605 fn attempted_action(&self) -> &'static str {
606 "install root wasm"
607 }
608
609 fn evidence(&self) -> Vec<String> {
610 vec![
611 format!("root_canister:{}", self.root_canister_id),
612 format!("root_wasm:{}", self.root_wasm.display()),
613 ]
614 }
615
616 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
617 reinstall_root_wasm(
618 self.icp_root,
619 self.network,
620 self.root_canister_id,
621 &self.root_wasm,
622 )
623 }
624}
625
626struct EnsureRootCyclesOperation<'a> {
627 icp_root: &'a Path,
628 network: &'a str,
629 root_canister_id: &'a str,
630 phase: &'static str,
631 attempted_action: &'static str,
632 phase_label: &'a str,
633}
634
635impl<'a> EnsureRootCyclesOperation<'a> {
636 const fn new(
637 icp_root: &'a Path,
638 network: &'a str,
639 root_canister_id: &'a str,
640 phase: &'static str,
641 attempted_action: &'static str,
642 phase_label: &'a str,
643 ) -> Self {
644 Self {
645 icp_root,
646 network,
647 root_canister_id,
648 phase,
649 attempted_action,
650 phase_label,
651 }
652 }
653}
654
655impl InstallPhaseOperation for EnsureRootCyclesOperation<'_> {
656 fn phase(&self) -> &'static str {
657 self.phase
658 }
659
660 fn attempted_action(&self) -> &'static str {
661 self.attempted_action
662 }
663
664 fn evidence(&self) -> Vec<String> {
665 vec![
666 format!("root_canister:{}", self.root_canister_id),
667 format!("minimum_cycles:{LOCAL_ROOT_MIN_READY_CYCLES}"),
668 format!("funding_phase:{}", self.phase_label),
669 ]
670 }
671
672 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
673 ensure_local_root_min_cycles(
674 self.icp_root,
675 self.network,
676 self.root_canister_id,
677 self.phase_label,
678 )
679 }
680}
681
682struct ResumeBootstrapOperation<'a> {
683 network: &'a str,
684 root_canister_id: &'a str,
685}
686
687impl<'a> ResumeBootstrapOperation<'a> {
688 const fn new(network: &'a str, root_canister_id: &'a str) -> Self {
689 Self {
690 network,
691 root_canister_id,
692 }
693 }
694}
695
696impl InstallPhaseOperation for ResumeBootstrapOperation<'_> {
697 fn phase(&self) -> &'static str {
698 "resume_bootstrap"
699 }
700
701 fn attempted_action(&self) -> &'static str {
702 "resume root bootstrap"
703 }
704
705 fn evidence(&self) -> Vec<String> {
706 vec![format!("root_canister:{}", self.root_canister_id)]
707 }
708
709 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
710 resume_root_bootstrap(self.network, self.root_canister_id)
711 }
712}
713
714struct WaitRootReadyOperation<'a> {
715 network: &'a str,
716 root_canister_id: &'a str,
717 timeout_seconds: u64,
718}
719
720impl<'a> WaitRootReadyOperation<'a> {
721 const fn new(network: &'a str, root_canister_id: &'a str, timeout_seconds: u64) -> Self {
722 Self {
723 network,
724 root_canister_id,
725 timeout_seconds,
726 }
727 }
728}
729
730impl InstallPhaseOperation for WaitRootReadyOperation<'_> {
731 fn phase(&self) -> &'static str {
732 "wait_ready"
733 }
734
735 fn attempted_action(&self) -> &'static str {
736 "wait for root bootstrap readiness"
737 }
738
739 fn evidence(&self) -> Vec<String> {
740 vec![
741 format!("root_canister:{}", self.root_canister_id),
742 format!("timeout_seconds:{}", self.timeout_seconds),
743 ]
744 }
745
746 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
747 wait_for_root_ready(self.network, self.root_canister_id, self.timeout_seconds)
748 }
749}
750
751fn write_completed_install_phase_receipt(
752 receipt_scope: InstallReceiptScope<'_>,
753 completed: CompletedInstallPhase,
754) -> Result<PathBuf, Box<dyn std::error::Error>> {
755 let role_phase_receipts = completed
756 .role_names
757 .iter()
758 .filter_map(|role| {
759 completed_phase_role_receipt(
760 receipt_scope.check,
761 completed.phase,
762 role,
763 crate::deployment_truth::RolePhaseResultV1::Applied,
764 None,
765 )
766 })
767 .collect();
768 let receipt =
769 receipt_scope.with_execution_context(install_deployment_truth_phase_receipt_with_result(
770 receipt_scope.check,
771 PhaseReceiptInput {
772 phase: completed.phase,
773 started_at: completed.started_at,
774 finished_at: completed.finished_at,
775 attempted_action: completed.attempted_action,
776 status: crate::deployment_truth::ObservationStatusV1::Observed,
777 evidence: completed.evidence,
778 role_phase_receipts,
779 operation_status: DeploymentExecutionStatusV1::Complete,
780 command_result: DeploymentCommandResultV1::Succeeded,
781 },
782 ));
783 receipt_scope.write_receipt(&receipt)
784}
785
786fn completed_phase_role_receipt(
787 check: &DeploymentCheckV1,
788 phase: &str,
789 role: &str,
790 result: crate::deployment_truth::RolePhaseResultV1,
791 error: Option<String>,
792) -> Option<crate::deployment_truth::RolePhaseReceiptV1> {
793 let planned = check
794 .plan
795 .role_artifacts
796 .iter()
797 .find(|artifact| artifact.role == role)?;
798 let observed = check
799 .inventory
800 .observed_artifacts
801 .iter()
802 .find(|artifact| artifact.role == role);
803 let artifact_digest = observed
804 .and_then(|artifact| artifact.file_sha256.clone())
805 .or_else(|| observed.and_then(|artifact| artifact.payload_sha256.clone()))
806 .or_else(|| planned.observed_wasm_gz_file_sha256.clone())
807 .or_else(|| planned.wasm_gz_sha256.clone());
808
809 Some(crate::deployment_truth::RolePhaseReceiptV1 {
810 role: role.to_string(),
811 phase: phase.to_string(),
812 result,
813 previous_module_hash: None,
814 target_module_hash: planned.installed_module_hash.clone(),
815 observed_module_hash_after: None,
816 artifact_digest,
817 canonical_embedded_config_sha256: planned.canonical_embedded_config_sha256.clone(),
818 error,
819 })
820}
821
822fn write_install_state_with_deployment_truth_receipt(
823 receipt_scope: InstallReceiptScope<'_>,
824 network: &str,
825 state: &InstallState,
826) -> Result<PathBuf, Box<dyn std::error::Error>> {
827 let started_at = current_unix_timestamp_label()?;
828 let state_path = write_install_state(receipt_scope.icp_root, network, state)?;
829 let completed = CompletedInstallPhase {
830 phase: "write_install_state",
831 attempted_action: "write local install state",
832 started_at,
833 finished_at: Some(current_unix_timestamp_label()?),
834 evidence: vec![
835 format!("install_state:{}", state_path.display()),
836 format!("fleet:{}", state.fleet),
837 format!("root_canister:{}", state.root_canister_id),
838 ],
839 role_names: Vec::new(),
840 };
841 write_completed_install_phase_receipt(receipt_scope, completed)?;
842 Ok(state_path)
843}
844
845impl InstallReceiptScope<'_> {
846 fn run_operation(
847 self,
848 operation: &impl InstallPhaseOperation,
849 ) -> Result<Duration, Box<dyn std::error::Error>> {
850 self.run_phase(
851 operation.phase(),
852 operation.attempted_action(),
853 operation.evidence(),
854 || operation.execute(),
855 )
856 }
857
858 fn run_phase(
859 self,
860 phase: &str,
861 attempted_action: &str,
862 evidence: Vec<String>,
863 run: impl FnOnce() -> Result<(), Box<dyn std::error::Error>>,
864 ) -> Result<Duration, Box<dyn std::error::Error>> {
865 let started_at = current_unix_timestamp_label()?;
866 let started = Instant::now();
867 match run() {
868 Ok(()) => {
869 let duration = started.elapsed();
870 let receipt = self.with_execution_context(install_deployment_truth_phase_receipt(
871 self.check,
872 phase,
873 started_at,
874 Some(current_unix_timestamp_label()?),
875 attempted_action,
876 crate::deployment_truth::ObservationStatusV1::Observed,
877 evidence,
878 ));
879 self.write_receipt(&receipt)?;
880 Ok(duration)
881 }
882 Err(err) => {
883 self.try_write_failed_phase_receipt(
884 phase,
885 started_at,
886 attempted_action,
887 evidence,
888 err.as_ref(),
889 );
890 Err(err)
891 }
892 }
893 }
894
895 fn with_execution_context(self, receipt: DeploymentReceiptV1) -> DeploymentReceiptV1 {
896 match self.execution_context {
897 Some(context) => receipt_with_execution_context(receipt, context),
898 None => receipt,
899 }
900 }
901
902 fn write_receipt(
903 self,
904 receipt: &DeploymentReceiptV1,
905 ) -> Result<PathBuf, Box<dyn std::error::Error>> {
906 let path = write_install_deployment_truth_receipt(
907 self.icp_root,
908 self.network,
909 self.fleet_name,
910 receipt,
911 )?;
912 println!("Deployment truth receipt JSON: {}", path.display());
913 Ok(path)
914 }
915
916 fn try_write_failed_phase_receipt(
917 self,
918 phase: &str,
919 started_at: String,
920 attempted_action: &str,
921 evidence: Vec<String>,
922 err: &dyn std::error::Error,
923 ) {
924 let receipt = install_deployment_truth_phase_receipt_with_result(
925 self.check,
926 PhaseReceiptInput {
927 phase,
928 started_at,
929 finished_at: Some(
930 current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string()),
931 ),
932 attempted_action,
933 status: crate::deployment_truth::ObservationStatusV1::Inconclusive,
934 evidence,
935 role_phase_receipts: Vec::new(),
936 operation_status: DeploymentExecutionStatusV1::FailedAfterMutation,
937 command_result: DeploymentCommandResultV1::Failed {
938 code: format!("{phase}_failed"),
939 message: err.to_string(),
940 },
941 },
942 );
943 let receipt = self.with_execution_context(receipt);
944 if let Err(write_err) = self.write_receipt(&receipt) {
945 eprintln!("Deployment truth receipt JSON write failed: {write_err}");
946 }
947 }
948}
949
950pub fn check_install_deployment_truth(
953 options: &InstallRootOptions,
954 observed_at: impl Into<String>,
955) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
956 let inputs = resolve_current_install_truth_inputs(options)?;
957 current_install_deployment_truth_check_at(
958 options,
959 &inputs.workspace_root,
960 &inputs.icp_root,
961 &inputs.config_path,
962 &inputs.fleet_name,
963 observed_at.into(),
964 )
965}
966
967pub fn check_install_execution_preflight(
973 options: &InstallRootOptions,
974 observed_at: impl Into<String>,
975) -> Result<DeploymentExecutionPreflightV1, Box<dyn std::error::Error>> {
976 let inputs = resolve_current_install_truth_inputs(options)?;
977 let check = current_install_deployment_truth_check_at(
978 options,
979 &inputs.workspace_root,
980 &inputs.icp_root,
981 &inputs.config_path,
982 &inputs.fleet_name,
983 observed_at.into(),
984 )?;
985 let execution_context = current_install_execution_context(
986 &inputs.workspace_root,
987 &inputs.icp_root,
988 &options.network,
989 );
990 let executor = CurrentCliDeploymentExecutor::new(
991 execution_context.workspace_root,
992 execution_context.icp_root,
993 execution_context.artifact_roots,
994 );
995 let preflight = deployment_execution_preflight_from_check(
996 &check,
997 &executor,
998 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
999 );
1000 validate_deployment_execution_preflight_for_check(&check, &preflight)?;
1001 Ok(preflight)
1002}
1003
1004fn resolve_current_install_truth_inputs(
1005 options: &InstallRootOptions,
1006) -> Result<CurrentInstallTruthInputs, Box<dyn std::error::Error>> {
1007 let workspace_root = workspace_root()?;
1008 let icp_root = match &options.icp_root {
1009 Some(path) => path.canonicalize()?,
1010 None => icp_root()?,
1011 };
1012 let config_path = resolve_install_config_path(
1013 &icp_root,
1014 options.config_path.as_deref(),
1015 options.interactive_config_selection,
1016 )?;
1017 let fleet_name = configured_fleet_name(&config_path)?;
1018 validate_expected_fleet_name(options.expected_fleet.as_deref(), &fleet_name, &config_path)?;
1019 validate_fleet_name(&fleet_name)?;
1020 Ok(CurrentInstallTruthInputs {
1021 workspace_root,
1022 icp_root,
1023 config_path,
1024 fleet_name,
1025 })
1026}
1027
1028fn current_install_deployment_truth_check_at(
1029 options: &InstallRootOptions,
1030 workspace_root: &Path,
1031 icp_root: &Path,
1032 config_path: &Path,
1033 fleet_name: &str,
1034 observed_at: String,
1035) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1036 let build_profile = options
1037 .build_profile
1038 .unwrap_or_else(CanisterBuildProfile::current)
1039 .target_dir_name()
1040 .to_string();
1041
1042 check_local_deployment(&LocalDeploymentCheckRequest {
1043 deployment_name: fleet_name.to_string(),
1044 network: options.network.clone(),
1045 workspace_root: workspace_root.to_path_buf(),
1046 icp_root: icp_root.to_path_buf(),
1047 config_path: Some(config_path.to_path_buf()),
1048 observed_at,
1049 runtime_variant: options.network.clone(),
1050 build_profile,
1051 })
1052 .map_err(Into::into)
1053}
1054
1055fn current_install_execution_context(
1056 workspace_root: &Path,
1057 icp_root: &Path,
1058 network: &str,
1059) -> DeploymentExecutionContextV1 {
1060 CurrentCliDeploymentExecutor::new(
1061 Some(workspace_root.display().to_string()),
1062 Some(icp_root.display().to_string()),
1063 current_install_artifact_roots(icp_root, network),
1064 )
1065 .execution_context()
1066}
1067
1068fn ensure_current_install_executor_capabilities(
1069 execution_context: &DeploymentExecutionContextV1,
1070) -> Result<(), Box<dyn std::error::Error>> {
1071 let missing = current_install_executor_missing_capabilities(execution_context);
1072 if missing.is_empty() {
1073 return Ok(());
1074 }
1075
1076 Err(format!(
1077 "current install executor backend {:?} is missing required capabilities: {missing:?}",
1078 execution_context.backend
1079 )
1080 .into())
1081}
1082
1083fn current_install_executor_missing_capabilities(
1084 execution_context: &DeploymentExecutionContextV1,
1085) -> Vec<DeploymentExecutorCapabilityV1> {
1086 missing_executor_capabilities(
1087 &execution_context.backend_capabilities,
1088 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1089 )
1090}
1091
1092fn current_install_artifact_roots(icp_root: &Path, network: &str) -> Vec<String> {
1093 let planned_root = planned_build_artifact_root(icp_root);
1094 let mut roots = vec![planned_root.display().to_string()];
1095 if let Ok(resolved_root) = resolve_artifact_root(icp_root, network)
1096 && resolved_root != planned_root
1097 {
1098 roots.push(resolved_root.display().to_string());
1099 }
1100 roots
1101}
1102
1103fn run_install_deployment_truth_safety_gate(
1104 options: &InstallRootOptions,
1105 workspace_root: &Path,
1106 icp_root: &Path,
1107 config_path: &Path,
1108 fleet_name: &str,
1109 execution_context: &DeploymentExecutionContextV1,
1110) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1111 let truth_gate_started_at = current_unix_timestamp_label()?;
1112 let deployment_truth_check = current_install_deployment_truth_check_at(
1113 options,
1114 workspace_root,
1115 icp_root,
1116 config_path,
1117 fleet_name,
1118 truth_gate_started_at.clone(),
1119 )?;
1120 let artifact_gate_receipt = artifact_gate_phase_receipt(
1121 &deployment_truth_check,
1122 truth_gate_started_at.clone(),
1123 Some(current_unix_timestamp_label()?),
1124 );
1125 let role_receipts = artifact_gate_role_phase_receipts(&deployment_truth_check);
1126 let deployment_receipt = receipt_with_execution_context(
1127 install_deployment_truth_gate_receipt(
1128 &deployment_truth_check,
1129 truth_gate_started_at,
1130 vec![artifact_gate_receipt],
1131 role_receipts,
1132 ),
1133 execution_context,
1134 );
1135 let receipt_write = write_install_deployment_truth_receipt(
1136 icp_root,
1137 &options.network,
1138 fleet_name,
1139 &deployment_receipt,
1140 );
1141 match &receipt_write {
1142 Ok(path) => println!("Deployment truth receipt JSON: {}", path.display()),
1143 Err(err) => eprintln!("Deployment truth receipt JSON write failed: {err}"),
1144 }
1145 print_install_deployment_truth_gate(&deployment_truth_check, &deployment_receipt);
1146 enforce_install_deployment_truth_gate(&deployment_truth_check)?;
1147 receipt_write?;
1148 write_current_install_execution_preflight_receipt(
1149 icp_root,
1150 &options.network,
1151 fleet_name,
1152 &deployment_truth_check,
1153 execution_context,
1154 )?;
1155 Ok(deployment_truth_check)
1156}
1157
1158fn enforce_install_deployment_truth_gate(
1159 check: &DeploymentCheckV1,
1160) -> Result<(), Box<dyn std::error::Error>> {
1161 let blockers = install_deployment_truth_gate_blockers(check);
1162 if blockers.is_empty() {
1163 return Ok(());
1164 }
1165
1166 let details = blockers
1167 .iter()
1168 .map(|finding| deployment_truth_finding_label(finding))
1169 .collect::<Vec<_>>()
1170 .join("; ");
1171 Err(format!("deployment truth safety gate blocked install: {details}").into())
1172}
1173
1174fn install_deployment_truth_gate_blockers(check: &DeploymentCheckV1) -> Vec<&SafetyFindingV1> {
1175 check.report.hard_failures.iter().collect()
1176}
1177
1178fn print_install_deployment_truth_gate(check: &DeploymentCheckV1, receipt: &DeploymentReceiptV1) {
1179 for line in install_deployment_truth_gate_lines(check, receipt) {
1180 println!("{line}");
1181 }
1182}
1183
1184fn install_deployment_truth_gate_lines(
1185 check: &DeploymentCheckV1,
1186 receipt: &DeploymentReceiptV1,
1187) -> Vec<String> {
1188 let mut lines = vec![
1189 format!("Deployment truth: {}", check.report.summary),
1190 format!(
1191 "Deployment truth receipt: operation={} status={:?}",
1192 receipt.operation_id, receipt.operation_status
1193 ),
1194 ];
1195 for phase_receipt in &receipt.phase_receipts {
1196 lines.push(format!(
1197 "Deployment truth phase receipt: phase={} postcondition={:?}",
1198 phase_receipt.phase, phase_receipt.verified_postcondition.status
1199 ));
1200 }
1201 if !receipt.role_phase_receipts.is_empty() {
1202 lines.push(format!(
1203 "Deployment truth role receipts: {}",
1204 receipt.role_phase_receipts.len()
1205 ));
1206 }
1207 for role_receipt in &receipt.role_phase_receipts {
1208 lines.push(format!(
1209 "Deployment truth role receipt: phase={} role={} result={:?}",
1210 role_receipt.phase, role_receipt.role, role_receipt.result
1211 ));
1212 }
1213
1214 if !check.report.hard_failures.is_empty() {
1215 lines.push(format!(
1216 "Deployment truth hard failures: {}",
1217 check.report.hard_failures.len()
1218 ));
1219 }
1220 for finding in install_deployment_truth_gate_blockers(check) {
1221 lines.push(format!(
1222 "Deployment truth blocker: {}",
1223 deployment_truth_finding_label(finding)
1224 ));
1225 }
1226 if !check.report.warnings.is_empty() {
1227 lines.push(format!(
1228 "Deployment truth warnings: {}",
1229 check.report.warnings.len()
1230 ));
1231 }
1232 for finding in &check.report.warnings {
1233 lines.push(format!(
1234 "Deployment truth warning: {}",
1235 deployment_truth_finding_label(finding)
1236 ));
1237 }
1238 lines
1239}
1240
1241fn install_deployment_truth_gate_receipt(
1242 check: &DeploymentCheckV1,
1243 started_at: String,
1244 phase_receipts: Vec<crate::deployment_truth::PhaseReceiptV1>,
1245 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
1246) -> DeploymentReceiptV1 {
1247 let blockers = install_deployment_truth_gate_blockers(check);
1248 let (operation_status, command_result) = if blockers.is_empty() {
1249 (
1250 DeploymentExecutionStatusV1::Complete,
1251 DeploymentCommandResultV1::Succeeded,
1252 )
1253 } else {
1254 (
1255 DeploymentExecutionStatusV1::FailedBeforeMutation,
1256 DeploymentCommandResultV1::Failed {
1257 code: "deployment_truth_blocked".to_string(),
1258 message: check.report.summary.clone(),
1259 },
1260 )
1261 };
1262 deployment_receipt_from_check_with_status(
1263 check,
1264 format!("{}:materialize_artifacts", check.check_id),
1265 operation_status,
1266 started_at,
1267 Some(current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string())),
1268 phase_receipts,
1269 role_phase_receipts,
1270 command_result,
1271 )
1272}
1273
1274fn write_current_install_execution_preflight_receipt(
1275 icp_root: &Path,
1276 network: &str,
1277 fleet_name: &str,
1278 check: &DeploymentCheckV1,
1279 execution_context: &DeploymentExecutionContextV1,
1280) -> Result<PathBuf, Box<dyn std::error::Error>> {
1281 let started_at = current_unix_timestamp_label()?;
1282 let executor = CurrentCliDeploymentExecutor::new(
1283 execution_context.workspace_root.clone(),
1284 execution_context.icp_root.clone(),
1285 execution_context.artifact_roots.clone(),
1286 );
1287 let preflight = deployment_execution_preflight_from_check(
1288 check,
1289 &executor,
1290 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1291 );
1292 validate_deployment_execution_preflight_for_check(check, &preflight)?;
1293 let blockers = preflight.blockers.clone();
1294 let (operation_status, command_result) = if blockers.is_empty() {
1295 (
1296 DeploymentExecutionStatusV1::Complete,
1297 DeploymentCommandResultV1::Succeeded,
1298 )
1299 } else {
1300 (
1301 DeploymentExecutionStatusV1::FailedBeforeMutation,
1302 DeploymentCommandResultV1::Failed {
1303 code: "execution_preflight_blocked".to_string(),
1304 message: "deployment execution preflight blocked current install".to_string(),
1305 },
1306 )
1307 };
1308 let finished_at = current_unix_timestamp_label()?;
1309 let receipt = receipt_with_execution_context(
1310 deployment_receipt_from_check_with_status(
1311 check,
1312 format!("{}:execution_preflight", check.check_id),
1313 operation_status,
1314 started_at.clone(),
1315 Some(finished_at.clone()),
1316 vec![phase_receipt(
1317 "execution_preflight",
1318 started_at,
1319 Some(finished_at),
1320 "validate deployment plan, authority, and executor capability readiness",
1321 crate::deployment_truth::ObservationStatusV1::Observed,
1322 current_install_execution_preflight_evidence(&preflight),
1323 )],
1324 Vec::new(),
1325 command_result,
1326 ),
1327 execution_context,
1328 );
1329 let path = write_install_deployment_truth_receipt(icp_root, network, fleet_name, &receipt)?;
1330 println!("Deployment truth receipt JSON: {}", path.display());
1331 if !blockers.is_empty() {
1332 let details = blockers
1333 .iter()
1334 .map(deployment_truth_finding_label)
1335 .collect::<Vec<_>>()
1336 .join("; ");
1337 return Err(format!("deployment execution preflight blocked install: {details}").into());
1338 }
1339 Ok(path)
1340}
1341
1342struct StageReleaseSetOperation<'a> {
1343 icp_root: &'a Path,
1344 network: &'a str,
1345 root_canister_id: &'a str,
1346 manifest_path: &'a Path,
1347 manifest: RootReleaseSetManifest,
1348}
1349
1350impl<'a> StageReleaseSetOperation<'a> {
1351 const fn new(
1352 icp_root: &'a Path,
1353 network: &'a str,
1354 root_canister_id: &'a str,
1355 manifest_path: &'a Path,
1356 manifest: RootReleaseSetManifest,
1357 ) -> Self {
1358 Self {
1359 icp_root,
1360 network,
1361 root_canister_id,
1362 manifest_path,
1363 manifest,
1364 }
1365 }
1366}
1367
1368impl InstallPhaseOperation for StageReleaseSetOperation<'_> {
1369 fn phase(&self) -> &'static str {
1370 "stage_release_set"
1371 }
1372
1373 fn attempted_action(&self) -> &'static str {
1374 "stage root release set"
1375 }
1376
1377 fn evidence(&self) -> Vec<String> {
1378 current_install_staging_evidence(self.root_canister_id, self.manifest_path, &self.manifest)
1379 }
1380
1381 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
1382 stage_root_release_set(
1383 self.icp_root,
1384 self.network,
1385 self.root_canister_id,
1386 &self.manifest,
1387 )
1388 }
1389}
1390
1391fn current_install_execution_preflight_evidence(
1392 preflight: &crate::deployment_truth::DeploymentExecutionPreflightV1,
1393) -> Vec<String> {
1394 let mut evidence = vec![
1395 format!("execution_preflight_status:{:?}", preflight.status),
1396 format!("authority_plan:{}", preflight.authority_plan_id),
1397 format!("planned_phases:{}", preflight.planned_phases.len()),
1398 format!(
1399 "required_capabilities:{}",
1400 preflight.required_capabilities.len()
1401 ),
1402 format!(
1403 "missing_capabilities:{}",
1404 preflight.missing_capabilities.len()
1405 ),
1406 format!("blockers:{}", preflight.blockers.len()),
1407 ];
1408 evidence.extend(
1409 preflight
1410 .missing_capabilities
1411 .iter()
1412 .map(|capability| format!("missing_capability:{capability:?}")),
1413 );
1414 evidence.extend(
1415 preflight
1416 .blockers
1417 .iter()
1418 .map(|finding| format!("blocker:{}:{}", finding.code, finding.message)),
1419 );
1420 evidence
1421}
1422
1423fn current_install_staging_evidence(
1424 root_canister_id: &str,
1425 manifest_path: &Path,
1426 manifest: &RootReleaseSetManifest,
1427) -> Vec<String> {
1428 let mut evidence = vec![
1429 format!("root_canister:{root_canister_id}"),
1430 format!("manifest_path:{}", manifest_path.display()),
1431 format!("release_version:{}", manifest.release_version),
1432 ];
1433 let staging_receipts = current_install_staging_receipts(root_canister_id, manifest);
1434 evidence.extend(staging_receipt_evidence(&staging_receipts));
1435 evidence
1436}
1437
1438fn current_install_staging_receipts(
1439 root_canister_id: &str,
1440 manifest: &RootReleaseSetManifest,
1441) -> Vec<StagingReceiptV1> {
1442 manifest
1443 .entries
1444 .iter()
1445 .map(|entry| StagingReceiptV1 {
1446 schema_version: crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1447 role: entry.role.clone(),
1448 artifact_identity: format!(
1449 "{}:{}:{}",
1450 entry.template_id, manifest.release_version, entry.payload_sha256_hex
1451 ),
1452 transport: ArtifactTransportV1::WasmStore,
1453 wasm_store_locator: Some(format!("root:{root_canister_id}:bootstrap")),
1454 prepared_chunk_hashes: entry.chunk_sha256_hex.clone(),
1455 published_chunk_count: entry.chunk_sha256_hex.len(),
1456 verified_postcondition: crate::deployment_truth::VerifiedPostconditionV1 {
1457 status: ObservationStatusV1::Observed,
1458 evidence: vec![
1459 format!("payload_sha256:{}", entry.payload_sha256_hex),
1460 format!("payload_size_bytes:{}", entry.payload_size_bytes),
1461 format!("chunk_size_bytes:{}", entry.chunk_size_bytes),
1462 format!("chunk_count:{}", entry.chunk_sha256_hex.len()),
1463 ],
1464 },
1465 })
1466 .collect()
1467}
1468
1469fn install_deployment_truth_phase_receipt(
1470 check: &DeploymentCheckV1,
1471 phase: &str,
1472 started_at: String,
1473 finished_at: Option<String>,
1474 attempted_action: &str,
1475 status: crate::deployment_truth::ObservationStatusV1,
1476 evidence: Vec<String>,
1477) -> DeploymentReceiptV1 {
1478 install_deployment_truth_phase_receipt_with_result(
1479 check,
1480 PhaseReceiptInput {
1481 phase,
1482 started_at,
1483 finished_at,
1484 attempted_action,
1485 status,
1486 evidence,
1487 role_phase_receipts: Vec::new(),
1488 operation_status: DeploymentExecutionStatusV1::Complete,
1489 command_result: DeploymentCommandResultV1::Succeeded,
1490 },
1491 )
1492}
1493
1494fn install_deployment_truth_phase_receipt_with_result(
1495 check: &DeploymentCheckV1,
1496 input: PhaseReceiptInput<'_>,
1497) -> DeploymentReceiptV1 {
1498 deployment_receipt_from_check_with_status(
1499 check,
1500 format!("{}:{}", check.check_id, input.phase),
1501 input.operation_status,
1502 input.started_at.clone(),
1503 input.finished_at.clone(),
1504 vec![phase_receipt(
1505 input.phase,
1506 input.started_at,
1507 input.finished_at,
1508 input.attempted_action,
1509 input.status,
1510 input.evidence,
1511 )],
1512 input.role_phase_receipts,
1513 input.command_result,
1514 )
1515}
1516
1517fn receipt_with_execution_context(
1518 mut receipt: DeploymentReceiptV1,
1519 execution_context: &DeploymentExecutionContextV1,
1520) -> DeploymentReceiptV1 {
1521 receipt.execution_context = Some(execution_context.clone());
1522 receipt
1523}
1524
1525struct PhaseReceiptInput<'a> {
1526 phase: &'a str,
1527 started_at: String,
1528 finished_at: Option<String>,
1529 attempted_action: &'a str,
1530 status: crate::deployment_truth::ObservationStatusV1,
1531 evidence: Vec<String>,
1532 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
1533 operation_status: DeploymentExecutionStatusV1,
1534 command_result: DeploymentCommandResultV1,
1535}
1536
1537fn write_install_deployment_truth_receipt(
1538 icp_root: &Path,
1539 network: &str,
1540 fleet_name: &str,
1541 receipt: &DeploymentReceiptV1,
1542) -> Result<PathBuf, Box<dyn std::error::Error>> {
1543 let path = install_deployment_truth_receipt_path(icp_root, network, fleet_name, receipt)?;
1544 if let Some(parent) = path.parent() {
1545 fs::create_dir_all(parent)?;
1546 }
1547 let mut bytes = serde_json::to_vec_pretty(receipt)?;
1548 bytes.push(b'\n');
1549 fs::write(&path, bytes)?;
1550 Ok(path)
1551}
1552
1553fn install_deployment_truth_receipt_path(
1554 icp_root: &Path,
1555 network: &str,
1556 fleet_name: &str,
1557 receipt: &DeploymentReceiptV1,
1558) -> Result<PathBuf, Box<dyn std::error::Error>> {
1559 validate_network_name(network)?;
1560 validate_fleet_name(fleet_name)?;
1561 let file_stem = format!(
1562 "{}-{}",
1563 safe_deployment_truth_path_label(&receipt.started_at),
1564 safe_deployment_truth_path_label(&receipt.operation_id)
1565 );
1566 Ok(
1567 install_deployment_truth_receipts_dir(icp_root, network, fleet_name)?
1568 .join(format!("{file_stem}.json")),
1569 )
1570}
1571
1572pub fn latest_deployment_truth_receipt_path_from_root(
1574 icp_root: &Path,
1575 network: &str,
1576 fleet_name: &str,
1577) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
1578 let dir = install_deployment_truth_receipts_dir(icp_root, network, fleet_name)?;
1579 if !dir.is_dir() {
1580 return Ok(None);
1581 }
1582
1583 let mut latest = None;
1584 for entry in fs::read_dir(dir)? {
1585 let path = entry?.path();
1586 if !path.is_file()
1587 || path
1588 .extension()
1589 .is_none_or(|ext| !ext.eq_ignore_ascii_case("json"))
1590 {
1591 continue;
1592 }
1593 if latest.as_ref().is_none_or(|current| path > *current) {
1594 latest = Some(path);
1595 }
1596 }
1597 Ok(latest)
1598}
1599
1600fn install_deployment_truth_receipts_dir(
1601 icp_root: &Path,
1602 network: &str,
1603 fleet_name: &str,
1604) -> Result<PathBuf, Box<dyn std::error::Error>> {
1605 validate_network_name(network)?;
1606 validate_fleet_name(fleet_name)?;
1607 Ok(icp_root
1608 .join(".canic")
1609 .join(network)
1610 .join("deployment-receipts")
1611 .join(fleet_name))
1612}
1613
1614fn safe_deployment_truth_path_label(value: &str) -> String {
1615 let label = value
1616 .chars()
1617 .map(|ch| {
1618 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
1619 ch
1620 } else {
1621 '_'
1622 }
1623 })
1624 .collect::<String>();
1625 if label.is_empty() {
1626 "unknown".to_string()
1627 } else {
1628 label
1629 }
1630}
1631
1632fn deployment_truth_finding_label(finding: &SafetyFindingV1) -> String {
1633 let subject = finding
1634 .subject
1635 .as_ref()
1636 .map_or_else(|| "<none>".to_string(), Clone::clone);
1637 format!(
1638 "{}:{}:{}: {}",
1639 deployment_truth_finding_source(&finding.code),
1640 finding.code,
1641 subject,
1642 finding.message
1643 )
1644}
1645
1646fn deployment_truth_finding_source(code: &str) -> &'static str {
1647 match code {
1648 "plan_assumption" => "plan",
1649 "observation_gap" => "inventory",
1650 _ => "diff",
1651 }
1652}
1653
1654fn validate_expected_fleet_name(
1655 expected: Option<&str>,
1656 actual: &str,
1657 config_path: &Path,
1658) -> Result<(), Box<dyn std::error::Error>> {
1659 let Some(expected) = expected else {
1660 return Ok(());
1661 };
1662 if expected == actual {
1663 return Ok(());
1664 }
1665 Err(format!(
1666 "install requested fleet {expected}, but {} declares [fleet].name = {actual:?}",
1667 config_path.display()
1668 )
1669 .into())
1670}
1671
1672fn ensure_root_canister_id(
1673 icp_root: &Path,
1674 network: &str,
1675 root_canister: &str,
1676 config_path: &Path,
1677) -> Result<String, Box<dyn std::error::Error>> {
1678 if Principal::from_text(root_canister).is_ok() {
1679 return Ok(root_canister.to_string());
1680 }
1681
1682 match resolve_root_canister_id(icp_root, network, root_canister) {
1683 Ok(canister_id) => return Ok(canister_id),
1684 Err(err) if !is_missing_canister_id_error(&err.to_string()) => return Err(err),
1685 Err(_) => {}
1686 }
1687
1688 let mut create = icp_canister_command_in_network(icp_root);
1689 add_create_root_target(&mut create, root_canister);
1690 add_local_root_create_cycles_arg(&mut create, config_path, network)?;
1691 add_icp_environment_target(&mut create, network);
1692 let output = run_command_stdout(&mut create)?;
1693 if let Some(canister_id) = parse_created_canister_id(&output) {
1694 return Ok(canister_id);
1695 }
1696
1697 resolve_root_canister_id(icp_root, network, root_canister).map_err(|_| {
1698 format!(
1699 "created root canister target '{root_canister}', but ICP CLI still has no canister ID for environment '{network}' under ICP root {}\nExpected project-local state under {}/.icp/{network}. If another foreground replica is reachable, stop it and restart with `canic replica start --background` from this Canic project.",
1700 icp_root.display(),
1701 icp_root.display(),
1702 )
1703 .into()
1704 })
1705}
1706
1707fn parse_created_canister_id(output: &str) -> Option<String> {
1708 if let Ok(value) = serde_json::from_str::<JsonValue>(output) {
1709 return parse_canister_id_json(&value);
1710 }
1711
1712 output
1713 .lines()
1714 .map(str::trim)
1715 .find(|line| Principal::from_text(*line).is_ok())
1716 .map(ToString::to_string)
1717}
1718
1719fn parse_canister_id_json(value: &JsonValue) -> Option<String> {
1720 match value {
1721 JsonValue::String(text) if Principal::from_text(text).is_ok() => Some(text.clone()),
1722 JsonValue::Array(values) => values.iter().find_map(parse_canister_id_json),
1723 JsonValue::Object(object) => ["canister_id", "id", "principal"]
1724 .iter()
1725 .filter_map(|key| object.get(*key))
1726 .find_map(parse_canister_id_json),
1727 _ => None,
1728 }
1729}
1730
1731fn add_create_root_target(command: &mut Command, root_canister: &str) {
1732 if env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV).is_some() {
1733 command.args(["create", "--detached", "--json"]);
1734 } else {
1735 command.args(["create", root_canister, "--json"]);
1736 }
1737}
1738
1739fn is_missing_canister_id_error(message: &str) -> bool {
1740 message.contains("failed to lookup canister ID")
1741 || message.contains("could not find ID for canister")
1742 || message.contains("Canister ID is missing")
1743}
1744
1745fn reinstall_root_wasm(
1746 icp_root: &Path,
1747 network: &str,
1748 root_canister: &str,
1749 root_wasm: &Path,
1750) -> Result<(), Box<dyn std::error::Error>> {
1751 let mut install = icp_canister_command_in_network(icp_root);
1752 install.args(["install", root_canister, "--mode=reinstall", "-y", "--wasm"]);
1753 install.arg(root_wasm);
1754 install.args(["--args", &root_init_args(root_wasm)?]);
1755 add_icp_environment_target(&mut install, network);
1756 run_command(&mut install)
1757}
1758
1759fn root_init_args(root_wasm: &Path) -> Result<String, Box<dyn std::error::Error>> {
1760 let wasm = std::fs::read(root_wasm)?;
1761 Ok(format!(
1762 "(variant {{ PrimeWithModuleHash = {} }})",
1763 idl_blob(&wasm_hash(&wasm))
1764 ))
1765}
1766
1767fn idl_blob(bytes: &[u8]) -> String {
1768 let mut encoded = String::from("blob \"");
1769 for byte in bytes {
1770 use std::fmt::Write as _;
1771 let _ = write!(encoded, "\\{byte:02X}");
1772 }
1773 encoded.push('"');
1774 encoded
1775}
1776
1777fn build_install_state(
1779 options: &InstallRootOptions,
1780 workspace_root: &Path,
1781 icp_root: &Path,
1782 config_path: &Path,
1783 release_set_manifest_path: &Path,
1784 fleet_name: &str,
1785 root_canister_id: &str,
1786) -> Result<InstallState, Box<dyn std::error::Error>> {
1787 Ok(InstallState {
1788 schema_version: INSTALL_STATE_SCHEMA_VERSION,
1789 fleet: fleet_name.to_string(),
1790 installed_at_unix_secs: current_unix_secs()?,
1791 network: options.network.clone(),
1792 root_target: options.root_canister.clone(),
1793 root_canister_id: root_canister_id.to_string(),
1794 root_build_target: options.root_build_target.clone(),
1795 workspace_root: workspace_root.display().to_string(),
1796 icp_root: icp_root.display().to_string(),
1797 config_path: config_path.display().to_string(),
1798 release_set_manifest_path: release_set_manifest_path.display().to_string(),
1799 })
1800}
1801
1802fn resolve_root_canister_id(
1804 icp_root: &Path,
1805 network: &str,
1806 root_canister: &str,
1807) -> Result<String, Box<dyn std::error::Error>> {
1808 if Principal::from_text(root_canister).is_ok() {
1809 return Ok(root_canister.to_string());
1810 }
1811
1812 let mut command = icp_canister_command_in_network(icp_root);
1813 command.args(["status", root_canister, "--json"]);
1814 add_icp_environment_target(&mut command, network);
1815 let output = run_command_stdout(&mut command)?;
1816 parse_created_canister_id(&output).ok_or_else(|| {
1817 format!("could not parse root canister id from ICP status JSON output: {output}").into()
1818 })
1819}
1820
1821fn current_unix_secs() -> Result<u64, Box<dyn std::error::Error>> {
1823 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
1824}
1825
1826fn current_unix_timestamp_label() -> Result<String, Box<dyn std::error::Error>> {
1827 Ok(format!("unix:{}", current_unix_secs()?))
1828}
1829
1830fn run_canic_build_targets(
1832 network: &str,
1833 targets: &[String],
1834 build_profile: Option<CanisterBuildProfile>,
1835 config_path: &Path,
1836 icp_root: &Path,
1837) -> Result<(), Box<dyn std::error::Error>> {
1838 let _env = BuildEnvGuard::apply(network, config_path, icp_root);
1839 let profile = build_profile.unwrap_or_else(CanisterBuildProfile::current);
1840 if let Some(context) = current_workspace_build_context_once(profile)? {
1841 for line in context.lines() {
1842 println!("{line}");
1843 }
1844 println!("config: {}", config_path.display());
1845 println!(
1846 "artifacts: {}",
1847 planned_build_artifact_root(icp_root).display()
1848 );
1849 println!();
1850 }
1851
1852 fs::create_dir_all(planned_build_artifact_root(icp_root))?;
1853 println!("Building {} canisters", targets.len());
1854 println!();
1855 let headers = ["CANISTER", "PROGRESS", "WASM", "ELAPSED"];
1856 let planned_rows = targets
1857 .iter()
1858 .map(|target| {
1859 [
1860 target.clone(),
1861 progress_bar(targets.len(), targets.len(), 10),
1862 "000.00 MiB (gz 000.00 MiB)".to_string(),
1863 "0.00s".to_string(),
1864 ]
1865 })
1866 .collect::<Vec<_>>();
1867 let alignments = [
1868 ColumnAlign::Left,
1869 ColumnAlign::Left,
1870 ColumnAlign::Right,
1871 ColumnAlign::Right,
1872 ];
1873 let widths = table_widths(&headers, &planned_rows);
1874 println!("{}", render_table_row(&headers, &widths, &alignments));
1875 println!("{}", render_separator(&widths));
1876
1877 for (index, target) in targets.iter().enumerate() {
1878 let started_at = Instant::now();
1879 let output = build_current_workspace_canister_artifact(target, profile)
1880 .map_err(|err| format!("artifact build failed for {target}: {err}"))?;
1881 let elapsed = started_at.elapsed();
1882 let artifact_size = wasm_artifact_size(&output.wasm_path, &output.wasm_gz_path)?;
1883
1884 let row = [
1885 target.clone(),
1886 progress_bar(index + 1, targets.len(), 10),
1887 artifact_size,
1888 format!("{:.2}s", elapsed.as_secs_f64()),
1889 ];
1890 println!("{}", render_table_row(&row, &widths, &alignments));
1891 }
1892
1893 println!();
1894 Ok(())
1895}
1896
1897fn planned_build_artifact_root(icp_root: &Path) -> PathBuf {
1898 icp_root.join(".icp/local/canisters")
1899}
1900
1901fn wasm_artifact_size(
1902 wasm_path: &Path,
1903 wasm_gz_path: &Path,
1904) -> Result<String, Box<dyn std::error::Error>> {
1905 let wasm_bytes = Some(std::fs::metadata(wasm_path)?.len());
1906 let gzip_bytes = std::fs::metadata(wasm_gz_path)
1907 .ok()
1908 .map(|metadata| metadata.len());
1909 Ok(wasm_size_label(wasm_bytes, gzip_bytes))
1910}
1911
1912struct BuildEnvGuard {
1913 previous_network: Option<OsString>,
1914 previous_config_path: Option<OsString>,
1915 previous_icp_root: Option<OsString>,
1916 previous_local_network_url: Option<OsString>,
1917 previous_local_root_key: Option<OsString>,
1918}
1919
1920impl BuildEnvGuard {
1921 fn apply(network: &str, config_path: &Path, icp_root: &Path) -> Self {
1922 let guard = Self {
1923 previous_network: env::var_os("ICP_ENVIRONMENT"),
1924 previous_config_path: env::var_os("CANIC_CONFIG_PATH"),
1925 previous_icp_root: env::var_os("CANIC_ICP_ROOT"),
1926 previous_local_network_url: env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV),
1927 previous_local_root_key: env::var_os(CANIC_ICP_LOCAL_ROOT_KEY_ENV),
1928 };
1929 set_env("ICP_ENVIRONMENT", network);
1930 set_env("CANIC_CONFIG_PATH", config_path);
1931 set_env("CANIC_ICP_ROOT", icp_root);
1932 if let Some(target) = local_replica_icp_target(network, icp_root) {
1933 set_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV, target.url);
1934 set_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV, target.root_key);
1935 } else {
1936 remove_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV);
1937 remove_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV);
1938 }
1939 guard
1940 }
1941}
1942
1943impl Drop for BuildEnvGuard {
1944 fn drop(&mut self) {
1945 restore_env("ICP_ENVIRONMENT", self.previous_network.take());
1946 restore_env("CANIC_CONFIG_PATH", self.previous_config_path.take());
1947 restore_env("CANIC_ICP_ROOT", self.previous_icp_root.take());
1948 restore_env(
1949 CANIC_ICP_LOCAL_NETWORK_URL_ENV,
1950 self.previous_local_network_url.take(),
1951 );
1952 restore_env(
1953 CANIC_ICP_LOCAL_ROOT_KEY_ENV,
1954 self.previous_local_root_key.take(),
1955 );
1956 }
1957}
1958
1959struct LocalReplicaIcpTarget {
1960 url: String,
1961 root_key: String,
1962}
1963
1964fn local_replica_icp_target(network: &str, icp_root: &Path) -> Option<LocalReplicaIcpTarget> {
1965 if !replica_query::should_use_local_replica_query(Some(network)) {
1966 return None;
1967 }
1968 if icp_ping(icp_root, network).unwrap_or(false) {
1969 return None;
1970 }
1971 let root_key = replica_query::local_replica_root_key_from_root(Some(network), icp_root)
1972 .ok()
1973 .flatten()?;
1974 Some(LocalReplicaIcpTarget {
1975 url: replica_query::local_replica_endpoint_from_root(Some(network), icp_root),
1976 root_key,
1977 })
1978}
1979
1980fn set_env<K, V>(key: K, value: V)
1981where
1982 K: AsRef<std::ffi::OsStr>,
1983 V: AsRef<std::ffi::OsStr>,
1984{
1985 unsafe {
1988 env::set_var(key, value);
1989 }
1990}
1991
1992fn remove_env<K>(key: K)
1993where
1994 K: AsRef<std::ffi::OsStr>,
1995{
1996 unsafe {
1999 env::remove_var(key);
2000 }
2001}
2002
2003fn restore_env(key: &str, value: Option<OsString>) {
2004 if let Some(value) = value {
2006 set_env(key, value);
2007 } else {
2008 remove_env(key);
2009 }
2010}
2011
2012fn add_local_root_create_cycles_arg(
2013 command: &mut Command,
2014 config_path: &Path,
2015 network: &str,
2016) -> Result<(), Box<dyn std::error::Error>> {
2017 if network != "local" {
2018 return Ok(());
2019 }
2020
2021 let cycles = configured_local_root_create_cycles(config_path)?;
2022 command.args(["--cycles", &cycles.to_string()]);
2023 Ok(())
2024}
2025
2026fn ensure_local_root_min_cycles(
2027 icp_root: &Path,
2028 network: &str,
2029 root_canister: &str,
2030 phase: &str,
2031) -> Result<(), Box<dyn std::error::Error>> {
2032 if network != "local" {
2033 return Ok(());
2034 }
2035
2036 let current = query_root_cycle_balance(network, root_canister)?;
2037 if current >= LOCAL_ROOT_MIN_READY_CYCLES {
2038 return Ok(());
2039 }
2040
2041 let amount = LOCAL_ROOT_MIN_READY_CYCLES.saturating_sub(current);
2042 let mut command = icp_canister_command_in_network(icp_root);
2043 command
2044 .args(["top-up", "--amount"])
2045 .arg(amount.to_string())
2046 .arg(root_canister);
2047 add_icp_environment_target(&mut command, network);
2048 run_command(&mut command)?;
2049 println!(
2050 "Local root cycles ({phase}): topped up {} ({} -> {} target)",
2051 crate::format::cycles_tc(amount),
2052 crate::format::cycles_tc(current),
2053 crate::format::cycles_tc(LOCAL_ROOT_MIN_READY_CYCLES)
2054 );
2055 Ok(())
2056}
2057
2058fn query_root_cycle_balance(
2059 network: &str,
2060 root_canister: &str,
2061) -> Result<u128, Box<dyn std::error::Error>> {
2062 let output = icp_query_on_network(
2063 network,
2064 root_canister,
2065 protocol::CANIC_CYCLE_BALANCE,
2066 None,
2067 Some("json"),
2068 )?;
2069 parse_cycle_balance_response(&output).ok_or_else(|| {
2070 format!(
2071 "could not parse {root_canister} {} response: {output}",
2072 protocol::CANIC_CYCLE_BALANCE
2073 )
2074 .into()
2075 })
2076}
2077
2078fn progress_bar(current: usize, total: usize, width: usize) -> String {
2079 if total == 0 || width == 0 {
2080 return "[] 0/0".to_string();
2081 }
2082
2083 let filled = current.saturating_mul(width).div_ceil(total);
2084 let filled = filled.min(width);
2085 format!(
2086 "[{}{}] {current}/{total}",
2087 "#".repeat(filled),
2088 " ".repeat(width - filled)
2089 )
2090}
2091
2092fn ensure_icp_environment_ready(
2094 icp_root: &Path,
2095 network: &str,
2096) -> Result<(), Box<dyn std::error::Error>> {
2097 if icp_ping(icp_root, network)? {
2098 return Ok(());
2099 }
2100 if replica_query::should_use_local_replica_query(Some(network))
2101 && replica_query::local_replica_status_reachable_from_root(Some(network), icp_root)
2102 {
2103 println!(
2104 "Replica reachable via HTTP status endpoint even though ICP CLI reports network '{network}' stopped; continuing from ICP root {}.",
2105 icp_root.display()
2106 );
2107 return Ok(());
2108 }
2109
2110 Err(format!(
2111 "icp environment is not running for network '{network}'\nStart the target replica in another terminal with `canic replica start` and rerun."
2112 )
2113 .into())
2114}
2115
2116fn icp_ping(icp_root: &Path, network: &str) -> Result<bool, Box<dyn std::error::Error>> {
2118 Ok(icp::default_command_in(icp_root)
2119 .args(["network", "ping", network])
2120 .output()?
2121 .status
2122 .success())
2123}
2124
2125fn print_install_timing_summary(timings: &InstallTimingSummary, total: Duration) {
2126 println!("Install timing summary:");
2127 println!("{}", render_install_timing_summary(timings, total));
2128}
2129
2130fn render_install_timing_summary(timings: &InstallTimingSummary, total: Duration) -> String {
2131 let rows = [
2132 timing_row("create_canisters", timings.create_canisters),
2133 timing_row("build_all", timings.build_all),
2134 timing_row("emit_manifest", timings.emit_manifest),
2135 timing_row("install_root", timings.install_root),
2136 timing_row("fund_root", timings.fund_root),
2137 timing_row("stage_release_set", timings.stage_release_set),
2138 timing_row("resume_bootstrap", timings.resume_bootstrap),
2139 timing_row("wait_ready", timings.wait_ready),
2140 timing_row("finalize_root_funding", timings.finalize_root_funding),
2141 timing_row("total", total),
2142 ];
2143 render_table(
2144 &["PHASE", "ELAPSED"],
2145 &rows,
2146 &[ColumnAlign::Left, ColumnAlign::Right],
2147 )
2148}
2149
2150fn timing_row(label: &str, duration: Duration) -> [String; 2] {
2151 [label.to_string(), format!("{:.2}s", duration.as_secs_f64())]
2152}
2153
2154fn print_install_result_summary(network: &str, fleet: &str, state_path: &Path) {
2156 println!("Install result:");
2157 println!("{:<14} success", "status");
2158 println!("{:<14} {}", "fleet", fleet);
2159 println!("{:<14} {}", "install_state", state_path.display());
2160 println!(
2161 "{:<14} canic list {} --network {}",
2162 "smoke_check", fleet, network
2163 );
2164}
2165
2166fn run_command(command: &mut Command) -> Result<(), Box<dyn std::error::Error>> {
2168 icp::run_status(command).map_err(Into::into)
2169}
2170
2171fn run_command_stdout(command: &mut Command) -> Result<String, Box<dyn std::error::Error>> {
2173 icp::run_output(command).map_err(Into::into)
2174}
2175
2176fn icp_command_on_network(network: &str) -> Command {
2179 let mut command = icp::default_command();
2180 command.env("ICP_ENVIRONMENT", network);
2181 command
2182}
2183
2184fn icp_command_in_network(icp_root: &Path, network: &str) -> Command {
2186 let mut command = icp::default_command_in(icp_root);
2187 command.env("ICP_ENVIRONMENT", network);
2188 command
2189}
2190
2191fn icp_canister_command_in_network(icp_root: &Path) -> Command {
2193 let mut command = icp::default_command_in(icp_root);
2194 command.arg("canister");
2195 command
2196}
2197
2198fn add_icp_environment_target(command: &mut Command, network: &str) {
2199 icp::add_target_args(command, Some(network), None);
2200}