LoginSignup
6
4

More than 3 years have passed since last update.

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

Posted at

はじめに

この記事は「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-directorylocal 以下に変更して、 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活動をしつつ、学生をしています。ぜひよろしくお願いします!

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4