doomemacs/lisp/cli/doctor.el
Henrik Lissner 4fe1cbeddb
refactor: s/when-let/when-let*/
The former is deprecated on Emacs 31 for the latter.
2026-03-09 03:28:06 -04:00

391 lines
21 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; lisp/cli/doctor.el --- userland heuristics and Emacs diagnostics -*- lexical-binding: t; -*-
;;; Commentary:
;;; Code:
(defvar doom-doctor--warnings ())
(defvar doom-doctor--errors ())
;;
;;; DSL
(defun elc-check-dir (dir)
(dolist (file (directory-files-recursively dir "\\.elc$"))
(when (file-newer-than-file-p (concat (file-name-sans-extension file) ".el")
file)
(warn! "%s is out-of-date" (abbreviate-file-name file)))))
(defmacro assert! (condition message &rest args)
`(unless ,condition
(error! ,message ,@args)))
(defmacro error! (&rest args)
`(progn (unless inhibit-message (print! (error ,@args)))
(push (format! (error ,@args)) doom-doctor--errors)))
(defmacro warn! (&rest args)
`(progn (unless inhibit-message (print! (warn ,@args)))
(push (format! (warn ,@args)) doom-doctor--warnings)))
(defmacro success! (&rest args)
`(print! (green ,@args)))
(defmacro section! (&rest args)
`(print! (bold (blue ,@args))))
(defmacro explain! (&rest args)
`(print-group! (print! (p ,@args))))
;;
;;; CLI commands
(defcli! ((doctor doc)) ()
"Diagnoses common issues on your system.
The Doom doctor is essentially one big, self-contained elisp shell script that
uses a series of simple heuristics to diagnose common issues on your system.
Issues that could intefere with Doom Emacs.
Doom modules may optionally have a doctor.el file to run their own heuristics
in."
:benchmark nil
(print! "The doctor will see you now...\n")
(print! (start "Checking your Emacs version..."))
(print-group!
(when (or (> emacs-major-version 30)
(string-match-p ".\\([56]0\\|9[0-9]\\)$" emacs-version))
(warn! "Detected a development build of Emacs (%s)" emacs-version)
(if (> emacs-major-version 30)
(explain! "This is the bleeding edge of Emacs and is inherently unstable.\n")
(explain! "A version that ends in .50, .60, or .9X indicates a pre-release build of Emacs "
"in between stable releases. Doom does not officially support them.\n"))
(explain! "If you encounter issues, make extra sure that your issue is reproducible on "
"a stable version of Emacs (between 27.130.2) before reporting them to Doom's "
"issue tracker! Check out the \"Why does Doom not support Emacs HEAD\" QnA "
"in Doom's FAQ for common issues and debugging."))
(when (and (version= emacs-version "29.4") (featurep 'pgtk))
(warn! "Detected emacs-pgtk 29.4!")
(explain! "If you are experiencing segfaults (crashes), consider downgrading to 29.3 or "
"upgrading to 30.2+. A known bug in 29.4 causes intermittent crashes. "
"See doomemacs#7915 for details.")))
(print! (start "Checking for Doom's prerequisites..."))
(print-group!
(if (not (executable-find "git"))
(error! "Couldn't find git on your machine! Doom's package manager won't work.")
(save-match-data
(let* ((version
(cdr (doom-call-process "git" "version")))
(version
(and (string-match "git version \\([0-9]+\\(?:\\.[0-9]+\\)\\{2\\}\\)" version)
(match-string 1 version))))
(if version
(when (version< version "2.23")
(error! "Git %s detected! Doom requires git 2.23 or newer!"
version))
(warn! "Cannot determine Git version. Doom requires git 2.23 or newer!"))))))
(print! (start "Checking for Emacs config conflicts..."))
(print-group!
(unless (or (file-equal-p doom-emacs-dir "~/.emacs.d")
(file-equal-p doom-emacs-dir "~/.config/emacs"))
(print! (warn "Doom is installed in a non-standard location"))
(explain! "The standard locations are ~/.config/emacs or ~/.emacs.d. Emacs will fail "
"to load Doom if it is not explicitly told where to look for it. In Emacs 29+, "
"this is possible with the --init-directory option:\n\n"
" $ emacs --init-directory '" (abbreviate-file-name doom-emacs-dir) "'\n\n"
"However, Emacs 27-28 users have no choice but to move Doom to a standard "
"location.\n\n"
"Chemacs users may ignore this warning, however."))
(let (found?)
(dolist (file '("~/_emacs" "~/.emacs" "~/.emacs.el" "~/.emacs.d" "~/.config/emacs"))
(when (and (file-exists-p file)
(not (file-equal-p file doom-emacs-dir)))
(setq found? t)
(print! (warn "Found another Emacs config: %s (%s)")
file (if (file-directory-p file) "directory" "file"))))
(when found?
(explain! "Having multiple Emacs configs may prevent Doom from loading properly. Emacs "
"will load the first it finds and ignore the rest. If Doom isn't starting up "
"correctly (e.g. you get a vanilla splash screen), make sure that only one of "
"these exist.\n\n"
"Chemacs users may ignore this warning."))))
(print! (start "Checking for missing Emacs features..."))
(print-group!
(unless (functionp 'json-serialize)
(warn! "Emacs was not built with native JSON support")
(explain! "Users will see a substantial performance gain by building Emacs with "
"jansson support (i.e. a native JSON library), particularly LSP users. "
"You must install a prebuilt Emacs binary with this included, or compile "
"Emacs with the --with-json option."))
(unless (featurep 'native-compile)
(warn! "Emacs was not built with native compilation support")
(explain! "Users will see a substantial performance gain by building Emacs with "
"native compilation support, availible in emacs 28+."
"You must install a prebuilt Emacs binary with this included, or compile "
"Emacs with the --with-native-compilation option.")))
(print! (start "Checking for fonts..."))
(print-group!
(if (not (executable-find "fc-list"))
(warn! "Warning: unable to detect fonts because fontconfig isn't installed")
(with-temp-buffer
(cl-destructuring-bind (status . output)
(doom-call-process "fc-list" "" "family")
(if (not (zerop status))
(print! (error "There was an error running `fc-list'. Is fontconfig installed correctly?"))
(insert output)
(unless (re-search-backward "Symbola" nil t)
(print! (warn "Failed to locate the 'Symbola' font on your system"))
(explain! "Symbola is Emacs' fallback font. It is used when no other active font can "
"render certain characters. This render failure can cause crash Emacs in "
"cases and massive slowdowns in other. It would be wise to have this font "
"installed.")))))))
(print! (start "Checking for private config conflicts..."))
(print-group!
(let* ((xdg-dir (concat (or (getenv "XDG_CONFIG_HOME")
"~/.config")
"/doom/"))
(doom-dir (or (getenv "DOOMDIR")
"~/.doom.d/"))
(dir (if (file-directory-p xdg-dir)
xdg-dir
doom-dir)))
(when (file-equal-p dir doom-emacs-dir)
(print! (error "Doom was cloned to %S, not ~/.emacs.d or ~/.config/emacs"
(path dir)))
(explain! "Doom's source and your private Doom config have to live in separate directories. "
"Putting them in the same directory (without changing the DOOMDIR environment "
"variable) will cause errors on startup."))
(when (and (not (file-equal-p xdg-dir doom-dir))
(file-directory-p xdg-dir)
(file-directory-p doom-dir))
(print! (warn "Detected two private configs, in %s and %s")
(abbreviate-file-name xdg-dir)
doom-dir)
(explain! "The second directory will be ignored, as it has lower precedence."))))
(print! (start "Checking for common environmental issues..."))
(print-group!
(unless (file-writable-p temporary-file-directory)
(print! (error "Your temporary file directory is not writable!"))
(explain! "Emacs and Doom uses this directory to store temporary files. It is currently "
"set to:\n\n"
(format " %s\n\n" temporary-file-directory)
"Set $TMPDIR to a writable directory to fix this. If it is not resolved, Doom "
"CLI commands and various Emacs components will unpredictably throw file "
"permissions errors at unpredictable times."))
(unless (doom-system-supports-symlinks-p)
(print! (warn "Symlinks are not enabled on this operating system"))
(explain! "In the near future, Doom will make extensive use of symlinks to save space "
"and simplify package and profile management. Without symlinks, much of it "
"won't be functional. To get around this, you have three options:"
"\n\n"
" - Enabling 'Developer Mode' in the Windows settings (search for 'Developer "
" Settings' in the start menu). This will warn you about its effect on system "
" security, but this can be ignored. If it bothers you, consider another option "
" below.\n"
" - Running your shell (cmd or powershell) in administrator mode anytime you "
" need to use the 'doom' script. Also, the `doom/reload' command won't work "
" unless Emacs itself is launched in administrator mode.\n"
" - Install Emacs in WSL 1/2; the native Linux environment it creates supports "
" symlinks out of the box and is the best option (as Emacs is generally more "
" stable, predictable, and faster there).\n\n")))
(print! (start "Checking for stale elc files..."))
(elc-check-dir doom-core-dir)
(elc-check-dir doom-modules-dir)
(elc-check-dir (doom-path doom-local-dir "straight" straight-build-dir))
(print! (start "Checking for problematic git global settings..."))
(if (executable-find "git")
(when (zerop (car (doom-call-process "git" "config" "--global" "--get-regexp" "^url\\.git://github\\.com")))
(warn! "Detected insteadOf rules in your global gitconfig.")
(explain! "Doom's package manager heavily relies on git. In particular, many of its packages "
"are hosted on github. Rewrite rules like these will break it:\n\n"
" [url \"git://github.com\"]\n"
" insteadOf = https://github.com\n\n"
"Please remove them from your gitconfig or use a conditional includeIf rule to "
"only apply your rewrites to specific repositories. See "
"'https://git-scm.com/docs/git-config#_includes' for more information."))
(error! "Couldn't find the `git' binary; this a hard dependecy for Doom!"))
(print! (start "Checking Doom Emacs..."))
(condition-case-unless-debug ex
(print-group!
(doom-initialize t)
(doom-startup)
(require 'straight)
(print! (success "Initialized Doom Emacs %s") doom-version)
(print!
(if (hash-table-p doom-modules)
(success "Detected %d modules" (hash-table-count doom-modules))
(warn "Failed to load any modules. Do you have an private init.el?")))
(print! (success "Detected %d packages") (length doom-packages))
(print! (start "Checking Doom core for irregularities..."))
(print-group!
;; Check for oversized problem files in cache that may cause unusual/tremendous
;; delays or freezing. This shouldn't happen often.
(dolist (file (list "savehist" "projectile.cache"))
(when-let* ((size (ignore-errors (doom-file-size file doom-cache-dir))))
(when (> size 1048576) ; larger than 1mb
(warn! "%s is too large (%.02fmb). This may cause freezes or odd startup delays"
file (/ size 1024 1024.0))
(explain! "Consider deleting it from your system (manually)"))))
(unless (ignore-errors (executable-find doom-ripgrep-executable))
(error! "Couldn't find the `rg' binary; this a hard dependecy for Doom, file searches may not work"))
(unless (ignore-errors (executable-find doom-fd-executable))
(warn! "Couldn't find the `fd' binary; project file searches will be slightly slower"))
(require 'projectile nil t)
(when (projectile-project-p "~")
(warn! "Your $HOME is recognized as a project root")
(let ((files
(let ((default-directory (expand-file-name "~")))
(append (seq-filter #'file-exists-p projectile-project-root-files-bottom-up)
(seq-filter #'file-exists-p projectile-project-root-files)))))
(explain! "The following files will interfere with projectile's project root detection "
"for any project that lives under $HOME:\n\n"
" - " (mapconcat #'expand-file-name files "\n - ") "\n\n"
"To fix this, these files must be deleted (recommended), or removed from the "
"`projectile-project-root-files-bottom-up' or `projectile-project-root-files' "
"variables in your Doom config (not recommended). For example:\n\n"
" ;; Add to $DOOMDIR/config.el\n"
" (after! projectile\n"
" (setq projectile-project-root-files-bottom-up\n"
" (remove \".git\" projectile-project-root-files-bottom-up)))\n")))
;; REVIEW: When projectile is replaced with project...
;; (require 'project)
;; (when (project-current nil doom-emacs-dir)
;; (let ((file (or (seq-find #'file-exists-p project-vc-extra-root-markers)
;; (seq-find #'file-directory-p (mapcar #'cdr project-vc-backend-markers-alist)))))
;; ;; ...
;; ))
;; There should only be one
(when (and (file-equal-p doom-user-dir "~/.config/doom")
(file-directory-p "~/.doom.d"))
(print! (warn "Both %S and '~/.doom.d' exist on your system")
(path doom-user-dir))
(explain! "Doom will only load one of these (~/.config/doom takes precedence). Possessing\n"
"both is rarely intentional; you should one or the other."))
(unless (string-match-p "/\\(ba\\|z\\|m?k\\|d?a\\)?sh$" shell-file-name)
(print! (warn "Detected a non-POSIX compliant shell (%s)" shell-file-name))
(explain! "Non-POSIX compliant shells (particularly Fish and Nushell) can cause "
"unpredictable issues with any Emacs utilities that spawn child processes "
"from shell commands (like diff-hl TRAMP, and terminal emulators). To get "
"around this, configure Emacs to use a POSIX shell internally, e.g.\n\n"
" ;;; add to $DOOMDIR/config.el\n"
" (setq shell-file-name (executable-find \"bash\"))\n\n"
"Emacs' terminal emulators can be safely configured to use your original $SHELL:\n\n"
" ;;; add to $DOOMDIR/config.el\n"
(format " (setq-default vterm-shell \"%s\")\n" shell-file-name)
(format " (setq-default explicit-shell-file-name \"%s\")\n" shell-file-name)))
;; Check for fonts
(if (not (executable-find "fc-list"))
(warn! "Warning: unable to detect fonts because fontconfig isn't installed")
;; nerd-icons fonts
(when (and (pcase system-type
(`gnu/linux (concat (or (getenv "XDG_DATA_HOME")
"~/.local/share")
"/fonts/"))
(`darwin "~/Library/Fonts/"))
(require 'nerd-icons nil t))
(with-temp-buffer
(cl-destructuring-bind (status . output)
(doom-call-process "fc-list" "" "family")
(if (not (zerop status))
(print! (error "There was an error running `fc-list'. Is fontconfig installed correctly?"))
(insert output)
(if (re-search-backward nerd-icons-font-family nil t)
(success! "Found %s" nerd-icons-font-family)
(print! (warn "Failed to locate '%s' font on your system") nerd-icons-font-family)
(explain! "This font is required for icons in Doom Emacs. To download and install "
"them, do one of the following:\n\n"
" - Execute `M-x nerd-icons-install-fonts' from within Doom Emacs (NOTE: "
" on Windows this command will only download them; the fonts must then "
" be installed manually afterwards).\n"
" - Download and install 'Symbols Nerd Font' from https://nerdfonts.com "
" or via your OS package manager. (You'll need to change the "
" `nerd-icons-font-names' and/or `nerd-icons-font-family' variables to "
" reflect a non-standard file or font family name).\n"))))))))
(print! (start "Checking for stale elc files in your DOOMDIR..."))
(when (file-directory-p doom-user-dir)
(print-group!
(elc-check-dir doom-user-dir)))
(when doom-modules
(print! (start "Checking your enabled modules..."))
(pcase-dolist (`(,group . ,name) (doom-module-list))
(with-doom-context 'doctor
(let (doom-local-errors
doom-local-warnings)
(let (doom-doctor--errors
doom-doctor--warnings)
(condition-case-unless-debug ex
(with-doom-module (cons group name)
(let ((doctor-file (doom-module-expand-path (cons group name) "doctor.el"))
(packages-file (doom-module-expand-path (cons group name) doom-module-packages-file)))
(when packages-file
(cl-loop with doom-output-indent = 6
for name in (with-doom-context 'package
(let* (doom-packages
doom-disabled-packages)
(load packages-file 'noerror 'nomessage)
(mapcar #'car doom-packages)))
unless (or (doom-package-get name :disable)
(eval (doom-package-get name :ignore))
(plist-member (doom-package-get name :recipe) :local-repo)
(locate-library (symbol-name name))
(doom-package-built-in-p name)
(doom-package-installed-p name))
do (print! (error "Missing emacs package: %S") name)))
(when doctor-file
(let ((inhibit-message t))
(load doctor-file 'noerror 'nomessage)))))
(file-missing (error! "%s" (error-message-string ex)))
(error (error! "Syntax error: %s" ex)))
(when (or doom-doctor--errors doom-doctor--warnings)
(print-group!
(print! (start (bold "%s %s")) group name)
(print! "%s" (string-join (append doom-doctor--errors doom-doctor--warnings) "\n")))
(setq doom-local-errors doom-doctor--errors
doom-local-warnings doom-doctor--warnings)))
(cl-callf append doom-doctor--errors doom-local-errors)
(cl-callf append doom-doctor--warnings doom-local-warnings))))))
(error
(warn! "Attempt to load DOOM failed\n %s\n"
(or (cdr-safe ex) (car ex)))
(setq doom-modules nil)))
;; Final report
(terpri)
(dolist (msg (list (list doom-doctor--warnings "warning" 'yellow)
(list doom-doctor--errors "error" 'red)))
(when (car msg)
(print! (color (nth 2 msg)
(if (cdar msg)
"There are %d %ss!"
"There is %d %s!")
(length (car msg)) (nth 1 msg)))))
(unless (or doom-doctor--errors doom-doctor--warnings)
(success! "Everything seems fine, happy Emacs'ing!"))
(exit! :pager? "+G"))
(provide 'doom-cli-doctor)
;;; doctor.el ends here