로컬 파일 다루기#

Author:

Silvan Mosberger @infinisil

로컬 프로젝트를 Nix derivation으로 빌드하려면 해당 derivation의 builder 실행 파일이 소스 파일에 접근할 수 있어야 합니다. 기본적으로 builder는 Nix 스토어에서 읽기만 허용하는 격리된 환경에서 실행됩니다. Nix 언어에는 로컬 파일을 스토어에 복사하고 그 결과로 생성된 스토어 경로를 노출하는 내장 기능이 있습니다.

하지만 이러한 기능을 직접 사용하는 것은 까다로울 수 있습니다.

  • 널리 쓰이는 src = ./. 패턴처럼 경로를 문자열로 강제 변환하면 derivation이 현재 디렉터리의 이름에 의존하게 됩니다. 또한 이 방식은 불필요한 파일까지 포함하여 디렉터리 전체를 항상 스토어에 추가하므로, 그런 파일이 변경될 때마다 불필요한 새 빌드가 발생합니다.

  • builtins.path 함수(그리고 이와 동등한 lib.sources.cleanSourceWith)로 이러한 문제를 해결할 수 있습니다. 그러나 filter 함수 인터페이스로는 원하는 경로 선택을 표현하기 어려운 경우가 많습니다.

이 튜토리얼에서는 Nixpkgs의 lib.fileset 라이브러리를 사용하여 derivation에서 로컬 파일을 다루는 방법을 배웁니다. 이 라이브러리는 내장 기능을 추상화하여 더 안전하고 편리한 인터페이스를 제공합니다.

파일 집합#

파일 집합은 로컬 파일의 모음을 나타내는 데이터 타입입니다. 파일 집합은 라이브러리의 다양한 함수로 생성하고, 조합하고, 조작할 수 있습니다.

nix repl로 라이브러리를 탐색하며 배울 수 있습니다.

$ nix repl -f channel:nixos-23.11
...
nix-repl> fs = lib.fileset

trace 함수는 주어진 파일 집합에 포함된 파일을 보기 좋게 출력합니다.

nix-repl> fs.trace ./. null
trace: /home/user (all files in directory)
null

파일 집합을 인자로 받는 모든 함수는 경로도 받을 수 있습니다. 이러한 경로 인자는 주어진 경로 아래의 모든 파일을 포함하는 집합으로 암시적으로 변환됩니다. 앞의 trace 출력에서는 이것이 (all files in directory)로 표시됩니다.

팁

trace 함수는 첫 번째 인자를 보기 좋게 출력하고 두 번째 인자를 반환합니다. 하지만 nix repl에서는 보기 좋은 출력만 필요한 경우가 많으므로 두 번째 인자를 생략할 수 있습니다.

nix-repl> fs.trace ./.
trace: /home/user (all files in directory)
«lambda @ /nix/store/1czr278x24s3bl6qdnifpvm5z03wfi2p-nixpkgs-src/lib/fileset/default.nix:555:8»

파일 집합은 개념적으로 로컬 파일을 포함하지만, 이 파일들은 명시적으로 요청하지 않는 한 Nix 스토어에 절대 추가되지 않습니다. 따라서 누구나 읽을 수 있는 스토어에 실수로 비밀 정보를 복사할까 봐 크게 걱정하지 않아도 됩니다.

이 예제에서는 홈 디렉터리를 보기 좋게 출력했지만 어떤 파일도 복사되지 않았습니다. 이는 "${./.}"처럼 경로를 문자열로 강제 변환하는 경우와 대조적입니다. 이 경우에는 평가 시 디렉터리 전체가 Nix 스토어에 복사됩니다.

경고

flakes 및 nix-command 실험적 기능을 사용할 때, 플레이크 내의 로컬 디렉터리는 Git 저장소가 아닌 한 항상 Nix 스토어에 통째로 복사됩니다.

이러한 암시적 강제 변환은 파일에도 적용됩니다.

$ touch some-file
nix-repl> fs.trace ./some-file
trace: /home/user
trace: - some-file (regular)

이 경우 포함된 파일과 함께 그 파일의 파일 유형도 출력됩니다.

예제 프로젝트#

