Automating your Rust workflows with cargo-make - Part 3 of 5 - Environment Variables, Conditions, Sub Tasks and Mixing

Sagie Gur-Ari
5 min readAug 29, 2017

--

In the last article we talked about platform overrides and aliases based on OS (linux, windows and mac). But in many cases we will need something more complex and cargo-make built for rust gives that extra power.

In this article you will see how cargo-make allows you to setup conditions and help your scripting with environment variables with specific Rust info (but not just).

The previous articles in this series were:

Environment Variables

Environment variables are used everywhere and cargo make helps you define and use them.

First of all, lets look at how we define environment variables to be used in our execution flow.

Global Configuration

In your Makefile.toml, there is an env section where you can define any environment variables you want. When cargo-make starts up, it will load all of them before running any task.

For example:

[env]
RUST_BACKTRACE = "1"
SOME_VAR = "SOME VALUE"

Except the environment variables you define here, there are more defined in the default internal Makefile found here.

Inside Tasks

Tasks can also trigger environment variables to be defined, however unlike the global env section, these environment variables are only defined if the task is going to be invoked.

For example:

[tasks.test-flow]
env = { "SOME_ENV_VAR" = "value" }
script = [
"echo var: ${SOME_ENV_VAR}"
]

Command Line

You can also ask cargo-make to define environment variables when you run it as part of the command line using the --env or -e cli arguments, for example:

cargo make --env ENV1=VALUE1 --env ENV2=VALUE2 -e ENV3=VALUE3

In addition to manually setting up all those environment variables, cargo-make will also automatically define a large set of them based on the current runtime.

The below list lists all of them:

  • CARGO_MAKE - Set to "true" to help sub processes identify they are running from cargo make.
  • CARGO_MAKE_TASK - Holds the name of the main task being executed.
  • CARGO_MAKE_WORKING_DIRECTORY - The current working directory (can be defined by setting the --cwd cli option)
  • CARGO_MAKE_RUST_VERSION - The rust version (for example 1.20.0)
  • CARGO_MAKE_RUST_CHANNEL - Rust channel (stable, beta, nightly)
  • CARGO_MAKE_RUST_TARGET_ARCH - x86, x86_64, arm, etc ... (see rust cfg feature)
  • CARGO_MAKE_RUST_TARGET_ENV - gnu, msvc, etc ... (see rust cfg feature)
  • CARGO_MAKE_RUST_TARGET_OS - windows, macos, ios, linux, android, etc ... (see rust cfg feature)
  • CARGO_MAKE_RUST_TARGET_POINTER_WIDTH - 32, 64
  • CARGO_MAKE_RUST_TARGET_VENDOR - apple, pc, unknown
  • CARGO_MAKE_CRATE_IS_WORKSPACE - Holds TRUE/FALSE based if this is a workspace crate or not (defined even if no Cargo.toml is found)
  • CARGO_MAKE_CRATE_WORKSPACE_MEMBERS - Holds list of member paths (defined as empty value if no Cargo.toml is found)

The following environment variables will be set by cargo-make if Cargo.toml file exists and the relevant value is defined:

  • CARGO_MAKE_CRATE_NAME
  • CARGO_MAKE_CRATE_FS_NAME - Same as CARGO_MAKE_CRATE_NAME however some characters are replaced (for example '-' to '_').
  • CARGO_MAKE_CRATE_VERSION
  • CARGO_MAKE_CRATE_DESCRIPTION
  • CARGO_MAKE_CRATE_LICENSE
  • CARGO_MAKE_CRATE_DOCUMENTATION
  • CARGO_MAKE_CRATE_HOMEPAGE
  • CARGO_MAKE_CRATE_REPOSITORY

The following environment variables will be set by cargo-make if the project is part of a git repo:

  • CARGO_MAKE_GIT_BRANCH - The current branch name.
  • CARGO_MAKE_GIT_USER_NAME - The user name pulled from the git config user.name key.
  • CARGO_MAKE_GIT_USER_EMAIL - The user email pulled from the git config user.email key.

