Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

flakoboros

flakoboros logo: the 6 lambda from nix flake, but rotated as if they are eating each other as a ouroboros

Circular Packaging framework with nix Flakes, including ROS support

Goal

When one package is defined in a flake, we can by default:

  • nix build: build the package (and run its tests)
  • nix shell: open a shell with the package ready to be used
  • nix develop: open a shell without the built package, but with everything required to build it
  • nix run: execute the main program from the package

Flakoboros provide the same experience with multiple packages in a flake:

  • nix build: build all packages (and run their tests)
  • nix shell: open a shell with all packages ready to be used
  • nix develop: open a shell without any of the packages, but with everything required to build them all
  • nix run: execute the main program from all the packages (eg. a python interpreter with all the python modules available)

To do so, the main API is designed around the idea that your packages are distributed in another more-or-less central Nix repository (eg. nixpkgs or nix-ros-overlay), and you just need my-package.overrideAttrs { src = lib.cleanSource ./. } in the flake of the source.

Circular Packaging ?

That notion of re-using an existing distribution of a package inside its source.

API overview

example for eigenpy:

{
  description = "Bindings between Numpy and Eigen using Boost.Python";

  inputs.flakoboros.url = "github:gepetto/flakoboros";

  outputs =
    inputs:
    inputs.flakoboros.lib.mkFlakoboros inputs (
      { lib, ... }:
      {
        pyOverrideAttrs.eigenpy = {
          src = lib.cleanSource ./.;
        };
      }
    );
}
If you need access to more data, a callable form is also available (clic here to reveal)
{
  pyOverrideAttrs.example-robot-data =
    { pkgs-final, pkgs-prev, drv-final, drv-prev, py-final, py-prev, ... }:
    {
      src = lib.cleanSource ./.;
      cmakeFlags = [ (lib.cmakeBool "BUILD_TESTING" drv-final.doCheck) ];
      nativeBuildInputs = drv-prev.nativeBuildinputs ++ [ pkgs-final.ninja ];
      dependencies = drv-prev.dependencies ++ [ py-final.rerun-sdk ];
    };
}
Behind the scene, this is a shallow wrapper around `flake-parts.lib.mkFlake`, which can be used directly (clic here to reveal)
{
  description = "Bindings between Numpy and Eigen using Boost.Python";

  inputs = {
    flakoboros.url = "github:gepetto/flakoboros";
    flake-parts.follows = "flakoboros/flake-parts";
    systems.follows = "flakoboros/systems";
  };

  outputs =
    inputs:
    inputs.flake-parts.lib.mkFlake { inherit inputs; } (
      { lib, ... }:
      {
        systems = import inputs.systems;
        imports = [
          inputs.flakoboros.flakeModule
          {
            flakoboros = {
              pyOverrideAttrs.eigenpy = {
                src = lib.cleanSource ./.;
              };
            };
          }
        ];
      }
    );
}

This will:

  • define overlays.flakoboros with this override
  • (if you don’t opt-out) instanciate pkgs with that overlay
  • inherit this in packages.${system}.py-eigenpy
  • define packages.${system}.default as a buildEnv including all others packages.${system}.* (for nix build & nix shell)
  • define devShells.${system}.default as a mkShell with inputsFrom the same packages.${system}.* (for nix develop / nix-direnv)

ROS

If you have ROS packages, the default package and devShell will use a default ROS distribution (eg. rolling), but the same features are available for other distros, with eg.

  • nix build .#ros-humble
  • nix shell .#ros-jazzy
  • nix develop .#ros-kilted
  • nix run .#ros-rolling

Also, standard ROS tools like colcon and ros2cli will be included.

Extend pkgs, aka alternate universes

{
  extends.eigen5 = final: { eigen = final.eigen_5; };
  pyOverrideAttrs.eigenpy = {
    src = lib.cleanSource ./.;
  };
};