라이브러리를 더 실험해 보기 위해 예제 프로젝트를 만듭시다. 새 디렉터리를 만들어 그 안으로 이동한 다음, npins를 설정하여 Nixpkgs 의존성을 고정하십시오.

$ mkdir fileset
$ cd fileset
$ nix-shell -p npins --run "npins init --bare; npins add github nixos nixpkgs --branch nixos-23.11"

그런 다음 아래 내용으로 default.nix 파일을 만드십시오.

default.nix#
{
  system ? builtins.currentSystem,
  sources ? import ./npins,
}:
let
  pkgs = import sources.nixpkgs {
    config = { };
    overlays = [ ];
    inherit system;
  };
in
pkgs.callPackage ./package.nix { }

작업에 사용할 소스 파일 두 개를 추가합니다:

$ echo hello > hello.txt
$ echo world > world.txt

Nix 스토어에 파일 추가하기#

주어진 파일 집합의 파일은 toSource로 Nix 스토어에 추가할 수 있습니다. 이 함수의 인자에는 스토어에 복사할 소스 디렉터리를 결정하는 root 속성이 있어야 합니다. 결과에는 fileset 속성에 있는 파일만 포함됩니다.

package.nix를 다음과 같이 정의합니다:

package.nix#
{ stdenv, lib }:
let
  fs = lib.fileset;
  sourceFiles = ./hello.txt;
in

fs.trace sourceFiles

stdenv.mkDerivation {
  name = "fileset";
  src = fs.toSource {
    root = ./.;
    fileset = sourceFiles;
  };
  postInstall = ''
    mkdir $out
    cp -v hello.txt $out
  '';
}

fs.trace 호출은 derivation 입력으로 사용될 파일 집합을 출력합니다.

빌드해 보십시오:

참고

처음에는 Nixpkgs를 가져오는 데 시간이 좀 걸립니다.

$ nix-build
trace: /home/user/fileset
trace: - hello.txt (regular)
this derivation will be built:
  /nix/store/3ci6avmjaijx5g8jhb218i183xi7bi2n-fileset.drv
...
'hello.txt' -> '/nix/store/sa4g6h13v0zbpfw6pzva860kp5aks44n-fileset/hello.txt'
...
/nix/store/sa4g6h13v0zbpfw6pzva860kp5aks44n-fileset

하지만 파일 집합 라이브러리의 진정한 이점은 파일 집합을 다양한 방식으로 조합하는 기능에서 나옵니다.

차집합#

hello.txt와 world.txt 두 파일을 모두 출력에 복사할 수 있도록 프로젝트 디렉터리 전체를 다시 소스로 추가합니다:

package.nix#
 { stdenv, lib }:
 let
   fs = lib.fileset;
-  sourceFiles = ./hello.txt;
+  sourceFiles = ./.;
 in

 fs.trace sourceFiles

 stdenv.mkDerivation {
   name = "fileset";
   src = fs.toSource {
     root = ./.;
     fileset = sourceFiles;
   };
   postInstall = ''
     mkdir $out
-    cp -v hello.txt $out
+    cp -v {hello,world}.txt $out
   '';
 }

예상대로 동작합니다:

$ nix-build
trace: /home/user/fileset (all files in directory)
this derivation will be built:
  /nix/store/fsihp8872vv9ngbkc7si5jcbigs81727-fileset.drv
...
'hello.txt' -> '/nix/store/wmsxfgbylagmf033nkazr3qfc96y7mwk-fileset/hello.txt'
'world.txt' -> '/nix/store/wmsxfgbylagmf033nkazr3qfc96y7mwk-fileset/world.txt'
...
/nix/store/wmsxfgbylagmf033nkazr3qfc96y7mwk-fileset

그런데 nix-build를 다시 실행하면 출력 경로가 달라집니다!

$ nix-build
trace: /home/user/fileset (all files in directory)
this derivation will be built:
  /nix/store/nlh7ismrf27xsnl3m20vfz6rvwlbbbca-fileset.drv
...
'hello.txt' -> '/nix/store/xknflcvjaa8dj6a6vkg629zmcrgz10rh-fileset/hello.txt'
'world.txt' -> '/nix/store/xknflcvjaa8dj6a6vkg629zmcrgz10rh-fileset/world.txt'
...
/nix/store/xknflcvjaa8dj6a6vkg629zmcrgz10rh-fileset

