Skip to content

Commit

Permalink
feat: extension canister type
Browse files Browse the repository at this point in the history
  • Loading branch information
ericswanson-dfinity committed Apr 17, 2024
1 parent 560a40f commit d93437a
Show file tree
Hide file tree
Showing 17 changed files with 644 additions and 37 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

# 0.20.0

### fix: set `CANISTER_CANDID_PATH_<canister name>` properly for remote canisters
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
88 changes: 88 additions & 0 deletions docs/concepts/extension-defined-canister-types.md
Original file line number Diff line number Diff line change
@@ -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.<field>}}` | 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/
2 changes: 1 addition & 1 deletion e2e/tests-dfx/build.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
161 changes: 161 additions & 0 deletions e2e/tests-dfx/extension.bash
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,167 @@ 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 <<EOF
{
"name": "embera",
"version": "0.1.0",
"homepage": "https://github.com/dfinity/dfx-extensions",
"authors": "DFINITY",
"summary": "Test extension for e2e purposes.",
"categories": [],
"keywords": [],
"canister_type": {
"evaluation_order": [ "wasm" ],
"defaults": {
"type": "custom",
"build": [
"echo the embera build step for canister {{canister_name}} with candid {{canister.candid}} and main file {{canister.main}} and gzip is {{canister.gzip}}",
"mkdir -p .embera/{{canister_name}}",
"cp main.wasm {{canister.wasm}}"
],
"gzip": true,
"wasm": ".embera/{{canister_name}}/{{canister_name}}.wasm",
"post_install": [
"echo the embera post-install step for canister {{canister_name}} with candid {{canister.candid}} and main file {{canister.main}} and gzip is {{canister.gzip}}"
],
"metadata": [
{
"name": "metadata-from-extension",
"content": "the content (from extension definition), gzip is {{canister.gzip}}"
},
{
"name": "metadata-in-extension-overwritten-by-dfx-json",
"content": "the content to be overwritten (from extension definition)"
},
{
"name": "extension-limits-to-local-network",
"networks": [ "local" ],
"content": "this only applies to the local network"
},
{
"name": "extension-limits-to-ic-network",
"networks": [ "ic" ],
"content": "this only applies to the ic network"
}
],
"tech_stack": {
"cdk": {
"embera": {
"version": "1.5.2"
},
"ic-cdk": {
"version": "2.14.4"
}
},
"language": {
"kotlin": {}
}
}
}
}
}
EOF
cat > dfx.json <<EOF
{
"canisters": {
"c1": {
"type": "embera",
"candid": "main.did",
"main": "main-file.embera",
"metadata": [
{
"name": "metadata-in-extension-overwritten-by-dfx-json",
"content": "the overwritten content (from dfx.json)"
}
]
},
"c2": {
"type": "embera",
"candid": "main.did",
"gzip": false,
"main": "main-file.embera"
},
"c3": {
"type": "embera",
"candid": "main.did",
"main": "main-file.embera",
"metadata": [
{
"name": "extension-limits-to-local-network",
"networks": [ "ic" ],
"content": "this only applies to the ic network"
},
{
"name": "extension-limits-to-ic-network",
"networks": [ "local" ],
"content": "dfx.json configuration applies only to local network"
}
]
},
"c4": {
"type": "embera",
"candid": "main.did",
"main": "main-file.embera",
"tech_stack": {
"cdk": {
"ic-cdk": {
"version": "1.16.6"
}
},
"language": {
"java": {
"version": "25.0.6"
}
}
}
}
}
}
EOF
#echo "service: {}" >service.did


assert_command dfx extension list
assert_contains embera
assert_command dfx deploy
assert_contains "the embera build step for canister c1 with candid main.did and main file main-file.embera and gzip is true"
assert_contains "the embera post-install step for canister c1 with candid main.did and main file main-file.embera and gzip is true"
assert_contains "the embera build step for canister c2 with candid main.did and main file main-file.embera and gzip is false"
assert_contains "the embera post-install step for canister c2 with candid main.did and main file main-file.embera and gzip is false"

assert_command dfx canister metadata c1 metadata-in-extension-overwritten-by-dfx-json
assert_eq "the overwritten content (from dfx.json)"
assert_command dfx canister metadata c1 metadata-from-extension
assert_eq "the content (from extension definition), gzip is true"

assert_command dfx canister metadata c2 metadata-in-extension-overwritten-by-dfx-json
assert_eq "the content to be overwritten (from extension definition)"
assert_command dfx canister metadata c2 metadata-from-extension
assert_eq "the content (from extension definition), gzip is false"

assert_command dfx canister metadata c3 extension-limits-to-local-network
assert_eq "this only applies to the local network"

assert_command dfx canister metadata c3 extension-limits-to-ic-network
assert_eq "dfx.json configuration applies only to local network"

assert_command dfx canister metadata c4 dfx
# shellcheck disable=SC2154
echo "$stdout" > 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
Expand Down
1 change: 1 addition & 0 deletions src/dfx-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
Loading

0 comments on commit d93437a

Please sign in to comment.