Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions crates/icp-cli/src/commands/canister/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
};

Expand All @@ -25,14 +28,28 @@ 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<WasmMemoryPersistenceOpt>,

/// Path to the WASM file to install. Uses the build output if not explicitly provided.
#[arg(long)]
pub(crate) wasm: Option<PathBuf>,

#[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,

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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?;

Expand Down
48 changes: 41 additions & 7 deletions crates/icp-cli/src/operations/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}'"))]
Expand Down Expand Up @@ -100,18 +127,24 @@ pub(crate) async fn install_canister(
mode: CanisterInstallMode,
status: CanisterStatusType,
init_args: Option<&[u8]>,
wasm_memory_persistence: Option<WasmMemoryPersistenceOpt>,
) -> 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
Expand Down Expand Up @@ -361,6 +394,7 @@ pub(crate) async fn install_many(
mode,
status,
init_args.as_deref(),
None,
)
.await
}
Expand Down
14 changes: 14 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,20 @@ Install a built WASM to a canister on a network

Possible values: `auto`, `install`, `reinstall`, `upgrade`

* `--wasm-memory-persistence <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 <WASM>` — Path to the WASM file to install. Uses the build output if not explicitly provided
* `--args <ARGS>` — Inline arguments, interpreted per `--args-format` (Candid by default)
* `--args-file <ARGS_FILE>` — Path to a file containing arguments
Expand Down
Loading