ENV variables for motoko builds

I’m not sure what you have in mind, but the equivalent of npx webpack --env production could be:

  1. Create production/lib.mo and development/lib.mo
  2. Call moc --package env production or moc --package env development
2 Likes

This is actually a pretty important feature request, as any canister that makes inter-canister calls to an external canister (like ICP ledger, governance, or some IS20 token) will need to hardcode the canister IDs of those external canisters in their Motoko in order to call them.

And the canister IDs will be different in local net vs prod net.

I wonder if there’s some pattern people use to deploy to local vs deploy to prod. Ideally, they would be one and the same but as I mentioned there are differences.

It’d be nice if there was a way in Motoko to check if the --network=ic flag was passed to dfx deploy. Unfortunately, I’m not sure if dfx build cares about that…

EDIT: For example, one way would be to expose environment variables in Motoko and have dfx build pass that in. These environment variables would be a good start.

3 Likes

Yup this is really necessary

I realized that there shouldn’t be a need to change to a custom canister type and that you should be able to use the packtool field instead.

Replace shared in those examples with env or whatever you named your package.

Check this out, this does solve the issue you are describing
in a few days a blog post is coming out that explains a lot about the setup but it comes down to generating a file with rust that is environment specific

You can test the repo by pulling it and doing a ENV=environment cargo test

Thanks Remco!

Btw. I’m always happy to see a readme and then disappointed that is almost always the starter readme :sweat_smile:

Haha sorry I will edit it, if I have some time to spare

1 Like

Has there been any progress on this front? Or is hardcoding canister ids still the only way to do intercanister calls?

You can define ‘remote’ canisters that your project doesn’t control in certain environments. You can then import these canisters the default way. For an example, have a look here

Would that be by using import “canister:canistername”? Cause it has some drawbacks, namely:

  • It doesn’t work with actor classes
  • It bloats the canister with all the imported canisters candid interface
  • The tooling support is flaky, getting it to work requires deploying the project in a specific way and even then the Motoko extension always consider the import as an error even though the code compiles and deploys fine, that error prevents the extension from highlighting other errors in the code, e.g:
    example

It’d be nice if such annoyances were fixed and having a process.env.CANISTER_ID equivalent in Motoko would help too when one wants to use actor types.

2 Likes

Yes, that would be with import "canister:canistername". I’ll ask the Motoko people how they would like to solve this.

Sorry, I don’t have a good answer to this.

Perhaps, just for referencing environment variables, we could allow a new form of string literal $id (or similar), that statically resolves to the text value of environment variable id when defined, and statically errors otherwise, but I don’t think that would be good enough for solving the conditional canister import problem, for which you need to know the canister id and where to find its did file. It also seems slightly dangerous.

For the problem of using different canister ids for different networks, on dfx 12 at least this seems to work, though it’s cumbersome.

dfx.json

{
  "version": 1,
    "canisters": {
      "import": {
      "type" : "custom",
      "candid" : "import.did",
      "wasm" : "",
      "remote": {
        "candid" : "import.did",
        "id": {
           "ic": "aaaaa-aa",
           "local": "rdmx6-jaaaa-aaaaa-aaadq-cai"
         }
      }
    },
    "mocenv_backend": {
      "type": "motoko",
      "args": "",
      "main": "src/mocenv_backend/main.mo"
    },
    "mocenv_frontend": {
      "type": "assets",
      "source": [
        "src/mocenv_frontend/assets"
      ],
      "dependencies": [
        "mocenv_backend"
      ]
    }
  },
  "defaults": {
    "build": {
      "packtool": "",
      "args": ""
    }
  }
}

Here I declare a dummy custom canister, “import”, with different ids on networks “ic” and “local” and just use the following file for the main motoko canister.

src/mocenv_backend/main.mo

import I "canister:import";
import Principal "mo:base/Principal";
actor {
  public query func greet(name : Text) : async Text {
    return (debug_show (Principal.fromActor(I)));
  };
};

