뒤로 미뤄 두던 Nix를 결국 만져야 되는 상황이 왔습니다. 국내에도 쓰는 분들이 점점 늘어나는 추세인듯 하나, 한글 자료는 많지 않습니다. 아래는 전혀 완성되지 않은 글입니다. 아직 필요한 부분들을 확인하고 있는 중입니다. 아래는 순수하게 개인이 볼 노트인 상태로, 아직 다른 분들을 위해 정리하지 않았습니다. 주의해서 보세요.
닉스는 패키지 빌드에 필요한 모든 정보를 모아둔 derivation(일종의 명세서 같은 것)을 기반으로, 빌드, 개발 환경에 필요한 의존성들을 같이 관리하는 패키지 매니저입니다. derivation이 핵심 아이디어인데, 은근 오해하게 만드는 요소가 있어, 천천히 살펴 보도록 하겠습니다.
Makefile
hello: hello.c
gcc -o hello hello.c
hello
: 빌드 목표targethello.c
: 의존성gcc -o hello hello.c
: 빌드 방법파일 수정시간을 기반으로 의존성을 관리하고, 시스템에 설치된 컴파일러와 라이브러리를 사용합니다.
default.nix
{ pkgs ? import <nixpkgs> {} }:
{
pkgs.stdenv.mkDerivation name = "hello";
src = ./hello.c;
buildPhase = ''
${pkgs.gcc}/bin/gcc -o hello hello.c
'';
installPhase =''
mkdir -p $out/bin
cp hello $out/bin
'';
}
gcc는 시스템 전역에 설치된 것이 아니라, nixpkgs에서 특정 버전을 가져오고, 빌드 결과를 닉스 저장소에 저장합니다.
둘 다 어떤 작업을 해서 빌드할 것인지를 써 놓은 것은 똑같은데, 닉스는 선언형이라고 말합니다. gcc -o hello hello.c
를 직접 지정한 것과 buildPhase
에 넣어서 프레임워크에서 돌아가게 한 것이 무슨 차이일까요? 단순히 함수나 변수에 바인딩했다 하여 선언형이라 하는 것이 아닙니다. 선언형은 이들을 조합(혹은 조립)할 수 있어야 합니다. default.nix
자체가 함수고, 내부의 mkDerivation
도 함수입니다. Makefile
처럼 실행해야 할 것들을 순서대로 써 놓은 것이 아닙니다. 이들 함수는, 다른 함수들과 합성하며, 최종 빌드 작업을 표현하게 됩니다.
구체적으로 morning
이란 패키지가 있고, 이 패키지는 hello
에 의존한다고 하면,
{ pkgs ? import <nixpkgs> {} }:
let
hello = import ../hello { inherit pkgs; }; # (가)
in
{
pkgs.stdenv.mkDerivation name = "morning";
src = ./morning.c;
buildInputs = [ hello ]; # hello 패키지를 의존성으로 추가합니다.
# 위와 아래 hello모두 실제 패키지가 아니라 derivation을 가리키고
# 닉스 저장소에 설치된 hello .drv 파일 풀경로로 인식된다고 보면 됩니다.
# buildInputs은 닉스 저장소에 있는 패키지의 .drv만 알려 주는 것이고,
# 이게 어떤 식으로 의존하는지는 별도로 써줘야 합니다.
# 아래 `-L${hello}/bin` 처럼 말입니다.
buildPhase = ''
${pkgs.gcc}/bin/gcc -o morning morning.c -L${hello}/bin -lhello
'';
installPhase = ''
mkdir -p $out/bin
cp morning $out/bin
'';
}
(가)
는 hello
디렉토리의 default.nix
를 불러옵니다. 즉 hello
의 derivation 생성식을 불러 옵니다. (morning
이 현재 쓰고 있는 nixpkgs를 인자로 전달하고 있습니다. default.nix
가 하나의 함수이니, 인자로 전달하는 것으로 관련 체인에 있는 모든 함수들이(derivation들이) 같은 정보를 가질 수 있습니다. 이 것도 함수형 표현의 장점 중 하나겠습니다.)
Q. 함수의 합성으로 “선언들의 조합”이 표현된다 했는데, 위에는 함수 합성이 안보이지 않나?
A.let ~ in
구문은 함수 합성의 슈가 문법으로 볼 수 있습니다. (상상, 검증 필요) 하스켈의 경우를 보면let
구문을 컴파일한 중간 언어 Core 결과물은 람다 함수 합성으로 표현이 바뀌어 있습니다. 닉스는 strict 언어고 단순 바인딩일 뿐 실제로 변환이 일어나는 건 아닙니다.
Makefile
은 hello
를 먼저 빌드하고(닉스식으로 말하면 realize하고), 그 다음 morning
을 빌드합니다. Nix는 hello
를 빌드하기 위한 함수와 morning
을 빌드하기 위한 함수를 먼저 합성한 후 realize하게 됩니다.
default.nix
도, mkDerivation
도 이펙트가 없는 순수 함수입니다. (사실, 사용자에게 드러나는 부분은 순수한 모양이지만, 내부 빌드 과정에서 외부와 상호 작용하는 부분이 있다고 합니다.) 예를 들어, 기존에 있던 프로젝트가 빌드할 때 환경 변수를 통해 정보를 받는 걸, 닉스 빌드로 변환할 때, 필요한 정보를 명시부터 하게 해서 순수한 인터페이스를 유지하고, 이를 기존 패키지를 위해 임시 환경 변수로 잡는 방법을 써서라도 순수한 모양으로 바꿉니다. ※ mkDerivation
을 실행하면, .drv
파일을 만들고, 닉스 저장소에 저장합니다. 이 후 nix build
나, nix-env
가 이 derivation을 명세서 삼아 실제 빌드해서 파일을 만들어 냅니다.
위 특징을 더 잘표현 하려면, 선언형이란 말보다 유연한 조합형이란 말이 더 적합할 수도 있겠습니다. 어디까지나 개인적인 생각입니다.
위 얘기는 로컬 패키지 빌더로서의 닉스 얘기고 (엄밀히 말하면 빌드를 위한 추상 레이어쯤 되겠습니다. 예를 들어 C프로젝트는 여전히 gcc
, make
를 써서 빌드합니다.), 이제 패키지 매니저로서의 닉스로 보면 아래 동작이 가장 기본 동작입니다.
let pkgs = import <nixpkgs> {};
in pkgs.hello
> nix-build
위 표현식은 hello
패키지를 빌드하는데 필요한 정보를 가진 derivation을 생성합니다. (pkgs.hello
는 derivation 자체를 반환하는 것은 아니고, /nix/store
에 hello
패키지 빌드에 필요한 정보를 가진 derivation을 담은 hello.drv
파일을 만들고, 해당 파일의 풀 경로를 반환합니다.)
nix derivation show $(nix-instantiate -E 'with import <nixpkgs> {}; hello')
derivation
을 보기 좋게 출력해 줍니다.
nixpkgs
에서 hello
패키지를 가져와 빌드합니다.
이제 로컬 빌더와 패키지 매니저를 섞어 보겠습니다. nixpkgs
의 hello
패키지를 일부만 수정해서 빌드 해보겠습니다.
let
pkgs = import <nixpkgs> {};
myHello = pkgs.hello.overrideAttrs (old: {
pname = "user-hello";
version = "2.12.1";
});
in
myHello
닉스에 있는 패키지는 모두 다음 속성을 가지고 있습니다. (전체 속성은 아니고, 가장 자주 쓰이는 것만 정리했습니다.)
속성 | 설명 |
---|---|
pname | 패키지 이름 |
version | 패키지 버전 |
src | 소스 경로 |
buildInputs | 런타임에 의존하는 패키지 |
nativeBuildInputs | 빌드할 때 필요한 패키지 (컴파일러, LSP, …) |
patches | 적용할 패치 |
meta | 메타 데이터 (설명, 라이센스) |
outputs | 결과물 종류 (“out”, “dev”, “doc”) |
configureFlags | ./configure 에 넘길 옵션 |
makeFlags | make 에 넘길 옵션 |
installPhase | 사용자 지정 설치 스크립트 |
nixpkgs 가진 패키지들을, 위 속성을 조정해서 사용자가 원하는 빌드를 할 수 있습니다.
※ 새로운 번역어를 억지로 만들어내는 건 저도 별로 선호하지 않습니다만, derivation을 만들어 내는 닉스 표현식을 derivation 생성식이라 부르니 입에 붙는 것 같아 계속 쓰도록 하겠습니다.
만일 패키지 A가 패키지 B에 의존 한다면, 닉스는 A의 의존성에 바로 B를 써주는 것이 아니라, B를 nixpkgs에 추가하고 이를 써주는 개념입니다.
A 패키지 derivation 생성식엔, nixpkgs에 있는 B 패키지 derivation 생성식을 가리키는 속성이 들어 있습니다.
let
pkgs = import <nixpkgs> {};
B = pkgs.callPackage ./path/to/B.nix {};
in
./path/to/A.nix {
pkgs.callPackage buildInputs = [ B ];
}
기존 패키지의 속성 일부만 바꾸고 싶을 때, callPackage
를 부르면서 바꾸고 싶은 속성을 넘겨 새로운 derivation을 다시 생성할 수도 있겠지만, overrideAttrs
로 이미 생성된 derivation을 가져와서 일부 속성만 바꿔 새로운 derivation을 생성할 수도 있습니다.
※ derivation은 닉스 표현식을 해석해서, 빌드하기 위한 모든 정보를 계산해서 하나의 리스트로 모아 놓은 테이블로 볼 수 있습니다. *overrideAttrs
는 이미 계산이 끝나 나온 결과물에서 고치고, callPackage
는 고친 다음 다시 계산하는 차이가 있습니다.
닉스 알약nix pills - 14 Override design pattern 오버레이overlay, 오버라이드override 차이
예를 들어 morning
이란 패키지가 hello
에 의존하고 있는데, hello
를 오버라이드하면, nixpkgs에 있는 hello
를 “바꿔 놓는 게 아니라” “새로운 걸 만들어 둡니다.”. 그래서 기존 morning
은 영향을 받지 않습니다. hello
오버레이는 기존 것을 “바꿔 놓는 것이라서” hello
에 의존하는 morning
도 영향을 받습니다.
이러면 불변이 깨진 것처럼 보일 수 있는데, 오버레이 수정은 아예 패키지셋nixpkgs 자체를 새로운 패키지셋으로 대체합니다. 복잡하게 얘기하면, 수정 전 패키지셋에 있는 morning
이 영향 받는 게 아니라, 수정 후 패키지셋의 morning
이 영향을 받습니다.
기존 빌드되어 있던 (수정전 nixpkgs)을 참고해 빌드된 morning
캐시(바이너리)는 변하지 않고,
오버레이 적용 후 morning
을 다시 빌드해야만 hello
가 바뀐 게 morning
에도 적용됩니다.
default.nix
파일의 derivation 생성식을 가진 함수가, derivation을 바로 반환하는 경우가 있고,
{pkgs ? import <nixpkgs> {} }:
{ ... } pkgs.stdenv.mkDerivation
속성 집합을 반환하는 경우도 있습니다.
{pkgs ? import <nixpkgs> {} }:
{
attr1 = pkgs.stdenv.mkDerivation { ... };
attr2 = ...;
}
닉스 패키지 매니저 설치부터 까다롭습니다. obelisk를 설치하며 nix가 어찌 저찌 설치된 것 같은데, nixpkgs를 못찾는다 해서 다시 multi-user 방식으로 설치했습니다.
https://nixos.org/manual/nix/stable/#transparent-sourcebinary-deployment
Nix는 패키지를 하스켈같은 순수 함수형 프로그래밍 언어에서의 값value처럼 취급합니다. side-effect가 없는 함수로 빌드하고, 한 번 빌드된 이후에는 절대 변하지 않습니다. 해시를 계산해서 파일명이나 디렉토리명에 붙여 두고, derivation이 조금이라도 정보가 바뀌면 해시를 다시 계산해서 절대 같은 해시에 다른 정보를 가지는 경우가 없게 합니다. Nix는 패키지를 Nix store(보통 /nix/store
)에 저장합니다. 각 패키지는 자신만의 고유 서브 디렉토리를 갖습니다.
/nix/store/b6gvzjyb2pg0kjfwrjmg1vfhh54ad73z-firefox-33.1/
패키지명 앞의 문자열은 의존성 정보에 쓰이는 고유 해시값입니다.
만약 전역 위치에 패키지를 설치하면, 개발하고 있는 앱에서 이 패키지 의존성을 지정하지 않아도 개발자 머신에선 잘 돌아가고, 나중에 사용자는 안 돌아가는 사태가 발생합니다. 이를 막기 위해 패키지를 글로벌하게 설치하지 않습니다.
λ> nix-env --uninstall firefox
설치 제거를 하면 바로 삭제하지 않습니다. 롤백을 원할 수도 있고, 다른 사용자의 프로필에 있는 패키지일 수도 있습니다.
사용하지 않는 패키지를 삭제하려면 Garbage collector를 돌립니다.
λ> nix-collect-garbage
Nix 표현식은 소스에서 어떻게 패키지를 빌드할지 설명합니다.
λ> nix-env --install --attr nixpkgs.firefox
(Nix store에 없다면 C라이브러와 컴파일러까지도 설치할 수 있습니다.) (패키지를 컴파일된 바이너리가 아닌) 소스로 배포deployment하는 모델입니다만, 가급적 소스에서 빌드하는 건 피하고, binary cache, pre-built 바이너리를 제공하는 웹서버들을 이용해서 시간 효율을 높입니다. 만일 /nix/store/b5lksdklsjfd...-firefox-33.1
을 빌드하라고 하면, 바로 소스에서 빌드하는 게 아니라, 우선 https://cache.nixos.org/b5lksdklsjfd...narinfo
파일이 있는지 체크하고, 있다면 빌드된pre-built 패키지를 가져오고, 없으면 빌드 단계로 들어갑니다.
Nixpkgs는, 수천 개(만 개쯤)의 유닉스 패키지를 위한 많은 양의 Nix 표현식(derivation 생성식)을 제공합니다. 단순한 바이너리 패키지 모음이 아니라, 패키지를 빌드하는 방법, 패키지를 개발하고 빌드하기 위한 환경을 만드는 방법을 같이 가지고 있습니다.
보통 /etc/nix/nix.conf
에서 찾을 수 있습니다.
시스템에 설치된 패키지를 덮어 쓸 때는 ~/.config/nixpkgs/config.nix
를 이용합니다.
환경 변수 NIX_CONF_DIR
로 닉스 패키지 매니저에게 전역 설정 파일 위치를 알려 줄 수 있습니다.
XDG_CONFIG_HOME
에서 사용자 설정 파일을 찾습니다.
대부분 시스템이 XDG_CONFIG_DIR
은 /etc/xdg
, XDG_CONFIG_HOME
은 $HOME/.config
위치를 씁니다.
Nigpkgs 버전은 nix-channels
옵션으로 지정합니다.
임시 설정을 위해선, 설정 파일을 안쓰고, 환경 변수 NIX_CONFIG
에 바로 넣어 놓을 수도 있습니다.
설정 파일은 한 줄에 name = value
하나 형태입니다.
λ> nix-shell '<nixpkgs>' --attr pan
위 명령어를 입력하면 nix shell로 들어 갑니다.
[nix-shell]$ unpackPhase
[nix-shell]$ cd pan-*
[nix-shell]$ configurePhase
[nix-shell]$ buildPhase [nix-shell]$ ./pan/gui/pan
기본 아이디어는 시스템을 설정할 때, 커맨드라인 명령어를 쓴다든지, GUI 툴로 클릭하며 설정하지 않고, 모든 설정은 설정파일에 남기면, 이 설정 파일만 있다면 언제든지 동일한 환경을 만들 수 있을 겁니다.
닉스를 기반(닉스 언어로 시스템 설정을 표현하는)으로 하는 리눅스 배포판입니다. 패키지 매니징에만 Nix를 쓰는 게 아니라, 시스템 설정에도 씁니다. (ex. /etc
에 있는 설정 파일들 빌드할 때) Nix로 시스템을 관리하면, 시스템 자체를 어떤 시점의 설정 상태로 편하게 롤백할 수도 있습니다.
리눅스를 기반으로 하고 있지만, 리눅스용으로 컴파일한 바이너리를 바로 실행할 수는 없습니다. 리눅스 바이너리를 NixOS용으로 패치하든가, 다른 도구들의 도움을 받아야 합니다.
https://nix.dev/tutorials/first-steps/ad-hoc-shell-environments
λ> nix-shell -p 앱이름
닉스 쉘로 들어가며, 그 환경에만 앱을 설치합니다. Ctrl-d
로 빠져 나오면 앱을 설치하지 않은 상태가 됩니다. 임시로 앱을 설치할 일이 있을 때 씁니다.
닉스쉘로 들어가지 않고, 현재 설치되어 있지 않은데, 간단히 실행해보려면
λ nix-shell -p cowsay --run "cowsay Nix"
--pure
기존 시스템에 있는 환경을 최대한 안쓰게 할 때 씁니다. 예를 들어 PATH
같은 환경 변수를 읽어오지 않기 때문에 시스템 디렉토리 /usr/bin
이나 /bin
등에 접근하지 않습니다.
-I
닉스 패키지 지정
nix-collect-garbage
임시 닉스 쉘에서 사용했던 패키지들 제거합니다.
닉스 쉘을 shebang 인터프리터로 쓰기
#!/usr/bin/env nix-shell
#! nix-shell -i bash --pure 파일의 나머지를 해석할 인터프리터
#! nix-shell -p bash cacert curl jq python3Packages.xmljson 인터프리터 환경에서 제공하는 패키지 목록
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/2a601aafdc5605a5133a2ca506a34a3a73377247.tar.gz
# -I 패키지 위치 명시적으로 지정
환경이 활성화되면, 자동으로 bash 명령어 실행
자동으로 환경 변수 지정
버전 컨트롤에 환경 정의를 넣고, 다른 장비에서 불러서 적용합니다.
shell.nix
파일을 만듭니다.
대충 apt install ...
등을 쉘에서 단발적으로 실행하지 않고, 쉘 스크립트 파일에 모두 모아 놓는 거랑 비슷합니다.
let
# 명시적인 버전 지정
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
# config, overlays를 이런식으로라도 지정해 놓으면, 전역값이 덮어 씌우거나 할 수 없다.
pkgs = import nixpkgs { config = {}; overlays = []; };
in
{ # 쉘 환경을 만들어내는 함수
pkgs.mkShell packages = with pkgs; [ # attribute라 부른다.
cowsay
lolcat];
GREETING = "Hello, Nix!";
# 시작
shellHook = ''
echo $GREETING | cowsay | lolcat
'';
}
mkShell
의 예시 중에, 패키지를 buildInputs
나 nativeBuildInputs
속성에 추가하는 것도 있습니다.
닉스쉘은 원래 “패키지 빌드 디버깅에 필요한 도구”를 가지고 있는 쉘 환경을 만들기 위해 나왔습니다. 처음 목적은 그랬지만, 지금은 임시 환경을 만드는 용도로도 사용합니다.
nix-shell
을 실행하면, 현재 폴더에서 shell.nix
파일을 찾습니다. packages
에 써 놓은 것들을 $PATH
에서 보이게 해 줍니다. 닉스쉘을 켜둔 상태에서 shell.nix
파일을 수정해도 바로 반영되진 않습니다. 닉스쉘을 내렸다 다시 올리면 반영됩니다.
쉘 환경에 들어가기 직전에 실행하고 싶은 것들은 shellHook
를 씁니다.
{ pkgs ? import <nixpkgs> {}
}:
닉스 패키지를 불러와서, 닉스 표현식을 실행하는 편리한 방법
However, the resulting Nix expression is not fully reproducible
닉스 패키지가 바뀔 수도 있으면, 결과가 항상 같은 건 아닙니다. 완벽하게 재현가능한 닉스 표현식을 만들려면, Nixpkgs의 버전을 고정해야 합니다.
좀 특이한데, import <nixpkgs>
이 자체가 하나의 함수고, 이 함수에 {...}
인자를 넘기는 모양입니다. 왜 이렇게 동작하냐면, import
는 지정한 파일에서 닉스 표현식을 읽어와 반환합니다. 이 반환값이 함수인 경우엔 보통의 함수처럼 인자를 취하는 모양이 됩니다.
import nixpkgs { config = {}; overlays = []; }; pkgs =
import nixpkgs
함수는 인자로 { config, overlays }
속성 집합을 받고 있습니다.
{ ... } pkgs.mkShell
mkShell
도 마찬가지입니다. 함수가 { ... }
인자를 받는 모양입니다.
default.nix
파일을 읽어 옵니다.
import ./.
이라 써 주면, 현재 디렉토리에서 default.nix
를 읽어 옵니다.import
는 빌트인 함수입니다. 대부분의 빌트인 함수는 builtins
를 통해 접근하는데, import
는 예외입니다.※ 닉스 언어에서 { x, y }: x + y
모양은, 속성 x
, y
를 가지고 있는 집합을 인자로 받는 람다 함수입니다.
Lazy 평가 전략을 취합니다. WHNF에 머물지 않고, 모두 평가된 걸로 보려면, repl에서 :p
를 붙입니다.
runHaskell
처럼 닉스 파일을 바로 실행하는 방법도 있습니다.
nix-instantiate --eval file.nix
닉스 파일을 따로 지정하지 않으면, 현재 폴더의 default.nix
파일을 읽습니다.
렉시컬 토큰을 구분하는 구분자. 인덴트나 줄바꿈은 따로 의미를 가지지 않습니다.
Value는 닉스 언어의 프리미티브 데이터 타입, 리스트, 속성 집합, 함수가 될 수 있습니다.
rec { ... }
함수형 언어라서 {...}
속성 집합을 정의 중에, 집합내의 한 속성이 다른 속성에 접근하려면, recursive한 동작이 필요합니다. 왜 rec
가 붙는지 좀 더 자세히 보려면 MonadFix를 참고하세요.
Attribute Set 이 튜플의 역할을 합니다.
let
attrset = { x = 1; };
in
attrset.x
let
attrset = { a = { b = { c = 1;};};};
in
attrset.a.b.c
익숙하지 않은 모양입니다. 아래와 같이 읽어 보면 조금 낫습니다.
{ a = <thumb> };
attrset = { b = <thumb> };
{ c = <thumb> };
1
{a, b}: a * b
a: b : a * b
위와 아래는 다른 함수입니다. 첫 번째는 속성 집합 하나를 인자로 받고, 두 번째는 \x -> \y -> a + y
같은 람다 함수입니다.
nix-repl> mul = {a, b, ...}: a * b * c <-------------(x) nix-repl> mul = s@{ a, b, ... }: a * b * s.c
둘 중 위에 처럼 할 수는 없습니다. 아직 인자로 c가 올지 뭐가 올지 알 수 없습니다. 하지만 인자 집합을 s@
바인딩하면 s.c
로 미리 쓸 수 있습니다. WHNF때문에 가능합니다. Lazy하게 나중에 s
에 있는 뭔가를 가져오고 있다는 것만 알고 지나갔다가 가변 인자로 c
가 들어오면 그 때 평가하면 됩니다.
let
a = {
x = 1;
y = 2;
z = 3;
};
in
with a; [x,y,z] # [ a.x, a.y, a.z]
with
의 범위는 다음 다음 세미 콜론까지 (즉, 다음 표현식이 끝날 때까지), a
의 속성을 현재 스코프로 가져 옵니다. - 이 것도 그다지 좋은 컨벤션을 도입한 것 같지 않지만, 타이핑을 줄여 주긴 합니다.
let
x = 1;
y = 2;
in
{
inherit x y; # x = x; y = y;
}
{
inherit (a) x y; # a.x = x; a.y = y;
}
같은 이름 쓰는 걸 줄여줍니다. 실제 코드를 보면, 인자를 그대로 다음 함수에게 넘기는 경우가 잦아 활용도가 높습니다.
let
name = "Nix";
in
"hello ${name}"
https://nix.dev/manual/nix/2.18/language/derivations
https://nix.dev/
nix-repl> 6/3 /home/lionhairdino/6/3
슬래시를 공백없이 바로 쓰면 경로path로 인식합니다. 나누기를 원하면,
nix-repl> 6/ 3
공백을 주고 다음 인자를 써주면 됩니다. 특이한 동작이지만, 패키지 매니징에 특화되어 있는 닉스가 나누기를 할 일이 많지 않아 선택한 동작 같습니다.
패키지 이름에 대시-
가 많이 쓰여, 식별자Identifier에도 -
를 쓸 수 있게 되어 있습니다.
{
lib,
stdenv,
fetchzip,
}: # 람다 함수의 인자를 구분했던 콜론
{
stdenv.mkDerivation pname = "hello";
version = "2.12.1";
src = fetchzip {
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
sha256 = lib.fakeSha256;
};
}
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-22.11";
pkgs = import nixpkgs { config = {}; overlays = []; };
in
{
hello = pkgs.callPackage ./hello.nix { }; # pkgs의 속성을 뒤 이은 함수에게 자동으로 넘긴다.
}
Nix building instructions
“마치 기계한테 이 설명서 따라해서 빌드해 줘” 라고 할 때, 이 설명서를 derivation이라 볼 수 있습니다. Nix(패키지 매니저)가 derivation을 해석해서 빌드한 후 nix/store
에 결과물이 생기는 걸 realised라 합니다. 위와 같은 닉스 소스들은 mkDerivation
같은 함수를 가지고 있고, 닉스가 이 표현식을 평가해서 derivation을 만들고, 이 derivation을 기반으로 패키지를 빌드합니다.
“derivation
에 있는 derivation
이 derivation
을 만들어 저장소에 derivation
을 남긴다.”
= derivation 생성식
에 있는 derivation 함수
가 derivation
을 만들어 derivation 파일(.drv)
에 저장한다.”
@jhhuh님의 설명을 옮깁니다.
- 수학에서 말하는 derivation은 아닙니다. 닉스에서 특별히 취급되는 데이터 타입입니다. (기본적으로는 그냥
type = "derivation"
이 포함된attrset
입니다.)
- 빌트인 명령어
derivation
으로 만들 수 있고 “realize”를 하면 nix store상에".drv"
파일이 (이것도 derivation이라고 부릅니다.) 생성되는데 빌드를 위한 모든 정보가 담긴 빽빽한 플레인 텍스트파일입니다.
- derivation을 “build”하게 되면 nix store에
outpath
(이름에 derivation정보로 부터 도출된 해쉬값이 prefix된 디렉토리 혹은 파일)이 nix store에 생성됩니다.정리하면,
- data type으로서의 derivation
- builtin 명령 중 하나
- 파일 시스템 상의
".drv"
파일이 중 하나를 말하는 건데, 그냥 간단히 derivation은 닉스가 빌드할 수 있는 무언가라고 할 수 있겠습니다.
※ .drv
는 nix/store
아래 임시 폴더에, 빌드 과정 중 임시로 파일로 생성된 후, 빌드가 끝나면 지워도 상관 없는 상태가 됩니다. 바로 지워지는 건 아닙니다. (나중에 GC를 돌리면 정리합니다.) nixpkgs
가 가지고 있는 건 derivation
이 아니라, derivation을 생성할 수 있는 닉스 표현식 (default.nix
같은 것들)을 가지고 있습니다. 다른 곳에서 흔히 쓰이는 번역어는 아니지만, derivation 생성식 모음을 가지고 있다고 말하면 적당합니다.
Derivation
은 빌드를 위한 레시피를 가지고 있는 특별한 파일입니다. 아래는 hello
를 출력하는 c언어 프로젝트를 빌드하는 방법을 적어 놓은 .drv
파일입니다.
/nix/store/jkxk7lcnkhadd8rwq5n3z1gwbdcmqn0f-hello.drv
([("out","/nix/store/n131z2zya6ifx050b8q3biymx2jxfdwi-hello","","")],[("/nix/store/029h9shccppyiw1l7qsk6xp0grxgzzbb-stdenv-linux.drv",["out"]),("/nix/store/20vwa6qpx8w3ar66x1fmrjlwy86c7b71-bash-4.4-p23.drv",["out"])],["/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh","/nix/store/m8x2zsc2awjyhwq1fw65czpkikifxq3x-source"],"x86_64-linux","/nix/store/hrpvwkjz04s9i4nmli843hyw9z4pwhww-bash-4.4-p23/bin/bash",["-e","/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],[("buildInputs",""),("buildPhase","gcc -o hello ./hello.c"),("builder","/nix/store/hrpvwkjz04s9i4nmli843hyw9z4pwhww-bash-4.4-p23/bin/bash"),("configureFlags",""),("depsBuildBuild",""),("depsBuildBuildPropagated",""),("depsBuildTarget",""),("depsBuildTargetPropagated",""),("depsHostHost",""),("depsHostHostPropagated",""),("depsTargetTarget",""),("depsTargetTargetPropagated",""),("doCheck",""),("doInstallCheck",""),("installPhase","mkdir -p $out/bin; install -t $out/bin hello"),("name","hello"),("nativeBuildInputs",""),("out","/nix/store/n131z2zya6ifx050b8q3biymx2jxfdwi-hello"),("outputs","out"),("patches",""),("propagatedBuildInputs",""),("propagatedNativeBuildInputs",""),("src","/nix/store/m8x2zsc2awjyhwq1fw65czpkikifxq3x-source"),("stdenv","/nix/store/sm7kk5n84vaisqvhk1yfsjqls50j8s0m-stdenv-linux"),("strictDeps",""),("system","x86_64-linux")]) Derive
해시 코드가 길게 있어 복잡하지만, 소스 path와 빌드 후 출력할 path, 빌드 스크립트, 메타데이터(프로젝트 이름, 플랫폼, …)를 가지고 있습니다. 필요한 파일들은 /nix/store
에 다 집어 넣고, 독립된 샌드박스에서 빌드합니다. 당연히 위 복잡한 코드를 손으로 만들 일은 없습니다.
{ pkgs ? import <nixpkgs> {} }:
derivation {
name = "hello-world";
builder = pkgs.writeShellScript "build-hello" ''
${pkgs.coreutils}/bin/mkdir -p $out/bin
${pkgs.gcc}/bin/gcc $src -o $out/bin/hello -O2
'';
src = ./hello.c;
system = builtins.currentSystem;
}
nixos.org - manual - derivation
import
로드하고 파일에 있는 “닉스 표현식을 반환”합니다.
<nixpkgs>
닉스 파일 검색 경로. $NIX_PATH
환경 변수로 지정할 수 있습니다.
위 파일은 전체가 아래같이 생긴 하나의 함수입니다.
{ 인자 } :
{ ... } dervibation
import <nixpkgs>
란 함수에 {}
인자를 넘겨 평가한 결과값을 매개 변수 pkgs
에 디폴트 값으로 바인딩해서 아래 함수 본문을 실행한다고 읽을 수 있겠습니다. (위 구문 전체가 하나의 함수니 프레임 워크 어딘가에서 “호출”이라는 절차가 있겠지요?)
derivation
은 가장 중요한 빌트인 함수입니다. single derivation을 기술하기 위해 씁니다.
name
(String
)
derivation의 심볼릭 이름. 대응하는 store derivation
의 store path
에 추가되고, ouput paths
에도 추가 됩니다.
system
(String
)
빌더 실행체executable?의 시스템 타입. builtins.currentSystem
을 평가해서 현재 시스템 타입을 가져올 수 있습니다.
builder
(Path
| String
)
빌드를 수행할 실행체 경로
writeShellScript
(nixstore에 저장할 파일명)
'' 파일 내용 ''
여기 경우엔, 빌드를 실행할 쉘 스크립트
※ ''
이 멀티 라인 문자열을 쿼트하는데 쓰입니다. 특이한 걸 골랐네요.
args
(List
of String
)
builder
실행체에 넘길 인자
outputs
(List
of String
)
nix store에 빌드 결과물을 저장하고, 해당 결과물을 심볼릭 링크합니다.
[ "lib" "dev" "doc"] outputs =
이렇게 잡아 두면, 예를 들어 Audoconf-style
패키지라면, 빌더는 아래 동작을 합니다.
./configure \
--libdir=$lib/lib \
--includedir=$dev/include \ --docdir=$doc/share/doc
name
이 있으면,
derivation {
name = "example";
outputs = [ "lib" "dev" "doc" "out" ];
}
/nix/store/<hash>-example-lib
/nix/store/<hash>-example-dev
/nix/store/<hash>-example-doc /nix/store/<hash>-example
Nix Store에서 쓰이는 파일 시스템은 OS의 파일 시스템과 다릅니다. 파일 시스템의 추상으로 파일 시스템 오브젝트(File, Directory, Symbolic Link)로 이루어진 간단한 모델을 씁니다. 하드 링크나 소유 권한, 날짜 등의 메타 정보가 없습니다. 파일들이 가진 메타 정보는 크기와 executable = true | false
만 있습니다.
※ 왜 용어로 derivation을 골랐을까?
derive라 하면 무언가를 만드는 과정에서 튕겨져 나온 결과물 같은 느낌입니다. 예를 들어 A
,B
,C
로 결과물가
를 만드는데, A
,b
,C
로 조금 설정을 바꾸면 다른 결과물 나
가 나오는 걸 상상할 수 있습니다. 이럴 때 나
는 파생물(유도물)이라 불러도 될 것 같습니다.
우리말로 하면 파생문, 유도문 정도 되겠지만, 익숙하지 않으니 일단은 안쓰기로 합니다. 고유한 개념을 지칭하는 거라 번역 안하는 게 맞을 것 같기도 합니다. 만일 번역한다 해도, 파생물, 유도물이 아닌 이유는 이들이 패키지 본체가 아니어서, 문이 더 어울립니다.
원래 이펙트 가득한 패키지 빌드를 단순히 집합(혹은 리스트)으로 표현하고 있어 보통의 값처럼 함수 인자로 넘기고, 결과로 출력할 수 있습니다. 당연한 듯 보이는 이 문장에 닉스의 핵심 아이디어가 들어가 있습니다. 패키지 빌드에 꼭 개입해야 하는 이펙트들을 나몰라라 하고 순수하게 조합하다가, 조합이 끝나서 패키지를 빌드해야 되는 순간이 오면 그 때 realize해서 패키지가 되도록 합니다. 마치, 모나드들의 runner처럼 볼 수 있습니다.
Flake를 활성화한 뒤의 nix shell
과 구별해야 합니다.
derivation 해석해서 패키지 자체가 아니라, 개발 환경에 필요한 툴들을 준비하고 의존성만 빌드합니다. 목적이 특정 앱을 빌드하는 게 아니라, 앱 빌드를 위한 환경만을 준비한다는 뜻입니다.
default.nix
파일을 닉스 패키지 매니저가 읽어 들여, derivation 생성식을 평가해서, 사람 말고 기계를 위한 derivation을 가진 .drv
파일이 만들어집니다. (꼭 사람이 못 읽는 건 아니고, 빌드에 필요한 모든 정보를 모은 리스트 모양입니다.)
.drv
파일은 아래 명령어로 직접 만들 수 있습니다.
> nix-instantiate default.nix
.drv
파일이 준비되면, 이 파일을 realize해서 패키지를 만들 수 있습니다.
> nix-store --realize <derivation.drv>
nix-build
는 위 두 단계를 순차대로 실행해서 .drv
파일을 만들고, 이를 realize(번역한다면 실체화?)해서 패키지가 만들어집니다.
※ nixpkgs에는 .drv
파일들이 아니라, .drv
를 생성하는 .nix
코드들이 모여 있습니다.
※ 로컬의 /nix/store/
에는 .drv
와 빌드 결과물인 실행 파일 등이 같이 있습니다.
※ 생각 같아선 .drv
의 해시코드와, 빌드 결과물의 해시코드가 같을 것 같지만 다릅니다. 만일, 결과물의 기반이 된 .drv
를 확인하려면 별도의 /nix/store
를 위한 DB가 유지되고 있어 추적이 가능하긴 하다.
※ 릴리즈 빌드를 한다면 최고의 툴이지만, incremental 빌드 시스템이 아니라서, 개발할 때는 좋지 않다고 합니다. 닉스는 한 곳만 변경해도 전체를 다시 재컴파일 해야 하는 단점이 있습니다.
※ 출력되는 메시지를 보면, configure
가 호출되고, Makefile
을 만들어냅니다.
stdenv
는 GNU Autoconf(자동으로 프로젝트 디렉토리 구조를 파악한다)를 기반으로 빌드합니다.
https://nix.dev/tutorials/packaging-existing-software
Zero to Nix - derivation
※ stdenv standard environment 표준 빌드 환경으로 컴파일러, 라이브러리, 빌드 도구들 포함합니다.
mkDerivation: Nix의 내장 함수 derivation
의 래핑 함수입니다.
derivation 함수는 실제 사용할 일은 거의 없고, 대부분 mkDerivation
등의 래핑 함수를 씁니다.
하스켈 스타일로 서명을 써 보면,
derivation ::
: String
{ system : String
, name : Path | Derivation
, builder ?args : [String]
, ?outputs : [String]
, -> Derivation }
stdenv.mkDerivation
Standard environment
대체로 표준 환경 설정만으로, Unix 패키지 빌드의 많은 부분을 자동으로 할 수 있습니다. ./configure; make; make install
빌드 인터페이스를 가지고 있으면, 따로 빌드 스크립트를 쓸 필요가 없습니다. 표준 환경이 알아서 합니다. 만일 자동으로 안될 경우 다양한 빌드 phases를 오버라이드 해서 상황에 맞게 만들 수 있습니다. 표준 환경으로 빌드할 때 stdenv.mkDerivation
을 씁니다. derivation 함수의 래퍼입니다.
{
stdenv.mkDerivation name = "libfoo-1.2.3";
src = fetchurl {
url = "http://...tar.bz2";
hash = "sha256-...";
}
}
name
은 pname
과 version
을 써주면 자동으로 만든다. 이 때 속성들이 자신들이 속한 집합에 있는 다른 속성을 참조하려면 rec
가 필요합니다. ※ MonadFix 참고
rec {
stdenv.mkDerivation pname = "libfoo";
version = "1.2.3";
name = "${pname}-${version}"; # rec가 있어야 가능하다.
}
인자를 하나 하나 살펴 보려하니 너무 많습니다. 자주 보이는 것 위주로 보고 넘어가야겠습니다.
name: "${pname}-${version}"로 자동 생성
pname: (필수)
version: (필수)
src: 소스 코드(로컬 디렉토리, tarball, Git, ...)
buildInputs: 빌드에 필요한 의존성 패키지, 빌드 자체에 필요하거나 혹은 런타임에 필요한 것
nativeBuildInputs: 개발 환경에서만 필요한 의존성 (컴파일러, autoconf, lsp, ...)
configurePhase: ./configure와 같은 설정 과정
buildPhase: make, cargo build, stack build 같은 빌드 명령
installPhase: 결과물을 $out으로 복사
...
# hello.nix
{
stdenv,
fetchzip,
}: # 인자 두 개를 받는 함수란 뜻이다.
{
stdenv.mkDerivation pname = "hello";
version = "2.12.1";
src = fetchzip {
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz"; sha256 = "";
};
}
# default.nix
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-22.11";
pkgs = import nixpkgs { config = {}; overlays = []; };
in
{
hello = pkgs.callPackage ./hello.nix { };
}
callPackage
가 hello.nix
파일이 돌려주는 “함수”가 필요로 하는 인자값 stdenv
, fetchzip
을 넣어 줍니다. 값을 넣어 주면, 또 함수를 돌려 주는데, 거기에 { }
를 넘기고 있습니다.
callPackage
함수는 derivation
생성 함수를 호출하는 함수입니다. 인자로 받은 표현식(위에선 파일이 가진 derivation
함수)를 실행해서 최종 derivation을 얻습니다. derivation
함수가 필요로 하는 stdenv
, gcc
같은 걸 자동으로 전달합니다. 패키지를 호출한다는 이름이 어색하긴 한데, 패키지 derivation을 정의하는 derivation
함수를 호출한다고 보면 됩니다.
# 아래 autoArgs를 pkgs로 고정하는 래퍼
callPackage = callPackageWith pkgs; autoArgs: fn: args: ... mkAttrOverridable f allargs; callPackageWith =
위 예시를 대입해서 읽어 보면, ./hello.nix
가 가진 hello
를 빌드하는 derivation을 생성하는 함수를, allargs
란 디폴트 속성 집합을 가지게 만들어 놓고, 여기에 아무 것도 override하지 않는 빈 속성 집합을 넘겨, 결국 derivation 생성 함수가 실행되고, 변수 hello
에 hello 패키지 derivation을 바인딩하고 있습니다.
※ makeOverridable : 속성 집합에 있는 속성을 override
하는 기능을 추가합니다. 아래 예시는, x
를 먼저 override
기능이 있는 함수로 만들고, 디폴트 인자 {a = 1, b = 2}: { result = a + b; }
를 가진 함수로 만듭니다.
nix-repl> x = {a, b}: { result = a + b; }
nix-repl> y = lib.makeOverridable x { a = 1; b = 2; }
nix-repl> y
{ override = «lambda»; overrideDerivation = «lambda»; result = 3; }
nix-repl> y.result
3
nix-repl> y.override { a = 10; }
{ override = «lambda»; overrideDerivation = «lambda»; result = 12; }
nix-repl> (y.override { a = 10; }).result 12
결국 mkDerivation
과 callPackage
의 차이는,
callPackage
는 ./hello.nix
가 가진 derivation 생성 함수가 pkgs
같은 값들을 미리 디폴트 속성으로 가지고 있게 만들어 두는 역할만 합니다.
최종 사용자 입장에서는 callPackage
는 몇 몇 속성을 알아서 넣어 주는 거고, mkDerivation
결과를 그대로 가져오는 건, 사용자가 모든 속성을 채워 넣어야 하는 차이만 있다고 보면 되겠습니다.
요약하면, 대충 위처럼 놓고 nix-build
를 돌리면 빌드 에러가 나는 이유들(보통은 라이브러리가 없어서 난다.)을 알려 줍니다. 라이브러리가 없다면, http://search.nixos.org/packages 에서 찾든가, nix-locate
를 이용해서 찾아서 매개 변수를 만들고, buildInputs = [ 필요한라이브러리1 필요한라이브러리2 ...]
이렇게 써주고, make install
이 없다면 installPhase
를 만들어 설치 방법을 적어주고, 다시 nix-build
를 돌려, 최종 성공하면 result
란 폴더가 생깁니다. 이 result
는 닉스 store의 특정 버전의 심볼릭 링크입니다.
사용자 레벨에서 패키지를 설치할 수 있습니다. nix-env
로 설치하면, 해당 사용자만 쓸 수 있습니다. 프로젝트별 환경을 만드는 것이 아니라, 현재 사용자 환경에만 설치하니, 현재 사용자의 모든 프로젝트에 영향을 미칩니다. 프로젝트별 환경을 위해서는 nix-shell
또는 direnv
같은 툴을 쓰고, 프로젝트별 shell.nix
를 생성하는 방식을 씁니다.
용도 | 명령어 |
---|---|
패키지 검색 | nix search nixpkgs packagename |
패키지 설치 | nix-env -iA packagename |
설치 목록 | nix-env -q |
패키지 제거 | nix-env -e packagename |
패키지 업데이트 | nix-env -u |
nixpkgs에 사용자 정의 패키지를 추가하는 방법입니다. Overlay
폴더를 추가하고, 여기에 default.nix
파일을 만들어 두면 nix-shell
이나 nix-build
명령어를 실행할 때 Overlay
를 참조합니다.
Nixpkgs를 “확장”하거나 “변경”할 때 overlays
를 사용합니다.
닉스 패키지는 여러 Nix 채널로 구분해서 배포된다.
용도 | 명령어 |
---|---|
현재 채널 목록 | nix-channel --list |
primary 채널 추가 | nix-channel --add https://nixos.org/channels/channel-name |
또 다른 채널 추가 | nix-channel --add https://some.channel/url my-alias |
채널 제거 | nix-channel --remove channel-alias |
채널 업데이트 | nix-channel --update channel-alias |
모든 채널 업데이트 | nix-channel --update |
derivation이나 닉스가 빌드한 패키지들이 위치하는 곳으로 읽기 전용입니다. 보통 /nix/store
.
/nix/store/nawl092prjblbhvv16kxxbk6j9gkgcqm-git-2.14.1
이런 앱이름 앞의 해시는, 빌드할 때 써 먹은 모든 입력값(소스, 의존성 트리,컴파일 플래그 등)을 기반으로 만든 값입니다. 같은 버전을 여러 빌드에서 써먹으면, 한 번 설치된 것에 심볼릭 링크를 걸어서 씁니다.
닉스 스토어 항목들을 프로필에 심볼릭 링크를 겁니다. 프로필에 변화를 주면, 버전 개념의 generation을 유지해서, 과거 generation으로 롤백할 수 있는 기능이 있습니다. ~/.nix-profile/
아래에 있는 bin
, etc
, lib
, share
폴더 안에는 /nix/store
에 있는 것들과 심볼릭 링크한 것들로 가득 차 있습니다.
/nix/var/nix/profiles
닉스OS는 리눅스이긴 한데, 다른 리눅스에서 돌아가는 바이너리가 그대로 돌지 않습니다. 아래 예시는, 리눅스에서 컴파일된 바이너리가 의존하는 라이브러들이나 실행 파일, 환경 등이 현재 닉스OS에는 없는 상태입니다. 리눅스에 있는 일반적인 폴더 구조의 각 종 공유 라이브러리들을 쓰고 있을텐데, 닉스OS는 정확한 버전을 링크 걸지 않으면 돌아가지 않습니다.
처음 아래 오류를 만났을 때, 쉘 스크립트가 아닌데, 왜 bash:
오류가 나는지 의아했습니다.
[nix-shell:~/.local/bin]$ ./powermate bash: ./powermate: cannot execute: required file not found
친절한 메시지가 나왔다면 좋았을텐데, 의미를 제대로 전달하는 메시지는 아닙니다. thalheim.io - Nix-ld:A clean solution for issues with pre-compiled executables on NixOS
※ OS가 바이너리를 실행하는 절차
리눅스가 바이너리를 실행하려면, 쉘은 시스템에게 바이너리를 실행하라고 execve
시스템 콜을 합니다.
※ strace -f ./powermate
로 무슨 일이 일어나고 있는지 들여다 볼 수 있습니다.
※ file ./powermate
로 실행 파일 정보를 자세히 볼 수 있습니다.
위 사이트를 요약하면, ELF실행 파일은 시스템과 소통하기 위한 라이브러리를 찾을 때 interpreter란 링크 로더를 쓰는데, 다른 리눅스에선 /lib64/ld-linux-x86-64.so.2
(x86기반 64비트일 경우)에서 제공하는 걸 쓰는데, 닉스OS의 링크 로더link-loader는 특정 버전을 붙인 걸 씁니다.
patchelf로 어떤 버전을 쓰고 있는지 찾을 수 있습니다.
[lionhairdino@nixos:~/.local/bin]$ patchelf --print-interpreter ./powermate
/lib64/ld-linux-x86-64.so.2
[lionhairdino@nixos:~/.local/bin]$ patchelf --print-interpreter /run/current-system/sw/bin/ls /nix/store/p9ysh5rk109gyjj3cn6jr54znvvlahfl-glibc-2.38-66/lib/ld-linux-x86-64.so.2
닉스OS에서 돌아가는 바이너리들은 특정 스냅샷 상태(보통 Nixpkgs의 특정 버전을 가리키는 것으로 stack의 스냅샷과 비슷합니다.)의 링크 로더(여기선 interpreter)를 씁니다. 예를 들어 일반적인 리눅스에선 /lib64/ld-linux-x86-64.so.2
인터프리터를 쓴다면, 닉스OS는 /nix/store
에 있는 특정 버전의 스냅샷에 있는 인터프리터를 씁니다. 위 사이트에선 autoPatchelfHook
에 관한 얘기를 하는데, 이는 명령어가 아니라, 닉스OS에서 쓰는 함수입니다. Nix의 빌드 환경에서 호출하는 것으로, 자동으로 링크 인터프리터와 동적 라이브러리 경로를 /nix/store
의 버전을 가리키게 자동으로 패치해 줍니다. 쉡에서 직접 실행하는 명령어는 아닙니다.
nix-ld
로 미리 버전 지정이 없는 것들에 대한 대비를 해놓는 방법이 있고, 바이너리내에 /usr/bin
같이 경로가 하드 코딩되어 있는 것들을 해결하려면 envfs
를 쓸 수 있다고 합니다.
설정 파일에 아래를 넣으면 좀 더 친절한 에러 메시지가 나옵니다.
programs.nix-ld.enable = true;
[lionhairdino@nixos:~/.local/bin]$ ./powermate cannot execute ./powermate: You are trying to run an unpatched binary on nixos, but you have not configured NIX_LD or NIX_LD_x86_64-linux. See https://github.com/Mic92/nix-ld for more details
실행 파일이 어떤 라이브러리를 필요로 하는지 보는 방법 StackExchange - run a non-nixos executable on NixOS
[lionhairdino@nixos:~/.local/bin]$ ldd ./powermate
linux-vdso.so.1 (0x00007ffc41f6b000)
libpulse.so.0 => not found
libc.so.6 => /nix/store/p9ysh5rk109gyjj3cn6jr54znvvlahfl-glibc-2.38-66/lib/libc.so.6 (0x00007f2ed22f3000) /lib64/ld-linux-x86-64.so.2 => /nix/store/p9ysh5rk109gyjj3cn6jr54znvvlahfl-glibc-2.38-66/lib64/ld-linux-x86-64.so.2 (0x00007f2ed24e9000)
보통의 리눅스들이 쓰는 폴더에 /lib64/ld-linux-x86-64.so.2
(@TODO 이걸 shim레이어라고 부르고 있는 것 같은데…)를 설치하고, 실제 링크 로더는 환경 변수 NIX_LD
를 써서 지정합니다.
[lionhairdino@nixos:~/.local/bin]$ LD_LIBRARY_PATH=/nix/store/g84s3q2rqd93jawhk4fdrsm0z43qxhz6-libpulseaudio-16.1/lib/libpulse.so.0:$LD_LIBRARY_PATH ./powermate cannot execute ./powermate: You are trying to run an unpatched binary on nixos, but you have not configured NIX_LD or NIX_LD_x86_64-linux. See https://github.com/Mic92/nix-ld for more details
커뮤니티에서 만든 nixpkgs를 위한 파일 데이터베이스. 특정 파일을 제공하는 패키지를 알려 줍니다.
[lionhairdino@nixos:~/.local/bin]$ nix-locate libpulse.so.0 --top-level
pulseaudioFull.out 0 s /nix/store/79y7fb33sm0xh2bmlbmklwxlbrnfm4fk-pulseaudio-16.1/lib/libpulse.so.0
pulseaudioFull.out 408,920 x /nix/store/79y7fb33sm0xh2bmlbmklwxlbrnfm4fk-pulseaudio-16.1/lib/libpulse.so.0.24.2
pulseaudio.out 0 s /nix/store/zd4r977fl0rvqk8v60dxxarrc4i6k274-pulseaudio-16.1/lib/libpulse.so.0
pulseaudio.out 408,920 x /nix/store/zd4r977fl0rvqk8v60dxxarrc4i6k274-pulseaudio-16.1/lib/libpulse.so.0.24.2
libpulseaudio.out 0 s /nix/store/g84s3q2rqd93jawhk4fdrsm0z43qxhz6-libpulseaudio-16.1/lib/libpulse.so.0
libpulseaudio.out 408,920 x /nix/store/g84s3q2rqd93jawhk4fdrsm0z43qxhz6-libpulseaudio-16.1/lib/libpulse.so.0.24.2
libpressureaudio.out 151,792 x /nix/store/xv5xcs49ap0rpjz9xg6biiaw24pzbbcj-libpressureaudio-0.1.13/lib/libpulse.so.0
libcardiacarrest.out 88,752 x /nix/store/2bg3lac9jh8akhlb055h1yvvh6f80ksj-libcardiacarrest-12.2.8/lib/libpulse.so.0 apulse.out 151,792 x /nix/store/h8hqmyfjxlpsf5lqpa29ihlfkk6pfivq-apulse-0.1.13/lib/apulse/libpulse.so.0
특정 파일을 가지고 있는 패키지 찾기
{ config, pkgs, ... }: # 인자 두 개를 받는 함수다.
# 아래는 옵션=값 형태의 집합
{ services.httpd.enable = true;
services.httpd.adminAddr = "alice@example.org";
services.httpd.virtualHosts.localhost.documentRoot = "/webroot";
}
그럼 누군가는 위 함수를 부른다는 거겠지요? 위 함수에 config
, pkgs
매개 변수에 인자를 넘기면, 인자 값에 따라 {옵션=값}
을 돌려준다고 보면 되겠습니다.
@TODO pkgs
로 nixpkgs
가 넘어 오겠지?
journalctl -p 0..3 -x
systemd
는 journal
이라는 바이너리로 로그를 저장합니다.
-n
최근 메시지 10개만
-n 5
최근 메시지 5개만
-x
상세 설명
-e
마지막 메시지부터
-f
tail -f
와 동일
-p
(emerg 0, alert, crit, err, warning, notice, info, debug 7) 우선 순위로 정렬?
--since 20240515 --until 20240516
--since "-2hour" --until "10min"
※ 갑자기 순간의 프리징이 보이거나, 아니면 곧바로 gdm 로그인 상태로 가버립니다.
다시 로그인하면 작업은 모두 사라져 있습니다. 저널 로그 확인하니, nouveau 드라이버가 의심갑니다. https://nixos.wiki/wiki/Nvidia 2024-05-17 nvidia 오피셜 드라이브로 바꾸고 문제는 사라진 것 같습니다.
@TODO
시스템 상태를 선언적으로, 즉 configuration.nix 파일에 모두 모아둘 수 있다면, 현실적으로 configuration.nix 파일만 들고 다니면, 똑같은 시스템을 “재현”할 수 있습니다. 하지만, DB에 들어 있는 설정에 필요한 정보들은 닉스 패키지 매니저로 관리할 수 없고, 사용자 디렉토리에 있는 dotfiles들도 그렇습니다. 완벽하게, 한 큐에 재현가능한 메커니즘을 만들기는 어렵습니다.
사용자 디렉토리는, 시스템과 별개인 사용자 데이터들이 모인 중요한 디렉토리입니다. 이 디렉토리를 시스템과 묶어서 특정 스냅샷으로 되돌리거나 하면 난리 날 것입니다.
이런 위험을 신경쓰지 않고, 사용자 디렉토리에서 “시스템 설정에 필요한 정보”만 따로 잘 컨트롤하기 위해 home-manager를 도입했습니다.
튜토리얼에선, desktop environment 뿐만 아니라 development environment, compilation environment, cloud virtual machine, 컨테이너 이미지 구성 들도 관리한다고 표현하고 있습니다. 이런 목적으로 NixOps, colmena 란 툴들 있는 것 같습니다.
Nixpkgs 모듈은 인자 5개를 받습니다.
lib
: nixpkgs에 있는 빌트인 함수들을 모아 놓은 라이브러리
config
: 현재 환경에 있는 옵션 값 집합
options
: 현재 환경에 있는 모든 모듈에 정의되어 있는 모든 옵션 집합
pkgs
: 모든 nixpkgs 패키지를 포함하는 컬렉션과 유틸리티 함수
초보 단계에선 디폴트값nixpkgs.legacyPackages."${system}"
만 생각해도 된다고 합니다.
modulesPath
: NixOS에서만 유효한 매개 변수, nixpkgs/nixos/modules
를 가리킵니다.
NixOS가 생성한 hardware-configuration.nix 파일에 볼 수 있으며, 추가적인 NixOS 모듈을 import하기 위해 쓰입니다.
디폴트가 아닌 매개 변수를 서브 모듈에 넘기는 법
_module.args
specialArgs
닉스는 시스템 레벨의 설정을 다루지, 사용자 레벨의 설정은 다루지 않습니다. 사용자 레벨의 설정을 다루려면 Home Manager 를 설치해야 합니다. NixOS의 모듈로 Home Manager를 설치합니다.
NixOS 모듈로 설치할 것인가, Home Manager로 설치할 것인가?
root로 작업할 일이 있다. - NixOS 모듈 여러 사용자가 쓸 일이 있다. - NixOS 모듈 NixOS, macOS, 다른 리눅스 배포판에서 모두 돌아가야 할 설정 - Home Manager
sudo nixos-rebuild switch --show-trace --print-build-logs --verbose
위 옵션으로 좀 더 자세한 빌드 오류를 확인할 수 있습니다.
2024.5 현재 home-manager는 nixos-unstable에서 돌아간다고 합니다.
home-manager - releases
home-manager install as module in flake -.users option and home-manager CLI not available
Nix 모듈 시스템에서 의외의 동작이 눈에 띕니다.
For example, if program.packages = […] is defined in multiple modules, then imports will merge all program.packages defined in all Nix modules into one list. Attribute sets can also be merged correctly. The specific behavior can be explored by yourself.
여러 모듈에서 program.packages
를 정의하고, 모듈들을 import
하면 흩어져 있는 program.packages
값을 모두 모아 하나의 리스트로 만듭니다.
Modularize Your NixOS Configuration
program.packages
만 이런 동작을 하는 게 아니라, 설정 파일을 여러 개로 쪼개어 관리할 수 있도록, 동일 항목이 있을 경우 모두 merge하는 동작을 합니다. 이 동작이 없다면, 설정을 어떻게 쪼갤까 생각해 보면, 당연한 필요한 동작입니다. 하지만, 언어적인 입장에선 애매해 보이는 동작이기도 합니다.
Overview of the NixOS Linux distribution
닉스OS는 Linux Standard Base (LSB) 파일 시스템 구조를 따르지 않습니다.
LSB는
분류 | 위치 |
---|---|
시스템 소프트웨어 | /{,usr}/{bin,lib,share} |
설정 파일 | /etc |
사용자 환경에서 쓸 바이너리 | /bin |
동적 라이브러리 | /lib, /usr/lib |
닉스OS는 /lib
, /usr/lib
가 없습니다. 시스템 라이브러리, 바이너리, 커널, 펌웨어, 설정 파일 모두 Nix Store에 저장합니다. 한 번 Nix Store에 저장되면 수정할 수 없습니다immutable. 새로운 버전은 다른 해시값으로 저장될 뿐, 기존 파일은 건드리지 않습니다. /bin
, /usr/bin
에는 shebang라인을 가진 스크립트와 호환을 위해 /bin/sh
, /usr/bin/env
만 들어 있습니다. 사용자 환경은 Nix Store에 있는 것들을 심볼릭 링크를 걸어 만듭니다. 이 환경을 profiles
라 부릅니다. /nix/var/nix/profiles
에 저장됩니다. 사용자들은 모두 자신만의 profile
을 가집니다.
이 구조를 써서 atomicity와 롤백을 지원할 수 있습니다.
Nix Store에 불변의immutable 설정 파일로 저장하는 걸 눈여겨 봐야 합니다. 설정 파일을 “수정”할 수 없다는 뜻입니다. 설정 파일은 실행판을 만들어내는 닉스 설정(클래식 환경에선 /etc/nixos/configuration.nix
)에서만 가능하고, 이를 설정하고 빌드(nixos-rebuild switch
)하면, 바뀐 “수정 파일”을 새로 만들어서 Nix Store에 저장합니다. 이전에 설정했던 환경으로 롤백이 가능한 이유입니다.
이렇게 설정해서 “빌드”한 결과물을 generation이라 부릅니다. 롤백은 “이전 generation으로 돌아가기”라 말하면 되겠습니다.
$ nix-env --rollback $ nixos-rebuild switch --rollback
이 전 generation은 부트 로더에서 고를 수도 있습니다.
새로 generation을 만들어도 이 전 generation은 지워지지 않습니다. 설정을 변경해서 빌드할 때마다 generation은 쌓이는데, 다음 명령으로 오래된 걸 삭제한다든지 하며 관리 합니다.
# 30일 지난 것들 삭제
$ nix-collect-garbage --delete-older-than 30d
# 모두 삭제
$ nix-collect-garbage -d
# 목록
$ nix-env --list-generations --profile /nix/var/nix/profiles/system
# 특정 generation으로 스위칭
$ nix-env --profile /nix/var/nix/profiles/system --switch-generation 204
# 특정 generation 삭제 $ nix-env --profile /nix/var/nix/profiles/system --delete-generations 205 206
/etc/nixos/configuration.nix
의 nix.gc
옵션을 통해 자동 삭제automatic garbage collection를 지정할 수 있습니다.
예를 들어, 최신 디스코드가 아직 nixpkgs에는 안 올라왔다면
nix-shell
을 써서 임시로 설치하든가,
nix-env
로 사용자 환경에서만 설치하든가,
overlays
로 nixpkgs
에 있는 discord를 덮어 씌우든가 할 수 있습니다.
flakes가 활성화된 상태인데, 찾은 패키지가 flakes는 제공하지 않고, default.nix
만 제공할 경우
nix build --no-flake -f default.nix
nix build -f '<nixpkgs>' <패키지명>
아래는 닉스 동작을 가늠해 볼 수 있는 초단순 모형 코드입니다. (검증 필요)
toynix
├── remoteGit 패키지 제공 저장소 모형
│ ├── hello
│ │ ├── hello.c
│ │ └── hello.h
│ └── morning
│ └── morning.c
├── store 토이닉스 저장소
│ ├── hello
│ │ ├── hello.c
│ │ ├── hello.h
│ │ ├── hello.o
│ │ └── libhello.a
│ └── morning
│ ├── morning
│ └── morning.c └── toynix.hs
toynix.hs
{-# LANGUAGE RecordWildCards #-}
module Main where
import System.Directory
import System.Process
import System.IO
import Data.Map (Map, empty, insert, lookup)
data Derivation = Derivation {
name :: String,
source :: String,
buildInputs :: [Derivation],
buildFunc :: FilePath -> [FilePath] -> IO FilePath,
outputPath :: Maybe FilePath
}
data NixStore = NixStore {
store :: Map String FilePath
}
build :: NixStore -> Derivation -> IO NixStore
= do
build nixStore derivation putStrLn $ "Building " ++ name derivation ++ "..."
<- mapM (\dep -> case outputPath dep of
buildInputsPath Just path -> return path
Nothing -> do -- 아직 빌드하지 않았다면
<- build nixStore dep
nixStore' case Data.Map.lookup (name dep) (store nixStore') of
Just path -> return path
Nothing -> error $ "Dependency " ++ name dep ++ " not found in store"
) (buildInputs derivation)
let buildDir = "store/" ++ name derivation -- store/hello
True buildDir
createDirectoryIfMissing <- (buildFunc derivation) buildDir buildInputsPath
outputPath' let store' = Data.Map.insert (name derivation) outputPath' (store nixStore)
putStrLn $ (name derivation) ++ " built at " ++ outputPath'
return NixStore { store = store' }
runNix :: NixStore -> Derivation -> IO ()
= do
runNix nixStore derivation <- build nixStore derivation
nixStore' case Data.Map.lookup (name derivation) (store nixStore') of
Just path -> putStrLn $ name derivation ++ ": " ++ path
Nothing -> error $ "Derivation " ++ name derivation ++ "not found in store"
buildHello :: FilePath -> [FilePath] -> IO FilePath
= do -- 어딘가에서 소스를 가져오는 것을 모형화
buildHello buildDir _ let remote = "remoteGit/hello/"
++ "/hello.c") (buildDir ++ "/hello.c")
copyFile (remote ++ "/hello.h") (buildDir ++ "/hello.h")
copyFile (remote $ "gcc -c -o " ++ buildDir ++ "/hello.o " ++ buildDir ++ "/hello.c"
system $ "ar rcs " ++ buildDir ++ "/libhello.a " ++ buildDir ++ "/hello.o"
system return $ buildDir ++ "/libhello.a"
buildMorning :: FilePath -> [FilePath] -> IO FilePath
= do
buildMorning buildDir [helloPath] let remote = "remoteGit/morning/"
= init $ reverse $ dropWhile (/= '/') $ reverse helloPath
helloOutputDir ++ "/morning.c") (buildDir ++ "/morning.c")
copyFile (remote let gcc = "gcc -o " ++ buildDir ++ "/morning "
++ buildDir ++ "/morning.c "
++ "-L" ++ helloOutputDir ++ " -lhello "
++ "-I" ++ helloOutputDir
putStrLn gcc
system gccreturn $ buildDir ++ "/morning"
main :: IO ()
= do
main let helloDerivation = Derivation { -- hello 패키지의 default.nix가 생성할 drv모형
= "hello",
name = "hello.c",
source = [],
buildInputs = buildHello, -- buildPhase 모형
buildFunc = Nothing
outputPath
}= Derivation { -- morning 패키지의 default.nix가 생성할 drv모형
morningDerivation = "morning",
name = "morning.c",
source = [helloDerivation], -- hello 라이브러리 의존
buildInputs = buildMorning, -- buildPhase 모형
buildFunc = Nothing
outputPath
}= NixStore{ store = Data.Map.empty }
nixStore -- 빌드 명령 runNix nixStore morningDerivation