여기서 문제는 nix-build가 기본적으로 작업 디렉터리에 방금 생성된 스토어 경로를 가리키는 result 심볼릭 링크를 만든다는 점입니다:

$ ls -l result
result -> /nix/store/xknflcvjaa8dj6a6vkg629zmcrgz10rh-fileset

src가 디렉터리 전체를 참조하는데 nix-build가 성공할 때마다 그 내용이 바뀌므로, Nix는 매번 처음부터 다시 시작해야 합니다.

참고

이 문제는 파일 집합 라이브러리를 사용하지 않아도 발생합니다. 예를 들어 src = ./.;를 직접 설정할 때도 마찬가지입니다.

difference 함수는 한 파일 집합에서 다른 파일 집합을 뺍니다. 그 결과는 첫 번째 인자의 파일 중 두 번째 인자에 없는 모든 파일을 담은 새 파일 집합입니다.

sourceFiles 정의를 바꿔 이 함수로 ./result를 걸러내십시오:

package.nix#
 { stdenv, lib }:
 let
   fs = lib.fileset;
-  sourceFiles = ./.;
+  sourceFiles = fs.difference ./. ./result;
 in

이것을 빌드하면 파일 집합 라이브러리가 디렉터리에서 어떤 파일을 가져오는지 보여 줍니다:

$ nix-build
trace: /home/user/fileset
trace: - package.nix (regular)
trace: - default.nix (regular)
trace: - hello.txt (regular)
trace: - npins (all files in directory)
trace: - world.txt (regular)
this derivation will be built:
  /nix/store/zr19bv51085zz005yk7pw4s9sglmafvn-fileset.drv
...
'hello.txt' -> '/nix/store/vhyhk6ij39gjapqavz1j1x3zbiy3qc1a-fileset/hello.txt'
'world.txt' -> '/nix/store/vhyhk6ij39gjapqavz1j1x3zbiy3qc1a-fileset/world.txt'
...
/nix/store/vhyhk6ij39gjapqavz1j1x3zbiy3qc1a-fileset

빌드를 반복하면 기존 스토어 경로를 재사용합니다:

$ nix-build
trace: /home/user/fileset
trace: - package.nix (regular)
trace: - default.nix (regular)
trace: - hello.txt (regular)
trace: - npins (all files in directory)
trace: - world.txt (regular)
/nix/store/vhyhk6ij39gjapqavz1j1x3zbiy3qc1a-fileset

누락된 파일#

하지만 ./result 심볼릭 링크를 제거하면 새로운 문제가 생깁니다:

$ rm result
$ nix-build
error: lib.fileset.difference: Second argument (negative set)
  (/home/user/fileset/result) is a path that does not exist.
  To create a file set from a path that may not exist, use `lib.fileset.maybeMissing`.

오류 메시지의 안내에 따라 maybeMissing을 사용하여 존재하지 않을 수도 있는 경로로부터 파일 집합을 만드십시오(경로가 없으면 파일 집합은 비어 있게 됩니다):

package.nix#
 { stdenv, lib }:
 let
   fs = lib.fileset;
-  sourceFiles = fs.difference ./. ./result;
+  sourceFiles = fs.difference ./. (fs.maybeMissing ./result);
 in

이제 ./result가 없으므로 디렉터리 전체를 사용하여 정상적으로 동작합니다:

$ nix-build
trace: /home/user/fileset (all files in directory)
this derivation will be built:
  /nix/store/zr19bv51085zz005yk7pw4s9sglmafvn-fileset.drv
...
/nix/store/vhyhk6ij39gjapqavz1j1x3zbiy3qc1a-fileset

빌드를 다시 시도하면 추적 결과는 다르게 출력되지만 출력 경로는 동일합니다:

$ nix-build
trace: /home/user/fileset
trace: - package.nix (regular)
trace: - default.nix (regular)
trace: - hello.txt (regular)
trace: - npins (all files in directory)
trace: - world.txt (regular)
/nix/store/vhyhk6ij39gjapqavz1j1x3zbiy3qc1a-fileset

합집합(파일을 명시적으로 제외하기)#

