Development shells with Nix: four quick examples

Michael Stapelberg

Nix 개발 셸: 네 가지 빠른 예제

제 프로젝트 중 하나에서 더 큰 스캔 이미지 안에서 종이 문서를 찾아 추출하기 위해 GoCV를 사용하고 싶었습니다. 시스템에 OpenCV를 영구적으로 설치하지 않고 말이죠.

간단한 일회성 대화형 개발 셸부터 완전히 선언적이고 밀폐적이며 재현 가능하고 공유 가능한 개발 셸까지, 제가 즐겨 쓰는 Nix 명령어 몇 가지를 소개하기에 좋은 사례 같았습니다.

참고로 이 명령어들을 실행하는 데 NixOS가 꼭 필요한 것은 아닙니다! Nix 경로를 설정하거나 Flakes를 사용하면 Debian, Arch 등 어떤 리눅스 시스템에서도 Nix를 설치해 사용할 수 있습니다(설정 참고).

비교를 위해: 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 found

Debian에서는 다음과 같이 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가 이 문제를 어떻게 도와줄 수 있는지 살펴보겠습니다!

준비: Debian에서 Nix 사용하기 (혹은 Arch에서 Nix 사용하기 등)

NixOS 사용자는 이 섹션을 건너뛰셔도 됩니다. NixOS에는 바로 사용할 수 있는 Nix가 포함되어 있기 때문입니다.

직접 예제를 실행해 보기 전에 다음 세 단계를 완료해야 합니다:

  1. Nix 설치
  2. Flakes 활성화
  3. Nix 경로 설정

1단계: Nix 설치

Debian, Arch, Fedora 등 다른 리눅스 시스템을 사용하는 경우 먼저 Nix를 설치해야 합니다. 다행히 Nix는 많은 주요 리눅스 배포판에서 사용할 수 있습니다:

2단계: Flakes 활성화

Nix flakes는 “Nix 결과물을 패키징하는 일반적인 방법”입니다.

예제 3과 4에서는 의존성을 고정하기 위해 Nix flakes를 사용하므로 Nix flakes를 활성화해야 합니다.

3단계: Nix 경로 설정

예제 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 검색 경로를 설정해 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-configopencv만 있으면 됩니다), 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행은 인자 집합을 받는 함수를 선언합니다 — 이는 nix-shellshell.nix 파일을 호출할 수 있도록 하는 데 필요한 구조입니다.
  • pkgs.mkShellnix-shell과 함께 사용하기 위한 편의 도우미입니다.
  • with pkgs; 부분 덕분에 pkgs.opencv 대신 opencv라고 쓸 수 있습니다.

참고로 nixd 언어 서버를 사용하면 LSP를 지원하는 편집기에서 패키지가 해석되는 버전을 보여주거나, 오타를 알려주거나, “정의로 이동” 같은 기능을 제공할 수 있습니다.

예를 들어 이 스크린샷에서는 Emacs에서 shell.nix를 편집하면서 opencv 패키지의 Nix 소스가 어떻게 생겼는지 궁금했습니다. opencv 위에 “point”를 두고 M-.(xref-find-definitions)를 누르자 로컬 Nix 저장소에 있는 opencv/4.x.nix로 이동했습니다:

opencv 정의로 이동한 뒤 opencv/4.x.nix를 보여주는 Emacs

예제 3: 밀폐적이고 고정된 devShell: Nix Flakes

앞선 예제들은 시스템(또는 Nix 경로)의 nixpkgs를 사용했습니다. 즉, 시스템을 업그레이드해도 .nix 파일을 바꿀 필요가 없다는 뜻인데, 사용 사례에 따라 이 동작은 편리하게 느껴질 수도 있고 끔찍하게 느껴질 수도 있습니다.

주변 OS 버전에 관계없이 .nix 파일이 항상 정확히 같은 방식으로 빌드되는 것이 중요한 경우에는 Nix Flakes를 사용해 밀폐적인 방식으로 빌드할 수 있으며, 이때 의존성 버전은 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.nixx86_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가 제공하는 재현성과 제어력을 높이 평가합니다!

원문은 Michael Stapelberg님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.