Nix로 만드는 개발 셸: 네 가지 빠른 예제
원문은 Michael Stapelberg님이 에 게재했습니다. 이 블로그 구독하기
프로젝트 중 하나에서 (큰 스캔 이미지 안에서 종이 문서를 찾아 추출하기 위해) GoCV를 사용하고 싶었는데, 시스템에 OpenCV를 영구적으로 설치하고 싶지는 않았다.
이건 내가 자주 쓰는 몇 가지 Nix 명령어를 소개하기에 좋은 사례라고 생각했다. 빠르고 즉흥적인 일회성 개발 셸부터 완전히 선언적이고 격리된(hermetic), 재현 가능하고 공유 가능한 개발 셸까지 다뤄볼 수 있기 때문이다.
중요한 점은, 이 명령어들을 실행하기 위해 NixOS를 쓸 필요가 없다는 것이다! Debian, Arch 등 어떤 리눅스 시스템에서든 Nix를 설치해 사용할 수 있다. Nix path를 설정하거나 Flakes를 사용하기만 하면 된다(준비 참고).
비교를 위해: Debian 방식
Nix를 본격적으로 살펴보기 전에, Debian에서 GoCV를 동작시키는 방법을 먼저 보여주겠다.
gocv.NewMat() 같은 GoCV 함수를 사용하는 최소한의 Go 프로그램을 하나 만들어, 이 프로그램이 컴파일되는지 확인해 보자:
package main
import "gocv.io/x/gocv"
func main() {
gocv.NewMat()
}Debian 시스템에서 빌드를 시도하면 다음과 같은 결과가 나온다:
debian % mkdir -p /tmp/minimal
debian % cd /tmp/minimal
debian % cat > minimal.go <<'EOT'
package main
import "gocv.io/x/gocv"
func main() { gocv.NewMat(); }
EOT
debian % go mod init minimal
go: creating new go.mod: module minimal
go: to add module requirements and sums:
go mod tidy
debian % go mod tidy
go: finding module for package gocv.io/x/gocv
go: downloading gocv.io/x/gocv v0.41.0
go: found gocv.io/x/gocv in gocv.io/x/gocv v0.41.0
debian % go build
# gocv.io/x/gocv
# [pkg-config --cflags -- opencv4]
Package opencv4 was not found in the pkg-config search path.
Perhaps you should add the directory containing `opencv4.pc'
to the PKG_CONFIG_PATH environment variable
Package 'opencv4', required by 'virtual:world', not foundDebian에서는 다음과 같이 OpenCV를 설치할 수 있다:
debian % sudo apt install libopencv-dev
[…]
Summary:
Upgrading: 7, Installing: 512, Removing: 0, Not Upgrading: 27
Download size: 367 MB
Space needed: 1590 MB / 281 GB available
Continue? [Y/n]이 프롬프트에서 “yes”라고 하면 500개가 넘는 패키지가 다운로드되어 설치된다(몇 분 정도 걸린다).
이제 빌드는 잘 된다:
debian % go build
debian % file minimal
minimal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), […]…하지만 이제 시스템에 500개가 넘는 추가 패키지가 생겼고, 앞으로 영원히 업데이트해야 한다. 그래서 나는 이런 일회성 실험을 평소 쓰는 시스템과는 분리하고 싶다.
Docker를 써서 Debian 컨테이너를 띄우고 그 안에서 작업할 수도 있지만, 작업에 따라서는 별도의 환경이라는 점 때문에 오히려 번거로울 수 있다. 이 예제의 경우, 입력 파일을 Docker 컨테이너에서 쓸 수 있도록 볼륨 마운트를 지정해야 하고, 컨테이너 안의 프로그램이 호스트에서 그래픽 창을 열 수 있도록 환경 변수도 설정해야 한다…
Nix가 이런 문제를 어떻게 해결해 주는지 살펴보자!
준비: Nix-on-Debian (또는 Nix-on-Arch 등)
NixOS 사용자는 이 섹션을 건너뛰어도 된다. NixOS에는 바로 사용할 수 있는 Nix가 포함되어 있기 때문이다.
본인 컴퓨터에서 예제를 직접 시도하기 전에 다음 세 단계를 완료해야 한다:
- Nix 설치
- Flakes 활성화
- Nix path 설정
1단계: Nix 설치
Debian, Arch, Fedora 또는 다른 리눅스 시스템을 쓰는 사용자라면 먼저 Nix를 설치해야 한다. 다행히 Nix는 대부분의 주요 리눅스 배포판에서 사용할 수 있다:
- Debian은 nix-setup-systemd를 제공한다
- Arch Linux는 nix를 패키징하고 Nix Arch Wiki 페이지에 문서를 제공한다. 실제로 나는 패키지를 설치하고 몇 개의
nixbld사용자를 설정했다. - 더 일반적으로는 여러 배포판을 위한 Nix 빌드(rpm, deb, pacman)가 제공된다: https://github.com/nix-community/nix-installers
2단계: Flakes 활성화
Nix flakes는 “Nix 결과물을 패키징하는 일반적인 방법”이다.
예제 3과 4에서는 의존성을 고정하기 위해 Nix flakes를 사용하므로, Nix flakes를 활성화해야 한다.
3단계: Nix path 설정
예제 1과 2에서는 Nix 표현식 import <nixpkgs>를 사용하려고 한다.
NixOS에서는 이 표현식이 시스템 버전을 따라간다. 즉, NixOS 25.05 설치 환경에서 import <nixpkgs>를 사용하면 nixos-25.05 버전의 nixpkgs를 가리키게 된다.
다른 리눅스 시스템에서는 다음과 같은 오류 메시지가 나타난다:
debian-server % nix-shell -p pkg-config opencv
error: file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I)
at «string»:1:25:
1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (pkg-config) (opencv) ]; } ""
| ^
(use '--show-trace' to show detailed location information)Nix search path를 설정해 Nix에게 어떤 버전의 nixpkgs를 사용할지 알려줘야 한다:
debian-server % export NIX_PATH=nixpkgs=channel:nixos-25.05
debian-server % nix-shell -p pkg-config opencv
[nix-shell:/tmp/opencv]#좋다! 이제 준비가 끝났다. 첫 번째 예제로 바로 들어가 보자!
예제 1: 일회성 인터랙티브 셸: nix-shell
Nix는 시스템에 OpenCV를 설치하는 것(위 예제처럼 apt install)과 별도의 Docker 컨테이너에 OpenCV를 설치하는 것 사이의 중간 지점을 제공한다. Nix를 이용하면 OpenCV를 영구적으로 설치하지 않고도 사용할 수 있게 만들 수 있다.
nix-shell(1)을 실행하면 지정한 패키지들이 사용 가능한 bash 셸을 시작할 수 있다. GoCV를 사용하는 Go 코드를 성공적으로 빌드하려면 OpenCV가 필요하므로 다음과 같이 실행한다:
% nix-shell -p pkg-config opencv
these 194 paths will be fetched (175.80 MiB download, 764.10 MiB unpacked):
/nix/store/ig2nk0hsha9xaailhaj69yv677nv95q4-abseil-cpp-20210324.2
/nix/store/yw5xqn8lqinrifm9ij80nrmf0i6fdcbx-alsa-lib-1.2.13
[…]
[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4혹시 궁금할까 봐 덧붙이자면, 네, 이 nix-shell 명령어에서는 pkg-config를 명시적으로 지정해야 한다. 그렇지 않으면 pkg-config를 실행할 때 호스트 버전(개발 셸 외부)이 실행되어 opencv4.pc를 찾지 못한다.
예제 2: nix-shell 설정 파일: shell.nix
프로젝트에 맞는 패키지 조합을 찾았다면(이 예제에서는 pkg-config와 opencv뿐이다), shell.nix를 만들 수 있다(어느 디렉터리에든 만들 수 있지만 보통은 프로젝트 루트에 만든다). nix-shell을 -p 플래그 없이 실행하면 이 파일을 읽는다:
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
packages = with pkgs; [
# Explicitly list pkg-config so that mkShell will arrange
# for the PKG_CONFIG_PATH to find the .pc files.
pkg-config
opencv
];
}…그리고 나서 그냥 nix-shell만 실행하면 된다:
% nix-shell
[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4패키지 목록 주변의 보일러플레이트가 궁금하다면, 참고할 만한 문서 몇 가지를 소개한다:
- 1~3번째 줄은 argument set을 받는 함수를 선언한다 — 이는
nix-shell이shell.nix파일을 호출할 수 있도록 하는 데 필요한 구조다. pkgs.mkShell은nix-shell과 함께 사용하기 위한 편의 헬퍼다.with pkgs;부분 덕분에pkgs.opencv대신opencv라고 쓸 수 있다.
참고로, nixd 언어 서버를 사용하면 LSP 지원을 갖춘 에디터에서 패키지가 실제로 어떤 버전으로 해석되는지 보여주거나, 오타를 짚어주거나, “정의로 이동” 같은 기능을 제공할 수 있다.
예를 들어 이 스크린샷에서는 Emacs에서 shell.nix를 편집하다가 opencv 패키지의 Nix 소스가 어떻게 생겼는지 궁금해졌다. opencv 위에 “point”를 두고 M-.(xref-find-definitions)를 누르자, 로컬 Nix 저장소에 있는 opencv/4.x.nix로 이동했다:

예제 3: 격리되고 고정된 devShell: Nix Flakes
앞선 예제들은 시스템(또는 Nix path)의 nixpkgs를 사용했다. 따라서 시스템을 업그레이드해도 .nix 파일을 바꿀 필요가 없다 — 사용 사례에 따라 이 동작은 편리하게 느껴질 수도, 끔찍하게 느껴질 수도 있다.
주변 OS 버전에 관계없이 .nix 파일이 항상 정확히 같은 방식으로 빌드되는 것이 중요한 경우에는 Nix Flakes를 사용해 격리된(hermetic) 방식으로 빌드할 수 있으며, 의존성 버전은 flake.lock 파일에 고정된다.
flake.nix는 위와 동일한 mkShell 표현식을 담고 있지만, 그 주변에 구조를 선언한다. mkShell 표현식은 outputs.devShells.x86_64-linux.default 속성에 들어가고, inputs 속성은 이 빌드에서 사용할 수 있는 Flake 레퍼런스를 담는다:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
outputs =
{ self, nixpkgs }:
{
devShells.x86_64-linux.default =
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
pkgs.mkShell {
packages = with pkgs; [
# Explicitly list pkg-config so that mkShell will arrange
# for the PKG_CONFIG_PATH to find the .pc files.
pkg-config
opencv
];
};
};
}참고로, 이름과 달리 nixpkgs.legacyPackages를 사용하는 것이 모범 사례다. 이는 개념적으로 단일 import nixpkgs 결과를 제공한다(효율성을 위해).
이제 nix develop을 사용해 OpenCV가 포함된 셸을 얻을 수 있다:
% nix develop
michael@midna$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4첫 번째 nix develop 실행 시 flake.lock 파일이 생성되므로, 이후에 nix develop을 실행하면 정확히 동일한 환경을 얻을 수 있다. 더 새로운 버전으로 업데이트하려면 nix flake update를 사용한다.
팁: 셸 대신 nix develop --command=emacs 같은 변형도 유용하다.
예제 4: Flake를 시스템 독립적으로 만들기
안타깝게도 위 flake.nix는 x86_64-linux를 하드코딩하고 있어, 예를 들어 aarch64-linux(ARM) 컴퓨터나 x86_64-darwin(Mac)에서는 사용할 수 없다.
기본적으로 system을 명시적으로 지정해야 한다는 점은 Nix Flakes에 대한 오랜 비판 중 하나다.
몇 가지 우회 방법이 있다. 예를 들어 numtide/flake-utils를 사용해 flake.nix를 리팩터링하고, eachDefaultSystem 편의 함수를 활용할 수 있다:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
formatter = pkgs.nixfmt-tree;
devShells.default = pkgs.mkShell {
packages = with pkgs; [
# Explicitly list pkg-config so that mkShell will arrange
# for the PKG_CONFIG_PATH to find the .pc files.
pkg-config
opencv
];
};
}
);
}혹은 그 정신적 후계자인 numtide/blueprint를 사용할 수도 있다.
LucPerkins의 dev-templates는 이 기법의 한 버전을 사실상 인라인으로 포함하고 있다.
Nix 자체는 아니지만 Nix와 인접한 해결책으로는 devenv가 있다. devenv는 Nix 위에 구축된 별도 도구로(더 이상 CppNix 구현을 사용하지 않고 사실은 tvix를 사용한다), 자체 .nix 파일을 사용한다.
팁: 패키지 유지하기
flake.lock이 변경되지 않았는데도 nix develop 등의 명령어가 패키지를 계속 가져온다면, Flake를 프로필에 설치해 Nix에 gcroot로 선언할 수 있다:
% nix profile install .#devShells.x86_64-linux.default잠깐, 그러면 Debian 방식과 같은 상태가 되는 것 아닌가? 아니다! Flake를 프로필에 설치하면 OpenCV가 무기한 남아 있기는 하지만, 여전히 분리 계층이 존재한다. 시스템 전체에서는 OpenCV를 사용할 수 없고, nix-shell이나 nix develop으로 개발 셸을 시작했을 때만 사용할 수 있다.
결론
위 네 가지 예제는 어떻게 비교될까? 개요를 정리하면 다음과 같다:
| 예제 | 보일러플레이트 | 고정됨? | 시스템 의존적? |
|---|---|---|---|
예제 1: nix-shell -p … | 😊 | 아니요 | 아니요 |
예제 2: shell.nix | 🙂 | 아니요 | 아니요 |
예제 3: flake.nix | 😲 | 예 | 예 |
예제 4: 시스템 독립적인 flake.nix | 🤨 | 예 | 아니요 |
개인적인 일회성 실험에는 nix-shell을 사용한다.
실험이 제대로 동작하면 보통 의존성을 고정하고 싶어지므로 flake.nix를 사용한다.
단순히 버전 관리를 넘어 배포하거나(혹은 여러 사람/시스템과 함께 작업하는) 소프트웨어라면, 시스템 독립적인 flake.nix로 만들기 위해 추가 노력을 들인다.
앞으로는 시스템 독립적인 flake를 더 쉽게 작성할 수 있게 되기를 바란다.
거친 부분이 남아 있긴 하지만, Nix가 제공하는 재현 가능성과 제어 능력은 정말 마음에 든다!
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기