Internet ComputerのSDKコマンドdfx deploy
とdfx canister install
の違いを整理し、ソースコードからこれらを読み解きます。
はじめに
こんにちは、@bioerrorlogです。
dfx deploy
とdfx canister install
は共に、Internet Computerのcanisterをデプロイするコマンドです。
dfx deploy
はいろいろと勝手にやってくれるコマンド、dfx canister install
はdfx build
とかいろいろ事前にやっておかないといけないやつ、くらいの認識はあっても、ふとした瞬間に両者を混同しがちです。
--help
フラッグのコマンド解説に互いへの言及が無いのも、分かりにくいポイントでしょう。
そこで今回は、dfx deploy
とdfx canister install
の違いを整理し、ソースコードからもこれらを読み解いていきます。
dfx deploy と dfx canister install の違い
dfx deploy
とdfx canister install
の違いは、ドキュメントで説明されています。
整理すると、dfx deploy
は下記の3つのコマンドをまとめて実行するコマンドと言えます。
dfx canister create --all dfx build dfx canister install --all
dfx deploy
はデフォルトで全てのcanister(上記における--all
)を対象としますが、この辺りはパラメータから変更可能です。
これらのコマンドの使い分け方針としては、
- 特にこだわりがない場合は
dfx deploy
が簡単 - 各ステップ(create/build/install)の実行タイミングを制御したいときは個別コマンドが良い
- 各ステップ(create/build/install)でオプションを細かく設定したいときは個別コマンドが見やすい
といった感じでしょうか。
上手いこと使い分けていきたいところですね。
ソースコードから読み解く
dfx deploy
とdfx canister install
の違いは理解できましたが、せっかくなのでソースコードからも読み解いていきます。
dfx deployの実装
まずは↓を出発点としましょうか。
pub fn exec(env: &dyn Environment, cmd: Command) -> DfxResult { match cmd { Command::Bootstrap(v) => bootstrap::exec(env, v), Command::Build(v) => build::exec(env, v), Command::Cache(v) => cache::exec(env, v), Command::Canister(v) => canister::exec(env, v), Command::Config(v) => config::exec(env, v), Command::Deploy(v) => deploy::exec(env, v), Command::Generate(v) => generate::exec(env, v), Command::Identity(v) => identity::exec(env, v), Command::LanguageServices(v) => language_service::exec(env, v), Command::Ledger(v) => ledger::exec(env, v), Command::New(v) => new::exec(env, v), Command::Ping(v) => ping::exec(env, v), Command::Remote(v) => remote::exec(env, v), Command::Replica(v) => replica::exec(env, v), Command::Start(v) => start::exec(env, v), Command::Stop(v) => stop::exec(env, v), Command::Toolchain(v) => toolchain::exec(env, v), Command::Upgrade(v) => upgrade::exec(env, v), Command::Wallet(v) => wallet::exec(env, v), } }
sdk/mod.rs at 3e3127acb87bd6782a537d96190f0f897655cd5e · dfinity/sdk · GitHub
この部分から、dfx
に与えられるサブコマンド毎に実行されるコードを辿ることができます。
Command::Deploy(v)
からはdeploy::exec(env, v)
が実行されているようです。
deploy::exec
の実装を見に行きしょう。
pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { let env = create_agent_environment(env, opts.network)?; let timeout = expiry_duration(); let canister_name = opts.canister_name.as_deref(); let argument = opts.argument.as_deref(); let argument_type = opts.argument_type.as_deref(); let mode = opts .mode .as_deref() .map(InstallMode::from_str) .transpose() .map_err(|err| anyhow!(err))?; let with_cycles = opts.with_cycles.as_deref(); let force_reinstall = match (mode, canister_name) { (None, _) => false, (Some(InstallMode::Reinstall), Some(_canister_name)) => true, (Some(InstallMode::Reinstall), None) => { bail!("The --mode=reinstall is only valid when deploying a single canister, because reinstallation destroys all data in the canister."); } (Some(_), _) => { unreachable!("The only valid option for --mode is --mode=reinstall"); } }; let runtime = Runtime::new().expect("Unable to create a runtime"); let call_sender = runtime.block_on(call_sender(&env, &opts.wallet))?; let proxy_sender; let create_call_sender = if !opts.no_wallet && !matches!(call_sender, CallSender::Wallet(_)) { let wallet = runtime.block_on(Identity::get_or_create_wallet_canister( &env, env.get_network_descriptor() .expect("Couldn't get the network descriptor"), env.get_selected_identity().expect("No selected identity"), true, ))?; proxy_sender = CallSender::Wallet(*wallet.canister_id_()); &proxy_sender } else { &call_sender }; runtime.block_on(fetch_root_key_if_needed(&env))?; runtime.block_on(deploy_canisters( &env, canister_name, argument, argument_type, force_reinstall, timeout, with_cycles, &call_sender, create_call_sender, )) }
sdk/deploy.rs at 3e3127acb87bd6782a537d96190f0f897655cd5e · dfinity/sdk · GitHub
ちょっと長いですね。
でも落ち着いて読んでみると、最終的に実行されているのは以下の部分のようです。
runtime.block_on(deploy_canisters( &env, canister_name, argument, argument_type, force_reinstall, timeout, with_cycles, &call_sender, create_call_sender, )) }
deploy_canisters
というそれっぽい名前の関数が実行されていますね。
この関数は、
use crate::lib::operations::canister::deploy_canisters;
これを頼りにdeploy_canisters
の実装元を見に行きましょう。
pub async fn deploy_canisters( env: &dyn Environment, some_canister: Option<&str>, argument: Option<&str>, argument_type: Option<&str>, force_reinstall: bool, upgrade_unchanged: bool, timeout: Duration, with_cycles: Option<&str>, call_sender: &CallSender, create_call_sender: &CallSender, ) -> DfxResult { let log = env.get_logger(); let config = env .get_config() .ok_or_else(|| anyhow!("Cannot find dfx configuration file in the current working directory. Did you forget to create one?"))?; let initial_canister_id_store = CanisterIdStore::for_env(env)?; let network = env.get_network_descriptor().unwrap(); let canisters_to_build = canister_with_dependencies(&config, some_canister)?; let canisters_to_deploy = if force_reinstall { // don't force-reinstall the dependencies too. match some_canister { Some(canister_name) => { if config.get_config().is_remote_canister(canister_name, &network.name)? { bail!("The '{}' canister is remote for network '{}' and cannot be force-reinstalled from here", canister_name, &network.name); } vec!(String::from(canister_name)) }, None => bail!("The --mode=reinstall is only valid when deploying a single canister, because reinstallation destroys all data in the canister."), } } else { canisters_to_build.clone() }; let canisters_to_deploy: Vec<String> = canisters_to_deploy .into_iter() .filter(|canister_name| { !matches!( config .get_config() .get_remote_canister_id(canister_name, &network.name), Ok(Some(_)) ) }) .collect(); if some_canister.is_some() { info!(log, "Deploying: {}", canisters_to_deploy.join(" ")); } else { info!(log, "Deploying all canisters."); } register_canisters( env, &canisters_to_build, &initial_canister_id_store, timeout, with_cycles, create_call_sender, &config, ) .await?; build_canisters(env, &canisters_to_build, &config)?; install_canisters( env, &canisters_to_deploy, &initial_canister_id_store, &config, argument, argument_type, force_reinstall, upgrade_unchanged, timeout, call_sender, ) .await?; info!(log, "Deployed canisters."); Ok(()) }
sdk/deploy_canisters.rs at 1625fefc0e0dc028875b704031dd8f9a3f83cb19 · dfinity/sdk · GitHub
これまた長いですね。
落ち着いて読んでみると、最終的に以下が実行されていることが分かります。
register_canisters( env, &canisters_to_build, &initial_canister_id_store, timeout, with_cycles, create_call_sender, &config, ) .await?; build_canisters(env, &canisters_to_build, &config)?; install_canisters( env, &canisters_to_deploy, &initial_canister_id_store, &config, argument, argument_type, force_reinstall, upgrade_unchanged, timeout, call_sender, ) .await?;
register_canisters
/build_canisters
/install_canisters
の三段階に分かれていますね。
先ほど、dfx deploy
は下記の3つのコマンドをまとめて実行するコマンド、とお伝えしました。
dfx canister create --all dfx build dfx canister install --all
この各コマンドと上記の三段階 (register_canisters
/build_canisters
/install_canisters
) の処理にそれぞれ対応していれば、納得感があります。
それぞれ見ていきましょう。
register_canistersとdfx canister create
register_canisters
とdfx canister create
が同じ処理をしているのかを見ていきます。
まずはregister_canisters
の実装をもう少し追いかけてみましょう。
register_canisters
は、deploy_canisters
と同じmoduleに実装されています。
async fn register_canisters( env: &dyn Environment, canister_names: &[String], canister_id_store: &CanisterIdStore, timeout: Duration, with_cycles: Option<&str>, call_sender: &CallSender, config: &Config, ) -> DfxResult { let canisters_to_create = canister_names .iter() .filter(|n| canister_id_store.find(n).is_none()) .cloned() .collect::<Vec<String>>(); if canisters_to_create.is_empty() { info!(env.get_logger(), "All canisters have already been created."); } else { info!(env.get_logger(), "Creating canisters..."); for canister_name in &canisters_to_create { let config_interface = config.get_config(); let compute_allocation = config_interface .get_compute_allocation(canister_name)? .map(|arg| { ComputeAllocation::try_from(arg.parse::<u64>().unwrap()) .expect("Compute Allocation must be a percentage.") }); let memory_allocation = config_interface .get_memory_allocation(canister_name)? .map(|arg| { MemoryAllocation::try_from( u64::try_from(arg.parse::<Bytes>().unwrap().size()).unwrap(), ) .expect( "Memory allocation must be between 0 and 2^48 (i.e 256TB), inclusively.", ) }); let freezing_threshold = config_interface .get_freezing_threshold(canister_name)? .map(|arg| { FreezingThreshold::try_from( u128::try_from(arg.parse::<Bytes>().unwrap().size()).unwrap(), ) .expect("Freezing threshold must be between 0 and 2^64-1, inclusively.") }); let controllers = None; create_canister( env, canister_name, timeout, with_cycles, call_sender, CanisterSettings { controllers, compute_allocation, memory_allocation, freezing_threshold, }, ) .await?; } } Ok(()) }
sdk/deploy_canisters.rs at 1625fefc0e0dc028875b704031dd8f9a3f83cb19 · dfinity/sdk · GitHub
ざっくり、create_canister
がcanisterの数だけ実行されていることが分かります。
use
によるインポートを確認すると、このcreate_canister
はcrate::lib::operations::canister
に宣言されています。
use crate::lib::operations::canister::{create_canister, install_canister};
sdk/deploy_canisters.rs at 1625fefc0e0dc028875b704031dd8f9a3f83cb19 · dfinity/sdk · GitHub
よってregister_canisters
は、crate::lib::operations::canister
のcreate_canister
が、canisterの数だけ実行される、と読み取れます。
では次に、dfx canister create
も同じ処理を処理をしているのかを見にいきましょう。
dfx deploy
のときと同じ手順でdfx canister create
の実装を辿っていくと、下記のコードに行き当たります。
pub async fn exec( env: &dyn Environment, opts: CanisterCreateOpts, mut call_sender: &CallSender, ) -> DfxResult { let config = env.get_config_or_anyhow()?; let timeout = expiry_duration(); fetch_root_key_if_needed(env).await?; let with_cycles = opts.with_cycles.as_deref(); let config_interface = config.get_config(); let network = env.get_network_descriptor().unwrap(); let proxy_sender; if !opts.no_wallet && !matches!(call_sender, CallSender::Wallet(_)) { let wallet = Identity::get_or_create_wallet_canister( env, env.get_network_descriptor() .expect("Couldn't get the network descriptor"), env.get_selected_identity().expect("No selected identity"), false, ) .await?; proxy_sender = CallSender::Wallet(*wallet.canister_id_()); call_sender = &proxy_sender; } let controllers: Option<Vec<_>> = opts .controller .clone() .map(|controllers| { controllers .iter() .map( |controller| match CanisterId::from_text(controller.clone()) { Ok(principal) => Ok(principal), Err(_) => { let current_id = env.get_selected_identity().unwrap(); if current_id == controller { Ok(env.get_selected_identity_principal().unwrap()) } else { let identity_name = controller; IdentityManager::new(env)? .instantiate_identity_from_name(identity_name) .and_then(|identity| { identity.sender().map_err(|err| anyhow!(err)) }) } } }, ) .collect::<DfxResult<Vec<_>>>() }) .transpose()?; if let Some(canister_name) = opts.canister_name.as_deref() { if config .get_config() .is_remote_canister(canister_name, &network.name)? { bail!("Canister '{}' is a remote canister on network '{}', and cannot be created from here.", canister_name, &network.name) } let compute_allocation = get_compute_allocation( opts.compute_allocation.clone(), config_interface, Some(canister_name), )?; let memory_allocation = get_memory_allocation( opts.memory_allocation.clone(), config_interface, Some(canister_name), )?; let freezing_threshold = get_freezing_threshold( opts.freezing_threshold.clone(), config_interface, Some(canister_name), )?; create_canister( env, canister_name, timeout, with_cycles, call_sender, CanisterSettings { controllers, compute_allocation, memory_allocation, freezing_threshold, }, ) .await?; Ok(()) } else if opts.all { // Create all canisters. if let Some(canisters) = &config.get_config().canisters { for canister_name in canisters.keys() { if config .get_config() .is_remote_canister(canister_name, &network.name)? { info!( env.get_logger(), "Skipping canister '{}' because it is remote for network '{}'", canister_name, &network.name, ); continue; } let compute_allocation = get_compute_allocation( opts.compute_allocation.clone(), config_interface, Some(canister_name), )?; let memory_allocation = get_memory_allocation( opts.memory_allocation.clone(), config_interface, Some(canister_name), )?; let freezing_threshold = get_freezing_threshold( opts.freezing_threshold.clone(), config_interface, Some(canister_name), )?; create_canister( env, canister_name, timeout, with_cycles, call_sender, CanisterSettings { controllers: controllers.clone(), compute_allocation, memory_allocation, freezing_threshold, }, ) .await?; } } Ok(()) } else { unreachable!() } }
sdk/create.rs at 633ce44308755a34acd994d682af1627d201c275 · dfinity/sdk · GitHub
少し長いですが落ち着いて読んでみましょう。
いくつか分岐がありますが、いずれもcreate_canister
が必要分呼ばれていることが読み取れます。
register_canisters
と同じですね。
以上より、dfx deploy
の第一段階register_canisters
は、dfx canister create
と大体同じ処理をしていることが分かりました。
build_canistersとdfx build
つぎはbuild_canisters
とdfx build
が同じ処理をしているのかを見ていきます。
build_canisters
の実装はこちらです。
fn build_canisters(env: &dyn Environment, canister_names: &[String], config: &Config) -> DfxResult { info!(env.get_logger(), "Building canisters..."); let build_mode_check = false; let canister_pool = CanisterPool::load(env, build_mode_check, canister_names)?; canister_pool.build_or_fail(&BuildConfig::from_config(config)?) }
sdk/deploy_canisters.rs at 1625fefc0e0dc028875b704031dd8f9a3f83cb19 · dfinity/sdk · GitHub
canister_pool.build_or_fail(&BuildConfig::from_config(config)?)
が実行されていることが分かります。
一方dfx build
の実装を辿っていくと、下記のコードにたどり着きます。
pub fn exec(env: &dyn Environment, opts: CanisterBuildOpts) -> DfxResult { let env = create_agent_environment(env, opts.network)?; let logger = env.get_logger(); // Read the config. let config = env.get_config_or_anyhow()?; // Check the cache. This will only install the cache if there isn't one installed // already. env.get_cache().install()?; let build_mode_check = opts.check; let _all = opts.all; // Option can be None in which case --all was specified let canister_names = config .get_config() .get_canister_names_with_dependencies(opts.canister_name.as_deref())?; // Get pool of canisters to build let canister_pool = CanisterPool::load(&env, build_mode_check, &canister_names)?; // Create canisters on the replica and associate canister ids locally. if build_mode_check { slog::warn!( env.get_logger(), "Building canisters to check they build ok. Canister IDs might be hard coded." ); } else { // CanisterIds would have been set in CanisterPool::load, if available. // This is just to display an error if trying to build before creating the canister. let store = CanisterIdStore::for_env(&env)?; for canister in canister_pool.get_canister_list() { store.get(canister.get_name())?; } } slog::info!(logger, "Building canisters..."); canister_pool.build_or_fail( &BuildConfig::from_config(&config)?.with_build_mode_check(build_mode_check), )?; Ok(()) }
sdk/build.rs at 3e3127acb87bd6782a537d96190f0f897655cd5e · dfinity/sdk · GitHub
build_canisters
よりだいぶ長いコードに見えますが、最終的には同じcanister_pool.build_or_fail
が実行されています。
以上より、build_canisters
とdfx build
も似たような処理をしていることが分かりました。
install_canistersとdfx canister install
最後にinstall_canisters
とdfx canister install
も同じ処理をしているのかを見ていきます。
install_canisters
の実装はこちらです。
async fn install_canisters( env: &dyn Environment, canister_names: &[String], initial_canister_id_store: &CanisterIdStore, config: &Config, argument: Option<&str>, argument_type: Option<&str>, force_reinstall: bool, upgrade_unchanged: bool, timeout: Duration, call_sender: &CallSender, ) -> DfxResult { info!(env.get_logger(), "Installing canisters..."); let agent = env .get_agent() .ok_or_else(|| anyhow!("Cannot find dfx configuration file in the current working directory. Did you forget to create one?"))?; let canister_id_store = CanisterIdStore::for_env(env)?; for canister_name in canister_names { let (install_mode, installed_module_hash) = if force_reinstall { (InstallMode::Reinstall, None) } else { match initial_canister_id_store.find(canister_name) { Some(canister_id) => { match agent .read_state_canister_info(canister_id, "module_hash", false) .await { Ok(installed_module_hash) => { (InstallMode::Upgrade, Some(installed_module_hash)) } // If the canister is empty, this path does not exist. // The replica doesn't support negative lookups, therefore if the canister // is empty, the replica will return lookup_path([], Pruned _) = Unknown Err(AgentError::LookupPathUnknown(_)) | Err(AgentError::LookupPathAbsent(_)) => (InstallMode::Install, None), Err(x) => bail!(x), } } None => (InstallMode::Install, None), } }; let canister_id = canister_id_store.get(canister_name)?; let canister_info = CanisterInfo::load(config, canister_name, Some(canister_id))?; let maybe_path = canister_info.get_output_idl_path(); let init_type = maybe_path.and_then(|path| get_candid_init_type(&path)); let install_args = blob_from_arguments(argument, None, argument_type, &init_type)?; install_canister( env, agent, &canister_info, &install_args, install_mode, timeout, call_sender, installed_module_hash, upgrade_unchanged, ) .await?; } Ok(()) }
sdk/deploy_canisters.rs at 1625fefc0e0dc028875b704031dd8f9a3f83cb19 · dfinity/sdk · GitHub
Canisterをfor loopで回してinstall_canister
を実行しています。
ちょうどregister_canisters
と似たような実装ですね。
一方dfx canister install
の方はというと、
pub async fn exec( env: &dyn Environment, opts: CanisterInstallOpts, call_sender: &CallSender, ) -> DfxResult { let config = env.get_config_or_anyhow()?; let agent = env .get_agent() .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; let timeout = expiry_duration(); fetch_root_key_if_needed(env).await?; let mode = InstallMode::from_str(opts.mode.as_str()).map_err(|err| anyhow!(err))?; let canister_id_store = CanisterIdStore::for_env(env)?; let network = env.get_network_descriptor().unwrap(); if mode == InstallMode::Reinstall && (opts.canister.is_none() || opts.all) { bail!("The --mode=reinstall is only valid when specifying a single canister, because reinstallation destroys all data in the canister."); } if let Some(canister) = opts.canister.as_deref() { if config .get_config() .is_remote_canister(canister, &network.name)? { bail!("Canister '{}' is a remote canister on network '{}', and cannot be installed from here.", canister, &network.name) } let canister_id = Principal::from_text(canister).or_else(|_| canister_id_store.get(canister))?; let canister_info = CanisterInfo::load(&config, canister, Some(canister_id))?; let maybe_path = canister_info.get_output_idl_path(); let init_type = maybe_path.and_then(|path| get_candid_init_type(&path)); let arguments = opts.argument.as_deref(); let arg_type = opts.argument_type.as_deref(); let install_args = blob_from_arguments(arguments, None, arg_type, &init_type)?; let installed_module_hash = read_module_hash(agent, &canister_id_store, &canister_info).await?; install_canister( env, agent, &canister_info, &install_args, mode, timeout, call_sender, installed_module_hash, ) .await } else if opts.all { // Install all canisters. if let Some(canisters) = &config.get_config().canisters { for canister in canisters.keys() { if config .get_config() .is_remote_canister(canister, &network.name)? { info!( env.get_logger(), "Skipping canister '{}' because it is remote for network '{}'", canister, &network.name, ); continue; } let canister_id = Principal::from_text(canister).or_else(|_| canister_id_store.get(canister))?; let canister_info = CanisterInfo::load(&config, canister, Some(canister_id))?; let installed_module_hash = read_module_hash(agent, &canister_id_store, &canister_info).await?; let install_args = []; install_canister( env, agent, &canister_info, &install_args, mode, timeout, call_sender, installed_module_hash, ) .await?; } } Ok(()) } else { unreachable!() } }
sdk/install.rs at 3e3127acb87bd6782a537d96190f0f897655cd5e · dfinity/sdk · GitHub
こちらもcanisterごとにinstall_canister
を実行しています。
install_canisters
と同じですね。
まとめ
以上より、
dfx deploy
はregister_canisters
/build_canisters
/install_canisters
の三段階の処理で構成されるregister_canisters
はdfx canister create
コマンドと似たような処理をやっているbuild_canisters
はdfx build
コマンドと似たような処理をやっているinstall_canisters
はdfx canister install
コマンドと似たような処理をやっている- よって
dfx deploy
コマンドはdfx canister create
/dfx build
/dfx canister install
の三つのコマンドをまとめたようなものである
ということが理解できました。
おわりに
今回は、dfx deploy
とdfx canister install
の違いを整理したうえで、ソースコードを読み込んでみました。
最近Rustに入門したことでDfintiy / Internet Computer関連のソースコードも抵抗なく読めるようになりました。 楽しみがまたひとつ増えたのは嬉しいことです。
今後も都度ソースコードをあたりながら、この界隈を深く楽しんでいければと思っています。
[関連記事]
参考
dfx deploy :: Internet Computer
GitHub - dfinity/sdk: The DFINITY Canister Software Development Kit (SDK)