如何优雅得为不从package.el安装的包生成autoloads?

Emacs: 智能感知和操作光标处的语法对象 继续讨论:

我也开始转用submodule了, 主要还是我用的不少包在github上都是一个包, 上了melpa就被拆分成N个包, 这实在是太蠢了.

但是这个autoloads没有什么好的办法生成, 不知道 @manateelazycat 有什么好办法?

我用 lazy-set-key.el lazycat-emacs/init-key.el at master · manateelazycat/lazycat-emacs · GitHub

submodule 先加到 load-path 路径中.

然后所有按键第一次按的时候再在runtime加载对应插件, 默认只加载很小很小的一部分插件. 大部分插件都是指头驱动加载的.

2 个赞

可以参考abo-abo/oremacs里面的一个函数,将不同目录里面的el文件中的autoload生成到一个文件,最后只load这个文件就是了。

https://github.com/abo-abo/oremacs/blob/ca8a92ce339fc419d89a47103a09956b699f3354/auto.el#L596-L610

但是这个方法也有一个痛点,有些插件并没有给函数打上 ;;;###autoload 的标记。

没有的自己手动加到配置里就行了

如果用use-package, 没有加 autoload 的可以用commands` 关键字自动生成。

准备把use-package也丢了, 所以来问问. use-package还是有时不太爽.

管理大量包的时候还是挺好使的。如果不用只能手动加上(autoload) 语句了。

这点不同意。在 github 上是一个项目便于开发&管理,并不等于说分发也只能一个包。melpa 下载量最高的 dash,就是一个项目分发两个包。拆分的目的就是为了让用户按需下载,不下载用不到的文件。

上 melpa 需要经过 package-lint,至少可以避免一些错误。上 elpa 就更严格了。

用 (m)elpa 不必操心依赖问题,从源代码安装就要手动管理了。

(m)elpa 至少解决了编译问题。简单的包无所谓,但是像 org 这样的包自行编译需要费些功夫。从仓库克隆的 org 跟从 gnu elpa/org 下载到的是不一样的,后者是 make 过的代码。

(m)elpa 上的包在使用/管理上比较简单,从仓库克隆的源代码目录组织各有不同,但是放到 (m)elpa 就统一了。

(m)elpa 缺点我认为是:

  1. 更新慢。bug 早就修复了,melpa 那边可能还没审核到(好像现在只有 purcell 一个人在审核?);
  2. 不能回滚和锁定版本,这点确实没有安全感,什么时侯爆炸不可控。
4 个赞

大部分的拆分也就是在melpa上拆分, dash和dash-functional在elpa上是一个包. use-package和bind-key一个repo, 在melpa上use-package把bind-key作为显式依赖. 最有趣的莫过于ivy, 在elpa上叫ivy, github repo叫做swiper, 在melpa上分成三个包, 安装一个counsel就能安装一套.

大部分拆分我觉得都是没有必要的, 我以前试用helm的时候发现大部分基本套件都被helm拆散了(甚至核心部分都被拆分成helm和helm-core)不如counsel一个文件来得开箱即用和方便(当然还有ivy的依赖). 另一方面,dash和dash-functional作为很基本的api封装库. 大部分插件都会用到. (我用了90多个包, 两个都安排上了.)

从仓库clone对一边hack一边使用方便, 可以直接看到作者的README文档, 然后再看看源代码查查API, 要改出自己的功能会比较方便.

至于编译, 大部分插件似乎都没有问题. org-mode我从不更新, 可能就一个auctex比较麻烦?

melpa似乎是机器人自动拉取更新, purcell也就管第一次上线的包审核罢. 不然他自己一个人的精力怕是管不动.

我试试看 哈哈哈

这点我记错了,是第一次上线审核,除非 recipe 文件更新。

Emacs插件的机制本来就是基于Elisp热替换环境和各种 hook, advice

所以相互依赖本身就很脆弱, 如果建立依赖的机制, 只要一个包升级挂了, 其他稳定的包都会挂, 即使其他包一点毛病都没有.

想遇到这种依赖底层的函数的问题, 比如判定光标是否在字符串中, 我都会在我的很多插件中重复一部分的代码 (名字改一下):

(defun color-rg-current-parse-state ()
  "Return parse state of point from beginning of defun."
  (let ((point (point)))
    (beginning-of-defun)
    (parse-partial-sexp (point) point)))

(defun color-rg-in-string-p (&optional state)
  (or (nth 3 (or state (color-rg-current-parse-state)))
      (and
       (eq (get-text-property (point) 'face) 'font-lock-string-face)
       (eq (get-text-property (- (point) 1) 'face) 'font-lock-string-face))
      (and
       (eq (get-text-property (point) 'face) 'font-lock-doc-face)
       (eq (get-text-property (- (point) 1) 'face) 'font-lock-doc-face))
      ))

(defun color-rg-string-start+end-points (&optional state)
  "Return a cons of the points of open and close quotes of the string.
The string is determined from the parse state STATE, or the parse state
  from the beginning of the defun to the point.
This assumes that `color-rg-in-string-p' has already returned true, i.e.
  that the point is already within a string."
  (save-excursion
    (let ((start (nth 8 (or state (color-rg-current-parse-state)))))
      (goto-char start)
      (forward-sexp 1)
      (cons start (1- (point))))))

