Philipp Schuster, Markus Partheymüller · 8 min read

Mastering Nix Packaging: CMake Projects with Corrosion and Rust Dependencies

At Cyberus, we use Nix and NixOS for reproducible, declarative builds and configurations. In a research project exploring VMI under Linux/KVM, we faced challenges packaging a key component. This post covers those difficulties and our solution.

Mastering Nix Packaging: CMake Projects with Corrosion and Rust Dependencies

At Cyberus we love to use Nix and NixOS, as they enable our team to use reproducible and declarative build environments and configurations. Since adopting Nix six years ago, we left the times behind where a project builds on one system but not another. Recently, we faced a new interesting challenge in that domain, which we would love to discuss in the following.

Introduction: Overcoming Packaging Challenges in Nix and NixOS

For one of our research projects we are packaging SmartVMI1 in Nix. Unfortunately, SmartVMI uses CMake with Corrosion2 as build system. The build does not split downloading of dependencies from the actual build process properly — a property that makes it hard to package in Nix!

We resolved these challenges by pre-fetching resources, modifying CMake configurations, and leveraging Nix tools like crane for Cargo vendoring. Read on for a detailed walkthrough.

In this post we therefore focus on patching CMake-projects with minor changes in a way that the projects can leverage Corrosion2 in a Nix derivation. To help follow along, we built a demonstrator3 that you can find on our GitHub.

Before diving into the specifics of our solution, we’ll take a closer look at the constraints and requirements of Nix packaging.

Why Nix and NixOS Demand Explicit Dependency Management

One requirement that helps Nix achieve reproducibility is that all builds happen in isolation and all dependencies must be explicitly stated. This ensures that no dependency is fulfilled by accident. Unfortunately this complicates things with projects that were not designed to be packaged with Nix, because they often download dependencies as part of the build step — which is not possible in a normal Nix derivation.

For proper Nix packaging, it is mandatory to download any external resources before the actual build step. This becomes problematic if other dependency management tools, such as cargo, are invoked deep in the call stack of the build step.

Fixed-output derivations 4, which are a way of downloading external resources from network in a Nix derivation, won’t help us here either. Fixed-output derivations don’t work for derivations with references into the Nix store — which we will end up with eventually, since the binary built from SmartVMI needs many (also Nix-packaged) runtime dependencies!

Bridging CMake and Cargo with Corrosion and adding Nix Compatibility

Abstractly speaking, we present our solution to package a project in Nix, where a binary is built with the help of external network resources. More specifically, our use case is focused on a CMake-based project producing a binary from C++ code. This binary artifact utilizes functionality from a library in Rust, which uses Cargo as build system. The Rust library includes dependencies via Cargo, which needs network access to be downloaded. Corrosion is used to introduce the Cargo project into the CMake world.

Corrosion is a CMake module that bridges the gap between the CMake/C++ world and the Cargo/Rust world. The module imports targets built by Cargo in a way that CMake can use them. Corrosion also enables including generated header files into regular C++ targets to interface with the Rust library.

Corrosion expects crates to use cxx infrastructure5 to generate and export C++ FFI (Foreign Function Interface) bindings. For the cxx infrastructure to be available, Cargo needs to fetch it from network along with the other dependencies of the Rust library!

Cargo: Conveniences — and Burdens in Nix

Cargo offers many conveniences. One of these conveniences — automatically fetching dependencies from the network — becomes a challenge when packaging projects with Nix.

When packaging with Nix, you have to decouple downloading any external resources from the actual build step. This is the base for reproducibility and (ideally) bit-identical rebuilds.

Although there are solutions to package Cargo projects in Nix, such as crane6, they are only suited for packaging standalone Cargo projects. The existing tooling does not work well for projects that use a combination of different build systems.

Corrosion: Failing Steps

The default Nix-approach to package the CMake project without further adjustments uncovers multiple failing build steps, as they need network access. Some of the steps are not easily observable when inspecting the code, whereas other steps are. Among them, we have:

  • the initial download of the Corrosion CMake module
  • installing the correct version of the cxxbridge command
  • cargo build (which downloads dependencies)
  • cargo tree -i cxx --depth=0 (yes, Cargo needs a local registry for that. When one isn’t available, Cargo fetches data from the network)

Nix Packaging Best Practices: Decoupling Downloads from Build Steps

As an initial step we pre-fetch and provide all external sources before the build starts.

Please note that you can also find the code snippets presented here on GitHub3.

Providing Corrosion to CMake

CMake projects typically download further modules using the CMake function FetchContent_Declare. We create an if() around the existing statements in the projects source code to ensure that the resources can also be passed from the outside world. This allows us to prevent network access originating from CMake:

if (DEFINED ENV{CORROSION_SRC})
    message("Fetching corrosion source from env var: $ENV{CORROSION_SRC}")
    FetchContent_Declare(
            Corrosion
            SOURCE_DIR "$ENV{CORROSION_SRC}"
    )
else ()
    message("Fetching corrosion source from git")
    FetchContent_Declare(
            Corrosion
            GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
            GIT_TAG stable/v0.4
    )
endif ()

Inside the Nix derivation for the CMake project, we later can provide the source as Nix derivation:

stdenv.mkDerivation {
  /* ... */
  CORROSION_SRC = builtins.fetchTarball {
    url = "https://github.com/corrosion-rs/corrosion/archive/refs/tags/v0.4.10.tar.gz";
    sha256 = "sha256:1avfjaxgqx05fv2jqbqnk20rkyhhzq0r7kp9xyimqdvmgajarh3p";
  };
  /* ... */
}