It seems to build ok, passing the correct ids when building for “ic” or “local” network, as verified with `dfx build -vvvv" (for verbose output).

2 Likes

The problem is current methods have flaws which make even basic cases tedious to deal with.
I’ll give you a practical example of a situation I’m currently in:

I need to deploy a service which interacts with the ckBTC ledger on mainnet and to test it, I deployed a test ledger both locally and on the IC, which I’m currently using to test ledger interactions and will eventually be swapped with the actual ckBTC ledger.
Now even if canister imports worked flawlessly, i.e no candid bloat but only used types, no extension errors, etc… I’d still be out of luck cause the Motoko ICRC1 ledger is an actor class and requires init arguments.
So I must resort to actor types and my code ends up looking like this ( I have to repeat the same process on the frontend too btw ):
example

If actor types had access to the canister id based on dfx.json that would already help a lot, though I’d still have to remember to toggle the remote option when switching between test and ckBTC ledger.

Generally speaking it’s a shame importing actors is in such a state, it seems to be mainly found in sample code and is more of a concept than something that can be relied upon, even experienced Motoko devs advise against using it in favour of actor types.

Would something like this work for you:

File env.sh:

#!/bin/bash

command_type="$1"
canister_id="$2"
environment="$3"

# Assign command-line arguments to variables



# Move the appropriate file to the parent directory and rename it
if [ "$environment" == "local" ]; then
    mv /src/env/local.mo /src/env.mo
elif [ "$environment" == "ic" ]; then
    mv /src/env/ic.mo /src/env.mo
else
    echo "Invalid environment. Please provide either 'local' or 'ic'."
    exit 1
fi

# Run the appropriate dfx command
if [ "$command_type" == "build" ]; then
    dfx build "$canister_id"
else
arguments="$4"
    dfx deploy --network "$environment" "$canister_id" --arguments "$arguments"
fi

Then you just have some files in /src/env that are like:

#local.mo
module {
  public let canister_id = "rdmx6-jaaaa-aaaaa-aaadq-cai";
};

#ic.mo
module {
  public let canister_id = "25x2w-paaaa-aaaab-qackq-cai";
};
``

Your actor then just imports env.mo and the script copies it before each build/deploy.

Then you can just:
env deploy my_canister ic '(variant { myargs=null})'

Or some thing like the above....I'm not great at bash

Added solution in Mops 0.12.1

  1. Update mops cli
npm i -g ic-mops
  1. mops.toml
[dependencies]
env = "./src/env-{MOPS_ENV}"

here {MOPS_ENV} will be replaced with MOPS_ENV env var. Default value local

src/env-local/lib.mo

module {
  public let IS_PROD = false;
};

src/env-ic/lib.mo

module {
  public let IS_PROD = true;
};
  1. src/main.mo:
import {IS_PROD} "mo:env";
  1. dfx.json
"packtool": "mops sources"

Local deploy:

dfx deploy ...

or

MOPS_ENV=local dfx deploy ...

Deploy to IC

MOPS_ENV=ic dfx deploy --network ic ...
4 Likes

Fantastic!!!

That is a nice improvement but still doesn’t seem to address the issue completely, if one also needs to interact with a canister from the frontend the same kind of logic should be replicated in the client’s code too. Is it possible to expose the MOPS_ENV to node? Otherwise the only solution to solve such cases at the moment would be extending @skilesare’s script to move a js file with canister ids.

I think you can do this in js:

if (process.env.MOPS_ENV == 'staging') {...}

Actually, with dfx 0.14.0 you can deploy local canisters with the mainnet canister id:

dfx deploy ledger --specified-id ryjl3-tyaaa-aaaaa-aaaba-cai

or

dfx canister create ledger --specified-id ryjl3-tyaaa-aaaaa-aaaba-cai
dfx deploy ...
4 Likes

I tried but process.env.MOPS_ENV is undefined.

I see, thing is I have 2 possible types of mainnet deploys: the ones where I interact with a test canister I’ve deployed and the ones where I use the actual one, so the mainnet id isn’t always the same.
Perhaps the ideal way to solve this would be to have build/deploy presets along ENV variables for Motoko. This way I could define a preset for when I own the canister on both networks and another one with the remote attribute for when I only own the local test canister and integrate with an existing one on mainnet.

Probably using process.env.DFX_NETWORK will work for you