‘순수 함수형’ 패키지 관리자 Nix 맛보기
Nix는 Linux와 macOS를 지원하는 패키지 관리 시스템입니다.
해커 뉴스 등에서 Nix에 대해 종종 접하게 되어서 궁금증이 생겼고, 조금 사용해보면서 파악한 내용을 정리합니다. 계속 사용할지는 아직 모르겠지만 이것저것 찾아보느라 들인 시간이 아까우니까요. 누군가에겐 도움이 되겠죠?
Nix에는 여러 특징이 있지만, 그 중에서도 같은 패키지의 여러 버전을 동시에 설치할 수 있어서 패키지를 한번 설치하면 시스템에 변화가 있더라도 계속 작동이 보장된다는 점이 유용해 보입니다. 이런 특징을 활용하면 프로젝트마다 독립된 개발 환경을 구축하는 데에 쓸 수 있습니다.
의존성 지옥!
APT나 Homebrew 등 일반적인 패키지 관리 시스템에서는 시스템 전역에 특정 패키지 이름으로는 딱 한가지 버전만 설치할 수 있습니다. 이를 우회하기 위해 패키지 이름에 버전을 명시하기도 합니다. (예를 들면 python3.9
와 python3.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
더 알아보기
- Nix 가이드 문서를 따라해봅니다.
- Nix expression language를 공부해봅니다. 구글링할 때 나오는 Nix 코드 스타일이 제각각이라 언어를 대충이라도 알아야 갖다 쓰기가 편합니다. Nix의 가장 큰 진입장벽인 것 같습니다.
- pip 등 언어 패키지 관리자의 기능을 Nix가 어느 정도 대체할 수 있습니다.
- Nix 패키지를 만들어보기! 아직 못 해봤어요.
- Nix: A Safe and Policy-Free System for Software Deployment (PDF) 논문을 읽어봅니다. 🤔 실용적인 부분을 제쳐두더라도 생각보다 역사가 오래되고 흥미로운 소프트웨어인 것을 알 수 있습니다.