This will:

  • define pkgs, packages.${system}.py-eigenpy and packages.${system}.default as before
  • define pkgs.pkgs-eigen5 as another pkgs instance but where eigen is overriden everywhere by eigen_5
  • define packages.${system}.pkgs-eigen5, equivalent to packages.${system}.default but with eigen 5
  • add scoped everything else, eg. packages.${system}.pkgs-eigen5.py-eigenpy (technically packages.${system}.pkgs-eigen5.passthru.py-eigenpy)
  • define devShells.${system}.pkgs-eigen5

So in your CI, you can build . and .#pkgs-eigen5 to check all your stack with both eigen 3.4.1 and 5.0.1.

Also, you can either echo 'use flake .' > .envrc or echo 'use flake .#pkgs-eigen5' > .envrc, and follow your usual cmake -B build && cmake --build build workflow.

API overview

flake-parts and mkFlake

Flakoboros is a flake-parts module. Its main entrypoint is a flakoboros config attrset which can be used in your flake.nix as:

{
  description = "The description of your project";

  inputs = {
    flakoboros.url = "github:gepetto/flakoboros";
    flake-parts.follows = "flakoboros/flake-parts";
    systems.follows = "flakoboros/systems";
  };

  outputs =
    inputs:
    inputs.flake-parts.lib.mkFlake { inherit inputs; } (
      { lib, ... }:
      {
        systems = import inputs.systems;
        imports = [
          inputs.flakoboros.flakeModule
          {
            flakoboros = {
              # Every config option goes here
            };
          }
        ];
      }
    );
}

mkFlakoboros shortcut

To reduce the boilerplate, if you don’t need anything else from flake-parts, you can use this shortcut:

{
  description = "The description of your project";

  inputs.flakoboros.url = "github:gepetto/flakoboros";

  outputs =
    inputs:
    inputs.flakoboros-parts.lib.mkFlakoboros inputs (
      { lib, ... }:
      {
        # Every config option goes here
      }
    );
}

Controlling overlays.flakoboros

overlays.flakoboros overview

The current flake overlays.flakoboros output is generated by:

  • overrideAttrs, pyOverrideAttrs, rosOverrideAttrs
  • overrides, pyOverrides, rosOverrides
  • packages, pyPackages, rosPackages

Packages attrs overrides

The common and recommended case is when your package is already defined in nixpkgs, nix-ros-overlay, nur, or any kind of already available nix package collection. In that case, you can simply override very few of its attrs:

overrideAttrs.foo = {
  src = lib.cleanSource ./.;
};

Note

This is just syntactic sugar that will generate overlays.flakoboros as

pkgs-final: pkgs-prev: {
  foo = pkgs-prev.foo.overridAttrs (drv-final: drv-prev: {
    src = lib.cleanSource ./.;
  });
}

If you need more control, you can use a callable form:

overrideAttrs.foo =
  { pkgs-final, pkgs-prev, drv-final, drv-prev }:
  {
    src = lib.cleanSource ./.;
    nativeBuildInputs = drv-prev.nativeBuildinputs ++ [ pkgs-final.ninja ];
  };

Tip

when you don’t need all of the inputs, something like { pkgs-final, drv-prev, ... } is more appropriate

If your package is in pythonPackages, you’ll need pyOverrideAttrs instead. In that case you can also use python-final and python-prev.

And if it scoped by ROS distribution, rosOverrideAttrs will generate one override for each rosDistros. And you get ros-final and ros-prev.

Packages overrides

In the same way, you may need to override some of the inputs of a package (or python / ROS package), eg.:

pyOverrides.django = { withGdal = true; };

Note

This example would configure overlays.flakoboros as

pkgs-final: pkgs-prev: {
  pythonPackagesExtensions = pkgs-prev.pythonPackagesExtensions ++ [
    (
      python-final: python-prev: {
        django = python-prev.django.override { withGdal = true; };
      }
    )
  ];
}

Important

You can use both overrideAttrs.foo and overrides.foo simultaneously.

Behind the hood, overlays.flakoboros is a lib.composeManyExtensions including an overlay with all overrides first, and overlay with all overrideAttrs afterwards.

New packages definitions

Finally, if your package is not available in your inputs, you can use an usual package.nix kind of file:

