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
の違いは理解できましたが、せっかくなのでソースコードからも読み解いていきます。
github.com
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 {
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 {
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();
let config = env.get_config_or_anyhow()?;
env.get_cache().install()?;
let build_mode_check = opts.check;
let _all = opts.all;
let canister_names = config
.get_config()
.get_canister_names_with_dependencies(opts.canister_name.as_deref())?;
let canister_pool = CanisterPool::load(&env, build_mode_check, &canister_names)?;
if build_mode_check {
slog::warn!(
env.get_logger(),
"Building canisters to check they build ok. Canister IDs might be hard coded."
);
} else {
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))
}
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 {
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関連のソースコードも抵抗なく読めるようになりました。
楽しみがまたひとつ増えたのは嬉しいことです。
今後も都度ソースコードをあたりながら、この界隈を深く楽しんでいければと思っています。
[関連記事]
www.bioerrorlog.work
www.bioerrorlog.work
www.bioerrorlog.work
参考
dfx deploy :: Internet Computer
GitHub - dfinity/sdk: The DFINITY Canister Software Development Kit (SDK)