Nix의 Flake

Posted on December 2, 2024

※ Flake 영단어 뜻은 작고 얇은 조각을 의미합니다. 아침에 먹던 그 콘프레이크의 프레이크와 같은 뜻입니다.

Nix 패키지 매니저 2.4부터 도입된, 아직은 실험적 기능이라 합니다만, 실험적인 것 치고는 너무 많은 곳에 쓰이고 있습니다. 어쨌든 공식적으론 여전히 실험적 특징이라 합니다.(2024.11 현재)

Nix 만으로 각 종 설치, 환경을 완벽히 재현할 수 있으면 좋겠지만, 현재의 Nix는 어떤 패키지의 어떤 버전을 쓰고 있는지 명확히 선언하지 않고 있습니다. 예를 들어, 어떤 채널에 있는 패키지를 참조한다고 한다면, 해당 채널이 고정된 상태가 아니라면 항상 같은 패키지를 가져 온다고 보장할 수 없습니다. 이를 보완하기 위해, 의존성 검사에 필요한 정보들을 추가로 저장해야 나중에 재현 가능성reproduicibily을 높일 수 있습니다. 이를 위해, Nix에 새로운 기능으로 Flake를 도입했습니다.

Flake는 의존성과 버전 정보를 flake.lock 파일에 저장합니다. Nixpkgs 어떤 커밋을 쓰고 있는지 커밋 해시값까자 모두 기록합니다.

flake.lockflake.nixinput에 써 준 모든 것들의 데이터 소스, 해시값, 버전을 기록합니다.

Flake 기능을 도입하기 전에 쓰던 설정 파일인 configuration.nixflake.nix에서 모듈로 불러 옵니다.(configuration.nix 파일도 하나의 모듈 파일입니다.)

※ 처음 볼 때 Flake를 여기 저기 쓰는데, 해석에 좀 혼란이 있었습니다. 마치 Nix라 하면, Nix OS인지, 패키지 매니저인지, 언어를 뜻하는 지 혼란스러웠던 것처럼요.

flake.nix 파일
  |------------- 외부 모듈 flake 파일
  |                     |------------- 외부 모듈 flake 파일 ...
  |                     |------------- 외부 모듈 flake 파일 ...
  |                     ...
  |------------- 외부 모듈 flake 파일
  |                     |------------- 외부 모듈 flake 파일 ...
  ...                   ...

Flake 기능의 시작점이 되는 flake.nix 파일을 그냥 flake라 부를 때도 있고, flake 구성에 쓰이는 모든 파일들을 flake라 부르기도 하고, 기능 자체를 Flake라 부르기도 합니다.

flake.nix 기본 구성
(모든 flake 파일의 구성이 아니라, flake.nix파일의 구성입니다.)

{
  description = ...;
  inputs = ...;
  outputs =
    {
      lib =                  ;# 공통 유틸리티들이 들어 있는, 스탠다드 라이브러리 같은 것
      overlays =             ;# Nixpkgs를 수정, 확장하기 위한 오버레이 정의
      packages =             ;# 특정 환경(default, x86_64-linux, aarch64-darwin 같은 것)에 맞는 패키지 정의
                             ;# nix run 실행할 때 참조
      apps =                 ;# nix run 으로 애플리케이션을 실행
      checks =               ;# 테스트 정의
      defaultPacakage =      ;# Flake가 실행될 때 기본적으로 사용할 패키지
                             ;# nix build 에서 참조
      defaultApp =           ;# Flake가 실행될 때 기본적으로 실행할 애플리케이션
                             ;# nix run 와 연동
      devShells =            ;# 개발 환경을 정의
                             ;# nix develop 이 호출
      nixosConfigurations =  ;# NixOS 시스템 설정 정의
      darwinConfigurations = ;# macOS 시스템 설정 정의
      templates =            ;# 프로젝트를 생성할 수 있는 템플릿
                             ;# nix flake init에서 씁니다.
    };
}

설정 파일을 작성할 때 쓸 도구(lib)들을 준비하고, 기본 패키지에서 사용자 입맛에 맞게 바꾸거나 추가할 패키지(overlays)를 설정하고, 설치할 패키지(packages)를 정의합니다

시스템을 영구적으로 변경하는 sudo nixos-rebuild switch에서만 이 파일을 읽어 들이는 게 아니라, nix run, nix build,… 등의 명령어들도 이 파일을 읽어 참조합니다.

Flake의 모듈 시스템

