Build Scripts

Some packages need to compile third-party non-Rust code, for example Clibraries. Other packages need to link to C libraries which can either belocated on the system or possibly need to be built from source. Others stillneed facilities for functionality such as code generation before building (thinkparser generators).

Cargo does not aim to replace other tools that are well-optimized forthese tasks, but it does integrate with them with the build configurationoption.

  1. [package]
  2. # ...
  3. build = "build.rs"

The Rust file designated by the build command (relative to the package root)will be compiled and invoked before anything else is compiled in the package,allowing your Rust code to depend on the built or generated artifacts.By default Cargo looks for a "build.rs" file in a package root (even if youdo not specify a value for build). Use build = "custom_build_name.rs" to specifya custom build name or build = false to disable automatic detection of the build script.

Some example use cases of the build command are:

  • Building a bundled C library.
  • Finding a C library on the host system.
  • Generating a Rust module from a specification.
  • Performing any platform-specific configuration needed for the crate.Each of these use cases will be detailed in full below to give examples of howthe build command works.

Inputs to the Build Script

When the build script is run, there are a number of inputs to the build script,all passed in the form of environment variables.

In addition to environment variables, the build script’s current directory isthe source directory of the build script’s package.

Outputs of the Build Script

All the lines printed to stdout by a build script are written to a file liketarget/debug/build/<pkg>/output (the precise location may depend on yourconfiguration). If you would like to see such output directly in your terminal,invoke cargo as 'very verbose' with the -vv flag. Note that if neither thebuild script nor package source files are modified, subsequent calls tocargo with -vv will not print output to the terminal because anew build is not executed. Run cargo clean before each cargo invocationif you want to ensure that output is always displayed on your terminal.Any line that starts with cargo: is interpreted directly by Cargo.This line must be of the form cargo:key=value, like the examples below:

  1. # specially recognized by Cargo
  2. cargo:rustc-link-lib=static=foo
  3. cargo:rustc-link-search=native=/path/to/foo
  4. cargo:rustc-cfg=foo
  5. cargo:rustc-env=FOO=bar
  6. cargo:rustc-cdylib-link-arg=-Wl,-soname,libfoo.so.1.2.3
  7. # arbitrary user-defined metadata
  8. cargo:root=/path/to/foo
  9. cargo:libdir=/path/to/foo/lib
  10. cargo:include=/path/to/foo/include

On the other hand, lines printed to stderr are written to a file liketarget/debug/build/<pkg>/stderr but are not interpreted by cargo.

