뒤로 미뤄 두던 Nix를 결국 만져야 되는 상황이 왔습니다. 국내에도 쓰는 분들이 점점 늘어나는 추세인듯 하나, 한글 자료는 많지 않습니다. 아래는 전혀 완성되지 않은 글입니다. 아직 필요한 부분들을 확인하고 있는 중입니다. 아래는 순수하게 개인이 볼 노트인 상태로, 아직 다른 분들을 위해 정리하지 않았습니다. 주의해서 보세요.
닉스는 패키지 빌드와 관리를 위한 도구라고 하는데, 더 줄여서 얘기하면, 빌드 추상 레이어입니다. 빌드한 패키지들을 저장소에 잘 쟁여두고, 또 빌드할 일이 생기면 캐시되어 있는 것들을 잘 활용하는 빌더입니다. 그리고, “패키지”만을 위한 빌더가 아닙니다. 무엇이든, 현실의 것들을 빌드하기 위해 필요한(의존하는) 무언가들을 먼저 준비하고, 환경 변수 같은 것들도 설정하며, 필요한 환경을 만듭니다. 이렇게 빌드에 필요한 지침을 모아 둔 것을 derivation이라 부릅니다. 마치 현실의 대상을 derivation으로 바꿔 모형 세계를 만드는 것과 비슷합니다. 예를 들어, A패키지를 빌드하기 위해 B패키지가 필요하고, C설정이 있어야 하며, D도구들이 있어야 빌드 및 개발할 수 있는 상태일 때, A,B,C,D 각각이 빌드가 필요한 것들이면, 그 것들 역시 derivation으로 표현합니다. 이들을 모아 “개발 환경 준비”를 하는데, 이 “개발 환경 준비”도 하나의 derivation으로 표현합니다. 패키지뿐만 아니라 무언가가 빌드라는 과정이 필요하다면, 닉스를 적용할 수 있을지도 모릅니다.
닉스는 패키지 빌드에 필요한 모든 정보를 모아둔 derivation(일종의 명세서 같은 것)을 기반으로, 빌드, 개발 환경에 필요한 의존성들을 같이 관리하는 패키지 매니저입니다. derivation이 핵심 아이디어인데, 은근 오해하게 만드는 요소가 있어, 천천히 살펴 보도록 하겠습니다.
※ 닉스 생태계의 공식 홈
nix 진영이 은근 nix와 nixos + nixpkgs가 나뉘어 있는 것 같습니다. (두 진영이 친하지 않다는 소문도 들어 봤습니다.) 아래 두 사이트가 공식 홈인데, 양 쪽 모두에 nix, nixos, nixpkgs 공식 매뉴얼이 있는 듯 하지만,
nixos.org - nixpkgs 공식 매뉴얼, nixos 공식 매뉴얼
nix.dev - nix 공식 매뉴얼
위와 같이 각각 관리하고 있고, 서로 링크를 걸고 있습니다.
그런데, 혼란스럽게도 공식 리포는 모두 Github NixOS 계정 아래 있습니다.
NixOS/nix
NixOS/nix.dev
NixOS/nixpkgs
(NixOS는 nixpkgs에서 같이 관리하는 것으로 보입니다.)
구글링으로 막 검색해서 공식 자료들을 볼 때 좀 산만한 느낌이 드는 이유가 이래서 그랬나 봅니다. NixOS를 쓰지 않고, Nix 패키지 매니저만 쓴다해도, nixpkgs 관련 자료를 봐야 하니, NixOS쪽 자료들도 같이 보게 됩니다.
이런 닉스 진영의 굵직한 기둥들의 배치를 먼저 알고 입문해도 좋겠습니다.
어느 문서를 보며 시작할까 고민이 됐었는데요. nix-community를 방문하면, 커뮤니티에서 관리하는 리포들을 볼 수 있는데, 이 중 볼만한 문서들을 추려놓은 awesome-nix에서 적당한 것을 고르면 되겠습니다. 저는 Nix-pills를 골랐는데, 잘 고른 것 같습니다.
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만 알려 주고,
# hello를 빌드하기 전에 "먼저 빌드되어야 한다"를 표시하는 것일 뿐,
# 이게 어떤 식으로 의존하는지는 별도로 써줘야 합니다.
# 아래 `-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들이) 같은 정보를 가질 수 있습니다. 이 것도 함수형 표현의 장점 중 하나겠습니다.)
Makefile
은 hello
를 먼저 빌드하고(닉스식으로 말하면 realize하고), 그 다음 morning
을 빌드합니다. Nix는 hello
를 빌드하기 위한 함수와 morning
을 빌드하기 위한 함수를 먼저 합성한 후 realize하게 됩니다.
default.nix
도, mkDerivation
도 이펙트가 없는 순수 함수입니다. (사실, 사용자에게 드러나는 부분은 순수한 모양이지만, 내부 빌드 과정에서 외부와 상호 작용하는 부분이 있다고 합니다.) 예를 들어, 기존에 있던 프로젝트가 빌드할 때 환경 변수를 통해 정보를 받는 걸, 닉스 빌드로 변환할 때, 필요한 정보를 명시부터 하게 해서 순수한 인터페이스를 유지하고, 이를 기존 패키지를 위해 임시 환경 변수로 잡는 방법을 써서라도 순수한 모양으로 바꿉니다. ※ mkDerivation
을 실행하면, .drv
파일을 만들고, 닉스 저장소에 저장합니다. 이 후 nix build
나, nix-env
가 이 derivation을 명세서 삼아 실제 빌드해서 파일을 만들어 냅니다.
위 특징을 더 잘표현 하려면, 선언형이란 말보다 유연한 조합형이란 말이 더 적합할 수도 있겠습니다. 어디까지나 개인적인 생각입니다.
위 얘기는 로컬 패키지 빌더로서의 닉스 얘기고 (엄밀히 말하면 빌드를 위한 추상 레이어쯤 되겠습니다. 예를 들어 C프로젝트는 여전히 gcc
, make
를 써서 빌드합니다.), 닉스는 소스 기반 패키지 매니저라 부릅니다. rpm
, apt
처럼 컴파일된 바이너리들 의존성을 관리하며 설치하는 것이 아닌, 빌드 방법만 기술한 derivation들의 의존성을 엮어놓고, 필요한 순간이 오면 빌드하여 바이너리를 만들게 됩니다.
가장 기본 동작은 아래와 같습니다.
# hello.nix
let pkgs = import <nixpkgs> {};
in pkgs.hello
> nix-build
위 명령어는 hello
패키지를 빌드하는데 필요한 정보를 가진 derivation을 생성하고, 이를 바탕으로 패키지를 빌드합니다. 다른 패키지에서 hello
패키지를 아래와 같이 의존성으로 불러 오는데 같은 nixpkgs
에 기반으로 할 수 있도록, nixpkgs
를 인자로 받도록, 람다 함수로 바꿉니다.
# 인자로 nixpkgs 특정 버전이 넘어 오지 않으면,
# 시스템 환경 변수 NIX_PATH에 잡혀 있는 nixpkgs 버전을 쓰겠다는 말입니다.
# 환경 변수에 접근하는 건 side effect입니다. 순수함을 강조하는 닉스가 시작부터 effect가 존재하네요.
{ pkgs ? import <nixpkgs> {} }:
pkgs.hello
위와 같이 nixpkgs
를 인자로 받게 하고, 다음처럼 hello
를 불러옵니다.
import ../hello { inherit pkgs; }; hello =
엄밀히 얘기하면 import
한 hello.nix
의 pkgs.hello
는 평가가 끝나고 나온 결과물 derivation이 아직 아닙니다. nixpkgs
는 .drv
를 생성할 표현식, 즉 derivation이 아니라, derivation 생성식들의 모음입니다. 해당 식을 평가하면(nix-instantiate
) derivation을 만들어내고, 이 걸, 나중에 또 계산하지 않기 위해 /nix/store
에 .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 생성 함수에 넘겨, 사용자가 원하는 빌드를 할 수 있습니다.
아래 예시는 Nix Pills에서 가져 왔습니다.
hello_builder.sh
export PATH="$gnutar/bin:$gcc/bin:$gnumake/bin:$coreutils/bin:$gawk/bin:$gzip/bin:$gnugrep/bin:$gnused/bin:$bintools/bin" # (가)
tar -xzf $src # (가).1 (가).2
cd hello-2.12.1
./configure --prefix=$out # (가).5
make # (가).3 (가).4
make install
hello.nix
에서는 위 빌드 스크립트를 돌리기 위해서 필요한 환경들을 모두 명시해 줘야 합니다. 위의 각 단계에 필요한 패키지를 연관시켜 보았습니다.
let
pkgs = import <nixpkgs> { };
in
derivation {
name = "hello";
builder = "${pkgs.bash}/bin/bash";# shebang처럼 스크립트 해석기 지정 역할
args = [ ./hello_builder.sh ];
inherit (pkgs) # (가)에 쓰이고 있는 툴들
# (가).1 $gnutar
gnutar # (가).2 $gzip
gzip # (가).3 $gnumake
gnumake # (가).4 $gcc
gcc # (가).5 $coreutils
coreutils # (가).5 $gawk
gawk # (가).5 $gnused
gnused # (가).5 $gnugrep
gnugrep ;
bintools = pkgs.binutils.bintools; # ld, ar 같은 툴들
src = ./hello-2.12.1.tar.gz; # $src
system = builtins.currentSystem;
※ 새로운 번역어를 억지로 만들어내는 건 저도 별로 선호하지 않습니다만, 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 = ...;
}
하나의 derivation
을 반환하고 있으면, 그 걸 instantiate
하면 되고, 속성 집합으로 여러 derivation
을 반환하고 있으면 아래와 같이 어떤 걸 instantiate
할지 지정해 주어야 합니다.
> nix-instantiate -A attr1
닉스 패키지 매니저 설치부터 까다롭습니다. 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
derivation의 타입은, output
이 하나만 있는 경우는 output:out
, 여러 개가 있는 경우 output:<id>
입니다. 흔히 사용되는 output
은 out
, dev
, doc
, man
, bin
등이 있습니다.
derivation {
name = "example";
outputs = [ "lib" "dev" "doc" "out" ];
}
각각에 대해 별도의 store 경로를 생성합니다.
/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
만 있습니다.
{
stdenv.mkDerivation ...
outputs = [ "out" "dev" "doc" ];
...
}
※ 왜 용어로 derivation을 골랐을까?
derive라 하면 무언가를 만드는 과정에서 가지쳐 나온 결과물 같은 느낌입니다. 예를 들어 A
,B
,C
로 결과물가
를 만드는데, A
,b
,C
로 조금 설정을 바꾸면 다른 결과물 나
가 나오는 걸 상상할 수 있습니다. 이럴 때 나
는 파생물이라 불러도 될 것 같지만, 조금 어색합니다.
우리말로 하면 파생문, 유도문 정도 되겠지만, 익숙하지 않으니 일단은 안쓰기로 합니다. 고유한 개념을 지칭하는 거라 번역 안하는 게 맞을 것 같기도 합니다.
원래 이펙트 가득한 패키지 빌드를 단순히 집합(혹은 리스트)으로 표현하고 있어 보통의 값처럼 함수 인자로 넘기고, 결과로 출력할 수 있습니다. 당연한 듯 보이는 이 문장에 닉스의 핵심 아이디어가 들어가 있습니다. 패키지 빌드에 꼭 개입해야 하는 이펙트들을 나몰라라 하고 순수하게 조합하다가, 조합이 끝나서 패키지를 빌드해야 되는 순간이 오면 그 때 realize해서 패키지가 되도록 합니다. 마치, 모나드들의 runner처럼 볼 수 있습니다.
추가 - 몇 달 공부하다 보니, 만일 derivation의 번역어가 자리 잡는다면, 파생물보다는, 유도문이 더 뜻을 잘 표현하지 않나 혼자 생각입니다. 닉스 패키지 매니저에서 derivation은 “부산”물이 아닌 주인공으로, 가지쳐서 나온다는 느낌의 파생과 다른 느낌입니다. 유도”물”이 아닌 “문”인 이유는, derivation은 실체라기보단, 실체를 선언해 놓은 문서에 가까운 느낌입니다.
비유하자면, 화살표 유도등을 따라가다 보면, 목적지(패키지)에 도달합니다.
NixOS.kr에 올렸던 내용을 추가합니다.
callPackage = f: args: f (pkgs // args);
Nix Pills에선 좀 더 설명적인 코드로 예를 들어 줍니다.
callPackage = set: f: overrides: f ((builtins.intersectAttrs (builtins.functionArgs f) set) // overrides)
@TODO
NixOS.kr 디스코드에 올렸는데, 다른 분들의 의견을 들어보려고 해커스펍에도 올렸습니다.
외국어를 익힐 때, 문법없이 실전과 부딪히며 배우는 방법이 더 좋기는 한데, 가끔은 문법을 따로 보기 전엔 넘기 힘든 것들이 있습니다. 닉스란 외국어를 익히는데도 실제 설정 파일을 많이 보는 것이 우선이지만, 가끔 “문법”을 짚고 넘어가면 도움이 되는 것들이 있습니다.
닉스 알약 (제목이 재밌네요. 알약) 글을 보면 mkDerivation
이 속성 집합을 받아, 거기에 stdenv 등 기본적인 것을 추가한 속성 집합을 만들어 derivaition
함수에 넘기는 간단한 래핑 함수임을 직관적으로 잘 설명하고 있습니다.
그런데, 실제 사용 예시들을 만나면, mkDerivation
에 속성 집합을 넘기지 않고, attr: {…} 형태의 함수를 넘기는 경우를 더 자주 만납니다. 그래서, 왜 그러는지 실제 구현 코드를 보고 이유를 찾아 봤습니다.
mkDerivation =fnOrAttrs:
if builtins.isFunction fnOrAttrs then
makeDerivationExtensible fnOrAttrselse
makeDerivationExtensibleConst fnOrAttrs;
mkDerivation
의 정의를 보면 인자로 함수를 받았느냐 아니냐에 따른 동작을 분기합니다. 단순히, stdenv에서 가져온 속성들을 추가한다면, 함수를 인자로 받지 않아도 속성 집합을 병합해주는 //
의 동작만 있어도 충분합니다.
{ a = 1; } // { b = stdenv.XXX; }
하지만, 함수로 받는 이유를 찾으면, 코드가 단순하지 않습니다. 아래는 함수를 받을 때 동작하는 실제 구현 일부를 가져 왔습니다.
makeDerivationExtensible =rattrs:
let
args = rattrs (args // { inherit finalPackage overrideAttrs; });
...
전체를 보기 전에 일단 args
에서부터 머리가 좀 복잡해집니다. args
가 args
를 재귀 참조하고 있습니다. 보통 rattrs
매개 변수로는 아래와 같은 함수들이 들어 옵니다.
(finalAttrs: {
stdenv.mkDerivation pname = "timed";
version = "3.6.23";
...tag = finalAttrs.version;
}
(와, 해커스펍은 코드 블록에 ANSI가 먹습니다! 지원하는 곳들이 드문데요.)
코드를 바로 분석하기 전에, 닉스의 재귀 동작을 먼저 보면 좋습니다.
재귀 생각 스트레칭
-repl> let r = {a = 1; b = a;}; in r nixerror: undefined variable 'a'
위 동작은 오류지만, 아래처럼
rec
를 넣어주면 가능합니다.-repl> let r = rec {a = 1; b = a;}; in r nix{ a = 1; b = 1; }
※
rec
동작은 Lazy 언어의fix
로 재귀를 구현하는 동작입니다. ※ 참고 Fix 함수
rec
를 써서 속성 안에 정의 중인 속성에 접근할 수 있습니다. 그런데, 아래 같이 속성을r.a
로 접근하면,rec
없이도 가능해집니다.-repl> let r = {a = 1; b = r.a;}; in r nix{ a = 1; b = 1; }
닉스 언어의 Lazy한 특성 때문에 가능합니다.
이제, 원래 문제와 비슷한 모양으로 넘어가 보겠습니다. 아래같은 형태로 바로 자기 자신에 재귀적으로 접근하면 무한 재귀 에러가 나지만,
-repl> let x = x + 1; in x
nixerror: infinite recursion encountered
아래처럼 람다 함수에 인자로 넘기면 얘기가 달라집니다.
-repl> let args = (attr: {c = 1; b = attr.a + attr.c + 1;}) (args // { a = 1; }); in args
nix{ b = 3; c = 1; }
여기서 속성b
의 정의 동작이 중요합니다.
attr:
{ c = 1;
b = attr.a + attr.c + 1;
}
속성 b
는 아직 알 수 없는, 미래의 attr에 있는 a를 받아 써먹고,
원래는 rec
없이 접근하지 못했던, c
에도 attr.c로 접근이 가능합니다.
원래 문제로 다시 설명하면, mkDerivation
에 넘기고 있는, 사용자 함수 finalAttrs: { ... }
에서,
닉스 시스템이 넣어주는 stdenv 값 같은 것들과 사용자 함수내의 속성들을 섞어서 속성 정의를 할 수 있다는 얘기입니다. 아래처럼 말입니다.
tag = finalAttrs.version;
뭐하러, 이런 복잡한 개념을 쓰는가 하면, 단순히 속성 추가가 아니라, 기존 속성이 앞으로 추가 될 속성을 기반으로 정의되어야 할 때는 이렇게 해야만 합니다. 함수형 언어에선 자주 보이는, 미래값을 가져다 쓰는 재귀 패턴인데, 저는 아직 그리 익숙하진 않습니다.
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(자동으로 프로젝트 디렉토리 구조를 파악한다)를 기반으로 빌드합니다.
Nix Pill - Nixpkgs Parameters
저는, 실전 코드들로만 감을 잡으려고 할 때, 살짝 방해가 됐던 동작들입니다. nix-build
는
※ 함수를 호출한 결과가, 또 derivation을 돌려주는 함수일 경우는 동작하지 않습니다.
이런 동작으로 인해, 처음 실전 코드를 보면 모양이 다른 두 가지가 나와, 혼란을 주는 것 같습니다.
https://nix.dev/tutorials/packaging-existing-software
Zero to Nix - derivation
아래는 derivation
함수의 실제 구현 코드에 있는 주석입니다.
# ‘derivation’ 내장 함수의 구현이다.
# ‘derivationStrict’ primop 함수 래퍼.
# 다음 주석은 repl에서 :doc 로 볼 수 있다.
/**
Create a derivation.
# Inputs
하나의 무엇을 어떻게 빌드할지를 기술하는 속성셋을 인자로 받는다.
See https://nix.dev/manual/nix/2.23/language/derivations
# Output
결과는 derivation을 기술하는 속성셋이다.
속성셋에는 outputs가 포함되어 있는데, 아직 존재하지 않는 출력 경로를 가리키는 문자열이다.
이 outputs는 필요할때만 realise된다.
* `nix-build`류 명령어를 실행하면, 명령줄에서 요청한 outputs를 realise한다.
See https://nix.dev/manual/nix/2.23/command-ref/nix-build
* `import`, `readFile`, `readDir`같은 함수들이 불렸을 때, 의존하는 outputs를 realise한다.
import from derivation 개념이라 불린다.
See https://nix.dev/manual/nix/2.23/language/import-from-derivation
`derivation`은 최소한의 기능만 제공하고, 빌드 중에 쓸 수 있는 명령어는 거의 없다.
대부분의 경우 이 함수를 직접 쓰지 않고,
기본 적인 빌드 환경을 자동으로 추가해주는 `stdenv.mkDerivation`을 사용하는 것이 좋다.
*/
※ 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가 있어야 가능하다.
}
정의 중인 속성을 참고하고 있어 rec
가 필요할 거라 생각했는데, mkDerivation
소스를 보면 자체적으로 fix
동작을 하고 있어, 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
생성 함수를 호출apply하는 함수입니다. 인자로 받은 표현식(위에선 파일이 가진 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>' <패키지명>
Nix ARchive. tar
같은 기존 아카이빙 툴이 Nix의 요구사항을 만족하지 않았다. 기존 툴들은 padding을 추가하고, 파일을 정렬하지 않고, 타임 스탬프를 추가하는 등, 비트가 같은 디렉토리를 아카이빙해도 다른 아카이브가 만들어질 수 있다. 닉스는 비트가 같은 디렉토리는 같은 해시를 가진 아카이브가 만들어져야 한다.
store 경로에서 NAR 아카이브 만들기
nix-store --dump
nix-store --restore
아래는 닉스 동작을 가늠해 볼 수 있는 초단순 모형 코드입니다. (검증 필요)
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