Nix + Emacs

Nix が流行っています。僕は 1 年半 NixOS を使っていますが、僅か 1 週間で完全に抜き去られました。インテリだ〜〜。この波に乗って Nix のパワーユーザを目指します。

やりたいこと

今一番面白い takeokunn/nixos-configuration を参考に、 Emacs パッケージを Nix で管理してみます。これでパッケージのバージョン固定や rollback が可能になるはずです。反面、エディタの更新に nixos-rebuild が必要になって面倒……ということもなく、 Nix 化されていないパッケージは従来どおり elpa/ に保存されます。

先に結論

進捗は以下のコミットです。ほぼ変更無しで Emacs パッケージを Nix 化できます。

ファイル分割を読む

takeokunn/nixos-configuration を読んで基本的な Nix Flakes を学びます。

nvfetcher.toml

nvfetchernvfetcher.toml を元に hash 値 の集まりを生成します。最新のソースを表す hash 値への更新がコマンド 1 つで実施できる点が便利です。

nixpkgs に登録されていないパッケージは nvfetcher.toml に記載し、自分でビルドする方針のようです。詳しくは後ほど紹介します。

flake.nix

設定ファイルを読んでいきます。 NixOS, macOS に両対応しています。

{
  inputs = { # 1
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-24.05";
    # Emacs の HEAD ビルド等ができる `overlay`.
    emacs-overlay = {
      url = "github:nix-community/emacs-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
      inputs.nixpkgs-stable.follows = "nixpkgs-stable";
    };
    # `org-babel-tangle` の Nix 式による実装 (すごい)
    org-babel.url = "github:emacs-twist/org-babel";
  };
  outputs = { self, nixpkgs, home-manager, org-babel, emacs-overlay, ... }: { # 2
      # macOS における設定
      darwinConfigurations = (import ./hosts/OPL2212-2 {
        inherit self nixpkgs nix-darwin home-manager org-babel emacs-overlay
          wezterm-flake neovim-nightly-overlay;
      });
      # NixOS における設定
      nixosConfigurations = (import ./hosts/X13Gen2 { # 3
        inherit self nixpkgs home-manager org-babel emacs-overlay; # 4
      });
    };
}

hosts/X13Gen2/default.nix

ここから NixOS の設定ファイルを読み込んでいます。

{ self, nixpkgs, home-manager, org-babel, emacs-overlay }: # 1
let
  username = "<name>";
  system = "x86_64-linux";
in {
  X13Gen2 = nixpkgs.lib.nixosSystem { # 2
    inherit system;
    specialArgs = { inherit xremap username; }; # 3
    # `configuration.nix` および `hardware-configuration.nix` 相当
    modules = [ # 4
      # ルートユーザの設定
      ../../nixos
      # ハードウェアとかドライブの情報 (?)
      ./hardware-configuration.nix
      # ログインユーザ (?) の設定
      home-manager.nixosModules.home-manager
      {
        home-manager.useUserPackages = true;
        home-manager.users."${username}" = import ../../home-manager { # 5
          inherit system nixpkgs org-babel emacs-overlay;
        };
      }
    ];
  };
}

home-manager/default.nix

分割された設定ファイルを imports で指定しています。

{ system, nixpkgs, org-babel, emacs-overlay }:
let
  lib = nixpkgs.lib;
  pkgs = import nixpkgs {
    inherit system;
    config.allowUnfree = true;
    overlays = import ./overlay { inherit emacs-overlay; }; # 1
  };
  advancedPkgs = import ./packages/advanced.nix { inherit pkgs; };
  sources = pkgs.callPackage ../_sources/generated.nix { }; # 2
  # その他省略
in {
  imports = modules ++ basicPrograms ++ advancedPrograms ++ basicServices ++ advancedServices; # 3
  home.stateVersion = "24.05";
  home.packages = basicPkgs ++ advancedPkgs ++ lib.optionals pkgs.stdenv.isDarwin darwinPkgs;
}

home-manager/programs/advanced.nix

Emacs 部分に注目すると、 import しているだけです:

{ lib, pkgs, org-babel, sources }:
let
  emacs = import ./emacs { inherit pkgs org-babel sources; };
in [
  emacs
]

./emacs/default.nix が肝心の Emacs の設定ファイルですね。

Emacs 部分を読む

本題です。 Emacs の部分はどうなっているのでしょうか。

home-manager/programs/emacs/default.nix

emacs-overlay を使用しています。

