From 8746c12fae3324a700ec0b9571b32f72efc953de Mon Sep 17 00:00:00 2001 From: Henrik Lissner Date: Thu, 24 May 2018 19:00:41 +0200 Subject: [PATCH] Redesign Doom bootstrap, caching & autoload generation logic The autoloads file has been split into doom-autoload-file and doom-package-autoload-file. The former is for Doom's modules and standard library; the latter is for compiling all package autoloads like load-path and auto-mode-alist (among other things). This reduced my startup speed from ~1s to ~0.5s --- core/autoload/modules.el | 350 +++++++++++++++++++++++++-------------- core/core-packages.el | 282 ++++++++++++++++++------------- core/core.el | 38 ++--- init.el | 2 - 4 files changed, 413 insertions(+), 259 deletions(-) diff --git a/core/autoload/modules.el b/core/autoload/modules.el index 7c23b2e34..ed8f162b1 100644 --- a/core/autoload/modules.el +++ b/core/autoload/modules.el @@ -1,149 +1,255 @@ ;;; core/autoload/modules.el -*- lexical-binding: t; -*- -;;;###autoload -(defun doom//reload () - "Reload your private Doom config. Experimental!" - (interactive) - (when (file-exists-p doom-packages-file) - (delete-file doom-packages-file)) - (cond ((and noninteractive (not (daemonp))) - (doom-initialize) - (doom//reload-autoloads) - (require 'server) - (when (server-running-p) - (message "Reloading active Emacs session...") - (server-eval-at server-name '(doom//reload)))) +(autoload 'print! "autoload/message" nil 'macro) +(autoload 'printerr! "autoload/message" nil 'macro) - ((let ((load-prefer-newer t) - doom-init-p) - (setq doom-modules (make-hash-table :test #'equal :size 100 :rehash-threshold 1.0)) - (doom-initialize t) - (message "%d packages reloaded" (length package-alist)) - (run-hooks 'doom-reload-hook))))) +(defun doom--server-eval (body) + (require 'server) + (when (server-running-p) + (server-eval-at server-name body))) + +;;;###autoload +(defun doom//reload (&optional force-p) + "Reloads your config. This is experimental! + +If called from a noninteractive session, this will try to communicate with a +live server (if one is found) to tell it to run this function. + +If called from an interactive session, tries to reload autoloads files (if +necessary), reinistalize doom (via `doom-initialize') and reloads your private +init.el and config.el. Then runs `doom-reload-hook'." + (interactive) + (unless doom--inhibit-reload + (cond ((and noninteractive (not (daemonp))) + (require 'server) + (if (not (server-running-p)) + (doom//reload-autoloads force-p) + (print! "Reloading active Emacs session...") + (print! + (bold "%%s") + (if (server-eval-at server-name '(doom//reload)) + (green "Done!") + (red "There were issues!"))))) + ((let ((load-prefer-newer t)) + (doom//reload-autoloads force-p) + (doom-initialize t) + (doom-initialize-modules t) + (print! (green "%d packages reloaded" (length package-alist))) + (run-hooks 'doom-reload-hook)))))) + + +;; +;; Autoload file generation +;; + +(defvar doom-autoload-excluded-packages '(marshal gh) + "Packages that have silly or destructive autoload files that try to load +everyone in the universe and their dog, causing errors that make babies cry. No +one wants that.") + +(defun doom--byte-compile (file) + (let ((short-name (file-name-nondirectory file))) + (condition-case-unless-debug ex + (when (byte-compile-file file) + (load (byte-compile-dest-file file) nil t) + (unless noninteractive + (message "Finished compiling %s" short-name))) + ('error + (doom-delete-autoloads-file file) + (error "Error in %s: %s -- %s" + short-name + (car ex) (error-message-string ex)))))) + +;;;###autoload +(defun doom-delete-autoloads-file (file) + "Delete FILE (an autoloads file), and delete the accompanying *.elc file, if +it exists." + (or (stringp file) + (signal 'wrong-type-argument (list 'stringp file))) + (when (file-exists-p file) + (delete-file file) + (ignore-errors (delete-file (byte-compile-dest-file file))) + (print! "Deleted old %s" (file-name-nondirectory file)))) + +;;;###autoload +(defun doom//reload-autoloads (&optional file force-p) + "Reloads FILE (an autoload file), if it needs reloading. + +FILE should be one of `doom-autoload-file' or `doom-package-autoload-file'. If +it is nil, it will try to reload both. If FORCE-P (universal argument) do it +even if it doesn't need reloading!" + (interactive + (list nil current-prefix-arg)) + (or (null file) + (stringp file) + (signal 'wrong-type-argument (list 'stringp file))) + (cond ((equal file doom-autoload-file) + (doom//reload-doom-autoloads force-p)) + ((equal file doom-package-autoload-file) + (doom//reload-package-autoloads force-p)) + ((progn + (doom//reload-doom-autoloads force-p) + (doom//reload-package-autoloads force-p))))) (defvar generated-autoload-load-name) ;;;###autoload -(defun doom//reload-autoloads (&optional force) - "Refreshes the autoloads.el file, specified by `doom-autoload-file'. +(defun doom//reload-doom-autoloads (&optional force-p) + "Refreshes the autoloads.el file, specified by `doom-autoload-file', if +necessary (or if FORCE-P is non-nil). It scans and reads core/autoload/*.el, modules/*/*/autoload.el and -modules/*/*/autoload/*.el, and generates an autoloads file at the path specified -by `doom-autoload-file'. This file tells Emacs where to find lazy-loaded -functions. +modules/*/*/autoload/*.el, and generates `doom-autoload-file'. This file tells +Emacs where to find lazy-loaded functions. -This should be run whenever init.el or an autoload file is modified. Running -'make autoloads' from the commandline executes this command." +This should be run whenever your `doom!' block, or a module autoload file, is +modified." (interactive) - ;; This function must not use autoloaded functions or external dependencies. - ;; It must assume nothing is set up! - (let ((default-directory doom-emacs-dir) + (let ((doom-modules (doom-module-table)) + (default-directory doom-emacs-dir) (targets (file-expand-wildcards - (expand-file-name "autoload/*.el" doom-core-dir))) - (generate-autoload-section-continuation "") - (generate-autoload-section-header "") - (generate-autoload-section-trailer "") - (doom--stage 'autoloads) - outdated) + (expand-file-name "autoload/*.el" doom-core-dir)))) (dolist (path (doom-module-load-path)) (let ((auto-dir (expand-file-name "autoload" path)) (auto-file (expand-file-name "autoload.el" path))) (when (file-exists-p auto-file) (push auto-file targets)) (when (file-directory-p auto-dir) - (dolist (file (doom-files-under auto-dir :match "\\.el$")) + (dolist (file (doom-files-in auto-dir :match "\\.el$" :full t)) (push file targets))))) - (when (file-exists-p doom-autoload-file) - (delete-file doom-autoload-file) - (ignore-errors (delete-file (byte-compile-dest-file doom-autoload-file))) - (message "Deleted old autoloads.el")) - (message "Generating new autoloads.el") - (dolist (file (mapcar #'file-truename (reverse targets))) - (let ((generated-autoload-load-name (file-name-sans-extension file))) - (message - (cond ((not (doom-file-cookie-p file)) - "⚠ Ignoring %s") - ((update-file-autoloads file nil doom-autoload-file) - "✕ Nothing in %s") - ("✓ Scanned %s")) - (if (file-in-directory-p file default-directory) - (file-relative-name file) - (abbreviate-file-name file))))) - (make-directory (file-name-directory doom-autoload-file) t) - (let ((buf (find-file-noselect doom-autoload-file t)) - (load-path (append doom-psuedo-module-dirs - doom-modules-dirs - load-path)) - case-fold-search) - ;; FIXME Make me faster - (unwind-protect - (with-current-buffer buf - (goto-char (point-min)) - (insert ";;; -*- lexical-binding:t -*-\n" - ";; This file is autogenerated by `doom//reload-autoloads', DO NOT EDIT !!\n\n") + (if (and (not force-p) + (file-exists-p doom-autoload-file) + (not (cl-loop for file in targets + if (file-newer-than-file-p file doom-autoload-file) + return t))) + (ignore (print! (green "Doom core autoloads is up-to-date")) + (doom-initialize-autoloads doom-autoload-file)) + (doom-delete-autoloads-file doom-autoload-file) + ;; in case the buffer is open somewhere and modified + (when-let* ((buf (find-buffer-visiting doom-autoload-file))) + (with-current-buffer buf + (set-buffer-modified-p nil)) + (kill-buffer buf)) + (message "Generating new autoloads.el") + (dolist (file (nreverse targets)) + (let* ((file (file-truename file)) + (generated-autoload-load-name (file-name-sans-extension file)) + (noninteractive (not doom-debug-mode))) + (print! + (cond ((not (doom-file-cookie-p file)) + "⚠ Ignoring %s") + ((update-file-autoloads file nil doom-autoload-file) + (yellow "✕ Nothing in %%s")) + ((green "✓ Scanned %%s"))) + (if (file-in-directory-p file default-directory) + (file-relative-name file) + (abbreviate-file-name file))))) + (make-directory (file-name-directory doom-autoload-file) t) + (let ((buf (find-file-noselect doom-autoload-file t)) + case-fold-search) + (unwind-protect + (with-current-buffer buf + (goto-char (point-min)) + (insert ";;; -*- lexical-binding:t -*-\n" + ";; This file is autogenerated by `doom//reload-doom-autoloads', DO NOT EDIT !!\n\n") + (save-excursion + ;; Replace autoload paths (only for module autoloads) with + ;; absolute paths for faster resolution during load and + ;; simpler `load-path' + (let ((load-path (append doom-psuedo-module-dirs + doom-modules-dirs + load-path)) + cache) + (save-excursion + (while (re-search-forward "^\\s-*(autoload\\s-+'[^ ]+\\s-+\"\\([^\"]*\\)\"" nil t) + (let ((path (match-string 1))) + (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) + t t nil 1))) + (print! (green "✓ Autoload paths expanded"))))) + ;; Remove byte-compile inhibiting file variables so we can + ;; byte-compile the file. + (when (re-search-forward "^;; no-byte-compile: t\n" nil t) + (replace-match "" t t)) + ;; Byte compile it to give the file a chance to reveal errors. + (save-buffer) + (doom--byte-compile doom-autoload-file) + (when (and noninteractive (not (daemonp))) + (doom--server-eval `(load-file ,doom-autoload-file))) + t) + (kill-buffer buf)))))) - ;; Replace autoload paths (only for module autoloads) with - ;; absolute paths for faster resolution during load and simpler - ;; `load-path' - (save-excursion - (let (cache) - (while (re-search-forward "^\\s-*(autoload\\s-+'[^ ]+\\s-+\"\\([^\"]*\\)\"" nil t) - (let ((path (match-string 1))) - (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) - (progn - (warn "Couldn't find absolute path for: %s" path) - path)) - t t nil 1)))) - (message "✓ Autoload paths expanded")) +;;;###autoload +(defun doom//reload-package-autoloads (&optional force-p) + "Compiles `doom-package-autoload-file' from the autoloads files of all +installed packages. It also caches `load-path', `Info-directory-list', +`doom-disabled-packages', `package-activated-list' and `auto-mode-alist'. - ;; insert package autoloads - (save-excursion - (dolist (spec package-alist) - (let ((pkg (car spec))) - (unless (memq pkg doom-autoload-excluded-packages) - (let ((file - (abbreviate-file-name - (concat (package--autoloads-file-name (cadr spec)) ".el")))) - (insert "(let ((load-file-name " (prin1-to-string file) "))\n") - (insert-file-contents file) - (while (re-search-forward "^\\(?:;;\\(.*\n\\)\\|\n\\)" nil t) - (unless (nth 8 (syntax-ppss)) - (replace-match "" t t))) - (unless (bolp) (insert "\n")) - (insert ")\n"))))) - (message "✓ Package autoloads included")) +Will do nothing if none of your installed packages have been modified. If +FORCE-P (universal argument) is non-nil, regenerate it anyway. - ;; Remove `load-path' and `auto-mode-alist' modifications (most - ;; of them, at least); they are cached elsewhere, so these are - ;; unnecessary overhead. - (while (re-search-forward (concat "^\\s-*(\\(add-to-list\\s-+'\\(?:load-path\\|auto-mode-alist\\)\\)") - nil t) - (beginning-of-line) - (skip-chars-forward " \t") - (kill-sexp)) - (message "✓ load-path/auto-mode-alist entries removed") +This should be run whenever your `doom!' block or update your packages." + (interactive) + (if (and (not force-p) + (file-exists-p doom-package-autoload-file) + (not (cl-loop initially do (doom-ensure-packages-initialized t) + for (_pkg desc) in package-alist + for autoload-file = (concat (package--autoloads-file-name desc) ".el") + if (file-newer-than-file-p autoload-file doom-package-autoload-file) + return t))) + (ignore (print! (green "Doom package autoloads is up-to-date")) + (doom-initialize-autoloads doom-package-autoload-file)) + (doom-delete-autoloads-file doom-package-autoload-file) + (with-temp-file doom-package-autoload-file + (insert ";;; -*- lexical-binding:t -*-\n" + ";; This file is autogenerated by `doom//reload-package-autoloads', DO NOT EDIT !!\n\n") + ;; insert package autoloads + (save-excursion + (dolist (spec package-alist) + (cl-destructuring-bind (pkg desc) spec + (unless (memq pkg doom-autoload-excluded-packages) + (let ((file + (abbreviate-file-name + (concat (package--autoloads-file-name desc) ".el")))) + (when (file-exists-p file) + (insert "(let ((load-file-name " (prin1-to-string file) "))\n") + (insert-file-contents file) + (while (re-search-forward "^\\(?:;;\\(.*\n\\)\\|\n\\)" nil t) + (unless (nth 8 (syntax-ppss)) + (replace-match "" t t))) + (unless (bolp) (insert "\n")) + (insert ")\n")))))) + (print! (green "✓ Package autoloads included")) + ;; Cache the important and expensive-to-initialize state here. + (doom-initialize-packages 'internal) + (prin1 `(setq load-path ',load-path + auto-mode-alist ',auto-mode-alist + Info-directory-list ',Info-directory-list + doom-disabled-packages ',doom-disabled-packages + package-activated-list ',package-activated-list) + (current-buffer)) + (print! (green "✓ Cached package state"))) + ;; Remove `load-path' and `auto-mode-alist' modifications (most of them, + ;; at least); they are cached later, so all those membership checks are + ;; unnecessary overhead. + (while (re-search-forward "^\\s-*\\((\\(?:add-to-list\\|when (boundp \\)\\s-+'\\(?:load-path\\|auto-mode-alist\\)\\)" nil t) + (goto-char (match-beginning 1)) + (kill-sexp)) + (print! (green "✓ Removed load-path/auto-mode-alist entries"))) + (doom--byte-compile doom-package-autoload-file) + (when (and noninteractive (not (daemonp))) + (doom--server-eval `(load-file ,doom-package-autoload-file))) + t)) - ;; Remove byte-compile inhibiting file variables so we can - ;; byte-compile the file. - (when (re-search-forward "^;; no-byte-compile: t\n$" nil t) - (replace-match "" t t)) - (save-buffer) - ;; Byte compile it to give the file a chance to reveal errors. - (condition-case-unless-debug ex - (quiet! (byte-compile-file doom-autoload-file 'load)) - ('error - (delete-file doom-autoload-file) - (message "Deleting autoloads file!") - (error "Error in autoloads.el: %s -- %s" - (car ex) (error-message-string ex)))) - (message "Done!")) - (kill-buffer buf))))) +;; +;; Byte compilation +;; ;;;###autoload (defun doom//byte-compile (&optional modules recompile-p) diff --git a/core/core-packages.el b/core/core-packages.el index ece2af661..08b4f89e4 100644 --- a/core/core-packages.el +++ b/core/core-packages.el @@ -40,53 +40,47 @@ ;; See core/autoload/packages.el for more functions. (defvar doom-init-p nil - "Non-nil if doom is done initializing (once `doom-post-init-hook' is done). If -this is nil after Emacs has started something is wrong.") + "Non-nil if `doom-initialize' has run.") + +(defvar doom-init-modules-p nil + "Non-nil if `doom-initialize-modules' has run.") (defvar doom-init-time nil "The time it took, in seconds, for DOOM Emacs to initialize.") -(defvar doom-modules - (make-hash-table :test #'equal :size 100 :rehash-threshold 1.0) +(defvar doom-modules () "A hash table of enabled modules. Set by `doom-initialize-modules'.") (defvar doom-modules-dirs (list (expand-file-name "modules/" doom-private-dir) doom-modules-dir) "A list of module root directories. Order determines priority.") -(defvar doom-psuedo-module-dirs - (list doom-private-dir) +(defvar doom-psuedo-module-dirs (list doom-private-dir) "Additional paths for modules that are outside of `doom-modules-dirs'. -`doom//reload-autoloads', `doom//byte-compile' and `doom-initialize-packages' -will include the directories in this list.") +`doom//reload-doom-autoloads', `doom//byte-compile' and +`doom-initialize-packages' will include the directories in this list.") (defvar doom-packages () "A list of enabled packages. Each element is a sublist, whose CAR is the package's name as a symbol, and whose CDR is the plist supplied to its `package!' declaration. Set by `doom-initialize-packages'.") -(defvar doom-core-packages - '(persistent-soft use-package quelpa async) +(defvar doom-core-packages '(persistent-soft use-package quelpa async) "A list of packages that must be installed (and will be auto-installed if missing) and shouldn't be deleted.") (defvar doom-disabled-packages () "A list of packages that should be ignored by `def-package!'.") -(defvar doom-autoload-excluded-packages '(marshal gh) - "Packages that have silly or destructive autoload files that try to load -everyone in the universe and their dog, causing errors that make babies cry. No -one wants that.") - (defvar doom-site-load-path load-path "The starting load-path, before it is altered by `doom-initialize'.") (defvar doom-autoload-file (concat doom-local-dir "autoloads.el") - "Where `doom//reload-autoloads' will generate its autoloads file.") + "Where `doom//reload-doom-autoloads' will generate its core autoloads file.") -(defvar doom-packages-file (concat doom-cache-dir "packages.el") - "Where to cache `load-path', `Info-directory-list', `doom-disabled-packages' -and `auto-mode-alist'.") +(defvar doom-package-autoload-file (concat doom-local-dir "autoloads.pkg.el") + "Where `doom//reload-package-autoloads' will generate its package.el autoloads +file.") (defvar doom-reload-hook nil "A list of hooks to run when `doom//reload-load-path' is called.") @@ -150,22 +144,6 @@ and `auto-mode-alist'.") (file-relative-name load-file-name doom-emacs-dir) (abbreviate-file-name load-file-name)))) -(defun doom|refresh-cache () - "Refresh `doom-packages-file', which caches `load-path', -`Info-directory-list', `doom-disabled-packages', `auto-mode-alist' and -`package-activated-list'." - (doom-initialize-packages 'internal) - (let ((coding-system-for-write 'emacs-internal)) - (with-temp-file doom-packages-file - (insert ";;; -*- lexical-binding:t -*-\n" - ";; This file was autogenerated by `doom|refresh-cache', DO NOT EDIT!\n") - (prin1 `(setq load-path ',load-path - auto-mode-alist ',auto-mode-alist - Info-directory-list ',Info-directory-list - doom-disabled-packages ',doom-disabled-packages - package-activated-list ',package-activated-list) - (current-buffer))))) - (defun doom|display-benchmark (&optional return-p) "Display a benchmark, showing number of packages and modules, and how quickly they were loaded at startup. @@ -195,17 +173,59 @@ session, with a different init.el, like so: 'window-setup-hook)) +;; +;; Bootstrap helpers +;; + +(defun doom-ensure-packages-initialized (&optional force-p) + "Make sure package.el is initialized." + (when (or force-p (not package--initialized)) + (require 'package) + (setq package-activated-list nil + package--initialized nil) + (let (byte-compile-warnings) + (condition-case _ + (quiet! (package-initialize)) + ('error (package-refresh-contents) + (setq doom--refreshed-p t) + (package-initialize)))))) + +(defun doom-ensure-core-packages () + "Make sure `doom-core-packages' are installed." + (when-let* ((core-packages (cl-remove-if #'package-installed-p doom-core-packages))) + (message "Installing core packages") + (unless doom--refreshed-p + (package-refresh-contents)) + (dolist (package core-packages) + (let ((inhibit-message t)) + (package-install package)) + (if (package-installed-p package) + (message "✓ Installed %s" package) + (error "✕ Couldn't install %s" package))) + (message "Installing core packages...done"))) + +(defun doom-ensure-core-directories () + "Make sure all Doom's essential local directories (in and including +`doom-local-dir') exist." + (dolist (dir (list doom-local-dir doom-etc-dir doom-cache-dir doom-packages-dir)) + (unless (file-directory-p dir) + (make-directory dir t)))) + + ;; ;; Bootstrap API ;; +(autoload 'doom//reload-doom-autoloads "autoload/modules" nil t) +(autoload 'doom//reload-package-autoloads "autoload/modules" nil t) + (defun doom-initialize (&optional force-p) "Bootstrap Doom, if it hasn't already (or if FORCE-P is non-nil). -The bootstrap process involves making sure the essential directories exist, core -packages are installed, `doom-autoload-file' is loaded, `doom-packages-file' -cache exists (and is loaded) and, finally, loads your private init.el (which -should contain your `doom!' block). +The bootstrap process involves making sure 1) the essential directories exist, +2) the core packages are installed, 3) `doom-autoload-file' and +`doom-package-autoload-file' exist and have been loaded, and 4) Doom's core +files are loaded. If the cache exists, much of this function isn't run, which substantially reduces startup time. @@ -228,71 +248,54 @@ Module load order is determined by your `doom!' block. See `doom-modules-dirs' for a list of all recognized module trees. Order defines precedence (from most to least)." (when (or force-p (not doom-init-p)) - (when (and (or force-p noninteractive) - (file-exists-p doom-packages-file)) - (message "Deleting packages.el cache") - (delete-file doom-packages-file)) - (unless (load doom-packages-file 'noerror 'nomessage 'nosuffix) - ;; Ensure core folders exist, otherwise we get errors - (dolist (dir (list doom-local-dir doom-etc-dir doom-cache-dir doom-packages-dir)) - (unless (file-directory-p dir) - (make-directory dir t))) - ;; Ensure plugins have been initialized - (require 'package) - (setq package-activated-list nil - package--initialized nil) - (let (byte-compile-warnings) - (condition-case _ - (package-initialize) - ('error (package-refresh-contents) - (setq doom--refreshed-p t) - (package-initialize)))) - ;; Ensure core packages are installed - (when-let* ((core-packages (cl-remove-if #'package-installed-p doom-core-packages))) - (message "Installing core packages") - (unless doom--refreshed-p - (package-refresh-contents)) - (dolist (package core-packages) - (let ((inhibit-message t)) - (package-install package)) - (if (package-installed-p package) - (message "✓ Installed %s" package) - (error "✕ Couldn't install %s" package))) - (message "Installing core packages...done")) - (unless noninteractive - (add-hook 'doom-pre-init-hook #'doom|refresh-cache))) - ;; Load autoloads file - (doom-initialize-autoloads)) + ;; Set this to prevent infinite recursive calls to `doom-initialize' + (setq doom-init-p t) + ;; `doom-autoload-file' tells Emacs where to load all its autoloaded + ;; functions from. This includes everything in core/autoload/*.el and all + ;; the autoload files in your enabled modules. + (unless (doom-initialize-autoloads doom-autoload-file force-p) + (doom-ensure-core-directories) + (doom-ensure-packages-initialized force-p) + (doom-ensure-core-packages) + ;; Regenerate `doom-autoload-file', which tells Doom where to find all its + ;; module autoloaded functions. + (unless (or force-p noninteractive) + (doom//reload-doom-autoloads))) + ;; Loads `doom-package-autoload-file', which caches `load-path', + ;; `auto-mode-alist', `Info-directory-list', `doom-disabled-packages' and + ;; `package-activated-list'. A big reduction in startup time. + (unless (doom-initialize-autoloads doom-package-autoload-file force-p) + (unless (or force-p noninteractive) + (doom//reload-package-autoloads)))) ;; Initialize Doom core (unless noninteractive (require 'core-ui) (require 'core-editor) (require 'core-projects) - (require 'core-keybinds)) - ;; Bootstrap Doom - (unless doom-init-p + (require 'core-keybinds))) + +(defun doom-initialize-modules (&optional force-p) + "Loads the init.el in `doom-private-dir' and sets up hooks for a healthy +session of Dooming. Will noop if used more than once, unless FORCE-P is +non-nil." + (when (or force-p (not doom-init-modules-p)) + ;; Set `doom-init-modules-p' early, so `doom-pre-init-hook' won't infinitely + ;; recurse by accident if any of them need `doom-initialize-modules'. + (setq doom-init-modules-p t) (unless noninteractive - (add-hook! 'doom-reload-hook - #'(doom|refresh-cache doom|display-benchmark)) (add-hook! 'emacs-startup-hook #'(doom|post-init doom|display-benchmark))) (run-hooks 'doom-pre-init-hook) (when doom-private-dir - (load (concat doom-private-dir "init") t t))) - (setq doom-init-p t)) + (let ((load-prefer-newer t)) + (load (expand-file-name "init" doom-private-dir) + 'noerror 'nomessage))))) -(defun doom-initialize-autoloads () - "Tries to load `doom-autoload-file', otherwise throws an error (unless in a -noninteractive session)." - (unless - (condition-case-unless-debug e - (load (substring doom-autoload-file 0 -3) 'noerror 'nomessage) - (error - (funcall (if noninteractive #'warn #'error) - "Autoload error: %s -> %s" - (car e) (error-message-string e)))) - (unless noninteractive - (error "No autoloads file! Run make autoloads")))) +(defun doom-initialize-autoloads (file &optional clear-p) + "Tries to load FILE (an autoloads file). Otherwise tries to regenerate it. If +CLEAR-P is non-nil, regenerate it anyway." + (unless clear-p + (load (file-name-sans-extension file) 'noerror 'nomessage))) (defun doom-initialize-packages (&optional force-p) "Ensures that Doom's package management system, package.el and quelpa are @@ -306,8 +309,8 @@ Use this before any of package.el, quelpa or Doom's package management's API to ensure all the necessary package metadata is initialized and available for them." (with-temp-buffer ; prevent buffer-local settings from propagating - ;; Prefer uncompiled files to reduce stale code issues - (let ((load-prefer-newer t)) + (let ((load-prefer-newer t) ; reduce stale code issues + (doom-modules (doom-module-table))) ;; package.el and quelpa handle themselves if their state changes during ;; the current session, but if you change an packages.el file in a module, ;; there's no non-trivial way to detect that, so we give you a way to @@ -454,6 +457,37 @@ added, if the file exists." collect (plist-get plist :path)) (cl-remove-if-not #'file-directory-p doom-psuedo-module-dirs))) +(defun doom-module-table (&optional modules) + "Converts MODULES (a malformed plist) into a hash table of modules, fit for +`doom-modules'. If MODULES is omitted, it will fetch your module mplist from the +`doom!' block in your private init.el file." + (let* ((doom-modules (make-hash-table :test #'equal + :size (if modules (length modules) 100) + :rehash-threshold 1.0))) + (when (null modules) + (let ((init-file (expand-file-name "init.el" doom-private-dir))) + (if (not (file-exists-p init-file)) + (error "%s doesn't exist" (abbreviate-file-name init-file)) + (with-temp-buffer + (insert-file-contents init-file) + (when (re-search-forward "^\\s-*\\((doom! \\)" nil t) + (goto-char (match-beginning 1)) + (setq modules (cdr (sexp-at-point)))))) + (unless modules + (error "Couldn't gather module list from %s" init-file)))) + (if (eq modules t) (setq modules nil)) + (let (category) + (dolist (m modules) + (cond ((keywordp m) (setq category m)) + ((not category) (error "No module category specified for %s" m)) + ((let ((module (if (listp m) (car m) m)) + (flags (if (listp m) (cdr m)))) + (if-let* ((path (doom-module-locate-path category module))) + (doom-module-set category module :flags flags :path path) + (when doom-debug-mode + (message "Couldn't find the %s %s module" category module)))))))) + doom-modules)) + ;; ;; Use-package modifications @@ -506,25 +540,40 @@ added, if the file exists." (defmacro doom! (&rest modules) "Bootstraps DOOM Emacs and its modules. -MODULES is an malformed plist of modules to load." - (let (init-forms config-forms file-name-handler-alist) - (let (module) - (dolist (m modules) - (cond ((keywordp m) (setq module m)) - ((not module) (error "No namespace specified in `doom!' for %s" m)) - ((let ((submodule (if (listp m) (car m) m)) - (flags (if (listp m) (cdr m)))) - (let ((path (doom-module-locate-path module submodule))) - (if (not path) - (when doom-debug-mode - (message "Couldn't find the %s %s module" module submodule)) - (doom-module-set module submodule :flags flags :path path) - (push `(let ((doom--current-module ',(cons module submodule))) - (load! init ,path t)) - init-forms) - (push `(let ((doom--current-module ',(cons module submodule))) - (load! config ,path t)) - config-forms)))))))) +The bootstrap process involves making sure the essential directories exist, core +packages are installed, `doom-autoload-file' is loaded, `doom-packages-file' +cache exists (and is loaded) and, finally, loads your private init.el (which +should contain your `doom!' block). + +If the cache exists, much of this function isn't run, which substantially +reduces startup time. + +The overall load order of Doom is as follows: + + ~/.emacs.d/init.el + ~/.emacs.d/core/core.el + `doom-pre-init-hook' + ~/.doom.d/init.el + Module init.el files + `doom-init-hook' + Module config.el files + ~/.doom.d/config.el + `after-init-hook' + `emacs-startup-hook' + `doom-post-init-hook' (at end of `emacs-startup-hook') + +Module load order is determined by your `doom!' block. See `doom-modules-dirs' +for a list of all recognized module trees. Order defines precedence (from most +to least)." + (let ((doom-modules (doom-module-table (or modules t))) + init-forms config-forms file-name-handler-alist) + (maphash (lambda (key value) + (let ((path (plist-get value :path))) + (push `(let ((doom--current-module ',key)) (load! init ,path t)) + init-forms) + (push `(let ((doom--current-module ',key)) (load! config ,path t)) + config-forms))) + doom-modules) `(let (file-name-handler-alist) (setq doom-modules ',doom-modules) ,@(nreverse init-forms) @@ -533,8 +582,9 @@ MODULES is an malformed plist of modules to load." (let ((doom--stage 'config)) ,@(nreverse config-forms) (when doom-private-dir - (load ,(concat doom-private-dir "config") - t (not doom-debug-mode)))))))) + (let ((load-prefer-newer t)) + (load ,(expand-file-name "config" doom-private-dir) + t (not doom-debug-mode))))))))) (defmacro def-package! (name &rest plist) "A thin wrapper around `use-package'." diff --git a/core/core.el b/core/core.el index 3a504f5f9..bfe831b34 100644 --- a/core/core.el +++ b/core/core.el @@ -58,6 +58,20 @@ Use this for files that change often, like cache files.") "Where your private customizations are placed. Must end in a slash. Respects XDG directory conventions if ~/.config/doom exists.") +;; Doom hooks +(defvar doom-pre-init-hook nil + "Hooks run after Doom is first initialized; after Doom's core files are +loaded, but before your private init.el file or anything else is loaded.") + +(defvar doom-init-hook nil + "Hooks run after all init.el files are loaded, including your private and all +module init.el files, but before their config.el files are loaded.") + +(defvar doom-post-init-hook nil + "A list of hooks run when Doom is fully initialized. Fires at the end of +`emacs-startup-hook', as late as possible. Guaranteed to run after everything +else (except for `window-setup-hook').") + ;;; ;; UTF-8 as the default coding system @@ -106,22 +120,6 @@ XDG directory conventions if ~/.config/doom exists.") url-configuration-directory (concat doom-etc-dir "url/")) -;; Custom init hooks; clearer than `after-init-hook', `emacs-startup-hook', and -;; `window-setup-hook'. -(defvar doom-pre-init-hook nil - "Hooks run after Doom is first initialized; after Doom's core files are -loaded, but before your private init.el file or anything else is loaded.") - -(defvar doom-init-hook nil - "Hooks run after all init.el files are loaded, including your private and all -module init.el files, but before their config.el files are loaded.") - -(defvar doom-post-init-hook nil - "A list of hooks run when Doom is fully initialized. Fires at the end of -`emacs-startup-hook', as late as possible. Guaranteed to run after everything -else (except for `window-setup-hook').") - - ;; ;; Emacs fixes/hacks ;; @@ -163,7 +161,7 @@ with functions that require it (like modeline segments)." (advice-add #'make-indirect-buffer :around #'doom*set-indirect-buffer-filename) ;; Truly silence startup message -(advice-add #'display-startup-echo-area-message :override #'ignore) +(fset #'display-startup-echo-area-message #'ignore) ;; @@ -202,8 +200,10 @@ this, you'll get stuttering and random freezes) and resets (require 'core-packages) (require 'core-os) -(unless noninteractive - (doom-initialize)) +(doom-initialize noninteractive) +(if noninteractive + (require 'core-dispatcher) + (doom-initialize-modules)) (provide 'core) ;;; core.el ends here diff --git a/init.el b/init.el index 497941f17..eda0e8894 100644 --- a/init.el +++ b/init.el @@ -31,5 +31,3 @@ load-prefer-newer noninteractive) (require 'core (concat user-emacs-directory "core/core")) -(when noninteractive - (require 'core-dispatcher))