Я использую 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.
Цели
Создать два проекта на Си и C++: с библиотеками для работы с JSON —
cJSONдля Си иnlohmann_jsonдля C++.Использовать CMake как систему сборки, в дополнение к Nix.
Все зависимости и рабочее окружение описаны в репозитории: для воспроизведения нужен только Nix.
Разработка в VSCode с работающим IntelliSense и плагином CMake.
Простота конфигурации, максимальная автоматизация и воспроизводимость.
Глоссарий
Рабочее пространство (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
Чтобы vscode использовал direnv необходимо одноименное расширение direnv.
Для 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.
#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)
#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 всегда находят в окружении то, что им нужно.