아직 문제가 남아 있습니다. derivation이 포함된 파일에 의존하지 않는데도 그 파일 중 어느 하나라도 변경하면 derivation이 다시 빌드됩니다.

package.nix에 빈 줄을 추가하십시오:

$ echo >> package.nix

이번에도 Nix는 처음부터 다시 시작합니다:

$ nix-build
trace: /home/user/fileset
trace: - default.nix (regular)
trace: - npins (all files in directory)
trace: - package.nix (regular)
trace: - string.txt (regular)
this derivation will be built:
  /nix/store/zmgpqlpfz2jq0w9rdacsnpx8ni4n77cn-filesets.drv
...
/nix/store/6pffjljjy3c7kla60nljk3fad4q4kkzn-filesets

이 문제를 해결하는 한 가지 방법은 unions를 사용하는 것입니다.

제외할 파일들의 합집합을 담은 파일 집합(fs.unions [ ... ])을 만들고, 이를 전체 디렉터리(./.)에서 빼십시오(difference):

package.nix#
  sourceFiles =
    fs.difference
      ./.
      (fs.unions [
        (fs.maybeMissing ./result)
        ./default.nix
        ./package.nix
        ./npins
      ]);

이제 예상대로 동작합니다:

$ nix-build
trace: /home/user/fileset
trace: - hello.txt (regular)
trace: - world.txt (regular)
this derivation will be built:
  /nix/store/gr2hw3gdjc28fmv0as1ikpj7lya4r51f-fileset.drv
...
/nix/store/ckn40y7hgqphhbhyrq64h9r6rvdh973r-fileset

이제 제외된 파일 중 어느 것을 변경해도 더 이상 반드시 새로 빌드되지는 않습니다:

$ echo >> package.nix
$ nix-build
trace: /home/user/fileset
trace: - hello.txt (regular)
trace: - world.txt (regular)
/nix/store/ckn40y7hgqphhbhyrq64h9r6rvdh973r-fileset

필터#

fileFilter 함수를 사용하면 포함되는 각 파일이 주어진 조건을 만족하도록 파일 집합을 필터링할 수 있습니다.

이 함수를 사용하여 이름이 .nix로 끝나는 모든 파일을 선택하십시오:

package.nix#
   sourceFiles =
     fs.difference
       ./.
       (fs.unions [
         (fs.maybeMissing ./result)
-        ./default.nix
-        ./package.nix
+        (fs.fileFilter (file: file.hasExt "nix") ./.)
         ./npins
       ]);

새 .nix 파일을 추가하더라도 결과는 바뀌지 않습니다.

$ nix-build
trace: /home/user/fileset
trace: - hello.txt (regular)
trace: - world.txt (regular)
/nix/store/ckn40y7hgqphhbhyrq64h9r6rvdh973r-fileset

특히 difference ./.를 사용하는 방식은 제외할 파일을 명시적으로 선택하므로, 소스 디렉터리에 새로 추가된 파일은 기본적으로 포함됩니다. 프로젝트에 따라서는 다음 절에서 다루는 대안보다 이 방식이 더 적합할 수 있습니다.

합집합(파일을 명시적으로 포함하기)#

앞의 방식과 달리, unions를 사용하여 포함할 파일만 선택할 수도 있습니다. 이 경우 현재 디렉터리에 새로 추가된 파일은 기본적으로 무시됩니다.

파일을 몇 개 더 만드십시오:

$ mkdir src
$ touch build.sh src/select.{c,h}

그런 다음 명시적으로 포함할 파일만으로 파일 집합을 만드십시오:

package.nix#
{ stdenv, lib }:
let
  fs = lib.fileset;
  sourceFiles = fs.unions [
    ./hello.txt
    ./world.txt
    ./build.sh
    (fs.fileFilter
      (file: file.hasExt "c" || file.hasExt "h")
      ./src
    )
  ];
in

fs.trace sourceFiles

stdenv.mkDerivation {
  name = "fileset";
  src = fs.toSource {
    root = ./.;
    fileset = sourceFiles;
  };
  postInstall = ''
    cp -vr . $out
  '';
}

postInstall 스크립트는 소스가 미리 적절히 필터링되어 있다고 전제하므로 더 단순해집니다:

