Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(extension): add support for custom canister types #3222

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dialoguer = "0.10.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"
Expand All @@ -48,6 +49,13 @@ mime_guess = "2.0.4"
num-traits = "0.2.14"
pem = "1.0.2"
proptest = "1.0.0"
regex = "1.5.5"
reqwest = { version = "0.11.9", default-features = false, features = [
"blocking",
"json",
"rustls-tls",
"native-tls-vendored",
]}
ring = "0.16.11"
schemars = "0.8"
sec1 = "0.3.0"
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests-dfx/build.bash
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,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 'ExtensionNotInstalled("unknown_canister_type")' # TODO: concrete error type
smallstepman marked this conversation as resolved.
Show resolved Hide resolved

# If canister type is invalid, `dfx stop` fails
jq '.canisters.e2e_project_backend.type="motoko"' dfx.json | sponge dfx.json
Expand Down
62 changes: 62 additions & 0 deletions e2e/tests-dfx/extension.bash
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,65 @@ EOF
assert_command dfx extension run test_extension abc --the-another-param 464646 --the-param 123 456 789
assert_eq "abc --the-another-param 464646 --the-param 123 456 789 --dfx-cache-path $CACHE_DIR"
}

@test "custom canister types" {
dfx cache install

CACHE_DIR=$(dfx cache show)
mkdir -p "$CACHE_DIR"/extensions/playground
cat > "$CACHE_DIR"/extensions/playground/extension.json <<EOF
{
"name": "playground",
"version": "0.1.0",
"homepage": "https://github.com/dfinity/playground",
"authors": "DFINITY",
"summary": "Motoko playground for the Internet Computer",
"categories": [],
"keywords": [],
"subcommands": {},
"canister_types": {
"playground": {
"type": "custom",
"build": "echo the wasm-utils canister is prebuilt",
"candid": "{{canister_name}}.did",
"wasm": "{{canister_name}}.wasm",
"gzip": true
}
}
}
EOF
cat > "$CACHE_DIR"/extensions/playground/playground <<EOF
#!/usr/bin/env bash
echo testoutput
EOF
chmod +x "$CACHE_DIR"/extensions/playground/playground

assert_command dfx extension list
assert_match "playground"

dfx_new hello
create_networks_json
install_asset playground_backend

cat > dfx.json <<EOF
{
"canisters": {
"wasm-utils": {
"type": "playground"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
EOF

dfx_start
assert_command dfx deploy -v
assert_match 'Backend canister via Candid interface'
}
4 changes: 3 additions & 1 deletion src/dfx-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ clap = { workspace = true, features = ["string"] }
dialoguer = "0.10.0"
directories-next.workspace = true
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"] }
Expand All @@ -26,7 +27,8 @@ ic-identity-hsm = { workspace = true }
k256 = { version = "0.11.4", features = ["pem"] }
keyring.workspace = true
lazy_static.workspace = true
reqwest = { version = "0.11.9", features = ["blocking", "json"] }
reqwest = { workspace = true, features = ["blocking", "json"] }
regex.workspace = true
ring.workspace = true
schemars.workspace = true
sec1 = { workspace = true, features = ["std"] }
Expand Down
59 changes: 45 additions & 14 deletions src/dfx-core/src/config/model/dfinity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ 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, ReadJsonFileFailed,
};
use crate::error::structured_file::{
ReadConfigurationError, StructuredFileError, TransformConfigurationError,
};
use crate::extension::manifest::custom_canister_type::TransformConfiguration;
use crate::json::save_json_file;
use crate::json::structure::{PossiblyStr, SerdeVec};
use byte_unit::Byte;
Expand Down Expand Up @@ -951,42 +954,70 @@ impl Config {
Ok(None)
}

fn from_file(path: &Path) -> Result<Config, StructuredFileError> {
fn from_file<T: TransformConfiguration>(
path: &Path,
transformer: &mut T,
) -> Result<Config, ReadConfigurationError> {
let content = crate::fs::read(path).map_err(ReadJsonFileFailed)?;
Config::from_slice(path.to_path_buf(), &content)
Config::from_slice(path.to_path_buf(), &content, transformer)
}

pub fn from_dir(working_dir: &Path) -> Result<Option<Config>, LoadDfxConfigError> {
pub fn from_dir<T: TransformConfiguration>(
working_dir: &Path,
transformer: &mut T,
) -> Result<Option<Config>, LoadDfxConfigError> {
let path = Config::resolve_config_path(working_dir)?;
path.map(|path| Config::from_file(&path))
path.map(|path| Config::from_file(&path, transformer))
.transpose()
.map_err(LoadFromFileFailed)
}

pub fn from_current_dir() -> Result<Option<Config>, LoadDfxConfigError> {
Config::from_dir(&std::env::current_dir().map_err(DetermineCurrentWorkingDirFailed)?)
pub fn from_current_dir<T: TransformConfiguration>(
transformer: &mut T,
) -> Result<Option<Config>, LoadDfxConfigError> {
Config::from_dir(
&std::env::current_dir().map_err(DetermineCurrentWorkingDirFailed)?,
transformer,
)
}

fn from_slice(path: PathBuf, content: &[u8]) -> Result<Config, StructuredFileError> {
let config = serde_json::from_slice(content)
fn from_slice<T: TransformConfiguration>(
path: PathBuf,
content: &[u8],
transformer: &mut T,
) -> Result<Config, ReadConfigurationError> {
let mut json: serde_json::Value = serde_json::from_slice(content)
.map_err(|e| DeserializeJsonFileFailed(Box::new(path.clone()), e))?;
let json = serde_json::from_slice(content)

transformer
.transform(&mut json)
.map_err(TransformConfigurationError::from)?;

let config = serde_json::from_value(json.clone())
.map_err(|e| DeserializeJsonFileFailed(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, StructuredFileError> {
Config::from_slice(PathBuf::from("-"), content.as_bytes())
pub(crate) fn from_str(content: &str) -> Result<Config, ReadConfigurationError> {
let mut no_op_transformer =
crate::extension::manifest::custom_canister_type::NoopTransformConfiguration;
Config::from_slice(
PathBuf::from("-"),
content.as_bytes(),
&mut no_op_transformer,
)
}

#[cfg(test)]
pub(crate) fn from_str_and_path(
path: PathBuf,
content: &str,
) -> Result<Config, StructuredFileError> {
Config::from_slice(path, content.as_bytes())
) -> Result<Config, ReadConfigurationError> {
let mut no_op_transformer =
crate::extension::manifest::custom_canister_type::NoopTransformConfiguration;
Config::from_slice(path, content.as_bytes(), &mut no_op_transformer)
}

pub fn get_path(&self) -> &PathBuf {
Expand Down
10 changes: 10 additions & 0 deletions src/dfx-core/src/error/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,14 @@ pub enum ExtensionError {

#[error("Extension exited with non-zero status code '{0}'.")]
ExtensionExitedWithNonZeroStatus(i32),

// errors related to custom canister types
#[error("Extension '{0}' does not support any custom canister types.")]
ExtensionDoesNotSupportAnyCustomCanisterTypes(String),

#[error("Extension '{0}' does not support the specific custom canister type '{1}'.")]
ExtensionDoesNotSupportSpecificCustomCanisterType(String, String),

#[error("Failed to load custom canister type template for canister type '{0}' from extension '{1}': {2}")]
CustomCanisterTypeTemplateError(String, String, String),
}
8 changes: 4 additions & 4 deletions src/dfx-core/src/error/load_dfx_config.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use crate::error::fs::FsError;
use crate::error::structured_file::StructuredFileError;
use crate::error::structured_file::ReadConfigurationError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum LoadDfxConfigError {
#[error("Failed to resolve config path: {0}")]
ResolveConfigPathFailed(FsError),

#[error("Failed to load dfx configuration: {0}")]
LoadFromFileFailed(StructuredFileError),

#[error("Failed to determine current working dir: {0}")]
DetermineCurrentWorkingDirFailed(std::io::Error),

#[error("Failed to load dfx configuration: {0}")]
LoadFromFileFailed(ReadConfigurationError),
}
18 changes: 18 additions & 0 deletions src/dfx-core/src/error/structured_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use crate::error::fs::FsError;
use std::path::PathBuf;
use thiserror::Error;

use super::extension::ExtensionError;

#[derive(Error, Debug)]
pub enum StructuredFileError {
#[error("Failed to parse contents of {0} as json: {1}")]
Expand All @@ -16,3 +18,19 @@ pub enum StructuredFileError {
#[error("Failed to write JSON file: {0}")]
WriteJsonFileFailed(FsError),
}

#[derive(Error, Debug)]
pub enum ReadConfigurationError {
#[error(transparent)]
StructuredFile(#[from] StructuredFileError),
#[error(transparent)]
TransformConfiguration(#[from] TransformConfigurationError),
}

#[derive(Error, Debug)]
pub enum TransformConfigurationError {
#[error("Configuration transformation failed: {0}")]
ConfigurationTransformationFailed(String), // Or another error type if necessary
#[error("Extension error: {0}")]
ExtensionError(#[from] ExtensionError), // Note that `from` here allows automatic conversion
}
Loading
Loading