1use crate::{
2 canister_build::cache::DefaultCanisterBuildCacheCleanup,
3 release_set::{icp_root, workspace_root},
4};
5use config_selection::resolve_install_config_path;
6use std::{
7 fmt,
8 path::{Path, PathBuf},
9 time::Instant,
10};
11use thiserror::Error as ThisError;
12
13mod activation;
14mod artifact_promotion;
15mod build_environment;
16mod build_snapshot;
17mod build_targets;
18mod capabilities;
19mod clock;
20mod commands;
21mod config_selection;
22mod current_execution;
23mod deployment_registration;
24mod deployment_truth_gate;
25mod execution_preflight;
26mod identity;
27mod install_state;
28mod operations;
29mod options;
30mod output;
31mod phase_receipts;
32mod plan_artifacts;
33mod preparation;
34mod readiness;
35mod receipt_io;
36mod root_canister;
37mod root_cycles;
38mod root_verification;
39mod staging;
40mod state;
41mod timing;
42mod truth_check;
43
44use activation::run_root_activation_phases;
45use artifact_promotion::write_artifact_promotion_execution_receipt_for_install;
46use build_environment::resolve_install_build_context;
47use build_snapshot::resolve_install_snapshot;
48pub use config_selection::{
49 ConfigDiscoveryError, current_canic_project_root, discover_canic_config_choices,
50 discover_canic_project_root_from, discover_project_canic_config_choices, project_fleet_roots,
51 select_discovered_fleet_config_path,
52};
53use current_execution::current_install_execution_context;
54pub use deployment_registration::{
55 RegisterDeploymentStateOptions, VerifyDeploymentRootOptions, register_deployment_state,
56 verify_registered_deployment_root,
57};
58use identity::resolve_install_identity;
59use install_state::{build_install_state, write_install_state_with_deployment_truth_receipt};
60pub use options::InstallRootOptions;
61use output::{print_install_result_summary, print_install_timing_summary};
62pub use phase_receipts::InstallPhaseFailureError;
63use phase_receipts::InstallReceiptScope;
64use plan_artifacts::emit_manifest_with_deployment_truth_receipt;
65use preparation::prepare_install_deployment_truth;
66pub use receipt_io::latest_deployment_truth_receipt_path_from_root;
67pub use state::{
68 InstallState, InstallStateError, RootVerificationStatus, read_named_deployment_install_state,
69 read_named_deployment_install_state_from_root,
70};
71pub(crate) use state::{decode_install_state, validate_network_name};
72use timing::InstallTimingSummary as CurrentInstallTimingSummary;
73pub use truth_check::{check_install_deployment_truth, check_install_execution_preflight};
74
75#[cfg(test)]
76mod tests;
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum InstallRootBlockKind {
86 DeploymentExecutionPreflight,
87 DeploymentTruth,
88}
89
90#[derive(Debug, ThisError)]
97#[error("{message}")]
98pub struct InstallRootBlockedError {
99 kind: InstallRootBlockKind,
100 message: String,
101}
102
103impl InstallRootBlockedError {
104 pub(super) const fn new(kind: InstallRootBlockKind, message: String) -> Self {
105 Self { kind, message }
106 }
107
108 #[must_use]
109 pub const fn kind(&self) -> InstallRootBlockKind {
110 self.kind
111 }
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub enum InstallRootPhase {
117 WorkspaceDiscovery,
118 ProjectDiscovery,
119 Configuration,
120 BuildInputs,
121 Identity,
122 Preparation,
123 Manifest,
124 Activation,
125 StatePersistence,
126 ArtifactPromotion,
127}
128
129impl fmt::Display for InstallRootPhase {
130 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131 formatter.write_str(match self {
132 Self::WorkspaceDiscovery => "workspace discovery",
133 Self::ProjectDiscovery => "ICP project discovery",
134 Self::Configuration => "configuration selection",
135 Self::BuildInputs => "build input validation",
136 Self::Identity => "deployment identity resolution",
137 Self::Preparation => "deployment preparation",
138 Self::Manifest => "manifest emission",
139 Self::Activation => "root activation",
140 Self::StatePersistence => "install-state persistence",
141 Self::ArtifactPromotion => "artifact-promotion receipt persistence",
142 })
143 }
144}
145
146#[derive(Debug, ThisError)]
148#[error("root install failed during {phase}: {source}")]
149pub struct InstallRootError {
150 phase: InstallRootPhase,
151 #[source]
152 source: Box<dyn std::error::Error>,
153}
154
155struct InstallCompletion<'a> {
156 fleet_name: &'a str,
157 root_canister_id: &'a str,
158 execution_context: &'a crate::deployment_truth::DeploymentExecutionContextV1,
159}
160
161impl InstallRootError {
162 pub fn new<E>(phase: InstallRootPhase, source: E) -> Self
164 where
165 E: std::error::Error + 'static,
166 {
167 Self {
168 phase,
169 source: Box::new(source),
170 }
171 }
172
173 fn from_boxed(phase: InstallRootPhase, source: Box<dyn std::error::Error>) -> Self {
174 Self { phase, source }
175 }
176
177 fn in_phase(phase: InstallRootPhase) -> impl FnOnce(Box<dyn std::error::Error>) -> Self {
178 move |source| Self::from_boxed(phase, source)
179 }
180
181 #[must_use]
182 pub const fn phase(&self) -> InstallRootPhase {
183 self.phase
184 }
185}
186
187pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, ConfigDiscoveryError> {
189 let project_root = current_canic_project_root()?;
190 let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
191 if !choices.is_empty() {
192 return Ok(choices);
193 }
194
195 let icp_root = icp_root()?;
196 if icp_root != project_root {
197 return config_selection::discover_workspace_canic_config_choices(&icp_root);
198 }
199
200 Ok(choices)
201}
202
203pub fn install_root(options: InstallRootOptions) -> Result<(), InstallRootError> {
205 let workspace_root = workspace_root()
206 .map_err(|source| InstallRootError::new(InstallRootPhase::WorkspaceDiscovery, source))?;
207 let _build_cache_cleanup = DefaultCanisterBuildCacheCleanup::for_install(&workspace_root);
208 let icp_root = match &options.icp_root {
209 Some(path) => path
210 .canonicalize()
211 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
212 None => icp_root()
213 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
214 };
215 let config_path = resolve_install_config_path(
216 &icp_root,
217 options.config_path.as_deref(),
218 options.interactive_config_selection,
219 )
220 .map_err(InstallRootError::in_phase(InstallRootPhase::Configuration))?;
221 let (build_context, install_snapshot) =
222 current_install_build_inputs(&workspace_root, &icp_root, &config_path, &options)
223 .map_err(InstallRootError::in_phase(InstallRootPhase::BuildInputs))?;
224 let (fleet_name, deployment_name) =
225 resolve_install_identity(&options, &config_path, &install_snapshot.fleet_name)
226 .map_err(InstallRootError::in_phase(InstallRootPhase::Identity))?;
227 let total_started_at = Instant::now();
228 let mut timings = CurrentInstallTimingSummary::default();
229 let network = options.network.as_str();
230 let execution_context = current_install_execution_context(&workspace_root, &icp_root, network);
231
232 println!("Installing deployment {deployment_name}");
233 println!("Fleet template {fleet_name}");
234 println!();
235 let prepared = prepare_install_deployment_truth(
236 &options,
237 &icp_root,
238 &config_path,
239 &deployment_name,
240 &execution_context,
241 &build_context,
242 &install_snapshot,
243 )
244 .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
245 timings.create_canisters = prepared.timings.create_canisters;
246 timings.build_all = prepared.timings.build_all;
247
248 let (manifest_path, emit_manifest_duration) = emit_manifest_with_deployment_truth_receipt(
249 &icp_root,
250 &options,
251 &deployment_name,
252 &prepared.deployment_truth_check,
253 &execution_context,
254 &install_snapshot,
255 &prepared.build_outputs,
256 )
257 .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
258 timings.emit_manifest = emit_manifest_duration;
259 let activation_timings = run_root_activation_phases(
260 InstallReceiptScope {
261 icp_root: &icp_root,
262 network,
263 deployment_name: &deployment_name,
264 check: &prepared.deployment_truth_check,
265 execution_context: Some(&execution_context),
266 },
267 &options,
268 &prepared.root_canister_id,
269 &manifest_path,
270 total_started_at,
271 &build_context,
272 )
273 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
274 timings.install_root = activation_timings.install_root;
275 timings.fund_root = activation_timings.fund_root;
276 timings.stage_release_set = activation_timings.stage_release_set;
277 timings.resume_bootstrap = activation_timings.resume_bootstrap;
278 timings.wait_ready = activation_timings.wait_ready;
279 timings.finalize_root_funding = activation_timings.finalize_root_funding;
280
281 print_install_timing_summary(&timings, total_started_at.elapsed());
282 persist_install_result(
283 InstallReceiptScope {
284 icp_root: &icp_root,
285 network,
286 deployment_name: &deployment_name,
287 check: &prepared.deployment_truth_check,
288 execution_context: Some(&execution_context),
289 },
290 &options,
291 &workspace_root,
292 &config_path,
293 &manifest_path,
294 InstallCompletion {
295 fleet_name: &fleet_name,
296 root_canister_id: &prepared.root_canister_id,
297 execution_context: &execution_context,
298 },
299 )
300}
301
302fn persist_install_result(
303 receipt_scope: InstallReceiptScope<'_>,
304 options: &InstallRootOptions,
305 workspace_root: &Path,
306 config_path: &Path,
307 manifest_path: &Path,
308 completion: InstallCompletion<'_>,
309) -> Result<(), InstallRootError> {
310 let state = build_install_state(
311 options,
312 workspace_root,
313 receipt_scope.icp_root,
314 config_path,
315 manifest_path,
316 (receipt_scope.deployment_name, completion.fleet_name),
317 completion.root_canister_id,
318 )
319 .map_err(InstallRootError::in_phase(
320 InstallRootPhase::StatePersistence,
321 ))?;
322 let state_path =
323 write_install_state_with_deployment_truth_receipt(receipt_scope, &options.network, &state)
324 .map_err(InstallRootError::in_phase(
325 InstallRootPhase::StatePersistence,
326 ))?;
327 write_artifact_promotion_execution_receipt_for_install(
328 options,
329 receipt_scope.icp_root,
330 receipt_scope.network,
331 receipt_scope.deployment_name,
332 receipt_scope.check,
333 completion.execution_context,
334 )
335 .map_err(InstallRootError::in_phase(
336 InstallRootPhase::ArtifactPromotion,
337 ))?;
338 print_install_result_summary(
339 receipt_scope.network,
340 &state.deployment_name,
341 &state.fleet_template,
342 &state_path,
343 );
344 Ok(())
345}
346
347fn current_install_build_inputs(
348 workspace_root: &std::path::Path,
349 icp_root: &std::path::Path,
350 config_path: &std::path::Path,
351 options: &InstallRootOptions,
352) -> Result<
353 (
354 crate::canister_build::WorkspaceBuildContext,
355 build_snapshot::ValidatedInstallSnapshot,
356 ),
357 Box<dyn std::error::Error>,
358> {
359 let context = resolve_install_build_context(
360 workspace_root,
361 icp_root,
362 config_path,
363 &options.network,
364 &options.root_build_target,
365 options.build_profile,
366 )?;
367 let snapshot = resolve_install_snapshot(
368 &context,
369 &options.root_build_target,
370 options.deployment_plan_override.is_some(),
371 )?;
372 Ok((context, snapshot))
373}