Please note that this is simplified. Using a Flake-input to fetch Corrosion is a better alternative as it is a standardized way to manage Nix dependencies.

Checking in Cargo.lock files

Although it is uncommon for Rust library crates to check in Cargo.lock file, adding it to git is crucial for build reproducibility in Nix and for crane to work in the following steps. So, we have to unignore those lock files, if they are in .gitignore and add them to the source tree.

Adding cxxbridge-cmd to Cargo.toml

Before we can vendor the Cargo sources in the next step, we must ensure that cxxbridge-cmd, which is a prerequisite as mentioned above, appears in Cargo.lock. We achieve this by adding the following snippet to Cargo.toml of the Rust library:

[build-dependencies]
cxxbridge-cmd = "=1.0.128" # must match version of 'cxx'

It is crucial that the version matches the version of the cxx dependency. This is a base assumption of Corrosion!

Vendoring Cargo Sources in Nix with Crane

Vendoring Cargo source means that all remote files normally requested from crates.io are fetched into a local directory. A specific Cargo configuration is used to instruct Cargo to use only our vendored sources. Use cargo vendor to experiment with vendoring sources locally.

In Nix, we are using crane to vendor the crate in standalone mode (only the Rust sources). Specifically, the part with cargoVendored = is important:

{ craneLib, nix-gitignore }:

let
  cargoVendored = craneLib.vendorCargoDeps {
    src = nix-gitignore.gitignoreSource [ ] ./rust_src;
  };
in
{
  inherit cargoVendored;
}

${cargoVendored}/config.toml contains the content for .cargo/config.toml, which looks something like this:

[source.nix-sources-c19b7c6f923b580ac259164a89f2577984ad5ab09ee9d583b888f934adbbe8d0]
directory = "/nix/store/69vf6lgyn8q8mrd7x2wdc4fh5bc5nmy6-vendor-cargo-deps/c19b7c6f923b580ac259164a89f2577984ad5ab09ee9d583b888f934adbbe8d0"
[source.'crates-io']
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = "nix-sources-c19b7c6f923b580ac259164a89f2577984ad5ab09ee9d583b888f934adbbe8d0"

The referenced Nix store directory looks similar to the following listing. All dependencies from Cargo.lock are listed there:

anstyle-1.0.8 -> /nix/store/q3ns9yz8pxhq1yfy9hm4w5zg243xvlmk-cargo-package-anstyle-1.0.8
...

Providing cxxbridge-cmd via Nix

Using the vendored sources, we install cxxbridge-cmd at the exact right version. A simplified version is shown in the following dedicated Nix derivation which performs cargo install fully in offline mode:

let
  cxxVersion = /* read from ./rust_src/Cargo.lock */
in
runCommand "cxxbridge-cmd-${cxxVersion}"
  {
    nativeBuildInputs = [
      cargo
      gcc
    ];
  }
  ''
    # Directory with vendored sources
    DIR=$(cat ${rustLib.cargoVendored}/config.toml | grep -o '/nix/store/[^"]*' | cut -d'/' -f1-5)

    # Append the information to use the vendored sources and registry
    mkdir -p .cargo
    cat ${rustLib.cargoVendored}/config.toml > .cargo/config.toml

    # Make sure our config.toml will be used
    export CARGO_HOME="$PWD/.cargo"

    export CARGO_TARGET_DIR="$PWD/target"

    mkdir -p $out/bin
    # We install the Crate from a local source without any network usage.
    cargo install \
      --verbose \
      --offline \
      --root $out \
      --path "$DIR/cxxbridge-cmd-${toString cxxVersion}"
  ''

Building the CMake Project

Now we only have to put all the parts together. The simplified Nix derivation with all relevant inputs and modifications looks like this:

stdenv.mkDerivation {
  name = "dummy_binary";
  version = "0.0.0";
  src = nix-gitignore.gitignoreSource [ ] ../.;
  nativeBuildInputs = [
    cargo
    cmake
    # The package we built in the derivation above
    cxxbridge-cmd
    rustc
  ];

  # Set env var so that CMake knows the source location.
  CORROSION_SRC = corrosionSrc;

  # Prevent any network access of Cargo.
  CARGO_NET_OFFLINE = "true";

  # Prepare the environment for Cargo so that no network access is required.
  preConfigure = ''
    mkdir -p rust_src/.cargo
    cat ${rustLib.cargoVendored}/config.toml >> rust_src/.cargo/config.toml
  '';
}

Conclusion: Simplifying Nix Packaging for Complex Build Systems

In this blog post, we discussed the problems of packaging CMake projects in Nix that fetch external data from the network as part of their build process. In addition to the cargo dependencies we discussed in this post, this could also include git submodules in other contexts. The approach would be similar in nature.

We showed that with only minor adjustments to the build system and no modifications to Corrosion, we can successfully package these kind of projects in Nix. Please find more information in our GitHub repository3.

This approach demonstrates how Cyberus can tackle packaging challenges for complex projects, reinforcing our expertise in Nix-based builds. We look forward to collaborating on similar endeavors — contact us to learn how we can assist with your projects.

References

Footnotes

  1. https://www.smartvmi.org/

  2. https://github.com/corrosion-rs/corrosion 2

  3. https://github.com/cyberus-technology/cmake-corrosion-nix 2 3

  4. https://phip1611.de/blog/accessing-network-from-a-nix-derivation/

  5. https://cxx.rs/

  6. https://crane.dev/

Share: