diff --git a/CHANGELOG.md b/CHANGELOG.md index 742ed55d..6667db8e 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 install arguments at the command line, overriding `init_args` in the manifest # v0.2.2 diff --git a/crates/icp-cli/src/commands/args.rs b/crates/icp-cli/src/commands/args.rs index 62836fd9..82f66e8c 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::ArgsFormat; +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 arguments, interpreted per `--args-format` (Candid by default). + #[arg(long, conflicts_with = "args_file")] + pub(crate) args: Option, + + /// Path to a file containing arguments. + #[arg(long, conflicts_with = "args")] + pub(crate) args_file: Option, + + /// Format of the arguments. + #[arg(long, default_value = "candid")] + pub(crate) args_format: ArgsFormat, +} + +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: &ArgsFormat, + inline_arg_name: &str, +) -> Result, anyhow::Error> { + match (inline_value, args_file) { + (Some(value), None) => { + if *args_format == ArgsFormat::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 { + ArgsFormat::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..c06f58ca 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -8,8 +8,7 @@ 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::manifest::ArgsFormat; use icp::parsers::CyclesAmount; use icp::prelude::*; use serde::Serialize; @@ -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, }; @@ -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. /// @@ -134,45 +134,36 @@ 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: ArgsFormat::Candid, + }) => Some(ResolvedArgs::Candid( + parse_idl_args(&content).context("failed to parse Candid arguments")?, + )), + Some(icp::InitArgs::Text { + content, + format: ArgsFormat::Hex, + }) => Some(ResolvedArgs::Bytes( + hex::decode(&content).context("failed to decode hex arguments")?, + )), + Some(icp::InitArgs::Text { + format: ArgsFormat::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) { - (_, 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-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..deb36de8 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, @@ -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 + icp deploy my_canister --args-file ./args.bin --args-format bin +")] pub(crate) struct DeployArgs { /// Canister names pub(crate) names: Vec, @@ -68,6 +81,11 @@ pub(crate) struct DeployArgs { /// Output command results as JSON #[arg(long)] pub(crate) json: bool, + + /// 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 +107,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 +278,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)) } diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index 327b1cc4..7f8affd0 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -685,6 +685,285 @@ 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_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() { + 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(); 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(), diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 62fe873e..22562467 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 + icp deploy my_canister --args-file ./args.bin --args-format bin + + ###### **Arguments:** * `` — Canister names @@ -839,6 +852,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 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` + + 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": {