Emacs 包管理指南

发布: 2021-05-05   上次更新: 2026-03-30   分类: 效率工具   标签: emacs

文章目录

对于 Emacs 用户来说,优化自己的配置是件乐趣无穷的事情,而且也是成为 Emacs 高手的必经之路。一般来说,新手的配置都是东拼西凑出来的,这是最快最有效的学习途径。随着对 Emacs 使用的加深,配置逐渐复杂,如果不对之前杂乱无章的配置进行重构,很难想象可以继续愉快地使用 Emacs。

这篇文章就来介绍一下我个人优化配置的心得,主要内容:包的加载原理与管理实践,希望对读者优化自己的配置有些帮助。

Package.el 问题

毫不夸张地说,高度的扩展性是 Emacs 延续几十年生生不息的主要原因, (length package-alist) 可以统计通过 package.el 安装的包数量,我是 137 个。

尽管 package.el 提供了一种便捷的方式来安装包,但它并不提供版本管理的功能,这是任何一个包管理器最基础的功能,我曾经多次因为包升级导致功能失效,这是十分让人沮丧的事情,参考这里

社区有一些解决方案,比如 straightborg ,但为了避免引入新问题,减轻学习负担,我目前没有采用这些方案,而是用 git 自带的 submodule 来管理一些重度使用的包(比如 lsp-mode/magit),闲暇时再专门去做升级工作,升级出问题直接回退到之前的 commit 即可,再也不用担心被工具打断的烦恼。

包加载原理

对于 package.el 管理的包,用户是无须了解 Emacs 加载包的方式就可以使用,但是如何要自己完全管理,就需要了解这些细节了。首先明确下包的定义:

包是一个多个 ELisp 文件的集合,Emacs 在 load-path 指定的文件夹中进行搜索。

Emacs 提供了两类高阶接口来进行包的自动加载:Autoload 与 Feature。

Autoload

Autoload 函数可以声明函数或宏,在真正使用时再去加载其对应的文件。

1(autoload filename docstring interactive type)

一般不直接使用 autoload 函数,而是 autoload 魔法注释,然后用一些函数来解析魔法注释自动生成 autoload 函数,比如在 my-mode 文件夹内有一文件 hello-world.el ,内容为:

1;;;###autoload
2(defun my-hello ()
3(interactive)
4(message "hello world"))

使用下面的命令生成 autoloads 文件

1(package-generate-autoloads "hello-world" "~/my-mode")

