순수 함수형 패키지 매니저 닉스Nix (스케치 중)

Posted on December 12, 2024

뒤로 미뤄 두던 Nix를 결국 만져야 되는 상황이 왔습니다. 국내에도 쓰는 분들이 점점 늘어나는 추세인듯 하나, 한글 자료는 많지 않습니다. 아래는 전혀 완성되지 않은 글입니다. 아직 필요한 부분들을 확인하고 있는 중입니다. 아래는 순수하게 개인이 볼 노트인 상태로, 아직 다른 분들을 위해 정리하지 않았습니다. 주의해서 보세요.

닉스는 패키지 빌드와 관리를 위한 도구라고 하는데, 더 줄여서 얘기하면, 빌드 추상 레이어입니다. 빌드한 패키지들을 저장소에 잘 쟁여두고, 또 빌드할 일이 생기면 캐시되어 있는 것들을 잘 활용하는 빌더입니다. 그리고, “패키지”만을 위한 빌더가 아닙니다. 무엇이든, 현실의 것들을 빌드하기 위해 필요한(의존하는) 무언가들을 먼저 준비하고, 환경 변수 같은 것들도 설정하며, 필요한 환경을 만듭니다. 이렇게 빌드에 필요한 지침을 모아 둔 것을 derivation이라 부릅니다. 마치 현실의 대상을 derivation으로 바꿔 모형 세계를 만드는 것과 비슷합니다. 예를 들어, A패키지를 빌드하기 위해 B패키지가 필요하고, C설정이 있어야 하며, D도구들이 있어야 빌드 및 개발할 수 있는 상태일 때, A,B,C,D 각각이 빌드가 필요한 것들이면, 그 것들 역시 derivation으로 표현합니다. 이들을 모아 “개발 환경 준비”를 하는데, 이 “개발 환경 준비”도 하나의 derivation으로 표현합니다. 패키지뿐만 아니라 무언가가 빌드라는 과정이 필요하다면, 닉스를 적용할 수 있을지도 모릅니다.

닉스는 패키지 빌드에 필요한 모든 정보를 모아둔 derivation(일종의 명세서 같은 것)을 기반으로, 빌드, 개발 환경에 필요한 의존성들을 같이 관리하는 패키지 매니저입니다. derivation이 핵심 아이디어인데, 은근 오해하게 만드는 요소가 있어, 천천히 살펴 보도록 하겠습니다.

닉스 생태계의 공식 홈
nix 진영이 은근 nixnixos + 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를 골랐는데, 잘 고른 것 같습니다.

생각 스트레칭

make

Makefile

hello: hello.c
        gcc -o hello hello.c

파일 수정시간을 기반으로 의존성을 관리하고, 시스템에 설치된 컴파일러와 라이브러리를 사용합니다.

Nix

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들이) 같은 정보를 가질 수 있습니다. 이 것도 함수형 표현의 장점 중 하나겠습니다.)

