Great Rust CI
Tags: rust, cargo, travis, ci
Published: 2018-01-20 03:39:40

---
title: Great Rust CI
published: true
description: Setting up Travis and cargo-travis for a new Rust crate
tags: rust, cargo, travis, ci
---

Great CI means coding with confidence. And the CI-compatible tooling situation for Rust is great, so you should take advantage of it.

If you don't know the full details of all of the available tools, though, it can be difficult to set up the perfect CI. So here, I'll walk you through the setup that I'm using.

---

## Install local tools.

Here's what I use every day:

1. [Clippy](https://github.com/rust-lang-nursery/rust-clippy): code linting for more opinionated things than what rustc gives you.

    For now, clippy will only work with the latest nightly. Install and update nightly rust with rustup, then run `cargo +nightly install clippy --force` to force a clippy install built against your current nightly. Use it with `cargo +nightly clippy`.

1. [Rustfmt](https://github.com/rust-lang-nursery/rustfmt): automatic code formatting.

    For _many_ (though not all, sadly) nightlies, rustfmt can be managed using rustup, just like you can for rls. If the nightly that you're on contains rustfmt, you can do `rustup component add rustfmt-preview`. If your nightly doesn't contain rustfmt-preview, you can install it from crates.io: `cargo +nightly install rustfmt-nightly --force`. Having both the rustup managed and cargo managed versions can lead to conflicts, so use one or the other. Use it with `cargo +nightly fmt`.

1. [Cargo-Update](https://github.com/nabijaczleweli/cargo-update): check for and update cargo-install-ed binaries.

    Not as much help for clippy/rustfmt, which need to be force-reinstalled for every new nightly (because they link against it), but invaluable for seamlessly updating other tools (including itself). (Requires CMake)

1. Optional: [Cargo-Edit](https://github.com/killercup/cargo-edit): modify the build manifest (`Cargo.toml`) from the command line with simple commands, rather than editing the file by hand.

1. Optional: IDE support. I use IntelliJ IDEA with the [IntelliJ Rust](https://intellij-rust.github.io/) plugin. There's also [RLS for VSCode](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust), or you could use [Vim](https://github.com/rust-lang/rust.vim) or [Emacs](https://github.com/rust-lang/rust-mode) if you're into that kind of thing (no judging!).

## Create your project!

```
$ cargo new great-ci
     Created library `great-ci` project
```

It's good practice to set up `.git/info/exclude` to tell git to ignore your IDE files. For me, that means the contents of the git exclude are:

```gitignore
.idea/
*.iml
```

And for the purpose of this example, let's make a tiny "Hello World" style library:

```rust
//! Example library for great CI integration!

use std::fmt;

/// Greetings to some target
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct Greeting<'a>(&'a str);

impl<'a> Greeting<'a> {
    /// Construct a Greeter to greet a target
    pub fn greet(target: &str) -> Greeting {
        Greeting(target)
    }
}

impl<'a> fmt::Display for Greeting<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Greetings, {}!", self.0)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn greet_author() {
        let greeter = Greeting::greet("CAD97");
        assert_eq!(greeter.to_string(), "Greetings, CAD97!");
    }
}
```

Add a LICENSE and README, then it's time to commit and [get the code online](https://github.com/CAD97/great-ci/tree/91a0947f638fb7beda5356e7dc520296fcffae93).

```
$ git add .
$ git commit -m "Initial commit"
$ git remote add CAD97 git@github.com:CAD97/great-ci.git
$ git push -u CAD97 master
```

## Set up CI

The part that we care about! I use [Travis CI](https://travis-ci.org/), [Cargo-Travis](https://github.com/roblabla/cargo-travis), [Codecov](https://codecov.io/), and [Bors](https://bors.tech/) for my CI stack.

So let's switch to a feature branch and set up a basic `bors.toml`

```toml
status = [
    "continuous-integration/travis-ci/push"
]
```

and `.travis.yml`:

```yaml
language: rust

rust:
  - stable
  - beta
  - nightly

script: |
  cargo build --verbose &&
  cargo test  --verbose &&
  cargo doc   --verbose

branches:
  only:
    - staging # bors r+
    - trying  # bors try
    - master
```

don't forget to enable Travis and Bors for the repository (I did), and then [send a PR](https://github.com/CAD97/great-ci/pull/2). If all goes well, your CI is green and you can merge with `bors r+`.

## Testing rustfmt and clippy

When you're working with a project you expect to get large, it can help to enforce standard formatting, and clippy does wonders for preventing simple mistakes and nudging people away from [questionable API decisions](https://rust-lang-nursery.github.io/rust-clippy/current/index.html#wrong_self_convention).

Because rustfmt and clippy are somewhat unstable, requiring certain small ranges of nightly compilers and sometimes breaking, and changing default formatting or adding lints may break your build, we depend on specific versions of the tools, and you can update the versions manually.

We use a [matrix include](https://docs.travis-ci.com/user/customizing-the-build/#Explicitly-Including-Jobs) on Travis to add the check:

```yaml
cache: cargo
matrix:
  include:
    - rust: nightly-2018-01-12
      env: # use env so updating versions causes cache invalidation
        - CLIPPY_VERSION=0.0.179
      before_script:
        - rustup component add rustfmt-preview
        - cargo install clippy --version $CLIPPY_VERSION || echo "clippy already installed"
      script:
        - cargo fmt -- --write-mode=diff
        - cargo clippy -- -D clippy
```

Here we install clippy using `cargo install`, or succeed if it's already installed. We install rustfmt using rustup, because in Travis's Rust setup, rustfmt is already tracked by rustup, so the `cargo-fmt` executable exists, and an install without `--force` will fail. And if we don't pick a nightly toolchain that actually contains `cargo-fmt`, using it will fail. Here we have `rust: nightly-2018-01-12 CLIPPY_VERSION=0.0.179` because this is the latest toolchain as-of-writing where rustfmt-preview is in, and the latest version of clippy which builds on that nightly.

Installing tools like rustfmt and clippy takes time, and CI is made great when it's _fast_, so we enable [Travis's Rust Cargo cache](https://docs.travis-ci.com/user/caching/#Rust-Cargo-cache) to cache cargo's symbols. This speeds up the build because cargo doesn't have to recompile dependencies, whether these be build dependencies or tools.

The cache is keyed by the language version (here `rust: nightly-2018-01-18`) and the env (here `CLIPPY_VERSION=0.0.180`), and shared between build jobs with the same rust version and env. By putting clippy's version in the env, we separate the tool build's cache from the other caches, and ensure that a version update causes a rebuild.

You may be tempted to stick these environment variables in the CI configuration, so that you can update them without an extra commit to the repository. This is ill-advisable, for two main reasons. Primarily, version bumping these tools has a high likelihood of changing requirements for your build, and the breakage will show up unannounced. This change should be marked in your repository. Secondarily, the use of env here also serves to separate the cache for these tools from the other caches. If the specified rust version is the most up-to-date nightly, this cache and the nightly cache would clobber each other. And you don't want to force installation of these tools on CI builds that don't require it, because we want those _zippy_ CI greens.

So make these changes, [submit a PR](https://github.com/CAD97/great-ci/pull/3), and we're go for the last step!

## Using cargo-travis

Cargo-travis enables you to easily use [kcov](https://github.com/SimonKagstrom/kcov) and upload coverage information to codecov or coveralls. Additionally, added by myself recently, it allows you to upload documentation for your crate to GitHub pages and maintain an understandable git history and directory structure allowing you to document multiple branches (`master`/`beta`/`release` branches to imitate Rust's `nightly`/`beta`/`stable` trains) if you so wish.

Because these tasks can take a while, and are more for informing decisions rather than to hard gate PRs, I stick these into a allowed-failure matrix line for my builds. This means that Travis passes as soon as the four above jobs pass (`rust: [stable, beta, nightly]`, rustfmt&clippy), and allows the cargo-travis job to continue running in the background.

Our `.travis.yml` additions:

```yaml
env: # required for allow_failures
matrix:
  fast_finish: true
  allow_failures:
    - env: NAME='cargo-travis'
  include:
    - env: NAME='cargo-travis'
      sudo: required # travis-ci/travis-ci#9061
      before_script:
        - cargo install cargo-update || echo "cargo-update already installed"
        - cargo install cargo-travis || echo "cargo-travis already installed"
        - cargo install-update -a
      script:
        - |
          cargo build    --verbose &&
          cargo coverage --verbose &&
          bash <(curl -s https://codecov.io/bash) -s target/kcov
        - |
          cargo doc --verbose &&
          cargo doc-upload
      addons: # required for kcov
        apt:
          packages:
            - libcurl4-openssl-dev
            - libelf-dev
            - libdw-dev
            - binutils-dev
            - cmake
```

If you want to use Coveralls, just use `cargo coveralls` instead of `cargo coverage`. If you don't want to use Codecov, just remove the line for the Codecov bash script.

This isn't enough for the `doc-upload` task, however (though this _will not_ make your CI go red, as we allow this job to fail). In order to upload documentation, the `doc-upload` script needs permission to push to your GitHub repository. The easiest way, which we will use here, is a [GitHub token](https://github.com/blog/1509-personal-api-tokens), but you can read more about the options at the [cargo-travis README](https://github.com/roblabla/cargo-travis#doc-upload).

In order to generate a GitHub token, navigate to [your tokens settings page](https://github.com/settings/tokens). While you're there, review your currently issued tokens, and revoke those that you're no longer using. Generate a new token, give it a descriptive name (this can be edited later) (I suggest `repo-name-doc-upload`) and the `public_repo` scope (this can be edited later). Make sure to copy the token once it's generated, as you will never be able to see it again (though you can regenerate it)! This token gives anyone with the token access to act as you when read/writing to any public repository you have the permission to read/write to, so keep it safe. Add it to the `GH_TOKEN` environment variable on Travis and _make sure that "Display value in build log" is set to off_.

![Environment Variables](https://imgur.com/kfu4hgY.png)

Once you [send your PR](https://github.com/CAD97/great-ci/pull/4), the new job will kick off.

A few things to note about this setup:

- CI will pass before coverage is done. This is intentional, as we want to see quick CI feedback for tests passing/failing, and coverage doesn't need to slow down that feedback loop for PRs (or the time it takes to `bors r+` and merge). We still want to see those stats, though, and Codecov/Coveralls will still do its analysis and leave a comment once the coverage information is uploaded.
- `cargo-travis` will take a good long time the first time around; you have to build a large number of tools, including kcov itself. Hopefully, however, all of this will be cached, so subsequent builds will be much faster.
- Documentation will be built when CI builds the master branch only. You can specify which branches to build by passing (optionally multiple) `--branch NAME` arguments to `cargo doc-upload`.
- `doc-upload` _does not_ build documentation for you, so you need to call `cargo doc` yourself. This is so that you can, if your library offers feature flags or other conditional compilation, build up a directory in `target/doc` (rustdoc's output directory) which `doc-upload` will then use when uploading to GitHub pages.
- Documentation ends up living at `https://<user>.github.io/<repo>/<branch>/<crate>/`, which is <https://cad97.github.io/great-ci/master/great_ci/> for this example.
- [Coverage is 100%](https://codecov.io/gh/CAD97/great-ci) 🎉

## Further extensions

If you want to (or need to) test on a Windows platform, [AppVeyor](https://www.appveyor.com/) is often used. This CI can be used in parallel to this Travis setup with no problems. Make sure to tell Bors to wait for the AppVeyor build, however!

Don't forget that you can suppress rustfmt and clippy warnings if you don't want them to apply to certain blocks of code. If it's an issue in the source library, submit an issue, or if it's just a disagreement, check the configuration and see if you can change it there.

For the security-minded, the `public_repo` scope of the GitHub token may be too broad. This is a solved problem; the solution is repo-specific deploy keys. If you provide a deploy key for your repository and load it into Travis (and don't provide a token), `doc-upload` will use it.

You've got the CI set up, brag about it in your README using badges and by embedding coverage charts! [Shields|IO](https://shields.io/) provides consistently-styled badges for practically every service, and [Codecov graphs](https://codecov.io/gh/CAD97/great-ci/branch/master/graphs) are 🔥🔥🔥.

The base page of your GitHub pages (`<user>.github.io/<repo>/`) is going to 404, as no `index.html` exists at the root of your `gh-pages` branch. The same for the branch root (`<user>.gihub.io/<repo>/<branch>`). `doc-upload` deliberately doesn't mess with your root directory or the `index.html` at the branch root so that you can include a HTML redirect at these spots or a more informative index if you want. For a HTML redirect, use the following:

```html
<meta http-equiv="refresh" content="0; url=<crate>">
<a href="<crate>">Redirect</a>
```

[Discuss this on Reddit](https://redd.it/7rnyg8) and tell me if I left something out or something is outdated!

[See the public repository on GitHub!](https://github.com/CAD97/great-ci)