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

Posted on December 12, 2024

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

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

생각 스트레칭

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만 알려 주는 것이고, 
  # 이게 어떤 식으로 의존하는지는 별도로 써줘야 합니다.
  # 아래 `-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 언어고 단순 바인딩일 뿐 실제로 변환이 일어나는 건 아닙니다.

Makefilehello를 먼저 빌드하고(닉스식으로 말하면 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/storehello 패키지 빌드에 필요한 정보를 가진 derivation을 담은 hello.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을 만들어 내는 닉스 표현식을 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 = ...;
}

닉스 패키지 매니저 설치

닉스 패키지 매니저 설치부터 까다롭습니다. 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

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처럼 볼 수 있습니다.

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(자동으로 프로젝트 디렉토리 구조를 파악한다)를 기반으로 빌드합니다.

Derivation 생성

https://nix.dev/tutorials/packaging-existing-software
Zero to Nix - derivation
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가 있어야 가능하다.
}

인자를 하나 하나 살펴 보려하니 너무 많습니다. 자주 보이는 것 위주로 보고 넘어가야겠습니다.

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 생성 함수를 호출하는 함수입니다. 인자로 받은 표현식(위에선 파일이 가진 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

닉스 모형 코드

아래는 닉스 동작을 가늠해 볼 수 있는 초단순 모형 코드입니다. (검증 필요)

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