From e9bcf1f3cce9bda14a4570a5e125df9e73d14009 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Mon, 18 May 2026 11:10:55 +0200 Subject: [PATCH 1/3] feat(canister install): add --wasm-memory-persistence flag with replace confirmation --- .../icp-cli/src/commands/canister/install.rs | 64 ++++++++++++++++++- crates/icp-cli/src/operations/install.rs | 58 ++++++++++++++--- docs/reference/cli.md | 14 ++++ examples/icp-motoko/icp.yaml | 17 ++++- examples/icp-motoko/src/main.mo | 18 +++++- 5 files changed, 159 insertions(+), 12 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index f0d84563d..4beec41ae 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -1,5 +1,6 @@ -use anyhow::{Context as _, anyhow}; +use anyhow::{Context as _, anyhow, bail}; use clap::Args; +use dialoguer::Confirm; use icp::context::{CanisterSelection, Context}; use icp::fs; use icp::prelude::*; @@ -7,7 +8,7 @@ use icp::prelude::*; use crate::{ commands::args, operations::{ - install::install_canister, + install::{WasmMemoryPersistenceOpt, install_canister, is_eop_canister}, misc::{ParsedArguments, parse_args}, }, }; @@ -18,6 +19,24 @@ 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, + + /// Skip the interactive confirmation prompt for dangerous operations + /// (currently: `--wasm-memory-persistence replace`). + #[arg(long, short = 'y')] + pub(crate) yes: bool, + /// Path to the WASM file to install. Uses the build output if not explicitly provided. #[arg(long)] pub(crate) wasm: Option, @@ -92,6 +111,46 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow }) .transpose()?; + // 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 { + ctx.term.write_line( + "Warning: --wasm-memory-persistence=replace will DISCARD the canister's \ + main (Wasm) memory.", + )?; + ctx.term.write_line( + "Only state held in `stable` variables survives. Heap state is lost \ + and cannot be recovered.", + )?; + if args.yes { + ctx.term + .write_line("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(); install_canister( &agent, @@ -100,6 +159,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow &wasm, &args.mode, 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 2958b765f..6b8ea95d7 100644 --- a/crates/icp-cli/src/operations/install.rs +++ b/crates/icp-cli/src/operations/install.rs @@ -16,6 +16,33 @@ use crate::progress::{ProgressManager, ProgressManagerSettings}; use super::misc::fetch_canister_metadata; +/// 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}'"))] @@ -45,6 +72,7 @@ pub(crate) async fn install_canister( wasm: &[u8], mode: &str, init_args: Option<&[u8]>, + wasm_memory_persistence: Option, ) -> Result<(), InstallOperationError> { let mgmt = ManagementCanister::create(agent); let install_mode = match mode { @@ -70,15 +98,20 @@ pub(crate) async fn install_canister( let install_mode = match install_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 { install_mode @@ -236,7 +269,16 @@ pub(crate) async fn install_many( } })?; - install_canister(&agent, &cid, &name, &wasm, &mode, init_args.as_deref()).await + install_canister( + &agent, + &cid, + &name, + &wasm, + &mode, + init_args.as_deref(), + None, + ) + .await } }; diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ad5ed5a6a..d3d93b1bf 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -227,6 +227,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 ` — Initialization arguments for the canister. Can be: diff --git a/examples/icp-motoko/icp.yaml b/examples/icp-motoko/icp.yaml index 2da141271..6aa148cb4 100644 --- a/examples/icp-motoko/icp.yaml +++ b/examples/icp-motoko/icp.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0-beta.2/docs/schemas/icp-yaml-schema.json canisters: - - name: my-canister + - name: alice build: steps: - type: script @@ -9,3 +9,18 @@ canisters: - command -v moc >/dev/null 2>&1 || { echo >&2 "moc not found. To install moc, see https://internetcomputer.org/docs/building-apps/getting-started/install \n"; exit 1; } - moc src/main.mo - mv main.wasm "$ICP_WASM_OUTPUT_PATH" + settings: + environment_variables: + test: "alice" + + - name: bob + build: + steps: + - type: script + commands: + - command -v moc >/dev/null 2>&1 || { echo >&2 "moc not found. To install moc, see https://internetcomputer.org/docs/building-apps/getting-started/install \n"; exit 1; } + - moc src/bob.mo + - mv bob.wasm "$ICP_WASM_OUTPUT_PATH" + settings: + environment_variables: + test: "bob" diff --git a/examples/icp-motoko/src/main.mo b/examples/icp-motoko/src/main.mo index 540f30b98..011e52e96 100644 --- a/examples/icp-motoko/src/main.mo +++ b/examples/icp-motoko/src/main.mo @@ -1,5 +1,21 @@ -actor { +import Prim "mo:prim"; + +persistent actor { + + transient var env : Text = ""; + for (v in Prim.envVarNames().vals()) { + env := env # v # ","; + // let val = Prim.envVar(v); + // env := env # v # "=" # val # "\n"; + }; + public query func greet(name : Text) : async Text { return "Hello, " # name # "!"; }; + + public query func getEnv() : async Text { env }; + + public func get(name : Text) : async ?Text { + Prim.envVar(name); + }; }; From 9f12c3b1cda51aef2df964982c0d8d3020d56083 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Mon, 18 May 2026 11:50:44 +0200 Subject: [PATCH 2/3] Revert accidental change in example --- examples/icp-motoko/src/main.mo | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/examples/icp-motoko/src/main.mo b/examples/icp-motoko/src/main.mo index 011e52e96..540f30b98 100644 --- a/examples/icp-motoko/src/main.mo +++ b/examples/icp-motoko/src/main.mo @@ -1,21 +1,5 @@ -import Prim "mo:prim"; - -persistent actor { - - transient var env : Text = ""; - for (v in Prim.envVarNames().vals()) { - env := env # v # ","; - // let val = Prim.envVar(v); - // env := env # v # "=" # val # "\n"; - }; - +actor { public query func greet(name : Text) : async Text { return "Hello, " # name # "!"; }; - - public query func getEnv() : async Text { env }; - - public func get(name : Text) : async ?Text { - Prim.envVar(name); - }; }; From 874a267b9a582d884da3b129793d27884f5c4983 Mon Sep 17 00:00:00 2001 From: Timo Hanke Date: Mon, 18 May 2026 11:52:45 +0200 Subject: [PATCH 3/3] Revert accidental change to icp.yaml (examples) --- examples/icp-motoko/icp.yaml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/examples/icp-motoko/icp.yaml b/examples/icp-motoko/icp.yaml index 5cbbd8288..997b586c9 100644 --- a/examples/icp-motoko/icp.yaml +++ b/examples/icp-motoko/icp.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0/docs/schemas/icp-yaml-schema.json canisters: - - name: alice + - name: my-canister build: steps: - type: script @@ -9,18 +9,3 @@ canisters: - command -v moc >/dev/null 2>&1 || { echo >&2 "moc not found. To install moc, see https://internetcomputer.org/docs/building-apps/getting-started/install \n"; exit 1; } - moc src/main.mo - mv main.wasm "$ICP_WASM_OUTPUT_PATH" - settings: - environment_variables: - test: "alice" - - - name: bob - build: - steps: - - type: script - commands: - - command -v moc >/dev/null 2>&1 || { echo >&2 "moc not found. To install moc, see https://internetcomputer.org/docs/building-apps/getting-started/install \n"; exit 1; } - - moc src/bob.mo - - mv bob.wasm "$ICP_WASM_OUTPUT_PATH" - settings: - environment_variables: - test: "bob"