讨论一下 Borg

最近从 package.el 换到了 borg ,总体来说体验不错。

只是有个小问题,删除包的时候怎么方便删除其依赖?

Borg 是半自动,依赖要自己管理,要自己手动通过 epkg 查询包的依赖关系,然后执行 M-x borg-remove 移除不想要的包。

你好,这个生成大的 autoloads.el 的功能在 borg 里已经实现了吗?我自己用的时候观察到 borg-activate 函数中,每个 clone 的 autoloads/loaddefs 仍然是分开被加载的,而您的 fork 里应该也是这么做的,所以想请教一下

应该没有,订阅了borg的邮件的,没看到相关的更新。

哈哈,那我去尝试自己实现一下

您引用的帖子我曾经反复阅读过,我自己的配置代码就是用和这个帖子里类似方法生成一个大的 loaddefs.el,来一次加载完所有的 autoload 的。

问题在于,Borg 在 build 每个 clone 时,是使用 borg-update-autoloads 这个函数来生成 autoload 的,每个 clone 的 autoload 均生成一个单独的 xxx-autoloads.el 文件,这是写死在代码中的。每次配置加载时,borg 的做法就是使用 borg-activate 函数一个一个地加载各个 clone,每个 clone 的 xxx-autoloads.el 文件也是分开加载的。

所以,如果想要生成一个单独的 loaddefs.el,我有如下两个方向可走:

  1. 用对待我自己配置代码的方法来对待本应由 Borg 管理的诸 clone,一次性生成所有 Lisp 代码的 autoload 和 load-path。这么做的话,在我的配置中就完全不需要调用 borg-initialize 函数了。
  2. 从源码上修改 Borg,让 Borg 本身就支持把所有 autoload 生成进一个文件。这么做可能会比上面的方法更麻烦一些,但是可能能够帮助到更多人。

所以我想尝试的是第二个方向,来把 Borg 这个神奇的包管理方式做得更好嘛

我记得 doom就是生成一个大的autoload 文件。

我现在的做法是:对比.gitmodules这个文件的时间戳(因包管理操作涉及到修改这个文件),将其作为大的 autoload 文件名一部分,当时间较新时,调用一次 borg-initialize,紧接着生成最新的 single autoload 文件,以后就可以直接 load 这个文件。大概逻辑这样:

(if (file-exists-p current-single-autoload-file)
		(load (string-remove-suffix ".el" current-single-autoload-file))
      (borg-initialize)
      (lld-collect-autoloads current-single-autoload-file)
      (with-no-warnings
		(byte-compile-file current-single-autoload-file)))

其中 lld-collect-autoloads 大多是从 doom 里面抄的:

