Files
doomemacs/lisp/doom-straight.el
Henrik Lissner dac6e05b87 refactor: deprecate appendq!, prependq!, & delq! macros
In the interest of slimming down Doom's core (as we near v3), I've
deprecated these macros. They doesn't really need to exist. Sure, the
alternatives aren't as ergonomic or elegant, but they're good enough
that we don't need these trivial wrappers. Their local uses have been
refactored out as well.
2025-03-25 14:16:56 -04:00

287 lines
14 KiB
EmacsLisp

;;; lisp/doom-straight.el -*- lexical-binding: t; -*-
;;; Commentary:
;;
;; Emacs package management is opinionated, and so is Doom. Doom uses `straight'
;; to create a declarative, lazy-loaded, and (nominally) reproducible package
;; management system. We use `straight' over `package' because the latter is
;; tempermental. ELPA sources suffer downtime occasionally and often fail to
;; build packages when GNU Tar is unavailable (e.g. MacOS users start with BSD
;; tar). Known gnutls errors plague the current stable release of Emacs (26.x)
;; which bork TLS handshakes with ELPA repos (mainly gnu.elpa.org). See
;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=3434.
;;
;; What's worse, you can only get the latest version of packages through ELPA.
;; In an ecosystem that is constantly changing, this is more frustrating than
;; convenient. Straight (and Doom) can do rolling release, but it is opt-in.
;;
;; Interacting with this package management system is done through Doom's
;; bin/doom script. Find out more about it by running 'doom help' (I highly
;; recommend you add the script to your PATH). Here are some highlights:
;;
;; - `doom install`: a wizard that guides you through setting up Doom and your
;; private config for the first time.
;; - `doom sync`: your go-to command for making sure Doom is in optimal
;; condition. It ensures all unneeded packages are removed, all needed ones
;; are installed, and all metadata associated with them is generated.
;; - `doom upgrade`: upgrades Doom Emacs and your packages to the latest
;; versions. There's also 'bin/doom sync -u' for updating only your packages.
;;
;; How this works is: the system reads packages.el files located in each
;; activated module, your private config (`doom-user-dir'), and one in
;; `doom-core-dir'. These contain `package!' declarations that tell DOOM what
;; packages to install and where from.
;;
;; All that said, you can still use package.el's commands, but 'doom sync' will
;; purge ELPA packages.
;;
;;; Code:
(setq straight-base-dir (file-truename doom-local-dir)
straight-repository-branch "develop"
;; Since byte-code is rarely compatible across different versions of
;; Emacs, it's best we build them in separate directories, per emacs
;; version.
straight-build-dir (format "build-%s" emacs-version)
straight-cache-autoloads nil ; we already do this, and better.
;; Doom doesn't encourage you to modify packages in place. Disabling this
;; makes 'doom sync' instant (once everything set up), which is much nicer
;; UX than the several seconds modification checks.
straight-check-for-modifications nil
;; We handle package.el ourselves (and a little more comprehensively)
straight-enable-package-integration nil
;; Before switching to straight, `doom-local-dir' would average out at
;; around 100mb with half Doom's modules at ~230 packages. Afterwards, at
;; around 1gb. With shallow cloning, that is reduced to ~400mb. This has
;; no affect on packages that are pinned, however (run 'doom sync --gc' to
;; compact those after-the-fact). Some packages break when shallow cloned
;; (like magit and org), but we'll deal with that elsewhere.
straight-vc-git-default-clone-depth '(1 single-branch))
(with-eval-after-load 'straight
;; HACK: Doom relies on deferred compilation, which spares the user 20-50min
;; of compilation at install time, but subjects them to ~50% CPU activity
;; when starting Emacs for the first time. To complete this, straight.el
;; needs to be told not to do native-compilation, but it won't obey
;; `straight-disable-native-compile'.
;;
;; It *will* obey `straight--native-comp-available', though. Trouble is:
;; it's a constant; it resets itself when straight is loaded, so it must be
;; changed afterwards.
(setq straight--native-comp-available nil)
;; `let-alist' is built into Emacs 26 and onwards
(add-to-list 'straight-built-in-pseudo-packages 'let-alist))
(defadvice! doom--read-pinned-packages-a (fn &rest args)
"Read `:pin's in `doom-packages' on top of straight's lockfiles."
:around #'straight--lockfile-read-all
(append (apply fn args) ; lockfiles still take priority
(doom-package-pinned-alist)))
;; HACK: This fixes an issue introduced in emacs-mirror/emacs@0d383b592c2f and
;; is present in >=29: Straight.el uses `loaddefs-generate' if it is
;; available, which activates `emacs-lisp-mode' to read autoloads files, but
;; does so without suppressing its hooks. Some packages (like overseer) add
;; hooks to `emacs-lisp-mode-hook' in their autoloads, and once triggered,
;; they will try to load their dependencies (like dash or pkg-info), causing
;; file errors.
;; REVIEW: Report this upstream.
(defadvice! doom--fix-loaddefs-generate--parse-file-a (fn &rest args)
:around #'loaddefs-generate--parse-file
(let (emacs-lisp-mode-hook)
(apply fn args)))
;;
;;; Hacks
;; Straight was designed primarily for interactive use, in an interactive Emacs
;; session, but Doom does its package management in the terminal. Some things
;; must be modified get straight to behave and improve its UX for our users.
(defvar doom-straight--auto-options
'(("has diverged from"
. "^Reset [^ ]+ to ")
("but recipe specifies a URL of"
. "Delete remote \"[^\"]+\", re-create it with correct URL")
("has a merge conflict:"
. "^Abort merge$")
("has a dirty worktree:"
. "^Discard changes$")
("^In repository \"[^\"]+\", [^ ]+ (on branch \"main\") is ahead of default branch \"master\""
. "^Checkout branch \"master\"")
("^In repository \"[^\"]+\", [^ ]+ (on branch \"[^\"]+\") is ahead of default branch \"[^\"]+\""
. "^Checkout branch \"")
("^In repository "
. "^Reset branch \\|^Delete remote [^,]+, re-create it with correct URL\\|^Checkout \""))
"A list of regexps, mapped to regexps.
Their CAR is tested against the prompt, and CDR is tested against the presented
option, and is used by `straight-vc-git--popup-raw' to select which option to
recommend.
It may not be obvious to users what they should do for some straight prompts,
so Doom will recommend the one that reverts a package back to its (or target)
original state.")
;; HACK Remove dired & magit options from prompt, since they're inaccessible in
;; noninteractive sessions.
(advice-add #'straight-vc-git--popup-raw :override #'straight--popup-raw)
;; HACK: `native-comp' only respects `native-comp-jit-compilation-deny-list'
;; when native-compiling packages in interactive sessions. It ignores the
;; variable when, say, straight is building packages. This advice forces it to
;; obey it, even when used by straight (but only in the CLI).
(defadvice! doom-straight--native--compile-async-skip-p (fn files &optional recursively load selector)
:around #'native-compile-async
(let (file-list)
(dolist (file-or-dir (ensure-list files))
(cond ((file-directory-p file-or-dir)
(dolist (file (if recursively
(directory-files-recursively
file-or-dir comp-valid-source-re)
(directory-files file-or-dir
t comp-valid-source-re)))
(push file file-list)))
((file-exists-p file-or-dir)
(push file-or-dir file-list))
((signal 'native-compiler-error
(list "Not a file nor directory" file-or-dir)))))
(funcall fn (seq-remove (lambda (file)
(seq-some (lambda (re) (string-match-p re file))
native-comp-deferred-compilation-deny-list))
file-list)
recursively load selector)))
;; HACK Replace GUI popup prompts (which hang indefinitely in tty Emacs) with
;; simple prompts.
(defadvice! doom-straight--fallback-to-y-or-n-prompt-a (fn &optional prompt noprompt?)
:around #'straight-are-you-sure
(or noprompt?
(if noninteractive
(y-or-n-p (format! "%s" (or prompt "")))
(funcall fn prompt))))
(defun doom-straight--recommended-option-p (prompt option)
(cl-loop for (prompt-re . opt-re) in doom-straight--auto-options
if (string-match-p prompt-re prompt)
return (string-match-p opt-re option)))
(defadvice! doom-straight--no-compute-prefixes-a (fn &rest args)
:around #'straight--build-autoloads
(let (autoload-compute-prefixes)
(apply fn args)))
(defadvice! doom-straight--suppress-confirm-a (&rest _)
:before-until #'straight-are-you-sure
(and (bound-and-true-p doom-cli--context)
(doom-cli-context-suppress-prompts-p doom-cli--context)))
(defadvice! doom-straight--fallback-to-tty-prompt-a (fn prompt actions)
"Modifies straight to prompt on the terminal when in noninteractive sessions."
:around #'straight--popup-raw
(if (bound-and-true-p async-in-child-emacs)
(error "Straight prompt: %s" prompt)
(let ((doom-straight--auto-options doom-straight--auto-options))
;; We can't intercept C-g, so no point displaying any options for this key
;; when C-c is the proper way to abort batch Emacs.
(cl-callf2 delq 'assoc actions)
;; HACK: Remove actions that don't work in noninteractive Emacs (like
;; opening dired or magit).
(setq actions
(cl-remove-if (lambda (o)
(string-match-p "^\\(?:Magit\\|Dired\\)" (nth 1 o)))
actions))
(if (doom-cli-context-suppress-prompts-p doom-cli--context)
(cl-loop for (_key desc func) in actions
when desc
when (doom-straight--recommended-option-p prompt desc)
return (funcall func))
(print! (start "%s") (red prompt))
(print-group!
(terpri)
(let (recommended options)
(print-group!
(print! " 1) Abort")
(cl-loop for (_key desc func) in actions
when desc
do (push func options)
and do
(print! "%2s) %s" (1+ (length options))
(if (doom-straight--recommended-option-p prompt desc)
(progn
(setq doom-straight--auto-options nil
recommended (length options))
(green (concat desc " (Choose this if unsure)")))
desc))))
(terpri)
(let* ((options
(cons (lambda ()
(let ((doom-output-indent 0))
(terpri)
(print! (warn "Aborted")))
(doom-cli--exit 1 doom-cli--context))
(nreverse options)))
(prompt
(format! "How to proceed? (%s%s) "
(mapconcat #'number-to-string
(number-sequence 1 (length options))
", ")
(if (not recommended) ""
(format "; don't know? Pick %d" (1+ recommended)))))
answer fn)
(while (null (nth (setq answer (1- (read-number prompt))) options))
(print! (warn "%s is not a valid answer, try again.") answer))
(funcall (nth answer options)))))))))
(setq straight-arrow " > ")
(defadvice! doom-straight--respect-print-indent-a (string &rest objects)
"Same as `message' (which see for STRING and OBJECTS) normally.
However, in batch mode, print to stdout instead of stderr."
:override #'straight--output
(let ((msg (apply #'format string objects)))
(save-match-data
(when (string-match (format "^%s\\(.+\\)$" (regexp-quote straight-arrow)) msg)
(setq msg (match-string 1 msg))))
(and (string-match-p "^\\(Cloning\\|\\(Reb\\|B\\)uilding\\) " msg)
(not (string-suffix-p "...done" msg))
(doom-print (concat "> " msg) :format t))))
(defadvice! doom-straight--ignore-gitconfig-a (fn &rest args)
"Prevent user and system git configuration from interfering with git calls."
:around #'straight--process-call
(with-environment-variables
(("GIT_CONFIG" nil)
("GIT_CONFIG_NOSYSTEM" "1")
("GIT_CONFIG_GLOBAL" (or (getenv "DOOMGITCONFIG")
"/dev/null")))
(apply fn args)))
;; If the repo failed to clone correctly (usually due to a connection failure),
;; straight proceeds as normal until a later call produces a garbage result
;; (typically, when it fails to fetch the remote branch of the empty directory).
;; This causes Straight to throw an otherwise cryptic type error when it tries
;; to sanitize the result for its log buffer.
;;
;; This error is a common source of user confusion and false positive bug
;; reports, so this advice catches them to regurgitates a more cogent
;; explanation.
(defadvice! doom-straight--throw-error-on-no-branch-a (fn &rest args)
:around #'straight--process-log
(letf! ((defun shell-quote-argument (&rest args)
(unless (car args)
(error "Package was not properly cloned due to a connection failure, please try again later"))
(apply shell-quote-argument args)))
(apply fn args)))
(defadvice! doom-straight--regurgitate-empty-string-error-a (fn &rest args)
:around #'straight-vc-git-local-repo-name
(condition-case-unless-debug e
(apply fn args)
(wrong-type-argument
(if (eq (cadr e) 'stringp)
(error "Package was not properly cloned due to a connection failure, please try again later")
(signal (car e) (cdr e))))))
(provide 'doom-straight)
;;; doom-packages.el ends here