packages.foo = ./package.nix

Or you could inline its definition:

pyPackages.pyboo =
  { buildPythonPackage, numpy, uv-build }:
  buildPythonPackage {
    inherit ((lib.importTOML ./pyproject.toml).project) name version;
    pyproject = true;

    src = lib.cleanSource ./.;

    build-system = [ uv-build ];
    dependencies = [ numpy ];
  };

Defining pkgs

Default pkgs instance

By default, a pkgs instance (provided as input of perSystem) will be defined from flakoboros nixpkgs input, plus several overlays:

  • flakoboros inputs.nix-ros-overlay.overlays.default
  • the overlays config option
  • the current flake overlays.flakoboros, which is built as described in Controlling overlay-flakoboros

You can control its configuration via nixpkgsConfig option.

Opt-out

If you don’t want this instance, you can set the pkgs config option to false.

Extends

The extends config option provide other pkgs instances (also available as input of perSystem).

In that config option, for each key name, a pkgs-${name} is defined as the pkgs instance from above plus the overlay provided as value.

This overlay is also exposed in the current flake overlays, for reference from other flakes.

Then, the current flake packages output gets a new pkgs-${name} env, similar to the default one, but from that pkgs instance. All other packages are also exposed as passthru of that pkgs-${name} env.

And finally, the current flake devShells also get a pkgs-${name} version of its default.

For example:

{
  pyOverrideAttrs.eigenpy = {
    src = lib.cleanSource ./.;
  };
  extends.eigen5 = final: _prev: {
    eigen = final.eigen_5;
  };
}

will define:

$ nix flake show

├───devShells
│   └───x86_64-linux
│       ├───default: development environment 'flakoboros-default-devShell'
│       └───pkgs-eigen5: development environment 'flakoboros-default-devShell'
├───overlays
│   ├───eigen5: Nixpkgs overlay
│   └───flakoboros: Nixpkgs overlay
└───packages
    └───x86_64-linux
        ├───default: package 'ros-env'
        ├───pkgs-eigen5: package 'ros-env'
        └───py-eigenpy: package 'python3.13-eigenpy-3.12.0'

So you can nix develop (or nix-direnv):

  • . to get all dependencies to build the package with eigen 3
  • .#pkgs-eigen5 to get all dependencies to build the package with eigen 5

And you can nix build:

  • .#py-eigenpy: the package (built with eigen 3)
  • .#pkgs-eigen5.py-eigenpy: the package (built with eigen 5)
  • .: an env with the package (built with eigen 3)
  • .#pkgs-eigen5: an env with the package (built with eigen 5)

With that, you can check everything in CI with eg.:

jobs:
  build:
    runs-on: "${{ matrix.os }}"
    strategy:
      matrix:
        os: ["ubuntu-24.04", "ubuntu-24.04-arm", "macos-26-intel", "macos-26"]
        extends: ["", "pkgs-eigen5"]
    steps:
      - uses: actions/checkout@v6
      # install nix
      - run: nix build -L ".#${{ matrix.extends }}"

Generating your packages and devShells

devShells

Flakoboros module will populate the current flake devShells.default output with a default mkShell whose inputsFrom are the pkgs (and pkgs.python3Packages / pkgs.rosPackages.${rosShellDistro} ) attributes listed in overlay-flakoboros.

Thus, you will get all the dependencies required to build all your packages in a nix shell accessible with nix develop (or nix-direnv).

If you have ROS packages, a ros-${distro} variant will be created for each ROS distro in the rosDistros config option. So devShells.default == devShells.ros-${rosShellDistro}.

packages

In the same manner, the current flake packages.default output will default to a buildEnv (from nix-ros-overlay) including everything listed in overlay-flakoboros.

Those individual packages are also exposed, in a name prefixed by py- for python packages and ros-${distro} (for each ROS distro in the rosDistros config options) for ROS packages.

Adding packages

The lists of packages in packages and devShells can be augmented with:

  • the extraPackages config option (and extraPyPackages / extraRosPackages),
  • the extraDevPackage config option (and extraDevPyPackages / extraDevRosPackages).

