mirror of
https://github.com/doomemacs/doomemacs.git
synced 2026-05-01 01:04:10 -07:00
391 lines
21 KiB
EmacsLisp
391 lines
21 KiB
EmacsLisp
;;; 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.1–30.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
|