From e01f3f1e3dac1ea42eb132cf628565369dac4f5e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 13:43:19 -0400 Subject: [PATCH 1/9] feat: add --args/--args-file/--args-format flags to `icp deploy` Extract shared ArgsOpt struct and load_args helper into args.rs to deduplicate init-args handling across canister install, canister call, and deploy. CLI flags take priority over manifest init_args when deploying a single canister. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-cli/src/commands/args.rs | 80 +++++++++++++++++++ crates/icp-cli/src/commands/canister/call.rs | 63 +++++++-------- .../icp-cli/src/commands/canister/install.rs | 51 ++---------- crates/icp-cli/src/commands/deploy.rs | 26 ++++-- 4 files changed, 132 insertions(+), 88 deletions(-) diff --git a/crates/icp-cli/src/commands/args.rs b/crates/icp-cli/src/commands/args.rs index 62836fd9..3282f365 100644 --- a/crates/icp-cli/src/commands/args.rs +++ b/crates/icp-cli/src/commands/args.rs @@ -1,11 +1,15 @@ use std::fmt::Display; use std::str::FromStr; +use anyhow::{Context as _, bail}; use candid::Principal; use clap::Args; use ic_ledger_types::AccountIdentifier; use icp::context::{CanisterSelection, EnvironmentSelection, NetworkSelection}; use icp::identity::IdentitySelection; +use icp::manifest::InitArgsFormat; +use icp::prelude::PathBuf; +use icp::{InitArgs, fs}; use icrc_ledger_types::icrc1::account::Account; use crate::options::{EnvironmentOpt, IdentityOpt, NetworkOpt}; @@ -204,6 +208,82 @@ impl Display for FlexibleAccountId { } } +/// Grouped flags for specifying canister install arguments, shared by `canister install`, and `deploy`. +#[derive(Args, Clone, Debug, Default)] +pub(crate) struct ArgsOpt { + /// Inline initialization arguments, interpreted per `--args-format` (Candid by default). + #[arg(long, conflicts_with = "args_file")] + pub(crate) args: Option, + + /// Path to a file containing initialization arguments. + #[arg(long, conflicts_with = "args")] + pub(crate) args_file: Option, + + /// Format of the initialization arguments. + #[arg(long, default_value = "candid")] + pub(crate) args_format: InitArgsFormat, +} + +impl ArgsOpt { + /// Returns whether any args were provided via CLI flags. + pub(crate) fn is_some(&self) -> bool { + self.args.is_some() || self.args_file.is_some() + } + + /// Resolve CLI args to raw bytes, reading files as needed. + /// Returns `None` if no args were provided. + pub(crate) fn resolve_bytes(&self) -> Result>, anyhow::Error> { + load_args( + self.args.as_deref(), + self.args_file.as_ref(), + &self.args_format, + "--args", + )? + .as_ref() + .map(|ia| ia.to_bytes().context("failed to encode args")) + .transpose() + } +} + +/// Load args from an inline value or a file, returning the intermediate [`InitArgs`] +/// representation. Returns `None` if neither was provided. +/// +/// `inline_arg_name` is used in the error message when `--args-format bin` is given +/// with an inline value (e.g. `"--args"` or `"a positional argument"`). +pub(crate) fn load_args( + inline_value: Option<&str>, + args_file: Option<&PathBuf>, + args_format: &InitArgsFormat, + inline_arg_name: &str, +) -> Result, anyhow::Error> { + match (inline_value, args_file) { + (Some(value), None) => { + if *args_format == InitArgsFormat::Bin { + bail!("--args-format bin requires --args-file, not {inline_arg_name}"); + } + Ok(Some(InitArgs::Text { + content: value.to_owned(), + format: args_format.clone(), + })) + } + (None, Some(file_path)) => Ok(Some(match args_format { + InitArgsFormat::Bin => { + let bytes = fs::read(file_path).context("failed to read args file")?; + InitArgs::Binary(bytes) + } + fmt => { + let content = fs::read_to_string(file_path).context("failed to read args file")?; + InitArgs::Text { + content: content.trim().to_owned(), + format: fmt.clone(), + } + } + })), + (None, None) => Ok(None), + (Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"), + } +} + #[cfg(test)] mod tests { use candid::Principal; diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index 50df8d83..0101a4d8 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -8,7 +8,6 @@ use clap::{Args, ValueEnum}; use dialoguer::console::Term; use ic_agent::Agent; use icp::context::Context; -use icp::fs; use icp::manifest::InitArgsFormat; use icp::parsers::CyclesAmount; use icp::prelude::*; @@ -17,7 +16,8 @@ use std::io::{self, Write}; use tracing::{error, warn}; use crate::{ - commands::args, operations::misc::fetch_canister_metadata, + commands::args::{self, load_args}, + operations::misc::fetch_canister_metadata, operations::proxy::update_or_proxy_raw, }; @@ -134,41 +134,32 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E Bytes(Vec), } - let resolved_args = match (&args.args, &args.args_file) { - (Some(value), None) => { - if args.args_format == InitArgsFormat::Bin { - bail!("--args-format bin requires --args-file, not a positional argument"); - } - Some(match args.args_format { - InitArgsFormat::Candid => ResolvedArgs::Candid( - parse_idl_args(value).context("failed to parse Candid arguments")?, - ), - InitArgsFormat::Hex => ResolvedArgs::Bytes( - hex::decode(value).context("failed to decode hex arguments")?, - ), - InitArgsFormat::Bin => unreachable!(), - }) + let resolved_args = match load_args( + args.args.as_deref(), + args.args_file.as_ref(), + &args.args_format, + "a positional argument", + )? { + None => None, + Some(icp::InitArgs::Binary(bytes)) => Some(ResolvedArgs::Bytes(bytes)), + Some(icp::InitArgs::Text { + content, + format: InitArgsFormat::Candid, + }) => Some(ResolvedArgs::Candid( + parse_idl_args(&content).context("failed to parse Candid arguments")?, + )), + Some(icp::InitArgs::Text { + content, + format: InitArgsFormat::Hex, + }) => Some(ResolvedArgs::Bytes( + hex::decode(&content).context("failed to decode hex arguments")?, + )), + Some(icp::InitArgs::Text { + format: InitArgsFormat::Bin, + .. + }) => { + unreachable!("load_args rejects bin format for inline values") } - (None, Some(file_path)) => Some(match args.args_format { - InitArgsFormat::Bin => { - let bytes = fs::read(file_path).context("failed to read args file")?; - ResolvedArgs::Bytes(bytes) - } - InitArgsFormat::Hex => { - let content = fs::read_to_string(file_path).context("failed to read args file")?; - ResolvedArgs::Bytes( - hex::decode(content.trim()).context("failed to decode hex from file")?, - ) - } - InitArgsFormat::Candid => { - let content = fs::read_to_string(file_path).context("failed to read args file")?; - ResolvedArgs::Candid( - parse_idl_args(content.trim()).context("failed to parse Candid from file")?, - ) - } - }), - (None, None) => None, - (Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"), }; let arg_bytes = match (&declared_method, resolved_args) { diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index 2b45f713..69b74ace 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -6,13 +6,12 @@ use clap::Args; use dialoguer::Confirm; use ic_management_canister_types::CanisterInstallMode; use icp::context::{CanisterSelection, Context}; -use icp::manifest::InitArgsFormat; +use icp::fs; use icp::prelude::*; -use icp::{InitArgs, fs}; use tracing::{info, warn}; use crate::{ - commands::args, + commands::args::{self, ArgsOpt}, operations::{ candid_compat::{CandidCompatibility, check_candid_compatibility}, install::{install_canister, resolve_install_mode_and_status}, @@ -30,17 +29,8 @@ pub(crate) struct InstallArgs { #[arg(long)] pub(crate) wasm: Option, - /// Inline initialization arguments, interpreted per `--args-format` (Candid by default). - #[arg(long, conflicts_with = "args_file")] - pub(crate) args: Option, - - /// Path to a file containing initialization arguments. - #[arg(long, conflicts_with = "args")] - pub(crate) args_file: Option, - - /// Format of the initialization arguments. - #[arg(long, default_value = "candid")] - pub(crate) args_format: InitArgsFormat, + #[command(flatten)] + pub(crate) args_opt: ArgsOpt, /// Skip confirmation prompts, including the Candid interface compatibility check. #[arg(long, short)] @@ -92,38 +82,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow .await?; // If you add .did support to this code, consider extracting/unifying with the logic from call.rs - let init_args = match (&args.args, &args.args_file) { - (Some(value), None) => { - if args.args_format == InitArgsFormat::Bin { - bail!("--args-format bin requires --args-file, not --args"); - } - Some(InitArgs::Text { - content: value.clone(), - format: args.args_format.clone(), - }) - } - (None, Some(file_path)) => Some(match args.args_format { - InitArgsFormat::Bin => { - let bytes = fs::read(file_path).context("failed to read init args file")?; - InitArgs::Binary(bytes) - } - ref fmt => { - let content = - fs::read_to_string(file_path).context("failed to read init args file")?; - InitArgs::Text { - content: content.trim().to_owned(), - format: fmt.clone(), - } - } - }), - (None, None) => None, - (Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"), - }; - - let init_args_bytes = init_args - .as_ref() - .map(|ia| ia.to_bytes().context("failed to encode init args")) - .transpose()?; + let init_args_bytes = args.args_opt.resolve_bytes()?; let canister_display = args.cmd_args.canister.to_string(); let (install_mode, status) = resolve_install_mode_and_status( diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index 443724b0..28923f95 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -14,7 +14,7 @@ use serde::Serialize; use tracing::info; use crate::{ - commands::canister::create, + commands::{args::ArgsOpt, canister::create}, operations::{ binding_env_vars::set_binding_env_vars_many, build::build_many_with_progress_bar, @@ -68,6 +68,11 @@ pub(crate) struct DeployArgs { /// Output command results as JSON #[arg(long)] pub(crate) json: bool, + + /// Initialization arguments to pass to the canister on install. + /// Only valid when deploying a single canister. Takes priority over `init_args` in the manifest. + #[command(flatten)] + pub(crate) args_opt: ArgsOpt, } pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow::Error> { @@ -89,6 +94,10 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: return Ok(()); } + if args.args_opt.is_some() && cnames.len() != 1 { + anyhow::bail!("--args and --args-file can only be used when deploying a single canister"); + } + let canisters_to_build = try_join_all( cnames .iter() @@ -256,11 +265,16 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: let (_canister_path, canister_info) = env.get_canister_info(name).map_err(|e| anyhow!(e))?; - let init_args_bytes = canister_info - .init_args - .as_ref() - .map(|ia| ia.to_bytes()) - .transpose()?; + // CLI --args/--args-file take priority over manifest init_args + let init_args_bytes = if args.args_opt.is_some() { + args.args_opt.resolve_bytes()? + } else { + canister_info + .init_args + .as_ref() + .map(|ia| ia.to_bytes()) + .transpose()? + }; Ok::<_, anyhow::Error>((name.clone(), cid, mode, status, init_args_bytes)) } From a0e3ec296bcbbde66d8c72c452fc35692c719a3e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 13:53:10 -0400 Subject: [PATCH 2/9] refactor: rename InitArgsFormat to ArgsFormat The type is used for both canister install and call args, not just init args. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- crates/icp-cli/src/commands/args.rs | 10 +++++----- crates/icp-cli/src/commands/canister/call.rs | 12 ++++++------ crates/icp/src/lib.rs | 13 +++++-------- crates/icp/src/manifest/canister.rs | 14 +++++++------- crates/icp/src/manifest/mod.rs | 2 +- crates/icp/src/manifest/project.rs | 4 ++-- crates/icp/src/project.rs | 8 ++++---- 7 files changed, 30 insertions(+), 33 deletions(-) diff --git a/crates/icp-cli/src/commands/args.rs b/crates/icp-cli/src/commands/args.rs index 3282f365..563c60c6 100644 --- a/crates/icp-cli/src/commands/args.rs +++ b/crates/icp-cli/src/commands/args.rs @@ -7,7 +7,7 @@ use clap::Args; use ic_ledger_types::AccountIdentifier; use icp::context::{CanisterSelection, EnvironmentSelection, NetworkSelection}; use icp::identity::IdentitySelection; -use icp::manifest::InitArgsFormat; +use icp::manifest::ArgsFormat; use icp::prelude::PathBuf; use icp::{InitArgs, fs}; use icrc_ledger_types::icrc1::account::Account; @@ -221,7 +221,7 @@ pub(crate) struct ArgsOpt { /// Format of the initialization arguments. #[arg(long, default_value = "candid")] - pub(crate) args_format: InitArgsFormat, + pub(crate) args_format: ArgsFormat, } impl ArgsOpt { @@ -253,12 +253,12 @@ impl ArgsOpt { pub(crate) fn load_args( inline_value: Option<&str>, args_file: Option<&PathBuf>, - args_format: &InitArgsFormat, + args_format: &ArgsFormat, inline_arg_name: &str, ) -> Result, anyhow::Error> { match (inline_value, args_file) { (Some(value), None) => { - if *args_format == InitArgsFormat::Bin { + if *args_format == ArgsFormat::Bin { bail!("--args-format bin requires --args-file, not {inline_arg_name}"); } Ok(Some(InitArgs::Text { @@ -267,7 +267,7 @@ pub(crate) fn load_args( })) } (None, Some(file_path)) => Ok(Some(match args_format { - InitArgsFormat::Bin => { + ArgsFormat::Bin => { let bytes = fs::read(file_path).context("failed to read args file")?; InitArgs::Binary(bytes) } diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index 0101a4d8..c06f58ca 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -8,7 +8,7 @@ use clap::{Args, ValueEnum}; use dialoguer::console::Term; use ic_agent::Agent; use icp::context::Context; -use icp::manifest::InitArgsFormat; +use icp::manifest::ArgsFormat; use icp::parsers::CyclesAmount; use icp::prelude::*; use serde::Serialize; @@ -56,7 +56,7 @@ pub(crate) struct CallArgs { /// Format of the call arguments. #[arg(long, default_value = "candid")] - pub(crate) args_format: InitArgsFormat, + pub(crate) args_format: ArgsFormat, /// Principal of a proxy canister to route the call through. /// @@ -144,18 +144,18 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E Some(icp::InitArgs::Binary(bytes)) => Some(ResolvedArgs::Bytes(bytes)), Some(icp::InitArgs::Text { content, - format: InitArgsFormat::Candid, + format: ArgsFormat::Candid, }) => Some(ResolvedArgs::Candid( parse_idl_args(&content).context("failed to parse Candid arguments")?, )), Some(icp::InitArgs::Text { content, - format: InitArgsFormat::Hex, + format: ArgsFormat::Hex, }) => Some(ResolvedArgs::Bytes( hex::decode(&content).context("failed to decode hex arguments")?, )), Some(icp::InitArgs::Text { - format: InitArgsFormat::Bin, + format: ArgsFormat::Bin, .. }) => { unreachable!("load_args rejects bin format for inline values") @@ -163,7 +163,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E }; let arg_bytes = match (&declared_method, resolved_args) { - (_, None) if args.args_format != InitArgsFormat::Candid => { + (_, None) if args.args_format != ArgsFormat::Candid => { bail!("arguments must be provided when --args-format is not candid"); } (None, None) => bail!( diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index 97adf3e8..b023cc83 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -11,7 +11,7 @@ use candid_parser::parse_idl_args; use crate::{ canister::{Settings, recipe::Resolve}, manifest::{ - InitArgsFormat, LoadManifestFromPathError, PROJECT_MANIFEST, ProjectRootLocate, + ArgsFormat, LoadManifestFromPathError, PROJECT_MANIFEST, ProjectRootLocate, ProjectRootLocateError, canister::{BuildSteps, SyncSteps}, load_manifest_from_path, @@ -46,10 +46,7 @@ const DATA_DIR: &str = "data"; #[derive(Clone, Debug, PartialEq, Serialize)] pub enum InitArgs { /// Text content (inline or loaded from file). Format is always known. - Text { - content: String, - format: InitArgsFormat, - }, + Text { content: String, format: ArgsFormat }, /// Raw binary bytes (from a file with `format: bin`). Used directly. Binary(Vec), } @@ -72,12 +69,12 @@ impl InitArgs { match self { InitArgs::Binary(bytes) => Ok(bytes.clone()), InitArgs::Text { content, format } => match format { - InitArgsFormat::Hex => hex::decode(content.trim()).context(HexDecodeSnafu), - InitArgsFormat::Candid => { + ArgsFormat::Hex => hex::decode(content.trim()).context(HexDecodeSnafu), + ArgsFormat::Candid => { let args = parse_idl_args(content.trim()).context(CandidParseSnafu)?; args.to_bytes().context(CandidEncodeSnafu) } - InitArgsFormat::Bin => { + ArgsFormat::Bin => { unreachable!("binary format cannot appear in InitArgs::Text") } }, diff --git a/crates/icp/src/manifest/canister.rs b/crates/icp/src/manifest/canister.rs index 9e0fdcbb..6fafaf72 100644 --- a/crates/icp/src/manifest/canister.rs +++ b/crates/icp/src/manifest/canister.rs @@ -7,11 +7,11 @@ use crate::canister::Settings; use super::{adapter, recipe::Recipe, serde_helpers::non_empty_vec}; -/// Format specifier for init args content. +/// Format specifier for canister call/install args content. #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, JsonSchema)] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[serde(rename_all = "lowercase")] -pub enum InitArgsFormat { +pub enum ArgsFormat { /// Hex-encoded bytes Hex, /// Candid text format @@ -48,13 +48,13 @@ pub enum ManifestInitArgs { Path { path: String, #[serde(default)] - format: InitArgsFormat, + format: ArgsFormat, }, /// Inline value with explicit format. Value { value: String, #[serde(default)] - format: InitArgsFormat, + format: ArgsFormat, }, } @@ -767,7 +767,7 @@ mod tests { ia, ManifestInitArgs::Path { path: "./args.bin".to_string(), - format: InitArgsFormat::Bin, + format: ArgsFormat::Bin, } ); } @@ -783,7 +783,7 @@ mod tests { ia, ManifestInitArgs::Value { value: "(42)".to_string(), - format: InitArgsFormat::Candid, + format: ArgsFormat::Candid, } ); } @@ -798,7 +798,7 @@ mod tests { ia, ManifestInitArgs::Value { value: "(42)".to_string(), - format: InitArgsFormat::Candid, + format: ArgsFormat::Candid, } ); } diff --git a/crates/icp/src/manifest/mod.rs b/crates/icp/src/manifest/mod.rs index 83bed7cc..d21a867d 100644 --- a/crates/icp/src/manifest/mod.rs +++ b/crates/icp/src/manifest/mod.rs @@ -16,7 +16,7 @@ pub(crate) mod recipe; pub(crate) mod serde_helpers; pub use { - canister::{CanisterManifest, InitArgsFormat, ManifestInitArgs}, + canister::{ArgsFormat, CanisterManifest, ManifestInitArgs}, environment::EnvironmentManifest, network::NetworkManifest, project::ProjectManifest, diff --git a/crates/icp/src/manifest/project.rs b/crates/icp/src/manifest/project.rs index 7559f28e..b158cbbd 100644 --- a/crates/icp/src/manifest/project.rs +++ b/crates/icp/src/manifest/project.rs @@ -30,7 +30,7 @@ mod tests { use crate::{ canister::Settings, manifest::{ - InitArgsFormat, ManifestInitArgs, + ArgsFormat, ManifestInitArgs, adapter::script, canister::{BuildStep, BuildSteps, Instructions}, environment::CanisterSelection, @@ -459,7 +459,7 @@ mod tests { "canister-2".to_string(), ManifestInitArgs::Value { value: "4449444c0000".to_string(), - format: InitArgsFormat::Hex, + format: ArgsFormat::Hex, }, ), ])), diff --git a/crates/icp/src/project.rs b/crates/icp/src/project.rs index 60171bae..1e48d133 100644 --- a/crates/icp/src/project.rs +++ b/crates/icp/src/project.rs @@ -8,7 +8,7 @@ use crate::{ context::IC_ROOT_KEY, fs, manifest::{ - CANISTER_MANIFEST, CanisterManifest, EnvironmentManifest, InitArgsFormat, Item, + ArgsFormat, CANISTER_MANIFEST, CanisterManifest, EnvironmentManifest, Item, LoadManifestFromPathError, ManifestInitArgs, NetworkManifest, ProjectManifest, ProjectRootLocateError, canister::{Instructions, SyncSteps}, @@ -108,12 +108,12 @@ fn resolve_manifest_init_args( match manifest_init_args { ManifestInitArgs::String(content) => Ok(InitArgs::Text { content: content.trim().to_owned(), - format: InitArgsFormat::Candid, + format: ArgsFormat::Candid, }), ManifestInitArgs::Path { path, format } => { let file_path = base_path.join(path); match format { - InitArgsFormat::Bin => { + ArgsFormat::Bin => { let bytes = fs::read(&file_path).context(ReadInitArgsSnafu { canister })?; Ok(InitArgs::Binary(bytes)) } @@ -128,7 +128,7 @@ fn resolve_manifest_init_args( } } ManifestInitArgs::Value { value, format } => match format { - InitArgsFormat::Bin => BinFormatInlineContentSnafu { canister }.fail(), + ArgsFormat::Bin => BinFormatInlineContentSnafu { canister }.fail(), fmt => Ok(InitArgs::Text { content: value.trim().to_owned(), format: fmt.clone(), From e5a04a82bebe49edc4265c9c974096c66003b736 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 13:54:11 -0400 Subject: [PATCH 3/9] chore: regenerate CLI docs and config schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates InitArgsFormat → ArgsFormat in JSON schemas and CLI reference docs. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- docs/reference/cli.md | 14 ++++++++ docs/schemas/canister-yaml-schema.json | 44 +++++++++++------------ docs/schemas/environment-yaml-schema.json | 44 +++++++++++------------ docs/schemas/icp-yaml-schema.json | 44 +++++++++++------------ 4 files changed, 80 insertions(+), 66 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 62fe873e..13075bfd 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -839,6 +839,20 @@ Deploy a project to an environment * `--identity ` — The user identity to run this command as * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--json` — Output command results as JSON +* `--args ` — Inline initialization arguments, interpreted per `--args-format` (Candid by default) +* `--args-file ` — Path to a file containing initialization arguments +* `--args-format ` — Format of the initialization arguments + + Default value: `candid` + + Possible values: + - `hex`: + Hex-encoded bytes + - `candid`: + Candid text format + - `bin`: + Raw binary (only valid for file references) + diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index 5e4982b3..ba1ac963 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -89,6 +89,26 @@ ], "type": "object" }, + "ArgsFormat": { + "description": "Format specifier for canister call/install args content.", + "oneOf": [ + { + "const": "hex", + "description": "Hex-encoded bytes", + "type": "string" + }, + { + "const": "candid", + "description": "Candid text format", + "type": "string" + }, + { + "const": "bin", + "description": "Raw binary (only valid for file references)", + "type": "string" + } + ] + }, "BuildStep": { "description": "Identifies the type of adapter used to build the canister,\nalong with its configuration.\n\nThe adapter type is specified via the `type` field in the YAML file.\nFor example:\n\n```yaml\ntype: script\ncommand: do_something.sh\n```", "oneOf": [ @@ -163,26 +183,6 @@ ], "description": "A duration in seconds.\n\nDeserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w)\nand optional underscore separators.\n\nSuffixes (case-insensitive):\n- `s` — seconds\n- `m` — minutes (×60)\n- `h` — hours (×3600)\n- `d` — days (×86400)\n- `w` — weeks (×604800)\n\nA bare number without suffix is treated as seconds." }, - "InitArgsFormat": { - "description": "Format specifier for init args content.", - "oneOf": [ - { - "const": "hex", - "description": "Hex-encoded bytes", - "type": "string" - }, - { - "const": "candid", - "description": "Candid text format", - "type": "string" - }, - { - "const": "bin", - "description": "Raw binary (only valid for file references)", - "type": "string" - } - ] - }, "LocalSource": { "properties": { "path": { @@ -236,7 +236,7 @@ "description": "File reference with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "path": { @@ -252,7 +252,7 @@ "description": "Inline value with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "value": { diff --git a/docs/schemas/environment-yaml-schema.json b/docs/schemas/environment-yaml-schema.json index 5dfceca4..82595812 100644 --- a/docs/schemas/environment-yaml-schema.json +++ b/docs/schemas/environment-yaml-schema.json @@ -1,5 +1,25 @@ { "$defs": { + "ArgsFormat": { + "description": "Format specifier for canister call/install args content.", + "oneOf": [ + { + "const": "hex", + "description": "Hex-encoded bytes", + "type": "string" + }, + { + "const": "candid", + "description": "Candid text format", + "type": "string" + }, + { + "const": "bin", + "description": "Raw binary (only valid for file references)", + "type": "string" + } + ] + }, "CyclesAmount": { "anyOf": [ { @@ -26,26 +46,6 @@ ], "description": "A duration in seconds.\n\nDeserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w)\nand optional underscore separators.\n\nSuffixes (case-insensitive):\n- `s` — seconds\n- `m` — minutes (×60)\n- `h` — hours (×3600)\n- `d` — days (×86400)\n- `w` — weeks (×604800)\n\nA bare number without suffix is treated as seconds." }, - "InitArgsFormat": { - "description": "Format specifier for init args content.", - "oneOf": [ - { - "const": "hex", - "description": "Hex-encoded bytes", - "type": "string" - }, - { - "const": "candid", - "description": "Candid text format", - "type": "string" - }, - { - "const": "bin", - "description": "Raw binary (only valid for file references)", - "type": "string" - } - ] - }, "LogVisibility": { "description": "Controls who can read canister logs.", "oneOf": [ @@ -87,7 +87,7 @@ "description": "File reference with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "path": { @@ -103,7 +103,7 @@ "description": "Inline value with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "value": { diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index 63a012e9..efd5d1bc 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -89,6 +89,26 @@ ], "type": "object" }, + "ArgsFormat": { + "description": "Format specifier for canister call/install args content.", + "oneOf": [ + { + "const": "hex", + "description": "Hex-encoded bytes", + "type": "string" + }, + { + "const": "candid", + "description": "Candid text format", + "type": "string" + }, + { + "const": "bin", + "description": "Raw binary (only valid for file references)", + "type": "string" + } + ] + }, "BuildStep": { "description": "Identifies the type of adapter used to build the canister,\nalong with its configuration.\n\nThe adapter type is specified via the `type` field in the YAML file.\nFor example:\n\n```yaml\ntype: script\ncommand: do_something.sh\n```", "oneOf": [ @@ -374,26 +394,6 @@ }, "type": "object" }, - "InitArgsFormat": { - "description": "Format specifier for init args content.", - "oneOf": [ - { - "const": "hex", - "description": "Hex-encoded bytes", - "type": "string" - }, - { - "const": "candid", - "description": "Candid text format", - "type": "string" - }, - { - "const": "bin", - "description": "Raw binary (only valid for file references)", - "type": "string" - } - ] - }, "Item": { "anyOf": [ { @@ -680,7 +680,7 @@ "description": "File reference with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "path": { @@ -696,7 +696,7 @@ "description": "Inline value with explicit format.", "properties": { "format": { - "$ref": "#/$defs/InitArgsFormat", + "$ref": "#/$defs/ArgsFormat", "default": "candid" }, "value": { From 5809cbfc96a1a8c6ffd89d8171078606dad26e11 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 14:04:58 -0400 Subject: [PATCH 4/9] test: add tests for `icp deploy --args*` flags Co-Authored-By: Claude Sonnet 4.6 (1M context) --- crates/icp-cli/tests/deploy_tests.rs | 218 +++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index 327b1cc4..9ec78d31 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -685,6 +685,224 @@ async fn deploy_cloud_engine() { .stdout(eq("(\"Hello, test!\")").trim()); } +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_inline_args_candid() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args", + "(opt (42 : nat8))", + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"42\")").trim()); +} + +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_args_file() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + write_string(&project_dir.join("args.txt"), "(opt (42 : nat8))") + .expect("failed to write args file"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args-file", + "args.txt", + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"42\")").trim()); +} + +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_args_hex_format() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + // Hex encoding of "(opt 100 : opt nat8)" — didc encode '(opt 100 : opt nat8)' + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args", + "4449444c016e7b01000164", + "--args-format", + "hex", + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"100\")").trim()); +} + +#[test] +fn deploy_with_args_multiple_canisters_fails() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + let pm = indoc! {r#" + canisters: + - name: canister-a + build: + steps: + - type: script + command: echo hi + - name: canister-b + build: + steps: + - type: script + command: echo hi + "#}; + + write_string(&project_dir.join("icp.yaml"), pm).expect("failed to write project manifest"); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "canister-a", + "canister-b", + "--subnet", + common::SUBNET_ID, + "--args", + "()", + ]) + .assert() + .failure() + .stderr(contains( + "--args and --args-file can only be used when deploying a single canister", + )); +} + #[tokio::test] async fn deploy_through_proxy() { let ctx = TestContext::new(); From 91aea5ffed79c6473f9070bb7ac4772de7755a71 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 14:05:13 -0400 Subject: [PATCH 5/9] chore: update changelog for deploy --args* flags Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 742ed55d..17193360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased * feat: Add `--proxy` to `icp canister` subcommands and `icp deploy` to route management canister calls through a proxy canister +* feat: Add `--args`, `--args-file`, and `--args-format` flags to `icp deploy` to pass initialization arguments at the command line, overriding `init_args` in the manifest # v0.2.2 From 49fd74549d2b1a1c7281887a734e5a85ab483cc4 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 14:05:49 -0400 Subject: [PATCH 6/9] chore: tweak changelog wording for deploy --args* flags Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17193360..6667db8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Unreleased * feat: Add `--proxy` to `icp canister` subcommands and `icp deploy` to route management canister calls through a proxy canister -* feat: Add `--args`, `--args-file`, and `--args-format` flags to `icp deploy` to pass initialization arguments at the command line, overriding `init_args` in the manifest +* feat: Add `--args`, `--args-file`, and `--args-format` flags to `icp deploy` to pass install arguments at the command line, overriding `init_args` in the manifest # v0.2.2 From 24f858b7f9299d7bc9bc62801d13e9bafff32e93 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 14:43:00 -0400 Subject: [PATCH 7/9] docs: improve deploy command help and remove 'initialization' from args field descriptions Co-Authored-By: Claude Sonnet 4.6 (1M context) --- crates/icp-cli/src/commands/args.rs | 6 +++--- crates/icp-cli/src/commands/deploy.rs | 15 ++++++++++++++- docs/reference/cli.md | 25 +++++++++++++++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/icp-cli/src/commands/args.rs b/crates/icp-cli/src/commands/args.rs index 563c60c6..82f66e8c 100644 --- a/crates/icp-cli/src/commands/args.rs +++ b/crates/icp-cli/src/commands/args.rs @@ -211,15 +211,15 @@ impl Display for FlexibleAccountId { /// Grouped flags for specifying canister install arguments, shared by `canister install`, and `deploy`. #[derive(Args, Clone, Debug, Default)] pub(crate) struct ArgsOpt { - /// Inline initialization arguments, interpreted per `--args-format` (Candid by default). + /// Inline arguments, interpreted per `--args-format` (Candid by default). #[arg(long, conflicts_with = "args_file")] pub(crate) args: Option, - /// Path to a file containing initialization arguments. + /// Path to a file containing arguments. #[arg(long, conflicts_with = "args")] pub(crate) args_file: Option, - /// Format of the initialization arguments. + /// Format of the arguments. #[arg(long, default_value = "candid")] pub(crate) args_format: ArgsFormat, } diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index 28923f95..7ba692eb 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -30,6 +30,19 @@ use crate::{ /// Deploy a project to an environment #[derive(Args, Debug)] +#[command(after_long_help = "\ +When deploying a single canister, you can pass arguments to the install call +using --args or --args-file: + + # Pass inline Candid arguments + icp deploy my_canister --args '(42 : nat)' + + # Pass arguments from a file + icp deploy my_canister --args-file ./args.did + + # Pass raw bytes (hex-encoded) + icp deploy my_canister --args-file ./args.bin --args-format raw +")] pub(crate) struct DeployArgs { /// Canister names pub(crate) names: Vec, @@ -69,7 +82,7 @@ pub(crate) struct DeployArgs { #[arg(long)] pub(crate) json: bool, - /// Initialization arguments to pass to the canister on install. + /// Arguments to pass to the canister on install. /// Only valid when deploying a single canister. Takes priority over `init_args` in the manifest. #[command(flatten)] pub(crate) args_opt: ArgsOpt, diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 13075bfd..e8ff464f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -289,9 +289,9 @@ Install a built WASM to a canister on a network Possible values: `auto`, `install`, `reinstall`, `upgrade` * `--wasm ` — Path to the WASM file to install. Uses the build output if not explicitly provided -* `--args ` — Inline initialization arguments, interpreted per `--args-format` (Candid by default) -* `--args-file ` — Path to a file containing initialization arguments -* `--args-format ` — Format of the initialization arguments +* `--args ` — Inline arguments, interpreted per `--args-format` (Candid by default) +* `--args-file ` — Path to a file containing arguments +* `--args-format ` — Format of the arguments Default value: `candid` @@ -817,6 +817,19 @@ Deploy a project to an environment **Usage:** `icp deploy [OPTIONS] [NAMES]...` +When deploying a single canister, you can pass arguments to the install call +using --args or --args-file: + + # Pass inline Candid arguments + icp deploy my_canister --args '(42 : nat)' + + # Pass arguments from a file + icp deploy my_canister --args-file ./args.did + + # Pass raw bytes (hex-encoded) + icp deploy my_canister --args-file ./args.bin --args-format raw + + ###### **Arguments:** * `` — Canister names @@ -839,9 +852,9 @@ Deploy a project to an environment * `--identity ` — The user identity to run this command as * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--json` — Output command results as JSON -* `--args ` — Inline initialization arguments, interpreted per `--args-format` (Candid by default) -* `--args-file ` — Path to a file containing initialization arguments -* `--args-format ` — Format of the initialization arguments +* `--args ` — Inline arguments, interpreted per `--args-format` (Candid by default) +* `--args-file ` — Path to a file containing arguments +* `--args-format ` — Format of the arguments Default value: `candid` From 2e5d1f1bdb727de5251e2af88ebcab79e997f70a Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 14:46:36 -0400 Subject: [PATCH 8/9] test: add test for CLI --args overriding manifest init_args Co-Authored-By: Claude Sonnet 4.6 (1M context) --- crates/icp-cli/tests/deploy_tests.rs | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index 9ec78d31..7f8affd0 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -743,6 +743,67 @@ async fn deploy_with_inline_args_candid() { .stdout(eq("(\"42\")").trim()); } +#[cfg(unix)] // moc +#[tokio::test] +async fn deploy_with_args_overrides_manifest_init_args() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + // Manifest sets init_args to 7; CLI --args should override it with 42 + let pm = formatdoc! {r#" + canisters: + - name: my-canister + init_args: "(opt (7 : nat8))" + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--environment", + "random-environment", + "--args", + "(opt (42 : nat8))", + ]) + .assert() + .success(); + + // CLI --args (42) should take priority over manifest init_args (7) + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + ]) + .assert() + .success() + .stdout(eq("(\"42\")").trim()); +} + #[cfg(unix)] // moc #[tokio::test] async fn deploy_with_args_file() { From 75bbdbc3066922c9424dc98beffbff6f25a82ef8 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 8 Apr 2026 15:58:57 -0400 Subject: [PATCH 9/9] docs: fix --args-format example to use 'bin' instead of 'raw' Co-Authored-By: Claude Sonnet 4.6 (1M context) --- crates/icp-cli/src/commands/deploy.rs | 4 ++-- docs/reference/cli.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index 7ba692eb..deb36de8 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -40,8 +40,8 @@ using --args or --args-file: # Pass arguments from a file icp deploy my_canister --args-file ./args.did - # Pass raw bytes (hex-encoded) - icp deploy my_canister --args-file ./args.bin --args-format raw + # Pass raw bytes + icp deploy my_canister --args-file ./args.bin --args-format bin ")] pub(crate) struct DeployArgs { /// Canister names diff --git a/docs/reference/cli.md b/docs/reference/cli.md index e8ff464f..22562467 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -826,8 +826,8 @@ using --args or --args-file: # Pass arguments from a file icp deploy my_canister --args-file ./args.did - # Pass raw bytes (hex-encoded) - icp deploy my_canister --args-file ./args.bin --args-format raw + # Pass raw bytes + icp deploy my_canister --args-file ./args.bin --args-format bin ###### **Arguments:**