Conao3 Notes

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

はじめに

この記事は「Emacs Advent Calendar 2019」の4日目の記事として書いたものです。

昨日は私の「leaf.elに依存したEmacs設定ファイル「init.el」をバイトコンパイルして爆速にする」でした。

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

去年のディレクトリ構造

去年、私は「Makefileで.emacs.dの理想的なディレクトリ構造を生成する話」を「Emacs Advent Calendar 2018」に投稿しました。

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

.
├── 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ですぐにジャンプできますし、leaf.elには強力な条件分岐キーワードがあります。

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

.
├── 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で見えています。

.
├── 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パッケージは多く存在しており、その情報は取り扱いに注意が必要なものもあります。

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

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

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

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

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

Makefile

現在の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-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」をバイトコンパイルして爆速にする」のまとめでも触れましたが、バイトコンパイルした後の elc はメジャーバージョンをまたいでの互換性はありません。

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

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

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

よって、init.elは以下のように、 user-emacs-directory を変更して、 conf.elc を読み込むだけです。

;;; 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の先頭ではleafの初期化を行って、あとは気のむくままにパッケージの設定を書いていきます。

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

(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 のロード追加されており、クラスタは色めきあっています。さらにEmacs-27からXDG Base Directory Specificationにのっとり、 .config/emacs/init.el を最優先で読み込むようになっているようです。(該当コミット / NEWS) (tomoyaさんが最初の報告、コミットとNEWSはelimさんが探してくだだり、emacs-jpのSlackで共有して頂いた情報です。)

ArchのWikiでもEmacsは「ハードコードされているソフトウェア」にリストされていますが、修正が必要になるかもしれません。

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

Emacs-27ももうすぐリリースされるようです(tak.kunihiroさんに共有して頂いた情報)。Emacsコントリビュータの人に感謝しつつ、いつか本体に貢献したいなと思っています。

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

https://www.patreon.com/conao3