无法通过配置gnus-directory更改gnus-cache-directory的值

最近在尝试使用gnus阅读邮件列表,在配置gnus目录路径的时候发现了这样奇怪的事情:

(use-package gnus
  :custom
  (gnus-home-directory
   (concat user-emacs-directory "gnus/")) ;; 目录已创建
  (gnus-directory
   (concat user-emacs-directory "gnus/News/")) ;; 目录已创建
)

然后发现,这两个变量的值的确设置成了我预期的值,但是,依赖它们的custom变量并没有被设置为预期的值。例如:

(defcustom gnus-cache-directory
  (nnheader-concat gnus-directory "cache/")
  "The directory where cached articles will be stored."
  :group 'gnus-cache
  :type 'directory)

它的值依赖gnus-directory的值,在配置了gnus-directory之后,它应该被重新求值,得到"~/.config/emacs/gnus/News/cache/"。但是,观察到的值是:

Its value is "~/News/cache/"
Original value was 
"~/.config/emacs/gnus/News/cache/"

我使用的是org literate启动。如果我把配置换成:preface (setq ...)的话,在启动时用org-babel-tangle-file产生el文件再加载不会成功,直接加载tangle之后的文件可以成功。

这是怎么回事呢?

P. S. 我启用了package-quickstartnative-compuse-package-always-defer

看了下这两 option,都没有 :set ,所以就是加载时普通地初始化一下,设置其中的一个,并不会引发另一个的自动设置。

option 加载前设置的话,默认行为是保留设置的值。

不是很熟 org 式配置 :rofl:

看上去是lexical-binding的锅,如果使能了lexical-binding,defcustom的值就会从当前的lexcial scope动态捕获。如果参数STANDARD是一个表达式,默认是会重新求值的。

`(custom-declare-variable
    ',symbol
    ,(if lexical-binding
         ;; The STANDARD arg should be an expression that evaluates to
         ;; the standard value.  The use of `eval' for it is spread
         ;; over many different places and hence difficult to
         ;; eliminate, yet we want to make sure that the `standard'
         ;; expression is checked by the byte-compiler, and that
         ;; lexical-binding is obeyed, so quote the expression with
         ;; `lambda' rather than with `quote'.
         ``(funcall #',(lambda () "" ,standard))
       `',standard)

从org启动的时候,我用的是org-babel-load-file,此时eval应该默认没有lexical-binding,所以重新求值的时候捕获的是旧的词法绑定的值,而不是我的配置里的值。但是生成el之后,因为我在文件头加了lexical-binding: t,此时我的配置在lexical scope里了,所以就生效了。

;; Don't attempt to find/apply special file handlers to files loaded during startup.
(let ((file-name-handler-alist nil)
      (literate-config (concat user-emacs-directory "koishimacs.org"))
      (code-config (concat user-emacs-directory "koishimacs.el")))
  (when (file-exists-p literate-config)
    ;; If config is pre-compiled and newer, then load it
    (if (and (file-exists-p code-config)
             (file-newer-than-file-p code-config literate-config))
        (load-file code-config)
      (progn
        ;; Otherwise use org-babel to tangle the literate configuration
        (require 'org)
        (org-babel-load-file literate-config)))
    ))

我对这里lexical scope的判定有一些疑问,你觉得是这样子吗?

此外对custom-theme-set-variables的行为为什么是这样,我还没有搞清楚。

我测试了一下,不是这样的。看起来org-babel-tangle-file的过程本身改变了某些顺序。

不用 org-babel-tangle-file, 只 tangle 出 el 加载会这个有问题吗?具体来说,不使用 org-babel-tangle-file, 而只用 org-babel-tangle 生成配置文件,然后把生成的 el 放在 load path 下,走 Emacs 默认的加载流程,还会有这个问题吗?如果不用 use-package 呢?

有没可能构造一个最小的可复现代码?

我在init.el里加入了(debug-on-variable-change 'gnus-cache-directory)

追踪的结果是:

  • 加载org文档的时候触发了set-auto-mode,因此会触发自动加载orgol-gnusgnus-sum,以及gnus。所以gnus的加载时机总是早于use-package。因此无论是:init还是:custom的求值时机都会晚于defcustom
  • yy说的是对的,如果没有配置:set,并不会触发重新求值。

因此,:custom甚至:preface都在使用org-babel-load-file的时候不生效。唯一能够阻止这件事发生的办法就是,把gnus的配置提前到加载org之前。但这并不是我希望的literate startup,我要做的事情就是加载org的时候规避ol-gnus,方法是先覆盖org-modules,等加载literate startup的时候再用use-package org重新设置它的值。

(let ((literate-config (concat user-emacs-directory "koishimacs.org"))
      (code-config (concat user-emacs-directory "koishimacs.el")))
  (when (file-exists-p literate-config)
    ;; If config is pre-compiled and newer, then load it
    (if (and (file-exists-p code-config)
             (file-newer-than-file-p code-config literate-config))
        (load-file code-config)
      (progn
        ;; Otherwise use org-babel to tangle the literate configuration
        (setq org-modules nil)
        (require 'org)
        (org-babel-load-file literate-config))))
  )
2 个赞

此外,我发现不可以用:custom设置gnus-select-method,如果在:custom里设置这个变量就会触发gnus提前加载,它居然会触发autoload

;; `M-x customize-variable RET gnus-select-method RET' should work without
;; starting or even loading Gnus.
;;;###autoload(custom-autoload 'gnus-select-method "gnus")

(defcustom gnus-select-method

这种做法破坏了其它custom变量配置的延迟加载,因此这个符号的加载必须要推迟到其它custom变量后面才行(比如,在use-package的:config部分进行配置)。我想这不会是个例,很多包可能都无意间做了这种陷阱式的custom。

无论是:custom还是setopt都有副作用,影响加载时机是最烦的,我现在设定custom variable一般用doomemacs的setq!宏,如果不想用这个宏我的建议是直接在包加载之前用defvar

很正确。defcustom允许其初始值引用其它变量,甚至允许在值被修改的时候触发同步的求值,这意味着defcustom不适合被延迟加载,因为延迟加载本身是异步进行求值,而发生在异步上下文中的同步行为往往都是不安全的(除非它能被交换到一个同步的上下文里去)。

举一个例子。org在加载时会根据变量org-babel-load-languages加载后端,如果你修改了它的值,它的:set会自动触发org-babel-do-load-languages加载对应的包。看起来很棒对不对?

考虑这个真实情况:ox-latex里有一个custom变量org-babel-latex-process-alist,它的初始值依赖org里的org-preview-latex-process-alist

假如我们现在想要(autoload #'org-babel-tangle-file "org"),但是又在包org:custom里写了(org-babel-load-languages '((latex . t)))

这时候就会产生一个bug,假如我们在org加载之前触发了(autoload #'org-babel-tangle-file "org"),那么它首先(require 'org),接着,因为我们在:custom里修改了org-babel-load-languages,于是自动触发了ox-latex的加载。注意,这个时候我们只能确定org-babel-load-languages这个变量已经加载,不确定整个org已经完成加载,因为defcustom被执行的时候就会触发:set

然后,org-babel-latex-process-alist被加载,然而这个时候org-preview-latex-process-alist还没有被加载,因为org-babel-load-languages在第290行,而org-preview-latex-process-alist在更靠后的3330行。于是org的加载就这么失败了。

目前我解决此类问题的办法是人为的改变求值顺序,例如这里可以把org-babel-load-languages的修改推后规避。即使现在提供一个统一的异步方案,开发者和用户大概率也不会买账。