There are a few special keys that Cargo recognizes, some affecting how thecrate is built:

  • rustc-link-lib=[KIND=]NAME indicates that the specified value is a libraryname and should be passed to the compiler as a -l flag. The optional KINDcan be one of static, dylib (the default), or framework, seerustc —help for more details.

  • rustc-link-search=[KIND=]PATH indicates the specified value is a librarysearch path and should be passed to the compiler as a -L flag. The optionalKIND can be one of dependency, crate, native, framework or all(the default), see rustc —help for more details.

  • rustc-flags=FLAGS is a set of flags passed to the compiler, only -l and-L flags are supported.

  • rustc-cfg=FEATURE indicates that the specified feature will be passed as a—cfg flag to the compiler. This is often useful for performing compile-timedetection of various features.

  • rustc-env=VAR=VALUE indicates that the specified environment variablewill be added to the environment which the compiler is run within.The value can be then retrieved by the env! macro in the compiled crate.This is useful for embedding additional metadata in crate's code,such as the hash of Git HEAD or the unique identifier of a continuousintegration server.

  • rustc-cdylib-link-arg=FLAG is a flag passed to the compiler as-C link-arg=FLAG when building a cdylib. Its usage is highly platformspecific. It is useful to set the shared library version or the runtime-path.

  • rerun-if-changed=PATH is a path to a file or directory which indicates thatthe build script should be re-run if it changes (detected by a more-recentlast-modified timestamp on the file). Normally build scripts are re-run ifany file inside the crate root changes, but this can be used to scope changesto just a small set of files. (If this path points to a directory the entiredirectory will not be traversed for changes — only changes to the timestampof the directory itself (which corresponds to some types of changes within thedirectory, depending on platform) will trigger a rebuild. To request a re-runon any changes within an entire directory, print a line for the directory andanother line for everything inside it, recursively.)Note that if the build script itself (or one of its dependencies) changes,then it's rebuilt and rerun unconditionally, socargo:rerun-if-changed=build.rs is almost always redundant (unless youwant to ignore changes in all other files except for build.rs).

  • rerun-if-env-changed=VAR is the name of an environment variable whichindicates that if the environment variable's value changes the build scriptshould be rerun. This basically behaves the same as rerun-if-changed exceptthat it works with environment variables instead. Note that the environmentvariables here are intended for global environment variables like CC andsuch, it's not necessary to use this for env vars like TARGET that Cargosets. Also note that if rerun-if-env-changed is printed out then Cargo willonly rerun the build script if those environment variables change or iffiles printed out by rerun-if-changed change.

  • warning=MESSAGE is a message that will be printed to the main console aftera build script has finished running. Warnings are only shown for pathdependencies (that is, those you're working on locally), so for examplewarnings printed out in crates.io crates are not emitted by default.

Any other element is a user-defined metadata that will be passed todependents. More information about this can be found in the linkssection.

Build Dependencies

Build scripts are also allowed to have dependencies on other Cargo-based crates.Dependencies are declared through the build-dependencies section of themanifest.

  1. [build-dependencies]
  2. foo = { git = "https://github.com/your-packages/foo" }

The build script does not have access to the dependencies listed in thedependencies or dev-dependencies section (they’re not built yet!). All builddependencies will also not be available to the package itself unless explicitlystated as so.

In addition to the manifest key build, Cargo also supports a links manifestkey to declare the name of a native library that is being linked to:

  1. [package]
  2. # ...
  3. links = "foo"
  4. build = "build.rs"

This manifest states that the package links to the libfoo native library, andit also has a build script for locating and/or building the library. Cargorequires that a build command is specified if a links entry is alsospecified.

The purpose of this manifest key is to give Cargo an understanding about the setof native dependencies that a package has, as well as providing a principledsystem of passing metadata between package build scripts.

Primarily, Cargo requires that there is at most one package per links value.In other words, it’s forbidden to have two packages link to the same nativelibrary. Note, however, that there are conventions in place toalleviate this.

As mentioned above in the output format, each build script can generate anarbitrary set of metadata in the form of key-value pairs. This metadata ispassed to the build scripts of dependent packages. For example, if libbardepends on libfoo, then if libfoo generates key=value as part of itsmetadata, then the build script of libbar will have the environment variablesDEP_FOO_KEY=value.

Note that metadata is only passed to immediate dependents, not transitivedependents. The motivation for this metadata passing is outlined in the linkingto system libraries case study below.

Overriding Build Scripts

If a manifest contains a links key, then Cargo supports overriding the buildscript specified with a custom library. The purpose of this functionality is toprevent running the build script in question altogether and instead supply themetadata ahead of time.

To override a build script, place the following configuration in any acceptableCargo configuration location.

  1. [target.x86_64-unknown-linux-gnu.foo]
  2. rustc-link-search = ["/path/to/foo"]
  3. rustc-link-lib = ["foo"]
  4. root = "/path/to/foo"
  5. key = "value"

This section states that for the target x86_64-unknown-linux-gnu the librarynamed foo has the metadata specified. This metadata is the same as themetadata generated as if the build script had run, providing a number ofkey/value pairs where the rustc-flags, rustc-link-search, andrustc-link-lib keys are slightly special.

With this configuration, if a package declares that it links to foo then thebuild script will not be compiled or run, and the metadata specified willinstead be used.

Case study: Code generation

Some Cargo packages need to have code generated just before they are compiledfor various reasons. Here we’ll walk through a simple example which generates alibrary call as part of the build script.

First, let’s take a look at the directory structure of this package:

  1. .
  2. ├── Cargo.toml
  3. ├── build.rs
  4. └── src
  5. └── main.rs
  6. 1 directory, 3 files

Here we can see that we have a build.rs build script and our binary inmain.rs. Next, let’s take a look at the manifest:

  1. # Cargo.toml
  2. [package]
  3. name = "hello-from-generated-code"
  4. version = "0.1.0"
  5. authors = ["you@example.com"]
  6. build = "build.rs"

Here we can see we’ve got a build script specified which we’ll use to generatesome code. Let’s see what’s inside the build script:

  1. // build.rs
  2. use std::env;
  3. use std::fs::File;
  4. use std::io::Write;
  5. use std::path::Path;
  6. fn main() {
  7.     let out_dir = env::var("OUT_DIR").unwrap();
  8.     let dest_path = Path::new(&out_dir).join("hello.rs");
  9.     let mut f = File::create(&dest_path).unwrap();
  10.     f.write_all(b"
  11.         pub fn message() -> &'static str {
  12.             \"Hello, World!\"
  13.         }
  14.     ").unwrap();
  15. }

There’s a couple of points of note here:

  • The script uses the OUT_DIR environment variable to discover where theoutput files should be located. It can use the process’ current workingdirectory to find where the input files should be located, but in this case wedon’t have any input files.
  • In general, build scripts should not modify any files outside of OUTDIR.It may seem fine on the first blush, but it does cause problems when you usesuch crate as a dependency, because there's an _implicit invariant thatsources in .cargo/registry should be immutable. cargo won't allow suchscripts when packaging.
  • This script is relatively simple as it just writes out a small generated file.One could imagine that other more fanciful operations could take place such asgenerating a Rust module from a C header file or another language definition,for example.Next, let’s peek at the library itself:
  1. // src/main.rs
  2. include!(concat!(env!("OUT_DIR"), "/hello.rs"));
  3. fn main() {
  4. println!("{}", message());
  5. }

This is where the real magic happens. The library is using the rustc-definedinclude! macro in combination with the concat! and env! macros to includethe generated file (hello.rs) into the crate’s compilation.

Using the structure shown here, crates can include any number of generated filesfrom the build script itself.

Case study: Building some native code

Sometimes it’s necessary to build some native C or C++ code as part of apackage. This is another excellent use case of leveraging the build script tobuild a native library before the Rust crate itself. As an example, we’ll createa Rust library which calls into C to print “Hello, World!”.

Like above, let’s first take a look at the package layout:

  1. .
  2. ├── Cargo.toml
  3. ├── build.rs
  4. └── src
  5. ├── hello.c
  6. └── main.rs
  7. 1 directory, 4 files

Pretty similar to before! Next, the manifest:

  1. # Cargo.toml
  2. [package]
  3. name = "hello-world-from-c"
  4. version = "0.1.0"
  5. authors = ["you@example.com"]
  6. build = "build.rs"

For now we’re not going to use any build dependencies, so let’s take a look atthe build script now:

  1. // build.rs
  2. use std::process::Command;
  3. use std::env;
  4. use std::path::Path;
  5. fn main() {
  6.     let out_dir = env::var("OUT_DIR").unwrap();
  7.     // note that there are a number of downsides to this approach, the comments
  8.     // below detail how to improve the portability of these commands.
  9.     Command::new("gcc").args(&["src/hello.c", "-c", "-fPIC", "-o"])
  10.                        .arg(&format!("{}/hello.o", out_dir))
  11.                        .status().unwrap();
  12.     Command::new("ar").args(&["crus", "libhello.a", "hello.o"])
  13.                       .current_dir(&Path::new(&out_dir))
  14.                       .status().unwrap();
  15.     println!("cargo:rustc-link-search=native={}", out_dir);
  16.     println!("cargo:rustc-link-lib=static=hello");
  17. }

This build script starts out by compiling our C file into an object file (byinvoking gcc) and then converting this object file into a static library (byinvoking ar). The final step is feedback to Cargo itself to say that ouroutput was in out_dir and the compiler should link the crate to libhello.astatically via the -l static=hello flag.

Note that there are a number of drawbacks to this hardcoded approach:

  • The gcc command itself is not portable across platforms. For example it’sunlikely that Windows platforms have gcc, and not even all Unix platformsmay have gcc. The ar command is also in a similar situation.
  • These commands do not take cross-compilation into account. If we’re crosscompiling for a platform such as Android it’s unlikely that gcc will producean ARM executable.Not to fear, though, this is where a build-dependencies entry would help! TheCargo ecosystem has a number of packages to make this sort of task much easier,portable, and standardized. For example, the build script could be written as:
  1. // build.rs
  2. // Bring in a dependency on an externally maintained `cc` package which manages
  3. // invoking the C compiler.
  4. extern crate cc;
  5. fn main() {
  6. cc::Build::new()
  7. .file("src/hello.c")
  8. .compile("hello");
  9. }

Add a build time dependency on the cc crate with the following addition toyour Cargo.toml:

  1. [build-dependencies]
  2. cc = "1.0"

The cc crate abstracts a range of buildscript requirements for C code:

  • It invokes the appropriate compiler (MSVC for windows, gcc for MinGW, ccfor Unix platforms, etc.).
  • It takes the TARGET variable into account by passing appropriate flags tothe compiler being used.
  • Other environment variables, such as OPT_LEVEL, DEBUG, etc., are allhandled automatically.
  • The stdout output and OUT_DIR locations are also handled by the cclibrary.Here we can start to see some of the major benefits of farming as muchfunctionality as possible out to common build dependencies rather thanduplicating logic across all build scripts!

Back to the case study though, let’s take a quick look at the contents of thesrc directory:

  1. // src/hello.c
  2. #include <stdio.h>
  3. void hello() {
  4. printf("Hello, World!\n");
  5. }
  1. // src/main.rs
  2. // Note the lack of the `#[link]` attribute. We’re delegating the responsibility
  3. // of selecting what to link to over to the build script rather than hardcoding
  4. // it in the source file.
  5. extern { fn hello(); }
  6. fn main() {
  7. unsafe { hello(); }
  8. }

And there we go! This should complete our example of building some C code from aCargo package using the build script itself. This also shows why using a builddependency can be crucial in many situations and even much more concise!

We’ve also seen a brief example of how a build script can use a crate as adependency purely for the build process and not for the crate itself at runtime.

Case study: Linking to system libraries

The final case study here will be investigating how a Cargo library links to asystem library and how the build script is leveraged to support this use case.

Quite frequently a Rust crate wants to link to a native library often providedon the system to bind its functionality or just use it as part of animplementation detail. This is quite a nuanced problem when it comes toperforming this in a platform-agnostic fashion, and the purpose of a buildscript is again to farm out as much of this as possible to make this as easy aspossible for consumers.

As an example to follow, let’s take a look at one of Cargo’s owndependencies, libgit2. The C library has a number ofconstraints:

  • It has an optional dependency on OpenSSL on Unix to implement the httpstransport.
  • It has an optional dependency on libssh2 on all platforms to implement the sshtransport.
  • It is often not installed on all systems by default.
  • It can be built from source using cmake.To visualize what’s going on here, let’s take a look at the manifest for therelevant Cargo package that links to the native C library.
  1. [package]
  2. name = "libgit2-sys"
  3. version = "0.1.0"
  4. authors = ["..."]
  5. links = "git2"
  6. build = "build.rs"
  7. [dependencies]
  8. libssh2-sys = { git = "https://github.com/alexcrichton/ssh2-rs" }
  9. [target.'cfg(unix)'.dependencies]
  10. openssl-sys = { git = "https://github.com/alexcrichton/openssl-sys" }
  11. # ...

As the above manifests show, we’ve got a build script specified, but it’sworth noting that this example has a links entry which indicates that thecrate (libgit2-sys) links to the git2 native library.

Here we also see that we chose to have the Rust crate have an unconditionaldependency on libssh2 via the libssh2-sys crate, as well as aplatform-specific dependency on openssl-sys for *nix (other variants elidedfor now). It may seem a little counterintuitive to express C dependencies inthe Cargo manifest, but this is actually using one of Cargo’s conventions inthis space.

*-sys Packages

To alleviate linking to system libraries, crates.io has a convention of packagenaming and functionality. Any package named foo-sys should provide two majorpieces of functionality:

  • The library crate should link to the native library libfoo. This will oftenprobe the current system for libfoo before resorting to building fromsource.
  • The library crate should provide declarations for functions in libfoo,but not bindings or higher-level abstractions.The set of *-sys packages provides a common set of dependencies for linkingto native libraries. There are a number of benefits earned from having thisconvention of native-library-related packages:

  • Common dependencies on foo-sys alleviates the above rule about one packageper value of links.

  • A common dependency allows centralizing logic on discovering libfoo itself(or building it from source).
  • These dependencies are easily overridable.

Building libgit2

Now that we’ve got libgit2’s dependencies sorted out, we need to actually writethe build script. We’re not going to look at specific snippets of code here andinstead only take a look at the high-level details of the build script oflibgit2-sys. This is not recommending all packages follow this strategy, butrather just outlining one specific strategy.

The first step of the build script should do is to query whether libgit2 isalready installed on the host system. To do this we’ll leverage the preexistingtool pkg-config (when its available). We’ll also use a build-dependenciessection to refactor out all the pkg-config related code (or someone’s alreadydone that!).

If pkg-config failed to find libgit2, or if pkg-config just wasn’tinstalled, the next step is to build libgit2 from bundled source code(distributed as part of libgit2-sys itself). There are a few nuances whendoing so that we need to take into account, however:

  • The build system of libgit2, cmake, needs to be able to find libgit2’soptional dependency of libssh2. We’re sure we’ve already built it (it’s aCargo dependency), we just need to communicate this information. To do thiswe leverage the metadata format to communicate information between buildscripts. In this example the libssh2 package printed out cargo:root=… totell us where libssh2 is installed at, and we can then pass this along tocmake with the CMAKE_PREFIX_PATH environment variable.

  • We’ll need to handle some CFLAGS values when compiling C code (and tellcmake about this). Some flags we may want to pass are -m64 for 64-bitcode, -m32 for 32-bit code, or -fPIC for 64-bit code as well.

  • Finally, we’ll invoke cmake to place all output into the OUT_DIRenvironment variable, and then we’ll print the necessary metadata to instructrustc how to link to libgit2.

Most of the functionality of this build script is easily refactorable intocommon dependencies, so our build script isn’t quite as intimidating as thisdescriptions! In reality it’s expected that build scripts are quite succinct byfarming logic such as above to build dependencies.