CaskからQuelpaに移行する

自分の環境でCaskが限界に達して僕の力ではデバッグも諦めたのでQuelpaに移住したメモです。

## 概要

Emacsの標準パッケージ管理ツールとして`package.el`があります。これは**ELPA**(Emacs Lisp Package Archive)と呼ばれるパッケージリポジトリからLispファイルをダウンロードして管理するための仕組みです。

`package.el`を補強するためのツールのひとつに[Cask](https://github.com/cask/cask)が、私の環境では最近ちょっとエラーがいろいろ出て、日常用途でのEmacsのパッケージ管理には断念せざるを得ない決断をするところまできました。**なのでQuelpaに移住します**。

## 注意

この記事はtadsanが一晩で[自分の`init.el`](https://github.com/zonuexe/dotfiles/tree/master/.emacs.d)を動かすためだけにQuelpaを触った範囲で書いたので、事実誤認があったらごめんね。

筆者はまだ二晩程度しかQuelpaに触れてない程度でのメモ書きなので、自分の中でもまだちゃんと運用に乗ってません。

## 最初にまとめ

私がCaskからQuelpaに移行するために行った変更はこちら
https://github.com/zonuexe/dotfiles/pull/1/files

 * CaskでやってたことはQuelpaで概ねできます
 * ただしQuelpaはめっちゃディスクスペースとります (数百MB〜1GB程度)
 * 最初のインストール時はかなり時間かかります (数十分)
 * アップデートするときもちょっと待ちます (僕の環境では2分)
 * QuelpaはMELPA以外のリポジトリを対象としないので、別途アプローチが必要になります
 * `use-package`を組み合せることで、Quelpaへの依存を減らせる可能性があります

この記事は飽くまでメモであり、Quelpa導入方法の決定版として書かれたものではありません。

こんな記事を書いておいてなんですが、**Quelpaよりも[El-Get](https://github.com/dimitri/el-get)を使った方が簡単に運用できる可能性があります**。

## ELPAとMELPA

[2015年Emacsパッケージ事情](https://qiita.com/tadsan/items/6c658cc471be61cbc8f6)に概ね書いた通りですが、ELPAには公式のパッケージリポジトリである[GNU ELPA](https://elpa.gnu.org/packages/)*(Emacsの初期設定)*と、その他の有志による非公式のリポジトリ*(ユーザーが各自で設定する必要がある)*があります。[MELPA](https://melpa.org/)はその中でも、現時点で最大規模で、パッケージ更新が最も頻繁なリポジトリです。

自作LispパッケージのMELPAへの追加は[GitHubのPull request](https://qiita.com/tadsan/items/b74856f555720a5ca2e5)で、一度追加されたパッケージの更新は`git push`だけで完結するのも大きな特徴です。

MELPAはELPAと互換性があり、URLをリストに追加するだけでMELPAのパッケージも利用することができるのが特徴です。 **(ただし、今回の記事の主題となるQuelpaを利用する場合は、その設定は不要です)**

この記事では以下の用語は明確に区別して書いてるので気をつけてください。

 * **ELPA**: Emacs Lispのパッケージリポジトリの総称
 * **GNU ELPA**: GNU公式のELPA
 * **MELPA**: 非公式のELPA

## なぜ`package.el`だけで済まないのか

ELPAに登録されたものだけを使ってるぶんにはCaskや、今回紹介する`Quelpa`のようなツールは基本的には不要です。しかし実際にはELPAに登録に登録されてないバージョンのパッケージや、開発中ブランチ、MELPAに登録してない自作のパッケージなどをインストールしたくなります。

ELPAに登録されたパッケージと同列にGitHubやBitBacketなどのVCSリポジトリ(以後、単に**VCS** (*Version Control System*)と呼ぶことがあります)からもEmacs Lispパッケージをインストールできるとべんりなことがあります。

別にそんな用事はねーよって方は、`use-package`を利用するとパッケージを簡単に管理できるかもしれません。

## Caskとは何か

Caskは基本的に`packages.el`のラッパーです。特定のディレクトリにEmacs Lispパッケージをダウンロードするためのツールですが、登録されたELPAリポジトリだけではなく、特定のVCSリポジトリ(Git, Mercurial, Subversion, etc...)からも取得することができます。

これは`gem`コマンドに対する[Bundler](http://bundler.io/)に相当し、Lispパッケージ作者にとっては**開発時に必要なパッケージのみ記述する**場合などに便利です。Caskの基本的な利用方法は[`Cask`ファイルに独自DSL](https://cask.github.io/dsl.html)で依存パッケージ一覧を記述します。

`Cask`ファイルに書き込む内容のDSLは構文はS式ですが、Emacs Lispとして実行できるように定義された関数ではありません。 (なので、このコードを実行することはできません)

```el:Cask
(source gnu)
(source melpa)
(source org)

(depends-on "0xc")
(depends-on "2048-game")
(depends-on "aa-edit-mode")
(depends-on "ac-geiser")
(depends-on "dmacro" :git "git@github.com:zonuexe/dmacro.git")
(depends-on "phpunit" :git "git@github.com:nlamirault/phpunit.el.git" :branch "develop")
(depends-on "mode-test" :git "git@github.com:emacs-php/mode-test.git" :files ("mode-test.el"))
```

このように記述することで、ELPAに登録されてないパッケージでもGitHubなどのリポジトリから直接インストールできるようになります。また、[Pallet](https://github.com/rdallasgray/pallet/tree/b8d0df1883224a371ac0a3bc9b9c1c4dc61e6ac0)パッケージでpackage.elと高度に統合することもできます。

## Quelpaとは何か

[Quelpa](https://github.com/quelpa/quelpa)もCaskと同じく`package.el`のラッパーですが、Caskとはアプローチが異なります。

 * MELPAのレシピを利用するが、任意のELPAを利用できるわけではない
 * 可能な限り必ずソースコードをVCSリポジトリから取得して利用する
 * ビルドしたファイルは通常のELPAと同じディレクトリ(通常は`~/.emacs.d/elpa/`)に保存される
 * Quelpaで追加したパッケージはELPAでインストールされたものと同様にロード可能 (＝Emacs起動時にはquelpa不要)

上記の`Cask`ファイルは、以下のように**ほぼ一対一**に置換できます。ファイル名はなんでもいいのですが、`~/.emacs.d/my-packages.el`とします。

```my-packages.el
;;; Code:

(require 'quelpa (locate-user-emacs-file "site-lisp/quelpa/quelpa"))

(package-initialize)

(quelpa 'exec-path-from-shell) ; 必ず先頭に置く
(exec-path-from-shell-initialize) ; PATHをインポートする

(quelpa '0xc)
(quelpa '2048-game)
(quelpa 'aa-edit-mode)
(quelpa 'ac-geiser)
(quelpa 'dmacro '(:fetcher github :repo "zonuexe/dmacro"))
(quelpa 'phpunit '(:fetcher github :repo "nlamirault/phpunit.el" :branch "develop"))
(quelpa 'mode-test '(:fetcher github :repo "emacs-php/mode-test" :files ("mode-test.el")))

;;; my-packages.el ends here
```

これは`Cask`ファイルと違って、普通のEmacs Lispのスクリプトです。特別なものではなく普通の関数なので、スクリプトの先頭から順に実行されます。

このフォーマットはCaskと微妙に違って、[MELPAのRecipe Format](https://github.com/melpa/melpa#recipe-format)と互換性があります。

QuelpaのREADMEでは`(quelpa '(dmacro :fetcher github :repo "zonuexe/dmacro"))`のような記法ですが、実行結果は同じになります。好きな書きかたで大丈夫です。

## Quelpaの準備

どこでもいいので[Quelpa](https://github.com/quelpa/quelpa)を持ってきます。

私は `~/.emacs.d/site-lisp/quelpa` に配置することにしました。あと私は`.emacs.d`を含めて[dotfilesリポジトリ](https://github.com/zonuexe/dotfiles)に集約してますので、これをsubmoduleで持つことにします。

```sh
mkdir -p ~/.emacs.d/site-lisp
cd ~/.emacs.d/site-lisp
git submodule add git@github.com:quelpa/quelpa.git
```

### Cask依存を外す

`init.el`にこのような行があったら**消します**。

```init.el
(require 'cask)
(cask-initialize)
(pallet-mode t)
```

### `site-lisp`以下のファイルをオートロード可能にする

こんな感じの`Makefile`を書きました。

```
EMACS ?= emacs

ELS = site-lisp/quelpa/quelpa.el
ELCS = $(ELS:.el=.elc)

all: site-autoload elc elpa

%.elc: %.el
	$(EMACS) -Q -batch -L . -f batch-byte-compile $<

elc: $(ELCS)

elpa: my-packages.el
	$(EMACS) -batch -l my-packages.el

site-autoload: $(ELS)
	$(EMACS) -Q -batch -L . --eval \
	"(progn \
           (require 'package) \
	   (package-generate-autoloads \"site-lisp\" (locate-user-emacs-file \"site-lisp\")))"

clean:
	@rm $(ELCS) site-lisp/site-lisp-autoloads.el

.PHONY: all clean
```

これで`make site-autoload`を実行すると`/site-lisp/site-lisp-autoloads.el`が生成されます。`init.el`で`site-lisp`のディレクトリに`load-path`を追加した上で読み込みます。

```init.el
(let ((default-directory (locate-user-emacs-file "./site-lisp")))
  (add-to-list 'load-path default-directory)
  (normal-top-level-add-subdirs-to-load-path))
(load (locate-user-emacs-file "./site-lisp/site-lisp-autoloads.el") t)
```

### `Cask`ファイルを移行する

先程挙げた`my-packages.el`です。

依存したいパッケージの中に[Mercurial](https://www.mercurial-scm.org/)(`hg`)に依存するものがあったのですが、私の環境では`/usr/local/bin/hg`にあったので[exec-path-from-shell](https://github.com/purcell/exec-path-from-shell)でシェルと同じ`PATH`をインポートする必要があります。なので、これを上の方に書いてやります。

Quelpaの罠なのですが `(quelpa 'hogehoge)`のように存在しないパッケージ名を選択しても**何のエラーも起こりません**。同様に、MELPAに存在しないパッケージやMELPAから削除されてしまったパッケージについても怒られません。これはつらい。

### ヘルパーコマンドを用意する

`M-x my/quelpa-setup` を起動すると、さくっと初期化できるようにします。

```init.el
(defun my/quelpa-setup ()
  "Setup Quelpa packages."
  (interactive)
  (load (locate-user-emacs-file "my-packages")))
```

### VCSとMELPA以外のパッケージ

MELPAとGNU ELPAの両方に登録されたパッケージはいくつかあるのですが([ivy](https://github.com/abo-abo/swiper)とか[YASnippet](https://elpa.gnu.org/packages/yasnippet.html)とか)、GNU ELPAにしかないパッケージも存在します。私が深く依存するパッケージとしては[undo-tree](https://elpa.gnu.org/packages/undo-tree.html)がそうでした。

私は[use-package](https://github.com/jwiegley/use-package)ユーザーなので`:ensure t` と付けるだけで、`package.el`を使っていい感じにインストールしてくれます。

```init.el
(use-package undo-tree :ensure t
  :diminish undo-tree-mode
  :init
  (global-undo-tree-mode)
  (bind-key "C-_" #'undo-tree-undo)
  (bind-key "C-?" #'undo-tree-redo))
```

おてがる！

## 運用方法

### パッケージのアップデート

`M-x quelpa-upgrade`を実行して、しばらく待ちます。

### インストールしたパッケージの削除

ふつうのパッケージと同じように`M-x list-packages`で消したいパッケージを選んで<kbd>d</kbd>を押してチェックしてから<kbd>x</kbd>で実行してください。`my-packages.el`からは手動で消します。

## Quelpaの欠点

いっぱいあります。

* 挙動の癖を理解しないとわかりにくい
* 全パッケージのVCSリポジトリをぜんぶ取得してくるのでディスクを消費する
    * 私の依存するパッケージで約1GB
* ビルドがめっちゃ時間かかる
    * Cask使ってたときにちゃんと時間を計測したことはないですが、体感で倍かかります
* アップデートの差分がわかりにくい
    * `cask update`したときの出力が好きでした

## Quelpaの長所

* **Caskではない**
* 起動時に読み込むべきものがない (`(package-initialize)` 以外)
* つまり起動時のコストはかからない
* Palletとか追加しなくてもpackage.elと統合されてる

## 今回やらなかったこと

### Caskからのインポート/エクスポート

今回は`Cask`ファイルをQuelpaの形式に手動で変換しましたが、構造が簡単なのでQuelpa用に`Cask`ファイルをインポートすることも、`Cask`からQuelpa用にファイルをエクスポートすることも、割と簡単に作れます。~~誰かやってみてね。~~

### QuelpaをVCSインストールのためだけに利用する

ひょっとして、ELPAに登録されたパッケージは`use-package`の`:ensure t`に頼って、QuelpaではVCSリポジトリに依存するものだけを入れるようにした方が話が早かったのでは……？

まあ、困ってないので、そのうち検証するかも。

### El-Getの検証

5年前はEl-Getを使ってたので、出戻ってもなあ… みたいな。

### Borgの検証

[Borg](https://github.com/emacscollective/borg)は[Magit!](https://magit.vc/)のメイン開発者のJonas Bernoulliが作った独特なパッケージ管理ツールです。

Caskからサクッと移行できるような感じではないので、今回の対象にはしませんでした。少なくとも初心者向けではないです。[Emacsair! Assimilate Emacs packages using Git submodules](https://emacsair.me/2016/05/17/assimilate-emacs-packages-as-git-submodules/)とか[Borg User Manual: Top](https://emacsmirror.net/manual/borg/)をよく読んで使ってみてください。

## あとがき

`cask update` がエラーしか吐かなくなって試行錯誤してたら、うっかりと `rm -rf .cask` してしまって依存パッケージの一切が消えてなくなってしまったときはどうなることかと思って半泣きだったのですが、意外にどうにかなりましたね ヾ(〃＞＜)ﾉﾞ

この記事に書きもらしたこともあるかもしれにあので、僕がCaskからQuelpaに移行した全差分を見たい型はプルリックを見てください。
https://github.com/zonuexe/dotfiles/pull/1/files