Automating your Rust workflows with cargo-make - Part 4 of 5 - Workspace Support, Init/End Tasks and Makefiles

In previous articles we talked about the many task features like scripts, commands, automatic crate installation, conditions and more.

In this article we will talk about Rust specific features like workspace support and general flow and loading process.

The previous articles in this series were:


Workspace Support

A Rust workspace is basically a grouping of multiple Rust modules, each with its own sources, tests, etc…

Example of a very simple workspace:

workspace
├── Cargo.toml
├── member1
│ └── Cargo.toml
└── member2
└── Cargo.toml

When working with workspaces, we will likely do parallel development of multiple modules and request to run full tests of all modules. Many cargo commands support the all flag that enables to run commands for each member of the workspace, but that is cumbersome and tiring to add each time for every command. Luckily cargo-make has a very simple solution for that.

Let’s say we defined the following flow:

[tasks.my-flow]
dependencies = [
"format",
"build",
"test"
]

We will run it with the command:

cargo make my-flow

Great, that works good for a single Rust module, but how do I run that flow for every member in the workspace? Simple! Just run the same command in the workspace directory.

Any task that is being asked to be executed by cargo-make, it will first check if this is a workspace directory. If it is, it will go to every workspace member and run the task there. No need for special flags for that. The opposite is true, meaning that by default, every task is executed for every member, but if you want to run a task on the workspace level and not for the members, than you have to pass the --no-workspace flag.

Mixing Workspace and Members Level Flows

What if we want to run a flow that runs a task on the workspace level and afterwards runs another sub flow for each member?

Below is an example flow that solves this:

[tasks.composite]
dependencies = ["member_flow", "workspace_flow"]

[tasks.member_flow]
command = "cargo"
args = ["make", "member_task"]

[tasks.member_task]
#run some member level command or flow

[tasks.workspace_flow]
#run some workspace level command or flow

We would run this flow from the workspace directory as follows:

cargo make --no-workspace composite

So workspace support is yet another great feature that only a Rust aware task runner like cargo-make can give you.


Init and End Tasks

Every task or flow that is executed by the cargo-make has additional two tasks. An init task that gets invoked at the start of all flows and end task that is invoked at the end of all flows. These allow you to do some common tasks for every flow you are running regardless of the flow itself.

By default those two tasks are empty tasks that do nothing but you can change that by either extending those tasks (named init and end) or pointing the init and end tasks to some custom tasks that you defined in the config section of the Makefile.toml.

The below show the default setup in the internal Makefile.toml:

[config]
init_task = "init"
end_task = "end"

[tasks.init]

[tasks.end]

Important to mention that init and end tasks invocation is different than other tasks.

  • Aliases and dependencies are ignored
  • If the same task is defined in the executed flow, those tasks will be invoked multiple times

Makefiles

By default cargo-make doesn’t need any external custom defined Makefile.toml as many of the tasks are predefined internally and can be used without any special configuration.

The external Makefile.toml allows you to change and add tasks on your own. By default, cargo-make will look for a file named Makefile.toml in the current working directory but you can ask it to look for a different name using the --makefile flag.

For example:

cargo make --makefile mytasks.toml

Sometimes you want a shared makefile. For example, you have a rust workspace and a makefile is defined in the workspace level with custom tasks, and you want each member to have access to those tasks. In order to pull those custom tasks from the workspace level makefile to your member level makefile you can use the extends keyword in your member makefile and point to the workspace level makefile.

extend = "../workspace_makefile.toml"

The extends works in the parent file as well so they too can extend other files. There is no limit to how many files are extended. The limitation is that extend only works with file system paths and not URLs.

Load Scripts

What if you have many standalone unrelated projects and you want to share the same makefile tasks? For those more complex scenarios, you can use the load scripts.

Load scripts are defined in the config section using the load_script attribute and are invoked before the extend attribute is evaluated.
This allows you to first pull the toml file from the remote server and put it in a location defined by the extend attribute.

Here is an example of a load script which downloads the common toml from a remote server using HTTP:

[config]
load_script = ["wget -O /home/myuser/common.toml companyserver.com/common.toml"]

Here is an example of pulling the common toml file from some git repo:

[config]
load_script = ["git clone git@mygitserver:user/project.git /home/myuser/common"]

You can run any command or set of commands you want, so you can build a more complex flow of how and where to fetch the toml file from and where to put it.
If needed, you can override the load_script per platform using the linux_load_script, windows_load_script and mac_load_script attributes.


That’s it for now, in the next article I’ll be explaining about cargo-make predefined tasks, CI Support and Conventions