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 started_at = current_unix_timestamp_label()?;
279 let started = Instant::now();
280 let root_canister_id = ensure_root_canister_id(
281 icp_root,
282 &options.network,
283 &options.root_canister,
284 config_path,
285 )?;
286 let duration = started.elapsed();
287 let phase = CompletedInstallPhase {
288 phase: "resolve_root_canister",
289 attempted_action: "resolve or create root canister id",
290 started_at,
291 finished_at: Some(current_unix_timestamp_label()?),
292 evidence: vec![
293 format!("root_target:{}", options.root_canister),
294 format!("root_canister:{root_canister_id}"),
295 ],
296 role_names: Vec::new(),
297 };
298 Ok((root_canister_id, phase, duration))
299}
300
301fn build_install_targets_with_phase(
302 options: &InstallRootOptions,
303 icp_root: &Path,
304 config_path: &Path,
305) -> Result<(CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
306 let build_targets = configured_install_targets(config_path, &options.root_build_target)?;
307 let started_at = current_unix_timestamp_label()?;
308 let started = Instant::now();
309 run_canic_build_targets(
310 &options.network,
311 &build_targets,
312 options.build_profile,
313 config_path,
314 icp_root,
315 )?;
316 let duration = started.elapsed();
317 let phase = CompletedInstallPhase {
318 phase: "build_artifacts",
319 attempted_action: "build configured install targets",
320 started_at,
321 finished_at: Some(current_unix_timestamp_label()?),
322 evidence: build_targets
323 .iter()
324 .map(|target| format!("build_target:{target}"))
325 .collect(),
326 role_names: build_targets,
327 };
328 Ok((phase, duration))
329}
330
331fn emit_manifest_with_deployment_truth_receipt(
332 workspace_root: &Path,
333 icp_root: &Path,
334 options: &InstallRootOptions,
335 config_path: &Path,
336 fleet_name: &str,
337 deployment_truth_check: &DeploymentCheckV1,
338 execution_context: &DeploymentExecutionContextV1,
339) -> Result<(PathBuf, Duration), Box<dyn std::error::Error>> {
340 let emit_manifest_started_at_label = current_unix_timestamp_label()?;
341 let emit_manifest_started_at = Instant::now();
342 let manifest_path = emit_root_release_set_manifest_with_config(
343 workspace_root,
344 icp_root,
345 &options.network,
346 config_path,
347 )?;
348 let emit_manifest_duration = emit_manifest_started_at.elapsed();
349 let emit_manifest_receipt = receipt_with_execution_context(
350 install_deployment_truth_phase_receipt(
351 deployment_truth_check,
352 "emit_manifest",
353 emit_manifest_started_at_label,
354 Some(current_unix_timestamp_label()?),
355 "emit root release-set manifest",
356 crate::deployment_truth::ObservationStatusV1::Observed,
357 vec![format!("manifest_path:{}", manifest_path.display())],
358 ),
359 execution_context,
360 );
361 let emit_manifest_receipt_path = write_install_deployment_truth_receipt(
362 icp_root,
363 &options.network,
364 fleet_name,
365 &emit_manifest_receipt,
366 )?;
367 println!(
368 "Deployment truth receipt JSON: {}",
369 emit_manifest_receipt_path.display()
370 );
371 Ok((manifest_path, emit_manifest_duration))
372}
373
374fn run_root_activation_phases(
375 receipt_scope: InstallReceiptScope<'_>,
376 options: &InstallRootOptions,
377 root_canister_id: &str,
378 manifest_path: &Path,
379 total_started_at: Instant,
380) -> Result<InstallTimingSummary, Box<dyn std::error::Error>> {
381 let mut timings = InstallTimingSummary::default();
382 let root_wasm = resolve_artifact_root(receipt_scope.icp_root, receipt_scope.network)?
383 .join(&options.root_build_target)
384 .join(format!("{}.wasm", options.root_build_target));
385 timings.install_root = receipt_scope.run_phase(
386 "install_root",
387 "install root wasm",
388 vec![
389 format!("root_canister:{root_canister_id}"),
390 format!("root_wasm:{}", root_wasm.display()),
391 ],
392 || {
393 reinstall_root_wasm(
394 receipt_scope.icp_root,
395 receipt_scope.network,
396 root_canister_id,
397 &root_wasm,
398 )
399 },
400 )?;
401 timings.fund_root = receipt_scope.run_phase(
402 "fund_root_pre_bootstrap",
403 "ensure local root minimum cycles before bootstrap",
404 vec![
405 format!("root_canister:{root_canister_id}"),
406 format!("minimum_cycles:{LOCAL_ROOT_MIN_READY_CYCLES}"),
407 ],
408 || {
409 ensure_local_root_min_cycles(
410 receipt_scope.icp_root,
411 receipt_scope.network,
412 root_canister_id,
413 "pre-bootstrap",
414 )
415 },
416 )?;
417 let manifest = load_root_release_set_manifest(manifest_path)?;
418 timings.stage_release_set = receipt_scope.run_phase(
419 "stage_release_set",
420 "stage root release set",
421 current_install_staging_evidence(root_canister_id, manifest_path, &manifest),
422 || {
423 stage_root_release_set(
424 receipt_scope.icp_root,
425 receipt_scope.network,
426 root_canister_id,
427 &manifest,
428 )
429 },
430 )?;
431 timings.resume_bootstrap = receipt_scope.run_phase(
432 "resume_bootstrap",
433 "resume root bootstrap",
434 vec![format!("root_canister:{root_canister_id}")],
435 || resume_root_bootstrap(receipt_scope.network, root_canister_id),
436 )?;
437 let wait_ready_result = receipt_scope.run_phase(
438 "wait_ready",
439 "wait for root bootstrap readiness",
440 vec![
441 format!("root_canister:{root_canister_id}"),
442 format!("timeout_seconds:{}", options.ready_timeout_seconds),
443 ],
444 || {
445 wait_for_root_ready(
446 receipt_scope.network,
447 root_canister_id,
448 options.ready_timeout_seconds,
449 )
450 },
451 );
452 match wait_ready_result {
453 Ok(duration) => timings.wait_ready = duration,
454 Err(err) => {
455 print_install_timing_summary(&timings, total_started_at.elapsed());
456 return Err(err);
457 }
458 }
459 timings.finalize_root_funding = receipt_scope.run_phase(
460 "fund_root_post_ready",
461 "ensure local root minimum cycles after ready",
462 vec![
463 format!("root_canister:{root_canister_id}"),
464 format!("minimum_cycles:{LOCAL_ROOT_MIN_READY_CYCLES}"),
465 ],
466 || {
467 ensure_local_root_min_cycles(
468 receipt_scope.icp_root,
469 receipt_scope.network,
470 root_canister_id,
471 "post-ready",
472 )
473 },
474 )?;
475 Ok(timings)
476}
477
478#[derive(Clone, Copy)]
479struct InstallReceiptScope<'a> {
480 icp_root: &'a Path,
481 network: &'a str,
482 fleet_name: &'a str,
483 check: &'a DeploymentCheckV1,
484 execution_context: Option<&'a DeploymentExecutionContextV1>,
485}
486
487struct CompletedInstallPhase {
488 phase: &'static str,
489 attempted_action: &'static str,
490 started_at: String,
491 finished_at: Option<String>,
492 evidence: Vec<String>,
493 role_names: Vec<String>,
494}
495
496fn write_completed_install_phase_receipt(
497 receipt_scope: InstallReceiptScope<'_>,
498 completed: CompletedInstallPhase,
499) -> Result<PathBuf, Box<dyn std::error::Error>> {
500 let role_phase_receipts = completed
501 .role_names
502 .iter()
503 .filter_map(|role| {
504 completed_phase_role_receipt(
505 receipt_scope.check,
506 completed.phase,
507 role,
508 crate::deployment_truth::RolePhaseResultV1::Applied,
509 None,
510 )
511 })
512 .collect();
513 let receipt =
514 receipt_scope.with_execution_context(install_deployment_truth_phase_receipt_with_result(
515 receipt_scope.check,
516 PhaseReceiptInput {
517 phase: completed.phase,
518 started_at: completed.started_at,
519 finished_at: completed.finished_at,
520 attempted_action: completed.attempted_action,
521 status: crate::deployment_truth::ObservationStatusV1::Observed,
522 evidence: completed.evidence,
523 role_phase_receipts,
524 operation_status: DeploymentExecutionStatusV1::Complete,
525 command_result: DeploymentCommandResultV1::Succeeded,
526 },
527 ));
528 receipt_scope.write_receipt(&receipt)
529}
530
531fn completed_phase_role_receipt(
532 check: &DeploymentCheckV1,
533 phase: &str,
534 role: &str,
535 result: crate::deployment_truth::RolePhaseResultV1,
536 error: Option<String>,
537) -> Option<crate::deployment_truth::RolePhaseReceiptV1> {
538 let planned = check
539 .plan
540 .role_artifacts
541 .iter()
542 .find(|artifact| artifact.role == role)?;
543 let observed = check
544 .inventory
545 .observed_artifacts
546 .iter()
547 .find(|artifact| artifact.role == role);
548 let artifact_digest = observed
549 .and_then(|artifact| artifact.file_sha256.clone())
550 .or_else(|| observed.and_then(|artifact| artifact.payload_sha256.clone()))
551 .or_else(|| planned.observed_wasm_gz_file_sha256.clone())
552 .or_else(|| planned.wasm_gz_sha256.clone());
553
554 Some(crate::deployment_truth::RolePhaseReceiptV1 {
555 role: role.to_string(),
556 phase: phase.to_string(),
557 result,
558 previous_module_hash: None,
559 target_module_hash: planned.installed_module_hash.clone(),
560 observed_module_hash_after: None,
561 artifact_digest,
562 canonical_embedded_config_sha256: planned.canonical_embedded_config_sha256.clone(),
563 error,
564 })
565}
566
567fn write_install_state_with_deployment_truth_receipt(
568 receipt_scope: InstallReceiptScope<'_>,
569 network: &str,
570 state: &InstallState,
571) -> Result<PathBuf, Box<dyn std::error::Error>> {
572 let started_at = current_unix_timestamp_label()?;
573 let state_path = write_install_state(receipt_scope.icp_root, network, state)?;
574 let completed = CompletedInstallPhase {
575 phase: "write_install_state",
576 attempted_action: "write local install state",
577 started_at,
578 finished_at: Some(current_unix_timestamp_label()?),
579 evidence: vec![
580 format!("install_state:{}", state_path.display()),
581 format!("fleet:{}", state.fleet),
582 format!("root_canister:{}", state.root_canister_id),
583 ],
584 role_names: Vec::new(),
585 };
586 write_completed_install_phase_receipt(receipt_scope, completed)?;
587 Ok(state_path)
588}
589
590impl InstallReceiptScope<'_> {
591 fn run_phase(
592 self,
593 phase: &str,
594 attempted_action: &str,
595 evidence: Vec<String>,
596 run: impl FnOnce() -> Result<(), Box<dyn std::error::Error>>,
597 ) -> Result<Duration, Box<dyn std::error::Error>> {
598 let started_at = current_unix_timestamp_label()?;
599 let started = Instant::now();
600 match run() {
601 Ok(()) => {
602 let duration = started.elapsed();
603 let receipt = self.with_execution_context(install_deployment_truth_phase_receipt(
604 self.check,
605 phase,
606 started_at,
607 Some(current_unix_timestamp_label()?),
608 attempted_action,
609 crate::deployment_truth::ObservationStatusV1::Observed,
610 evidence,
611 ));
612 self.write_receipt(&receipt)?;
613 Ok(duration)
614 }
615 Err(err) => {
616 self.try_write_failed_phase_receipt(
617 phase,
618 started_at,
619 attempted_action,
620 evidence,
621 err.as_ref(),
622 );
623 Err(err)
624 }
625 }
626 }
627
628 fn with_execution_context(self, receipt: DeploymentReceiptV1) -> DeploymentReceiptV1 {
629 match self.execution_context {
630 Some(context) => receipt_with_execution_context(receipt, context),
631 None => receipt,
632 }
633 }
634
635 fn write_receipt(
636 self,
637 receipt: &DeploymentReceiptV1,
638 ) -> Result<PathBuf, Box<dyn std::error::Error>> {
639 let path = write_install_deployment_truth_receipt(
640 self.icp_root,
641 self.network,
642 self.fleet_name,
643 receipt,
644 )?;
645 println!("Deployment truth receipt JSON: {}", path.display());
646 Ok(path)
647 }
648
649 fn try_write_failed_phase_receipt(
650 self,
651 phase: &str,
652 started_at: String,
653 attempted_action: &str,
654 evidence: Vec<String>,
655 err: &dyn std::error::Error,
656 ) {
657 let receipt = install_deployment_truth_phase_receipt_with_result(
658 self.check,
659 PhaseReceiptInput {
660 phase,
661 started_at,
662 finished_at: Some(
663 current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string()),
664 ),
665 attempted_action,
666 status: crate::deployment_truth::ObservationStatusV1::Inconclusive,
667 evidence,
668 role_phase_receipts: Vec::new(),
669 operation_status: DeploymentExecutionStatusV1::FailedAfterMutation,
670 command_result: DeploymentCommandResultV1::Failed {
671 code: format!("{phase}_failed"),
672 message: err.to_string(),
673 },
674 },
675 );
676 let receipt = self.with_execution_context(receipt);
677 if let Err(write_err) = self.write_receipt(&receipt) {
678 eprintln!("Deployment truth receipt JSON write failed: {write_err}");
679 }
680 }
681}
682
683pub fn check_install_deployment_truth(
686 options: &InstallRootOptions,
687 observed_at: impl Into<String>,
688) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
689 let inputs = resolve_current_install_truth_inputs(options)?;
690 current_install_deployment_truth_check_at(
691 options,
692 &inputs.workspace_root,
693 &inputs.icp_root,
694 &inputs.config_path,
695 &inputs.fleet_name,
696 observed_at.into(),
697 )
698}
699
700pub fn check_install_execution_preflight(
706 options: &InstallRootOptions,
707 observed_at: impl Into<String>,
708) -> Result<DeploymentExecutionPreflightV1, Box<dyn std::error::Error>> {
709 let inputs = resolve_current_install_truth_inputs(options)?;
710 let check = current_install_deployment_truth_check_at(
711 options,
712 &inputs.workspace_root,
713 &inputs.icp_root,
714 &inputs.config_path,
715 &inputs.fleet_name,
716 observed_at.into(),
717 )?;
718 let execution_context = current_install_execution_context(
719 &inputs.workspace_root,
720 &inputs.icp_root,
721 &options.network,
722 );
723 let executor = CurrentCliDeploymentExecutor::new(
724 execution_context.workspace_root,
725 execution_context.icp_root,
726 execution_context.artifact_roots,
727 );
728 let preflight = deployment_execution_preflight_from_check(
729 &check,
730 &executor,
731 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
732 );
733 validate_deployment_execution_preflight_for_check(&check, &preflight)?;
734 Ok(preflight)
735}
736
737fn resolve_current_install_truth_inputs(
738 options: &InstallRootOptions,
739) -> Result<CurrentInstallTruthInputs, Box<dyn std::error::Error>> {
740 let workspace_root = workspace_root()?;
741 let icp_root = match &options.icp_root {
742 Some(path) => path.canonicalize()?,
743 None => icp_root()?,
744 };
745 let config_path = resolve_install_config_path(
746 &icp_root,
747 options.config_path.as_deref(),
748 options.interactive_config_selection,
749 )?;
750 let fleet_name = configured_fleet_name(&config_path)?;
751 validate_expected_fleet_name(options.expected_fleet.as_deref(), &fleet_name, &config_path)?;
752 validate_fleet_name(&fleet_name)?;
753 Ok(CurrentInstallTruthInputs {
754 workspace_root,
755 icp_root,
756 config_path,
757 fleet_name,
758 })
759}
760
761fn current_install_deployment_truth_check_at(
762 options: &InstallRootOptions,
763 workspace_root: &Path,
764 icp_root: &Path,
765 config_path: &Path,
766 fleet_name: &str,
767 observed_at: String,
768) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
769 let build_profile = options
770 .build_profile
771 .unwrap_or_else(CanisterBuildProfile::current)
772 .target_dir_name()
773 .to_string();
774
775 check_local_deployment(&LocalDeploymentCheckRequest {
776 deployment_name: fleet_name.to_string(),
777 network: options.network.clone(),
778 workspace_root: workspace_root.to_path_buf(),
779 icp_root: icp_root.to_path_buf(),
780 config_path: Some(config_path.to_path_buf()),
781 observed_at,
782 runtime_variant: options.network.clone(),
783 build_profile,
784 })
785 .map_err(Into::into)
786}
787
788fn current_install_execution_context(
789 workspace_root: &Path,
790 icp_root: &Path,
791 network: &str,
792) -> DeploymentExecutionContextV1 {
793 CurrentCliDeploymentExecutor::new(
794 Some(workspace_root.display().to_string()),
795 Some(icp_root.display().to_string()),
796 current_install_artifact_roots(icp_root, network),
797 )
798 .execution_context()
799}
800
801fn ensure_current_install_executor_capabilities(
802 execution_context: &DeploymentExecutionContextV1,
803) -> Result<(), Box<dyn std::error::Error>> {
804 let missing = current_install_executor_missing_capabilities(execution_context);
805 if missing.is_empty() {
806 return Ok(());
807 }
808
809 Err(format!(
810 "current install executor backend {:?} is missing required capabilities: {missing:?}",
811 execution_context.backend
812 )
813 .into())
814}
815
816fn current_install_executor_missing_capabilities(
817 execution_context: &DeploymentExecutionContextV1,
818) -> Vec<DeploymentExecutorCapabilityV1> {
819 missing_executor_capabilities(
820 &execution_context.backend_capabilities,
821 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
822 )
823}
824
825fn current_install_artifact_roots(icp_root: &Path, network: &str) -> Vec<String> {
826 let planned_root = planned_build_artifact_root(icp_root);
827 let mut roots = vec![planned_root.display().to_string()];
828 if let Ok(resolved_root) = resolve_artifact_root(icp_root, network)
829 && resolved_root != planned_root
830 {
831 roots.push(resolved_root.display().to_string());
832 }
833 roots
834}
835
836fn run_install_deployment_truth_safety_gate(
837 options: &InstallRootOptions,
838 workspace_root: &Path,
839 icp_root: &Path,
840 config_path: &Path,
841 fleet_name: &str,
842 execution_context: &DeploymentExecutionContextV1,
843) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
844 let truth_gate_started_at = current_unix_timestamp_label()?;
845 let deployment_truth_check = current_install_deployment_truth_check_at(
846 options,
847 workspace_root,
848 icp_root,
849 config_path,
850 fleet_name,
851 truth_gate_started_at.clone(),
852 )?;
853 let artifact_gate_receipt = artifact_gate_phase_receipt(
854 &deployment_truth_check,
855 truth_gate_started_at.clone(),
856 Some(current_unix_timestamp_label()?),
857 );
858 let role_receipts = artifact_gate_role_phase_receipts(&deployment_truth_check);
859 let deployment_receipt = receipt_with_execution_context(
860 install_deployment_truth_gate_receipt(
861 &deployment_truth_check,
862 truth_gate_started_at,
863 vec![artifact_gate_receipt],
864 role_receipts,
865 ),
866 execution_context,
867 );
868 let receipt_write = write_install_deployment_truth_receipt(
869 icp_root,
870 &options.network,
871 fleet_name,
872 &deployment_receipt,
873 );
874 match &receipt_write {
875 Ok(path) => println!("Deployment truth receipt JSON: {}", path.display()),
876 Err(err) => eprintln!("Deployment truth receipt JSON write failed: {err}"),
877 }
878 print_install_deployment_truth_gate(&deployment_truth_check, &deployment_receipt);
879 enforce_install_deployment_truth_gate(&deployment_truth_check)?;
880 receipt_write?;
881 write_current_install_execution_preflight_receipt(
882 icp_root,
883 &options.network,
884 fleet_name,
885 &deployment_truth_check,
886 execution_context,
887 )?;
888 Ok(deployment_truth_check)
889}
890
891fn enforce_install_deployment_truth_gate(
892 check: &DeploymentCheckV1,
893) -> Result<(), Box<dyn std::error::Error>> {
894 let blockers = install_deployment_truth_gate_blockers(check);
895 if blockers.is_empty() {
896 return Ok(());
897 }
898
899 let details = blockers
900 .iter()
901 .map(|finding| deployment_truth_finding_label(finding))
902 .collect::<Vec<_>>()
903 .join("; ");
904 Err(format!("deployment truth safety gate blocked install: {details}").into())
905}
906
907fn install_deployment_truth_gate_blockers(check: &DeploymentCheckV1) -> Vec<&SafetyFindingV1> {
908 check.report.hard_failures.iter().collect()
909}
910
911fn print_install_deployment_truth_gate(check: &DeploymentCheckV1, receipt: &DeploymentReceiptV1) {
912 for line in install_deployment_truth_gate_lines(check, receipt) {
913 println!("{line}");
914 }
915}
916
917fn install_deployment_truth_gate_lines(
918 check: &DeploymentCheckV1,
919 receipt: &DeploymentReceiptV1,
920) -> Vec<String> {
921 let mut lines = vec![
922 format!("Deployment truth: {}", check.report.summary),
923 format!(
924 "Deployment truth receipt: operation={} status={:?}",
925 receipt.operation_id, receipt.operation_status
926 ),
927 ];
928 for phase_receipt in &receipt.phase_receipts {
929 lines.push(format!(
930 "Deployment truth phase receipt: phase={} postcondition={:?}",
931 phase_receipt.phase, phase_receipt.verified_postcondition.status
932 ));
933 }
934 if !receipt.role_phase_receipts.is_empty() {
935 lines.push(format!(
936 "Deployment truth role receipts: {}",
937 receipt.role_phase_receipts.len()
938 ));
939 }
940 for role_receipt in &receipt.role_phase_receipts {
941 lines.push(format!(
942 "Deployment truth role receipt: phase={} role={} result={:?}",
943 role_receipt.phase, role_receipt.role, role_receipt.result
944 ));
945 }
946
947 if !check.report.hard_failures.is_empty() {
948 lines.push(format!(
949 "Deployment truth hard failures: {}",
950 check.report.hard_failures.len()
951 ));
952 }
953 for finding in install_deployment_truth_gate_blockers(check) {
954 lines.push(format!(
955 "Deployment truth blocker: {}",
956 deployment_truth_finding_label(finding)
957 ));
958 }
959 if !check.report.warnings.is_empty() {
960 lines.push(format!(
961 "Deployment truth warnings: {}",
962 check.report.warnings.len()
963 ));
964 }
965 for finding in &check.report.warnings {
966 lines.push(format!(
967 "Deployment truth warning: {}",
968 deployment_truth_finding_label(finding)
969 ));
970 }
971 lines
972}
973
974fn install_deployment_truth_gate_receipt(
975 check: &DeploymentCheckV1,
976 started_at: String,
977 phase_receipts: Vec<crate::deployment_truth::PhaseReceiptV1>,
978 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
979) -> DeploymentReceiptV1 {
980 let blockers = install_deployment_truth_gate_blockers(check);
981 let (operation_status, command_result) = if blockers.is_empty() {
982 (
983 DeploymentExecutionStatusV1::Complete,
984 DeploymentCommandResultV1::Succeeded,
985 )
986 } else {
987 (
988 DeploymentExecutionStatusV1::FailedBeforeMutation,
989 DeploymentCommandResultV1::Failed {
990 code: "deployment_truth_blocked".to_string(),
991 message: check.report.summary.clone(),
992 },
993 )
994 };
995 deployment_receipt_from_check_with_status(
996 check,
997 format!("{}:materialize_artifacts", check.check_id),
998 operation_status,
999 started_at,
1000 Some(current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string())),
1001 phase_receipts,
1002 role_phase_receipts,
1003 command_result,
1004 )
1005}
1006
1007fn write_current_install_execution_preflight_receipt(
1008 icp_root: &Path,
1009 network: &str,
1010 fleet_name: &str,
1011 check: &DeploymentCheckV1,
1012 execution_context: &DeploymentExecutionContextV1,
1013) -> Result<PathBuf, Box<dyn std::error::Error>> {
1014 let started_at = current_unix_timestamp_label()?;
1015 let executor = CurrentCliDeploymentExecutor::new(
1016 execution_context.workspace_root.clone(),
1017 execution_context.icp_root.clone(),
1018 execution_context.artifact_roots.clone(),
1019 );
1020 let preflight = deployment_execution_preflight_from_check(
1021 check,
1022 &executor,
1023 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1024 );
1025 validate_deployment_execution_preflight_for_check(check, &preflight)?;
1026 let blockers = preflight.blockers.clone();
1027 let (operation_status, command_result) = if blockers.is_empty() {
1028 (
1029 DeploymentExecutionStatusV1::Complete,
1030 DeploymentCommandResultV1::Succeeded,
1031 )
1032 } else {
1033 (
1034 DeploymentExecutionStatusV1::FailedBeforeMutation,
1035 DeploymentCommandResultV1::Failed {
1036 code: "execution_preflight_blocked".to_string(),
1037 message: "deployment execution preflight blocked current install".to_string(),
1038 },
1039 )
1040 };
1041 let finished_at = current_unix_timestamp_label()?;
1042 let receipt = receipt_with_execution_context(
1043 deployment_receipt_from_check_with_status(
1044 check,
1045 format!("{}:execution_preflight", check.check_id),
1046 operation_status,
1047 started_at.clone(),
1048 Some(finished_at.clone()),
1049 vec![phase_receipt(
1050 "execution_preflight",
1051 started_at,
1052 Some(finished_at),
1053 "validate deployment plan, authority, and executor capability readiness",
1054 crate::deployment_truth::ObservationStatusV1::Observed,
1055 current_install_execution_preflight_evidence(&preflight),
1056 )],
1057 Vec::new(),
1058 command_result,
1059 ),
1060 execution_context,
1061 );
1062 let path = write_install_deployment_truth_receipt(icp_root, network, fleet_name, &receipt)?;
1063 println!("Deployment truth receipt JSON: {}", path.display());
1064 if !blockers.is_empty() {
1065 let details = blockers
1066 .iter()
1067 .map(deployment_truth_finding_label)
1068 .collect::<Vec<_>>()
1069 .join("; ");
1070 return Err(format!("deployment execution preflight blocked install: {details}").into());
1071 }
1072 Ok(path)
1073}
1074
1075fn current_install_execution_preflight_evidence(
1076 preflight: &crate::deployment_truth::DeploymentExecutionPreflightV1,
1077) -> Vec<String> {
1078 let mut evidence = vec![
1079 format!("execution_preflight_status:{:?}", preflight.status),
1080 format!("authority_plan:{}", preflight.authority_plan_id),
1081 format!("planned_phases:{}", preflight.planned_phases.len()),
1082 format!(
1083 "required_capabilities:{}",
1084 preflight.required_capabilities.len()
1085 ),
1086 format!(
1087 "missing_capabilities:{}",
1088 preflight.missing_capabilities.len()
1089 ),
1090 format!("blockers:{}", preflight.blockers.len()),
1091 ];
1092 evidence.extend(
1093 preflight
1094 .missing_capabilities
1095 .iter()
1096 .map(|capability| format!("missing_capability:{capability:?}")),
1097 );
1098 evidence.extend(
1099 preflight
1100 .blockers
1101 .iter()
1102 .map(|finding| format!("blocker:{}:{}", finding.code, finding.message)),
1103 );
1104 evidence
1105}
1106
1107fn current_install_staging_evidence(
1108 root_canister_id: &str,
1109 manifest_path: &Path,
1110 manifest: &RootReleaseSetManifest,
1111) -> Vec<String> {
1112 let mut evidence = vec![
1113 format!("root_canister:{root_canister_id}"),
1114 format!("manifest_path:{}", manifest_path.display()),
1115 format!("release_version:{}", manifest.release_version),
1116 ];
1117 let staging_receipts = current_install_staging_receipts(root_canister_id, manifest);
1118 evidence.extend(staging_receipt_evidence(&staging_receipts));
1119 evidence
1120}
1121
1122fn current_install_staging_receipts(
1123 root_canister_id: &str,
1124 manifest: &RootReleaseSetManifest,
1125) -> Vec<StagingReceiptV1> {
1126 manifest
1127 .entries
1128 .iter()
1129 .map(|entry| StagingReceiptV1 {
1130 schema_version: crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1131 role: entry.role.clone(),
1132 artifact_identity: format!(
1133 "{}:{}:{}",
1134 entry.template_id, manifest.release_version, entry.payload_sha256_hex
1135 ),
1136 transport: ArtifactTransportV1::WasmStore,
1137 wasm_store_locator: Some(format!("root:{root_canister_id}:bootstrap")),
1138 prepared_chunk_hashes: entry.chunk_sha256_hex.clone(),
1139 published_chunk_count: entry.chunk_sha256_hex.len(),
1140 verified_postcondition: crate::deployment_truth::VerifiedPostconditionV1 {
1141 status: ObservationStatusV1::Observed,
1142 evidence: vec![
1143 format!("payload_sha256:{}", entry.payload_sha256_hex),
1144 format!("payload_size_bytes:{}", entry.payload_size_bytes),
1145 format!("chunk_size_bytes:{}", entry.chunk_size_bytes),
1146 format!("chunk_count:{}", entry.chunk_sha256_hex.len()),
1147 ],
1148 },
1149 })
1150 .collect()
1151}
1152
1153fn install_deployment_truth_phase_receipt(
1154 check: &DeploymentCheckV1,
1155 phase: &str,
1156 started_at: String,
1157 finished_at: Option<String>,
1158 attempted_action: &str,
1159 status: crate::deployment_truth::ObservationStatusV1,
1160 evidence: Vec<String>,
1161) -> DeploymentReceiptV1 {
1162 install_deployment_truth_phase_receipt_with_result(
1163 check,
1164 PhaseReceiptInput {
1165 phase,
1166 started_at,
1167 finished_at,
1168 attempted_action,
1169 status,
1170 evidence,
1171 role_phase_receipts: Vec::new(),
1172 operation_status: DeploymentExecutionStatusV1::Complete,
1173 command_result: DeploymentCommandResultV1::Succeeded,
1174 },
1175 )
1176}
1177
1178fn install_deployment_truth_phase_receipt_with_result(
1179 check: &DeploymentCheckV1,
1180 input: PhaseReceiptInput<'_>,
1181) -> DeploymentReceiptV1 {
1182 deployment_receipt_from_check_with_status(
1183 check,
1184 format!("{}:{}", check.check_id, input.phase),
1185 input.operation_status,
1186 input.started_at.clone(),
1187 input.finished_at.clone(),
1188 vec![phase_receipt(
1189 input.phase,
1190 input.started_at,
1191 input.finished_at,
1192 input.attempted_action,
1193 input.status,
1194 input.evidence,
1195 )],
1196 input.role_phase_receipts,
1197 input.command_result,
1198 )
1199}
1200
1201fn receipt_with_execution_context(
1202 mut receipt: DeploymentReceiptV1,
1203 execution_context: &DeploymentExecutionContextV1,
1204) -> DeploymentReceiptV1 {
1205 receipt.execution_context = Some(execution_context.clone());
1206 receipt
1207}
1208
1209struct PhaseReceiptInput<'a> {
1210 phase: &'a str,
1211 started_at: String,
1212 finished_at: Option<String>,
1213 attempted_action: &'a str,
1214 status: crate::deployment_truth::ObservationStatusV1,
1215 evidence: Vec<String>,
1216 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
1217 operation_status: DeploymentExecutionStatusV1,
1218 command_result: DeploymentCommandResultV1,
1219}
1220
1221fn write_install_deployment_truth_receipt(
1222 icp_root: &Path,
1223 network: &str,
1224 fleet_name: &str,
1225 receipt: &DeploymentReceiptV1,
1226) -> Result<PathBuf, Box<dyn std::error::Error>> {
1227 let path = install_deployment_truth_receipt_path(icp_root, network, fleet_name, receipt)?;
1228 if let Some(parent) = path.parent() {
1229 fs::create_dir_all(parent)?;
1230 }
1231 let mut bytes = serde_json::to_vec_pretty(receipt)?;
1232 bytes.push(b'\n');
1233 fs::write(&path, bytes)?;
1234 Ok(path)
1235}
1236
1237fn install_deployment_truth_receipt_path(
1238 icp_root: &Path,
1239 network: &str,
1240 fleet_name: &str,
1241 receipt: &DeploymentReceiptV1,
1242) -> Result<PathBuf, Box<dyn std::error::Error>> {
1243 validate_network_name(network)?;
1244 validate_fleet_name(fleet_name)?;
1245 let file_stem = format!(
1246 "{}-{}",
1247 safe_deployment_truth_path_label(&receipt.started_at),
1248 safe_deployment_truth_path_label(&receipt.operation_id)
1249 );
1250 Ok(
1251 install_deployment_truth_receipts_dir(icp_root, network, fleet_name)?
1252 .join(format!("{file_stem}.json")),
1253 )
1254}
1255
1256pub fn latest_deployment_truth_receipt_path_from_root(
1258 icp_root: &Path,
1259 network: &str,
1260 fleet_name: &str,
1261) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
1262 let dir = install_deployment_truth_receipts_dir(icp_root, network, fleet_name)?;
1263 if !dir.is_dir() {
1264 return Ok(None);
1265 }
1266
1267 let mut latest = None;
1268 for entry in fs::read_dir(dir)? {
1269 let path = entry?.path();
1270 if !path.is_file()
1271 || path
1272 .extension()
1273 .is_none_or(|ext| !ext.eq_ignore_ascii_case("json"))
1274 {
1275 continue;
1276 }
1277 if latest.as_ref().is_none_or(|current| path > *current) {
1278 latest = Some(path);
1279 }
1280 }
1281 Ok(latest)
1282}
1283
1284fn install_deployment_truth_receipts_dir(
1285 icp_root: &Path,
1286 network: &str,
1287 fleet_name: &str,
1288) -> Result<PathBuf, Box<dyn std::error::Error>> {
1289 validate_network_name(network)?;
1290 validate_fleet_name(fleet_name)?;
1291 Ok(icp_root
1292 .join(".canic")
1293 .join(network)
1294 .join("deployment-receipts")
1295 .join(fleet_name))
1296}
1297
1298fn safe_deployment_truth_path_label(value: &str) -> String {
1299 let label = value
1300 .chars()
1301 .map(|ch| {
1302 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
1303 ch
1304 } else {
1305 '_'
1306 }
1307 })
1308 .collect::<String>();
1309 if label.is_empty() {
1310 "unknown".to_string()
1311 } else {
1312 label
1313 }
1314}
1315
1316fn deployment_truth_finding_label(finding: &SafetyFindingV1) -> String {
1317 let subject = finding
1318 .subject
1319 .as_ref()
1320 .map_or_else(|| "<none>".to_string(), Clone::clone);
1321 format!(
1322 "{}:{}:{}: {}",
1323 deployment_truth_finding_source(&finding.code),
1324 finding.code,
1325 subject,
1326 finding.message
1327 )
1328}
1329
1330fn deployment_truth_finding_source(code: &str) -> &'static str {
1331 match code {
1332 "plan_assumption" => "plan",
1333 "observation_gap" => "inventory",
1334 _ => "diff",
1335 }
1336}
1337
1338fn validate_expected_fleet_name(
1339 expected: Option<&str>,
1340 actual: &str,
1341 config_path: &Path,
1342) -> Result<(), Box<dyn std::error::Error>> {
1343 let Some(expected) = expected else {
1344 return Ok(());
1345 };
1346 if expected == actual {
1347 return Ok(());
1348 }
1349 Err(format!(
1350 "install requested fleet {expected}, but {} declares [fleet].name = {actual:?}",
1351 config_path.display()
1352 )
1353 .into())
1354}
1355
1356fn ensure_root_canister_id(
1357 icp_root: &Path,
1358 network: &str,
1359 root_canister: &str,
1360 config_path: &Path,
1361) -> Result<String, Box<dyn std::error::Error>> {
1362 if Principal::from_text(root_canister).is_ok() {
1363 return Ok(root_canister.to_string());
1364 }
1365
1366 match resolve_root_canister_id(icp_root, network, root_canister) {
1367 Ok(canister_id) => return Ok(canister_id),
1368 Err(err) if !is_missing_canister_id_error(&err.to_string()) => return Err(err),
1369 Err(_) => {}
1370 }
1371
1372 let mut create = icp_canister_command_in_network(icp_root);
1373 add_create_root_target(&mut create, root_canister);
1374 add_local_root_create_cycles_arg(&mut create, config_path, network)?;
1375 add_icp_environment_target(&mut create, network);
1376 let output = run_command_stdout(&mut create)?;
1377 if let Some(canister_id) = parse_created_canister_id(&output) {
1378 return Ok(canister_id);
1379 }
1380
1381 resolve_root_canister_id(icp_root, network, root_canister).map_err(|_| {
1382 format!(
1383 "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.",
1384 icp_root.display(),
1385 icp_root.display(),
1386 )
1387 .into()
1388 })
1389}
1390
1391fn parse_created_canister_id(output: &str) -> Option<String> {
1392 if let Ok(value) = serde_json::from_str::<JsonValue>(output) {
1393 return parse_canister_id_json(&value);
1394 }
1395
1396 output
1397 .lines()
1398 .map(str::trim)
1399 .find(|line| Principal::from_text(*line).is_ok())
1400 .map(ToString::to_string)
1401}
1402
1403fn parse_canister_id_json(value: &JsonValue) -> Option<String> {
1404 match value {
1405 JsonValue::String(text) if Principal::from_text(text).is_ok() => Some(text.clone()),
1406 JsonValue::Array(values) => values.iter().find_map(parse_canister_id_json),
1407 JsonValue::Object(object) => ["canister_id", "id", "principal"]
1408 .iter()
1409 .filter_map(|key| object.get(*key))
1410 .find_map(parse_canister_id_json),
1411 _ => None,
1412 }
1413}
1414
1415fn add_create_root_target(command: &mut Command, root_canister: &str) {
1416 if env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV).is_some() {
1417 command.args(["create", "--detached", "--json"]);
1418 } else {
1419 command.args(["create", root_canister, "--json"]);
1420 }
1421}
1422
1423fn is_missing_canister_id_error(message: &str) -> bool {
1424 message.contains("failed to lookup canister ID")
1425 || message.contains("could not find ID for canister")
1426 || message.contains("Canister ID is missing")
1427}
1428
1429fn reinstall_root_wasm(
1430 icp_root: &Path,
1431 network: &str,
1432 root_canister: &str,
1433 root_wasm: &Path,
1434) -> Result<(), Box<dyn std::error::Error>> {
1435 let mut install = icp_canister_command_in_network(icp_root);
1436 install.args(["install", root_canister, "--mode=reinstall", "-y", "--wasm"]);
1437 install.arg(root_wasm);
1438 install.args(["--args", &root_init_args(root_wasm)?]);
1439 add_icp_environment_target(&mut install, network);
1440 run_command(&mut install)
1441}
1442
1443fn root_init_args(root_wasm: &Path) -> Result<String, Box<dyn std::error::Error>> {
1444 let wasm = std::fs::read(root_wasm)?;
1445 Ok(format!(
1446 "(variant {{ PrimeWithModuleHash = {} }})",
1447 idl_blob(&wasm_hash(&wasm))
1448 ))
1449}
1450
1451fn idl_blob(bytes: &[u8]) -> String {
1452 let mut encoded = String::from("blob \"");
1453 for byte in bytes {
1454 use std::fmt::Write as _;
1455 let _ = write!(encoded, "\\{byte:02X}");
1456 }
1457 encoded.push('"');
1458 encoded
1459}
1460
1461fn build_install_state(
1463 options: &InstallRootOptions,
1464 workspace_root: &Path,
1465 icp_root: &Path,
1466 config_path: &Path,
1467 release_set_manifest_path: &Path,
1468 fleet_name: &str,
1469 root_canister_id: &str,
1470) -> Result<InstallState, Box<dyn std::error::Error>> {
1471 Ok(InstallState {
1472 schema_version: INSTALL_STATE_SCHEMA_VERSION,
1473 fleet: fleet_name.to_string(),
1474 installed_at_unix_secs: current_unix_secs()?,
1475 network: options.network.clone(),
1476 root_target: options.root_canister.clone(),
1477 root_canister_id: root_canister_id.to_string(),
1478 root_build_target: options.root_build_target.clone(),
1479 workspace_root: workspace_root.display().to_string(),
1480 icp_root: icp_root.display().to_string(),
1481 config_path: config_path.display().to_string(),
1482 release_set_manifest_path: release_set_manifest_path.display().to_string(),
1483 })
1484}
1485
1486fn resolve_root_canister_id(
1488 icp_root: &Path,
1489 network: &str,
1490 root_canister: &str,
1491) -> Result<String, Box<dyn std::error::Error>> {
1492 if Principal::from_text(root_canister).is_ok() {
1493 return Ok(root_canister.to_string());
1494 }
1495
1496 let mut command = icp_canister_command_in_network(icp_root);
1497 command.args(["status", root_canister, "--json"]);
1498 add_icp_environment_target(&mut command, network);
1499 let output = run_command_stdout(&mut command)?;
1500 parse_created_canister_id(&output).ok_or_else(|| {
1501 format!("could not parse root canister id from ICP status JSON output: {output}").into()
1502 })
1503}
1504
1505fn current_unix_secs() -> Result<u64, Box<dyn std::error::Error>> {
1507 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
1508}
1509
1510fn current_unix_timestamp_label() -> Result<String, Box<dyn std::error::Error>> {
1511 Ok(format!("unix:{}", current_unix_secs()?))
1512}
1513
1514fn run_canic_build_targets(
1516 network: &str,
1517 targets: &[String],
1518 build_profile: Option<CanisterBuildProfile>,
1519 config_path: &Path,
1520 icp_root: &Path,
1521) -> Result<(), Box<dyn std::error::Error>> {
1522 let _env = BuildEnvGuard::apply(network, config_path, icp_root);
1523 let profile = build_profile.unwrap_or_else(CanisterBuildProfile::current);
1524 if let Some(context) = current_workspace_build_context_once(profile)? {
1525 for line in context.lines() {
1526 println!("{line}");
1527 }
1528 println!("config: {}", config_path.display());
1529 println!(
1530 "artifacts: {}",
1531 planned_build_artifact_root(icp_root).display()
1532 );
1533 println!();
1534 }
1535
1536 fs::create_dir_all(planned_build_artifact_root(icp_root))?;
1537 println!("Building {} canisters", targets.len());
1538 println!();
1539 let headers = ["CANISTER", "PROGRESS", "WASM", "ELAPSED"];
1540 let planned_rows = targets
1541 .iter()
1542 .map(|target| {
1543 [
1544 target.clone(),
1545 progress_bar(targets.len(), targets.len(), 10),
1546 "000.00 MiB (gz 000.00 MiB)".to_string(),
1547 "0.00s".to_string(),
1548 ]
1549 })
1550 .collect::<Vec<_>>();
1551 let alignments = [
1552 ColumnAlign::Left,
1553 ColumnAlign::Left,
1554 ColumnAlign::Right,
1555 ColumnAlign::Right,
1556 ];
1557 let widths = table_widths(&headers, &planned_rows);
1558 println!("{}", render_table_row(&headers, &widths, &alignments));
1559 println!("{}", render_separator(&widths));
1560
1561 for (index, target) in targets.iter().enumerate() {
1562 let started_at = Instant::now();
1563 let output = build_current_workspace_canister_artifact(target, profile)
1564 .map_err(|err| format!("artifact build failed for {target}: {err}"))?;
1565 let elapsed = started_at.elapsed();
1566 let artifact_size = wasm_artifact_size(&output.wasm_path, &output.wasm_gz_path)?;
1567
1568 let row = [
1569 target.clone(),
1570 progress_bar(index + 1, targets.len(), 10),
1571 artifact_size,
1572 format!("{:.2}s", elapsed.as_secs_f64()),
1573 ];
1574 println!("{}", render_table_row(&row, &widths, &alignments));
1575 }
1576
1577 println!();
1578 Ok(())
1579}
1580
1581fn planned_build_artifact_root(icp_root: &Path) -> PathBuf {
1582 icp_root.join(".icp/local/canisters")
1583}
1584
1585fn wasm_artifact_size(
1586 wasm_path: &Path,
1587 wasm_gz_path: &Path,
1588) -> Result<String, Box<dyn std::error::Error>> {
1589 let wasm_bytes = Some(std::fs::metadata(wasm_path)?.len());
1590 let gzip_bytes = std::fs::metadata(wasm_gz_path)
1591 .ok()
1592 .map(|metadata| metadata.len());
1593 Ok(wasm_size_label(wasm_bytes, gzip_bytes))
1594}
1595
1596struct BuildEnvGuard {
1597 previous_network: Option<OsString>,
1598 previous_config_path: Option<OsString>,
1599 previous_icp_root: Option<OsString>,
1600 previous_local_network_url: Option<OsString>,
1601 previous_local_root_key: Option<OsString>,
1602}
1603
1604impl BuildEnvGuard {
1605 fn apply(network: &str, config_path: &Path, icp_root: &Path) -> Self {
1606 let guard = Self {
1607 previous_network: env::var_os("ICP_ENVIRONMENT"),
1608 previous_config_path: env::var_os("CANIC_CONFIG_PATH"),
1609 previous_icp_root: env::var_os("CANIC_ICP_ROOT"),
1610 previous_local_network_url: env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV),
1611 previous_local_root_key: env::var_os(CANIC_ICP_LOCAL_ROOT_KEY_ENV),
1612 };
1613 set_env("ICP_ENVIRONMENT", network);
1614 set_env("CANIC_CONFIG_PATH", config_path);
1615 set_env("CANIC_ICP_ROOT", icp_root);
1616 if let Some(target) = local_replica_icp_target(network, icp_root) {
1617 set_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV, target.url);
1618 set_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV, target.root_key);
1619 } else {
1620 remove_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV);
1621 remove_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV);
1622 }
1623 guard
1624 }
1625}
1626
1627impl Drop for BuildEnvGuard {
1628 fn drop(&mut self) {
1629 restore_env("ICP_ENVIRONMENT", self.previous_network.take());
1630 restore_env("CANIC_CONFIG_PATH", self.previous_config_path.take());
1631 restore_env("CANIC_ICP_ROOT", self.previous_icp_root.take());
1632 restore_env(
1633 CANIC_ICP_LOCAL_NETWORK_URL_ENV,
1634 self.previous_local_network_url.take(),
1635 );
1636 restore_env(
1637 CANIC_ICP_LOCAL_ROOT_KEY_ENV,
1638 self.previous_local_root_key.take(),
1639 );
1640 }
1641}
1642
1643struct LocalReplicaIcpTarget {
1644 url: String,
1645 root_key: String,
1646}
1647
1648fn local_replica_icp_target(network: &str, icp_root: &Path) -> Option<LocalReplicaIcpTarget> {
1649 if !replica_query::should_use_local_replica_query(Some(network)) {
1650 return None;
1651 }
1652 if icp_ping(icp_root, network).unwrap_or(false) {
1653 return None;
1654 }
1655 let root_key = replica_query::local_replica_root_key_from_root(Some(network), icp_root)
1656 .ok()
1657 .flatten()?;
1658 Some(LocalReplicaIcpTarget {
1659 url: replica_query::local_replica_endpoint_from_root(Some(network), icp_root),
1660 root_key,
1661 })
1662}
1663
1664fn set_env<K, V>(key: K, value: V)
1665where
1666 K: AsRef<std::ffi::OsStr>,
1667 V: AsRef<std::ffi::OsStr>,
1668{
1669 unsafe {
1672 env::set_var(key, value);
1673 }
1674}
1675
1676fn remove_env<K>(key: K)
1677where
1678 K: AsRef<std::ffi::OsStr>,
1679{
1680 unsafe {
1683 env::remove_var(key);
1684 }
1685}
1686
1687fn restore_env(key: &str, value: Option<OsString>) {
1688 if let Some(value) = value {
1690 set_env(key, value);
1691 } else {
1692 remove_env(key);
1693 }
1694}
1695
1696fn add_local_root_create_cycles_arg(
1697 command: &mut Command,
1698 config_path: &Path,
1699 network: &str,
1700) -> Result<(), Box<dyn std::error::Error>> {
1701 if network != "local" {
1702 return Ok(());
1703 }
1704
1705 let cycles = configured_local_root_create_cycles(config_path)?;
1706 command.args(["--cycles", &cycles.to_string()]);
1707 Ok(())
1708}
1709
1710fn ensure_local_root_min_cycles(
1711 icp_root: &Path,
1712 network: &str,
1713 root_canister: &str,
1714 phase: &str,
1715) -> Result<(), Box<dyn std::error::Error>> {
1716 if network != "local" {
1717 return Ok(());
1718 }
1719
1720 let current = query_root_cycle_balance(network, root_canister)?;
1721 if current >= LOCAL_ROOT_MIN_READY_CYCLES {
1722 return Ok(());
1723 }
1724
1725 let amount = LOCAL_ROOT_MIN_READY_CYCLES.saturating_sub(current);
1726 let mut command = icp_canister_command_in_network(icp_root);
1727 command
1728 .args(["top-up", "--amount"])
1729 .arg(amount.to_string())
1730 .arg(root_canister);
1731 add_icp_environment_target(&mut command, network);
1732 run_command(&mut command)?;
1733 println!(
1734 "Local root cycles ({phase}): topped up {} ({} -> {} target)",
1735 crate::format::cycles_tc(amount),
1736 crate::format::cycles_tc(current),
1737 crate::format::cycles_tc(LOCAL_ROOT_MIN_READY_CYCLES)
1738 );
1739 Ok(())
1740}
1741
1742fn query_root_cycle_balance(
1743 network: &str,
1744 root_canister: &str,
1745) -> Result<u128, Box<dyn std::error::Error>> {
1746 let output = icp_query_on_network(
1747 network,
1748 root_canister,
1749 protocol::CANIC_CYCLE_BALANCE,
1750 None,
1751 Some("json"),
1752 )?;
1753 parse_cycle_balance_response(&output).ok_or_else(|| {
1754 format!(
1755 "could not parse {root_canister} {} response: {output}",
1756 protocol::CANIC_CYCLE_BALANCE
1757 )
1758 .into()
1759 })
1760}
1761
1762fn progress_bar(current: usize, total: usize, width: usize) -> String {
1763 if total == 0 || width == 0 {
1764 return "[] 0/0".to_string();
1765 }
1766
1767 let filled = current.saturating_mul(width).div_ceil(total);
1768 let filled = filled.min(width);
1769 format!(
1770 "[{}{}] {current}/{total}",
1771 "#".repeat(filled),
1772 " ".repeat(width - filled)
1773 )
1774}
1775
1776fn ensure_icp_environment_ready(
1778 icp_root: &Path,
1779 network: &str,
1780) -> Result<(), Box<dyn std::error::Error>> {
1781 if icp_ping(icp_root, network)? {
1782 return Ok(());
1783 }
1784 if replica_query::should_use_local_replica_query(Some(network))
1785 && replica_query::local_replica_status_reachable_from_root(Some(network), icp_root)
1786 {
1787 println!(
1788 "Replica reachable via HTTP status endpoint even though ICP CLI reports network '{network}' stopped; continuing from ICP root {}.",
1789 icp_root.display()
1790 );
1791 return Ok(());
1792 }
1793
1794 Err(format!(
1795 "icp environment is not running for network '{network}'\nStart the target replica in another terminal with `canic replica start` and rerun."
1796 )
1797 .into())
1798}
1799
1800fn icp_ping(icp_root: &Path, network: &str) -> Result<bool, Box<dyn std::error::Error>> {
1802 Ok(icp::default_command_in(icp_root)
1803 .args(["network", "ping", network])
1804 .output()?
1805 .status
1806 .success())
1807}
1808
1809fn print_install_timing_summary(timings: &InstallTimingSummary, total: Duration) {
1810 println!("Install timing summary:");
1811 println!("{}", render_install_timing_summary(timings, total));
1812}
1813
1814fn render_install_timing_summary(timings: &InstallTimingSummary, total: Duration) -> String {
1815 let rows = [
1816 timing_row("create_canisters", timings.create_canisters),
1817 timing_row("build_all", timings.build_all),
1818 timing_row("emit_manifest", timings.emit_manifest),
1819 timing_row("install_root", timings.install_root),
1820 timing_row("fund_root", timings.fund_root),
1821 timing_row("stage_release_set", timings.stage_release_set),
1822 timing_row("resume_bootstrap", timings.resume_bootstrap),
1823 timing_row("wait_ready", timings.wait_ready),
1824 timing_row("finalize_root_funding", timings.finalize_root_funding),
1825 timing_row("total", total),
1826 ];
1827 render_table(
1828 &["PHASE", "ELAPSED"],
1829 &rows,
1830 &[ColumnAlign::Left, ColumnAlign::Right],
1831 )
1832}
1833
1834fn timing_row(label: &str, duration: Duration) -> [String; 2] {
1835 [label.to_string(), format!("{:.2}s", duration.as_secs_f64())]
1836}
1837
1838fn print_install_result_summary(network: &str, fleet: &str, state_path: &Path) {
1840 println!("Install result:");
1841 println!("{:<14} success", "status");
1842 println!("{:<14} {}", "fleet", fleet);
1843 println!("{:<14} {}", "install_state", state_path.display());
1844 println!(
1845 "{:<14} canic list {} --network {}",
1846 "smoke_check", fleet, network
1847 );
1848}
1849
1850fn run_command(command: &mut Command) -> Result<(), Box<dyn std::error::Error>> {
1852 icp::run_status(command).map_err(Into::into)
1853}
1854
1855fn run_command_stdout(command: &mut Command) -> Result<String, Box<dyn std::error::Error>> {
1857 icp::run_output(command).map_err(Into::into)
1858}
1859
1860fn icp_command_on_network(network: &str) -> Command {
1863 let mut command = icp::default_command();
1864 command.env("ICP_ENVIRONMENT", network);
1865 command
1866}
1867
1868fn icp_command_in_network(icp_root: &Path, network: &str) -> Command {
1870 let mut command = icp::default_command_in(icp_root);
1871 command.env("ICP_ENVIRONMENT", network);
1872 command
1873}
1874
1875fn icp_canister_command_in_network(icp_root: &Path) -> Command {
1877 let mut command = icp::default_command_in(icp_root);
1878 command.arg("canister");
1879 command
1880}
1881
1882fn add_icp_environment_target(command: &mut Command, network: &str) {
1883 icp::add_target_args(command, Some(network), None);
1884}