(file-sets-tutorial)=
# 로컬 파일 다루기

```{contributors}
:authors: infinisil
```

로컬 프로젝트를 Nix derivation으로 빌드하려면 해당 derivation의 [`builder` 실행 파일](https://nix.dev/manual/nix/stable/language/derivations#attr-builder)이 소스 파일에 접근할 수 있어야 합니다. 기본적으로 `builder`는 Nix 스토어에서 읽기만 허용하는 [격리된 환경](https://nix.dev/manual/nix/stable/command-ref/conf-file.html#conf-sandbox)에서 실행됩니다. Nix 언어에는 로컬 파일을 스토어에 복사하고 그 결과로 생성된 스토어 경로를 노출하는 내장 기능이 있습니다.

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

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

- [`builtins.path`](https://nix.dev/manual/nix/stable/language/builtins.html#builtins-path) 함수(그리고 이와 동등한 [`lib.sources.cleanSourceWith`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.sources.cleanSourceWith))로 이러한 문제를 해결할 수 있습니다. 그러나 `filter` 함수 인터페이스로는 원하는 경로 선택을 표현하기 어려운 경우가 많습니다.

이 튜토리얼에서는 Nixpkgs의 [`lib.fileset` 라이브러리](https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-fileset)를 사용하여 derivation에서 로컬 파일을 다루는 방법을 배웁니다. 이 라이브러리는 내장 기능을 추상화하여 더 안전하고 편리한 인터페이스를 제공합니다.

## 파일 집합

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

[`nix repl`](https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-repl)로 라이브러리를 탐색하며 배울 수 있습니다.

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

[`trace`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.trace) 함수는 주어진 파일 집합에 포함된 파일을 보기 좋게 출력합니다.

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

파일 집합을 인자로 받는 모든 함수는 [경로](https://nix.dev/manual/nix/stable/language/values#type-path)도 받을 수 있습니다. 이러한 경로 인자는 주어진 경로 아래의 _모든_ 파일을 포함하는 [집합으로 암시적으로 변환](https://nixos.org/manual/nixpkgs/stable/#sec-fileset-path-coercion)됩니다. 앞의 trace 출력에서는 이것이 `(all files in directory)`로 표시됩니다.

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

```shell-session
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 스토어에 복사됩니다.

:::{warning}
[`flakes` 및 `nix-command` 실험적 기능](https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake)을 사용할 때, 플레이크 내의 로컬 디렉터리는 Git 저장소가 아닌 한 항상 Nix 스토어에 *통째로* 복사됩니다.
:::

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

```shell-session
$ touch some-file
```

```shell-session
nix-repl> fs.trace ./some-file
trace: /home/user
trace: - some-file (regular)
```

이 경우 포함된 파일과 함께 그 파일의 [파일 유형](https://nix.dev/manual/nix/stable/language/builtins.html#builtins-readFileType)도 출력됩니다.


## 예제 프로젝트

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

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

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

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

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

```shell-session
$ echo hello > hello.txt
$ echo world > world.txt
```

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

주어진 파일 집합의 파일은 [`toSource`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.toSource)로 Nix 스토어에 추가할 수 있습니다. 이 함수의 인자에는 스토어에 복사할 소스 디렉터리를 결정하는 `root` 속성이 있어야 합니다. 결과에는 `fileset` 속성에 있는 파일만 포함됩니다.

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

```{code-block} nix
:caption: 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 입력으로 사용될 파일 집합을 출력합니다.

빌드해 보십시오:

:::{note}
처음에는 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` 두 파일을 모두 출력에 복사할 수 있도록 프로젝트 디렉터리 전체를 다시 소스로 추가합니다:

```{code-block} diff
:caption: 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
   '';
 }
```

예상대로 동작합니다:

```shell-session
$ 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`를 다시 실행하면 출력 경로가 달라집니다!

```shell-session
$ 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는 매번 처음부터 다시 시작해야 합니다.

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

[`difference`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.difference) 함수는 한 파일 집합에서 다른 파일 집합을 뺍니다. 그 결과는 첫 번째 인자의 파일 중 두 번째 인자에 없는 모든 파일을 담은 새 파일 집합입니다.

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

```{code-block} diff
:caption: package.nix
 { stdenv, lib }:
 let
   fs = lib.fileset;
-  sourceFiles = ./.;
+  sourceFiles = fs.difference ./. ./result;
 in
```

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

```shell-session
$ 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` 심볼릭 링크를 제거하면 새로운 문제가 생깁니다:

```shell-session
$ 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`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.maybeMissing)을 사용하여 존재하지 않을 수도 있는 경로로부터 파일 집합을 만드십시오(경로가 없으면 파일 집합은 비어 있게 됩니다):

```{code-block} diff
:caption: 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`에 빈 줄을 추가하십시오:

```shell-session
$ echo >> package.nix
```

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

```shell-session
$ 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`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.unions)를 사용하는 것입니다.

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

```{code-block} nix
:caption: 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`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.fileFilter) 함수를 사용하면 포함되는 각 파일이 주어진 조건을 만족하도록 파일 집합을 필터링할 수 있습니다.

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

```{code-block} diff
:caption: package.nix
   sourceFiles =
     fs.difference
       ./.
       (fs.unions [
         (fs.maybeMissing ./result)
-        ./default.nix
-        ./package.nix
+        (fs.fileFilter (file: file.hasExt "nix") ./.)
         ./npins
       ]);
```

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

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

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

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

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

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

```shell-session
$ mkdir src
$ touch build.sh src/select.{c,h}
```

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

```{code-block} nix
:caption: 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` 스크립트는 소스가 미리 적절히 필터링되어 있다고 전제하므로 더 단순해집니다:

```shell-session
$ 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
```

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

```shell-session
$ 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`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.gitTracked)에 전달하여 Git이 추적하는 파일만 포함하는 파일 집합을 얻을 수 있습니다.

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

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

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

```{code-block} nix
:caption: package.nix
  sourceFiles = fs.gitTracked ./.;
```

다시 빌드하십시오:

```shell-session
$ 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을 빌드하는 데 이 파일들이 모두 필요한 것은 아니므로, 이 방식은 너무 많은 파일을 포함합니다.

:::{note}
[`flakes` 및 `nix-command` 실험적 기능](https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake)을 사용할 때는 `nix build`가 기본적으로 Git이 추적하는 파일에만 접근을 허용하므로 이 함수가 필요하지 않습니다. 그러나 안정 버전 Nix에서도 동일한 개발자 경험을 제공하려면 그럼에도 이 함수를 사용하는 것을 권장합니다.
:::

## 교집합

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

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

```{code-block} nix
:caption: package.nix
  sourceFiles =
    fs.intersection
      (fs.gitTracked ./.)
      (fs.unions [
        ./hello.txt
        ./world.txt
        ./build.sh
        ./src
      ]);
```

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

```shell-session
$ 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` 참조 문서](https://nixos.org/manual/nixpkgs/stable/#sec-functions-library-fileset)를 참고하십시오.
