While Travis supports Rust natively for its build pipeline, CircleCI still misses first-class support for Rust. This short post explains how to build a Rust project on CircleCI so you don't have to go through all the trouble.

We'll be using CircleCI 2.0, which comes with a new yaml definition. This has the advantage of allowing you to pre-cache all your dependencies into a docker image which dramatically reduces build times. We will also use kcov and Codecov to produce code coverage reports.

CircleCI config

At the root of your project, you must create a .circleci folder in which you will create a config.yaml file containing the following snippet:

version: 2

jobs:
  build:
    docker:
      # The image used to build our project, build
      # your own using the Dockerfile provided below
      # and replace here. I put my own image here for
      # the example.
      - image: abronan/rust-circleci:latest

    environment:
      # Set your codecov token if your repository is private.
      CODECOV_TOKEN: <your-token>
      TZ: "/usr/share/zoneinfo/Europe/Paris"

    steps:
      - checkout
      - restore_cache:
          key: project-cache
      - run:
          name: Check formatting
          command: |
            rustfmt --version
            cargo fmt -- --write-mode=diff
      - run:
          name: Nightly Build
          command: |
            rustup run nightly rustc --version --verbose
            rustup run nightly cargo --version --verbose
            rustup run nightly cargo build
      - run:
          name: Stable Build
          command: |
            rustup run stable rustc --version --verbose
            rustup run stable cargo --version --verbose
            rustup run stable cargo build
      - run:
          name: Test
          command: rustup run stable cargo test
      - run:
          name: Upload Coverage
          command: ./scripts/codecov.sh
      - save_cache:
          key: project-cache
          paths:
            - "~/.cargo"
            - "./target"

In short, this will:

  • Restore the project cache which includes .cargo and .target folders.
  • Run formatting check with rustfmt-nightly.
  • Run Nightly and Stable builds.
  • Run tests and upload coverage report.
  • Finally, cache folders for next builds.

On nightly: You shouldn't activate cargo clippy as it breaks often with cargo updates, thus it's not worth the trouble and will often break the CI pipeline. Run it locally on your environment or make it an always succeeding step on CircleCI with:

rustup run nightly cargo build --features clippy | true

Important thing to note: If you were using CircleCI 1.0 to build your project before, beware of the key you're using for the restore_cache/save_cache steps: use a new key to make sure CircleCI does not restore an old cache from precedent builds using 1.0.

Codecov Script

The codecov script is pretty simple as well, it produces coverage report using kcov, goes through all the report files in the build directory and uploads it to your codecov account. We assume this script is located under $PROJECT_ROOT/.scripts and that you setup your CODECOV_TOKEN accordingly in your .circleci/config.yml file (if you're using a private repository).

#!/bin/bash

# Replace 'myproject' by your project name
REPORT=$(find target/debug -maxdepth 1 -name '<myproject-*' -a ! -name '*.d')

for file in $REPORT; do
    mkdir -p "target/cov/$(basename $file)"
    kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"
done

wget -O - -q "https://codecov.io/bash" > .codecov
chmod +x .codecov
./.codecov -t $CODECOV_TOKEN
echo "Uploaded code coverage"

The Dockerfile

The rust image is defined by the Dockerfile below (I stripped the portions where I install some dependencies such as Capn'proto):

FROM debian:stretch

# Some of the dependencies I need to build a few libraries,
# personalize to your needs. You can use multi-stage builds 
# to produce a lightweight image.
RUN apt-get update && \
    apt-get install -y curl file gcc g++ git make openssh-client \
    autoconf automake cmake libtool libcurl4-openssl-dev libssl-dev \
    libelf-dev libdw-dev binutils-dev zlib1g-dev libiberty-dev wget \
    xz-utils pkg-config python

# Install libraries/tools to cache here (OpenSSL, Capn'proto, kcov, etc.)
[...]

ENV KCOV_VERSION 33
RUN wget https://github.com/SimonKagstrom/kcov/archive/v$KCOV_VERSION.tar.gz && \
    tar xzf v$KCOV_VERSION.tar.gz && \
    rm v$KCOV_VERSION.tar.gz && \
    cd kcov-$KCOV_VERSION && \
    mkdir build && cd build && \
    cmake .. && make && make install && \
    cd ../.. && rm -rf kcov-$KCOV_VERSION

RUN curl https://sh.rustup.rs -sSf | sh -s -- -y

ENV PATH "$PATH:/root/.cargo/bin"
ENV RUSTFLAGS "-C link-dead-code"
ENV CFG_RELEASE_CHANNEL "nightly"

RUN rustup update && \
    rustup install nightly && \
    rustup default nightly

ENV RUSTFMT_VERSION 0.3.1
RUN wget https://github.com/rust-lang-nursery/rustfmt/archive/${RUSTFMT_VERSION}.tar.gz && \
    tar xzf ${RUSTFMT_VERSION}.tar.gz && rm ${RUSTFMT_VERSION}.tar.gz && \
    cd rustfmt-${RUSTFMT_VERSION} && \
    $HOME/.cargo/bin/cargo install --path . && \
    cd .. && rm -rf rustfmt-${RUSTFMT_VERSION}

RUN bash -l -c 'echo $(rustc --print sysroot)/lib >> /etc/ld.so.conf'
RUN bash -l -c 'echo /usr/local/lib >> /etc/ld.so.conf'
RUN ldconfig

The Rust installation is standard using rustup. We install both stable and nightly channels.

ENV RUSTFLAGS "-C link-dead-code" is important here as it allows you to produce a coverage report on dead portions of your codebase.

The last three lines of the Dockerfile avoids having to export LD_LIBRARY_PATH in your circle.yml in order to be able to find .so files that are needed to correctly run rustfmt-nightly (libsyntax). This also exports /usr/local/lib so that it could find libraries such as Capn'proto or kcov.

Important thing to note: make sure the rustfmt-nightly version installed on your machine matches the one installed in the image, otherwise you'll have build failing often because of formatting issues. A good reflex is to upload a new image once you update your local installation.

Personalize this image to your needs and don't forget to upload to your Docker Hub account or any other registry with docker build/docker push.

With these, you should be good to go and build your project successfully.

Happy Rust'ing!