#![allow(dead_code)]
#![allow(clippy::should_implement_trait)] use crate::config::directories::get_user_dfx_config_dir;
use crate::config::model::bitcoin_adapter::BitcoinAdapterLogLevel;
use crate::config::model::canister_http_adapter::HttpAdapterLogLevel;
use crate::config::model::extension_canister_type::apply_extension_canister_types;
use crate::error::config::{GetOutputEnvFileError, GetTempPathError};
use crate::error::dfx_config::AddDependenciesError::CanisterCircularDependency;
use crate::error::dfx_config::GetCanisterNamesWithDependenciesError::AddDependenciesFailed;
use crate::error::dfx_config::GetComputeAllocationError::GetComputeAllocationFailed;
use crate::error::dfx_config::GetFreezingThresholdError::GetFreezingThresholdFailed;
use crate::error::dfx_config::GetLogVisibilityError::GetLogVisibilityFailed;
use crate::error::dfx_config::GetMemoryAllocationError::GetMemoryAllocationFailed;
use crate::error::dfx_config::GetPullCanistersError::PullCanistersSameId;
use crate::error::dfx_config::GetRemoteCanisterIdError::GetRemoteCanisterIdFailed;
use crate::error::dfx_config::GetReservedCyclesLimitError::GetReservedCyclesLimitFailed;
use crate::error::dfx_config::GetSpecifiedIdError::GetSpecifiedIdFailed;
use crate::error::dfx_config::GetWasmMemoryLimitError::GetWasmMemoryLimitFailed;
use crate::error::dfx_config::{
AddDependenciesError, GetCanisterConfigError, GetCanisterNamesWithDependenciesError,
GetComputeAllocationError, GetFreezingThresholdError, GetLogVisibilityError,
GetMemoryAllocationError, GetPullCanistersError, GetRemoteCanisterIdError,
GetReservedCyclesLimitError, GetSpecifiedIdError, GetWasmMemoryLimitError,
};
use crate::error::fs::CanonicalizePathError;
use crate::error::load_dfx_config::LoadDfxConfigError;
use crate::error::load_dfx_config::LoadDfxConfigError::{
DetermineCurrentWorkingDirFailed, ResolveConfigPath,
};
use crate::error::load_networks_config::LoadNetworksConfigError;
use crate::error::load_networks_config::LoadNetworksConfigError::{
GetConfigPathFailed, LoadConfigFromFileFailed,
};
use crate::error::socket_addr_conversion::SocketAddrConversionError;
use crate::error::socket_addr_conversion::SocketAddrConversionError::{
EmptyIterator, ParseSocketAddrFailed,
};
use crate::error::structured_file::StructuredFileError;
use crate::error::structured_file::StructuredFileError::DeserializeJsonFileFailed;
use crate::extension::manager::ExtensionManager;
use crate::fs::create_dir_all;
use crate::json::save_json_file;
use crate::json::structure::{PossiblyStr, SerdeVec};
use crate::util::ByteSchema;
use byte_unit::Byte;
use candid::Principal;
use ic_utils::interfaces::management_canister::LogVisibility;
use schemars::JsonSchema;
use serde::de::{Error as _, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::default::Default;
use std::fmt;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
use std::path::{Path, PathBuf};
use std::time::Duration;
use super::network_descriptor::MOTOKO_PLAYGROUND_CANISTER_TIMEOUT_SECONDS;
pub const CONFIG_FILE_NAME: &str = "dfx.json";
pub const BUILTIN_CANISTER_TYPES: [&str; 5] = ["rust", "motoko", "assets", "custom", "pull"];
const EMPTY_CONFIG_DEFAULTS: ConfigDefaults = ConfigDefaults {
bitcoin: None,
bootstrap: None,
build: None,
canister_http: None,
proxy: None,
replica: None,
};
const EMPTY_CONFIG_DEFAULTS_BUILD: ConfigDefaultsBuild = ConfigDefaultsBuild {
packtool: None,
args: None,
};
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct ConfigCanistersCanisterRemote {
pub candid: Option<PathBuf>,
#[schemars(with = "BTreeMap<String, String>")]
pub id: BTreeMap<String, Principal>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum WasmOptLevel {
#[serde(rename = "cycles")]
Cycles,
#[serde(rename = "size")]
Size,
O4,
O3,
O2,
O1,
O0,
Oz,
Os,
}
impl std::fmt::Display for WasmOptLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "lowercase")]
pub enum MetadataVisibility {
#[default]
Public,
Private,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct CanisterMetadataSection {
pub name: String,
#[serde(default)]
pub visibility: MetadataVisibility,
pub networks: Option<BTreeSet<String>>,
pub path: Option<PathBuf>,
pub content: Option<String>,
}
impl CanisterMetadataSection {
pub fn applies_to_network(&self, network: &str) -> bool {
self.networks
.as_ref()
.map(|networks| networks.contains(network))
.unwrap_or(true)
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct Pullable {
pub wasm_url: String,
pub wasm_hash: Option<String>,
pub wasm_hash_url: Option<String>,
#[schemars(with = "Vec::<String>")]
pub dependencies: Vec<Principal>,
pub init_guide: String,
pub init_arg: Option<String>,
}
pub type TechStackCategoryMap = HashMap<String, HashMap<String, String>>;
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct TechStack {
#[serde(skip_serializing_if = "Option::is_none")]
pub cdk: Option<TechStackCategoryMap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<TechStackCategoryMap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lib: Option<TechStackCategoryMap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<TechStackCategoryMap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub other: Option<TechStackCategoryMap>,
}
pub const DEFAULT_SHARED_LOCAL_BIND: &str = "127.0.0.1:4943"; pub const DEFAULT_PROJECT_LOCAL_BIND: &str = "127.0.0.1:8000";
pub const DEFAULT_IC_GATEWAY: &str = "https://icp0.io";
pub const DEFAULT_IC_GATEWAY_TRAILING_SLASH: &str = "https://icp0.io/";
pub const DEFAULT_REPLICA_PORT: u16 = 8080;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ConfigCanistersCanister {
#[serde(default)]
pub declarations: CanisterDeclarationsConfig,
#[serde(default)]
pub remote: Option<ConfigCanistersCanisterRemote>,
pub args: Option<String>,
#[serde(default)]
pub initialization_values: InitializationValues,
#[serde(default)]
pub dependencies: Vec<String>,
pub frontend: Option<BTreeMap<String, String>>,
#[serde(flatten)]
pub type_specific: CanisterTypeProperties,
#[serde(default)]
pub post_install: SerdeVec<String>,
pub main: Option<PathBuf>,
pub shrink: Option<bool>,
#[serde(default)]
pub optimize: Option<WasmOptLevel>,
#[serde(default)]
pub metadata: Vec<CanisterMetadataSection>,
#[serde(default)]
pub pullable: Option<Pullable>,
#[serde(default)]
pub tech_stack: Option<TechStack>,
pub gzip: Option<bool>,
#[schemars(with = "Option<String>")]
pub specified_id: Option<Principal>,
pub init_arg: Option<String>,
pub init_arg_file: Option<String>,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CanisterTypeProperties {
Rust {
package: String,
#[serde(rename = "crate")]
crate_name: Option<String>,
candid: PathBuf,
},
Assets {
source: Vec<PathBuf>,
#[schemars(default)]
build: SerdeVec<String>,
workspace: Option<String>,
},
Custom {
wasm: String,
candid: String,
#[schemars(default)]
build: SerdeVec<String>,
},
Motoko,
Pull {
#[schemars(with = "String")]
id: Principal,
},
}
impl CanisterTypeProperties {
pub fn name(&self) -> &'static str {
match self {
Self::Rust { .. } => "rust",
Self::Motoko { .. } => "motoko",
Self::Assets { .. } => "assets",
Self::Custom { .. } => "custom",
Self::Pull { .. } => "pull",
}
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CanisterLogVisibility {
#[default]
Controllers,
Public,
#[schemars(with = "Vec::<String>")]
AllowedViewers(Vec<Principal>),
}
impl From<CanisterLogVisibility> for LogVisibility {
fn from(value: CanisterLogVisibility) -> Self {
match value {
CanisterLogVisibility::Controllers => LogVisibility::Controllers,
CanisterLogVisibility::Public => LogVisibility::Public,
CanisterLogVisibility::AllowedViewers(viewers) => {
LogVisibility::AllowedViewers(viewers)
}
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct InitializationValues {
pub compute_allocation: Option<PossiblyStr<u64>>,
#[schemars(with = "Option<ByteSchema>")]
pub memory_allocation: Option<Byte>,
#[serde(with = "humantime_serde")]
#[schemars(with = "Option<String>")]
pub freezing_threshold: Option<Duration>,
#[schemars(with = "Option<u128>")]
pub reserved_cycles_limit: Option<u128>,
#[schemars(with = "Option<ByteSchema>")]
pub wasm_memory_limit: Option<Byte>,
#[schemars(with = "Option<CanisterLogVisibility>")]
pub log_visibility: Option<CanisterLogVisibility>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct CanisterDeclarationsConfig {
pub output: Option<PathBuf>,
pub bindings: Option<Vec<String>>,
pub env_override: Option<String>,
#[serde(default)]
pub node_compatibility: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ConfigDefaultsBitcoin {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub nodes: Option<Vec<SocketAddr>>,
#[serde(default = "default_bitcoin_log_level")]
pub log_level: BitcoinAdapterLogLevel,
#[serde(default = "default_bitcoin_canister_init_arg")]
pub canister_init_arg: String,
}
pub fn default_bitcoin_log_level() -> BitcoinAdapterLogLevel {
BitcoinAdapterLogLevel::Info
}
pub fn default_bitcoin_canister_init_arg() -> String {
"(record { stability_threshold = 0 : nat; network = variant { regtest }; blocks_source = principal \"aaaaa-aa\"; fees = record { get_utxos_base = 0 : nat; get_utxos_cycles_per_ten_instructions = 0 : nat; get_utxos_maximum = 0 : nat; get_balance = 0 : nat; get_balance_maximum = 0 : nat; get_current_fee_percentiles = 0 : nat; get_current_fee_percentiles_maximum = 0 : nat; send_transaction_base = 0 : nat; send_transaction_per_byte = 0 : nat; }; syncing = variant { enabled }; api_access = variant { enabled }; disable_api_if_not_fully_synced = variant { enabled }})".to_string()
}
impl Default for ConfigDefaultsBitcoin {
fn default() -> Self {
ConfigDefaultsBitcoin {
enabled: false,
nodes: None,
log_level: default_bitcoin_log_level(),
canister_init_arg: default_bitcoin_canister_init_arg(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConfigDefaultsCanisterHttp {
#[serde(default = "default_as_true")]
pub enabled: bool,
#[serde(default)]
pub log_level: HttpAdapterLogLevel,
}
impl Default for ConfigDefaultsCanisterHttp {
fn default() -> Self {
ConfigDefaultsCanisterHttp {
enabled: true,
log_level: HttpAdapterLogLevel::default(),
}
}
}
fn default_as_true() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConfigDefaultsBootstrap {
#[serde(default = "default_bootstrap_ip")]
pub ip: IpAddr,
#[serde(default = "default_bootstrap_port")]
pub port: u16,
#[serde(default = "default_bootstrap_timeout")]
pub timeout: u64,
}
pub fn default_bootstrap_ip() -> IpAddr {
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
}
pub fn default_bootstrap_port() -> u16 {
8081
}
pub fn default_bootstrap_timeout() -> u64 {
30
}
impl Default for ConfigDefaultsBootstrap {
fn default() -> Self {
ConfigDefaultsBootstrap {
ip: default_bootstrap_ip(),
port: default_bootstrap_port(),
timeout: default_bootstrap_timeout(),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct ConfigDefaultsBuild {
pub packtool: Option<String>,
pub args: Option<String>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ReplicaLogLevel {
Critical,
Error,
Warning,
Info,
Debug,
Trace,
}
impl Default for ReplicaLogLevel {
fn default() -> Self {
Self::Error
}
}
impl ReplicaLogLevel {
pub fn to_ic_starter_string(&self) -> String {
match self {
Self::Critical => "critical".to_string(),
Self::Error => "error".to_string(),
Self::Warning => "warning".to_string(),
Self::Info => "info".to_string(),
Self::Debug => "debug".to_string(),
Self::Trace => "trace".to_string(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConfigDefaultsReplica {
pub port: Option<u16>,
pub subnet_type: Option<ReplicaSubnetType>,
pub log_level: Option<ReplicaLogLevel>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConfigDefaultsProxy {
pub domain: Option<SerdeVec<String>>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "lowercase")]
pub enum NetworkType {
#[default]
Ephemeral,
Persistent,
}
impl NetworkType {
fn ephemeral() -> Self {
NetworkType::Ephemeral
}
fn persistent() -> Self {
NetworkType::Persistent
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "lowercase")]
pub enum ReplicaSubnetType {
System,
#[default]
Application,
VerifiedApplication,
}
impl ReplicaSubnetType {
pub fn as_ic_starter_string(&self) -> String {
match self {
ReplicaSubnetType::System => "system".to_string(),
ReplicaSubnetType::Application => "application".to_string(),
ReplicaSubnetType::VerifiedApplication => "verified_application".to_string(),
}
}
}
fn default_playground_timeout_seconds() -> u64 {
MOTOKO_PLAYGROUND_CANISTER_TIMEOUT_SECONDS
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PlaygroundConfig {
pub playground_canister: String,
#[serde(default = "default_playground_timeout_seconds")]
pub timeout_seconds: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConfigNetworkProvider {
pub providers: Vec<String>,
#[serde(default = "NetworkType::persistent")]
pub r#type: NetworkType,
pub playground: Option<PlaygroundConfig>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ConfigLocalProvider {
pub bind: Option<String>,
#[serde(default = "NetworkType::ephemeral")]
pub r#type: NetworkType,
pub bitcoin: Option<ConfigDefaultsBitcoin>,
pub bootstrap: Option<ConfigDefaultsBootstrap>,
pub canister_http: Option<ConfigDefaultsCanisterHttp>,
pub replica: Option<ConfigDefaultsReplica>,
pub playground: Option<PlaygroundConfig>,
pub proxy: Option<ConfigDefaultsProxy>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum ConfigNetwork {
ConfigNetworkProvider(ConfigNetworkProvider),
ConfigLocalProvider(ConfigLocalProvider),
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum Profile {
Debug,
Release,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct ConfigDefaults {
pub bitcoin: Option<ConfigDefaultsBitcoin>,
pub bootstrap: Option<ConfigDefaultsBootstrap>,
pub build: Option<ConfigDefaultsBuild>,
pub canister_http: Option<ConfigDefaultsCanisterHttp>,
pub proxy: Option<ConfigDefaultsProxy>,
pub replica: Option<ConfigDefaultsReplica>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ConfigInterface {
pub profile: Option<Profile>,
pub version: Option<u32>,
pub dfx: Option<String>,
pub canisters: Option<BTreeMap<String, ConfigCanistersCanister>>,
pub defaults: Option<ConfigDefaults>,
pub networks: Option<BTreeMap<String, ConfigNetwork>>,
pub output_env_file: Option<PathBuf>,
}
pub type TopLevelConfigNetworks = BTreeMap<String, ConfigNetwork>;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NetworksConfigInterface {
pub networks: TopLevelConfigNetworks,
}
impl ConfigCanistersCanister {}
pub fn to_socket_addr(s: &str) -> Result<SocketAddr, SocketAddrConversionError> {
match s.to_socket_addrs() {
Ok(mut a) => match a.next() {
Some(res) => Ok(res),
None => Err(EmptyIterator(s.to_string())),
},
Err(err) => Err(ParseSocketAddrFailed(s.to_string(), err)),
}
}
impl ConfigDefaultsBuild {
pub fn get_packtool(&self) -> Option<String> {
match &self.packtool {
Some(v) if !v.is_empty() => self.packtool.to_owned(),
_ => None,
}
}
pub fn get_args(&self) -> Option<String> {
match &self.args {
Some(v) if !v.is_empty() => self.args.to_owned(),
_ => None,
}
}
}
impl ConfigDefaults {
pub fn get_build(&self) -> &ConfigDefaultsBuild {
match &self.build {
Some(x) => x,
None => &EMPTY_CONFIG_DEFAULTS_BUILD,
}
}
}
impl NetworksConfigInterface {
pub fn get_network(&self, name: &str) -> Option<&ConfigNetwork> {
self.networks.get(name)
}
}
impl ConfigInterface {
pub fn get_defaults(&self) -> &ConfigDefaults {
match &self.defaults {
Some(v) => v,
_ => &EMPTY_CONFIG_DEFAULTS,
}
}
pub fn get_network(&self, name: &str) -> Option<&ConfigNetwork> {
self.networks
.as_ref()
.and_then(|networks| networks.get(name))
}
pub fn get_version(&self) -> u32 {
self.version.unwrap_or(1)
}
pub fn get_dfx(&self) -> Option<String> {
self.dfx.to_owned()
}
pub fn get_canister_names_with_dependencies(
&self,
some_canister: Option<&str>,
) -> Result<Vec<String>, GetCanisterNamesWithDependenciesError> {
self.canisters
.as_ref()
.ok_or(GetCanisterNamesWithDependenciesError::CanistersFieldDoesNotExist())
.and_then(|canister_map| match some_canister {
Some(specific_canister) => {
let mut names = HashSet::new();
let mut path = vec![];
add_dependencies(canister_map, &mut names, &mut path, specific_canister)
.map(|_| names.into_iter().collect())
.map_err(|err| AddDependenciesFailed(specific_canister.to_string(), err))
}
None => Ok(canister_map.keys().cloned().collect()),
})
}
pub fn get_remote_canister_id(
&self,
canister: &str,
network: &str,
) -> Result<Option<Principal>, GetRemoteCanisterIdError> {
let maybe_principal = self
.get_canister_config(canister)
.map_err(|e| {
GetRemoteCanisterIdFailed(
Box::new(canister.to_string()),
Box::new(network.to_string()),
e,
)
})?
.remote
.as_ref()
.and_then(|r| r.id.get(network))
.copied();
Ok(maybe_principal)
}
pub fn is_remote_canister(
&self,
canister: &str,
network: &str,
) -> Result<bool, GetRemoteCanisterIdError> {
Ok(self.get_remote_canister_id(canister, network)?.is_some())
}
pub fn get_compute_allocation(
&self,
canister_name: &str,
) -> Result<Option<u64>, GetComputeAllocationError> {
Ok(self
.get_canister_config(canister_name)
.map_err(|e| GetComputeAllocationFailed(canister_name.to_string(), e))?
.initialization_values
.compute_allocation
.map(|x| x.0))
}
pub fn get_memory_allocation(
&self,
canister_name: &str,
) -> Result<Option<Byte>, GetMemoryAllocationError> {
Ok(self
.get_canister_config(canister_name)
.map_err(|e| GetMemoryAllocationFailed(canister_name.to_string(), e))?
.initialization_values
.memory_allocation)
}
pub fn get_freezing_threshold(
&self,
canister_name: &str,
) -> Result<Option<Duration>, GetFreezingThresholdError> {
Ok(self
.get_canister_config(canister_name)
.map_err(|e| GetFreezingThresholdFailed(canister_name.to_string(), e))?
.initialization_values
.freezing_threshold)
}
pub fn get_reserved_cycles_limit(
&self,
canister_name: &str,
) -> Result<Option<u128>, GetReservedCyclesLimitError> {
Ok(self
.get_canister_config(canister_name)
.map_err(|e| GetReservedCyclesLimitFailed(canister_name.to_string(), e))?
.initialization_values
.reserved_cycles_limit)
}
pub fn get_wasm_memory_limit(
&self,
canister_name: &str,
) -> Result<Option<Byte>, GetWasmMemoryLimitError> {
Ok(self
.get_canister_config(canister_name)
.map_err(|e| GetWasmMemoryLimitFailed(canister_name.to_string(), e))?
.initialization_values
.wasm_memory_limit)
}
pub fn get_log_visibility(
&self,
canister_name: &str,
) -> Result<Option<LogVisibility>, GetLogVisibilityError> {
Ok(self
.get_canister_config(canister_name)
.map_err(|e| GetLogVisibilityFailed(canister_name.to_string(), e))?
.initialization_values
.log_visibility
.clone()
.map(|visibility| visibility.into()))
}
fn get_canister_config(
&self,
canister_name: &str,
) -> Result<&ConfigCanistersCanister, GetCanisterConfigError> {
self.canisters
.as_ref()
.ok_or(GetCanisterConfigError::CanistersFieldDoesNotExist())?
.get(canister_name)
.ok_or_else(|| GetCanisterConfigError::CanisterNotFound(canister_name.to_string()))
}
pub fn get_pull_canisters(&self) -> Result<BTreeMap<String, Principal>, GetPullCanistersError> {
let mut res = BTreeMap::new();
let mut id_to_name: BTreeMap<Principal, &String> = BTreeMap::new();
if let Some(map) = &self.canisters {
for (k, v) in map {
if let CanisterTypeProperties::Pull { id } = v.type_specific {
if let Some(other_name) = id_to_name.get(&id) {
return Err(PullCanistersSameId(other_name.to_string(), k.clone(), id));
}
res.insert(k.clone(), id);
id_to_name.insert(id, k);
}
}
};
Ok(res)
}
pub fn get_specified_id(
&self,
canister_name: &str,
) -> Result<Option<Principal>, GetSpecifiedIdError> {
Ok(self
.get_canister_config(canister_name)
.map_err(|e| GetSpecifiedIdFailed(canister_name.to_string(), e))?
.specified_id)
}
}
fn add_dependencies(
all_canisters: &BTreeMap<String, ConfigCanistersCanister>,
names: &mut HashSet<String>,
path: &mut Vec<String>,
canister_name: &str,
) -> Result<(), AddDependenciesError> {
let inserted = names.insert(String::from(canister_name));
if !inserted {
return if path.contains(&String::from(canister_name)) {
path.push(String::from(canister_name));
Err(CanisterCircularDependency(path.clone()))
} else {
Ok(())
};
}
let canister_config = all_canisters
.get(canister_name)
.ok_or_else(|| AddDependenciesError::CanisterNotFound(canister_name.to_string()))?;
path.push(String::from(canister_name));
for canister in &canister_config.dependencies {
add_dependencies(all_canisters, names, path, canister)?;
}
path.pop();
Ok(())
}
#[derive(Clone, Debug)]
pub struct Config {
path: PathBuf,
json: Value,
pub config: ConfigInterface,
}
#[allow(dead_code)]
impl Config {
fn resolve_config_path(working_dir: &Path) -> Result<Option<PathBuf>, CanonicalizePathError> {
let mut curr = crate::fs::canonicalize(working_dir)?;
while curr.parent().is_some() {
if curr.join(CONFIG_FILE_NAME).is_file() {
return Ok(Some(curr.join(CONFIG_FILE_NAME)));
} else {
curr.pop();
}
}
if curr.join(CONFIG_FILE_NAME).is_file() {
return Ok(Some(curr.join(CONFIG_FILE_NAME)));
}
Ok(None)
}
fn from_file(
path: &Path,
extension_manager: Option<&ExtensionManager>,
) -> Result<Config, LoadDfxConfigError> {
let content = crate::fs::read(path)?;
Config::from_slice(path.to_path_buf(), &content, extension_manager)
}
pub fn from_dir(
working_dir: &Path,
extension_manager: Option<&ExtensionManager>,
) -> Result<Option<Config>, LoadDfxConfigError> {
let path = Config::resolve_config_path(working_dir).map_err(ResolveConfigPath)?;
path.map(|path| Config::from_file(&path, extension_manager))
.transpose()
}
pub fn from_current_dir(
extension_manager: Option<&ExtensionManager>,
) -> Result<Option<Config>, LoadDfxConfigError> {
let working_dir = std::env::current_dir().map_err(DetermineCurrentWorkingDirFailed)?;
Config::from_dir(&working_dir, extension_manager)
}
fn from_slice(
path: PathBuf,
content: &[u8],
extension_manager: Option<&ExtensionManager>,
) -> Result<Config, LoadDfxConfigError> {
let json: Value = serde_json::from_slice(content)
.map_err(|e| LoadDfxConfigError::DeserializeValueFailed(Box::new(path.clone()), e))?;
let effective_json = apply_extension_canister_types(json.clone(), extension_manager)?;
let config = serde_json::from_value(effective_json)
.map_err(|e| LoadDfxConfigError::DeserializeValueFailed(Box::new(path.clone()), e))?;
Ok(Config { path, json, config })
}
#[cfg(test)]
pub(crate) fn from_str(content: &str) -> Result<Config, StructuredFileError> {
Ok(Config::from_slice(PathBuf::from("-"), content.as_bytes(), None).unwrap())
}
#[cfg(test)]
pub(crate) fn from_str_and_path(
path: PathBuf,
content: &str,
) -> Result<Config, StructuredFileError> {
Ok(Config::from_slice(path, content.as_bytes(), None).unwrap())
}
pub fn get_path(&self) -> &PathBuf {
&self.path
}
pub fn get_temp_path(&self) -> Result<PathBuf, GetTempPathError> {
let path = self.get_path().parent().unwrap().join(".dfx");
create_dir_all(&path)?;
Ok(path)
}
pub fn get_json(&self) -> &Value {
&self.json
}
pub fn get_mut_json(&mut self) -> &mut Value {
&mut self.json
}
pub fn get_config(&self) -> &ConfigInterface {
&self.config
}
pub fn get_project_root(&self) -> &Path {
self.path.parent().expect(
"An incorrect configuration path was set with no parent, i.e. did not include root",
)
}
pub fn get_output_env_file(
&self,
from_cmdline: Option<PathBuf>,
) -> Result<Option<PathBuf>, GetOutputEnvFileError> {
from_cmdline
.or(self.config.output_env_file.clone())
.map(|p| {
if p.is_relative() {
let p = self.get_project_root().join(p);
let env_parent = crate::fs::parent(&p)?;
let env_parent = crate::fs::canonicalize(&env_parent)?;
if !env_parent.starts_with(self.get_project_root()) {
Err(GetOutputEnvFileError::OutputEnvFileMustBeInProjectRoot(p))
} else {
Ok(self.get_project_root().join(p))
}
} else {
Err(GetOutputEnvFileError::OutputEnvFileMustBeRelative(p))
}
})
.transpose()
}
pub fn save(&self) -> Result<(), StructuredFileError> {
save_json_file(&self.path, &self.json)
}
}
impl<'de> Deserialize<'de> for CanisterTypeProperties {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(PropertiesVisitor)
}
}
struct PropertiesVisitor;
impl<'de> Visitor<'de> for PropertiesVisitor {
type Value = CanisterTypeProperties;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("canister type metadata")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let missing_field = A::Error::missing_field;
let mut wasm = None;
let mut candid = None;
let mut package = None;
let mut crate_name = None;
let mut source = None;
let mut build = None;
let mut r#type = None;
let mut id = None;
let mut workspace = None;
while let Some(key) = map.next_key::<String>()? {
match &*key {
"package" => package = Some(map.next_value()?),
"crate" => crate_name = Some(map.next_value()?),
"source" => source = Some(map.next_value()?),
"candid" => candid = Some(map.next_value()?),
"build" => build = Some(map.next_value()?),
"wasm" => wasm = Some(map.next_value()?),
"type" => r#type = Some(map.next_value::<String>()?),
"id" => id = Some(map.next_value()?),
"workspace" => workspace = Some(map.next_value()?),
_ => continue,
}
}
let props = match r#type.as_deref() {
Some("motoko") | None => CanisterTypeProperties::Motoko,
Some("rust") => CanisterTypeProperties::Rust {
candid: PathBuf::from(candid.ok_or_else(|| missing_field("candid"))?),
package: package.ok_or_else(|| missing_field("package"))?,
crate_name,
},
Some("assets") => CanisterTypeProperties::Assets {
source: source.ok_or_else(|| missing_field("source"))?,
build: build.unwrap_or_default(),
workspace,
},
Some("custom") => CanisterTypeProperties::Custom {
build: build.unwrap_or_default(),
candid: candid.ok_or_else(|| missing_field("candid"))?,
wasm: wasm.ok_or_else(|| missing_field("wasm"))?,
},
Some("pull") => CanisterTypeProperties::Pull {
id: id.ok_or_else(|| missing_field("id"))?,
},
Some(x) => return Err(A::Error::unknown_variant(x, &BUILTIN_CANISTER_TYPES)),
};
Ok(props)
}
}
#[derive(Clone)]
pub struct NetworksConfig {
path: PathBuf,
json: Value,
networks_config: NetworksConfigInterface,
}
impl NetworksConfig {
pub fn get_path(&self) -> &PathBuf {
&self.path
}
pub fn get_interface(&self) -> &NetworksConfigInterface {
&self.networks_config
}
pub fn new() -> Result<NetworksConfig, LoadNetworksConfigError> {
let dir = get_user_dfx_config_dir().map_err(GetConfigPathFailed)?;
let path = dir.join("networks.json");
if path.exists() {
NetworksConfig::from_file(&path).map_err(LoadConfigFromFileFailed)
} else {
Ok(NetworksConfig {
path,
json: Default::default(),
networks_config: NetworksConfigInterface {
networks: BTreeMap::new(),
},
})
}
}
fn from_file(path: &Path) -> Result<NetworksConfig, StructuredFileError> {
let content = crate::fs::read(path)?;
let networks: BTreeMap<String, ConfigNetwork> = serde_json::from_slice(&content)
.map_err(|e| DeserializeJsonFileFailed(Box::new(path.to_path_buf()), e))?;
let networks_config = NetworksConfigInterface { networks };
let json = serde_json::from_slice(&content)
.map_err(|e| DeserializeJsonFileFailed(Box::new(path.to_path_buf()), e))?;
let path = PathBuf::from(path);
Ok(NetworksConfig {
path,
json,
networks_config,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_dfinity_config_current_path() {
let root_dir = tempfile::tempdir().unwrap();
let root_path = root_dir.into_path().canonicalize().unwrap();
let config_path = root_path.join("foo/fah/bar").join(CONFIG_FILE_NAME);
std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
std::fs::write(&config_path, "{}").unwrap();
assert_eq!(
config_path,
Config::resolve_config_path(config_path.parent().unwrap())
.unwrap()
.unwrap(),
);
}
#[test]
fn find_dfinity_config_parent() {
let root_dir = tempfile::tempdir().unwrap();
let root_path = root_dir.into_path().canonicalize().unwrap();
let config_path = root_path.join("foo/fah/bar").join(CONFIG_FILE_NAME);
std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
std::fs::write(&config_path, "{}").unwrap();
assert!(
Config::resolve_config_path(config_path.parent().unwrap().parent().unwrap())
.unwrap()
.is_none()
);
}
#[test]
fn find_dfinity_config_subdir() {
let root_dir = tempfile::tempdir().unwrap();
let root_path = root_dir.into_path().canonicalize().unwrap();
let config_path = root_path.join("foo/fah/bar").join(CONFIG_FILE_NAME);
let subdir_path = config_path.parent().unwrap().join("baz/blue");
std::fs::create_dir_all(&subdir_path).unwrap();
std::fs::write(&config_path, "{}").unwrap();
assert_eq!(
config_path,
Config::resolve_config_path(subdir_path.as_path())
.unwrap()
.unwrap(),
);
}
#[test]
fn local_defaults_to_ephemeral() {
let config = Config::from_str(
r#"{
"networks": {
"local": {
"bind": "localhost:8000"
}
}
}"#,
)
.unwrap();
let network = config.get_config().get_network("local").unwrap();
if let ConfigNetwork::ConfigLocalProvider(local_network) = network {
assert_eq!(local_network.r#type, NetworkType::Ephemeral);
} else {
panic!("not a local provider");
}
}
#[test]
fn local_can_override_to_persistent() {
let config = Config::from_str(
r#"{
"networks": {
"local": {
"bind": "localhost:8000",
"type": "persistent"
}
}
}"#,
)
.unwrap();
let network = config.get_config().get_network("local").unwrap();
if let ConfigNetwork::ConfigLocalProvider(local_network) = network {
assert_eq!(local_network.r#type, NetworkType::Persistent);
} else {
panic!("not a local provider");
}
}
#[test]
fn network_defaults_to_persistent() {
let config = Config::from_str(
r#"{
"networks": {
"somewhere": {
"providers": [ "https://1.2.3.4:5000" ]
}
}
}"#,
)
.unwrap();
let network = config.get_config().get_network("somewhere").unwrap();
if let ConfigNetwork::ConfigNetworkProvider(network_provider) = network {
assert_eq!(network_provider.r#type, NetworkType::Persistent);
} else {
panic!("not a network provider");
}
}
#[test]
fn network_can_override_to_ephemeral() {
let config = Config::from_str(
r#"{
"networks": {
"staging": {
"providers": [ "https://1.2.3.4:5000" ],
"type": "ephemeral"
}
}
}"#,
)
.unwrap();
let network = config.get_config().get_network("staging").unwrap();
if let ConfigNetwork::ConfigNetworkProvider(network_provider) = network {
assert_eq!(network_provider.r#type, NetworkType::Ephemeral);
} else {
panic!("not a network provider");
}
assert_eq!(
config.get_config().get_network("staging").unwrap(),
&ConfigNetwork::ConfigNetworkProvider(ConfigNetworkProvider {
providers: vec![String::from("https://1.2.3.4:5000")],
r#type: NetworkType::Ephemeral,
playground: None,
})
);
}
#[test]
fn get_correct_initialization_values() {
let config = Config::from_str(
r#"{
"canisters": {
"test_project": {
"initialization_values": {
"compute_allocation" : "100",
"memory_allocation": "8GB"
}
}
}
}"#,
)
.unwrap();
let config_interface = config.get_config();
let compute_allocation = config_interface
.get_compute_allocation("test_project")
.unwrap()
.unwrap();
assert_eq!(100, compute_allocation);
let memory_allocation = config_interface
.get_memory_allocation("test_project")
.unwrap()
.unwrap();
assert_eq!("8GB".parse::<Byte>().unwrap(), memory_allocation);
let config_no_values = Config::from_str(
r#"{
"canisters": {
"test_project_two": {
}
}
}"#,
)
.unwrap();
let config_interface = config_no_values.get_config();
let compute_allocation = config_interface
.get_compute_allocation("test_project_two")
.unwrap();
let memory_allocation = config_interface
.get_memory_allocation("test_project_two")
.unwrap();
assert_eq!(None, compute_allocation);
assert_eq!(None, memory_allocation);
}
}