Emacs 31 的 lexical-binding cookie 警告与 elpa 重编译
从 Emacs 30 换到 31 之后,打开 org 文件会冒出一条警告:
Warning (files): Missing ‘lexical-binding’ cookie in "~/.emacs.d/elpa/toc-org-20260514.1415/toc-org.el".
You can add one with ‘M-x elisp-enable-lexical-binding RET’.
警告只在 .el 被选中时出现
这条检查针对的是没有 -*- lexical-binding: t -*- cookie 的源文件,而 .elc 不做这项检查 —— 所以正常加载字节码根本看不到它。Emacs 30 只在 byte-compile 时提出这件事,31 把它延伸到了加载阶段,于是升级之后才开始冒出来。
出现的前提只有两种:
load-prefer-newer为t,且.el比.elc新- 该包没有
.elc
配置从别的机器或别的 Emacs 版本整体拷过来时最典型:elpa/ 里的 .elc 是旧版本编译的,与 .el 的 mtime 只差几十毫秒且顺序颠倒,于是每次加载都走源码。
看某个包实际加载了哪个文件:
(let ((before (length load-history)))
(load "/path/to/toc-org")
(princ (car (nth (1- (- (length load-history) before)) load-history))))
;; => ".../toc-org.el" 走了源码
;; => ".../toc-org.elc" 走了字节码
批量找出 elpa 里被源码压过的包:
cd ~/.emacs.d/elpa
for f in */*.el; do [ -e "${f}c" ] && [ "$f" -nt "${f}c" ] && echo "$f"; done
修法一:按警告类型抑制
警告的 type 是嵌套列表 (files missing-lexbind-cookie "<绝对路径>"),可以按前缀匹配抑制:
(setq warning-suppress-types '((files missing-lexbind-cookie))
warning-suppress-log-types '((files missing-lexbind-cookie)))
这两个变量分工不同,只设第一个不生效(日志通道仍会输出),必须同时设。type 也必须写成嵌套列表:((missing-lexbind-cookie)) 匹配不上,裸写 '(files missing-lexbind-cookie) 直接报 wrong-type-argument listp。
修法二:重编译 elpa
M-x package-recompile-all 比抑制治本:.elc 更新之后 load-prefer-newer 自然选字节码,警告消失,也省掉每次解释源码的开销。
(require 'package)
(setq package-user-dir (expand-file-name "elpa" user-emacs-directory))
(package-initialize) ; 关键:激活 load-path
(package-recompile-all)
emacs -Q --batch -l recompile.el > /tmp/rc.log 2>&1
三个坑:
- 必须
package-initialize。只用package-load-all-descriptors读描述符的话,load-path里没有其它包,编译时凡(require 'avy)、(require 'dash)一律Cannot open load file,大面积失败 byte-compile-file在编译前会删掉旧的.elc,所以编译失败等于字节码文件直接消失。动手前备份整个elpa/,并且用 pax 格式(tar --format=posix)—— 普通 tar 把 mtime 截到整秒,恢复后同一秒内的源文件会被误判为「更新」- compat 包要单独补:
compat-26..31.el按文件名load "compat-macs.el",而 compat 目录不在load-path里,必然失败。把该目录 push 进load-path后逐个byte-compile-file(compat-macs.el本身返回no-byte-compile,宏文件正常)
编译过程会刷一堆 file has no ‘lexical-binding’ directive,那是编译期警告 —— 说明这些包按 dynamic binding 编译,语义和升级前一致。
为什么不直接给第三方包加 cookie
加 lexical-binding: t 会改变文件的求值语义(lexical 与 dynamic 对自由变量的处理不同),是包作者该判断的事,而且包一升级就被覆盖。抑制和重编译都不动源码。