In the first case, the nix-built version will be available directly in the devShells. In the second case, devShells will only include their dependencies, so that you can build them too.

Excluding packages

The filterPackages config option is for that.

Building everything in checks

Most of the time, running nix build will build the packages.default env including all the packages you care about, so this should be enough.

But if you add manually other things to packages and/or devShells, you may want a shortcut to build everything.

The check config option is for that, as it will expose everything in the current flake checks output, so that you can simply nix flake check.

Plain lib

lib.ros2gz

mapping of recommended Gazebo distro per ROS distro

lib.ros2qt

mapping of recommended Qt per ROS distro

lib.mkQtHelpers

Qt helpers

lib.rosWrapperArgs

set many env vars in a makeWrapperArgs format for postBuild

lib.rosShellHook

set many env vars in a bash format for pkgs.mkShell { shellHook = … }

lib.getRosBasePackages

get a list of common ros packages.

Don’t hesitate to contact us to extend this list !

lib.mkLibFlakoboros

Generate libFlakoboros

lib.loadVersion

Extract version from a structured file

lib.rosVersion

Extract version from a ROS package.xml file

lib.pythonVersion

Extract version from a python pyproject.toml file

Generated lib

libFlakoboros.buildFlakoborosShell

Build a shell with all packages, except those excluded

libFlakoboros.buildFlakoborosDevShell

Build a shell without the packages in buildFlakoborosShell, but with their dependencies

libFlakoboros.buildFlakoborosEnv

Build an env with all packages from buildFlakoborosShell, plus extra Qt 5 or 6 things

libFlakoboros.buildFlakoborosRosEnv

buildFlakoborosEnv plus ros base packages

libFlakoboros.buildFlakoborosRosDevEnv

buildFlakoborosRosEnv, without the packages in buildFlakoborosShell, but with their dependencies

libFlakoboros.buildFlakoborosRosDevShell

buildFlakoborosDevShell plus ros base packages

technically, we use buildFlakoborosRosDevEnv to set some ros path variables.

Setup Nix

If you are here and don’t have nix yet, here is probably the easiest and fastest way to get started on ubuntu >= 24.04 “noble” / debian >= 13 “trixie” (because we need nix >= 2.18):

# 1. install the right apt package
sudo apt install -y nix-setup-systemd

# 2. activate the new CLI and flake features
echo 'experimental-features = nix-command flakes' | sudo tee -a /etc/nix/nix.conf

# 3. (optional) if you trust us, add our binary caches to avoid recompiling everything
echo 'extra-substituters = https://gepetto.cachix.org https://attic.iid.ciirc.cvut.cz/ros' | sudo tee -a /etc/nix/nix.conf
echo 'extra-trusted-public-keys = gepetto.cachix.org-1:toswMl31VewC0jGkN6+gOelO2Yom0SOHzPwJMY2XiDY= ros:JR95vUYsShSqfA1VTYoFt1Nz6uXasm5QrcOsGry9f6Q=' | sudo tee -a /etc/nix/nix.conf

# 4. activate your new nix.conf
sudo systemctl restart nix-daemon

# 5. allow yourself to use nix
sudo usermod -aG nix-users $(whoami)
newgrp nix-users

# 6. test everything is fine
nix run nixpkgs#ponysay it works

Other setup methods

If you don’t want this nix-setup-systemd apt package, other options include:

Docker

If you want to use nix in docker but don’t already know exactly how, please discuss with us about it. There are many good solutions, and the best one really depend on your use case. There are also many wrong problems that you should simply avoid.

Build a ROS workspace

In this example, we will show how to deal with a standard ROS development workflow, where the dependencies are backed by nix.

Define which repos you want to manually build

We will split the whole dependency tree in two distinct sets: the source repositories you will manually build for your development, and those which will be handled by nix.

Note

The limitiation here is that we should not have any package in the second set that depend on any package from the first :)

One usual way of defining multiple repositories in the ROS community is with this eg. agimus.repos yaml file:

