Я использую 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 всегда находят в окружении то, что им нужно.