Re: Makefileで.emacs.dの理想的なディレクトリ構造を生成する話



# はじめに

この記事は「[Emacs Advent Calendar 2019](https://qiita.com/advent-calendar/2019/emacs)」の4日目の記事として書いたものです。

昨日は私の「[leaf.elに依存したEmacs設定ファイル「init.el」をバイトコンパイルして爆速にする](https://qiita.com/conao3/items/42d34080b69b33ccf313)」でした。

まだ空きがあるので、ぜひ参加頂ければと思います!


# 去年のディレクトリ構造

去年、私は「[Makefileで.emacs.dの理想的なディレクトリ構造を生成する話](https://qiita.com/conao3/items/851f6dea9e94ce73f385)」を「[Emacs Advent Calendar 2018](https://qiita.com/advent-calendar/2018/emacs)」に投稿しました。

そこで提案したのは以下の構造でした。

    .
    ├── Makefile
    ├── Makefunc.mk
    ├── init.el
    ├── conf/
    │   ├── 00_leaf.el
    │   ├── 01_core-emacs.el
    │   ├── 10_standard-elisp.el
    │   ├── 20_editor.el
    │   ├── 30_utility.el
    │   └── 40_major-mode.el
    ├── site-lisp/
    │   ├── cort.el/
    │   ├── dash.el/
    │   ├── f.el/
    │   ├── feather.el/
    │   ├── leaf.el/
    │   ├── org-mode/
    │   ├── orglyth.el/
    │   ├── s.el/
    │   └── use-package/
    ├── snippets/
    ├── templete/
    └── local/
        ├── conao-mixed-raw.el
        ├── conao-mixed.el
        ├── 22.1/
        │   ├── build/
        │   │   ├── conao-mixed.el -> ../../conao-mixed.el
        │   │   └── conao-mixed.elc
        │   │   snippets/ -> ../../snippets/
        │   │   templete/ -> ../../templete/
        │   └── site-lisp/
        │       ├── cort.el/
        │       ├── ...
        │       └── use-pacakge/
        ├── 23.4/
        ├── 24.5/
        ├── 25.3/
        └── 26.1/

また、自動生成される前はこの構造です。

    .
    ├── Makefile
    ├── Makefunc.mk
    ├── init.el
    ├── conf/
    │   ├── 00_leaf.el
    │   ├── 01_core-emacs.el
    │   ├── 10_standard-elisp.el
    │   ├── 20_editor.el
    │   ├── 30_utility.el
    │   └── 40_major-mode.el
    ├── snippets/
    └── templete/


# 今年のディレクトリ構造

さて、1年使ってみると考えも変わってきます。大きく変わったのは「モダンなinit.elはファイル分割する」という観点です。実際、ファイル分割によって見通しのよいinit.el群の管理と読み込み順の管理、さらには条件付きの設定が実現できていました。

しかし、leaf.elの開発によって、巨大なinit.elを一つだけもっていたとしても[imenuですぐにジャンプ](https://qiita.com/conao3/items/db06dc1338aaf8e9b7b1#add-imenu-support-feature)できますし、leaf.elには[強力な条件分岐キーワード](https://qiita.com/conao3/items/dc88bdadb0523ef95878#%E6%9D%A1%E4%BB%B6%E5%88%86%E5%B2%90%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89)があります。

よって、現在ではこのようなディレクトリ構造になっています

    .
    ├── Makefile
    ├── Makefunc.mk
    ├── init.el
    ├── conf.el
    ├── site-lisp/
    │   ├── cort-test.el/
    │   ├── feather.el/
    │   ├── orglyth.el/
    │   └── leaf.el/
    ├── snippets/
    ├── templetes/
    └── local/
      ├── 22.1/
      ├── 23.4/
      ├── 24.5/
      ├── 25.3/
      ├── 26.1/
      ├── 26.2/
      └── 26.3/
          ├── Makefile
          ├── conf.el -> ../../conf.el
          ├── conf.elc
          ├── elpa/
          ├── snippets/ -> ../../snippets/
          ├── templetes/ -> ../../templetes/
          └── site-lisp/ -> ../../site-lisp/

なお自動生成前はこのようなディレクトリ構造です。つまりこの構造が[GitHubで見えて](https://github.com/conao3/dotfiles/tree/master/.dotfiles/.emacs.d)います。

    .
    ├── Makefile
    ├── Makefile-local.mk
    ├── init.el
    ├── conf.el
    ├── snippets/
    └── templete/

このディレクトリ構造は以下の特徴を持っています。

-   メジャーバージョンの異なる、複数のEmacsを同時に使用できる。
-   (M)ELPAからダウンロード、コンパイルしたパッケージは `local/[ver]/elpa/` 以下に置かれる。(異なるバージョン間で混同しない)
-   site-lispは全バージョンで同じディレクトリを使用する。(開発中のパッケージを置いてるので、いちいちコピーするのが面倒)
-   Emacsパッケージが保存するセンシティブな情報を一括で.gitignoreできる。

去年は気づいていませんでいませんでしたが、特に最後の特徴は重要な意味を持つことに気付きました。データの永続化のために `user-emacs-directory` (通常は `$HOME/.emacs.d` )にファイルを保存するEmacsパッケージは多く存在しており、その情報は取り扱いに注意が必要なものもあります。

やばそう(普通他人に見せない)なものの代表例はこちらです。

-   [session](https://github.com/search?utf8=%E2%9C%93&q=path%3A.emacs.d+filename%3Asession&type=Code&ref=advsearch&l=&l=)
-   [.emacs.desktop](https://github.com/search?q=path%3A.emacs.d+filename%3A.emacs.desktop&type=Code)
-   [tramp](https://github.com/search?utf8=%E2%9C%93&q=path%3A.emacs.d+filename%3Atramp&type=Code&ref=advsearch&l=&l=)
-   [recentf](https://github.com/search?utf8=%E2%9C%93&q=path%3A.emacs.d+filename%3Arecentf&type=Code&ref=advsearch&l=&l=)
-   [jisyo](https://github.com/search?q=path%3A.emacs.d+filename%3Ajisyo&type=Code) (ddskk)

これらはおそらく `.gitignore` 漏れだと思います。(意図してgit管理している場合もあるのでしょうが。。)

重要なのは、「どのEmacsパッケージが、どんな名前で情報を保存するのかは分からない」という点です。

この状況でブラックリスト管理するのは無理があります。たしかに `.gitignore` にもホワイトリスト管理する方法がありますが、簡単なのは 「 `user-emacs-directory` を `local` 以下に変更して、 `local` を `.gitignore` する」という方法です。

これなら `user-emacs-directory` 以下にどれだけセンシティブな情報を保存したとしてもまとめてignoreされます。


# Makefile

現在のMakefileを紹介します。これを見ると、[去年](https://qiita.com/conao3/items/851f6dea9e94ce73f385#makefile%E5%85%A8%E6%96%87)よりは簡単になっています。

```makefile
## Makefile

all:

XARGS := xargs $(shell if ( echo | xargs -r > /dev/null 2>&1 ); then echo "-r"; else echo ""; fi)

DEV_PKGS := leaf.el leaf-keywords.el orglyth.el cort-test.el seml-mode.el navbar.el
DEV_PKGS  += feather.el feather-server.el leaf-browser.el solarized-emacs
DEV_PKGS  += liskk.el
DEV_PKGS  += phantom-inline-comment annotate.el point-history
DEV_PKGS  += ivy-posframe helm-swoop flylint

EMACS_LST_RAW := 22.0 22.1 22.2 22.3
EMACS_LST_RAW  += 23.0 23.1 23.2 23.3 23.4
EMACS_LST_RAW  += 24.0 24.1 24.2 24.3 24.4 24.5
EMACS_LST_RAW  += 25.0 25.1 25.2 25.3
EMACS_LST_RAW  += 26.0 26.1 26.2 26.3
EMACS_LST_RAW  += 27.0

EMACS_LST := $(shell echo $(EMACS_LST_RAW) | sed "s/ /\n/g" | $(XARGS) -I% sh -c "if type emacs-% >/dev/null 2>&1; then echo %; fi")
DIRS := local

export XARGS
export DEV_PKGS

##################################################

.PHONY: all clean
.PRECIOUS: site-lisp/%
all: $(DIRS) $(DEV_PKGS:%=site-lisp/%) $(EMACS_LST:%=local/%)
    @$(call ECHO_YELLOW,"make job:all completed!!","\n","\n")

$(DIRS):
    mkdir -p $@

##############################

site-lisp/%:
    @mkdir -p $(@D)
    $(if $(wildcard ~/dev/repos/$*), cd site-lisp; ln -sf ~/dev/repos/$*,\
    $(if $(wildcard ~/dev/forks/$*), cd site-lisp; ln -sf ~/dev/forks/$*,\
    cd site-lisp; git clone https://github.com/conao3/$*.git))

local/%: local conf.el Makefile-local.mk
    mkdir -p $@
    cp -f Makefile-local.mk $@/Makefile
    cp -f conf.el $@/conf.el
    $(MAKE) -C $@

##############################

clean-elc:
    find site-lisp -follow -name "*.elc" | $(XARGS) rm -rf

clean:
    -rm -rf local
    @$(call ECHO_CYAN,"make job:clean completed!!","\n","\n")
```

```makefile
## Makefile-local

all:

EMACS ?= emacs-$(shell basename $$(pwd))
XARGS ?=

SYMS := latex-math-preview-cache lice snippets templates conf.el

##################################################

all: conf.elc $(SYMS)

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

%.el: ../../%.el

$(SYMS):
    ln -sf ../../$@
```


# バイトコンパイルについて

昨日の記事、「[leaf.elに依存したemacs設定ファイル「init.el」をバイトコンパイルして爆速にする](https://qiita.com/conao3/items/42d34080b69b33ccf313)」の[まとめ](https://qiita.com/conao3/items/42d34080b69b33ccf313#%E3%81%BE%E3%81%A8%E3%82%81)でも触れましたが、バイトコンパイルした後の `elc` はメジャーバージョンをまたいでの互換性はありません。

つまり異なるメジャーバージョンのEmacsを同時に動かそうとすると `elc` にコンパイルできませんでした。

しかし、 `.emacs.d/init.el` はバージョン間で共通にしておいて、本体を別のファイルにしておけば、そのファイルはバイトコンパイルできることに気付きました。

ということでディレクトリ構成もそうなっていて、 `.emacs.d/init.el` は全バージョン共通(バイトコンパイルしない)、`conf.el` は `.emacs.d/local/{{ver}}/conf.el` からシンボリックリンクを張り、それぞれのバージョンで `conf.elc` にバイトコンパイルすることになっています。

よって、[init.el](https://github.com/conao3/dotfiles/blob/master/.dotfiles/.emacs.d/init.el)は以下のように、 `user-emacs-directory` を変更して、 `conf.elc` を読み込むだけです。

```el
;;; init.el

(prog1 "Change user-emacs-directory"
  ;; enable debug
  (setq debug-on-error  t
        init-file-debug t)

  ;; you can run like 'emacs -q -l ~/hoge/init.el'
  (when load-file-name
    (setq user-emacs-directory
          (expand-file-name (file-name-directory load-file-name))))

  ;; change user-emacs-directory
  (setq user-emacs-directory
        (expand-file-name
         (format "local/%s.%s/"
                 emacs-major-version emacs-minor-version)
         user-emacs-directory)))

(condition-case err
    (load (expand-file-name "conf" user-emacs-directory))
  (error
   (error "You need `make' before running Emacs! Raw err: %s" err)))
```

[conf.el](https://github.com/conao3/dotfiles/blob/master/.dotfiles/.emacs.d/conf.el)の先頭ではleafの初期化を行って、あとは気のむくままにパッケージの設定を書いていきます。

注意点として、私はleafの開発版をsite-lispに持っているので、少しインストールコードが異なっています。

```el
(eval-when-compile
  (setq user-emacs-directory
        (expand-file-name (file-name-directory default-directory))))

(eval-and-compile
  (defvar package-archives)
  (setq package-archives '(("org"   . "https://orgmode.org/elpa/")
                           ("melpa" . "https://melpa.org/packages/")
                           ("gnu"   . "https://elpa.gnu.org/packages/")))
  (package-initialize)

  (add-to-list 'load-path (locate-user-emacs-file "site-lisp/leaf.el"))
  (add-to-list 'load-path (locate-user-emacs-file "site-lisp/leaf-keywords.el"))

  (require 'leaf)

  (leaf leaf
    :config
    (leaf leaf-keywords
      :require t
      :init
      (leaf package
        :init
        (leaf *elpa-workaround
          :when (or (version= "26.1" emacs-version)
                    (version= "26.2" emacs-version))
          :custom ((gnutls-algorithm-priority . "NORMAL:-VERS-TLS1.3")))
        :config
        (leaf *elsa-workaround
          :custom ((package-user-dir . "/home/conao/.emacs.d/local/26.3/elpa"))))

      (leaf hydra
        :ensure t
        :config
        (leaf *hydra-posframe
          :when (version<= "26.1" emacs-version)
          :when window-system
          :custom `((hydra-hint-display-type . 'posframe)
                    (hydra-posframe-show-params
                     . ',(list :internal-border-width 1
                               :internal-border-color "red"
                               :poshandler 'posframe-poshandler-window-bottom-right-corner)))))

      (leaf el-get
        :ensure t
        :init (unless (executable-find "git")
                (warn "'git' couldn't found. el-get can't download any packages"))
        :custom ((el-get-git-shallow-clone  . t)))

      (leaf diminish
        :ensure t)

      (leaf cus-edit
        :custom `((custom-file . ,(locate-user-emacs-file "custom.el"))))

      :config
      (leaf-keywords-init)))

  (leaf *libraries
    :config
    (leaf dash :ensure t :require t)
    (leaf ts :ensure t :require t)
    (leaf buttercup :ensure t :require t)
    (leaf ert-async :ensure t :require t)))

  ;; あとはパッケージの設定を書く
  (leaf ...)
```


# まとめ

色んなバージョンのEmacsを使うために、このディレクトリ構造にしましたが、思いがけず不要なファイルをgit管理しない構造になっていたことを発見しました。

この構造にメリットを見いだす人はわずかだとは思いますが、備忘録として残しておきます。

なお、Emacs-27からは `early-init.el` のロード追加されており、[クラスタは色めきあって](https://www.reddit.com/r/emacs/comments/7yns85/emacs_adds_support_for_a_second_read_earlier_init/)います。さらにEmacs-27から[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)にのっとり、 `.config/emacs/init.el` を最優先で読み込むようになっているようです。([該当コミット](https://github.com/emacs-mirror/emacs/commit/4118297ae2fab4886b20d193ba511a229637aea3) / [NEWS](https://github.com/emacs-mirror/emacs/blob/4118297ae2fab4886b20d193ba511a229637aea3/etc/NEWS#L135-L142)) 
(tomoyaさんが最初の報告、コミットとNEWSはelimさんが探してくだだり、[emacs-jpのSlack](https://emacs-jp.github.io/)で共有して頂いた情報です。)

[ArchのWiki](https://wiki.archlinux.jp/index.php/XDG_Base_Directory_%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88)でもEmacsは「ハードコードされているソフトウェア」にリストされていますが、修正が必要になるかもしれません。

これらのupstreamの修正により私のdotfilesも変更を迫られるでしょうが、過度に保守的にならず、現在も修正が加えられているというのは喜ばしいことです。

[Emacs-27ももうすぐリリース](https://lists.gnu.org/archive/html/emacs-devel/2019-11/msg00970.html)されるようです(tak.kunihiroさんに共有して頂いた情報)。Emacsコントリビュータの人に感謝しつつ、いつか本体に貢献したいなと思っています。

最後になりますが、Patreonでご支援を頂ける方を募集しています。普段はleafなどのElispパッケージなどを中心にOSS活動をしつつ、学生をしています。ぜひよろしくお願いします!

<https://www.patreon.com/conao3>