use crate::{
cli::clap::{parse_matches, string_option, value_arg},
cli::defaults::local_network,
cli::globals::internal_network_arg,
cli::help::print_help_or_version,
version_text,
};
use canic_host::canister_build::CanisterBuildProfile;
use canic_host::icp_config::resolve_current_canic_icp_root;
use canic_host::install_root::{InstallRootOptions, install_root};
use clap::Command as ClapCommand;
use std::{ffi::OsString, path::PathBuf};
use thiserror::Error as ThisError;
const DEFAULT_ROOT_TARGET: &str = "root";
const DEFAULT_READY_TIMEOUT_SECONDS: u64 = 120;
const INSTALL_HELP_AFTER: &str = "\
Examples:
canic install test
canic install --profile fast test
canic install uses fleets/<fleet>/canic.toml.
The selected canic.toml must include:
[fleet]
name = \"test\"";
#[derive(Debug, ThisError)]
pub enum InstallCommandError {
#[error("{0}")]
Usage(String),
#[error(transparent)]
Install(#[from] Box<dyn std::error::Error>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InstallOptions {
pub fleet: String,
pub network: String,
pub profile: Option<CanisterBuildProfile>,
}
impl InstallOptions {
pub fn parse<I>(args: I) -> Result<Self, InstallCommandError>
where
I: IntoIterator<Item = OsString>,
{
let matches = parse_matches(install_command(), args)
.map_err(|_| InstallCommandError::Usage(usage()))?;
let fleet = string_option(&matches, "fleet").expect("clap requires fleet");
Ok(Self {
fleet,
network: string_option(&matches, "network").unwrap_or_else(local_network),
profile: string_option(&matches, "profile")
.as_deref()
.map(parse_profile)
.transpose()?,
})
}
#[must_use]
#[cfg(test)]
pub fn into_install_root_options(self) -> InstallRootOptions {
self.into_install_root_options_with_icp_root(None)
}
pub fn into_install_root_options_with_icp_root(
self,
icp_root: Option<PathBuf>,
) -> InstallRootOptions {
let config_path = icp_root
.as_deref()
.map(|root| root.join(default_fleet_config_path(&self.fleet)))
.filter(|path| path.is_file())
.map_or_else(
|| default_fleet_config_path(&self.fleet),
|path| path.display().to_string(),
);
InstallRootOptions {
root_canister: DEFAULT_ROOT_TARGET.to_string(),
root_build_target: DEFAULT_ROOT_TARGET.to_string(),
network: self.network,
icp_root,
build_profile: self.profile,
ready_timeout_seconds: DEFAULT_READY_TIMEOUT_SECONDS,
config_path: Some(config_path),
expected_fleet: Some(self.fleet),
interactive_config_selection: false,
}
}
}
fn install_command() -> ClapCommand {
ClapCommand::new("install")
.bin_name("canic install")
.about("Install and bootstrap a Canic fleet")
.disable_help_flag(true)
.override_usage("canic install <fleet>")
.arg(
value_arg("fleet")
.value_name("fleet")
.required(true)
.help("Config-defined fleet name to install"),
)
.arg(
value_arg("profile")
.long("profile")
.value_name("debug|fast|release")
.num_args(1)
.help("Canister wasm build profile; defaults to CANIC_WASM_PROFILE or release"),
)
.arg(internal_network_arg())
.after_help(INSTALL_HELP_AFTER)
}
pub fn run<I>(args: I) -> Result<(), InstallCommandError>
where
I: IntoIterator<Item = OsString>,
{
let args = args.into_iter().collect::<Vec<_>>();
if print_help_or_version(&args, usage, version_text()) {
return Ok(());
}
let options = InstallOptions::parse(args)?;
let icp_root = resolve_current_canic_icp_root(None).ok();
install_root(options.into_install_root_options_with_icp_root(icp_root))
.map_err(InstallCommandError::from)
}
fn default_fleet_config_path(fleet: &str) -> String {
format!("fleets/{fleet}/canic.toml")
}
fn usage() -> String {
let mut command = install_command();
command.render_help().to_string()
}
fn parse_profile(value: &str) -> Result<CanisterBuildProfile, InstallCommandError> {
match value {
"debug" => Ok(CanisterBuildProfile::Debug),
"fast" => Ok(CanisterBuildProfile::Fast),
"release" => Ok(CanisterBuildProfile::Release),
_ => Err(InstallCommandError::Usage(format!(
"invalid build profile: {value}\n\n{}",
usage()
))),
}
}
#[cfg(test)]
mod tests;