通过 Nix 管理 C 和 C++ 依赖项
大家好!我是大聪明-PLUS!
我使用 Nix 已经有一段时间了。它是一款功能强大的工具,可以通过自动化开发环境管理,大大简化开发人员的工作。
今天我想仔细研究一下 Nix 最有用的部分:从 nixpkgs 存储库管理软件包。
我曾经使用过多种语言,并且仍在继续使用。Rust 有一个很棒的包管理器叫 cargo,还有一个安装程序叫 rustup;JavaScript 有 npm。在 Python 的世界里,我也喜欢 conda。
我总是在 C 和 C++ 项目中遇到类似的问题。这些语言的包管理器常常让人很失望。即使它们能正常工作,它们的仓库也可能缺少必要的库。即使一切看起来都运行良好,但最终实现二进制缓存可能也需要付出努力,而当涉及到不同版本的 Qt 时,在开发人员的机器上编译所有内容会非常麻烦。
我希望该仪器无需任何额外调整就能发挥最大功效。
因此,我将向您展示如何使用 Nix 作为 C 和 C++ 的包管理器。
为什么是 Nix?
-
Nix 软件包的 nixpkgs 存储库包含超过100,000 个用于构建各种程序和库的配方。
-
它们大多数是用 C 和 C++ 编写的。这意味着任何流行的库或实用程序可能都已经定义好了,要使用它,你只需要添加依赖项。
-
在 C 和 C++ 世界中,管理依赖关系是一件令人头疼的事情;Nix 几乎完全解决了这个问题,让您可以专注于最重要的事情——编写生产代码。
-
Nix 并不与特定语言绑定:编译器、linters 和各种工具几乎肯定已经在 nixpkgs 中,因此添加新语言只是添加依赖项。
例如,我编写了一个 Bash 脚本,并使用 Nix 对其进行了“编译”,以确保在全新 Ubuntu 安装中可能不存在的实用程序的存在。即使是简单的脚本也存在依赖项:Nix 允许您定义例如 [[your_username] jq] ,并确保此工具所需的版本在您的 PATH 中。
开发人员容器
公司通常使用 Docker 或类似的容器作为其开发环境。这种方法确实有效,但大多数情况下,依赖项管理并不需要容器。语言包管理器可以处理简单的文件。
容器化增加了复杂性。它增加了一层额外的抽象,这可能会带来麻烦:配置、同步以及潜在的本地/CI 差异。
Docker 对于初学者来说可能具有挑战性,因为它需要额外的配置并且需要更多地了解基础设施。
就我个人而言,我不太喜欢使用容器进行开发。我一直都是直接在构建方案中全局获取并安装依赖项。
开发人员体验
-
当实现一个新工具时,您通常不想改变现有的构建系统(GNU Make、CMake 等)。
-
有时您无法控制要构建的包的存储库并且无法修改它。
Nix 尤其适合用作现有构建系统的外部包管理器。功能完善的构建系统清单对于“nix 化”项目来说是一个很大的优势。
IDE 集成
C/C++ 世界中的另一个痛点是 IDE 支持。
-
默认情况下,编辑器不知道项目有哪些依赖项,并且只加载系统标题(例如
/usr/include等)。 -
如果手动构建库,则可能需要手动编码
Include path。
我想说,即使支持控制台构建,Plus 版本也存在问题。JavaScript 或 Rust 项目始终可以使用相同的命令构建,该命令将安装所需的编译器版本和库,运行其他配置脚本,并在需要时设置环境变量。在 Plus 版本中,我们可能有也可能没有带有build.sh自身依赖项的脚本。
/usr/include当您处理需要不同版本库的多个项目时,基于标头的脚本也很容易中断。
今天我们将用 Nix 解决所有这些问题。
目标
-
用 C 和 C++ 创建两个项目:使用用于处理 JSON 的库 -
cJSON用于 C 和nlohmann_json用于 C++。 -
除了 Nix 之外,还可以使用 CMake 作为构建系统。
-
所有依赖项和工作环境均在存储库中描述:只需要 Nix 即可复制。
-
使用可用的 IntelliSense 和 CMake 插件在 VSCode 中开发。
-
易于配置、最大程度自动化和可重复性。
词汇表
-
工作区是存储库的根,即您在 IDE(例如 VSCode)中打开的内容。
-
项目/包是存储库的一部分,可以构建以生成有用的、独立的工件(例如,可执行文件)。
必要的
-
启用薄片的 Nix 。
-
我建议使用这个安装程序:
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 结合使用。
现在您有了 Nix,您可以从 nixpkgs 安装程序:
nix profile install nixpkgs
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
Nix 还具有支持语言服务器协议的 VSCode 扩展。为了简单起见,我们不会在本文中介绍它们。
存储库结构
.
├── 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;
}
这里的一切都很简单:我们从具有字段message和文本的对象创建 JSON "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.cjsonpkgs.cmake];
};
我们将项目命名为 C 项目,指定和hello-json-c作为构建依赖项。我们指定源代码文件的路径。cjsoncmake./src/c
该软件包hello-json-cc仅在一个依赖项上有所不同:
hello-json-cc = pkgs.stdenv.mkDerivation {pname = "hello-json-cc";version = "0.1.0";src = ./src/cc;nativeBuildInputs = [pkgs.cmakepkgs.nlohmann_json];
};
此处的 mkDerivation 函数基于 GNU Make、CMake 等常见构建工具生成项目构建脚本。nixpkgs手册中更详细地描述了构建环境。为了减少样板代码,构建脚本会根据构建依赖项进行调整,在本例中,将使用 CMake 而不是 GNU Make。如果您包含pkgs.ninja,它也将自动使用。
我们还需要一个包含所有包依赖项的开发环境:
devShells.${system}.default = pkgs.mkShell {name = "hello-json";inputsFrom = [hello-json-chello-json-cc];
};
最终文件的全部内容flake.nix:
{description = "Parse JSON in C and C++";inputs = {nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";};outputs ={ self, nixpkgs }:letsystem = "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.cjsonpkgs.cmake];};hello-json-cc = pkgs.stdenv.mkDerivation {pname = "hello-json-cc";version = "0.1.0";src = ./src/cc;nativeBuildInputs = [pkgs.cmakepkgs.nlohmann_json];};in{packages.${system} = { inherit hello-json-c hello-json-cc; };devShells.${system}.default = pkgs.mkShell {name = "hello-json";inputsFrom = [hello-json-chello-json-cc];};};
}
这样做git commit是因为 Nix 会忽略未由 Git 管理的文件。
开发者 Shell
此时,我们的工作区应该已经启动并运行了。要打开开发者 shell,请输入:
nix develop
它还将cmake包含和 的CMAKE_INCLUDE_PATH路径。因此,以下命令应该可以成功完成:cjsonnlohmann_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!"
}
您可以按 退出 shell Ctrl+D。
direnv
您可以自动加载和卸载开发人员外壳。
创建文件.envrc:
use flake
要启用存储库中的环境,请键入
direnv allow
在存储库目录中。现在,存储库中当前目录中的终端将始终拥有开发人员所需的工具。
Visual Studio 代码
允许 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!"
}
结果
我们创建了一个包含两个项目的 monorepo:hello-json-c和hello-json-cc。我们使用 CMake 描述了它们的构建,以确保 Nix 体验与传统 Nix 一致。依赖项在中描述flake.nix,具体版本在中描述flake.lock。它们以相同的方式解析,并用于最终构建、CI、开发人员 shell 和 IDE。
我们的 IDE 开箱即用,功能齐全;您只需要克隆存储库并启用 direnv。
理想情况下,Nix 对于普通开发者来说是完全不可见的。Nix 仅用于管理依赖项,确保 CMake 和 IDE 始终能够在环境中找到所需的内容。
