Я использую Nix уже не на первой своей работе. Это универсальный инструмент, который может сильно облегчить жизнь разработчика, автоматизируя управление средой разработки.

Сегодня я хотел бы подробнее разобрать самую полезную часть Nix — управление пакетами из репозитория nixpkgs.

Я использовал и использую разные языки. В Rust есть прекрасный пакетный менеджер cargo и инсталлятор rustup, для JavaScript — npm. Мне также нравится conda в мире Python.

Мне всегда не хватало чего-то подобного для проектов на Си и C++. Пакетные менеджеры для этих языков часто оставляют желать лучшего. Даже если они работают, в их репозиториях может не быть нужных библиотек. Даже если вроде всё работает хорошо, может оказаться, что для работы бинарного кэширования нужно прилагать усилия, а когда это что-то вроде разных версий Qt — собирать всё на машине разработчика неприятно.

Я хотел, чтобы инструмент из коробки давал максимум без дополнительной настройки.

Поэтому я расскажу, как использовать Nix в качестве пакетного менеджера для Си и C++.


Почему именно Nix?

  • В nixpkgs — репозитории с пакетами для Nix — более 100 тысяч рецептов сборки разных программ и библиотек.

  • Большая часть из них — на Си и C++. Это значит, что любая популярная библиотека или утилита может уже быть описана, и для её подключения достаточно лишь добавить зависимость.

  • Работа с зависимостями — головная боль в мире C и C++; Nix решает её почти полностью, позволяя сфокусироваться на том, что важно — на написании продуктового кода.

  • Nix не привязан к конкретному языку: компиляторы, линтеры, различные тулзы почти наверняка уже есть в nixpkgs, так что добавление нового языка — просто добавление зависимости.

Например, я писал Bash-скрипт и “собирал” его с помощью Nix, чтобы гарантировать наличие утилит, которых может не быть в чистой установке Ubuntu. Даже у простых скриптов есть зависимости: Nix позволяет описать, например, jq, и быть уверенным, что в PATH будет нужная версия этого инструмента.

Контейнеры разработчика

Компании часто используют Docker или подобные контейнеры для среды разработки. Это работает, но в большинстве случаев управление зависимостями не требует контейнеров. Пакетные менеджеры языков справляются на простых файлах.

Контейнеризация — дополнительная сложность. Она приносит лишний слой абстракции, который может мешать: нужно настраивать, синхронизировать, возможны различия локально / в CI.

Новичкам Docker может мешать из-за дополнительных настроек и нужды понимать больше инфраструктуры.

Лично я предпочитаю не использовать контейнеры для разработки. Всегда просто брал и устанавливал глобально зависимости из их рецепта сборки.

Опыт разработчика

  • При внедрении нового инструмента часто не хочется менять существующую систему сборки (GNU Make, CMake и др.).

  • Иногда вы не контролируете репозиторий пакета, который надо собрать, и не можете его модифицировать.

Nix особенно хорош как внешний менеджер пакетов к уже существующей системе сборки. Хорошо работающие манифесты для систем сборки - это большой плюс в "никсификации" проекта.

Интеграция с IDE

Ещё одна боль в мире Си/C++ — поддержка со стороны IDE.

  • Редактор по умолчанию не знает, какие зависимости у проекта, и загружает только системные заголовки (например, /usr/include и др.).

  • Если библиотека собрана вручную, может потребоваться ручное прописывание Include path.

Я бы сказал что даже с поддержкой сборки из консоли у плюсов проблемы. Проект на JavaScript или Rust можно собрать всегда одной и той же командой, которая по необходимости установит нужную версию компилятора, библиотек, выполнит дополнительные конфигурационные скрипты, выставит переменные окружения. На плюсах у нас может быть (а может и не быть) скрипт build.sh. Со своими зависимостями.

Сценарий с заголовками в /usr/include тоже легко ломается в момент, когда вы работаете над несколькими проектами, которым требуются разные версии библиотек.

Сегодня мы решим все эти проблемы с помощью Nix.


Цели

  1. Создать два проекта на Си и C++: с библиотеками для работы с JSON — cJSON для Си и nlohmann_json для C++.

  2. Использовать CMake как систему сборки, в дополнение к Nix.

  3. Все зависимости и рабочее окружение описаны в репозитории: для воспроизведения нужен только Nix.

  4. Разработка в VSCode с работающим IntelliSense и плагином CMake.

  5. Простота конфигурации, максимальная автоматизация и воспроизводимость.

Глоссарий

  • Рабочее пространство (workspace) — корень репозитория, то, что вы открываете в IDE (например, VSCode).

  • Проект / пакет — часть репозитория, которую можно собрать и получить полезный самодостаточный артефакт (например, исполняемый файл).