flake.nix에서 불러 들일 모듈은 다음과 같은 구조를 가지고 있습니다. 아래 정의는 역시 Nix 언어의 함수 표현식입니다. 모듈 시스템이 자동으로 값을 넣어주는, 선언 없이 사용 가능한, 몇 개의 매개 변수를 가지고 있습니다. (※ 함수라고, 사용자가 어디에 직접 적용하거나 그러진 않습니다. 함수형 언어에서 데이터와 관련 메소드들을 묶어서 가지고 다니기에 가장 적합한 구조는 함수입니다.) NixOS modules - NixOS Wiki

flake.nix 파일의 구성이 아니라 Flake 모듈 파일의 기본 구성

{
  lib,
  config,
  options,
  pkgs,
  # 위의 것들이 자주 보이는데, 아래도 표준이라 합니다.
  modulePath, 
  configDir, 
  system, 
  parentConfig, 
  optionsPath, 
  specialArgs,
  ...
}: 
# 위 매개 변수로 들어오는 인자들을 닉스OS 모듈 시스템의 표준 인자라 합니다.
# 특별히 넘겨 주는 코드를 쓰지 않아도 자동으로 넘어 옵니다.
# 표준 인자만 받는 모듈일 경우 위는 통채로 생략하고, 파일 시작을 아래부터 할 수도 있습니다. 
{
  # 다른 모듈에서 불러 오기
  imports = [
    # ...
    ./xxx.nix # 파일 경로만(절대, 상대 모두 가능) 써주면 됩니다.
  ];

  options = {
    # ...
  };

  config = {
    # ...
  };

  foo.bar.enable = true;
  # 다른 옵션 선언
  # ...
}

매개 변수

※ 나중에 위 매개 변수에 값을 넘기는 건, 모듈 시스템이 알아서 합니다.

※ 기본값이 아닌 값을 서브 모듈에 넘길 때 nixpkgs.lib.nixosSystem 함수의 specialArgs 매개 변수를 이용하거나, _module.args 옵션을 이용합니다.

imports

디폴트 모듈셋은 nixos/modules/module-list.nix에 정의되어 있고, 이들은 명시적으로 imports에 추가하지 않아도 됩니다.

표현식들에서 자주 쓰이는 내장 함수 import some.nixsome.nix파일에 있는 닉스 표현식을 로드하는 것으로 (some.nix 같은 파일들은 특별히 지정된 구조를 가질 필요는 없습니다.) imports = [./module.nix]와 혼동하지 마세요.

다른 Flake 소스에서 소프트웨어 설치

nixpkgs가 아닌 다른 소스에서 설치 할 수 있습니다. (※ 보통 nixpkgs에 최신 버전이 아직 안 올라 왔을 경우 자주 씁니다.)

코드들은 NixOS & Flakes Book에서 발췌했습니다.

# flake.nix 예시
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";

    # helix의 데이터 소스를 추가한다. helix의 마스터 브랜치 
    helix.url = "github:helix-editor/helix/master";
  };

  outputs = inputs@{ self, nixpkgs, ... }: {
    nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      specialArgs = { inherit inputs; }; # (가)
      modules = [
        ./configuration.nix
        # { _module.args = { inherit inputs; };} (나)
        # (가)와 (나)가 하는 일은 같다. 원하는 걸로 골라 쓰면 됩니다.
      ];
    };
  };
}

flake.nixinputs에서 데이터 소스(위 helix같은 것)를 설정해 주면, specailArgs = { inherit inputs }를 통해 configuration.nix에 넘겨서, 이 소스를 가져다 쓸 수 있습니다. inputs 인자를 받기 위해 매개 변수 inputs를 추가했습니다.

# configuration.nix
{ config, pkgs, inputs, ... }:
{
  # ...
  environment.systemPackages = with pkgs; [
    git
    vim
    wget
    # 여기서, helix input 소스에서 가져와 helix 패키지를 설치합니다.
    inputs.helix.packages."${pkgs.system}".helix
  ];
  # ...
}

configuration.nix파일에 선언해서 영구적으로 설치하는 게 아닌, 시도만 해보려면 nix run github:helix-editor/helix/master를 이용할 수 있습니다.

모듈 불러오기

https://nixos.org/manual/nixos/unstable/#sec-modularity

# packages.nix
{
  config, # 최소 두 개의 매개 변수를 가집니다.
  pkgs,   # 닉스 모듈 시스템이 값을 넣어 줍니다.
  ...
}: {
  imports = [ # 외부 모듈을 불러올 때
    (import ./special-fonts-1.nix {inherit config pkgs;}) # (1)
    ./special-fonts-2.nix # (2)
  ];

  fontconfig.enable = true;
}

불러 올import 모듈은 두 가지 방식으로 적어 줄 수 있습니다.

