关于 setopt 和 :custom 的模块加载行为问题

最近改动了一下自己的配置文件,发现一个 bug:

(setopt package-enable-at-startup nil)

我的 early-init.el 中的这行代码属于搬起石头砸自己的脚—— setopt 直接加载了 package-enable-at-startup 所属的 package.el 包,导致我 (with-eval-after-load 'package ...) 中修改 package-user-dir 的代码失效。

当初之所以从 setq 改成 setopt 是源于看到了 设定 user option 应该用 setq, setopt, custom-set-default, customize-set-value, 还是 customize-set-variable? - #5,来自 LdBeth 这篇介绍,于是决定把所有 customizable 的变量都改成用 setopt 来设置,没想到破坏了加载顺序。

让我困惑的是,实际查看 package-enable-at-startup 的 help 的时候,发现该变量是 autoload 的,而且也没有 :set 之类的东西,为什么会引起整个 package.el 的加载呢?同理,平时在使用 setopt 改动各种包变量的时候,如何保证不会破坏设计好的加载顺序?

我目前的做法是尽量把 setopt 都尽量放在 :config 中延迟运行,但如上所述,不能覆盖所有情况。难道要把所有非延迟运行的 setopt 都改成 setq 吗?


Update: 将标题从 “关于 setopt 和 setq 的使用问题” 改为更贴切于贴内讨论的 “关于 setopt:custom 的模块加载行为问题”

setopt 会运行 setter 函数,如果该变量的 defcustom 有声明函数的话。否则就和 setopt 差不多。因此你的情况就是当调用了 setopt 的时候,就运行了该变量对应的 setter 函数。

我个人的用法依然是只用 setq,只有当需要使用 setopt 也就是会调用 :set 函数的时候。

我原本的理解也是 setopt 调用 :set 导致的过早加载,但是 package-enable-at-startup 并没有定义 setter 函数。因此我对 setopt 到底什么情况下会加载整个 module 的行为有点不确定了。

FYI: 删掉注释之后,package.el 中的代码如下:

;;;###autoload
(defcustom package-enable-at-startup t
  :type 'boolean
  :version "24.1")

检查了一下 setopt 宏展开的结果以及调用链,发现 setopt 会调用 custom-load-symbol 这个函数,而这个函数会 require 需要 autoload 定义所在的包。

由此可见,setopt 的正确使用姿势应该在 config 或者 with-eval-after-load 的代码块里使用。其实用 use-package 的话,如果需要用到 setter,感觉可能用 :custom 应该是更好的选择。

3 个赞

根据您的提示,我刚抽空研究了一下:

  1. setopt 宏展开后调用 setopt--set,后者调用 custom-load-symbol
  2. custom-load-symbol 会加载变量的 custom-loads symbol property 中的所有包

我对于 custom-loads 不太了解,而且在我上面贴的 package-enable-at-startup 的定义中没有看到和这个相关的 keyword。因此,我抽样了几个配置文件中的 customizable 变量想实验一下,发现基本都是 (get SYMBOL 'custom-loads) => nil 的,包括其它的一些在 package.el 中定义的 customizable 变量。

在整个 emacs-mirror 的代码库中搜索 package-enable-at-startup 也没有看到有 put 的行为。

于是我猜测是不是只有 autoload 的变量才会有 custom-loads property,经实验:

  1. 自己随手写的 test module 中的 autoload 的 customizable 变量没有 custom-loads property
  2. package.el 中其它的 autoload 的 customizable 变量,如 package-user-dir 是有 custom-loads property 的

关于这个区别,我又发现 Emacs 会自动生成一个 cus-load.el 文件,里面有大量诸如

...
(custom--add-custom-loads 'package '(package package-vc package-x))
...

的调用,然后 custom--add-custom-loads 也确实有 put 的行为:

(defun custom--add-custom-loads (symbol loads)
  ;; Don't overwrite existing `custom-loads'.
  (dolist (load (get symbol 'custom-loads))
    (unless (memq load loads)
      (push load loads)))
  (put symbol 'custom-loads loads))

从代码层面能调查到的就这些了,我对 Emacs 本身的了解不足以有更多的宏观上的 insights 了(比如整个 customization 系统的依赖机制等)。

另外关于 use-package:custom 关键字,我之前的理解一直是把它视作 :init (setopt ...) + theme 的 toggle 机制,而且我也在多个场合看到有人提到这个是 setopt 诞生之前的“时代产物”。

趁此机会我也看了一下 :custom 所依赖的 custom-theme-set-variable 函数,其中也调用了 custom-load-symbol(虽然我不是百分百确定,因为我不太了解 standard-valuecustom-autoload 这两个 symbol property):

  ...
  (dolist (entry args)
    (let* ((symbol (indirect-variable (nth 0 entry))))
      (unless (or (get symbol 'standard-value)
                  (memq (get symbol 'custom-autoload) '(nil noset)))
        ;; This symbol needs to be autoloaded, even just for a `set'.
        (custom-load-symbol symbol))))
  ...

因此我觉得我的模糊理解还算恰当,:custom 相对于 setopt 并没有除了可以随 theme 开关以外的好处,并且还有着固定在包加载前设置变量的弊端(也可能不算弊端?但是从目前我们的讨论看来这个有同样的破坏加载顺序的问题)。

:custom 相对于 setopt 并没有除了可以随 theme 开关以外的好处,并且还有着固定在包加载前设置变量的弊端(也可能不算弊端?但是从目前我们的讨论看来这个有同样的破坏加载顺序的问题)。

检查了一下 use-package 的 :custom 的宏展开,你的理解是正确的。我之前以为 :custom 关键字后面的变量是会默认懒加载的 (展开后放入 eval-after-load 的 block 里面)。但是看起来并不是这样。