Необходимое

  • Nix с включёнными flakes.

  • Рекомендую использовать этот инсталлятор:

curl -fsSL https://install.determinate.systems/nix | sh -s -- install

Nix можно установить разными способами, в том числе и без root-прав в системе.

Если следующая команда отрабатывает без ошибок, все отлично:

❯ nix flake --version  
nix (Nix) 2.28.3

Статья ориентирована на версию Nix 2.28.

direnv

direnv — утилита для управления средой в командной оболочке. Хотя напрямую не связана с Nix, она часто используется в связке.

Теперь, после того как у вас есть Nix, программы можно устанавливать из nixpkgs:

nix profile install nixpkgs#direnv
# Если используете bash, добавьте хук или прочитайте в руководстве
# к direnv как добавить его в вашу оболочку.
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc

Расширения VSCode

Для Nix тоже есть расширения для VSCode, с поддержкой Language Server Protocol. Для простоты мы не будем рассматривать это в этой статье.

Структура репозитория

Описываем репозиторий находится здесь.

.
├── CMakeLists.txt
├── README.md
├── flake.lock
├── flake.nix
└── src
    ├── c
    │   ├── CMakeLists.txt
    │   └── main.c
    └── cc
        ├── CMakeLists.txt
        └── main.cc

Корневой CMakeLists.txt содержит:

cmake_minimum_required(VERSION 3.10)
project(hello_json)

add_subdirectory(src/c)
add_subdirectory(src/cc)

Проект на C: hello-json-c

Начнем с описателя CMakeLists.txt:

find_package(cJSON REQUIRED)
add_executable(hello-json-c main.c)
target_link_libraries(hello-json-c PRIVATE cjson)
install(TARGETS hello-json-c DESTINATION bin)

Хотя find_package() ищет пакет в "системе" но к моменту когда вы или Nix запустит команду сборки, он установит необходимые пути поиска и это сработает.

Nix и другие пакетные менеджеры определяют содержание окончательного пакета по тому, что записано в результирующую (install) директорию на фазе установки. Поэтому нам необходимо описать эту фазу. Если этого не сделать, сборка через Nix не будет работать. Ручная сборка выполнением cmake --build будет работать всегда.

Структура директории с результатом должна удовлетворять FHS, поэтому мы пишем в /bin.

main.c:

#include <stdio.h>
#include <stdlib.h>
#include <cjson/cJSON.h>

int main(void)
{
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "message", "Hello, world from C and cJSON!");
    
    char *json_str = cJSON_Print(root);
    printf("%s\n", json_str);

    cJSON_Delete(root);
    free(json_str);

    return EXIT_SUCCESS;
}

Здесь все просто: создаем JSON из объекта с полем message и текстом "Hello, world from C and cJSON!".

Проект на C++: hello-json-cc

CMakeLists.txt не сильно отличается:

set(CMAKE_CXX_STANDARD 17)

find_package(nlohmann_json REQUIRED)
add_executable(hello-json-cpp main.cc)
target_link_libraries(hello-json-cpp PRIVATE nlohmann_json::nlohmann_json)
install(TARGETS hello-json-cpp DESTINATION bin)

main.cc:

#include <iostream>
#include <nlohmann/json.hpp>

int main()
{
    nlohmann::json hello;
    hello["message"] = "Hello, world from C++ and nlohmann::json!";

    std::cout << hello.dump(4) << std::endl;
    assert(hello.count("message") == 1);

    return 0;
}

У нас есть исходные файлы, рецепты их сборки на CMake, теперь перейдем к описанию зависимостей на языке Nix.

flake.nix

Здесь мы опишем два пакета:

hello-json-c = pkgs.stdenv.mkDerivation {
  pname = "hello-json-c";
  version = "0.1.0";
  src = ./src/c;
  nativeBuildInputs = [
    pkgs.cjson
    pkgs.cmake
  ];
};

Называем проект на Си hello-json-c, указываем в качестве зависимостей сборки cjson и cmake. Указываем путь до файлов исходного кода ./src/c.

Пакет для hello-json-cc отличается лишь одной зависимостью:

hello-json-cc = pkgs.stdenv.mkDerivation {
  pname = "hello-json-cc";
  version = "0.1.0";
  src = ./src/cc;
  nativeBuildInputs = [
    pkgs.cmake
    pkgs.nlohmann_json
  ];
};

mkDerivation здесь - это функция, генерирующая скрипт сборки проектов на основе распространенных инструментов сборки, вроде GNU Make, CMake и множества других. Подробнее среда сборки описана в руководстве по nixpkgs. Ради снижения количества бойлерплейта, сборочный скрипт адаптируется под зависимости сборки, и в нашем случае будет использовать CMake вместо GNU Make. Если вы подключите pkgs.ninja, то он также автоматически будет использован.