(1)은 불러 온import 함수를 바로 실행하는 구문입니다. 인자로 넘겨 주려면, 그냥 config와, pkgs를 쓰면 되지 않나 했는데, {inherit config pkgs}{config = config; pkgs = pkgs}와 같은 구문입니다. 함수 실행 결과를 imports 배열에 원소로 추가하고 있습니다.

(2)도 닉스가 모듈로 간주하고, 내부 함수가 필요로 하는 config, pkgs를 명시하지 않아도, 모듈 시스템이 자동으로 전달합니다. import ./special-fonts-2.nix { config = config; pkgs = pkgs; }를 실행하고 있다고 보면 됩니다.

special-fonts-1.nix 파일이나 special-fonts-2 둘 모두 모듈이고, 아래 같은 모양을 가지고 있을 겁니다.

{ config, pkgs, ...}: {
    # Configuration stuff ...
}

위와 같은 packages.nix 모듈 파일을 만들면, 다른 Flake 어디서든 import 할 수 있습니다. 주로 configuration.niximports 항목이나, flake.nixmodules 항목에서 볼 수 있습니다.

    nixosConfigurations.mySystem = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./packages.nix
      ];

mkBefore 함수

※ 만일 configuration.nixsomeModule.nix에서 모두 environment.systemPackages 옵션을 정의하면, 닉스OS가 알아서 병합merge합니다. 그런데, 병합 순서가 있습니다. configuration.nix에 있는 것들이 가장 끝에 병합됩니다. 순서를 바꾸려면 유틸리티에 있는 mkBefore를 씁니다.

{
  boot.kernelModules = mkBefore ["kvm-intel"]
}

kernelMondules 중에 가장 먼저 kvm-intel이 로드 됩니다. 두 모듈에 리스트 타입이 아닌데 겹치는 옵션이 있으면 병합을 못하고, 오류가 납니다.

{
  services.httpd.adminAddr = pkgs.lib.mkForce "bob@example.org";
}

mkForce 함수

강제로 하나만 살아 남게 하려면 mkForce를 씁니다. 위에서 본 모듈 코드 모양을 보면 {config, pkgs, ...}config를 항상 받습니다. 모든 모듈들을 읽어 합칠 건 합치고, 겹치는 것들 처리하고 나온 결과를 묶어, 이 config를 통해 받습니다. 생각이 약간 복잡해지는데요. 모든 모듈들이 받는 config는, 어딘가에서 모듈 내용을 읽어들여 정리한 결과를 받는다니, 자신이 자신의 것을 이용하는 재귀적인 모양이 됩니다. Nix언어는 지연 평가lazy하기 때문에 이렇게 하는 게 가능합니다. (개별 항목이 자기 자신에 의존하는 경우는 안됩니다.) 아마 fixpoint로 구현하고 있을 겁니다.

nixos-option 명령어

여기 저기 동일한 옵션이 정의 되어 있어서, 다 병합하고 나면 어느게 살아남는지 궁금할 때 다음 명령어를 써서 알아 볼 수 있습니다.

$ nixos-option services.xserver.enable

※ 불러 들인 모듈에 있는 옵션에 접근하려면, config를 이용하면 됩니다. 어떻게? config는 재귀적 결과물이라 이렇게 하는 게 가능합니다. 몇 단계를 걸쳐 모듈을 불러오든 모두 config를 통해 접근할 수 있습니다.

Overriding 과 Overlays

Overriding

pkgs에 있는 닉스 패키지를 override 함수로 사용자가 덮어 씌울 수 있습니다. override 함수로 사용자 빌드 매개 변수를 정의할 수 있고, 덮어 씌워진 값을 가진, 새로운 derivation을 돌려 줍니다. 예)

pkgs.fcitx5-rime.override { rimeDataPkgs = [ ./rime-data-flypy ]; }

fcitx5-rime에 있는 rimeDataPkgs 매개 변수를, 커스텀 패키지 rime-data-flypy를 이용해 override합니다. override함수는 해당 필드만 덮어 씌워진 새로운 derivation을 만들어 냅니다.

패키지에 override로 덮어 씌울 수 있는 매개 변수가 뭐가 있는지 알아보는 방법
nix repl -f '<nixpkgs>'
:e pkgs.hello