Makefilehello를 먼저 빌드하고(닉스식으로 말하면 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를 불러옵니다.

hello = import ../hello { inherit pkgs; };

엄밀히 얘기하면 importhello.nixpkgs.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 패키지를 가져와 빌드합니다.

이제 nixpkgshello 패키지를 일부만 사용자화해서(수정해서) 빌드 해보겠습니다.

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) # (가)에 쓰이고 있는 툴들
    gnutar # (가).1 $gnutar
    gzip # (가).2 $gzip
    gnumake # (가).3 $gnumake
    gcc # (가).4 $gcc
    coreutils # (가).5 $coreutils
    gawk # (가).5 $gawk
    gnused # (가).5 $gnused
    gnugrep # (가).5 $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
  pkgs.callPackage ./path/to/A.nix {
    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의 반환값

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/

패키지명 앞의 문자열은 의존성 정보에 쓰이는 고유 해시값입니다.

만약 전역 위치에 패키지를 설치하면, 개발하고 있는 앱에서 이 패키지 의존성을 지정하지 않아도 개발자 머신에선 잘 돌아가고, 나중에 사용자는 안 돌아가는 사태가 발생합니다. 이를 막기 위해 패키지를 글로벌하게 설치하지 않습니다.

Garbage collection

λ> nix-env --uninstall firefox

설치 제거를 하면 바로 삭제하지 않습니다. 롤백을 원할 수도 있고, 다른 사용자의 프로필에 있는 패키지일 수도 있습니다.

사용하지 않는 패키지를 삭제하려면 Garbage collector를 돌립니다.

λ> nix-collect-garbage

참조 투명한 소스/바이너리 배포deployment

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 패키지를 가져오고, 없으면 빌드 단계로 들어갑니다.

Nix Packages collection

Nixpkgs는, 수천 개(만 개쯤)의 유닉스 패키지를 위한 많은 양의 Nix 표현식(derivation 생성식)을 제공합니다. 단순한 바이너리 패키지 모음이 아니라, 패키지를 빌드하는 방법, 패키지를 개발하고 빌드하기 위한 환경을 만드는 방법을 같이 가지고 있습니다.

nix.conf

보통 /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

NixOS

기본 아이디어는 시스템을 설정할 때, 커맨드라인 명령어를 쓴다든지, GUI 툴로 클릭하며 설정하지 않고, 모든 설정은 설정파일에 남기면, 이 설정 파일만 있다면 언제든지 동일한 환경을 만들 수 있을 겁니다.

닉스를 기반(닉스 언어로 시스템 설정을 표현하는)으로 하는 리눅스 배포판입니다. 패키지 매니징에만 Nix를 쓰는 게 아니라, 시스템 설정에도 씁니다. (ex. /etc에 있는 설정 파일들 빌드할 때) Nix로 시스템을 관리하면, 시스템 자체를 어떤 시점의 설정 상태로 편하게 롤백할 수도 있습니다.

리눅스를 기반으로 하고 있지만, 리눅스용으로 컴파일한 바이너리를 바로 실행할 수는 없습니다. 리눅스 바이너리를 NixOS용으로 패치하든가, 다른 도구들의 도움을 받아야 합니다.

https://nix.dev/tutorials/first-steps/ad-hoc-shell-environments

Ad hoc 쉘 환경

λ> 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의 예시 중에, 패키지를 buildInputsnativeBuildInputs 속성에 추가하는 것도 있습니다.  닉스쉘은 원래 “패키지 빌드 디버깅에 필요한 도구”를 가지고 있는 쉘 환경을 만들기 위해 나왔습니다. 처음 목적은 그랬지만, 지금은 임시 환경을 만드는 용도로도 사용합니다.
nix-shell을 실행하면, 현재 폴더에서 shell.nix 파일을 찾습니다. packages에 써 놓은 것들을 $PATH에서 보이게 해 줍니다. 닉스쉘을 켜둔 상태에서 shell.nix 파일을 수정해도 바로 반영되진 않습니다. 닉스쉘을 내렸다 다시 올리면 반영됩니다.

쉘 환경에 들어가기 직전에 실행하고 싶은 것들은 shellHook를 씁니다.

재현성을 높이기 위해 Nixpkgs 버전 고정

{ pkgs ? import <nixpkgs> {}
}:

닉스 패키지를 불러와서, 닉스 표현식을 실행하는 편리한 방법

However, the resulting Nix expression is not fully reproducible

닉스 패키지가 바뀔 수도 있으면, 결과가 항상 같은 건 아닙니다. 완벽하게 재현가능한 닉스 표현식을 만들려면, Nixpkgs의 버전을 고정해야 합니다.

import는 예약어가 아니라 보통의 함수다

좀 특이한데, import <nixpkgs> 이 자체가 하나의 함수고, 이 함수에 {...} 인자를 넘기는 모양입니다. 왜 이렇게 동작하냐면, import는 지정한 파일에서 닉스 표현식을 읽어와 반환합니다. 이 반환값이 함수인 경우엔 보통의 함수처럼 인자를 취하는 모양이 됩니다.

pkgs = import nixpkgs { config = {}; overlays = []; };

import nixpkgs 함수는 인자로 { config, overlays } 속성 집합을 받고 있습니다.

pkgs.mkShell { ... } 

mkShell도 마찬가지입니다. 함수가 { ... } 인자를 받는 모양입니다.

※ 닉스 언어에서 { x, y }: x + y 모양은, 속성 x, y를 가지고 있는 집합을 인자로 받는 람다 함수입니다.

사용자와 상호 작용interactive해서 평가

Lazy 평가 전략을 취합니다. WHNF에 머물지 않고, 모두 평가된 걸로 보려면, repl에서 :p를 붙입니다.
runHaskell처럼 닉스 파일을 바로 실행하는 방법도 있습니다.

nix-instantiate --eval file.nix

닉스 파일을 따로 지정하지 않으면, 현재 폴더의 default.nix파일을 읽습니다.

닉스 언어 특징

공백

렉시컬 토큰을 구분하는 구분자. 인덴트나 줄바꿈은 따로 의미를 가지지 않습니다.

이름name과 값value

Value는 닉스 언어의 프리미티브 데이터 타입, 리스트, 속성 집합, 함수가 될 수 있습니다.

재귀 속성 집합Recursive attribute set

rec { ... }
함수형 언어라서 {...} 속성 집합을 정의 중에, 집합내의 한 속성이 다른 속성에 접근하려면, recursive한 동작이 필요합니다. 왜 rec가 붙는지 좀 더 자세히 보려면 MonadFix를 참고하세요.

속성 접근

Attribute Set 이 튜플의 역할을 합니다.

let
  attrset = { x = 1; };
in
attrset.x
let
  attrset = { a = { b = { c = 1;};};};
in
attrset.a.b.c

익숙하지 않은 모양입니다. 아래와 같이 읽어 보면 조금 낫습니다.

attrset = { a = <thumb> };
                { 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가 들어오면 그 때 평가하면 됩니다.

with

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
with a; [x,y,z] # [ a.x, a.y, a.z]

with의 범위는 다음 다음 세미 콜론까지 (즉, 다음 표현식이 끝날 때까지), a의 속성을 현재 스코프로 가져 옵니다. - 이 것도 그다지 좋은 컨벤션을 도입한 것 같지 않지만, 타이핑을 줄여 주긴 합니다.

inherit

let
  x = 1;
  y = 2;
in
{
  inherit x y; # x = x; y = y;
}
{
  inherit (a) x y; # a.x = x; a.y = y;
}

같은 이름 쓰는 걸 줄여줍니다. 실제 코드를 보면, 인자를 그대로 다음 함수에게 넘기는 경우가 잦아 활용도가 높습니다.

문자열 interpolation

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의 속성을 뒤 이은 함수에게 자동으로 넘긴다.
}

Derivation

Nix building instructions
“마치 기계한테 이 설명서 따라해서 빌드해 줘” 라고 할 때, 이 설명서를 derivation이라 볼 수 있습니다. Nix(패키지 매니저)가 derivation을 해석해서 빌드한 후 nix/store에 결과물이 생기는 걸 realised라 합니다. 위와 같은 닉스 소스들은 mkDerivation같은 함수를 가지고 있고, 닉스가 이 표현식을 평가해서 derivation을 만들고, 이 derivation을 기반으로 패키지를 빌드합니다.

derivation에 있는 derivationderivation을 만들어 저장소에 derivation을 남긴다.”
= derivation 생성식에 있는 derivation 함수derivation을 만들어 derivation 파일(.drv)에 저장한다.”

@jhhuh님의 설명을 옮깁니다.

정리하면,

이 중 하나를 말하는 건데, 그냥 간단히 derivation은 닉스가 빌드할 수 있는 무언가라고 할 수 있겠습니다.

.drvnix/store 아래 임시 폴더에, 빌드 과정 중 임시로 파일로 생성된 후, 빌드가 끝나면 지워도 상관 없는 상태가 됩니다. 바로 지워지는 건 아닙니다. (나중에 GC를 돌리면 정리합니다.) nixpkgs가 가지고 있는 건 derivation이 아니라, derivation을 생성할 수 있는 닉스 표현식 (default.nix같은 것들)을 가지고 있습니다. 다른 곳에서 흔히 쓰이는 번역어는 아니지만, derivation 생성식 모음을 가지고 있다고 말하면 적당합니다.

Derivation은 빌드를 위한 레시피를 가지고 있는 특별한 파일입니다. 아래는 hello를 출력하는 c언어 프로젝트를 빌드하는 방법을 적어 놓은 .drv 파일입니다.

/nix/store/jkxk7lcnkhadd8rwq5n3z1gwbdcmqn0f-hello.drv

Derive([("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")])

해시 코드가 길게 있어 복잡하지만, 소스 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 derivationstore path에 추가되고, ouput paths에도 추가 됩니다.

system (String)
빌더 실행체executable?의 시스템 타입. builtins.currentSystem을 평가해서 현재 시스템 타입을 가져올 수 있습니다.

builder (Path | String)
빌드를 수행할 실행체 경로

writeShellScript (nixstore에 저장할 파일명) '' 파일 내용 '' 여기 경우엔, 빌드를 실행할 쉘 스크립트

''이 멀티 라인 문자열을 쿼트하는데 쓰입니다. 특이한 걸 골랐네요.

args (List of String)
builder 실행체에 넘길 인자

outputs (List of String)
nix store에 빌드 결과물을 저장하고, 해당 결과물을 심볼릭 링크합니다.

outputs = [ "lib" "dev" "doc"]

이렇게 잡아 두면, 예를 들어 Audoconf-style 패키지라면, 빌더는 아래 동작을 합니다.

./configure \
--libdir=$lib/lib \
--includedir=$dev/include \
--docdir=$doc/share/doc

derivation의 타입은, output이 하나만 있는 경우는 output:out, 여러 개가 있는 경우 output:<id>입니다. 흔히 사용되는 outputout, 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

mkDerivation

NixOS.kr 디스코드에 올렸는데, 다른 분들의 의견을 들어보려고 해커스펍에도 올렸습니다.

외국어를 익힐 때, 문법없이 실전과 부딪히며 배우는 방법이 더 좋기는 한데, 가끔은 문법을 따로 보기 전엔 넘기 힘든 것들이 있습니다. 닉스란 외국어를 익히는데도 실제 설정 파일을 많이 보는 것이 우선이지만, 가끔 “문법”을 짚고 넘어가면 도움이 되는 것들이 있습니다.

닉스 알약 (제목이 재밌네요. 알약) 글을 보면 mkDerivation속성 집합을 받아, 거기에 stdenv 등 기본적인 것을 추가한 속성 집합을 만들어 derivaition 함수에 넘기는 간단한 래핑 함수임을 직관적으로 잘 설명하고 있습니다.

그런데, 실제 사용 예시들을 만나면, mkDerivation에 속성 집합을 넘기지 않고, attr: {…} 형태의 함수를 넘기는 경우를 더 자주 만납니다. 그래서, 왜 그러는지 실제 구현 코드를 보고 이유를 찾아 봤습니다.

  mkDerivation =
    fnOrAttrs:
    if builtins.isFunction fnOrAttrs then
      makeDerivationExtensible fnOrAttrs
    else
      makeDerivationExtensibleConst fnOrAttrs;

mkDerivation정의를 보면 인자로 함수를 받았느냐 아니냐에 따른 동작을 분기합니다. 단순히, stdenv에서 가져온 속성들을 추가한다면, 함수를 인자로 받지 않아도 속성 집합을 병합해주는 //의 동작만 있어도 충분합니다.

{ a = 1; } // { b = stdenv.XXX; }

하지만, 함수로 받는 이유를 찾으면, 코드가 단순하지 않습니다. 아래는 함수를 받을 때 동작하는 실제 구현 일부를 가져 왔습니다.

makeDerivationExtensible =
    rattrs:
    let
      args = rattrs (args // { inherit finalPackage overrideAttrs; });
      ...

전체를 보기 전에 일단 args에서부터 머리가 좀 복잡해집니다. argsargs를 재귀 참조하고 있습니다. 보통 rattrs 매개 변수로는 아래와 같은 함수들이 들어 옵니다.

stdenv.mkDerivation (finalAttrs: {
  pname = "timed";
  version = "3.6.23";
  ...
    tag = finalAttrs.version;
}

(와, 해커스펍은 코드 블록에 ANSI가 먹습니다! 지원하는 곳들이 드문데요.)
코드를 바로 분석하기 전에, 닉스의 재귀 동작을 먼저 보면 좋습니다.

재귀 생각 스트레칭

nix-repl> let r = {a = 1; b = a;}; in r
error: undefined variable 'a'

위 동작은 오류지만, 아래처럼 rec를 넣어주면 가능합니다.

nix-repl> let r = rec {a = 1; b = a;}; in r 
{ a = 1; b = 1; }

rec동작은 Lazy 언어의 fix로 재귀를 구현하는 동작입니다. ※ 참고 Fix 함수

rec를 써서 속성 안에 정의 중인 속성에 접근할 수 있습니다. 그런데, 아래 같이 속성을 r.a 로 접근하면, rec 없이도 가능해집니다.

nix-repl> let r = {a = 1; b = r.a;}; in r
{ a = 1; b = 1; }

닉스 언어의 Lazy한 특성 때문에 가능합니다.

이제, 원래 문제와 비슷한 모양으로 넘어가 보겠습니다. 아래같은 형태로 바로 자기 자신에 재귀적으로 접근하면 무한 재귀 에러가 나지만,

nix-repl> let x = x + 1; in x
       error: infinite recursion encountered

아래처럼 람다 함수에 인자로 넘기면 얘기가 달라집니다.

nix-repl> let args = (attr: {c = 1; b = attr.a + attr.c + 1;}) (args // { a = 1; }); in args
{ 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;

뭐하러, 이런 복잡한 개념을 쓰는가 하면, 단순히 속성 추가가 아니라, 기존 속성이 앞으로 추가 될 속성을 기반으로 정의되어야 할 때는 이렇게 해야만 합니다. 함수형 언어에선 자주 보이는, 미래값을 가져다 쓰는 재귀 패턴인데, 저는 아직 그리 익숙하진 않습니다.

nix-shell

Flake를 활성화한 뒤의 nix shell과 구별해야 합니다.
derivation을 해석해서 패키지 자체가 아니라, 개발 환경에 필요한 툴들을 준비하고 의존성만 빌드합니다. 목적이 특정 앱을 빌드하는 게 아니라, 앱 빌드를 위한 환경만을 준비한다는 뜻입니다.

nix-build

.nix -> .drv

default.nix 파일을 닉스 패키지 매니저가 읽어 들여, derivation 생성식을 평가해서, 사람 말고 기계를 위한 derivation을 가진 .drv 파일이 만들어집니다. (꼭 사람이 못 읽는 건 아니고, 빌드에 필요한 모든 정보를 모은 리스트 모양입니다.)

.drv파일은 아래 명령어로 직접 만들 수 있습니다.

> nix-instantiate default.nix 

.drv -> 패키지

.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을 돌려주는 함수일 경우는 동작하지 않습니다.

이런 동작으로 인해, 처음 실전 코드를 보면 모양이 다른 두 가지가 나와, 혼란을 주는 것 같습니다.

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 ::
    { system   : String
    , name     : String
    , builder  : Path | Derivation
    , ?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-...";
  }
}

namepnameversion을 써주면 자동으로 만든다. 이 때 속성들이 자신들이 속한 집합에 있는 다른 속성을 참조하려면 rec가 필요합니다. ※ MonadFix 참고

stdenv.mkDerivation rec {
  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 { };
}

callPackagehello.nix 파일이 돌려주는 “함수”가 필요로 하는 인자값 stdenv, fetchzip을 넣어 줍니다. 값을 넣어 주면, 또 함수를 돌려 주는데, 거기에 { }를 넘기고 있습니다.

callPackage함수는 derivation 생성 함수를 호출apply하는 함수입니다. 인자로 받은 표현식(위에선 파일이 가진 derivation 함수)를 실행해서 최종 derivation을 얻습니다. derivation함수가 필요로 하는 stdenv, gcc 같은 걸 자동으로 전달합니다. 패키지를 호출한다는 이름이 어색하긴 한데, 패키지 derivation을 정의하는 derivation 함수를 호출한다고 보면 됩니다.

lib.costomisation.nix

callPackage = callPackageWith pkgs; # 아래 autoArgs를 pkgs로 고정하는 래퍼 
callPackageWith = autoArgs: fn: args: ... mkAttrOverridable f allargs;

위 예시를 대입해서 읽어 보면, ./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

결국 mkDerivationcallPackage의 차이는,
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-env로 설치하면, 해당 사용자만 쓸 수 있습니다. 프로젝트별 환경을 만드는 것이 아니라, 현재 사용자 환경에만 설치하니, 현재 사용자의 모든 프로젝트에 영향을 미칩니다. 프로젝트별 환경을 위해서는 nix-shell 또는 direnv 같은 툴을 쓰고, 프로젝트별 shell.nix를 생성하는 방식을 씁니다.

용도 명령어
패키지 검색 nix search nixpkgs packagename
패키지 설치 nix-env -iA packagename
설치 목록 nix-env -q
패키지 제거 nix-env -e packagename
패키지 업데이트 nix-env -u

nix-overlay

nixpkgs에 사용자 정의 패키지를 추가하는 방법입니다. Overlay 폴더를 추가하고, 여기에 default.nix 파일을 만들어 두면 nix-shell이나 nix-build 명령어를 실행할 때 Overlay를 참조합니다.

Nixpkgs를 “확장”하거나 “변경”할 때 overlays를 사용합니다.

Nix Channels

닉스 패키지는 여러 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

Nix store

derivation이나 닉스가 빌드한 패키지들이 위치하는 곳으로 읽기 전용입니다. 보통 /nix/store.
/nix/store/nawl092prjblbhvv16kxxbk6j9gkgcqm-git-2.14.1 이런 앱이름 앞의 해시는, 빌드할 때 써 먹은 모든 입력값(소스, 의존성 트리,컴파일 플래그 등)을 기반으로 만든 값입니다. 같은 버전을 여러 빌드에서 써먹으면, 한 번 설치된 것에 심볼릭 링크를 걸어서 씁니다.

Profiles

닉스 스토어 항목들을 프로필에 심볼릭 링크를 겁니다. 프로필에 변화를 주면, 버전 개념의 generation을 유지해서, 과거 generation으로 롤백할 수 있는 기능이 있습니다. ~/.nix-profile/ 아래에 있는 bin, etc, lib, share 폴더 안에는 /nix/store에 있는 것들과 심볼릭 링크한 것들로 가득 차 있습니다.

/nix/var/nix/profiles

닉스OS에서 리눅스 바이너리 사용하기

닉스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를 쓸 수 있다고 합니다.

nix-ld

설정 파일에 아래를 넣으면 좀 더 친절한 에러 메시지가 나옵니다. 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

nix-index

커뮤니티에서 만든 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

nix-locate

특정 파일을 가지고 있는 패키지 찾기

닉스OS 설정 파일

{ config, pkgs, ... }: # 인자 두 개를 받는 함수다. 
# 아래는 옵션=값 형태의 집합
{ services.httpd.enable = true; 
  services.httpd.adminAddr = "alice@example.org";
  services.httpd.virtualHosts.localhost.documentRoot = "/webroot";
}

그럼 누군가는 위 함수를 부른다는 거겠지요? 위 함수에 config, pkgs 매개 변수에 인자를 넘기면, 인자 값에 따라 {옵션=값}을 돌려준다고 보면 되겠습니다. @TODO pkgsnixpkgs가 넘어 오겠지?

저널 로그 보는 방법

journalctl -p 0..3 -x

systemdjournal이라는 바이너리로 로그를 저장합니다. -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 오피셜 드라이브로 바꾸고 문제는 사라진 것 같습니다.

flatpak

@TODO

Nix home-manager

시스템 상태를 선언적으로, 즉 configuration.nix 파일에 모두 모아둘 수 있다면, 현실적으로 configuration.nix 파일만 들고 다니면, 똑같은 시스템을 “재현”할 수 있습니다. 하지만, DB에 들어 있는 설정에 필요한 정보들은 닉스 패키지 매니저로 관리할 수 없고, 사용자 디렉토리에 있는 dotfiles들도 그렇습니다. 완벽하게, 한 큐에 재현가능한 메커니즘을 만들기는 어렵습니다.

사용자 디렉토리는, 시스템과 별개인 사용자 데이터들이 모인 중요한 디렉토리입니다. 이 디렉토리를 시스템과 묶어서 특정 스냅샷으로 되돌리거나 하면 난리 날 것입니다.

이런 위험을 신경쓰지 않고, 사용자 디렉토리에서 “시스템 설정에 필요한 정보”만 따로 잘 컨트롤하기 위해 home-manager를 도입했습니다.

튜토리얼에선, desktop environment 뿐만 아니라 development environment, compilation environment, cloud virtual machine, 컨테이너 이미지 구성 들도 관리한다고 표현하고 있습니다. 이런 목적으로 NixOps, colmena 란 툴들 있는 것 같습니다.

기존 /etc/nix/configuration.nix는 flake.nix의 모듈 정의를 따르고 있어, 그대로 모듈로 불러오면 됩니다.

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.nixnix.gc 옵션을 통해 자동 삭제automatic garbage collection를 지정할 수 있습니다.

최신 패키지가 아직 nixpkgs에는 안 올라왔다면

예를 들어, 최신 디스코드가 아직 nixpkgs에는 안 올라왔다면
nix-shell을 써서 임시로 설치하든가,
nix-env로 사용자 환경에서만 설치하든가,
overlaysnixpkgs에 있는 discord를 덮어 씌우든가 할 수 있습니다.

flakes가 활성화된 상태에서 default.nix만 있는 프로젝트 빌드

flakes가 활성화된 상태인데, 찾은 패키지가 flakes는 제공하지 않고, default.nix만 제공할 경우

nix build --no-flake -f default.nix
nix build -f '<nixpkgs>' <패키지명>

닉스 시스템 설정을 제어할 수 있는 환경 변수

Common Environment Variables

NAR 파일

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
build nixStore derivation = do
  putStrLn $ "Building " ++ name derivation ++ "..."

  buildInputsPath <- mapM (\dep -> case outputPath dep of
    Just path -> return path
    Nothing -> do -- 아직 빌드하지 않았다면
      nixStore' <- build nixStore dep
      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
  createDirectoryIfMissing True buildDir
  outputPath' <- (buildFunc derivation) buildDir buildInputsPath
  let store' = Data.Map.insert (name derivation) outputPath' (store nixStore)
  putStrLn $ (name derivation) ++ " built at " ++ outputPath'
  return NixStore { store = store' }
     
runNix :: NixStore -> Derivation -> IO ()
runNix nixStore derivation = do
  nixStore' <- build nixStore derivation
  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
buildHello buildDir _ = do -- 어딘가에서 소스를 가져오는 것을 모형화
  let remote = "remoteGit/hello/"
  copyFile (remote ++ "/hello.c") (buildDir ++ "/hello.c")
  copyFile (remote ++ "/hello.h") (buildDir ++ "/hello.h")
  system $ "gcc -c -o " ++ buildDir ++ "/hello.o " ++ buildDir ++ "/hello.c"
  system $ "ar rcs " ++ buildDir ++ "/libhello.a " ++ buildDir ++ "/hello.o"
  return $ buildDir ++ "/libhello.a"

buildMorning :: FilePath -> [FilePath] -> IO FilePath
buildMorning buildDir [helloPath] = do
  let remote = "remoteGit/morning/"
      helloOutputDir = init $ reverse $ dropWhile (/= '/') $ reverse helloPath
  copyFile (remote ++ "/morning.c") (buildDir ++ "/morning.c")
  let gcc = "gcc -o " ++ buildDir ++ "/morning " 
                     ++ buildDir ++ "/morning.c " 
                     ++ "-L" ++ helloOutputDir ++ " -lhello "
                     ++ "-I" ++ helloOutputDir
  putStrLn gcc
  system gcc
  return $ buildDir ++ "/morning"

main :: IO ()
main = do
  let helloDerivation = Derivation { -- hello 패키지의 default.nix가 생성할 drv모형 
        name = "hello",
        source = "hello.c",
        buildInputs = [],
        buildFunc = buildHello, -- buildPhase 모형
        outputPath = Nothing
      }
      morningDerivation = Derivation { -- morning 패키지의 default.nix가 생성할 drv모형 
        name = "morning",
        source = "morning.c",
        buildInputs = [helloDerivation], -- hello 라이브러리 의존
        buildFunc = buildMorning, -- buildPhase 모형
        outputPath = Nothing
      }
      nixStore = NixStore{ store = Data.Map.empty }
  runNix nixStore morningDerivation -- 빌드 명령
Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com