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_network;
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_network::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_environment_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 environment = options.environment.as_str();
230 let execution_context = current_install_execution_context(
231 &workspace_root,
232 &icp_root,
233 options.artifact_environment(),
234 );
235
236 println!("Installing deployment {deployment_name}");
237 println!("Fleet template {fleet_name}");
238 println!();
239 let prepared = prepare_install_deployment_truth(
240 &options,
241 &icp_root,
242 &config_path,
243 &deployment_name,
244 &execution_context,
245 &build_context,
246 &install_snapshot,
247 )
248 .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
249 timings.create_canisters = prepared.timings.create_canisters;
250 timings.build_all = prepared.timings.build_all;
251
252 let (manifest_path, emit_manifest_duration) = emit_manifest_with_deployment_truth_receipt(
253 &icp_root,
254 &options,
255 &deployment_name,
256 &prepared.deployment_truth_check,
257 &execution_context,
258 &install_snapshot,
259 &prepared.build_outputs,
260 prepared.plan_artifacts.as_ref(),
261 )
262 .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
263 timings.emit_manifest = emit_manifest_duration;
264 let activation_timings = run_root_activation_phases(
265 InstallReceiptScope {
266 icp_root: &icp_root,
267 environment,
268 deployment_name: &deployment_name,
269 check: &prepared.deployment_truth_check,
270 execution_context: Some(&execution_context),
271 },
272 &options,
273 &prepared.root_canister_id,
274 &manifest_path,
275 total_started_at,
276 &build_context,
277 prepared.plan_artifacts.as_ref(),
278 )
279 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
280 timings.install_root = activation_timings.install_root;
281 timings.fund_root = activation_timings.fund_root;
282 timings.stage_release_set = activation_timings.stage_release_set;
283 timings.resume_bootstrap = activation_timings.resume_bootstrap;
284 timings.wait_ready = activation_timings.wait_ready;
285 timings.finalize_root_funding = activation_timings.finalize_root_funding;
286
287 print_install_timing_summary(&timings, total_started_at.elapsed());
288 persist_install_result(
289 InstallReceiptScope {
290 icp_root: &icp_root,
291 environment,
292 deployment_name: &deployment_name,
293 check: &prepared.deployment_truth_check,
294 execution_context: Some(&execution_context),
295 },
296 &options,
297 &workspace_root,
298 &config_path,
299 &manifest_path,
300 InstallCompletion {
301 fleet_name: &fleet_name,
302 root_canister_id: &prepared.root_canister_id,
303 execution_context: &execution_context,
304 },
305 )
306}
307
308fn persist_install_result(
309 receipt_scope: InstallReceiptScope<'_>,
310 options: &InstallRootOptions,
311 workspace_root: &Path,
312 config_path: &Path,
313 manifest_path: &Path,
314 completion: InstallCompletion<'_>,
315) -> Result<(), InstallRootError> {
316 let state = build_install_state(
317 options,
318 workspace_root,
319 receipt_scope.icp_root,
320 config_path,
321 manifest_path,
322 (receipt_scope.deployment_name, completion.fleet_name),
323 completion.root_canister_id,
324 )
325 .map_err(InstallRootError::in_phase(
326 InstallRootPhase::StatePersistence,
327 ))?;
328 let state_path = write_install_state_with_deployment_truth_receipt(
329 receipt_scope,
330 &options.environment,
331 &state,
332 )
333 .map_err(InstallRootError::in_phase(
334 InstallRootPhase::StatePersistence,
335 ))?;
336 write_artifact_promotion_execution_receipt_for_install(
337 options,
338 receipt_scope.icp_root,
339 receipt_scope.environment,
340 receipt_scope.deployment_name,
341 receipt_scope.check,
342 completion.execution_context,
343 )
344 .map_err(InstallRootError::in_phase(
345 InstallRootPhase::ArtifactPromotion,
346 ))?;
347 print_install_result_summary(
348 receipt_scope.environment,
349 &state.deployment_name,
350 &state.fleet_template,
351 &state_path,
352 );
353 Ok(())
354}
355
356fn current_install_build_inputs(
357 workspace_root: &std::path::Path,
358 icp_root: &std::path::Path,
359 config_path: &std::path::Path,
360 options: &InstallRootOptions,
361) -> Result<
362 (
363 crate::canister_build::WorkspaceBuildContext,
364 build_snapshot::ValidatedInstallSnapshot,
365 ),
366 Box<dyn std::error::Error>,
367> {
368 let context = resolve_install_build_context(
369 workspace_root,
370 icp_root,
371 config_path,
372 &options.environment,
373 &options.root_build_target,
374 options.build_profile,
375 )?;
376 let snapshot = resolve_install_snapshot(
377 &context,
378 &options.root_build_target,
379 options.deployment_plan_override.is_some(),
380 )?;
381 Ok((context, snapshot))
382}