5

Emacs 包管理指南

 3 years ago
source link: https://liujiacai.net/blog/2021/05/05/emacs-package/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

对于 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 函数可以声明函数或宏,在真正使用时再去加载其对应的文件。

(autoload filename docstring interactive type)

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

;;;###autoload
(defun my-hello ()
(interactive)
(message "hello world"))

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

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

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

;;; hello-world-autoloads.el --- automatically extracted autoloads
;;
;;; Code:

(add-to-list 'load-path (directory-file-name
                      (or (file-name-directory #$) (car load-path))))


;;;### (autoloads nil "hello-world" "hello-world.el" (0 0 0 0))
;;; Generated autoloads from hello-world.el

(autoload 'my-hello "hello-world" nil t nil)

;;;***

;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; coding: utf-8
;; End:
;;; 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 文件的机制,使用示例:

(defun my-hello ()
  (interactive)
  (message "hello world"))

;; feature 名与文件名相同
(provide 'hello-world)

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

Load

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

上面的 autoload 与 feature 都会调用 load 函数去加载文件,load 为相对低级的 API,不推荐上层直接调用。

Submodule 管理包

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

  1. 解析依赖,递归下载

  2. 把包目录追加到 load-path 中

  3. 自动生成 autoloads 文件,并且加载它

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

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

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

(progn
  (eval-and-compile
    (add-to-list 'load-path "~/.emacs.d/vendor/lsp-mode"))
  (eval-and-compile
    (add-to-list 'load-path "~/.emacs.d/vendor/lsp-mode/clients"))

  (let
      ((custom--inhibit-theme-enable nil))
    (unless
        (memq 'use-package custom-known-themes)
      (deftheme use-package)
      (enable-theme 'use-package)
      (setq custom-enabled-themes
            (remq 'use-package custom-enabled-themes)))
    (custom-theme-set-variables 'use-package
                                '(lsp-log-io nil nil nil "Customized with use-package lsp-mode")))
  (unless
      (fboundp 'lsp-deferred)
    (autoload #'lsp-deferred "lsp-mode" nil t))
  (unless
      (fboundp 'lsp-find-definition)
    (autoload #'lsp-find-definition "lsp-mode" nil t))
  (unless
      (fboundp 'lsp-find-references)
    (autoload #'lsp-find-references "lsp-mode" nil t))
  (unless
      (fboundp 'lsp)
    (autoload #'lsp "lsp-mode" nil t))
  (condition-case-unless-debug err
      (setq lsp-keymap-prefix "C-c l")
    (error
     (funcall use-package--warning139 :init err)))
  (eval-after-load 'lsp-mode
    '(progn
       (require 'lsp-modeline)
       (push "[/\\\\]vendor$" lsp-file-watch-ignored-directories)
       t)
    (add-hook 'go-mode-hook #'lsp-deferred)
    (bind-keys :package lsp-mode :map lsp-mode-map
      ("M-." . lsp-find-definition)
      ("M-n" . lsp-find-references))
    ))

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

;; lsp-mode deps
(use-package spinner
  :defer t)
(use-package lv
  :defer t)
;; ...

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

use-package bootstrap

(package-initialize)
(when (not package-archive-contents)
  (package-refresh-contents))

(dolist (p '(use-package))
  (when (not (package-installed-p p))
    (package-install p)))

(setq use-package-always-ensure t
      use-package-verbose t)

;; 后面就可以直接使用 use-package 来安装、配置包了

常用 Git 命令

# 修改 .gitmodules 后
git submodule sync

# 更新到最新 commit
git submodule update --init --recursive --remote

# https://stackoverflow.com/a/18854453/2163429
# 更新到 .gitmodules 中指定的 commit
git submodule update --init

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK