Skip to main content

canic_host/install_root/
mod.rs

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///
72/// InstallRootBlockKind
73///
74/// Machine-readable reason that a fresh root install stopped before mutation.
75///
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum InstallRootBlockKind {
79    DeploymentExecutionPreflight,
80    DeploymentTruth,
81}
82
83///
84/// InstallRootBlockedError
85///
86/// Typed install block retained through the host/CLI error boundary.
87///
88
89#[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/// Stable phase in which a root install failed.
108#[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/// Typed public failure for the root-install workflow.
136#[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    /// Preserve a concrete cause while assigning it to a stable install phase.
146    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
170/// Discover installable Canic config choices under the current workspace.
171pub 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
186// Execute the local thin-root install flow against an already running replica.
187pub 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}