callPackage를 이용한 패키지 매개변수와 오버라이드#
- Author:
Norbert Melzer @NobbZ
- Editors:
Matthias Meschede @mmesch, Valentin Gagarin @fricklerhandwerk
Nix에는 패키지와 설정을 만들기 위한 특수 목적 프로그래밍 언어인 Nix 언어가 함께 제공됩니다. 이 언어는 Nixpkgs로 알려진 Nix 패키지 모음을 빌드하는 데 사용됩니다.
Nix 언어는 순수 함수형 언어이므로 공통 패턴을 추상화하는 사용자 정의 함수를 선언할 수 있습니다. Nixpkgs에서 가장 두드러지는 패턴 중 하나는 패키지 레시피의 매개변수화입니다.
개요#
Nixpkgs는 그 자체로 상당한 규모의 소프트웨어 프로젝트이며, 수년에 걸쳐 형성된 코딩 관례와 관용구를 가지고 있습니다. Nixpkgs는 callPackage라는 함수를 통해 매개변수화된 패키지를 자동 설정으로 조합하는 관례를 확립했습니다. 이 튜토리얼에서는 이 함수를 사용하는 방법과 그 이점을 보여 줍니다.
무엇을 배우게 됩니까?#
callPackage를 사용하여 Nixpkgs 관례를 따르는 패키지 레시피 호출하기패키지 매개변수 오버라이드하기
상호 의존적인 패키지 집합 만들기
무엇이 필요합니까?#
Nix 언어에 대한 이해
기존 소프트웨어 패키징에 대한 첫 경험
얼마나 걸립니까?#
45분
자동 함수 호출#
Nixpkgs에서 볼 수 있는 전형적인 패키지 레시피라고 할 수 있는 새 파일 hello.nix를 만드십시오. 이는 최상위 패키지 집합의 derivation에 대응하는 속성을 가진 속성 집합을 받아 derivation을 반환하는 함수입니다.
{ writeShellScriptBin }:
writeShellScriptBin "hello" ''
echo "Hello, world!"
''
자세한 설명
hello.nix는 writeShellScriptBin이라는 요소 하나를 가진 속성 집합을 인수로 받는 함수를 선언합니다. writeShellScriptBin은 마침 Nixpkgs에 존재하는 함수로, derivation을 반환하는 빌드 헬퍼입니다. 이 경우 derivation 출력에는 실행하면 “Hello world”를 출력하는 실행 가능한 셸 스크립트가 $out/bin/hello에 포함됩니다.
이제 다음 내용으로 default.nix 파일을 만드십시오.
let
pkgs = import <nixpkgs> { };
in
pkgs.callPackage ./hello.nix { }
default.nix의 derivation을 실현하고 생성된 실행 파일을 실행하십시오.
$ nix-build
$ ./result/bin/hello
Hello, world!
hello.nix의 함수가 평가될 때 인수 writeShellScriptBin이 자동으로 채워집니다. callPackage는 함수 인수의 모든 속성에 대해, pkgs 속성 집합에 해당 속성이 있으면 그 속성을 전달합니다.
이렇게 간단한 구성에서 패키지를 위해 별도의 파일 hello.nix를 만드는 것은 번거로워 보일 수 있습니다. 그렇게 한 이유는 Nixpkgs가 정확히 이런 방식으로 구성되어 있기 때문입니다. 모든 패키지 레시피는 함수를 선언하는 파일입니다. 이 함수는 패키지의 의존성을 인수로 받습니다.
매개변수화된 빌드#
default.nix를 수정하여 derivation의 속성 집합을 만들고, 속성 hello에 원래 derivation이 담기도록 하십시오:
let
pkgs = import <nixpkgs> { };
in
{
hello = pkgs.callPackage ./hello.nix { };
}
-A / --attr 옵션으로 속성 hello에 접근하여 빌드하면 결과는 이전과 같습니다:
$ nix-build -A hello
$ ./result/bin/hello
Hello, world!
또한 hello.nix를 수정하여 기본값이 "world"인 매개변수 audience를 추가하십시오:
{
writeShellScriptBin,
audience ? "world",
}:
writeShellScriptBin "hello" ''
echo "Hello, ${audience}!"
''
이 변경도 결과를 바꾸지 않습니다.
이 새 인수를 사용하도록 default.nix를 수정하면 더 흥미로워집니다. callPackage의 두 번째 인수로 매개변수 audience를 전달하십시오:
let
pkgs = import <nixpkgs> { };
in
{
- hello = pkgs.callPackage ./hello.nix { };
+ hello = pkgs.callPackage ./hello.nix { audience = "people"; };
}
이 속성은 hello.nix에 정의된 함수의 인수로 전달됩니다. 같은 구문으로 writeShellScriptBin처럼 자동으로 찾아지는 인수를 명시적으로 설정할 수도 있지만, 여기서는 그렇게 할 필요가 없습니다.
직접 해 보십시오:
$ nix-build -A hello
$ ./result/bin/hello
Hello, people!
이 패턴은 Nixpkgs에서 널리 사용됩니다. 예를 들어 Go 프로그램을 나타내는 함수에는 buildGoModule 매개변수가 있는 경우가 많습니다. 기본 Go 컴파일러 버전을 바꾸기 위해 callPackage ./go-program.nix { buildGoModule = buildGo116Module; }와 같은 표현식을 쓰는 경우를 흔히 볼 수 있습니다. 따라서 Nixpkgs는 단순히 미리 설정된 패키지를 모아 둔 거대한 라이브러리가 아니라, 코드를 중복하지 않고도 패키지는 물론 생태계 전체(예: “내가 직접 설정한 인터프리터를 사용하는 모든 Python 패키지”)를 즉석에서 사용자 정의할 수 있는 함수, 즉 패키지 레시피의 모음입니다.
오버라이드#
callPackage는 반환된 derivation의 override 함수를 사용해 매개변수를 나중에 사용자 정의할 수 있게 하여 편의성을 더합니다.
default.nix에 세 번째 속성 hello-folks를 추가하고, audience에 새 값을 넘겨 호출한 hello.override를 그 값으로 설정하십시오:
let
pkgs = import <nixpkgs> { };
in
-{
+rec {
hello = pkgs.callPackage ./hello.nix { audience = "people"; };
+ hello-folks = hello.override { audience = "folks"; };
}
참고
이제 결과 속성 집합은 (키워드 rec에 의해) 재귀적입니다. 즉, 속성 값이 같은 속성 집합 안의 이름을 참조할 수 있습니다.
override는 hello.nix에 있는 원래 함수에 audience를 전달합니다. 이 값은 derivation hello를 만든 원래의 callPackage에 어떤 인수가 전달되었든 그 인수를 오버라이드합니다. 나머지 매개변수는 모두 그대로 유지됩니다. 이 기능은 특히 유용하며, 패키지를 사용자 정의할 수 있는 옵션을 많이 제공하는 패키지에서 자주 볼 수 있습니다.
hello-folks 속성을 빌드하고 결과 실행 파일을 실행하면 이번에도 새로운 버전의 스크립트가 만들어집니다:
$ nix-build -A hello-folks
$ ./result/bin/hello
Hello, folks!
실제 예로는 neovim 패키지 레시피가 있습니다. 이 레시피에는 extraLuaPackages, extraPythonPackages, withRuby와 같이 오버라이드할 수 있는 인수가 있습니다. 현재 이러한 매개변수는 소스 코드를 읽어야만 알 수 있으며, 소스 코드는 search.nixos.org/packages에서 📦 Source 링크를 따라가면 찾을 수 있습니다.
상호 의존하는 패키지 집합#
실제로 callPackage를 직접 만들 수도 있습니다! 이는 레시피가 서로 의존하는 패키지 집합에서 유용합니다.
참고
다음 예제에서는 “호출되는” 파일을 보여 주지 않습니다. 원리를 이해하는 데 필요하지 않기 때문입니다.
다음과 같은 derivation의 재귀 속성 집합을 살펴보십시오:
let
pkgs = import <nixpkgs> { };
in
rec {
a = pkgs.callPackage ./a.nix { };
b = pkgs.callPackage ./b.nix { inherit a; };
c = pkgs.callPackage ./c.nix { inherit b; };
d = pkgs.callPackage ./d.nix { };
e = pkgs.callPackage ./e.nix { inherit c d; };
}
참고
여기서 inherit a;는 a = a;와 같습니다.
앞서 선언한 derivation은 callPackage를 통해 다른 derivation에 인자로 전달됩니다.
이 경우 각 패키지가 요구하는 인자 중 Nixpkgs에 없는 것은 모두 해당 Nix 파일에서 직접 지정해야 한다는 점을 기억해야 합니다. ./b.nix가 인자 a를 요구하는데 pkgs.a가 없으면 함수 호출 시 오류가 발생합니다. 이 작업은 금세 상당히 번거로워질 수 있으며, 패키지 집합이 클수록 더욱 그렇습니다.
lib.callPackageWith를 사용하여 속성 집합을 기반으로 나만의 callPackage를 만드십시오.
let
pkgs = import <nixpkgs> { };
callPackage = pkgs.lib.callPackageWith (pkgs // packages);
packages = {
a = callPackage ./a.nix { };
b = callPackage ./b.nix { };
c = callPackage ./c.nix { };
d = callPackage ./d.nix { };
e = callPackage ./e.nix { };
};
in
packages
이 부분은 약간의 설명이 필요합니다.
먼저, 이제는 재귀 속성 집합 대신 let 바인딩에서 우리가 다루는 이름을 할당한다는 점에 주목하십시오. 이 방식은 재귀 집합과 같은 성질을 가집니다. 즉, 등호(=) 왼쪽의 이름을 오른쪽 표현식에서 사용할 수 있습니다. 그래서 // 연산자로 packages의 내용을 기존 속성 집합 pkgs와 병합할 때 packages를 참조할 수 있습니다.
이제 직접 만든 callPackages는 호출된 패키지 함수에 pkgs 및 packages의 모든 속성을 제공하며(이름이 같으면 packages의 것이 우선합니다), packages는 호출될 때마다 재귀적으로 구성됩니다.
마지막 부분은 머리를 어지럽게 만들 수도 있습니다. 이러한 구성은 Nix 언어가 지연 평가되기 때문에 비로소 가능합니다. 즉, 값은 실제로 필요할 때에만 계산됩니다. 덕분에 packages를 완전히 정의하지 않은 상태에서도 이리저리 전달할 수 있습니다.
이제 이 수준에서는 각 패키지의 의존성이 암묵적이며(각 패키지 파일에서는 여전히 명시적입니다), callPackage가 이를 알아서 척척 해결합니다. 덕분에 의존성을 직접 처리할 필요가 없어지고, 긴 빌드 과정의 후반에야 드러날 수 있는 설정 오류를 미리 방지할 수 있습니다.
물론 이 작은 예제는 원래 형태로도 충분히 관리할 수 있습니다. 또한 암묵적으로 재귀적인 이 방식은 지연 평가에 익숙하지 않은 소프트웨어 개발자에게 구조를 불분명하게 만들어, 이전보다 읽기 어렵게 할 수 있습니다. 하지만 이 이점은 코드의 양 때문에 구조가 불분명해지고 수동 수정이 번거롭고 오류가 발생하기 쉬워지는 대규모 구성에서 진가를 발휘합니다.
요약#
callPackage를 사용하면 Nixpkgs 관례를 따르게 되어 숙련된 Nix 사용자가 코드를 이해하기 쉬워질 뿐만 아니라, 다음과 같은 이점도 덤으로 얻을 수 있습니다.
매개변수화된 빌드
오버라이드 가능한 빌드
상호 의존적인 패키지 집합의 간결한 구현
참고 자료#
다음 단계#
로컬 파일 다루기 - Nix로 자신의 프로젝트를 패키지로 만드는 방법을 배웁니다
모듈 시스템 심층 탐구 - NixOS를 뒷받침하는 함수형 프로그래밍의 마법을 다루는 방법을 배웁니다