Skip to content

Tutorial: Building a C++ package#

This example shows how to build a C++ project with CMake and use it together with pixi-build. To read more about how building packages work with pixi see the Getting Started guide.

We'll start off by creating a project that use nanobind to build Python bindings. That we can also test using pixi. We'll later combine this example together with a Python package.

Creating a new project#

To get started, create a new project with pixi:

pixi init python_bindings

This should give you the basic pixi.toml to get started.

We'll now create the following source directory structure:

python_bindings/
├── CMakeLists.txt
├── pixi.toml
├── .gitignore
└── src
    └── bindings.cpp

Creating the project files#

Next up we'll create the:

  • pixi.toml file that will be used to configure pixi.
  • CMakeLists.txt file that will be used to build the bindings.
  • src/bindings.cpp file that will contain the bindings.

The pixi.toml file#

Use the following pixi.toml file, you can hover over the annotations to see why each step was added.

[workspace]
channels = ["https://prefix.dev/conda-forge"]
platforms = ["osx-arm64", "linux-64", "win-64"]
preview = ["pixi-build"]                        # (1)!

[dependencies] # (2)!
python = ">=3.12, <3.13"
python_bindings = { path = "." }

[tasks]
start = "python -c 'import python_bindings as b; print(b.add(1, 2))'" # (3)!

[package] # (4)!
name = "python_bindings"
version = "0.1.0"

[build-system]
build-backend = { name = "pixi-build-cmake", version = "*" } # (5)!
channels = [
  "https://prefix.dev/pixi-build-backends",
  "https://prefix.dev/conda-forge",
]

[host-dependencies]
cmake = ">=3.20, <3.27"      # (8)!
nanobind = ">=2.4.0, <2.5.0" # (6)!
python_abi = ">=3.12, <3.13" # (7)!
  1. Add the preview feature pixi-build that enables pixi to build the package.
  2. These are the workspace dependencies, and we add a reference to our own package.
  3. Let's add a task that will run our test, for this we require a python interpreter.
  4. This is where we specify the package name and version. This section denotes that there is both a workspace and a package defined by this pixi.toml file.
  5. We use pixi-build-cmake as the build-system, so that we get the backend to build cmake packages.
  6. We use the nanobind package to build our bindings.
  7. We need python to build the bindings, so we add a host dependency on the python_abi package.
  8. We override the cmake version to ensure it matches our CMakeLists.txt file.

The CMakeLists.txt file#

Next lets add the CMakeList.txt file:

cmake_minimum_required(VERSION 3.20...3.27)
project(python_bindings)

find_package(Python 3.8 COMPONENTS Interpreter Development.Module REQUIRED) # (1)!

execute_process(
  COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
  OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT
) # (2)!

execute_process(
    COMMAND ${Python_EXECUTABLE} -c "import sysconfig; print(sysconfig.get_path('purelib'))"
    OUTPUT_VARIABLE PYTHON_SITE_PACKAGES
    OUTPUT_STRIP_TRAILING_WHITESPACE
) # (3)!

find_package(nanobind CONFIG REQUIRED) # (4)!

nanobind_add_module(${PROJECT_NAME} src/bindings.cpp) # (5)!

install( # (6)!
    TARGETS ${PROJECT_NAME}
    EXPORT ${PROJECT_NAME}Targets
    LIBRARY DESTINATION ${PYTHON_SITE_PACKAGES}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${BINDIR}
)

  1. Find python, this actually finds anything above 3.8, but we are using 3.8 as a minimum version.
  2. Because we are using python in a conda environment we need to query the python interpreter to find the nanobind package.
  3. Because we want to make the installation directory independent of the python version, we query the python site-packages directory.
  4. Find the installed nanobind package.
  5. Use our bindings file as the source file.
  6. Install the bindings into the designated prefix.

The src/bindings.cpp file#

Next lets add the src/bindings.cpp file, this one is quite simple:

#include <nanobind/nanobind.h>

int add(int a, int b) { return a + b; } // (1)!

NB_MODULE(python_bindings, m)
{
    m.def("add", &add); // (2)!
}
  1. We define a function that will be used to add two numbers together.
  2. We bind this function to the python module using the nanobind package.

Testing if everything works#

Now that we have created the files we can test if everything works:

$ pixi run start
3

This command builds the bindings, installs them and then runs the test task.

Conclusion#

In this tutorial, we created a pixi package using C++. It can be used as-is, to upload to a conda channel. In another tutorial we will learn how to add multiple pixi packages to the same workspace and let one pixi package use another.

Thanks for reading! Happy Coding 🚀

Any questions? Feel free to reach out or share this tutorial on X, join our Discord, e-mail us or follow our GitHub.