Building C and C++ Projects with CMake

Updated

September 12, 2026

Building C and C++ Projects with CMake

CMake projects should not grow a hand-written cmake -B build in buildPhase. The boring default is: cmake in nativeBuildInputs, optional ninja, flags in cmakeFlags, and stdenv’s configure/build/install hooks.

Mental model

Putting cmake on nativeBuildInputs enables the CMake setup hook:

cmake -B build -DCMAKE_INSTALL_PREFIX=$out  [+ cmakeFlags]
cmake --build build
cmake --install build

nativeBuildInputs = tools that run on the build machine (cmake, ninja, pkg-config). buildInputs = libraries linked into the binary (zlib, openssl). Mixing them up is how cross breaks.

stdenv.mkDerivation {
  nativeBuildInputs = [ cmake ninja ];
  buildInputs = [ zlib ];
  cmakeFlags = [ "-DENABLE_DESK=ON" ];
}

Worked examples

Case 1: Tiny CMake project

Save as cmake_project.nix:

# cmake_project.nix
{ lib, stdenv, cmake }:

stdenv.mkDerivation {
  pname = "desk-math-engine";
  version = "1.0.0";
  src = ./.;
  nativeBuildInputs = [ cmake ];
  preConfigure = ''
    cat > CMakeLists.txt << 'EOF'
    cmake_minimum_required(VERSION 3.20)
    project(DeskMath C)
    add_executable(desk-calc main.c)
    install(TARGETS desk-calc DESTINATION bin)
    EOF
    cat > main.c << 'EOF'
    #include <stdio.h>
    int main(void) {
      puts("Desk Math Engine: 42");
      return 0;
    }
    EOF
  '';
  meta = {
    description = "Desk calculation engine";
    license = lib.licenses.mit;
  };
}
# default.nix
{ pkgs ? import <nixpkgs> { } }:

pkgs.callPackage ./cmake_project.nix { }
nix-build default.nix
./result/bin/desk-calc

Output:

Desk Math Engine: 42

Move CMakeLists.txt / main.c into git and delete preConfigure for a real tree.

Case 2: cmakeFlags

cmakeFlags = [
  "-DCMAKE_BUILD_TYPE=Release"
  "-DBUILD_TESTING=OFF"
];

Do not append these onto a custom buildPhase. The hook reads cmakeFlags.

Case 3: Ninja

nativeBuildInputs = [ cmake ninja ];

The hook prefers Ninja when ninja is on nativeBuildInputs. Faster incremental compiles in the sandbox still start from zero each derivation — Ninja mainly helps huge trees and nicer logs.

Case 5: Tests

doCheck = true;
# CMake: enable tests in cmakeFlags if needed
# cmakeFlags = [ "-DBUILD_TESTING=ON" ];

Hermetic tests only. A test that curls the network fails the sandbox. doCheck = false; with a comment when upstream tests are broken on Nix — then a passthru.tests later.

The trap

The trap is buildPhase = "cmake -B build && cmake --build build";. You skip CMAKE_INSTALL_PREFIX=$out and the stdenv flags. Use cmakeFlags and the hook.

The other trap is buildInputs = [ cmake ];. cmake then looks like a runtime library. nativeBuildInputs.

The boring rule

  • cmake (and ninja) in nativeBuildInputs.
  • Linked libs in buildInputs. pkg-config is native. strictDeps on new CMake packages.
  • Options via cmakeFlags, not a custom cmake command.
  • Real CMakeLists.txt in git; preConfigure only for the listing in this book.
  • doCheck when tests do not need the network.

Try this

  1. Case 2: add -DCMAKE_BUILD_TYPE=Release; nix log and grep CMAKE_BUILD_TYPE.
  2. Move cmake to buildInputs; nix-build; note warnings or cross pain; put it back.
  3. Add zlib and find_package(ZLIB) in a lab CMakeLists.txt.
  4. nix path-info -Shr result — cmake should not appear in the runtime closure.