How to generate .did files automatically rather than manually?

Hey guys, recently I’ve been running “candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > ./backend/canisters/backend/backend.did” before deploying canister to generate .did files, but it’s a little troublesome.I’ve written a script to generate Candid files but my teammate think it would be better to integrate it into dfx.json. I’ve learned that dfx.json has a build parameter that automatically executes commands at compile time, but it doesn’t seem to work when I put this code in there.
Here are the codes below:

"dfx": "0.17.0",
  "canisters": {
    "assets": {
      "dependencies": [],
      "frontend": {
        "entrypoint": "dist/index.html"
      },
      "source": ["dist/"],
      "type": "assets"
    },
    "backend": {
      "candid": "backend/canisters/backend/backend.did",
      "package": "backend",
      "type": "rust",
      "build":[
        "cargo build --release --target wasm32-unknown-unknown --package backend",
        "candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > ./backend/canisters/backend/backend.did"
      ]
    }
  },

If you run this as a single command in the command line, what is the end result? Do you receive an error?

Hi! @jennifertran , I run this command successfully and no errors are reported during the run.

1 Like

Does it generate the Candid files then? What error do you get when you try to run dfx build then?

Yeah, the Candid files are generated then, @jennifertran, but when I run dfx build an error occurs

Let me try it out and get back to you by tomorrow.

Wow~Thanks a lot, @jennifertran.I just want to figure it out so that I don’t have to generate Candid files manually.

Hey,

I have wrote this sh script and use it on all of my project.

filename : predeploy.sh

#!/usr/bin/env bash
set -eu

backend_dir="./backend"
target_dir="./target/wasm32-unknown-unknown/release"

yellow='\033[1;33m'
green='\033[0;32m'
red='\033[0;31m'
blue='\033[0;34m'
no_color='\033[0m'

# Check if a specific package name is provided
specified_package=${1:-}

# Check if the name is valid
if [ -n "$specified_package" ] && [ ! -d "$backend_dir/$specified_package" ]; then
    printf "${red}No package named $specified_package found in $backend_dir. Skipping building $specified_package.${no_color}\n"
    exit 0
fi

