Cargo
Cargo is the default build system for Rust projects. It makes it easy to create
build and test Rust code, manages dependencies from crates.io, and allows
you to publish your own crates there. It uses semantic versioning to resolve
dependency version from constraints you define and uses a lockfile to ensure
you are always building with the same dependency versions. Since rustc
is LLVM-based,
it is also easy to cross-compile your Rust code for other targets, see the
list of supported Rust targets.
Cargo supports installing other tools that integrate into it and extend it
with new subcommands. This guide mentions several of such tools, such as cargo-hack
or cargo-llvm-cov
.
One nice property of having Cargo as the default build system for all Rust
projects is that you can typically clone any repository that contains a Rust
crate and run cargo build
to build it, or cargo test
to run tests.
This is quite different to languages such as C, C++ or JavaScript that have
a more fragmented build ecosystem.
What Cargo Lacks
If you only use built-in commands and only build Rust code, then Cargo is a great build system for Rust projects. However, there are some features it does not have.
If you rely on plugins to build your project, such as trunk
for building
WebAssembly-powered web frontend applications powered by Rust, Cargo will not
install it automatically. Rather, developers need to install it manually by
running cargo install trunk
.
If you rely on native dependencies, such as OpenSSL or other libraries, Cargo
will not handle installing them on your behalf. There are some workarounds for
this, for example some crates like rusqlite
ship the C code and have a
feature flag where Cargo will build the required library from source if you
request it.
If you need to execute build steps, such as compiling C code or your have some parts of your project that use for example JavaScript, there is only rudimentary support for doing so with Cargo.
In short, Cargo is great at all things Rust, but it does not help you much if you mix other languages into your project. And that is by design: Cargo’s goal is not to reinvent the world. It does one thing, and it does it well, which is build Rust code.
The next sections discuss some approaches that you can use to use Cargo in situations that it is not designed for, but that yet seem to work.
Complex build steps
Cargo is great at building Rust code, but has few features for building projects that involve other languages. This makes sense, because such functionality is not needed by it.
Cargo does come with some support for running arbitrary steps at build time, through the use of build scripts. These are little Rust programs that you can write that are executed at build time and let you do anything you like, including building other code. It also supports linking with C/C++ libraries by having these build scripts emit some data that Cargo parses.
The other sections of this chapter are only relevant to you if your project
consists of a mixture of languages, and building it is sufficiently complex
that it cannot trivially be expressed or implemented in a build.rs
file (such
as: it needs external dependencies).
build.rs
to define custom build actions
If you have a few more complex steps that you need to do when building your code, you can always use a build script.
Build scripts in Cargo are little Rust programs defined in a build.rs
in the crate
root which are compiled and run before your crate is compiled. They are able to do
some build steps (such as compile an external, vendored C library) and they can
emit some information to Cargo, for example to tell it to link against a specific
library.
Build scripts receive a number of environment variables as inputs, and output some metadata that controls Cargo’s behaviour.
A simple build script might look like this:
fn main() { }
For common tasks such as building C code, generating bindings for native libraries there are crates that allow you to write build scripts easily, these are presented in the next sections.
Compiling C/C++ Code
If you have some C or C++ code that you want built with your crate, you can use
the cc
crate to do so. It is a helper library that you can call inside
your build script to run the native C/C++ compiler to compile some code, link
it into a static archive and tell Cargo to link it when building your crate. It
also has support for compiling CUDA code.
A basic use of this crate looks by adding something like this to the main
function of your build script:
#![allow(unused)] fn main() { cc::Build::new() .file("foo.c") .file("bar.c") .compile("foo"); }
The crate will take care of the rest of finding a suitable compiler and
communicating to Cargo that you wish to link the foo
library.
Here is an example of how this looks like. In this crate, a build script is used to compile and link some C code, and the unsafe C API is wrapped and exposed as a native Rust function.
- src/
/target
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "cc"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"shlex",
]
[[package]]
name = "clap"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "levenshtein"
version = "0.1.0"
dependencies = [
"cc",
"clap",
"libc",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[package]
name = "levenshtein"
version = "0.1.0"
edition = "2021"
[dependencies]
# used to parse command-line arguments
clap = { version = "4.5.16", features = ["derive"] }
# used for FFI interface (defines size_t)
libc = "0.2.158"
[build-dependencies]
# used to build the levenshtein.c library
cc = "1.1.15"
# Levenshtein
Wrapper around [levenshtein.c][], a C library to compute the Levenshtein
distance between two strings. Also contains a command-line tool to compute the
distance for two strings passed as command-line parameters.
## Examples
You can build the library using Cargo. Ensure that you have a C compiler installed,
as this crate relies on the [cc][] crate to build the library.
```
$ cargo run -- "hello" "hello"
0
$ cargo run -- "kitten" "sitting"
3
```
[levenshtein.c]: https://github.com/wooorm/levenshtein.c
[cc]: https://docs.rs/cc/latest/cc/
/// Compiles the `levenshtein.c` library using the C compiler and instructs Cargo to link the
/// resulting archive.
fn main() {
cc::Build::new().file("src/levenshtein.c").compile("levenshtein");
}
// `levenshtein.c` - levenshtein
// MIT licensed.
// Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "levenshtein.h"
// Returns a size_t, depicting the difference between `a` and `b`.
// See <https://en.wikipedia.org/wiki/Levenshtein_distance> for more information.
size_t
levenshtein_n(const char *a, const size_t length, const char *b, const size_t bLength) {
// Shortcut optimizations / degenerate cases.
if (a == b) {
return 0;
}
if (length == 0) {
return bLength;
}
if (bLength == 0) {
return length;
}
size_t *cache = calloc(length, sizeof(size_t));
size_t index = 0;
size_t bIndex = 0;
size_t distance;
size_t bDistance;
size_t result;
char code;
// initialize the vector.
while (index < length) {
cache[index] = index + 1;
index++;
}
// Loop.
while (bIndex < bLength) {
code = b[bIndex];
result = distance = bIndex++;
index = SIZE_MAX;
while (++index < length) {
bDistance = code == a[index] ? distance : distance + 1;
distance = cache[index];
cache[index] = result = distance > result
? bDistance > result
? result + 1
: bDistance
: bDistance > distance
? distance + 1
: bDistance;
}
}
free(cache);
return result;
}
size_t
levenshtein(const char *a, const char *b) {
const size_t length = strlen(a);
const size_t bLength = strlen(b);
return levenshtein_n(a, length, b, bLength);
}
#ifndef LEVENSHTEIN_H
#define LEVENSHTEIN_H
#include <stddef.h>
// `levenshtein.h` - levenshtein
// MIT licensed.
// Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>
// Returns a size_t, depicting the difference between `a` and `b`.
// See <https://en.wikipedia.org/wiki/Levenshtein_distance> for more information.
#ifdef __cplusplus
extern "C" {
#endif
size_t
levenshtein(const char *a, const char *b);
size_t
levenshtein_n (const char *a, const size_t length, const char *b, const size_t bLength);
#ifdef __cplusplus
}
#endif
#endif // LEVENSHTEIN_H
//! The Levenshtein distance measures how similar two words are, by how many substitutions are
//! needed to get from one word to the other. This Crate wraps a C library that implements this
//! algorithm in a safe Rust interface.
/// Raw access to the unsafe C API of the levenshtein library.
pub mod raw {
use libc::size_t;
use std::ffi::c_char;
extern "C" {
/// Raw binding to the C `levenshtein_n` function.
///
/// `a` and `b` must be valid pointers to character arrays, and `a_length` and `b_length`
/// their lengths respectively.
pub fn levenshtein_n(
a: *const c_char,
a_length: size_t,
b: *const c_char,
b_length: size_t,
) -> size_t;
}
}
/// Computes the Levenshtein distance between the strings `a` and `b`.
///
/// # Examples
///
/// The Levenshtein distance between two equal words is zero.
///
/// ```
/// # use levenshtein::levenshtein;
/// assert_eq!(levenshtein("hello", "hello"), 0);
/// ```
///
/// The Levenshtein distance between two words that have a single letter substituted is one.
///
/// ```
/// # use levenshtein::levenshtein;
/// assert_eq!(levenshtein("hello", "hallo"), 1);
/// ```
pub fn levenshtein(a: &str, b: &str) -> u64 {
use std::ffi::c_char;
let result = unsafe {
raw::levenshtein_n(
a.as_ptr() as *const c_char,
a.len(),
b.as_ptr() as *const c_char,
b.len(),
)
};
result as u64
}
#[test]
fn test_levenshtein() {
macro_rules! assert_distance {
($a:expr, $b:expr, $d:expr) => {
assert_eq!(levenshtein($a, $b), $d);
};
}
assert_distance!("", "a", 1);
assert_distance!("a", "", 1);
assert_distance!("", "", 0);
assert_distance!("levenshtein", "levenshtein", 0);
assert_distance!("sitting", "kitten", 3);
assert_distance!("gumbo", "gambol", 2);
assert_distance!("saturday", "sunday", 3);
// It should match case sensitive.
assert_distance!("DwAyNE", "DUANE", 2);
assert_distance!("dwayne", "DuAnE", 5);
// It not care about parameter ordering.
assert_distance!("aarrgh", "aargh", 1);
assert_distance!("aargh", "aarrgh", 1);
// Some tests form `hiddentao/fast-levenshtein`.
assert_distance!("a", "b", 1);
assert_distance!("ab", "ac", 1);
assert_distance!("ac", "bc", 1);
assert_distance!("abc", "axc", 1);
assert_distance!("xabxcdxxefxgx", "1ab2cd34ef5g6", 6);
assert_distance!("xabxcdxxefxgx", "abcdefg", 6);
assert_distance!("javawasneat", "scalaisgreat", 7);
assert_distance!("example", "samples", 3);
assert_distance!("sturgeon", "urgently", 6);
assert_distance!("levenshtein", "frankenstein", 6);
assert_distance!("distance", "difference", 5);
}
use clap::Parser;
#[derive(Parser)]
struct Options {
a: String,
b: String,
}
fn main() {
let options = Options::parse();
let distance = levenshtein::levenshtein(&options.a, &options.b);
println!("{distance}");
}
Note that in order to make the C function “visible” from Rust, you need to
declare it in an extern "C"
block. It needs a function definition that
matches the one in the C header. Writing this by hand is error-prone, and can
lead to unsafety issues.
This example also shows how this unsafe C function is wrapped into a safe Rust function. Doing so involves dealing with raw pointers, and it is easy to get something wrong. It is important to write good Unit Tests, and often it can help to use Dynamic Analysis to make sure you did it correctly.
Compiling CMake projects
- use the
cmake
crate
Generating Bindings for C/C++ Libraries
- using rust-bindgen
Caching builds
You may find that Rust takes a long time to compile, which is certainly the
case. You can partially mitigate this by using a build cache, which is a
service that will cache the compiled artifacts and allow you to compile
considerably faster. One tool that lets you do this is sccache
, which is
discussed in a future chapter.
Toolchain Pinning
If you depend on specific Cargo or Rust features, you may find that you can run into issues if people with older toolchain versions try to build your code. For this reason, it is sometimes useful to pin a specific version of the Rust toolchain in a project, to make sure everyone is using the same versions.
There are two mechanisms that you can use here, depending on where you want this pinning to work:
- You can use a
rust-toolchain.toml
file to pin the Rust version for the current project. This file is picked up byrustup
, which most people use to manage and update their Rust toolchain. When running any Cargo command in a project that has such a file,rustup
will ensure that the specified toolchain version is installed on the system and will only use that. - Conversely, if you are building a library and you want users of your library (as in, people that depend on your library as a dependency, but do not directly work on it) to use a specified minimum Rust toolchain version, you can set the MSRV in the Cargo metadata. This means that users of your library that are on older Rust versions will get an error or a warning when they try to add your library as a dependency.
Pinning the toolchain version for projects
The way you can solve this is by putting a rust-toolchain.toml
file into the
repository. This will instruct rustup
to fetch the exact toolchain mentioned
in this file whenever you run any operations in the project.
Typically, such a file simply looks like this:
[toolchain]
channel = "1.75"
components = ["rustfmt", "clippy"]
Keep in mind that this file is only picked up by people who use rustup
to manage
their Rust toolchains.
Putting rust-toolchain.toml
file in your project lets you specify exactly which
version of the Rust compiler is used by the people working on the project.
Specifying the minimum toolchain version for library crates
However, this rust-toolchain.toml
file is only consulted when you are building
the current project. What if your crate is used as a dependency by other crates?
How can you communicate that it needs a certain version of the Rust compiler?
For this, Cargo has the option of specifying a MSRV for each crate. This is the minimum version of the Rust compiler that the crate will build with.
In a later chapter, we will show you how you can determine the MSRV programmatically and how you can test it to make sure that the version you put there actually works.
If you build library crates, you should specify the minimum version of the Rust toolchain that is needed to build your library. This helps other crate authors by telling them which version of Rust they need to use your library. You should always specify this.
Convenience Commands
Cargo has a useful selection of convenience commands built-in to it that make using it to manage Rust projects easy.
https://blog.logrocket.com/demystifying-cargo-in-rust/
Initializing Cargo project
To quickly create a Cargo project, you can use cargo new
. By default, it will
create a binary crate, but you can use the --lib
flag to create a library crate
instead.
cargo new my-crate
Building and running Code and Examples
The main thing you likely use Cargo for is to build and run Rust code.
Cargo has two commands for this, cargo build
and cargo run
.
cargo build
cargo run
If you have multiple binaries and you want to build or run a specific one,
you can specify it using the --bin
flag.
cargo build --bin my_binary
cargo run --bin my_binary
If you instead want to build or run an example, you can specify that using
the --example
flag.
cargo build --example my_example
cargo run --example my_example
Running Tests and Benchmarks
Besides building and running Rust code, you will likely also use Cargo to run unit tests and benchmarks. It has built-in commands for this, too.
cargo test
cargo bench
As explained in the Unit testing section, you can
also use the external tool cargo-nextest
to run tests faster.
Managing Dependencies
Cargo comes with built-in commands for managing dependencies. Originally, these commands were part of cargo-edit, but due to their popularity the Cargo team has decided to adopt them as first-class citizens and integrate them into Cargo.
cargo add serde
cargo remove serde
Recently, they also added support for Workspace dependencies. If you use cargo-add
to add
a dependency to a crate, which already exists in the root workspace as a dependency, it will do the
right thing and add it as a workspace dependency to your Cargo manifst.
You can also use Cargo to query the dependency tree. This lets you see a list of all dependencies,
and their child dependencies. It lets you find out if you have duplicate dependencies (with different
versions), and when that is the case, why they get pulled in. For example, if you have one dependency
that uses uuid v1.0.0
, but you depend on uuid v0.7.0
, then you will end up with two versions of
the uuid
crate that are being pulled in.
cargo tree
This command used to be a separate plugin called cargo-tree, but was incorporated into Cargo by the team due to it being useful.
Building Documentation
cargo doc
Installing Rust Tools
Besides just being a build system for Rust, Cargo also acts as a kind of package manager. Any binary Rust crates that are published on a registry can be compiled and installed using it. This is often used to install Cargo plugins or other supporting tools.
cargo install ripgrep
- mention cargo-binstall
Profiling Builds
If you want to figure out what Cargo is spending most of the time on during builds, you can use the built-in profiling support. This generates a HTML report of the timings of the build and allows you to debug slow builds. However, it only works using nightly Rust.
cargo +nightly build --timings=html
Conclusion
If your project can get away with only using it to define and run all of the steps needed to build your project, then you should prefer it over using a third-party build system. Everyone who writes Rust code uses Cargo, it is very simple to use and comes with features that cover the majority of the use-cases you might run into.
If you do have a multi-language project, or a project with complicated build steps, you might soon find that build scripts are rather limited. Dependency tracking is possible with them, but it feels hacky. They are not hermetic, and there is no built-in caching that you can use. In this case, you may find it useful to take a look at the other popular build systems and determine if they might help you achieve what you want in a way that is more robust or more maintainable.
Do keep in mind that usually, using third-party build systems can be more pain than using Cargo itself, because they need to reimplement some functionality that you get for free when using Cargo. However, sometimes there are advantages that they bring that outweigh the additional complexity.
Reading
Reference guide for Cargo. This book discusses all features that Cargo has and how they can be used.
Build Scripts in The Cargo Book
Section in the Cargo Book that talks about using build scripts. It shows some examples for how they can be used and explains what can be achieved with them.
The Missing Parts in Cargo by Weihang Lo
Weihang discusses Cargo, and what is missing from it.
Foreign Function Interface in The Rustonomicon
This chapter in The Rustonomicon explains how to interact with foreign functions, that is code written in C or C++, in Rust.