diff --git a/CHANGELOG.md b/CHANGELOG.md index 653367807d..ac15284cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ # UNRELEASED +### feat: extensions can define a canister type + +Please see [extension-defined-canister-types](docs/concepts/extension-defined-canister-types.md) for details. + ### Cycles wallet Updated cycles wallet to a gzipped version of `20240410` release: diff --git a/Cargo.lock b/Cargo.lock index 35305978da..ec52992065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1564,6 +1564,7 @@ dependencies = [ "directories-next", "dunce", "flate2", + "handlebars", "hex", "humantime-serde", "ic-agent", diff --git a/Cargo.toml b/Cargo.toml index a79cd46e9f..1a689930e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ dialoguer = "0.11.0" directories-next = "2.0.0" flate2 = { version = "1.0.11", default-features = false } futures = "0.3.21" +handlebars = "4.3.3" hex = "0.4.3" humantime = "2.1.0" itertools = "0.10.3" diff --git a/docs/concepts/extension-defined-canister-types.md b/docs/concepts/extension-defined-canister-types.md new file mode 100644 index 0000000000..802c48c784 --- /dev/null +++ b/docs/concepts/extension-defined-canister-types.md @@ -0,0 +1,88 @@ +# Extension-Defined Canister Types + +## Overview + +An extension can define a canister type. + +# Specification + +The `canister_type` field in an extension's `extension.json` defines the +characteristics of the canister type. It has the following fields: + +| Field | Type | Description | +|-------|------|--------------------------------------------------| +| `defaults` | Object | Default values for canister fields. | +| `evaluation_order` | Array | Default fields to evaluate first. | + +The `canister_type.defaults` field is an object that defines canister properties, +as if they were found in dfx.json. Any fields present in dfx.json +override those found in the extension-defined canister type. + +The `metadata` and `tech_stack` fields have special handling. + +All elements defined in the `metadata` array in the canister type are appended +to the `metadata` array found in dfx.json. This has the effect that any +metadata specified in dfx.json will take precedence over those that the +extension defines. + +If the `tech_stack` field is present in both extension.json and dfx.json, +then dfx merges the two together. Individual items found in dfx.json +will take precedence over those found in extension.json. + +## Handlebar replacement + +dfx will perform [handlebars] string replacement on every string field in the +canister type definition. The following data are available for replacement: + +| Handlebar | Description | +|------------------------|---------------------------------------------------------------------------------------------------| +| `{{canister_name}}` | The name of the canister. | +| `{{canister.}}` | Any field from the canister definition in dfx.json, or `canister_type.defaults` in extension.json | + +# Examples + +Suppose a fictional extension called `addyr` defined a canister type in its +extension.json as follows: +```json +{ + "name": "addyr", + "canister_type": { + "defaults": { + "build": "python -m addyr {{canister_name}} {{canister.main}} {{canister.candid}}", + "gzip": true, + "post_install": ".addyr/{{canister_name}}/post_install.sh", + "wasm": ".addyr/{{canister_name}}/{{canister_name}}.wasm" + } + } +} +``` + +And dfx.json contained this canister definition: +```json +{ + "canisters": { + "backend": { + "type": "addyr", + "candid": "src/hello_backend/hello_backend.did", + "main": "src/hello_backend/src/main.py" + } + } +} +``` +This would be treated as if dfx.json defined the following custom canister: +```json +{ + "canisters": { + "hello_backend": { + "build": "python -m addyr hello_backend src/hello_backend/src/main.py src/hello_backend/hello_backend.did", + "candid": "src/hello_backend/hello_backend.did", + "gzip": true, + "post_install": ".addyr/hello_backend/post_install.sh", + "type": "custom", + "wasm": ".addyr/hello_backend/hello_backend.wasm" + } + } +} +``` + +[handlebars]: https://handlebarsjs.com/ diff --git a/e2e/tests-dfx/build.bash b/e2e/tests-dfx/build.bash index 52a39b3248..b891c185d7 100644 --- a/e2e/tests-dfx/build.bash +++ b/e2e/tests-dfx/build.bash @@ -223,7 +223,7 @@ teardown() { jq '.canisters.e2e_project_backend.type="unknown_canister_type"' dfx.json | sponge dfx.json assert_command_fail dfx build # shellcheck disable=SC2016 - assert_match 'unknown variant `unknown_canister_type`' + assert_match "canister 'e2e_project_backend' has unknown type 'unknown_canister_type' and there is no installed extension by that name which could define it" # If canister type is invalid, `dfx stop` fails jq '.canisters.e2e_project_backend.type="motoko"' dfx.json | sponge dfx.json diff --git a/e2e/tests-dfx/extension.bash b/e2e/tests-dfx/extension.bash index bf2c627940..91773e4452 100644 --- a/e2e/tests-dfx/extension.bash +++ b/e2e/tests-dfx/extension.bash @@ -13,6 +13,165 @@ teardown() { standard_teardown } +@test "extension canister type" { + dfx_start + + install_asset wasm/identity + CACHE_DIR=$(dfx cache show) + mkdir -p "$CACHE_DIR"/extensions/embera + cat > "$CACHE_DIR"/extensions/embera/extension.json < dfx.json < f.json + assert_command jq -r '.tech_stack.cdk | keys | sort | join(",")' f.json + assert_eq "embera,ic-cdk" + assert_command jq -r '.tech_stack.cdk."ic-cdk".version' f.json + assert_eq "1.16.6" + assert_command jq -r '.tech_stack.language | keys | sort | join(",")' f.json + assert_eq "java,kotlin" +} + @test "extension install with an empty cache does not create a corrupt cache" { dfx cache delete dfx extension install nns --version 0.2.1 diff --git a/src/dfx-core/Cargo.toml b/src/dfx-core/Cargo.toml index fe08ca3087..0f93a05c49 100644 --- a/src/dfx-core/Cargo.toml +++ b/src/dfx-core/Cargo.toml @@ -19,6 +19,7 @@ dialoguer = { workspace = true } directories-next.workspace = true dunce = "1.0" flate2 = { workspace = true, default-features = false, features = ["zlib-ng"] } +handlebars.workspace = true hex = { workspace = true, features = ["serde"] } humantime-serde = "1.1.1" ic-agent = { workspace = true, features = ["reqwest"] } diff --git a/src/dfx-core/src/config/model/dfinity.rs b/src/dfx-core/src/config/model/dfinity.rs index 0567ef7dfc..f2391af572 100644 --- a/src/dfx-core/src/config/model/dfinity.rs +++ b/src/dfx-core/src/config/model/dfinity.rs @@ -3,6 +3,7 @@ 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; @@ -21,7 +22,7 @@ use crate::error::dfx_config::{ }; use crate::error::load_dfx_config::LoadDfxConfigError; use crate::error::load_dfx_config::LoadDfxConfigError::{ - DetermineCurrentWorkingDirFailed, LoadFromFileFailed, ResolveConfigPathFailed, + DetermineCurrentWorkingDirFailed, ResolveConfigPathFailed, }; use crate::error::load_networks_config::LoadNetworksConfigError; use crate::error::load_networks_config::LoadNetworksConfigError::{ @@ -35,6 +36,7 @@ use crate::error::structured_file::StructuredFileError; use crate::error::structured_file::StructuredFileError::{ DeserializeJsonFileFailed, ReadJsonFileFailed, }; +use crate::extension::manager::ExtensionManager; use crate::fs::create_dir_all; use crate::json::save_json_file; use crate::json::structure::{PossiblyStr, SerdeVec}; @@ -55,6 +57,8 @@ 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, @@ -1028,34 +1032,48 @@ impl Config { Ok(None) } - fn from_file(path: &Path) -> Result { - let content = crate::fs::read(path).map_err(ReadJsonFileFailed)?; - Config::from_slice(path.to_path_buf(), &content) + fn from_file( + path: &Path, + extension_manager: Option<&ExtensionManager>, + ) -> Result { + let content = crate::fs::read(path).map_err(LoadDfxConfigError::ReadFile)?; + Config::from_slice(path.to_path_buf(), &content, extension_manager) } - pub fn from_dir(working_dir: &Path) -> Result, LoadDfxConfigError> { + pub fn from_dir( + working_dir: &Path, + extension_manager: Option<&ExtensionManager>, + ) -> Result, LoadDfxConfigError> { let path = Config::resolve_config_path(working_dir)?; - path.map(|path| Config::from_file(&path)) + path.map(|path| Config::from_file(&path, extension_manager)) .transpose() - .map_err(LoadFromFileFailed) } - pub fn from_current_dir() -> Result, LoadDfxConfigError> { - Config::from_dir(&std::env::current_dir().map_err(DetermineCurrentWorkingDirFailed)?) + pub fn from_current_dir( + extension_manager: Option<&ExtensionManager>, + ) -> Result, 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]) -> Result { - let config = serde_json::from_slice(content) - .map_err(|e| DeserializeJsonFileFailed(Box::new(path.clone()), e))?; - let json = serde_json::from_slice(content) - .map_err(|e| DeserializeJsonFileFailed(Box::new(path.clone()), e))?; + fn from_slice( + path: PathBuf, + content: &[u8], + extension_manager: Option<&ExtensionManager>, + ) -> Result { + 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 }) } /// Create a configuration from a string. #[cfg(test)] pub(crate) fn from_str(content: &str) -> Result { - Config::from_slice(PathBuf::from("-"), content.as_bytes()) + Ok(Config::from_slice(PathBuf::from("-"), content.as_bytes(), None).unwrap()) } #[cfg(test)] @@ -1063,7 +1081,7 @@ impl Config { path: PathBuf, content: &str, ) -> Result { - Config::from_slice(path, content.as_bytes()) + Ok(Config::from_slice(path, content.as_bytes(), None).unwrap()) } pub fn get_path(&self) -> &PathBuf { @@ -1189,12 +1207,7 @@ impl<'de> Visitor<'de> for PropertiesVisitor { Some("pull") => CanisterTypeProperties::Pull { id: id.ok_or_else(|| missing_field("id"))?, }, - Some(x) => { - return Err(A::Error::unknown_variant( - x, - &["motoko", "rust", "assets", "custom"], - )) - } + Some(x) => return Err(A::Error::unknown_variant(x, &BUILTIN_CANISTER_TYPES)), }; Ok(props) } diff --git a/src/dfx-core/src/config/model/extension_canister_type.rs b/src/dfx-core/src/config/model/extension_canister_type.rs new file mode 100644 index 0000000000..9a3c6363fc --- /dev/null +++ b/src/dfx-core/src/config/model/extension_canister_type.rs @@ -0,0 +1,226 @@ +use crate::config::model::dfinity::BUILTIN_CANISTER_TYPES; +use crate::error::config::{ + AppendMetadataError, ApplyExtensionCanisterTypeDefaultsError, ApplyExtensionCanisterTypeError, + ApplyExtensionCanisterTypesError, MergeTechStackError, RenderErrorWithContext, +}; +use crate::extension::manager::ExtensionManager; +use crate::extension::manifest::{extension::ExtensionCanisterType, ExtensionManifest}; +use handlebars::{Handlebars, RenderError}; +use serde_json::{Map, Value}; +use std::collections::{BTreeMap, BTreeSet}; + +pub fn apply_extension_canister_types( + mut json: Value, + extension_manager: Option<&ExtensionManager>, +) -> Result { + let Some(extension_manager) = extension_manager else { + return Ok(json); + }; + let Some(canisters) = json.get_mut("canisters") else { + return Ok(json); + }; + + let canisters: &mut Map = canisters + .as_object_mut() + .ok_or(ApplyExtensionCanisterTypesError::CanistersFieldIsNotAnObject())?; + for (canister_name, v) in canisters.iter_mut() { + let canister_json = + v.as_object_mut() + .ok_or(ApplyExtensionCanisterTypesError::CanisterIsNotAnObject( + canister_name.to_string(), + ))?; + apply_extension_canister_type(canister_name, canister_json, extension_manager)? + } + Ok(json) +} + +fn apply_extension_canister_type( + canister_name: &str, + fields: &mut Map, + extension_manager: &ExtensionManager, +) -> Result<(), ApplyExtensionCanisterTypeError> { + let canister_type = fields + .get("type") + .and_then(|t| t.as_str()) + .unwrap_or("custom") + .to_string(); + + if BUILTIN_CANISTER_TYPES.contains(&canister_type.as_str()) { + return Ok(()); + } + + let extension = canister_type; + if !ExtensionManifest::exists(&extension, &extension_manager.dir) { + return Err( + ApplyExtensionCanisterTypeError::NoExtensionForUnknownCanisterType { + canister: canister_name.to_string(), + extension, + }, + ); + } + let manifest = ExtensionManifest::load(&extension, &extension_manager.dir) + .map_err(ApplyExtensionCanisterTypeError::LoadExtensionManifest)?; + let extension_canister_type = manifest.canister_type.ok_or( + ApplyExtensionCanisterTypeError::ExtensionDoesNotDefineCanisterType { + canister: canister_name.to_string(), + extension: extension.clone(), + }, + )?; + + apply_defaults(canister_name, fields, &extension_canister_type).map_err(|source| { + ApplyExtensionCanisterTypeError::ApplyDefaults { + canister: Box::new(canister_name.to_string()), + extension: Box::new(extension), + source, + } + })?; + fields.insert("type".to_string(), Value::String("custom".to_string())); + Ok(()) +} + +fn apply_defaults( + canister_name: &str, + canister_json: &mut Map, + extension_canister_type: &ExtensionCanisterType, +) -> Result<(), ApplyExtensionCanisterTypeDefaultsError> { + let evaluate_keys = keys_in_evaluation_order(extension_canister_type); + + let handlebars = Handlebars::new(); + + for k in evaluate_keys { + let data = build_render_data(canister_name, canister_json, extension_canister_type); + let v = extension_canister_type.defaults.get(&k).unwrap(); + let v = recursive_render_templates(v.clone(), &handlebars, &data).map_err(|source| { + ApplyExtensionCanisterTypeDefaultsError::Render(Box::new(RenderErrorWithContext { + field: k.to_string(), + value: v.to_string(), + source, + })) + })?; + + let handled_metadata = k == "metadata" && append_metadata(canister_json, &v)?; + let handled_tech_stack = k == "tech_stack" && merge_tech_stack(canister_json, &v)?; + let handled = handled_metadata || handled_tech_stack; + let already_in_dfx_json = canister_json.contains_key(&k); + if !handled && !already_in_dfx_json { + canister_json.insert(k.clone(), v); + } + } + Ok(()) +} + +fn keys_in_evaluation_order(extension_canister_type: &ExtensionCanisterType) -> Vec { + let mut remaining_keys = extension_canister_type + .defaults + .keys() + .cloned() + .collect::>(); + let mut evaluate_keys = extension_canister_type.evaluation_order.clone(); + for k in evaluate_keys.iter() { + remaining_keys.remove(k); + } + evaluate_keys.extend(remaining_keys); + evaluate_keys +} + +fn recursive_render_templates( + v: Value, + handlebars: &Handlebars, + data: &BTreeMap, +) -> Result { + match v { + Value::String(s) => { + let s = handlebars.render_template(&s, data)?; + Ok(Value::String(s)) + } + Value::Array(arr) => { + let arr = arr + .into_iter() + .map(|v| recursive_render_templates(v, handlebars, data)) + .collect::, _>>()?; + Ok(Value::Array(arr)) + } + Value::Object(obj) => { + let obj: Result, RenderError> = obj + .into_iter() + .map(|(k, v)| { + let v: Value = recursive_render_templates(v, handlebars, data)?; + Ok((k, v)) + }) + .collect(); + Ok(Value::Object(obj?)) + } + _ => Ok(v), + } +} + +fn build_render_data( + canister_name: &str, + canister_json: &Map, + extension_canister_type: &ExtensionCanisterType, +) -> BTreeMap { + let mut data = BTreeMap::new(); + data.insert( + "canister_name".to_string(), + Value::String(canister_name.to_string()), + ); + let mut canister = Map::new(); + canister.extend(extension_canister_type.defaults.clone()); + canister.extend(canister_json.clone()); + data.insert("canister".to_string(), Value::Object(canister)); + data +} + +fn append_metadata( + canister_json: &mut Map, + extension_metadata: &Value, +) -> Result { + let Some(metadata) = canister_json.get_mut("metadata") else { + return Ok(false); + }; + + let metadata = metadata + .as_array_mut() + .ok_or(AppendMetadataError::ExpectedCanisterMetadataArray)?; + let mut extension_metadata = extension_metadata + .as_array() + .ok_or(AppendMetadataError::ExpectedExtensionCanisterTypeMetadataArray)? + .clone(); + metadata.append(&mut extension_metadata); + Ok(true) +} + +fn merge_tech_stack( + canister_json: &mut Map, + extension_tech_stack: &Value, +) -> Result { + let Some(canister_tech_stack) = canister_json.get_mut("tech_stack") else { + return Ok(false); + }; + + merge_tech_stack_maps(canister_tech_stack, extension_tech_stack)?; + + Ok(true) +} + +fn merge_tech_stack_maps( + canister_tech_stack: &mut Value, + extension_tech_stack: &Value, +) -> Result<(), MergeTechStackError> { + let canister_tech_stack = canister_tech_stack + .as_object_mut() + .ok_or(MergeTechStackError::ExpectedCanisterTechStackObject)?; + let extension_tech_stack = extension_tech_stack + .as_object() + .ok_or(MergeTechStackError::ExpectedExtensionCanisterTypeTechStackObject)?; + for (k, v) in extension_tech_stack.iter() { + if let Some(canister_value) = canister_tech_stack.get_mut(k) { + if canister_value.is_object() { + merge_tech_stack_maps(canister_value, v)?; + } + } else { + canister_tech_stack.insert(k.clone(), v.clone()); + } + } + Ok(()) +} diff --git a/src/dfx-core/src/config/model/mod.rs b/src/dfx-core/src/config/model/mod.rs index af58fdb255..9c489205b1 100644 --- a/src/dfx-core/src/config/model/mod.rs +++ b/src/dfx-core/src/config/model/mod.rs @@ -2,5 +2,6 @@ pub mod bitcoin_adapter; pub mod canister_http_adapter; pub mod canister_id_store; pub mod dfinity; +pub mod extension_canister_type; pub mod local_server_descriptor; pub mod network_descriptor; diff --git a/src/dfx-core/src/error/config.rs b/src/dfx-core/src/error/config.rs index 00534f71f7..1f053d031e 100644 --- a/src/dfx-core/src/error/config.rs +++ b/src/dfx-core/src/error/config.rs @@ -1,5 +1,7 @@ +use crate::error::extension::ExtensionError; use crate::error::fs::FsError; use crate::error::get_user_home::GetUserHomeError; +use handlebars::RenderError; use std::path::PathBuf; use thiserror::Error; @@ -35,3 +37,82 @@ pub enum GetTempPathError { #[error(transparent)] CreateDirAll(#[from] FsError), } + +#[derive(Error, Debug)] +#[error("failed to render field '{field}' with value '{value}': {source}")] +pub struct RenderErrorWithContext { + pub field: String, + pub value: String, + pub source: RenderError, +} + +#[derive(Error, Debug)] +#[error("failed to apply extension canister type '{extension}' to canister '{canister}': {source}")] +pub struct ApplyExtensionCanisterTypeErrorWithContext { + pub canister: Box, + pub extension: Box, + pub source: ApplyExtensionCanisterTypeError, +} + +#[derive(Error, Debug)] +pub enum ApplyExtensionCanisterTypesError { + #[error("the canisters field in dfx.json must be an object")] + CanistersFieldIsNotAnObject(), + + #[error("canister '{0}' in dfx.json must be an object")] + CanisterIsNotAnObject(String), + + #[error(transparent)] + ApplyExtensionCanisterType(#[from] ApplyExtensionCanisterTypeError), +} + +#[derive(Error, Debug)] +pub enum ApplyExtensionCanisterTypeError { + #[error( + "failed to apply defaults from extension '{extension}' to canister '{canister}': {source}" + )] + ApplyDefaults { + canister: Box, + extension: Box, + source: ApplyExtensionCanisterTypeDefaultsError, + }, + + #[error("canister '{canister}' has unknown type '{extension}' and there is no installed extension by that name which could define it")] + NoExtensionForUnknownCanisterType { canister: String, extension: String }, + + #[error(transparent)] + LoadExtensionManifest(ExtensionError), + + #[error("canister '{canister}' has type '{extension}', but that extension does not define a canister type")] + ExtensionDoesNotDefineCanisterType { canister: String, extension: String }, +} + +#[derive(Error, Debug)] +pub enum ApplyExtensionCanisterTypeDefaultsError { + #[error(transparent)] + AppendMetadata(#[from] AppendMetadataError), + + #[error(transparent)] + MergeTechStackError(#[from] MergeTechStackError), + + #[error(transparent)] + Render(Box), +} + +#[derive(Error, Debug)] +pub enum AppendMetadataError { + #[error("expected canister metadata to be an array")] + ExpectedCanisterMetadataArray, + + #[error("expected extension canister type metadata to be an array")] + ExpectedExtensionCanisterTypeMetadataArray, +} + +#[derive(Error, Debug)] +pub enum MergeTechStackError { + #[error("expected canister tech_stack to be an object")] + ExpectedCanisterTechStackObject, + + #[error("expected extension canister type tech_stack to be an object")] + ExpectedExtensionCanisterTypeTechStackObject, +} diff --git a/src/dfx-core/src/error/load_dfx_config.rs b/src/dfx-core/src/error/load_dfx_config.rs index 674d93954e..98a72b4f27 100644 --- a/src/dfx-core/src/error/load_dfx_config.rs +++ b/src/dfx-core/src/error/load_dfx_config.rs @@ -1,14 +1,21 @@ +use crate::error::config::ApplyExtensionCanisterTypesError; use crate::error::fs::FsError; -use crate::error::structured_file::StructuredFileError; +use std::path::PathBuf; use thiserror::Error; #[derive(Error, Debug)] pub enum LoadDfxConfigError { + #[error(transparent)] + ApplyExtensionCanisterTypesError(#[from] ApplyExtensionCanisterTypesError), + + #[error("Failed to deserialize json from {0}: {1}")] + DeserializeValueFailed(Box, serde_json::Error), + #[error("Failed to resolve config path: {0}")] ResolveConfigPathFailed(FsError), #[error("Failed to load dfx configuration: {0}")] - LoadFromFileFailed(StructuredFileError), + ReadFile(FsError), #[error("Failed to determine current working dir: {0}")] DetermineCurrentWorkingDirFailed(std::io::Error), diff --git a/src/dfx-core/src/extension/manifest/extension.rs b/src/dfx-core/src/extension/manifest/extension.rs index a9fcfa3927..7b9678efe8 100644 --- a/src/dfx-core/src/extension/manifest/extension.rs +++ b/src/dfx-core/src/extension/manifest/extension.rs @@ -1,5 +1,7 @@ use crate::error::extension::ExtensionError; use serde::{Deserialize, Deserializer}; +use serde_json::Value; +use std::path::PathBuf; use std::{ collections::{BTreeMap, HashMap}, path::Path, @@ -23,17 +25,26 @@ pub struct ExtensionManifest { pub description: Option, pub subcommands: Option, pub dependencies: Option>, + pub canister_type: Option, } impl ExtensionManifest { - pub fn new(name: &str, extensions_root_dir: &Path) -> Result { - let manifest_path = extensions_root_dir.join(name).join(MANIFEST_FILE_NAME); + pub fn load(name: &str, extensions_root_dir: &Path) -> Result { + let manifest_path = Self::manifest_path(name, extensions_root_dir); let mut m: ExtensionManifest = crate::json::load_json_file(&manifest_path) .map_err(ExtensionError::LoadExtensionManifestFailed)?; m.name = name.to_string(); Ok(m) } + pub fn exists(name: &str, extensions_root_dir: &Path) -> bool { + Self::manifest_path(name, extensions_root_dir).exists() + } + + fn manifest_path(name: &str, extensions_root_dir: &Path) -> PathBuf { + extensions_root_dir.join(name).join(MANIFEST_FILE_NAME) + } + pub fn into_clap_commands(self) -> Result, ExtensionError> { self.subcommands .unwrap_or_default() @@ -44,6 +55,23 @@ impl ExtensionManifest { } } +#[derive(Debug, Deserialize)] +pub struct ExtensionCanisterType { + /// If one field depends on another and both specify a handlebars expression, + /// list the fields in the order that they should be evaluated. + #[serde(default)] + pub evaluation_order: Vec, + + /// Default values for the canister type. These values are used when the user does not provide + /// values in dfx.json. + /// The "metadata" field, if present, is appended to the metadata field from dfx.json, which + /// has the effect of providing defaults. + /// The "tech_stack field, if present, it merged with the tech_stack field from dfx.json, + /// which also has the effect of providing defaults. + #[serde(default)] + pub defaults: BTreeMap, +} + #[derive(Debug, Deserialize, Default)] pub struct ExtensionSubcommandsOpts(BTreeMap); diff --git a/src/dfx-core/src/extension/mod.rs b/src/dfx-core/src/extension/mod.rs index 599355e70e..25d6c4188b 100644 --- a/src/dfx-core/src/extension/mod.rs +++ b/src/dfx-core/src/extension/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - pub mod manager; pub mod manifest; use crate::error::extension::ExtensionError; @@ -29,11 +27,8 @@ impl Display for Extension { } impl Extension { - pub fn into_clap_command( - self, - manager: &ExtensionManager, - ) -> Result { - let manifest = ExtensionManifest::new(&self.name, &manager.dir)?; + pub fn into_clap_command(self, manager: &ExtensionManager) -> Result { + let manifest = ExtensionManifest::load(&self.name, &manager.dir)?; let cmd = Command::new(&self.name) // don't accept unknown options .allow_missing_positional(false) diff --git a/src/dfx-core/src/network/provider.rs b/src/dfx-core/src/network/provider.rs index 0b60aada85..9d14777053 100644 --- a/src/dfx-core/src/network/provider.rs +++ b/src/dfx-core/src/network/provider.rs @@ -598,7 +598,7 @@ mod tests { .unwrap(); } - let config = Config::from_dir(&project_dir).unwrap().unwrap(); + let config = Config::from_dir(&project_dir, None).unwrap().unwrap(); let network_descriptor = create_network_descriptor( Some(Arc::new(config)), Arc::new(NetworksConfig::new().unwrap()), diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 1d9cb97ab6..7a3d0b0691 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -59,7 +59,7 @@ flate2 = { workspace = true, default-features = false, features = ["zlib-ng"] } fn-error-context = "0.2.0" futures-util = "0.3.21" futures.workspace = true -handlebars = "4.3.3" +handlebars.workspace = true hex = { workspace = true, features = ["serde"] } humantime.workspace = true hyper-rustls = { version = "0.24.1", default-features = false, features = [ diff --git a/src/dfx/src/lib/environment.rs b/src/dfx/src/lib/environment.rs index c5a1035430..716077ed58 100644 --- a/src/dfx/src/lib/environment.rs +++ b/src/dfx/src/lib/environment.rs @@ -106,7 +106,6 @@ pub struct EnvironmentImpl { impl EnvironmentImpl { pub fn new(extension_manager: ExtensionManager) -> DfxResult { let shared_networks_config = NetworksConfig::new()?; - let version = dfx_version().clone(); Ok(EnvironmentImpl { @@ -151,10 +150,11 @@ impl EnvironmentImpl { } fn load_config(&self) -> Result<(), LoadDfxConfigError> { - let project_config = Config::from_current_dir()? - .map_or(ProjectConfig::NoProject, |config| { - ProjectConfig::Loaded(Arc::new(config)) - }); + let config = Config::from_current_dir(Some(&self.extension_manager))?; + + let project_config = config.map_or(ProjectConfig::NoProject, |config| { + ProjectConfig::Loaded(Arc::new(config)) + }); self.project_config.replace(project_config); Ok(()) }