repositories:
  agimus-controller:
    type: git
    url: https://github.com/agimus-project/agimus-controller
    version: main
  agimus-demos:
    type: git
    url: https://github.com/agimus-project/agimus-demos
    version: main
  agimus-msgs:
    type: git
    url: https://github.com/agimus-project/agimus-msgs
    version: main
  linear-feedback-controller:
    type: git
    url: https://github.com/loco-3d/linear-feedback-controller
    version: main
  linear-feedback-controller-msgs:
    type: git
    url: https://github.com/loco-3d/linear-feedback-controller-msgs
    version: main

Then you can clone all of that in a src directory:

mkdir src
vcs import --input agimus.repos src

Tip

If you don’t already have the vcs program, you can eg.: nix run nixpkgs#vcs2l -- import --input agimus.repos src

Find all the ROS packages in those repositories

Then we can find all the package.xml files in our src dir, look for the name key, and format that for nix:

fd package.xml src -x xq .package.name | tr _ - | sort -u

Tip

If you don’t already have the wonderful fd and/or xq available: nix shell nixpkgs#{fd,xq} --command fd package.xml src -x xq .package.name | tr _ - | sort -u

Write your flake

Now you can create a flake.nix:

{
  description = "example flake for agimus demos with flakoboros";
  inputs.gepetto.url = "github:gepetto/nix";
  outputs =
    inputs:
    inputs.gepetto.lib.mkFlakoboros inputs (
      { lib, ... }:
      {
        rosDistros = [ # We don't have kilted/rolling on those packages yet
          "humble"
          "jazzy"
        ];
        rosShellDistro = "humble"; # rolling would be the default without this
        extraDevRosPackages = [ # Put here the list you got at the previous step
          "agimus-controller"
          "agimus-controller-ros"
          "agimus-demo-00-franka-controller"
          "agimus-demo-01-lfc-alone"
          "agimus-demo-02-simple-pd-plus"
          "agimus-demo-02-simple-pd-plus-tiago-pro"
          "agimus-demo-03-mpc-dummy-traj"
          "agimus-demo-03-mpc-dummy-traj-tiago-pro"
          # "agimus-demo-04-dual-arm-tiago-pro"
          "agimus-demo-04-visual-servoing"
          "agimus-demo-05-pick-and-place"
          "agimus-demo-06-regrasp"
          # "agimus-demo-07-deburring"
          "agimus-demo-08-collision-avoidance"
          "agimus-demos"
          "agimus-demos-common"
          "agimus-demos-controllers"
          "agimus-msgs"
          "linear-feedback-controller"
          "linear-feedback-controller-msgs"
        ];
      }
    );
}

Warning

Work in progress: demos 04 and 07 must be commented for now

Activate your development shell

The standard nix way here is nix develop: it will start a bash shell with everything you need to continue.

But if you want something a bit more comfortable, you can set up nix-direnv. Once this is done, auto-activating the nix develop in the current folder by adjusting environment variables instead of spawning a new shell can be achieved with:

echo 'use flake .' > .envrc
direnv allow

Build your packages

colcon build

Enable the new prefix

This is automatically done on entering nix develop if install/setup.bash exists, but before the first compilation that was not the case. Therefore, for this time only, you should probably:

source install/setup.bash

Profit

ros2 launch agimus_demo_03_mpc_dummy_traj bringup.launch.py use_gazebo:=true use_rviz:=true

Switch ros distro ?

Time to change ROS distro, like upgrade from humble to jazzy ? Sure :)

Remember that rosShellDistro = "humble"; ? It means that by default, nix develop . / use flake . will be equivalent to nix develop .#ros-humble / use flake .#ros-humble.

Obviously, you can therefore use .#ros-jazzy in this example:

rm -rf build install
echo 'use flake .#ros-jazzy' > .envrc # or 'nix develop .#ros-jazzy'
direnv allow
colcon build
source install/setup.bash
ros2 launch agimus_demo_03_mpc_dummy_traj bringup.launch.py use_gazebo:=true use_rviz:=true

Full module options search