Нам также понадобится среда для разработки, состоящая из зависимостей всех пакетов:

devShells.${system}.default = pkgs.mkShell {
  name = "hello-json";
  inputsFrom = [
    hello-json-c
    hello-json-cc
  ];
};

Полное содержание итогового файла flake.nix:

{
  description = "Parse JSON in C and C++";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
  };

  outputs =
    { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};

      hello-json-c = pkgs.stdenv.mkDerivation {
        pname = "hello-json-c";
        version = "0.1.0";
        src = ./src/c;
        nativeBuildInputs = [
          pkgs.cjson
          pkgs.cmake
        ];
      };

      hello-json-cc = pkgs.stdenv.mkDerivation {
        pname = "hello-json-cc";
        version = "0.1.0";
        src = ./src/cc;
        nativeBuildInputs = [
          pkgs.cmake
          pkgs.nlohmann_json
        ];
      };
    in
    {
      packages.${system} = { inherit hello-json-c hello-json-cc; };
      devShells.${system}.default = pkgs.mkShell {
        name = "hello-json";
        inputsFrom = [
          hello-json-c
          hello-json-cc
        ];
      };
    };
}

Сделайте git commit, так как Nix игнорирует файлы, которые не управляются Git.

Оболочка разработчика

На этом этапе наше рабочее пространство должно начать работать. Для того чтобы открыть оболочку разработчика, наберите:

nix develop

В ней будет cmake а также CMAKE_INCLUDE_PATH с путями до cjson и nlohmann_json. Поэтому следующие команды до��жны завершиться успешно:

cmake -S . -B build
cmake --build build --target hello-json-c
cmake --build build --target hello-json-cc
❯ ./build/src/c/hello-json-c 
{
        "message":      "Hello, world from C and cJSON!"
}

❯ ./build/src/cc/hello-json-cc
{
    "message": "Hello, world from C++ and nlohmann::json!"
}

Выйти из оболочки можно нажатием Ctrl+D.

direnv

Можно автоматизировать загрузку и выгрузку оболочки разработчика.

Создайте файл .envrc:

use flake

Чтобы разрешить применение среды из репозитория наберите

direnv allow

в директории с репозиторием. Теперь терминал с текущей директории внутри репозитория всегда будет иметь нужные разработчику инструменты.

Visual Studio Code

Достаточно разрешить расширению direnv загрузку профиля:

И перезагрузить окно:

Теперь CMake увидит компилятор:

И цели сборки:

Если открыть файл исходного кода, то видно что заголовки загружены:

Причем при переходе к определению cJSON_Print() открывается файл из /nix/store:

Обратите внимание, что кроме установки расширения direnv и разрешения его работы, мы больше никак не конфигурировали VSCode для работы с проектом. Конфигурация описана в репозитории в Nix файлах и одинакова у всех разработчиков. Все работает сразу после того как вы склонировали репозиторий.

Сборка и запуск через Nix

Мы описали два пакета. Если вы их не разрабатываете, то скорее всего они нужны вам лишь в готовом виде и ручной процесс сборки вас не интересует. В этом случае вам лучше использовать сам Nix для сборки и запуска:

❯ nix run .#hello-json-c
{
        "message":      "Hello, world from C and cJSON!"
}

❯ nix run .#hello-json-cc
{
    "message": "Hello, world from C++ and nlohmann::json!"
}

На самом деле, вам не нужно даже клонировать репозиторий, для того чтобы строить и запускать артефакты из него! Это сработает:

❯ nix run git+https://gitverse.ru/nix-store/hello-json#hello-json-c  
{  
"message": "Hello, world from C and cJSON!"  
}  
   
❯ nix run git+https://gitverse.ru/nix-store/hello-json#hello-json-cc  
{  
"message": "Hello, world from C++ and nlohmann::json!"  
}

Итоги

Мы создали монорепозиторий с двумя проектами: hello-json-c и hello-json-cc. Описали их сборку на CMake, чтобы опыт при использовании Nix не отличался от классического. Зависимости описаны в flake.nix, конкретные версии - в flake.lock. Они одинаково разрешаются и используются для: финальной сборки, в CI, оболочке разработчика и IDE.

Наша IDE полнофункционально работает из коробки, достаточно лишь склонировать репозиторий и разрешить работу direnv.

В идеале Nix вообще незаметен для рядового разработчика. Nix используется лишь для работы с зависимостями, благодаря чему CMake и IDE всегда находят в окружении то, что им нужно.