(defun lld-collect-autoloads (file)
  "insert all enabled drone's autoloads file to a single file."
  (make-directory (file-name-directory file) 'parents)

  ;; cleanup obsolete autoloads file
  (dolist (f (directory-files single-autoload-path t "autoload-[0-9]+-[0-9]+\\.elc?\\'"))
    (unless (string= file f)
      (delete-file f)))

  (message "Generating single big autoload file.")
  (condition-case-unless-debug e
      (with-temp-file file
        (setq-local coding-system-for-write 'utf-8)
        (let ((standard-output (current-buffer))
              (print-quoted t)
              (print-level nil)
              (print-length nil)
              (home (expand-file-name "~"))
              path-list
              theme-path-list
              drones-path
              auto)
          (insert ";; -*- lexical-binding: t; coding: utf-8; no-native-compile: t -*-\n"
                  ";; This file is generated from enabled drones.\n")

          ;; replace absolute path to ~
          (dolist (p load-path)
            ;; collect all drone's load-path
            (when (string-prefix-p (expand-file-name user-emacs-directory) (expand-file-name p))
              (push p drones-path))

            (if (string-prefix-p home p)
                (push (concat "~" (string-remove-prefix home p)) path-list)
              (push p path-list)))

          (dolist (p custom-theme-load-path)
            (if (and (stringp p)
                     (string-prefix-p home p))
                (push (concat "~" (string-remove-prefix home p)) theme-path-list)
              (push p theme-path-list)))

          (prin1 `(set `load-path ',(nreverse path-list)))
          (insert "\n")
          (print `(set `custom-theme-load-path ',(nreverse theme-path-list)))
          (insert "\n")

          ;; insert all drone's autoloads.el to this file
          (dolist (p drones-path)
            (when (file-exists-p p)
              (setq auto (car (directory-files p t ".*-autoloads.el\\'")))
              (when (and auto
                         (file-exists-p auto))
                (insert-file-contents auto))))
          ;; remove all #$ load code
          (goto-char (point-min))
          (while (re-search-forward "\(add-to-list 'load-path.*#$.*\n" nil t)
            (replace-match ""))

          ;; write local variables region
          (goto-char (point-max))
          (insert  "\n\"
                   "\n;; Local Variables:"
                   "\n;; version-control: never"
                   "\n;; no-update-autoloads: t"
                   "\n;; End:"
                   ))
        t)
    (error (delete-file file)
           (signal 'collect-autoload-error (list file e)))))
1 个赞

是否可以把这个想法直接加进 borg-initialize 里面,就可以直接 load single autoload-xxx.el 文件,而不用每次调用 borg-initialize 去执行一堆 git 命令了。

减少了 0.02s(4%) 的启动时间,感谢。 顺便捉个虫,这里多了个 \

我觉得完全可以啊,这样就方便多了。我之前还没想到可以直接把所有生成好的 autoload 文件合并呢。

我之前的做法大致是这样的(模仿 manateelazycat 的做法):使用 zy/-genload-for 函数来为一个目录生成所有 loaddefs。此函数会为该目录下的所有 .el 文件生成 autoload;如果该目录下存在后缀在 zy/useful-exts 中的文件,则将它加入到 load-path;对其所有子目录递归地进行这些操作。

(defvar zy/loaddefs-file (expand-file-name "loaddefs.el" user-emacs-directory)
  "File that contains all loaddefs of ZyEmacs.")

(defvar zy/useful-exts '("el" "so" "dll")
  "ZyEmacs considers files with these extensions \"useful\".")

(defun zy/-genload-for (dir &optional nogen)
  "Recursively populate `load-path' and loaddefs for DIR.

A \"useful\" subdirectory is a subdirectory containing any file
that ends with one of the extensions in `zy/useful-exts'. This
function searches recursively inside DIR, add any useful
subdirectory (including DIR itself) to `load-path', and generate
loaddefs for any Emacs Lisp file inside it.

If NOGEN is non-nil, do not generate loaddefs."
  (let* ((dir (file-name-as-directory dir))
	 (subdirs nil)
	 (dir-useful-p nil))
    (require 'autoload)
    ;; Filter subdirectories, Emacs Lisp files, and other useful files.
    (dolist (file (directory-files dir))
      (if (file-directory-p (concat dir file))
	  ;; Collect subdirectories except the version control ones.
	  (when (not (member file '("." ".." "dist" "node_modules"
				    "__pycache__" "RCS" "CVS" "rcs" "cvs"
				    ".git" ".github")))
	    (push file subdirs))
	(when (file-regular-p (concat dir file))
	  (let ((ext (file-name-extension file)))
	    ;; Generate loaddefs for Emacs Lisp files.
	    (unless nogen
	      (when (string= ext "el")
		(let ((generated-autoload-load-name file))
		  (update-file-autoloads (concat dir file) t zy/loaddefs-file))))
	    ;; Determine the usefulness of DIR.
	    (when (member ext zy/useful-exts)
	      (setq dir-useful-p t))))))
    ;; Dive into subdirectories.
    (dolist (subdir subdirs)
      (zy/-genload-for (concat dir subdir) nogen))
    ;; Populate `load-path'.
    (when dir-useful-p
      (add-to-list 'load-path dir))
    (unless inhibit-message
      (message "Genload for \"%s\" complete" dir))))

然后再用一个总的 zy/genload 函数来给所有想要处理的目录进行生成操作。在初始化时,只有当 loaddefs.el 不存在时,才会生成它;如果使用 M-x zy/genload 调用之,则会覆盖生成新的 loaddefs.el

(defvar zy/genload-dirs '("lisp" "site-lisp")
  "`zy/genload' operates on these directories.")

(defun zy/genload (&optional nogen)
  "Populate `load-path' and generate loaddefs.

If NOGEN is non-nil, do not re-generate loaddefs if there is
one."
  (interactive)
  (let ((regen
	 ;; Regenerate loaddefs only when there is no loaddefs file
	 ;; available, or when NOGEN is nil
	 (or (not (file-exists-p zy/loaddefs-file))
	     (not nogen))))
    ;; Remove the old loaddefs file if necessary
    (when (and regen
	       (file-exists-p zy/loaddefs-file))
      (delete-file zy/loaddefs-file)
      (make-empty-file zy/loaddefs-file))
  ;; Genload for each directory in `zy/genload-dirs'.
  (dolist (dir zy/genload-dirs)
    (zy/-genload-for (expand-file-name dir user-emacs-directory)
		     (not regen)))
  ;; Compile the newly generated file
  (when regen
    (if (native-comp-available-p)
	(native-compile zy/loaddefs-file)
      (byte-compile-file zy/loaddefs-file)))
  ;; Load the loaddefs file
  (load zy/loaddefs-file nil inhibit-message)))

;; Genload on need at startup

(let ((inhibit-message t))
  (zy/genload 'nogen))

这样的方法,用于我自己的少量脚本(lisp)和少量第三方脚本(site-lisp)倒还好,今天我把本应由 Borg 管理的文件夹 lib 放进去一起处理后,光这个递归搜索、添加路径就要花一秒多 :rofl: 不过我是在 WSL1 上用的 Emacs,在真正的 Linux 上应该会更快。

我觉我这样做的问题是,递归搜索对 lib 这样错综复杂的文件夹来说,效率确实太低了。用你展示的方法确实会更好地解决问题,不过最好给 borg-activate 函数给 override 一下,把里面有关 autoload 的部分删去,来避免做重复工作。

我试用 Borg 也就这两天的事情,现在感觉手动管理依赖好麻烦 :rofl: 已经动摇了,想去用 straight + lockfile 的模式了。

其实没必要为了那 0.02s 折腾这个,直接用 Borg 默认的就好了。

配置稳定了以后,也很少需要去折腾包管理器了。

Submodule 管理还是很靠谱,保证滚挂后尽快恢复。 比如,前几天,aggressive-indent-mode的一个更新把我的配置都整挂了,新的 PR 现在都没合并(估计是作者忙)。

用 straight 又会有新的问题😄

Borg 的缺点是安装依赖多的包会比较累人,我是尽量不用那种依赖很多的包。

那里不是那样 :sweat_smile:,是这样 ^L,其实我也不明白为什么,直接抄得: image

记得是因为每个包的 autoload.el 文件都有这么一段,不想全部删除,所以就在最后插入一段,编译也没报错。

不明白什么意思。生成的单个 autoload.el 文件正常,并且可以编译成 .elc,后面就没再管了。

你说得对,我应该坚持下去 :fist:

其实之前我也就用的 straight,但我一直没有用明白它,只是知道用 straight-use-package 来安装包罢了。现在使用 Borg,才需要真正地去仔细阅读文档、弄明白底层的机理。在此激励下我又返回去阅读了 straight 的 README,发现 straight 的设计真的是好精妙,而以前居然从来都没有想过好好读一下它的文档。

不过,使用 submodule 的方式确实很赞,除了配置的时候麻烦一点,稳定性几乎是最好的。像懒猫这样的大佬甚至是直接全手动管理各个包,自然有它的道理。如果只有 0.02 秒的提升的话,那我也还是不去折腾这个东西了 :rofl:

我现在就是有个小问题,Borg 该怎么使用不在 Emacsmirror 上的包呢?我想安装 lsp-bridge,但是如果直接提供 clone 名和 URL 的话,可以拷贝下来,但好像不会 build,也不会被当作是一个 Borg clone。所以我目前的做法是把它放进 site-lisp 里,用我前面列出来的函数来手动管理它的 load-path 和 loaddefs。我觉得我这个做法应该多少是有点蠢的吧,但是确实没弄明白正确的做法是什么。

我的意思是,(borg-initialize) 函数中调用了 (borg-activate) 函数,对每个 clone 做处理,其处理为:将各个 clone 的相关路径加入到 load-path,并且 (load) 它们的 xxx-autoloads.elxxx-loaddefs.el 文件,这些在源代码里都能够看到。也就是说,在执行 (borg-initialize) 函数之后,每个包的 autoloads/loaddefs 已经全部被加载过了。如果再在此基础上去加载一遍多合一的 autoloads/loaddefs 文件的话,可能就画蛇添足、不见得能够提升效率了。

所以我认为,正确的做法应该是修改一下 borg-activate 函数(使用 :override 形式的 advice),把它里面涉及到加载 autoloads/loaddefs 的部分删去,这样就能实现真正的多合一加载了。

Borg 不一定要使用 Emacsmirror的包,我是尽量用包的官方仓库,不用镜像(但是像 cmake-mode 这种的就只能用镜像了,原仓库包含太多不需要的东西),它可以用 github,gitlab ,codeberg 等等任何的托管平台。

以安装 lsp-bridge 为例,这个包在Emacsmirror 根本不存在,你可以用 C-u M-x borg-clone lsp-bridge 然后输入 [email protected]:manateelazycat/lsp-bridge.git 来克隆仓库,然后在你的配置中执行 M-x magit-status,在 untracked files下面的 lib/lsp-bridge 按一下 s 按键,就会自动编译 lsp-bridge了(当然你要提前安装好相应的依赖,不然编译会有错误;如果有错误,安装好依赖后再执行 M-x borg-build lsp-bridge 就可以重新编译了)

1 个赞

你说的 load-path 问题,已经通过以下两句写入到单个 autoloads文件。而各个包中的 xxx-autoloads.el,也已经通过后面的合并在一起,所以不用调用 borg-activate了。

我这个方法的目的是:安装(或者删除、更新)完包后,emacs第一遍按原来的 borg-initialize 执行一遍,可以生成一个单独的 autoloads.el 文件,并且编译好。后续启动就直接 load,不用每次都 borg-initialize 了。好处是不用每次调用 git 进程。相当于配置不依赖于 git。而我把相对个人的配置放在一起,作为一个 drone。这样包管理和个人配置完全独立,不怕搞挂也避免了 windows 下 submodule慢的问题。因为日常使用包管理属少数情况,慢就少慢几次了。

好的,感觉你的方法非常有价值。我也会有在 Windows 上用 Emacs 的需求,学习了。

你结合这段看一下。如果存在单个 autoload.el 文件,完全没调用 borg-initialize,更不会涉及 borg-activate ,直接 load 。所以不存在多加载一遍的问题。这是我现在用的文件,240左右个包,上一次升级包是6月了。

不过你的问题提醒了我,既然存在这个autoload文件,可能 borg 也不用提前 require 了,包管理的时候再 require。

1 个赞