로컬 파일 다루기#
- 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 파일을 만드십시오.
{
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를 다음과 같이 정의합니다:
{ 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 두 파일을 모두 출력에 복사할 수 있도록 프로젝트 디렉터리 전체를 다시 소스로 추가합니다:
{ 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를 걸러내십시오:
{ 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을 사용하여 존재하지 않을 수도 있는 경로로부터 파일 집합을 만드십시오(경로가 없으면 파일 집합은 비어 있게 됩니다):
{ 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):
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로 끝나는 모든 파일을 선택하십시오:
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}
그런 다음 명시적으로 포함할 파일만으로 파일 집합을 만드십시오:
{ 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로 이 파일 선택을 재사용하십시오:
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이 추적하면서 동시에 빌드와 관련된 모든 파일을 선택하십시오:
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 참조 문서를 참고하십시오.