1use crate::{
2 canister_build::cache::DefaultCanisterBuildCacheCleanup,
3 network::resolve_canonical_network_id_from_root,
4 release_set::{icp_root, workspace_root},
5};
6use config_selection::resolve_install_config_path;
7use std::{
8 fmt,
9 path::{Path, PathBuf},
10 time::Instant,
11};
12use thiserror::Error as ThisError;
13
14mod activation;
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 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 root_verification;
38mod state;
39mod timing;
40mod truth_check;
41
42use crate::release_build::{ReleaseBuildPlanError, plan_release_build};
43use activation::{PreparedRootInstall, install_root_prepared};
44use build_network::resolve_install_build_context;
45use build_snapshot::resolve_install_snapshot;
46pub use config_selection::{
47 ConfigDiscoveryError, current_canic_project_root, discover_canic_config_choices,
48 discover_canic_project_root_from, discover_project_canic_config_choices, project_app_roots,
49 select_discovered_app_config_path,
50};
51use current_execution::current_install_execution_context;
52pub use deployment_registration::{
53 RegisterDeploymentStateOptions, VerifyDeploymentRootOptions, register_deployment_state,
54 verify_registered_deployment_root,
55};
56use identity::resolve_install_identity;
57pub use operations::{
58 InstallRootActivationStatusError, InstallRootExecutionReconciliationError,
59 InstallRootModuleVerificationError,
60};
61pub use options::InstallRootOptions;
62use output::print_install_timing_summary;
63pub use phase_receipts::InstallPhaseFailureError;
64use phase_receipts::InstallReceiptScope;
65use plan_artifacts::emit_manifest_with_deployment_truth_receipt;
66use preparation::{prepare_install_deployment_truth, resolve_root_canister_after_manifest};
67pub use receipt_io::latest_deployment_truth_receipt_path_from_root;
68pub(crate) use state::validate_environment_name;
69pub use state::{
70 InstallState, InstallStateError, RootVerificationStatus,
71 read_named_deployment_install_state_from_root,
72};
73use timing::InstallTimingSummary as CurrentInstallTimingSummary;
74pub use truth_check::{check_install_deployment_truth, check_install_execution_preflight};
75
76#[cfg(test)]
77mod tests;
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum InstallRootBlockKind {
87 DeploymentExecutionPreflight,
88 DeploymentTruth,
89}
90
91#[derive(Debug, ThisError)]
98#[error("{message}")]
99pub struct InstallRootBlockedError {
100 kind: InstallRootBlockKind,
101 message: String,
102}
103
104impl InstallRootBlockedError {
105 pub(super) const fn new(kind: InstallRootBlockKind, message: String) -> Self {
106 Self { kind, message }
107 }
108
109 #[must_use]
110 pub const fn kind(&self) -> InstallRootBlockKind {
111 self.kind
112 }
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum InstallRootPhase {
118 WorkspaceDiscovery,
119 ProjectDiscovery,
120 Configuration,
121 BuildInputs,
122 Identity,
123 Preparation,
124 Manifest,
125 Activation,
126}
127
128impl fmt::Display for InstallRootPhase {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 formatter.write_str(match self {
131 Self::WorkspaceDiscovery => "workspace discovery",
132 Self::ProjectDiscovery => "ICP project discovery",
133 Self::Configuration => "configuration selection",
134 Self::BuildInputs => "build input validation",
135 Self::Identity => "deployment identity resolution",
136 Self::Preparation => "deployment preparation",
137 Self::Manifest => "manifest emission",
138 Self::Activation => "root activation",
139 })
140 }
141}
142
143#[derive(Debug, ThisError)]
145#[error("root install failed during {phase}: {source}")]
146pub struct InstallRootError {
147 phase: InstallRootPhase,
148 #[source]
149 source: Box<dyn std::error::Error>,
150}
151
152#[derive(Debug, ThisError)]
154#[error(
155 "root {root_canister_id} is durably Prepared at activation journal {} sequence {sequence}; no operational Fleet state was published",
156 journal_path.display()
157)]
158pub struct FleetActivationContinuationRequired {
159 root_canister_id: String,
160 journal_path: PathBuf,
161 sequence: u64,
162}
163
164impl FleetActivationContinuationRequired {
165 #[must_use]
166 pub fn root_canister_id(&self) -> &str {
167 &self.root_canister_id
168 }
169
170 #[must_use]
171 pub fn journal_path(&self) -> &Path {
172 &self.journal_path
173 }
174
175 #[must_use]
176 pub const fn sequence(&self) -> u64 {
177 self.sequence
178 }
179}
180
181fn continuation_required(
182 root_canister_id: String,
183 prepared_root: &PreparedRootInstall,
184) -> InstallRootError {
185 InstallRootError::new(
186 InstallRootPhase::Activation,
187 FleetActivationContinuationRequired {
188 root_canister_id,
189 journal_path: prepared_root.activation.path.clone(),
190 sequence: prepared_root.activation.journal.sequence,
191 },
192 )
193}
194
195impl InstallRootError {
196 pub fn new<E>(phase: InstallRootPhase, source: E) -> Self
198 where
199 E: std::error::Error + 'static,
200 {
201 Self {
202 phase,
203 source: Box::new(source),
204 }
205 }
206
207 fn from_boxed(phase: InstallRootPhase, source: Box<dyn std::error::Error>) -> Self {
208 Self { phase, source }
209 }
210
211 fn in_phase(phase: InstallRootPhase) -> impl FnOnce(Box<dyn std::error::Error>) -> Self {
212 move |source| Self::from_boxed(phase, source)
213 }
214
215 #[must_use]
216 pub const fn phase(&self) -> InstallRootPhase {
217 self.phase
218 }
219}
220
221pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, ConfigDiscoveryError> {
223 let project_root = current_canic_project_root()?;
224 let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
225 if !choices.is_empty() {
226 return Ok(choices);
227 }
228
229 let icp_root = icp_root()?;
230 if icp_root != project_root {
231 return config_selection::discover_workspace_canic_config_choices(&icp_root);
232 }
233
234 Ok(choices)
235}
236
237pub fn install_root(options: InstallRootOptions) -> Result<(), InstallRootError> {
239 let workspace_root = workspace_root()
240 .map_err(|source| InstallRootError::new(InstallRootPhase::WorkspaceDiscovery, source))?;
241 let _build_cache_cleanup = DefaultCanisterBuildCacheCleanup::for_install(&workspace_root);
242 let icp_root = match &options.icp_root {
243 Some(path) => path
244 .canonicalize()
245 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
246 None => icp_root()
247 .map_err(|source| InstallRootError::new(InstallRootPhase::ProjectDiscovery, source))?,
248 };
249 let config_path = resolve_install_config_path(
250 &icp_root,
251 options.config_path.as_deref(),
252 options.interactive_config_selection,
253 )
254 .map_err(InstallRootError::in_phase(InstallRootPhase::Configuration))?;
255 let (build_context, install_snapshot) =
256 current_install_build_inputs(&workspace_root, &icp_root, &config_path, &options)
257 .map_err(InstallRootError::in_phase(InstallRootPhase::BuildInputs))?;
258 let (app_id, fleet_name) =
259 resolve_install_identity(&options, &config_path, &install_snapshot.app_id)
260 .map_err(InstallRootError::in_phase(InstallRootPhase::Identity))?;
261 let total_started_at = Instant::now();
262 let mut timings = CurrentInstallTimingSummary::default();
263 let environment = options.environment.as_str();
264 let execution_context = current_install_execution_context(
265 &workspace_root,
266 &icp_root,
267 options.artifact_environment(),
268 );
269
270 println!("Installing Fleet {fleet_name}");
271 println!("Source App {app_id}");
272 println!();
273 let prepared = prepare_install_deployment_truth(
274 &options,
275 &icp_root,
276 &config_path,
277 &fleet_name,
278 &execution_context,
279 &build_context,
280 &install_snapshot,
281 )
282 .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
283 timings.build_all = prepared.timings.build_all;
284 let receipt_scope = InstallReceiptScope {
285 icp_root: &icp_root,
286 environment,
287 deployment_name: &fleet_name,
288 check: &prepared.deployment_truth_check,
289 execution_context: Some(&execution_context),
290 };
291
292 let (_manifest_path, emit_manifest_duration, finalized_release_build) =
293 emit_manifest_with_deployment_truth_receipt(
294 receipt_scope,
295 &options,
296 &install_snapshot,
297 &prepared.build_outputs,
298 prepared.plan_artifacts.as_ref(),
299 )
300 .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
301 timings.emit_manifest = emit_manifest_duration;
302 let finalized_release_build = finalized_release_build.ok_or_else(|| {
303 InstallRootError::new(
304 InstallRootPhase::Manifest,
305 ReleaseBuildPlanError::MissingFinalizedAuthority,
306 )
307 })?;
308 let canonical_network_id = resolve_canonical_network_id_from_root(&icp_root, environment)
309 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
310 let activation = fleet_activation_journal::plan_fleet_install_activation(
311 fleet_activation_journal::PlanFleetInstallActivationRequest {
312 root: &icp_root,
313 canonical_network_id,
314 fleet_name: fleet_name
315 .parse()
316 .map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?,
317 app: app_id.into(),
318 finalized_release_build: &finalized_release_build,
319 },
320 )
321 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
322 let (root_canister_id, create_duration) =
323 resolve_root_canister_after_manifest(receipt_scope, &options, &config_path, &build_context)
324 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
325 timings.create_canisters = create_duration;
326 let prepared_root = install_root_prepared(
327 receipt_scope,
328 &options,
329 &root_canister_id,
330 &build_context,
331 prepared.plan_artifacts.as_ref(),
332 &activation,
333 )
334 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
335 timings.record_activation(prepared_root.timings);
336
337 print_install_timing_summary(&timings, total_started_at.elapsed());
338 Err(continuation_required(root_canister_id, &prepared_root))
339}
340
341fn current_install_build_inputs(
342 workspace_root: &std::path::Path,
343 icp_root: &std::path::Path,
344 config_path: &std::path::Path,
345 options: &InstallRootOptions,
346) -> Result<
347 (
348 crate::canister_build::WorkspaceBuildContext,
349 build_snapshot::ValidatedInstallSnapshot,
350 ),
351 Box<dyn std::error::Error>,
352> {
353 let mut context = resolve_install_build_context(
354 workspace_root,
355 icp_root,
356 config_path,
357 &options.environment,
358 &options.root_build_target,
359 options.build_profile,
360 )?;
361 let mut snapshot = resolve_install_snapshot(
362 &context,
363 &options.root_build_target,
364 options.deployment_plan_override.is_some(),
365 )?;
366 if snapshot.complete_build.is_some() {
367 let release_build = plan_release_build(icp_root)?;
368 context = context.with_release_build_id(release_build.record.release_build_id);
369 snapshot.release_build = Some(release_build);
370 }
371 Ok((context, snapshot))
372}