这样做的好处是, 所有插件都可以单文件保持独立, 互相不影响.

Package.el给人的假象有三个:

  1. 稳定可以升级, 其实并不是, Emacs这种基于 api/hook 约定的插件机制注定一个包挂了, 会导致升级影响很多, 排错要花巨大的时间, 为什么? 因为当你发现错误的时候, 本地的Elisp插件文件都换了一大批, 没有git怎么回滚定位问题?

  2. 插件是稳定无bug的版本, 没有人懂所有的Elisp插件, 也没有任何开发者都会保证自己的 master 分支稳定, 所以, 如果觉得只要更最新的git就是最稳定的版本, 本身这种假设就是不适合用户的, 除非你就想天天帮别人提issue和修bug.

  3. 政治正确的, 感觉不使用ELPA就是不正确的, ELPA才是正确的方式. 我个人认为, Emacs在Elisp上就没有版本管理的强制约束, 更没有API兼容性的要求, 基本上都是看开发者的心情, 所以改API是非常正常的事情, 只要自己的插件不挂就可以了.

什么是我的建设性意见?

  1. Package.el只适合入门, 你可以快速体验Emacs功能的时候用, 这就是Package.el最大的价值, 值得肯定
  2. 学Elisp编程, 学的越多, 你才会真正的用好Emacs, 甚至按照自己的要求随便玩不会挂
  3. 基于Git和Submodule去管理你自己的插件, Elisp本来就是放到 load-path 以后, 就随便 require 的机制, 基于 Git 管理, 随时挂了都可以回滚
  4. 插件都有核心功能和锦上添花的功能, 核心功能可以用就没有必要天天升级, 工具是Emacs, 而不是我们.
  5. 不要去教条的把Linux包管理的概念套用在Emacs, 然后就认为有包管理器就一定好, Emacs这种热替换的解释器本身就变化很快, 热替换是它的优点和自由, 版本管理和依赖管理是无法约束它的, 让它又复杂适应用户需求又不会挂.

这个世界上稳定、最新和个性化只能取一个平衡, 让自己舒服后赶紧干活去, 没有完美的方案, 也不要期望天天升级就会稳定.

唯一保证Emacs永远稳定的方法就是学Elisp编程, 没有别的更好的方法.

11 个赞

分拆包主要是为了灵活性吧。比如我只想用到ivy包,不想要counsel那些功能我就不用安装这个了。当然,都做成一个包,不用就不加载也是可以的。看作者怎么权衡吧。

非常同意你说的 Package.el 给人稳定、无BUG的假象。

@lwczzhiwu 提到的 update-directory-autoloads

包管理器做的事情很多,只考虑安装(并激活)一个包就大致需要:

  1. 获得包信息
  2. 下载包
  3. 分析依赖并安装依赖(回到 1)
  4. 编译 Emacs Lisp
  5. 生成 Autoloads
  6. 编译 Info Manual
  7. 添加 load-path
  8. 添加 Info-directory-list
  9. 执行 Autoloads 文件

如果不用包管理器,手动管理包的话除了「生成 autoloads」需要担心的事情还有很多。

3 个赞

最后我从doom-emacs那里抄了一个过来改了一下

注:cl-loop真香

(require 'cl-lib)

(defun cm/refresh-load-path (&optional dir)
  "Refresh the load path of `site-lisp'."
  (interactive)
  (let ((default-directory (file-name-as-directory (or dir cm/site-lisp-directory))))
    (push default-directory load-path)
    (normal-top-level-add-subdirs-to-load-path)))

(defun cm/generate-autoloads (&optional dir target)
  "Generate autoload files for dir"
  (interactive)
  (with-temp-file (or target cm/extra-autoloads-file)
    (cl-loop with generated-autoload-file = nil
             with dir = (or dir cm/site-lisp-directory)
             with inhibit-message = t
             for f in (directory-files-recursively
                       dir
                       "\\.el$")
             for file = (file-truename f)
             for generated-autoload-load-name = (file-name-sans-extension
                                                 file)
             do (autoload-generate-file-autoloads file (current-buffer)))
    (cl-loop with cache = nil
             with load-path = (cm/refresh-load-path dir)
             while (re-search-forward "^\\s-*(autoload\\s-+'[^ ]+\\s-+\"\\([^\"]*\\)\"" nil :no-error)
             for path = (match-string 1)
             do (replace-match
                 (or (cdr (assoc path cache))
                     (when-let* ((libpath (locate-library path))
                                 (libpath (file-name-sans-extension libpath)))
                       (push (cons path (abbreviate-file-name libpath)) cache)
                       libpath)
                     path)
                 :fixed-case :literal nil 1))))

我在reddit上看到的一个方法是,用package.el安装包,版本控制安装的包文件,这样出了问题直接把那个包的文件回滚。算是一种暴力方法,但是看上去蛮有道理的。