diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index 69b74ace..3fae18c7 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -14,7 +14,10 @@ use crate::{ commands::args::{self, ArgsOpt}, operations::{ candid_compat::{CandidCompatibility, check_candid_compatibility}, - install::{install_canister, resolve_install_mode_and_status}, + install::{ + WasmMemoryPersistenceOpt, install_canister, is_eop_canister, + resolve_install_mode_and_status, + }, }, }; @@ -25,6 +28,19 @@ pub(crate) struct InstallArgs { #[arg(long, short, default_value = "auto", value_parser = ["auto", "install", "reinstall", "upgrade"])] pub(crate) mode: String, + /// For Motoko canisters with enhanced orthogonal persistence (EOP), controls whether + /// the canister's main (Wasm) memory is preserved across an upgrade. + /// + /// Only valid with `--mode upgrade` on an EOP canister. + /// + /// - `keep`: preserve main memory — the normal EOP upgrade (the default if this flag + /// is omitted). + /// + /// - `replace`: discard main memory. DANGEROUS: any state not held in `stable` + /// variables is lost. Requires interactive confirmation (or `--yes`). + #[arg(long, value_enum)] + pub(crate) wasm_memory_persistence: Option, + /// Path to the WASM file to install. Uses the build output if not explicitly provided. #[arg(long)] pub(crate) wasm: Option, @@ -32,7 +48,8 @@ pub(crate) struct InstallArgs { #[command(flatten)] pub(crate) args_opt: ArgsOpt, - /// Skip confirmation prompts, including the Candid interface compatibility check. + /// Skip confirmation prompts, including the Candid interface compatibility check and + /// the dangerous-operation prompt for `--wasm-memory-persistence replace`. #[arg(long, short)] pub(crate) yes: bool, @@ -84,6 +101,45 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow // If you add .did support to this code, consider extracting/unifying with the logic from call.rs let init_args_bytes = args.args_opt.resolve_bytes()?; + // Validate --wasm-memory-persistence: only meaningful for upgrades of EOP canisters. + if let Some(persistence) = args.wasm_memory_persistence { + if args.mode != "upgrade" { + bail!( + "--wasm-memory-persistence can only be used with `--mode upgrade` \ + (got `--mode {}`). It has no effect for install/reinstall, and `auto` \ + is ambiguous; pass `--mode upgrade` explicitly.", + args.mode + ); + } + if !is_eop_canister(&agent, &canister_id).await { + bail!( + "--wasm-memory-persistence only applies to Motoko canisters with enhanced \ + orthogonal persistence (EOP). The target canister is not an EOP canister." + ); + } + if persistence == WasmMemoryPersistenceOpt::Replace { + warn!( + "--wasm-memory-persistence=replace will DISCARD the canister's \ + main (Wasm) memory." + ); + warn!( + "Only state held in `stable` variables survives. Heap state is lost \ + and cannot be recovered." + ); + if args.yes { + info!("Proceeding without confirmation (--yes)."); + } else { + let confirmed = Confirm::new() + .with_prompt("Do you want to proceed?") + .default(false) + .interact()?; + if !confirmed { + bail!("Operation cancelled by user"); + } + } + } + } + let canister_display = args.cmd_args.canister.to_string(); let (install_mode, status) = resolve_install_mode_and_status( &agent, @@ -132,6 +188,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow install_mode, status, init_args_bytes.as_deref(), + args.wasm_memory_persistence, ) .await?; diff --git a/crates/icp-cli/src/operations/install.rs b/crates/icp-cli/src/operations/install.rs index 0564f3ed..24f90fd4 100644 --- a/crates/icp-cli/src/operations/install.rs +++ b/crates/icp-cli/src/operations/install.rs @@ -17,6 +17,33 @@ use super::misc::fetch_canister_metadata; use super::proxy::UpdateOrProxyError; use super::proxy_management; +/// CLI-facing choice for `wasm_memory_persistence` on EOP upgrades. +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum WasmMemoryPersistenceOpt { + /// Preserve canister main memory across upgrade (normal EOP upgrade). + Keep, + /// Discard canister main memory; only `stable` variables survive. + /// Dangerous — heap state is lost. + Replace, +} + +impl WasmMemoryPersistenceOpt { + fn to_ic(self) -> WasmMemoryPersistence { + match self { + WasmMemoryPersistenceOpt::Keep => WasmMemoryPersistence::Keep, + WasmMemoryPersistenceOpt::Replace => WasmMemoryPersistence::Replace, + } + } +} + +/// Returns true if the canister exposes the `enhanced-orthogonal-persistence` +/// custom-section metadata (i.e. it is a Motoko EOP canister). +pub(crate) async fn is_eop_canister(agent: &Agent, canister_id: &Principal) -> bool { + fetch_canister_metadata(agent, *canister_id, "enhanced-orthogonal-persistence") + .await + .is_some() +} + #[derive(Debug, Snafu)] pub enum InstallOperationError { #[snafu(display("Could not find build artifact for canister '{canister_name}'"))] @@ -100,18 +127,24 @@ pub(crate) async fn install_canister( mode: CanisterInstallMode, status: CanisterStatusType, init_args: Option<&[u8]>, + wasm_memory_persistence: Option, ) -> Result<(), InstallOperationError> { let mode = match mode { CanisterInstallMode::Upgrade(_) => { - // if this is a motoko canister using EOP - // we need to set additional options - if fetch_canister_metadata(agent, *canister_id, "enhanced-orthogonal-persistence") - .await - .is_some() - { + // if this is a motoko canister using EOP we need to set additional options. + // If the caller supplied an explicit override, trust it (the CLI layer has + // already validated that it's an EOP canister); otherwise auto-detect and + // default to Keep. + let persistence = match wasm_memory_persistence { + Some(opt) => Some(opt.to_ic()), + None => is_eop_canister(agent, canister_id) + .await + .then_some(WasmMemoryPersistence::Keep), + }; + if let Some(persistence) = persistence { CanisterInstallMode::Upgrade(Some(UpgradeFlags { skip_pre_upgrade: None, - wasm_memory_persistence: Some(WasmMemoryPersistence::Keep), + wasm_memory_persistence: Some(persistence), })) } else { mode @@ -361,6 +394,7 @@ pub(crate) async fn install_many( mode, status, init_args.as_deref(), + None, ) .await } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 67dcd9c0..9693dfd1 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -292,6 +292,20 @@ Install a built WASM to a canister on a network Possible values: `auto`, `install`, `reinstall`, `upgrade` +* `--wasm-memory-persistence ` — For Motoko canisters with enhanced orthogonal persistence (EOP), controls whether the canister's main (Wasm) memory is preserved across an upgrade. + + Only valid with `--mode upgrade` on an EOP canister. + + - `keep`: preserve main memory — the normal EOP upgrade (the default if this flag is omitted). + + - `replace`: discard main memory. DANGEROUS: any state not held in `stable` variables is lost. Requires interactive confirmation. + + Possible values: + - `keep`: + Preserve canister main memory across upgrade (normal EOP upgrade) + - `replace`: + Discard canister main memory; only `stable` variables survive. Dangerous — heap state is lost + * `--wasm ` — Path to the WASM file to install. Uses the build output if not explicitly provided * `--args ` — Inline arguments, interpreted per `--args-format` (Candid by default) * `--args-file ` — Path to a file containing arguments