{ callPackage
, lib
, stdenv
, fetchurl
, nixos
, testers
, hello
}:
# 위에 있는 것들은 override로 덮어 씌우고
stdenv.mkDerivation (finalAttrs: {
  # 아래 있는 항목들은 overrideAttrs로 덮어 씌웁니다.
  pname = "hello";
  version = "2.12.1";

  src = fetchurl {
    url = "mirror://gnu/hello/hello-${finalAttrs.version}.tar.gz";
    sha256 = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yA=";
  };

  doCheck = true;
  ...
})
hellowWithDebug = pkgs.hello.overrideAttrs (finalAttrs: previousAttrs: {
#                                             인자1         인자2
  doCheck = false;
})

인자 두 개가 커링된 람다 함수로 보면 됩니다.

위 hello 파일에 보이는 속성들 말고, stdenv.mkDerivation에 있는 separateDebugInfo같은 속성도 덮어 씌울 수 있습니다.

hellowWithDebug = pkgs.hello.overrideAttrs (finalAttrs: previousAttrs: {
  separateDebugInfo = true;
})

:e stdenv.mkDerivation으로 매개 변수 목록을 뽑아 볼 수 있습니다.

overlays

디폴트 nixpkgs 인스턴스에서, 전역적으로 derivations을 수정하기 위해 제공되는 기능입니다. 전역적이란 말이 무슨 말이냐 하면, override로 덮어 씌운 건 해당 패키지만 바뀌치기 하고, 다른 패키지가 해당 패키지를 참조하고 있다면, 그 때는 여전히 수정되지 않은 패키지를 참조합니다. 글로벌하게 바꾸려면 overlays를 씁니다.

