👷 The Sapzil

‘순수 함수형’ 패키지 관리자 Nix 맛보기

Nix는 Linux와 macOS를 지원하는 패키지 관리 시스템입니다.

해커 뉴스 등에서 Nix에 대해 종종 접하게 되어서 궁금증이 생겼고, 조금 사용해보면서 파악한 내용을 정리합니다. 계속 사용할지는 아직 모르겠지만 이것저것 찾아보느라 들인 시간이 아까우니까요. 누군가에겐 도움이 되겠죠?

Nix에는 여러 특징이 있지만, 그 중에서도 같은 패키지의 여러 버전을 동시에 설치할 수 있어서 패키지를 한번 설치하면 시스템에 변화가 있더라도 계속 작동이 보장된다는 점이 유용해 보입니다. 이런 특징을 활용하면 프로젝트마다 독립된 개발 환경을 구축하는 데에 쓸 수 있습니다.

의존성 지옥!

APT나 Homebrew 등 일반적인 패키지 관리 시스템에서는 시스템 전역에 특정 패키지 이름으로는 딱 한가지 버전만 설치할 수 있습니다. 이를 우회하기 위해 패키지 이름에 버전을 명시하기도 합니다. (예를 들면 python3.9python3.10을 별개의 패키지로 배포하는 등)

여러 패키지가 하나의 공통 패키지에 의존하는 경우 특히 문제가 있습니다. 예를 들어 OpenSSL 같은 라이브러리를 업그레이드하면 OpenSSL에 직간접적으로 의존하는 모든 패키지가 영향을 받습니다.

패키지마다 호환되는 의존성의 버전을 느슨하게 정의해두고 있기는 하지만, 운이 나쁘면 이전과 똑같이 작동하지 않을 수 있습니다. 그리고 업그레이드하려는 버전이 어떤 패키지에서 요구하는 버전과 충돌하는 경우 아예 업그레이드를 못할 수도 있습니다.

Nix에서는 의존성을 yc41q33h5xrw1zbyw5hp1y1ga0jk9hwd-openssl-1.1.1k과 같이 정확한 버전과 특정 빌드로 정의합니다. 따라서 패키지마다 각자 독립적으로 의존성의 버전을 선택할 수 있습니다. 패키지끼리 서로 영향을 주지 않기 때문에 안전하게 패키지를 설치하거나 업그레이드할 수 있습니다. 또한 의존성이 고정되므로 패키지가 저자의 의도대로 작동할 가능성이 높아집니다.

Nix 맛보기

nix-shell을 사용하면 특정 Nix 패키지가 설치된 환경을 여러개 만들 수 있습니다. 예제로 Python과 Node가 설치된 환경을 만들어보겠습니다.

먼저 Nix를 설치합니다.

프로젝트 디렉토리에 shell.nix 파일을 만들고 다음 내용을 추가합니다.

let
  pkgs = import <nixpkgs> {};
in pkgs.mkShell {
  nativeBuildInputs = [
    pkgs.python3
    pkgs.nodejs
  ];
}

그 다음 해당 디렉토리에서 nix-shell을 실행하면 필요한 패키지를 다운로드 받고 새로운 쉘이 켜집니다. 이 쉘 환경에서는 python, node/nix/store 하위에 설치된 특정 바이너리를 가리키고 있는 것을 확인할 수 있습니다.

~/myproject$ nix-shell
these paths will be fetched (...):
  (생략)

[nix-shell:~/myproject]$ which python
/nix/store/d44wd6n98f93hjr6q1d1phhh1hw7a17d-python3-3.8.8/bin/python

[nix-shell:~/myproject]$ which node
/nix/store/y9ay04l5mfm255r296vhcjbxjqkjxp39-nodejs-14.16.1/bin/node

패키지를 추가하자

shell.nix가 무언가 생소한 언어로 작성되어서 혼란스러울 것입니다. 사실 이것은 Nix expression language 코드이지만 일단은 설명하지 않겠습니다.

중요한 부분은 nativeBuildInput입니다. python3, node 외에 다른 패키지는 Nixpkgs에서 검색해서 찾으면 됩니다. Channel을 unstable로 설정해서 찾아야 합니다. 그리고 macOS를 지원하지 않는 패키지가 종종 있으므로 Platforms에 x86_64-darwin가 있는지 확인합시다.

패키지를 찾았다면 pkgs.패키지명을 추가하고, nix-shell에서 나갔다가 (Ctrl-D 입력) 다시 nix-shell을 실행하면 해당 패키지가 추가된 환경으로 들어갈 수 있습니다.

Nixpkgs를 고정하자

2번째 줄 pkgs = import <nixpkgs> {};에서 <nixpkgs>는 시스템 전역에 설정된 채널을 따라가기 때문에 계속 변할 수 있는 값입니다. 정말 개발 환경이 항상 같으려면 Nixpkgs를 특정 버전으로 고정해야합니다.

이를 위해 niv를 사용하겠습니다. 프로젝트 디렉토리에서 다음 명령을 실행합니다.

nix-shell -p niv --run "niv init -b nixpkgs-unstable"

그러면 nix/sources.json, nix/sources.nix가 생성됩니다. 이제 shell.nix를 수정해서 고정된 Nixpkgs를 사용하도록 설정합니다.

let
  sources = import ./nix/sources.nix;
  pkgs = import sources.nixpkgs {};
in pkgs.mkShell {
  ...
}

direnv를 연동하자

매번 적절한 nix-shell을 켜는 것은 불편하므로 direnv를 활용하면 좋습니다.

direnv도 Nix를 활용해서 설치해보겠습니다. (이미 direnv가 설치되어 있다면 넘어가시면 됩니다.)

nix-env -iA nixpkgs.direnv

그리고 문서를 참고해서 direnv hook을 추가한 뒤 쉘을 새로 띄웁니다.

프로젝트 디렉토리 하위에 .envrc를 만들고 use nix를 적어줍니다. 최초 한번 direnv allow를 실행해주어야 합니다.

~/myproject$ echo "use nix" > .envrc
direnv: error /home/ditto/myproject/.envrc is blocked. Run `direnv allow` to approve its content

~/myproject$ direnv allow
direnv: loading ~/myproject/.envrc
direnv: using nix
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +DETERMINISTIC_BUILD +HOST_PATH +IN_NIX_SHELL (...생략)

# 와! nix-shell 안에 있어요
~/myproject$ which node
/nix/store/y9ay04l5mfm255r296vhcjbxjqkjxp39-nodejs-14.16.1/bin/node

# 다른 디렉토리로 빠져나오면 이전 상태로 돌아옵니다
~/myproject$ cd
direnv: unloading
~$ which node
/usr/bin/node

더 알아보기

태그로 찾기: announcement architecture backend build-tools code-review database dependency-injection distributed docker github gradle graphql java javascript jersey jpa jquery kotlin linux maven microservice nix package-manager python react reactive redux relay rxjs spring ssr sysadmin testing tools translation ubuntu upstart web windows