1use crate::{
2 canister_build::cache::DefaultCanisterBuildCacheCleanup,
3 deployment_truth::DeploymentReceiptV1,
4 network::resolve_canonical_network_id_from_root,
5 release_set::{icp_root, workspace_root},
6};
7use config_selection::resolve_install_config_path;
8use std::{
9 fmt,
10 path::{Path, PathBuf},
11 time::{Duration, Instant},
12};
13use thiserror::Error as ThisError;
14
15mod activation;
16mod build_network;
17mod build_snapshot;
18mod build_targets;
19mod capabilities;
20mod clock;
21mod commands;
22mod config_selection;
23mod current_execution;
24mod deployment_truth_gate;
25mod execution_preflight;
26mod fleet_activation_journal;
27mod identity;
28mod operations;
29mod options;
30mod output;
31mod phase_receipts;
32mod plan_artifacts;
33mod preparation;
34mod receipt_io;
35mod root_canister;
36mod root_cycles;
37mod timing;
38mod truth_check;
39
40use crate::release_build::{ReleaseBuildPlanError, plan_release_build};
41use activation::install_root_committed;
42use build_network::resolve_install_build_context;
43use build_snapshot::resolve_install_snapshot;
44pub use config_selection::{
45 ConfigDiscoveryError, current_canic_project_root, discover_canic_config_choices,
46 discover_canic_project_root_from, discover_project_canic_config_choices, project_app_roots,
47 select_discovered_app_config_path,
48};
49use current_execution::current_install_execution_context;
50use identity::resolve_install_identity;
51pub use operations::{
52 InstallRootActivationStatusError, InstallRootExecutionReconciliationError,
53 InstallRootModuleVerificationError,
54};
55pub use options::InstallRootOptions;
56use output::print_install_timing_summary;
57pub use phase_receipts::InstallPhaseFailureError;
58use phase_receipts::{
59 CompletedInstallPhase, InstallReceiptScope, write_completed_install_phase_receipt,
60};
61use plan_artifacts::emit_manifest_with_phase;
62use preparation::{prepare_install_deployment_truth, resolve_root_canister_after_manifest};
63use receipt_io::install_deployment_truth_receipts_dir;
64pub use receipt_io::latest_deployment_truth_receipt_path_from_root;
65use timing::InstallTimingSummary as CurrentInstallTimingSummary;
66pub use truth_check::{check_install_deployment_truth, check_install_execution_preflight};
67
68#[cfg(test)]
69mod tests;
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum InstallRootBlockKind {
79 DeploymentExecutionPreflight,
80 DeploymentTruth,
81}
82
83#[derive(Debug, ThisError)]
90#[error("{message}")]
91pub struct InstallRootBlockedError {
92 kind: InstallRootBlockKind,
93 message: String,
94}
95
96impl InstallRootBlockedError {
97 pub(super) const fn new(kind: InstallRootBlockKind, message: String) -> Self {
98 Self { kind, message }
99 }
100
101 #[must_use]
102 pub const fn kind(&self) -> InstallRootBlockKind {
103 self.kind
104 }
105}
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum InstallRootPhase {
110 WorkspaceDiscovery,
111 ProjectDiscovery,
112 Configuration,
113 BuildInputs,
114 Identity,
115 Preparation,
116 Manifest,
117 Activation,
118}
119
120impl fmt::Display for InstallRootPhase {
121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122 formatter.write_str(match self {
123 Self::WorkspaceDiscovery => "workspace discovery",
124 Self::ProjectDiscovery => "ICP project discovery",
125 Self::Configuration => "configuration selection",
126 Self::BuildInputs => "build input validation",
127 Self::Identity => "deployment identity resolution",
128 Self::Preparation => "deployment preparation",
129 Self::Manifest => "manifest emission",
130 Self::Activation => "root activation",
131 })
132 }
133}
134
135#[derive(Debug, ThisError)]
137#[error("root install failed during {phase}: {source}")]
138pub struct InstallRootError {
139 phase: InstallRootPhase,
140 #[source]
141 source: Box<dyn std::error::Error>,
142}
143
144impl InstallRootError {
145 pub fn new<E>(phase: InstallRootPhase, source: E) -> Self
147 where
148 E: std::error::Error + 'static,
149 {
150 Self {
151 phase,
152 source: Box::new(source),
153 }
154 }
155
156 fn from_boxed(phase: InstallRootPhase, source: Box<dyn std::error::Error>) -> Self {
157 Self { phase, source }
158 }
159
160 fn in_phase(phase: InstallRootPhase) -> impl FnOnce(Box<dyn std::error::Error>) -> Self {
161 move |source| Self::from_boxed(phase, source)
162 }
163
164 #[must_use]
165 pub const fn phase(&self) -> InstallRootPhase {
166 self.phase
167 }
168}
169
170pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, ConfigDiscoveryError> {
172 let project_root = current_canic_project_root()?;
173 let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
174 if !choices.is_empty() {
175 return Ok(choices);
176 }
177
178 let icp_root = icp_root()?;
179 if icp_root != project_root {
180 return config_selection::discover_workspace_canic_config_choices(&icp_root);
181 }
182
183 Ok(choices)
184}
185
186pub fn install_root(options: InstallRootOptions) -> Result<(), InstallRootError> {
188 let workspace_root = workspace_root()
189 .map_err(|source| InstallRootError::new(InstallRootPhase::WorkspaceDiscovery, source))?;
190 let _build_cache_cleanup = DefaultCanisterBuildCacheCleanup::for_install(&workspace_root);
191 let icp_root = match &options.icp_root {
192 Some(path) => path
193 .canonicalize()
194 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
195 None => icp_root()
196 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
197 };
198 let config_path = resolve_install_config_path(
199 &icp_root,
200 options.config_path.as_deref(),
201 options.interactive_config_selection,
202 )
203 .map_err(InstallRootError::in_phase(InstallRootPhase::Configuration))?;
204 let (build_context, install_snapshot) =
205 current_install_build_inputs(&workspace_root, &icp_root, &config_path, &options)
206 .map_err(InstallRootError::in_phase(InstallRootPhase::BuildInputs))?;
207 let (app_id, fleet_name) =
208 resolve_install_identity(&options, &config_path, &install_snapshot.app_id)
209 .map_err(InstallRootError::in_phase(InstallRootPhase::Identity))?;
210 let total_started_at = Instant::now();
211 let mut timings = CurrentInstallTimingSummary::default();
212 let environment = options.environment.as_str();
213 let execution_context = current_install_execution_context(
214 &workspace_root,
215 &icp_root,
216 options.artifact_environment(),
217 );
218
219 print_install_identity(&app_id, &fleet_name);
220 let prepared = prepare_install_deployment_truth(
221 &options,
222 &icp_root,
223 &config_path,
224 &fleet_name,
225 &execution_context,
226 &build_context,
227 &install_snapshot,
228 )
229 .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
230 timings.build_all = prepared.timings.build_all;
231 let emitted_manifest = emit_manifest_with_phase(
232 &icp_root,
233 &install_snapshot,
234 &prepared.build_outputs,
235 prepared.plan_artifacts.as_ref(),
236 )
237 .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
238 timings.emit_manifest = emitted_manifest.duration;
239 let finalized_release_build = emitted_manifest.finalized_release_build.ok_or_else(|| {
240 InstallRootError::new(
241 InstallRootPhase::Manifest,
242 ReleaseBuildPlanError::MissingFinalizedAuthority,
243 )
244 })?;
245 let activation = plan_current_fleet_activation(
246 &icp_root,
247 environment,
248 &fleet_name,
249 &app_id,
250 &finalized_release_build,
251 )?;
252 let receipt_scope = InstallReceiptScope {
253 icp_root: &icp_root,
254 environment,
255 fleet: activation.journal.activation.identity.fleet.fleet,
256 check: &prepared.deployment_truth_check,
257 execution_context: Some(&execution_context),
258 };
259 persist_pre_root_receipts(
260 receipt_scope,
261 &prepared.pre_activation_receipts,
262 prepared.build_phase,
263 emitted_manifest.phase,
264 )
265 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
266 let (root_canister_id, create_duration) = resolve_or_recover_activation_root(
267 &activation,
268 receipt_scope,
269 &options,
270 &config_path,
271 &build_context,
272 )
273 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
274 timings.create_canisters = create_duration;
275 let committed_root = install_root_committed(
276 receipt_scope,
277 &options,
278 &root_canister_id,
279 &build_context,
280 prepared.plan_artifacts.as_ref(),
281 &activation,
282 )
283 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
284 timings.record_activation(committed_root.timings);
285
286 print_install_timing_summary(&timings, total_started_at.elapsed());
287 Ok(())
288}
289
290fn plan_current_fleet_activation(
291 icp_root: &Path,
292 environment: &str,
293 fleet_name: &str,
294 app_id: &str,
295 finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
296) -> Result<fleet_activation_journal::ResolvedFleetInstallActivation, InstallRootError> {
297 let canonical_network_id = resolve_canonical_network_id_from_root(icp_root, environment)
298 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
299 let fleet_name = fleet_name
300 .parse()
301 .map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?;
302 fleet_activation_journal::plan_fleet_install_activation(
303 fleet_activation_journal::PlanFleetInstallActivationRequest {
304 root: icp_root,
305 canonical_network_id,
306 fleet_name,
307 app: app_id.into(),
308 finalized_release_build,
309 },
310 )
311 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))
312}
313
314fn resolve_or_recover_activation_root(
315 activation: &fleet_activation_journal::ResolvedFleetInstallActivation,
316 receipt_scope: InstallReceiptScope<'_>,
317 options: &InstallRootOptions,
318 config_path: &Path,
319 build_context: &crate::canister_build::WorkspaceBuildContext,
320) -> Result<(String, Duration), Box<dyn std::error::Error>> {
321 let receipt_directory =
322 install_deployment_truth_receipts_dir(receipt_scope.icp_root, receipt_scope.fleet);
323 match fleet_activation_journal::recover_activation_root_canister(
324 activation,
325 &receipt_directory,
326 )? {
327 Some(root_canister) => Ok((root_canister.to_text(), Duration::ZERO)),
328 None => {
329 resolve_root_canister_after_manifest(receipt_scope, options, config_path, build_context)
330 }
331 }
332}
333
334fn print_install_identity(app: &str, fleet_name: &str) {
335 println!("Installing Fleet {fleet_name}");
336 println!("Source App {app}");
337 println!();
338}
339
340fn persist_pre_root_receipts(
341 receipt_scope: InstallReceiptScope<'_>,
342 prepared_receipts: &[DeploymentReceiptV1],
343 build_phase: CompletedInstallPhase,
344 manifest_phase: CompletedInstallPhase,
345) -> Result<(), Box<dyn std::error::Error>> {
346 for receipt in prepared_receipts {
347 receipt_scope.write_receipt(receipt)?;
348 }
349 write_completed_install_phase_receipt(receipt_scope, build_phase)?;
350 write_completed_install_phase_receipt(receipt_scope, manifest_phase)?;
351 Ok(())
352}
353
354fn current_install_build_inputs(
355 workspace_root: &std::path::Path,
356 icp_root: &std::path::Path,
357 config_path: &std::path::Path,
358 options: &InstallRootOptions,
359) -> Result<
360 (
361 crate::canister_build::WorkspaceBuildContext,
362 build_snapshot::ValidatedInstallSnapshot,
363 ),
364 Box<dyn std::error::Error>,
365> {
366 let mut context = resolve_install_build_context(
367 workspace_root,
368 icp_root,
369 config_path,
370 &options.environment,
371 &options.root_build_target,
372 options.build_profile,
373 )?;
374 let mut snapshot = resolve_install_snapshot(
375 &context,
376 &options.root_build_target,
377 options.deployment_plan_override.is_some(),
378 )?;
379 if snapshot.complete_build.is_some() {
380 let release_build = plan_release_build(icp_root)?;
381 context = context.with_release_build_id(release_build.record.release_build_id);
382 snapshot.release_build = Some(release_build);
383 }
384 Ok((context, snapshot))
385}