在同一目录生成 hello-world-autoloads.el 文件,内容为:

 1;;; hello-world-autoloads.el --- automatically extracted autoloads
 2;;
 3;;; Code:
 4
 5(add-to-list 'load-path (directory-file-name
 6                      (or (file-name-directory #$) (car load-path))))
 7
 8
 9;;;### (autoloads nil "hello-world" "hello-world.el" (0 0 0 0))
10;;; Generated autoloads from hello-world.el
11
12(autoload 'my-hello "hello-world" nil t nil)
13
14;;;***
15
16;; Local Variables:
17;; version-control: never
18;; no-byte-compile: t
19;; no-update-autoloads: t
20;; coding: utf-8
21;; End:
22;;; hello-world-autoloads.el ends here

这意味着只在第一次 M-x my-hello 时,才回去加载 hello-world.el 文件。

这里需要注意,为了让 Emacs 识别到 my-hello 函数的声明,需要去加载 hello-world-autoloads.el 文件,对于通过 package.el 管理的包,package.el 在下载该包时,会进行下面的操作:

  1. 解析依赖,递归下载
  2. 把包目录追加到 load-path 中
  3. 自动生成 autoloads 文件,并且加载它

这样用户就能够直接使用该包提供的函数了。如果使用 submodule 管理,上述操作则需要自己实现,后文会介绍。

Feature

Feature 是 Emacs 提供的另一种自动加载 ELisp 文件的机制,使用示例:

1(defun my-hello ()
2  (interactive)
3  (message "hello world"))
4
5;; feature 名与文件名相同
6(provide 'hello-world)

上述代码即生成了一个 feature,名为 hello-world,由于与文件同名,只需在使用 my-hello 前 (require 'hello-world) 即可,这样就会去自动加载 hello-world.el。

feature 的一个优势就是可以防止重复加载,所有被加载的 feature 会被记录在 features 变量中,只有在第一次 require 时才会去加载文件,后续的 require 则会直接返回。如果想要重复加载,需要先调用 unload-feature 进行卸载。

Load

1(load filename &optional missing-ok nomessage nosuffix must-suffix)

上面的 autoload 与 feature 底层都是通过调用 load 函数去加载文件,但 load 为相对低级的 API,功能虽然比较丰富,但用起来不如上述两个高级 API 方便,因此一般 Emacs 用户不会直接使用它。

Submodule 管理包

在上面介绍 autoload 时,介绍了 package.el 下载一个包时的大致步骤,这里重新温故下:

  1. 解析依赖,递归下载
  2. 把包目录追加到 load-path 中
  3. 自动生成 autoloads 文件,并且加载它

使用 submodule 的话,只能下载包本身,上述三步都需要自己做,我目前使用 use-package 来下载、配置包,下面通过一个示例来介绍其用法:

 1(use-package lsp-mode
 2  ;; 配置 load-path,lsp-mode 通过 submodule 下载到 ~/.emacs.d/vendor/lsp-mode 目录内
 3  :load-path ("~/.emacs.d/vendor/lsp-mode" "~/.emacs.d/vendor/lsp-mode/clients")
 4  :init (setq lsp-keymap-prefix "C-c l")
 5  ;; 配置 mode 的 hook
 6  :hook ((go-mode . lsp-deferred))
 7  ;; 生成 autoloads
 8  :commands (lsp lsp-deferred)
 9  ;; 配置 custom 变量
10  :custom ((lsp-log-io nil))
11  :config
12  (require 'lsp-modeline)
13  (push "[/\\\\]vendor$" lsp-file-watch-ignored-directories)
14  ;; 配置 mode-map 快捷键
15  :bind (:map lsp-mode-map
16              ("M-." . lsp-find-definition)
17              ("M-n" . lsp-find-references)))

可以看到,use-package 宏的使用非常简明扼要,而且把包的各种配置都统一起来了,强烈推荐。使用 macroexpand-1 展开 use-package,会发现和我们手动配置的代码相差无几:

 1(progn
 2  (eval-and-compile
 3    (add-to-list 'load-path "~/.emacs.d/vendor/lsp-mode"))
 4  (eval-and-compile
 5    (add-to-list 'load-path "~/.emacs.d/vendor/lsp-mode/clients"))
 6
 7  (let
 8      ((custom--inhibit-theme-enable nil))
 9    (unless
10        (memq 'use-package custom-known-themes)
11      (deftheme use-package)
12      (enable-theme 'use-package)
13      (setq custom-enabled-themes
14            (remq 'use-package custom-enabled-themes)))
15    (custom-theme-set-variables 'use-package
16                                '(lsp-log-io nil nil nil "Customized with use-package lsp-mode")))
17  (unless
18      (fboundp 'lsp-deferred)
19    (autoload #'lsp-deferred "lsp-mode" nil t))
20  (unless
21      (fboundp 'lsp-find-definition)
22    (autoload #'lsp-find-definition "lsp-mode" nil t))
23  (unless
24      (fboundp 'lsp-find-references)
25    (autoload #'lsp-find-references "lsp-mode" nil t))
26  (unless
27      (fboundp 'lsp)
28    (autoload #'lsp "lsp-mode" nil t))
29  (condition-case-unless-debug err
30      (setq lsp-keymap-prefix "C-c l")
31    (error
32     (funcall use-package--warning139 :init err)))
33  (eval-after-load 'lsp-mode
34    '(progn
35       (require 'lsp-modeline)
36       (push "[/\\\\]vendor$" lsp-file-watch-ignored-directories)
37       t)
38    (add-hook 'go-mode-hook #'lsp-deferred)
39    (bind-keys :package lsp-mode :map lsp-mode-map
40      ("M-." . lsp-find-definition)
41      ("M-n" . lsp-find-references))
42    ))

use-package 解决了繁琐的配置问题,但并不能解决包依赖的问题,只能一个个下载(具体依赖见包的 Package-Requires 声明):

1;; lsp-mode deps
2(use-package spinner
3  :defer t)
4(use-package lv
5  :defer t)
6;; ...

use-package 在 load-path 中找不到这些依赖时,会自动利用 package.el 去下载。我这里的做法是折中的,对于一些轻量的包,没必要用 submodule 管理。读者可能会觉得这种手动管理依赖的方式会比较繁琐,但是实际上不同包的依赖很有可能是相同的,比如:dash.els.elf.el 等这些基础包,所以实际需要手动管理的依赖不是很多。

use-package bootstrap

 1(package-initialize)
 2(when (not package-archive-contents)
 3  (package-refresh-contents))
 4
 5(dolist (p '(use-package))
 6  (when (not (package-installed-p p))
 7    (package-install p)))
 8
 9(setq use-package-always-ensure t
10      use-package-verbose t)
11
12;; 后面就可以直接使用 use-package 来安装、配置包了

常用 Git 命令

1# 修改 .gitmodules 后
2git submodule sync
3
4# 更新到最新 commit
5git submodule update --init --recursive --remote
6
7# https://stackoverflow.com/a/18854453/2163429
8# 更新到 .gitmodules 中指定的 commit
9git submodule update --init

对于 submodule 的增加与删除,直接在 magit 中操作就好了, magit-status-mode 下按 o 即可。

参考

评论

欢迎读者通过邮件与我交流,也可以在 MastodonTwitter 上关注我。