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