$ nix-build
trace: /home/user/fileset
trace: - build.sh (regular)
trace: - hello.txt (regular)
trace: - src (all files in directory)
trace: - world.txt (regular)
this derivation will be built:
  /nix/store/sjzkn07d6a4qfp60p6dc64pzvmmdafff-fileset.drv
...
'.' -> '/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset'
'./build.sh' -> '/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset/build.sh'
'./hello.txt' -> '/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset/hello.txt'
'./world.txt' -> '/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset/world.txt'
'./src' -> '/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset/src'
'./src/select.c' -> '/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset/src/select.c'
'./src/select.h' -> '/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset/src/select.h'
...
/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset

새 파일을 추가하더라도 지정된 파일만 사용됩니다:

$ touch src/select.o README.md

$ nix-build
trace: - build.sh (regular)
trace: - hello.txt (regular)
trace: - src
trace:   - select.c (regular)
trace:   - select.h (regular)
trace: - world.txt (regular)
/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset

Git이 추적하는 파일 선택하기#

디렉터리가 Git 저장소에 속해 있다면, 그 디렉터리를 gitTracked에 전달하여 Git이 추적하는 파일만 포함하는 파일 집합을 얻을 수 있습니다.

로컬 Git 저장소를 만들고 src/select.o와 ./result를 제외한 모든 파일을 저장소에 추가하십시오:

$ git init
Initialized empty Git repository in /home/user/fileset/.git/
$ git add -A
$ git reset src/select.o result

gitTracked로 이 파일 선택을 재사용하십시오:

package.nix#
  sourceFiles = fs.gitTracked ./.;

다시 빌드하십시오:

$ nix-build
warning: Git tree '/home/user/fileset' is dirty
trace: /home/vg/src/nix.dev/fileset
trace: - README.md (regular)
trace: - package.nix (regular)
trace: - build.sh (regular)
trace: - default.nix (regular)
trace: - hello.txt (regular)
trace: - npins (all files in directory)
trace: - src
trace:   - select.c (regular)
trace:   - select.h (regular)
trace: - world.txt (regular)
this derivation will be built:
  /nix/store/p9aw3fl5xcjbgg9yagykywvskzgrmk5y-fileset.drv
...
/nix/store/cw4bza1r27iimzrdbfl4yn5xr36d6k5l-fileset

하지만 원래 의도대로 derivation을 빌드하는 데 이 파일들이 모두 필요한 것은 아니므로, 이 방식은 너무 많은 파일을 포함합니다.

참고

flakes 및 nix-command 실험적 기능을 사용할 때는 nix build가 기본적으로 Git이 추적하는 파일에만 접근을 허용하므로 이 함수가 필요하지 않습니다. 그러나 안정 버전 Nix에서도 동일한 개발자 경험을 제공하려면 그럼에도 이 함수를 사용하는 것을 권장합니다.

교집합#

바로 이때 intersection이 필요합니다. 이 함수를 사용하면 주어진 두 파일 집합에 모두 포함된 파일로만 구성된 파일 집합을 만들 수 있습니다.

Git이 추적하면서 동시에 빌드와 관련된 모든 파일을 선택하십시오:

package.nix#
  sourceFiles =
    fs.intersection
      (fs.gitTracked ./.)
      (fs.unions [
        ./hello.txt
        ./world.txt
        ./build.sh
        ./src
      ]);

이렇게 하면 다른 방식과 동일한 출력이 생성되므로 이전 빌드 결과를 재사용합니다:

$ nix-build
warning: Git tree '/home/user/fileset' is dirty
trace: - build.sh (regular)
trace: - hello.txt (regular)
trace: - src
trace:   - select.c (regular)
trace:   - select.h (regular)
trace: - world.txt (regular)
/nix/store/zl4n1g6is4cmsqf02dci5b2h5zd0ia4r-fileset

결론#

지금까지 모든 기본 파일 집합 함수의 사용법을 보여 주는 몇 가지 예시를 살펴보았습니다. 더 복잡한 사용 사례에서는 필요에 따라 이 함수들을 조합할 수 있습니다.

전체 목록과 자세한 내용은 lib.fileset 참조 문서를 참고하십시오.