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