Flake를 쓰지 않는 Nix에서는 ~/.config/nixpkgs/overlays.nix 또는 ~/.config/nixpkgs/overlays/*.nix 파일들로 overlays를 구성할 수 있었지만, Flake는 재현 가능성을 위해 Git 저장소를 벗어나는 설정을 쓸 수 없습니다.

Home Manager와 NixOS는 nixpkgs.overlays 옵션을 가지고 있습니다.

# ./overlays/default.nix
{ config, pkgs, lib, ... }:

{
  nixpkgs.overlays = [
    # Overlay 1: 상속 관계를 표현하기 위해 `self`와 `super`를 씁니다.
    (self: super: {
      google-chrome = super.google-chrome.override {
        commandLineArgs =
          "--proxy-server='https=127.0.0.1:3128;http=127.0.0.1:3128'";
      };
    })

    # Overlay 2: 새로운 것과 오래된 것을 나타내는 의미로 `final`과 `prev`를 씁니다.
    (final: prev: {
      steam = prev.steam.override {
        extraPkgs = pkgs: with pkgs; [
          keyutils
          libkrb5
          libpng
          libpulseaudio
          libvorbis
          stdenv.cc.cc.lib
          xorg.libXcursor
          xorg.libXi
          xorg.libXinerama
          xorg.libXScrnSaver
        ];
        extraProfile = "export GDK_SCALE=2";
      };
    })

    # Overlay 3: 다른 파일에 overlays를 정의합니다.
    # ./overlays/overlay3/default.nix 의 내용은 위와 같습니다.
    # `(final: prev: { xxx = prev.xxx.override { ... }; })`
    (import ./overlay3)
  ];
}

위와 같이 작성하고 flake.nix는 아래와 같이 작성해서 오버레이를 불러 옵니다.

# ./flake.nix
{
  inputs = {
    # ...
  };

  outputs = inputs@{ nixpkgs, ... }: {
    nixosConfigurations = {
      my-nixos = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./configuration.nix

          (import ./overlays) # 이렇게 폴더만 써주면, default.nix를 찾는다.
        ];
      };
    };
  };
}

폴더에 있는 모든 오버레이를 불러 오려면

{ pkgs, lib, ... }:
let
  overlays = builtins.attrValues (builtins.readDir ./overlays);
in
{ overlays = overlays; }

specialArgs

nixosConfigurations = {
  my-nixos = nixpkgs.lib.nixosSystem rec {
    system = ...
    specialArgs = s1 # <-- 자동으로 modules에 넘어 갑니다.
    modules = [m1 m2]
  }
}

m1m2s1에 접근 가능하다. specialArgs는 자동으로 모듈에 넘어 갑니다.

rust-overlay의 flake.nix 분석

rust-overlay/flake.nix에서 가져 왔습니다.

{
  description = ''
    Pure and reproducible overlay for binary distributed rust toolchains.
    A compatible but better replacement for rust overlay of github:mozilla/nixpkgs-mozilla.
  ''; # ''로 여러 줄 텍스트를 넣어 줄 수 있다.

  inputs = { # flake의 입력
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  };

  outputs = { self, nixpkgs }@inputs: let
    inherit (nixpkgs) lib; # lib = nixpkgs.lib 보통 유틸리티 함수를 쓸 때 필요한 것 같다.
    inherit (lib) filterAttrs mapAttrs' replaceStrings;  
    # filterAttrs = lib.filterAttrs 속성 필터
    # mapAttrs' = lib.mapAttrs' 속성 매핑
    # replaceStrings = lib.replaceStrings 문자열 치환

    forEachSystem = lib.genAttrs lib.systems.flakeExposed; # 함수 정의
    #                            지원되는 시스템 목록 제공
    #                            x86_64-linux, aarch64-linux, x86_64-darwin 같은 것들

    overlay = import ./.; # 현재 디렉토리에서 오버레이 가져 오기. 아마도 default.nix?

    defaultDistRoot = import ./lib/dist-root.nix;
    mkManifests = distRoot: import ./lib/manifests.nix { inherit lib distRoot; };
    #                                                    distRoot = lib.distRoot

    # Rust 도구 체인 인터페이스 빌더
    # Builder to construct `rust-bin` interface on an existing `pkgs`.
    # This would be immutable, non-intrusive and (hopefully) can benefit from
    # flake eval-cache.
    #
    # Note that this does not contain compatible attrs for mozilla-overlay.

    # Rust 바이너리 인터페이스
    mkRustBin =
      { distRoot ? defaultDistRoot }: # 값이 들어 오지 않으면, defaultDistRoot를 사용
                                      # 매개 변수의 기본값을 정의하는 구문
      pkgs:
      lib.fix (rust-bin: import ./lib/rust-bin.nix {
        inherit lib pkgs; # pkgs = lib.pkgs
        inherit (pkgs.rust) toRustTarget; # toRustTarget = pkgs.rust.toRustTarget
        inherit (rust-bin) nightly; # nightly = rust-bin.nightly
        manifests = mkManifests distRoot; # manifest: 파일 메타 정보 목록
                                          # 아마도 버전 목록?
                                          # {이름, 버전, 의존성, ...}을 받아 메니페스트 생성
      });

  in {
    lib = { 
      _internal = { # 내부에서만 사용할 라이브러리
        defaultManifests = mkManifests defaultDistRoot;
      };

      inherit mkRustBin; # 외부에서 접근 가능한 Rust 도구 체인 빌더
    };

    overlays = { # 패키지에서 특정 부분만 바뀌 치기 위한 오버레이
      default = overlay; # 현재 폴더에서 가져온 오버레이(위에서 정의된 변수)
      rust-overlay = overlay;
    };

    # TODO: Flake outputs except `overlay[s]` are not stabilized yet.

    packages = # 다양한 Rust 버전을 정의
      let
        select = version: comps: # 함수
          if comps ? default then
            comps.default // {
              minimal = comps.minimal or (throw "missing profile 'minimal' for ${version}");
            }
          else
            null;
        result = rust-bin: # 함수
          mapAttrs' (version: comps: {
            name = if version == "latest"
              then "rust" # latest면 그냥 rust
              else "rust_${replaceStrings ["."] ["_"] version}";
                  # ex) rust_1_81_0
            value = select version comps;
          }) rust-bin.stable //
          mapAttrs' (version: comps: { # 고차 함수를 인자로 넘기고 있다.
            name = if version == "latest"
              then "rust-nightly"
              else "rust-nightly_${version}";
            value = select version comps;
          }) rust-bin.nightly //
          mapAttrs' (version: comps: {
            name = if version == "latest"
              then "rust-beta"
              else "rust-beta_${version}";
            value = select version comps;
          }) rust-bin.beta;
        result' = rust-bin: filterAttrs (name: drv: drv != null) (result rust-bin);
      in
      forEachSystem (system:
        result' (mkRustBin {} nixpkgs.legacyPackages.${system})
        // { default = self.packages.${system}.rust; }
      );
      # todo: legacyPackage가 구버전 패키지를 뜻한다는데,
      # 신버전은 뭐고, 구버전은 뭐지?
    checks = forEachSystem (import ./tests inputs);
  };
}

genAttrs 함수

lib.genAttrs :: [String] -> (String -> a) -> {String = a}

lib.genAttrs [ "x86_64-linux" "aarch64-darwin" ] (system: "Hello, ${system}!")
#                                                           함수 
# 아래와 같습니다. 
{
  "x86_64-linux" = "Hello, x86_64-linux!";
  "aarch64-darwin" = "Hello, aarch64-darwin!";
}

filterAttrs 함수

속성 필터

mapAttrs’ 함수

속성 매핑

replaceStrings 함수

문자열 치환

Nixpkgs Manual https://ryantm.github.io/nixpkgs/

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com