{ pkgs, org-babel, sources }:
let tangle = org-babel.lib.tangleOrgBabel { languages = [ "emacs-lisp" ]; }; # 1
in {
  programs.emacs = { # 2
    enable = true;
    package = pkgs.emacsWithPackagesFromUsePackage { # 3
      config = ./elisp/init.org;
      defaultInitFile = true;
      package = pkgs.emacs-git; # 4
      alwaysTangle = true;
      extraEmacsPackages = import ./epkgs { inherit pkgs sources; }; # 5
    };
  };

  home.file = { # 6
    ".config/emacs/init.el".text = tangle (builtins.readFile ./elisp/init.org);
    ".config/emacs/early-init.el".text =
      tangle (builtins.readFile ./elisp/early-init.org);
    ".config/emacs/yasnippet.org".source = ./yasnippet.org;
  };

  home.packages = with pkgs; [ emacs-lsp-booster pinentry-emacs cmigemo ];
}

home-manager/programs/emacs/epkgs/default.nix

これはパッケージのリストを作るだけですね。リストされたパッケージは load-path に入ります。

{ pkgs, sources }:
epkgs:
let
  ai = import ./packages/ai { inherit epkgs pkgs sources; };
  # 略
in ai ++ awesome ++ buffer ++ client ++ coding ++ cursor ++ dired ++ elfeed
++ eshell ++ eww ++ exwm ++ file ++ ime ++ language ++ language_specific
++ monitor ++ org ++ project ++ remote_access ++ themes ++ search ++ window

nixpkgs に無いパッケージは、 nvfetcher によりソース指定して melpaBuild により Nix 化しています。

{ sources, epkgs }: {
  rainbow-csv = epkgs.melpaBuild {
    pname = "rainbow-csv";
    version = "0.0.1";
    src = sources.emacs-rainbow-csv.src;

    packageRequires = with epkgs; [ csv-mode ];

    ignoreCompilationError = false;
  };
  ## ~~
}

……ハッ、それだけ?! すごいエコシステムです。使い方を調べるのは非常に大変だと思いますが、今回は take さんに便乗できて楽できました。今後もフォースペンギンぐらいの歩き方をして行こうかなと……。

備考: nvfetcher の使い方

smyx への修正 マージまでの間、 fork を nvfetcher 経由で使ってみることにしました。

もちろん straightelpaca を使っても良いです。

source を作成する

以下の nvfetcher.toml にリポジトリの一覧を記載します:

[emacs-smyx]
src.git = "https://github.com/toyboot4e/smyx"
src.branch = "master"
fetch.github = "toyboot4e/smyx"

nvfetcher コマンドで .nix を生成します:

$ nvfetcher
# CheckGit
    url: https://github.com/toyboot4e/smyx
    branch: master
Changes:
emacs-smyx: ∅ → 97a2e1ef2bcffd34e43b1cabad17d317e41258ec
$ ls _sources/
generated.json  generated.nix

ファイル内容は次の通りです:

# This file was generated by nvfetcher, please do not modify it manually.
{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }:
{
  emacs-smyx = {
    pname = "emacs-smyx";
    version = "97a2e1ef2bcffd34e43b1cabad17d317e41258ec";
    src = fetchFromGitHub {
      owner = "toyboot4e";
      repo = "smyx";
      rev = "97a2e1ef2bcffd34e43b1cabad17d317e41258ec";
      fetchSubmodules = false;
      sha256 = "sha256-1UZRtQ74p4xAuB6JXFHMxrNBO9BG6JPRYLvEeCOz5rc=";
    };
    date = "2024-09-14";
  };
}

generated.nix を読み込みビルドする

generated.nix の読み込みには pkgs.callPackage が使われています:

let sources = pkgs.callPackage ./_sources/generated.nix { };
in {
  # 以降 import の度に `sources` を渡して行く
}

extraPackagesepkgs.el で指定することにします:

# home-manager の各ユーザ設定ファイルにて
programs.emacs = {
  enable = true;
  package = pkgs.emacsWithPackagesFromUsePackage {
    config = ../../editor/emacs-leaf/init.org;
    defaultInitFile = false; # true;
    package = pkgs.emacs-unstable; # pkgs.emacs-git;
    alwaysTangle = true;
    alwaysEnsure = true;
    extraEmacsPackages import ./epkgs { inherit pkgs sources; }; # 1
  };
};

Emacs パッケージは epkgs.melpaBuild でビルドできます:

# emacs packages
{ pkgs, sources }: epkgs: [
  # meplaBuild:
  # https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/editors/emacs/build-support/melpa.nix

  (epkgs.melpaBuild {
    # pname = "smyx";
    pname = "smyx-theme";
    version = "0.0.1";
    src = sources.emacs-smyx.src;
    # packageRequires = with epkgs; [];
    # files = ["smyx-theme.el"];
    ignoreCompilationError = false;
  })
]

nixos-rebuild して完成です:

$ git add _sources
$ git add epkgs.nix
$ sudo nixos-rebuild --flake .#tbm switch

まとめ

emacs-overlay により一挙に Emacs の Nix 化ができることが分かりました。

課題