新しいパッケージマネージャー 【Elpaca】

# 背景

これまで様々なパッケージマネージャが登場していましたが、現在`use-package`を除くと`package.el`と[straight.el](https://github.com/radian-software/straight.el)が人気のようです。

[Emacs User Survey — 2022](https://emacssurvey.org/results/3425413930)
[Emacsパッケージマネージャ決定版：これからのパッケージ管理はstraight.elで決まり！](https://nukosuke.hatenablog.jp/entry/straight-el)

私も以下の点から[straight.el](https://github.com/radian-software/straight.el)を愛用していました。

- `package.el`は一度インストールしたパッケージは、ちゃんと削除しないとロードされてしまう。
- melpaがたまに壊れていることがあり、アップデートを行うとEmacsが動かなくなることがある。

しかし、[straight.el](https://github.com/radian-software/straight.el)はインストールとアップデートに時間がかかり、またWindowsだと`straight-pull-all`のあと自動でビルドしてくれないのが不満に感じていました。

この`package.el`と[straight.el](https://github.com/radian-software/straight.el)の不満を解消する方法はないかと思っていたところ、[elpaca](https://github.com/progfolio/elpaca)というパッケージを見つけました。

# [elpaca](https://github.com/progfolio/elpaca)とは

[elpaca](https://github.com/progfolio/elpaca)は[straight.el](https://github.com/radian-software/straight.el)と同様に`git clone`でパッケージをインストール・ビルドを行いますが、非同期で処理することで高速にインストールされるようになっています。
[straight.el](https://github.com/radian-software/straight.el)よりも高速にインストール・更新が可能になっています。

## 【Windows】シンボリックリンクを有効にする

Elpacaではシンボリックリンクが必須となっていますので、下記の方法を使用する必要があります。

### 方法１：開発者モードを利用する
Emacs30を利用すると特別な設定もなく開発者モードを有効にするだけでシンボリックリンクが使えましたので、私はEmacs30＋開発者モードにしています。

Scoopのインストール
```
$ Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
$ irm get.scoop.sh | iex
```
Emacs30のインストール（[Emacs Wiki](https://www.emacswiki.org/emacs/GccEmacs#h5o-13)）
```
$ scoop bucket add kiennq-scoop https://github.com/kiennq/scoop-misc
$ scoop install emacs-k
```

https://www.ipentec.com/document/windows-windows-10-set-developer-mode

### 方法２：管理者権限で実行する
Windowsでシンボリックリンク有効にするには管理者権限で実行する必要があります。
[アプリケーションを管理者権限で実行する方法（Windows 10/8/7/Vista）](https://helpx.adobe.com/jp/x-productkb/global/230789.html)

https://qiita.com/ucho/items/c5ea0beb8acf2f1e4772

# [elpaca](https://github.com/progfolio/elpaca)のインストール

下記のコードを`init.el`に追加して、[elpaca](https://github.com/progfolio/elpaca)をインストールします。

```init.el
(defvar elpaca-installer-version 0.5)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
                              :ref nil
                              :files (:defaults (:exclude "extensions"))
                              :build (:not elpaca--activate-package)))
(let* ((repo  (expand-file-name "elpaca/" elpaca-repos-directory))
       (build (expand-file-name "elpaca/" elpaca-builds-directory))
       (order (cdr elpaca-order))
       (default-directory repo))
  (add-to-list 'load-path (if (file-exists-p build) build repo))
  (unless (file-exists-p repo)
    (make-directory repo t)
    (when (< emacs-major-version 28) (require 'subr-x))
    (condition-case-unless-debug err
        (if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
                 ((zerop (call-process "git" nil buffer t "clone"
                                       (plist-get order :repo) repo)))
                 ((zerop (call-process "git" nil buffer t "checkout"
                                       (or (plist-get order :ref) "--"))))
                 (emacs (concat invocation-directory invocation-name))
                 ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
                                       "--eval" "(byte-recompile-directory \".\" 0 'force)")))
                 ((require 'elpaca))
                 ((elpaca-generate-autoloads "elpaca" repo)))
            (progn (message "%s" (buffer-string)) (kill-buffer buffer))
          (error "%s" (with-current-buffer buffer (buffer-string))))
      ((error) (warn "%s" err) (delete-directory repo 'recursive))))
  (unless (require 'elpaca-autoloads nil t)
    (require 'elpaca)
    (elpaca-generate-autoloads "elpaca" repo)
    (load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
```

また一度に多くのパッケージをインストール・アップデートする場合、`too many open files`というエラーが出る可能性が高いので、下記のコードを追加しキューのリミットを設定しておきます。
※端末のスペックに応じて変更してください）

```init.el
(setq elpaca-queue-limit 15)
```

# パッケージのインストール

[elpaca](https://github.com/progfolio/elpaca)は`queue`に溜まったパッケージを非同期にインストール・ロードしていきます。
下記のように記述すると`queue`にパッケージや処理が溜まっていきます。

```init.el
(elpaca org)

(elpaca xxx
  (setq xxx-yyy nil)
  (hogehoge)
  (fogafoga))
```

また、`use-package`も下記のようにインストールすることで利用できるようになります。

```init.el
;; Install use-package support
(elpaca elpaca-use-package
  ;; Enable :elpaca use-package keyword.
  (elpaca-use-package-mode)
  ;; Assume :elpaca t unless otherwise specified.
  (setq elpaca-use-package-by-default t))
```

## elpaca-wait

`elpaca-wait`はパッケージのインストール・ロードが完了するまで、処理を待ってくれます。`use-package`等、`init.el`の構成に使用しているパッケージは`elpaca-wait`を使用してあらかじめインストールしておきます。


```init.el
;; Install use-package support
(elpaca elpaca-use-package
  ;; Enable :elpaca use-package keyword.
  (elpaca-use-package-mode)
  ;; Assume :elpaca t unless otherwise specified.
  (setq elpaca-use-package-by-default t))

(elpaca general)

;; Block until current queue processed.
(elpaca-wait)
```

## elpaca-process-queues

`elpaca-process-queues`はqueueに溜まっているパッケージの設定を非同期に実行してくれるコマンドです。公式では下記のように`after-init-hook`のあとに実行するようになっています。

```init.el
(add-hook 'after-init-hook #'elpaca-process-queues)
```

しかし、`dashboard`等のパッケージを利用している場合、上記のタイミングだと都合が悪い場合があるので、私は`init.el`の末尾に直接`elpaca-process-queues`を呼び出しています。

```init.el
;;; package --- Summary
;;; Commentary:
;;; Code:

;; 1.elpacaのインストール
;; 2.init.elの設定に使用するパッケージ
(elpaca-wait)

;; 3.各設定の記述
(use-package hogehoge)
(use-package fugafuga

;; 3で記述された処理を非同期に実行
(elpaca-process-queues)

(provide 'init)
;;; init.el ends here
```

# 設定例

```init.el
;;; package --- Summary
;;; Commentary:
;;; Code:

(defvar elpaca-installer-version 0.5)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
                              :ref nil
                              :files (:defaults (:exclude "extensions"))
                              :build (:not elpaca--activate-package)))
(let* ((repo  (expand-file-name "elpaca/" elpaca-repos-directory))
       (build (expand-file-name "elpaca/" elpaca-builds-directory))
       (order (cdr elpaca-order))
       (default-directory repo))
  (add-to-list 'load-path (if (file-exists-p build) build repo))
  (unless (file-exists-p repo)
    (make-directory repo t)
    (when (< emacs-major-version 28) (require 'subr-x))
    (condition-case-unless-debug err
        (if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
                 ((zerop (call-process "git" nil buffer t "clone"
                                       (plist-get order :repo) repo)))
                 ((zerop (call-process "git" nil buffer t "checkout"
                                       (or (plist-get order :ref) "--"))))
                 (emacs (concat invocation-directory invocation-name))
                 ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
                                       "--eval" "(byte-recompile-directory \".\" 0 'force)")))
                 ((require 'elpaca))
                 ((elpaca-generate-autoloads "elpaca" repo)))
            (progn (message "%s" (buffer-string)) (kill-buffer buffer))
          (error "%s" (with-current-buffer buffer (buffer-string))))
      ((error) (warn "%s" err) (delete-directory repo 'recursive))))
  (unless (require 'elpaca-autoloads nil t)
    (require 'elpaca)
    (elpaca-generate-autoloads "elpaca" repo)
    (load "./elpaca-autoloads")))
;; init.elの最後に実行
;; (add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))

;; queueの上限設定
(setq elpaca-queue-limit 15)

;; use-packageなどの設定に使用するパッケージを記述
(elpaca elpaca-use-package
  (elpaca-use-package-mode)
  (setq elpaca-use-package-by-default t))
(elpaca general)

;; 設定に利用するパッケージをセットアップ
(elpaca-wait)

;;各パッケージの設定を記述
(use-package evil
  :config
  (evil-mode +1))

(use-package vertico
  :config
  (vertico-mode +1))

;; 設定を非同期でロード
(elpaca-process-queues)

(provide 'init)
;;; init.el ends here
```

# 実際の様子

[elpaca](https://github.com/progfolio/elpaca)はUIも提供してくれているので、進捗状況を一覧で確認することができます。

![qiita-output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696232/47c6923c-2787-7b9e-f642-c7fc73e50052.gif)

# 注意点

【2023/03/30】init.elをバイトコンパイルして`init.elc`の状態で[elpaca](https://github.com/progfolio/elpaca)を呼び出すとエラーがでます。修正されるまでバイトコンパイルせずに運用したほうが良さそうです。

# 所感

最初はエラーが色々出ていたり、導入に手こずっていましたが、
慣れると非常に快適になりました。

開発が活発に行われているので、今後更に機能が改善されることを楽しみにしています。