for app_root in "$backend_dir"/*; do
    package=$(basename "$app_root")
    # Skip packages that do not match the specified package (if one is specified)
    if [ -n "$specified_package" ] && [ "$specified_package" != "$package" ]; then
        continue
    fi

    did_file="$app_root/$package.did"
    optimised_target_dir="./canisters/$package"


    if [ ! -f "$app_root/Cargo.toml" ]; then
        printf "${yellow}No Cargo.toml found in $app_root. Skipping $package.${no_color}\n"
        continue
    fi
    
    printf "${green}Building $package in $app_root${no_color}\n"
    cargo build --manifest-path="$app_root/Cargo.toml" \
        --target wasm32-unknown-unknown \
        --release \
        --package "$package"
    printf "Size of $package.wasm: $(ls -lh "$target_dir/$package.wasm" | awk '{print $5}')\n"

    if command -v candid-extractor >/dev/null 2>&1; then
        printf "${green}Generating Candid file for $package${no_color}\n"
        candid-extractor "$target_dir/$package.wasm" > "$did_file"
        printf "Size of $package.did: $(ls -lh "$did_file" | awk '{print $5}')\n"
    else
        printf "${yellow}candid-extractor not found. Skipping generating $package.did.${no_color}\n"
    fi

    # Check if ic-wasm is installed before attempting to shrink the wasm file
    # you can install ic-wasm via `cargo install ic-wasm` for smaller wasm files
    if command -v ic-wasm >/dev/null 2>&1; then
        # create the optimised target dir
        mkdir -p "$optimised_target_dir"
        # copy the candid file to the optimised target dir
        cp "$did_file" "$optimised_target_dir/$package.did"

        # add candid file into wasm file as metadata
        printf "${green}Adding Candid file into $package.wasm${no_color}\n"
        ic-wasm "$target_dir/$package.wasm" -o "$optimised_target_dir/$package.wasm" metadata candid:service -f "$optimised_target_dir/$package.did" -v public
        printf "Size of $package.wasm with Candid metadata: $(ls -lh "$optimised_target_dir/$package.wasm" | awk '{print $5}')\n"
        
        # shrink wasm file
        printf "${green}Shrinking $package.wasm${no_color}\n"
        ic-wasm "$optimised_target_dir/$package.wasm" -o "$optimised_target_dir/$package.wasm" optimize O3
        printf "Size of shrunk $package.wasm: $(ls -lh "$optimised_target_dir/$package.wasm" | awk '{print $5}')\n"
        
        # Gunzip target
        printf "${green}Gunzipping $package.wasm${no_color}\n"
        gzip -c "$optimised_target_dir/$package.wasm" > "$optimised_target_dir/$package.wasm.gz"
        printf "Size of Gunzipped $package.wasm.gz: $(ls -lh "$optimised_target_dir/$package.wasm.gz" | awk '{print $5}')\n"
    else
        printf "${yellow}ic-wasm not found. Skipping shrinking $package.${no_color}\n"
    fi
    # print the file directory for the package
    printf "${blue}Files for $package are in $optimised_target_dir${no_color}\n"

    dfx generate "$package"

done

Script Overview

The script, predeploy.sh, automates the build process for Rust-based canisters, including generating .did files and optimizing the WebAssembly (Wasm) output. Here’s a step-by-step explanation:

  1. Setup and Variables:

    • The script sets up input (./backend) and output (./canisters) directories.
    • It checks if a specific package name is provided as an argument.
  2. Package Validation:

    • If a package name is specified, the script verifies its existence in the backend directory. If not found, it skips the build for that package.
  3. Build Loop:

    • The script iterates over all packages in the backend directory.
    • For each package, it checks for the presence of a Cargo.toml file to ensure it’s a valid Rust package.
  4. Building the Package:

    • It builds the package using cargo build with the target set to wasm32-unknown-unknown and in release mode.
    • The script then prints the size of the generated Wasm file.
  5. Generating Candid Files:

    • If candid-extractor is installed, the script generates a .did file from the Wasm file and prints its size.
    • If candid-extractor is not found, it skips this step.
  6. Optimizing Wasm Files:

    • If ic-wasm is installed, the script performs several optimizations:
      • Adds Candid metadata to the Wasm file.
      • Shrinks the Wasm file for smaller size.
      • Compresses the Wasm file using gzip.
    • It prints the sizes of the optimized and compressed Wasm files.
  7. Output Directory:

    • The script creates an optimized target directory for each package and copies the .did file there.
    • It prints the directory where the files are stored.
  8. DFX Generate:

    • Finally, the script runs dfx generate for each package to generate TypeScript bindings.

Usage

You can use the script by running:

sh ./predeploy.sh <canister name>

dfx.json Configuration

Here’s how you can configure your dfx.json to use this script:

"b3forge": {
  "name": "B3Forge",
  "build": "scripts/predeploy.sh b3forge",
  "candid": "canisters/b3forge/b3forge.did",
  "declarations": {
    "output": "frontend/declarations/b3forge",
    "node_compatibility": true
  },
  "package": "b3forge",
  "type": "custom",
  "wasm": "canisters/b3forge/b3forge.wasm"
}

This setup ensures that the build process is automated, and the .did files are generated and optimized without manual intervention. I hope this helps!

Wow~Thanks a lot,@b3hr4d, actually I 've written a script to generate Candid files, it is written by python language, but my teammate think that it would be better to integrate this function into dfx.json. Afterall, I’m really appreciate it that you give me help, deeply thank you!

Oh, @b3hr4d ,I just talked to my teammate and he thinks that your solution is a good option if we can’t integrate this into dfx.json, thanks again for your help!

Happy it helped! It’s actually integrated with dfx.json! If you put the script inside the dfx.json, it will build and use it directly from there, just like I mentioned.

This way, you can streamline your build process without needing to run the script manually. Thanks again for your kind words!