Conditions

Platform overrides and platform aliases give you some sort of condition handling for your scripts, but cargo-make has a much more powerful condition mechanism.

There are two types of built in conditions:

  • Criteria
  • Scripts

A task can define any or both types of conditions and only if all conditions are satisfied will the task be invoked.

Important to note that conditions only affect the task action and will not prevent its dependencies from being invoked. For dependencies there is another solution which will be covered in the next sections.

Criteria

Criteria conditions are defined by the condition attribute which can define multiple different conditions to validate.

Below is an example of a condition script that checks that we are running on windows or linux (but not mac) and that we are running on beta or nightly (but not stable):

[tasks.test-condition]
condition = { platforms = ["windows", "linux"], channels = ["beta", "nightly"] }
script = [
"echo \"condition was met\""
]

The follow criteria types exist:

  • platforms — List of platform names (windows, linux, mac)
  • channels — List of rust channels (stable, beta, nightly)
  • env_set — List of environment variables that must be defined
  • env_not_set — List of environment variables that must not be defined
  • env — Map of environment variables that must be defined and equal to the provided values

Let’s look at another example with all types:

[tasks.test-condition]
condition = { platforms = ["windows", "linux"], channels = ["beta", "nightly"], env_set = [ "KCOV_VERSION" ], env_not_set = [ "CARGO_MAKE_SKIP_CODECOV" ], env = { "TRAVIS" = "true", "CARGO_MAKE_RUN_CODECOV" = "true", } }

As you can see setting up conditions using the rust channel shows yet another advantage of having a Rust aware task runner as you might want to run certain tasks for nightly channel only and so on…

Scripts

You can also run custom scripts to validate more complex conditions using the condition_script attribute.

The condition is only met in case the script exit value is 0, otherwise the task will not be invoked.

Here is an example of a condition that always fails and prevents the task from running the build command:

[tasks.never]
condition_script = [
"exit 1"
]
command = "cargo"
args = ["build"]

Sub Tasks

In the first article I talked about task being able to do three things, run commands, script or other tasks. The article goes on explaining the first two but not how to run other tasks.

A task can run another task using the run_task attribute. Unlike dependencies which are invoked before the current task, the task defined in the run_task is invoked after the current task.

For example:

[tasks.my-task]
run_task = "echo"
[tasks.echo]
script = ["echo hello"]

The run_task is also another way to invoke a specific task more than once (in previous article I talked about aliases being the first way).

Mixing Conditions and Sub Tasks

We previously showed how conditions work and explained that conditions prevent the task from being invoked, but will not stop its dependencies. We also talked about in previous articles that dependencies is the cargo-make way to define flows. So how do we have a condition prevent an entire flow?

If you mix conditions and sub tasks (run_task), you now have a way to define a conditional sub flow.

For example, if you have a flow that should only be invoked in a travis build, not on windows, and only if the rust channel is nightly. The flow is defined as follows:

[tasks.my-flow]
dependencies = ["task1", "task2", "task3"]

But we need to setup conditions to prevent tasks1, 2, 3 from being invoked. We can set it up as follows:

[tasks.my-flow]
condition = { env = { "TRAVIS" = "true" }, platforms = ["mac", "linux"], channels = ["nightly"] }
run_task = "my-flow-no-condition"
[tasks.my-flow-no-condition]
dependencies = ["task1", "task2", "task3"]

We now invoke my-flow which in turn, if all conditions are met, will invoke the my-flow-no-condition task. And only now, the dependencies are invoked.

Mixing conditions and sub tasks is an important feature which is used a lot in the internal cargo-make Makefile.toml

In the next article I’ll talk about Rust workspaces and how cargo-make supports them, in addition to init/end tasks and external makefiles.

--

--

Sagie Gur-Ari

Software Engineer, today working mostly in web development.