diff --git a/.gitignore b/.gitignore index 555bbf2b5..acb81ccc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ # machine generated doom profiles or metadata -/profiles/* -!/profiles/*.org -!/profiles/*@static/ +/profiles/init.*el /.local*/ # possible user config files diff --git a/bin/doom b/bin/doom index 255d68c99..4523d1ae8 100755 --- a/bin/doom +++ b/bin/doom @@ -259,6 +259,7 @@ SEE ALSO: (defcli-group! "Config Management" :docs "Commands for maintaining your Doom Emacs configuration." (defcli-autoload! ((sync s))) + (defcli-autoload! ((profiles profile))) (defcli-autoload! ((upgrade up))) (defcli-autoload! (env)) (defcli-autoload! ((build b purge p rollback)) "packages") diff --git a/docs/examples.org b/docs/examples.org index a89daa5c6..f86a096da 100644 --- a/docs/examples.org +++ b/docs/examples.org @@ -714,11 +714,92 @@ These are side-by-side comparisons, showing how to bind keys with and without **** TODO with-file-contents! ** TODO Configuration files -*** TODO doomprofiles.el +*** =profiles.el= +:PROPERTIES: +:ID: f9bce7da-d155-4727-9b6f-b566b5b8d824 +:END: +This file can live in any of: + +- =$DOOMDIR/profiles.el= +- =$EMACSDIR/profiles.el= +- =~/.config/doom-profiles.el= +- =~/.doom-profiles.el= + +Here is an exhaustive example of all its syntax and capabilities: +#+begin_src emacs-lisp +;; -*- mode: emacs-lisp; -*- +((profile1 + ;; The permitted formats of each entry: + (var . value) + ("envvar" . value) + (var :directive values...) + + ;; `user-emacs-directory' is often the first variable you want to set, so + ;; Emacs knows where this profile lives. If you don't, it'll use the config + ;; living in the default locations (~/.config/emacs or ~/.emacs.d). + (user-emacs-directory . "~/another/emacs/config/") + ;; If this is a Doom config, you'll also want to set `doom-user-dir', which + ;; defaults to ~/.config/doom or ~/.doom.d: + (doom-user-dir . "~/another/doom/config/") + ;; If a CAR is a string, it is assumed you want to set an environment + ;; variable. (Side-note: setting DOOMDIR will be unnecessary if you're setting + ;; `doom-user-dir' above). + ("DOOMDIR" . "~/another/doom/config/") + + ;; Doom profiles support a number of special directives. They are: + ;; + ;; (VAR :path SEGMENTS...) -- set VAR to an exapnded path built from SEGMENTS, + ;; relative to `user-emacs-directory', unless an absolute path is in SEGMENTS. + (doom-cache-dir :path doom-user-dir ".local/cache") + (doom-data-dir :path doom-user-dir ".local/data") + (doom-state-dir :path doom-user-dir ".local/state") + ;; (VAR :plist VALUE) -- use VALUE as a literal plist; ignoring any profile + ;; directives that may be in it. + (some-plist :plist (:foo bar :baz womp)) + ;; (VAR :eval FORMS...) -- use to evaluate arbitrary elisp forms. Note that + ;; his runs early in early-init.el. It's wise to assume no APIs are available + ;; or loaded, only the previous bindings in this profile. + (doom-theme :eval (if (equal (system-name) "foo") 'doom-one 'doom-dracula)) + ;; Though discouraged, you may evaluate forms without a binding by using `_'. + ;; You really should be doing this in the profile though... + (_ :eval (message "Hello world!")) + (_ :eval (with-eval-after-load 'company (setq-default company-idle-delay 2.0))) + ;; (VAR :prepend FORMS...) or (VAR :append FORMS...) -- prepend or append the + ;; evaluated result of each form in FORMS to VAR (a list). If VAR is undefined + ;; at startup, it will be deferred until the variable is available. + (load-path :prepend (expand-file-name "packages/" doom-user-dir)) + (load-path :prepend (expand-file-name "lisp/" doom-user-dir)) + (load-path :append (expand-file-name "fallback/" doom-user-dir)) + (exec-path :prepend (expand-file-name "bin/" doom-user-dir)) + (auto-mode-alist :prepend '("\\.el\\'" . lisp-mode))) + + (profile2 + ...) + + (profile3 + ...)) +#+end_src + +*** =.doomprofile= +:PROPERTIES: +:ID: ac37ac6f-6082-4c34-b98c-962bc1e528c9 +:END: +This file takes after the second level of =profiles.el='s format (see a more +complete example in [[id:f9bce7da-d155-4727-9b6f-b566b5b8d824][the previous section]]). For example: + +#+begin_src emacs-lisp +;;; -*- mode: emacs-lisp -*- +;; A .doomprofile can be placed under an implicit profile. Same rules as +;; .doom-profiles.el, but one level deeper. + +((var . value) + ("envvar" . value) + (var :directive values...)) +#+end_src + *** TODO =.doomrc= *** TODO =.doomproject= *** TODO =.doommodule= -*** TODO =.doomprofile= ** TODO Templates *** TODO User configuration *** TODO Module diff --git a/early-init.el b/early-init.el index ec21f4a42..cd12e248e 100644 --- a/early-init.el +++ b/early-init.el @@ -62,39 +62,16 @@ (when profile ;; FIX: Discard the switch to prevent "invalid option" errors later. (push (cons "--profile" (lambda (_) (pop argv))) command-switch-alist) - ;; While processing the requested profile, Doom loosely expects - ;; `user-emacs-directory' to be changed. If it doesn't, then you're using - ;; profiles.el as a glorified, runtime dir-locals.el (which is fine, if - ;; intended). - (catch 'found - (let ((profiles-file (expand-file-name "profiles.el" user-emacs-directory))) - (when (file-exists-p profiles-file) - (with-temp-buffer - (let ((coding-system-for-read 'utf-8-auto)) - (insert-file-contents profiles-file)) - (condition-case-unless-debug e - (let ((profile-data (cdr (assq (intern profile) (read (current-buffer)))))) - (dolist (var profile-data (if profile-data (throw 'found t))) - (if (eq (car var) 'env) - (dolist (env (cdr var)) (setenv (car env) (cdr env))) - (set (car var) (cdr var))))) - (error (error "Failed to parse profiles.el: %s" (error-message-string e)))))) - ;; If the requested profile isn't in profiles.el, then see if - ;; $EMACSDIR/profiles/$DOOMPROFILE exists. These are implicit - ;; profiles, where `emacs --profile foo` will be equivalent to `emacs - ;; --init-directory $EMACSDIR/profile/foo', if that directory exists. - (let ((profile-dir - (expand-file-name - profile (or (getenv-internal "DOOMPROFILESDIR") - (expand-file-name "profiles/" user-emacs-directory))))) - (when (file-directory-p profile-dir) - (setq user-emacs-directory profile-dir) - (throw 'found t))) - (user-error "No %S profile found" profile))) - (when init-file-debug - (message "Selected profile: %s" profile)) - ;; Ensure the selected profile persists through the session - (setenv "DOOMPROFILE" profile))) + ;; Running 'doom sync' will (re)generate a lightweight profile + ;; bootstrapper in $EMACSDIR/profiles/init.el, after reading + ;; $EMACSDIR/profiles.el, $DOOMDIR/profiles, + ;; $XDG_CONFIG_HOME/doom-profiles.el, and ~/.doom-profiles.el. All it + ;; needs is for `$DOOMPROFILE' to be set. + (setenv "DOOMPROFILE" profile) + (or (load (expand-file-name (format "profiles/init.%d" emacs-major-version) + user-emacs-directory) + 'noerror 'nomessage nil t) + (user-error "Profiles not initialized yet; run 'doom sync' first")))) ;; PERF: When `load'ing or `require'ing files, each permutation of ;; `load-suffixes' and `load-file-rep-suffixes' (then `load-suffixes' + @@ -110,29 +87,10 @@ (if (load (expand-file-name "lisp/doom" user-emacs-directory) 'noerror 'nomessage nil 'must-suffix) ;; ...and prepare for the rest of the session. - (if noninteractive - (doom-require 'doom-cli) - ;; HACK: This advice hijacks Emacs' initfile resolver to replace - ;; $EMACSDIR/init.el (and ~/.emacs or ~/_emacs) with a - ;; a Doom-provided init file. Later, this file will be - ;; generated by 'doom sync' for the active Doom profile; - ;; `doom-start' is its stand-in until that's implemented. - ;; - ;; This effort spares Emacs the overhead of searching for - ;; initfiles we don't care about, enables savvier hackers to - ;; use $EMACSDIR as their $DOOMDIR, and gives us an opportunity - ;; to fall back to a "safe mode", so we can present a more - ;; user-friendly failure state. - (define-advice startup--load-user-init-file (:filter-args (args) init-doom) - "Initialize Doom Emacs in an interactive session." - (list (lambda () - (file-name-concat doom-core-dir "doom-start")) - (lambda () - (file-name-concat doom-profiles-dir "safe-mode" "init.el")) - (caddr args)))))) + (doom-require (if noninteractive 'doom-cli 'doom-start)))) ;; Failing that, assume we're loading a non-Doom config and prepare. (ignore - (setq early-init-file (expand-file-name "early-init" user-emacs-directory) + (setq user-init-file (expand-file-name "early-init" user-emacs-directory) ;; I make no assumptions about the config we're about to load, so ;; to limit side-effects, undo any leftover optimizations: load-prefer-newer t)))) diff --git a/lisp/cli/autoloads.el b/lisp/cli/autoloads.el index 42471589a..fba0b3da0 100644 --- a/lisp/cli/autoloads.el +++ b/lisp/cli/autoloads.el @@ -26,57 +26,6 @@ hoist buggy forms into autoloads.") ;; ;;; Library -(defun doom-autoloads-reload (&optional file) - "Regenerates Doom's autoloads and writes them to FILE." - (unless file - ;; TODO Uncomment when profile system is implemented - ;; (make-directory doom-profile-dir t) - ;; (setq file (expand-file-name "init.el" doom-profile-dir)) - (setq file doom-autoloads-file)) - (print! (start "(Re)generating autoloads file...")) - (print-group! - (cl-check-type file string) - (doom-initialize-packages) - (and (print! (start "Generating autoloads file...")) - (doom-autoloads--write - file - `((unless (equal doom-version ,doom-version) - (signal 'doom-error - (list "The installed version of Doom has changed since last 'doom sync' ran" - "Run 'doom sync' to bring Doom up to speed")))) - (cl-loop for var in doom-autoloads-cached-vars - when (boundp var) - collect `(set ',var ',(symbol-value var))) - ;; Cache module state and flags in symbol plists for quick lookup by - ;; `modulep!' later. - (cl-loop for (category . modules) in (seq-group-by #'car (doom-module-list)) - collect `(setplist ',category - (quote ,(cl-loop for (_ . module) in modules - nconc `(,module ,(get category module)))))) - (doom-autoloads--scan - (append (doom-glob doom-core-dir "lib/*.el") - (cl-loop for dir - in (append (doom-module-load-path doom-modules-dirs) - (list doom-user-dir)) - if (doom-glob dir "autoload.el") collect (car it) - if (doom-glob dir "autoload/*.el") append it) - (mapcan #'doom-glob doom-autoloads-files)) - nil) - (doom-autoloads--scan - (mapcar #'straight--autoloads-file - (seq-difference (hash-table-keys straight--build-cache) - doom-autoloads-excluded-packages)) - doom-autoloads-excluded-files - 'literal) - ;; TODO Uncomment when profile system is implemented - ;; `((unless noninteractive (require 'doom-start))) - ) - (print! (start "Byte-compiling autoloads file...")) - (doom-autoloads--compile-file file) - (print! (success "Generated %s") - (relpath (byte-compile-dest-file file) - doom-emacs-dir))))) - (defun doom-autoloads--write (file &rest forms) (make-directory (file-name-directory file) 'parents) (condition-case-unless-debug e diff --git a/lisp/cli/doctor.el b/lisp/cli/doctor.el index 789575035..e971efe40 100644 --- a/lisp/cli/doctor.el +++ b/lisp/cli/doctor.el @@ -159,125 +159,118 @@ in." (print! (start "Checking Doom Emacs...")) (condition-case-unless-debug ex (print-group! - (let ((noninteractive nil) - kill-emacs-query-functions - kill-emacs-hook) - (defvar doom-reloading-p nil) - (unless (file-exists-p doom-autoloads-file) - (user-error "Autoloads file not generated. Did you remember to run 'doom sync'?")) - (require 'doom-start) - (doom-initialize-packages)) + (require 'doom-start) - (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 "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! (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)")))) + (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-projectile-fd-binary)) - (warn! "Couldn't find the `fd' binary; project file searches will be slightly slower")) + (unless (ignore-errors (executable-find doom-projectile-fd-binary)) + (warn! "Couldn't find the `fd' binary; project file searches will be slightly slower")) - (require 'projectile) - (when (projectile-project-root "~") - (warn! "Your $HOME is recognized as a project root") - (explain! "Emacs will assume $HOME is the root of any project living under $HOME. If this isn't\n" - "desired, you will need to remove \".git\" from `projectile-project-root-files-bottom-up'\n" - "(a variable), e.g.\n\n" - " (after! projectile\n" - " (setq projectile-project-root-files-bottom-up\n" - " (remove \".git\" projectile-project-root-files-bottom-up)))")) + (require 'projectile) + (when (projectile-project-root "~") + (warn! "Your $HOME is recognized as a project root") + (explain! "Emacs will assume $HOME is the root of any project living under $HOME. If this isn't\n" + "desired, you will need to remove \".git\" from `projectile-project-root-files-bottom-up'\n" + "(a variable), e.g.\n\n" + " (after! projectile\n" + " (setq projectile-project-root-files-bottom-up\n" + " (remove \".git\" projectile-project-root-files-bottom-up)))")) - ;; 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.")) + ;; 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.")) - ;; Check for fonts - (if (not (executable-find "fc-list")) - (warn! "Warning: unable to detect fonts because fontconfig isn't installed") - ;; all-the-icons fonts - (when (and (pcase system-type - (`gnu/linux (concat (or (getenv "XDG_DATA_HOME") - "~/.local/share") - "/fonts/")) - (`darwin "~/Library/Fonts/")) - (require 'all-the-icons nil t)) - (with-temp-buffer - (let ((errors 0)) - (cl-destructuring-bind (status . output) - (doom-call-process "fc-list" "" "file") - (if (not (zerop status)) - (print! (error "There was an error running `fc-list'. Is fontconfig installed correctly?")) - (insert (cdr (doom-call-process "fc-list" "" "file"))) - (dolist (font all-the-icons-font-names) - (if (save-excursion (re-search-backward font nil t)) - (success! "Found font %s" font) - (print! (warn "Warning: couldn't find %S font") font))) - (when (> errors 0) - (explain! "Some all-the-icons fonts were missing.\n\n" - "You can install them by running `M-x all-the-icons-install-fonts' within Emacs.\n" - "This could also mean you've installed them in non-standard locations, in which " - "case feel free to ignore this warning."))))))))) + ;; Check for fonts + (if (not (executable-find "fc-list")) + (warn! "Warning: unable to detect fonts because fontconfig isn't installed") + ;; all-the-icons fonts + (when (and (pcase system-type + (`gnu/linux (concat (or (getenv "XDG_DATA_HOME") + "~/.local/share") + "/fonts/")) + (`darwin "~/Library/Fonts/")) + (require 'all-the-icons nil t)) + (with-temp-buffer + (let ((errors 0)) + (cl-destructuring-bind (status . output) + (doom-call-process "fc-list" "" "file") + (if (not (zerop status)) + (print! (error "There was an error running `fc-list'. Is fontconfig installed correctly?")) + (insert (cdr (doom-call-process "fc-list" "" "file"))) + (dolist (font all-the-icons-font-names) + (if (save-excursion (re-search-backward font nil t)) + (success! "Found font %s" font) + (print! (warn "Warning: couldn't find %S font") font))) + (when (> errors 0) + (explain! "Some all-the-icons fonts were missing.\n\n" + "You can install them by running `M-x all-the-icons-install-fonts' within Emacs.\n" + "This could also mean you've installed them in non-standard locations, in which " + "case feel free to ignore this warning."))))))))) - (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))) + (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...")) - (advice-add #'require :around #'doom-shut-up-a) - (maphash (lambda (key plist) - (let (doom-local-errors - doom-local-warnings) - (let (doom-doctor--errors - doom-doctor--warnings) - (condition-case-unless-debug ex - (let ((doom--current-module key) - (doom--current-flags (plist-get plist :flags)) - (doctor-file (doom-module-expand-path (car key) (cdr key) "doctor.el")) - (packages-file (doom-module-expand-path (car key) (cdr key) "packages.el"))) - (cl-loop with doom-output-indent = 6 - for name in (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)) - (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")) (car key) (cdr key)) - (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))) - (appendq! doom-doctor--errors doom-local-errors) - (appendq! doom-doctor--warnings doom-local-warnings))) - doom-modules))) + (when doom-modules + (print! (start "Checking your enabled modules...")) + (advice-add #'require :around #'doom-shut-up-a) + (maphash (lambda (key plist) + (let (doom-local-errors + doom-local-warnings) + (let (doom-doctor--errors + doom-doctor--warnings) + (condition-case-unless-debug ex + (let ((doom--current-module key) + (doom--current-flags (plist-get plist :flags)) + (doctor-file (doom-module-expand-path (car key) (cdr key) "doctor.el")) + (packages-file (doom-module-expand-path (car key) (cdr key) "packages.el"))) + (cl-loop with doom-output-indent = 6 + for name in (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)) + (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")) (car key) (cdr key)) + (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))) + (appendq! doom-doctor--errors doom-local-errors) + (appendq! doom-doctor--warnings doom-local-warnings))) + doom-modules))) (error (warn! "Attempt to load DOOM failed\n %s\n" (or (cdr-safe ex) (car ex))) diff --git a/lisp/cli/sync.el b/lisp/cli/sync.el index 6559f2049..9af7426cd 100644 --- a/lisp/cli/sync.el +++ b/lisp/cli/sync.el @@ -45,6 +45,7 @@ OPTIONS: Defaults to the maximum number of threads (or 1, if your CPU's threadcount can't be determined)." :benchmark t + (call! '(profiles sync)) (run-hooks 'doom-before-sync-hook) (add-hook 'kill-emacs-hook #'doom-sync--abort-warning-h) (when jobs @@ -63,9 +64,9 @@ OPTIONS: (when update? (doom-packages-update)) (doom-packages-purge purge? 'builds-p purge? purge? purge?) - (run-hooks 'doom-after-sync-hook) - (when (doom-autoloads-reload) - (print! (item "Restart Emacs or use 'M-x doom/reload' for changes to take effect"))) + (when (doom-profile-generate) + (print! (item "Restart Emacs or use 'M-x doom/reload' for changes to take effect")) + (run-hooks 'doom-after-sync-hook)) t) (remove-hook 'kill-emacs-hook #'doom-sync--abort-warning-h))) diff --git a/lisp/doom-cli.el b/lisp/doom-cli.el index 8b952ffd8..004e7ff82 100644 --- a/lisp/doom-cli.el +++ b/lisp/doom-cli.el @@ -72,9 +72,9 @@ ;; (doom-require 'doom-lib 'autoloads) ;; Ensure straight and core packages are ready to go for CLI commands. - ;; (require 'doom-profiles) (require 'doom-modules) (require 'doom-packages) + (require 'doom-profiles) ;; For any last-minute initialization. (run-hooks 'doom-before-init-hook)) diff --git a/lisp/doom-lib.el b/lisp/doom-lib.el index 83bc17ba1..f37b54c27 100644 --- a/lisp/doom-lib.el +++ b/lisp/doom-lib.el @@ -4,6 +4,7 @@ ;;; Custom error types (define-error 'doom-error "An unexpected Doom error") +(define-error 'doom-nosync-error "Doom hasn't been initialized yet; did you remember to run 'doom sync' in the shell?" 'doom-error) (define-error 'doom-core-error "Unexpected error in Doom's core" 'doom-error) (define-error 'doom-hook-error "Error in a Doom startup hook" 'doom-error) (define-error 'doom-autoload-error "Error in Doom's autoloads file" 'doom-error) diff --git a/lisp/doom-modules.el b/lisp/doom-modules.el index e693d76a8..da22f5bf7 100644 --- a/lisp/doom-modules.el +++ b/lisp/doom-modules.el @@ -99,49 +99,6 @@ your `doom!' block, a warning is emitted before replacing it with :emacs vc and (defvar doom--current-flags nil) -;; -;;; Bootstrap API - -(defun doom-initialize-core-modules () - "Load Doom's core files for an interactive session." - (require 'doom-keybinds) - (require 'doom-ui) - (require 'doom-projects) - (require 'doom-editor)) - -(defun doom-module-loader (file) - "Return a closure that loads FILE from a module. - -This closure takes two arguments: a cons cell containing (CATEGORY . MODULE) -symbols, and that module's plist." - (lambda (module plist) - (let ((doom--current-module module) - (doom--current-flags (cdr (get (car module) (cdr module)))) - (inhibit-redisplay t)) - (load! file (plist-get plist :path) t)))) - -(defun doom-initialize-modules (&optional force-p no-config-p) - "Loads the init.el in `doom-user-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)) - (setq doom-init-modules-p t) - (unless no-config-p - (doom-log "Initializing core modules") - (doom-initialize-core-modules)) - (when-let (init-p (load! doom-module-init-file doom-user-dir t)) - (doom-log "Initializing user config") - (doom-run-hooks 'doom-before-module-init-hook) - (maphash (doom-module-loader doom-module-init-file) doom-modules) - (doom-run-hooks 'doom-after-module-init-hook) - (unless no-config-p - (doom-run-hooks 'doom-before-module-config-hook) - (maphash (doom-module-loader doom-module-config-file) doom-modules) - (doom-run-hooks 'doom-after-module-config-hook) - (load! "config" doom-user-dir t) - (when custom-file - (load custom-file 'noerror (not doom-debug-mode))))))) - - ;; ;;; Module API diff --git a/lisp/doom-profiles.el b/lisp/doom-profiles.el new file mode 100644 index 000000000..54da058ac --- /dev/null +++ b/lisp/doom-profiles.el @@ -0,0 +1,422 @@ +;;; lisp/doom-profiles.el -*- lexical-binding: t; -*- +;;; Commentary: +;;; Code: +(eval-when-compile (require 'doom)) ; be silent, o'byte-compiler + + +;; +;;; Variables + +;;; File/directory variables +(defvar doom-profiles-dir doom-data-dir + "Where generated profiles are kept. + +Profile directories are in the format {data-profiles-dir}/$NAME/@/$VERSION, for +example: '~/.local/share/doom/_/@/0/'") + +(defvar doom-profile-dirs + (list (file-name-concat doom-user-dir "profiles") + (file-name-concat doom-emacs-dir "profiles")) + "A list of directories to search for implicit Doom profiles in.") + +(defvar doom-profile-config-files + (list (file-name-concat doom-user-dir "profiles.el") + (file-name-concat doom-emacs-dir "profiles.el") + (expand-file-name "doom-profiles.el" (or (getenv "XDG_CONFIG_HOME") "~/.config")) + (expand-file-name "~/.doom-profiles.el")) + "A list of potential locations for a profiles.el file. + +`doom-profiles-initialize' will load and merge all profiles defined in the above +files, and will write a summary profiles.el to the first entry in this +variable.") + +(defvar doom-profiles-bootstrap-file + (file-name-concat doom-emacs-dir (format "profiles/init.%d.el" emacs-major-version)) + "Where Doom writes its profile bootstrap script.") + +(defvar doom-profile-init-file-name (format "init.%d.el" emacs-major-version) + "TODO") + +(defvar doom-profile-init-dir-name (format "init.%d.d" emacs-major-version) + "The subdirectory of `doom-profile-dir'") + +(defvar doom-profiles-config-file-name ".doomprofile" + "TODO") + +;;; Profile storage variables +(defvar doom-profile-generators + '(("05-init-vars.auto.el" . doom-profile--generate-init-vars) + ("80-loaddefs.auto.el" . doom-profile--generate-doom-autoloads) + ("90-loaddefs-packages.auto.el" . doom-profile--generate-package-autoloads) + ("95-load-modules.auto.el" . doom-profile--generate-load-modules)) + "An alist mapping file names to generator functions. + +The file will be generated in `doom-profile-dir'/`doom-profile-init-dir-name', +and later combined into `doom-profile-dir'/`doom-profile-init-file-name' in +lexicographical order. These partials are left behind in case the use wants to +load them directly (for whatever use), or for commands to use (e.g. +`doom/reload-autoloads' loads any file with a NN-loaddefs[-.] prefix to +accomplish its namesake). + +Files with an .auto.el suffix will be automatically deleted whenever the profile +is regenerated. Users (or Doom CLIs, like `doom env') may add their own +generators to this list, or to `doom-profile-dir'/`doom-profile-init-dir-name', +and they will be included in the profile init file next time `doom sync' is +run.") + +(defvar doom--profiles ()) + +;; TODO Restore this in 3.0 +(defconst doom-profile-default nil) +;; (defconst doom-profile-default (cons "_" "0")) + + +;; +;;; Helpers + +(defun doom-profiles-read (&rest paths) + "TODO" + (let (profiles) + (dolist (path (flatten-list paths)) + (cond + ((file-directory-p path) + (dolist (subdir (doom-files-in (file-truename path) :depth 0 :match "/[^.][^/]+$" :type 'dirs :map #'file-name-base)) + (unless (string-prefix-p "_" subdir) + (cl-pushnew + (cons (intern subdir) + (if-let (profile-file (file-exists-p! doom-profiles-config-file-name path)) + (car (doom-file-read profile-file :by 'read*)) + (let ((subdir (file-name-as-directory (abbreviate-file-name subdir)))) + `((user-emacs-directory . ,subdir) + ,@(when (file-exists-p! "lisp/doom.el" subdir) + '(doom-user-dir . ,subdir)))))) + profiles + :test #'eq + :key #'car)))) + ((file-exists-p path) + (dolist (profile (car (doom-file-read path :by 'read*))) + (unless (string-prefix-p "_" (symbol-name (car profile))) + (cl-pushnew profile profiles + :test #'eq + :key #'car)))))) + (when (assq '_ profiles) + (signal 'doom-profile-error (list "Profile cannot be named _, as this is reserved for the implicit global profile"))) + (nreverse profiles))) + +(defun doom-profiles-autodetect () + "Return all known profiles as a nested alist. + +This reads all profiles in `doom-profile-config-files', then reads implicit profiles +living in `doom-profile-dirs', then caches them in `doom--profiles'. If RELOAD? +is non-nil, refresh the cache." + (doom-profiles-read doom-profile-config-files + doom-profile-dirs)) + +(defun doom-profiles-outdated-p () + "Return non-nil if files in `doom-profiles-bootstrap-file' are outdated." + (cl-find-if (doom-rpartial #'file-newer-than-file-p doom-profiles-bootstrap-file) + doom-profile-config-files)) + +(defun doom-profile<-id (id) + "Return a (NAME . VERSION) profile cons cell from an id string NAME@VERSION." + (save-match-data + (if (string-match "^\\([^@]+\\)@\\(.+\\)$" id) + (cons (match-string 1 id) + (match-string 2 id)) + (cons id (cdr doom-profile-default))))) + +(defun doom-profile->id (profile) + "Return a NAME@VERSION id string from profile cons cell (NAME . VERSION)." + (cl-check-type profile cons) + (format "%s@%s" (car profile) (cdr profile))) + +;; TODO (defun doom-profile--read (profile) +;; (doom-profile-create )) + +;; TODO (defun doom-profile-initialize (profile-name &optional ref) +;; ) + +(defun doom-profiles-save (profiles file) + "Generate a profile bootstrapper for Doom to load at startup." + (doom-file-write + file `(";; -*- lexical-binding: t; tab-width: 8; -*-\n" + ";; Updated: " ,(format-time-string "%Y-%m-%d %H:%M:%S") "\n" + ";; Generated by 'doom profiles sync' or 'doom sync'.\n" + ";; DO NOT EDIT THIS BY HAND!\n" + ,(format "%S" doom-version) + (funcall + (alist-get + (intern (getenv-internal "DOOMPROFILE")) + (list + ,@(cl-loop + with deferred? + = (seq-find (fn! (memq (car-safe %) '(:prepend :prepend? :append :append?))) + (mapcar #'cdr profiles)) + with deferred-varsym = (make-symbol "deferred-vars") + for (name . bindings) in profiles + collect + `(cons ',name + (lambda () + (let ,(if deferred? '(--deferred-vars--)) + ,@(cl-loop + for (var . val) in bindings + collect + (pcase (car-safe val) + (:path + `(,(if (stringp var) 'setenv 'set) + ',var ,(if (cddr val) + (macroexpand-all + `(cl-loop with path = ',(cadr val) + for dir in ',(cddr val) + do (setq path (expand-file-name dir path)) + finally return path)) + `(file-truename ,(cadr val))))) + (:eval + (if (eq var '_) + (macroexp-progn (cdr val)) + `(,(if (stringp var) 'setenv 'set) + ',var ,(macroexp-progn (cdr val))))) + (:plist + `(,(if (stringp var) 'setenv 'set) + ',var ',(if (stringp var) + (prin1-to-string (cadr val)) + (cadr val)))) + ((or :prepend :prepend?) + (if (stringp var) + `(setenv ',var (concat ,val (getenv ,var))) + (setq deferred? t) + `(push (cons ',var + (lambda () + (dolist (item (list ,@(cdr val))) + ,(if (eq (car val) :append?) + `(add-to-list ',var item) + `(push item ',var))))) + --deferred-vars--))) + ((or :append :append?) + (if (stringp var) + `(setenv ,var (concat (getenv ,var) ,val)) + (setq deferred? t) + `(push (cons ',var + (lambda () + (dolist (item (list ,@(cdr val))) + ,(if (eq (car val) :append?) + `(add-to-list ',var item 'append) + `(setq ',var (append ',var (list item))))))) + --deferred-vars--))) + (_ `(,(if (stringp var) 'setenv 'set) + ',var ,(if (and (symbolp var) + (or (eq var 'user-emacs-directory) + (string-match-p "^doom-.+-dir$" (symbol-name var)))) + `(file-truename ,var) + ''var))))) + ,@(when deferred? + `((defun --defer-vars-- (_) + (dolist (var --deferred-vars--) + (when (boundp (car var)) + (funcall (cdr var)) + (setq --deferred-vars-- (delete var --deferred-vars--)))) + (unless --deferred-vars-- + (remove-hook 'after-load-functions #'--defer-vars--) + (unintern '--defer-vars-- obarray) + (unintern '--deferred-vars-- obarray))) + (add-hook 'after-load-functions #'--defer-vars--) + (--defer-vars--)))))))) + (lambda () + (if (or noninteractive + (file-equal-p user-emacs-directory "~/.config/emacs") + (file-equal-p user-emacs-directory "~/.emacs.d")) + (user-error "Failed to find profile: %s" id) + (user-error "To be a bootloader, Doom must be installed in ~/.config/emacs or ~/.emacs.d")))))) + :mode #o600 + :printfn #'pp) + (byte-compile-file file)) + +(defun doom-profile-p (profile-name) + "Return t if PROFILE-NAME is a valid and existing profile." + (when (stringp profile-name) + (setq profile-name (intern profile-name))) + (and (assq profile-name (doom-profiles)) + t)) + +(defun doom-profile-get (profile-name &optional property null-value) + "Return PROFILE-NAME's PROFILE, otherwise its PROPERTY, otherwise NULL-VALUE." + (when (stringp profile-name) + (setq profile-name (intern profile-name))) + (if-let (profile (assq profile-name (doom-profiles))) + (if property + (if-let (propval (assq property (cdr profile))) + (cdr propval) + null-value) + profile) + null-value)) + +(defun doom-profile-emacs-dir (profile-name) + "Return the `user-emacs-directory' for PROFILE-NAME. + +If the profile doesn't specify one, fall back to `doom-emacs-dir'." + (doom-profile-get profile-name 'user-emacs-directory doom-emacs-dir)) + +(defun doom-profile-init-file (&optional profile-id version) + "Return the init file for PROFILE-ID at VERSION. + +Defaults to the profile at `doom-profile-default'." + (cl-destructuring-bind (profile . version) + (if (and (stringp profile-id) (null version)) + (doom-profile<-id profile-id) + (cl-check-type profile-id (or null string)) + (cl-check-type version (or null string)) + (cons (or profile-id (car doom-profile-default)) + (or version (cdr doom-profile-default)))) + (file-name-concat doom-data-dir + profile "@" version + (format doom-profile-init-file-name emacs-major-version)))) + + +;; +;;; Data structures + +;; TODO + + +;; +;;; API + +;; TODO (defun doom-profile-create (name)) + +;; TODO (defun doom-profile-hash (profile)) + +;; TODO (defmacro with-profile! (profile &rest body)) + + +;; +;;; Generators + +(defun doom-profile-generate (&optional _profile regenerate-only?) + "Generate profile init files." + (doom-initialize-packages) + (let* ((default-directory doom-profile-dir) + (init-dir doom-profile-init-dir-name) + (init-file doom-profile-init-file-name)) + (print! (start "(Re)building profile in %s/...") (dirname doom-profile-dir)) + (condition-case-unless-debug e + (with-file-modes #o750 + (print-group! + (make-directory init-dir t) + (print! (start "Deleting old init files...")) + (print-group! :level 'info + (cl-loop for file in (cons init-file (doom-glob "*.elc")) + if (file-exists-p file) + do (print! (item "Deleting %s...") file) + and do (delete-file file))) + (let ((auto-files (doom-glob init-dir "*.auto.el"))) + (print! (start "Generating %d init files...") (length doom-profile-generators)) + (print-group! :level 'info + (dolist (file auto-files) + (print! (item "Deleting %s...") file) + (delete-file file)) + (pcase-dolist (`(,file . ,fn) doom-profile-generators) + (let ((file (doom-path init-dir file))) + (doom-log "Building %s..." file) + (doom-file-write file (funcall fn)))))) + (with-file! init-file + (insert ";; -*- coding: utf-8; lexical-binding: t; -*-\n" + ";; This file was autogenerated; do not edit it by hand!\n") + ;; Doom needs to be synced/rebuilt if either Doom or Emacs has been + ;; up/downgraded. This is because byte-code isn't backwards + ;; compatible, and many packages (including Doom), make in absolute + ;; paths into their caches that need to be refreshed. + (prin1 `(unless (equal doom-version ,doom-version) + (error ,(concat + "The installed version of Doom (%s) has changed (to %s) since last " + "'doom sync'. Run 'doom sync' to bring Doom up to speed") + ,doom-version doom-version)) + (current-buffer)) + (dolist (file (doom-glob init-dir "*.el")) + (print-group! :level 'info + (print! (start "Reading %s...") file)) + (doom-file-read file :by 'insert))) + (print! (start "Byte-compiling %s...") (relpath init-file)) + (print-group! + (let ((byte-compile-warnings (if init-file-debug '(suspicious make-local callargs)))) + (byte-compile-file init-file))) + (print! (success "Built %s") (byte-compile-dest-file init-file)))) + (error (delete-file init-file) + (delete-file (byte-compile-dest-file init-file)) + (signal 'doom-autoload-error (list init-file e)))))) + +(defun doom-profile--generate-init-vars () + (setq doom-autoloads-cached-vars '(load-path + Info-directory-list + auto-mode-alist + interpreter-mode-alist)) + (let ((v (version-to-list doom-version)) + (ref (doom-call-process "git" "-C" (doom-path doom-emacs-dir) "rev-parse" "HEAD")) + (branch (doom-call-process "git" "-C" (doom-path doom-emacs-dir) "branch" "--show-current"))) + `(,@(cl-loop for var in doom-autoloads-cached-vars + if (boundp var) + collect `(set-default ',var ',(symbol-value var))) + (setplist 'doom-version + '(major ,(nth 0 v) + minor ,(nth 1 v) + build ,(nth 2 v) + tag ,(cadr (split-string doom-version "-" t)) + ref ,(if (zerop (car ref)) (cdr ref)) + branch ,(if (zerop (car branch)) (cdr branch))))))) + +(defun doom-profile--generate-load-modules () + (let ((module-list (cddr (doom-module-list)))) + `((set 'doom-disabled-packages ',doom-disabled-packages) + (set 'doom-modules ',doom-modules) + ;; Cache module state and flags in symbol plists for quick lookup by + ;; `modulep!' later. + ,@(cl-loop for (category . modules) in (seq-group-by #'car (doom-module-list)) + collect `(setplist ',category + (quote ,(cl-loop for (_ . module) in modules + nconc `(,module ,(get category module)))))) + (doom-run-hooks 'doom-before-modules-init-hook) + ;; TODO: Until these files are byte-compiler-ready, I must use `load' + ;; instead of `require', as to not invite the byte-compiler to load them + ;; while this init file is compiled. + (doom-load ,(doom-path doom-core-dir "doom-keybinds")) + (doom-load ,(doom-path doom-core-dir "doom-ui")) + (doom-load ,(doom-path doom-core-dir "doom-projects")) + (doom-load ,(doom-path doom-core-dir "doom-editor")) + ,@(cl-loop for (cat . mod) in module-list + if (doom-module-locate-path cat mod (concat doom-module-init-file ".el")) + collect `(let ((doom--current-module '(,cat . ,mod)) + (doom--current-flags ',(doom-module-get cat mod :flags))) + (doom-load ,it))) + (doom-run-hooks 'doom-after-modules-init-hook) + (doom-run-hooks 'doom-before-modules-config-hook) + ,@(cl-loop for (cat . mod) in module-list + if (doom-module-locate-path cat mod (concat doom-module-config-file ".el")) + collect `(let ((doom--current-module '(,cat . ,mod)) + (doom--current-flags ',(doom-module-get cat mod :flags))) + (doom-load ,it))) + (doom-run-hooks 'doom-after-modules-config-hook) + (let ((old-custom-file custom-file)) + (doom-load ,(doom-path doom-user-dir doom-module-config-file) 'noerror) + (when (eq custom-file old-custom-file) + (doom-load custom-file 'noerror)))))) + +(defun doom-profile--generate-doom-autoloads () + (doom-autoloads--scan + (append (doom-glob doom-core-dir "lib/*.el") + (cl-loop for dir + in (append (doom-module-load-path doom-modules-dirs) + (list doom-user-dir)) + if (doom-glob dir "autoload.el") collect (car it) + if (doom-glob dir "autoload/*.el") append it) + (mapcan #'doom-glob doom-autoloads-files)) + nil)) + +(defun doom-profile--generate-package-autoloads () + (doom-autoloads--scan + (mapcar #'straight--autoloads-file + (seq-difference (hash-table-keys straight--build-cache) + doom-autoloads-excluded-packages)) + doom-autoloads-excluded-files + 'literal)) + +(provide 'doom-profiles) +;;; doom-profiles.el ends here diff --git a/lisp/doom-start.el b/lisp/doom-start.el index 1707f179c..443d07f87 100644 --- a/lisp/doom-start.el +++ b/lisp/doom-start.el @@ -246,28 +246,6 @@ If RETURN-P, return the message as a string instead of displaying it." ;; ;;; Let 'er rip! -;;; Load loaddefs -;; Doom caches a lot of information in `doom-autoloads-file'. Module and package -;; autoloads, autodefs like `set-company-backend!', and variables like -;; `doom-modules', `doom-disabled-packages', `load-path', `auto-mode-alist', and -;; `Info-directory-list'. etc. Compiling them into one place is a big reduction -;; in startup time. -(condition-case-unless-debug e - ;; Avoid `file-name-sans-extension' for premature optimization reasons. - ;; `string-remove-suffix' is cheaper because it performs no file sanity - ;; checks; just plain ol' string manipulation. - (load (string-remove-suffix ".el" doom-autoloads-file) nil 'nomessage) - (file-missing - ;; If the autoloads file fails to load then the user forgot to sync, or - ;; aborted a doom command midway! - (if (locate-file doom-autoloads-file load-path) - ;; Something inside the autoloads file is triggering this error; - ;; forward it to the caller! - (signal 'doom-autoload-error e) - (signal 'doom-error - (list "Doom is in an incomplete state" - "run 'doom sync' on the command line to repair it"))))) - ;;; Load envvar file ;; 'doom env' generates an envvar file. This is a snapshot of your shell ;; environment, which Doom loads here. This is helpful in scenarios where Emacs @@ -278,7 +256,23 @@ If RETURN-P, return the message as a string instead of displaying it." (setq-default process-environment (get 'process-environment 'initial-value)) (doom-load-envvars-file doom-env-file 'noerror)) -;; Bootstrap the interactive session +;;; Load core modules and set up their autoloads +(require 'doom-modules) +(autoload 'doom-initialize-packages "doom-packages") +;; TODO (autoload 'doom-profiles-initialize "doom-profiles") +;; TODO (autoload 'doom-packages-initialize "doom-packages") + +;; UX: There's a chance the user will later use package.el or straight in this +;; interactive session. If they do, make sure they're properly initialized +;; when they do. +(with-eval-after-load 'package (require 'doom-packages)) +(with-eval-after-load 'straight (doom-initialize-packages)) + +;; A last ditch opportunity to undo dodgy optimizations or do extra +;; configuration before the session is complicated by user config and packages. +(doom-run-hooks 'doom-before-init-hook) + +;;; Last minute setup (add-hook 'after-change-major-mode-hook #'doom-run-local-var-hooks-h 100) (add-hook 'hack-local-variables-hook #'doom-run-local-var-hooks-h) (add-hook 'doom-after-init-hook #'doom-load-packages-incrementally-h) @@ -287,21 +281,96 @@ If RETURN-P, return the message as a string instead of displaying it." (doom-run-hook-on 'doom-first-file-hook '(find-file-hook dired-initial-position-hook)) (doom-run-hook-on 'doom-first-input-hook '(pre-command-hook)) -;;; Setup autoloads for major core libraries -;; UX: There's a chance the user will later use package.el or straight in this -;; interactive session. If they do, make sure they're properly initialized -;; when they do. -(autoload 'doom-initialize-packages "doom-packages") -(eval-after-load 'package '(require 'doom-packages)) -(eval-after-load 'straight '(doom-initialize-packages)) -(require 'doom-modules) +;;; Load $DOOMDIR/init.el early +;; TODO: Catch errors +(doom-load (file-name-concat doom-user-dir doom-module-init-file) t) -;; A last ditch opportunity to undo dodgy optimizations or do extra -;; configuration before the session is complicated by user config and packages. -(doom-run-hooks 'doom-before-init-hook) +;;; Load the rest of $DOOMDIR + modules if noninteractive +;; If the user is loading this file from a batch script, let's assume they want +;; to load their userland config as well. +(when noninteractive + (doom-require 'doom-profiles) + (let ((init-file (doom-profile-init-file))) + (unless (file-exists-p init-file) + (user-error "Profile init file hasn't been generated. Did you forgot to run 'doom sync'?")) + (let (kill-emacs-query-functions + kill-emacs-hook) + ;; Loads modules, then $DOOMDIR/config.el + (doom-load init-file 'noerror) + (doom-initialize-packages)))) -;; Load user config + modules -(doom-initialize-modules) +;;; Entry point +;; HACK: This advice hijacks Emacs' initfile loader to accomplish the following: +;; +;; 1. Load the profile init file directory (generated on `doom sync`) +;; 2. Ignore initfiles we don't care about (like $EMACSDIR/init.el, ~/.emacs, +;; and ~/_emacs) -- and spare us the IO of searching for them, and allows +;; savvy hackers to use $EMACSDIR as their $DOOMDIR, if they wanted. +;; 3. Cut down on unnecessary logic in Emacs' bootstrapper. +;; 4. Offer a more user-friendly error state/screen, especially for errors +;; emitted from Doom's core or the user's config. +(define-advice startup--load-user-init-file (:override (file-fn _ _) init-doom 100) + (let ((debug-on-error-from-init-file nil) + (debug-on-error-should-be-set nil) + (debug-on-error-initial (if (eq init-file-debug t) 'startup init-file-debug))) + (let ((debug-on-error debug-on-error-initial)) + (condition-case-unless-debug error + (when init-file-user + (let ((init-file-name + ;; This dynamically generated init file stores a lot of + ;; precomputed information, such as module and package + ;; autoloads, and values for expensive variables like + ;; `doom-modules', `doom-disabled-packages', `load-path', + ;; `auto-mode-alist', and `Info-directory-list'. etc. + ;; Compiling them in one place is a big reduction in startup + ;; time, and by keeping a history of them, you get a snapshot + ;; of your config in time. + (file-name-concat doom-profile-dir (format "init.%d.elc" emacs-major-version)))) + ;; If `user-init-file' is t, then `load' will store the name of + ;; the file that it loads into `user-init-file'. + (setq user-init-file t) + (when init-file-name + (load init-file-name 'noerror 'nomessage nil 'must-suffix)) + ;; If we did not find the user's init file, set user-init-file + ;; conclusively. Don't let it be set from default.el. + (when (eq user-init-file t) + (signal 'doom-nosync-error (list init-file-name)))) + ;; If we loaded a compiled file, set `user-init-file' to the source + ;; version if that exists. + (setq user-init-file + (concat (string-remove-suffix (format ".%d.elc" emacs-major-version) + user-init-file) + ".el"))) + ;; TODO: Add safe-mode profile. + ;; (error + ;; ;; HACK: This is not really this variable's intended purpose, but it + ;; ;; doesn't mind what value its set to, only that its non-nil, so I'm + ;; ;; exploiting its dynamic scope to pass the error to the profile. + ;; (setq init-file-had-error error) + ;; (load (file-name-concat doom-emacs-dir "profiles" "safe-mode" "init.el") + ;; nil 'nomessage 'nosuffix)) + (error + (display-warning + 'initialization + (format-message "\ +An error occurred while loading `%s':\n\n%s%s%s\n\n\ +To ensure normal operation, you should investigate and remove the +cause of the error in your initialization file. Start Emacs with +the `--debug-init' option to view a complete error backtrace." + user-init-file + (get (car error) 'error-message) + (if (cdr error) ": " "") + (mapconcat (lambda (s) (prin1-to-string s t)) + (cdr error) ", ")) + :warning) + (setq init-file-had-error t))) + ;; If we can tell that the init file altered debug-on-error, arrange to + ;; preserve the value that it set up. + (or (eq debug-on-error debug-on-error-initial) + (setq debug-on-error-should-be-set t + debug-on-error-from-init-file debug-on-error))) + (when debug-on-error-should-be-set + (setq debug-on-error debug-on-error-from-init-file)))) (provide 'doom-start) ;;; doom-start.el ends here diff --git a/lisp/doom.el b/lisp/doom.el index 82e2fdd4c..c948e4e25 100644 --- a/lisp/doom.el +++ b/lisp/doom.el @@ -32,23 +32,36 @@ ;; ;; The overall load order of Doom is as follows: ;; -;; $EMACSDIR/early-init.el -;; $EMACSDIR/lisp/doom.el -;; $EMACSDIR/lisp/doom-start.el -;; `doom-before-init-hook' -;; $DOOMDIR/init.el -;; `doom-before-modules-init-hook' -;; {$DOOMDIR,~/.emacs.d}/modules/*/*/init.el -;; `doom-after-modules-init-hook' -;; `doom-before-modules-config-hook' -;; {$DOOMDIR,~/.emacs.d}/modules/*/*/config.el -;; `doom-after-modules-config-hook' -;; $DOOMDIR/config.el -;; `after-init-hook' -;; `emacs-startup-hook' -;; `doom-init-ui-hook' -;; `window-setup-hook' -;; `doom-after-init-hook' +;; > $EMACSDIR/early-init.el +;; > $EMACSDIR/lisp/doom.el +;; - $EMACSDIR/lisp/doom-lib.el +;; > $EMACSDIR/lisp/doom-start.el +;; - $EMACSDIR/doom-{keybinds,ui,projects,editor}.el +;; - hook: `doom-before-init-hook' +;; - $DOOMDIR/init.el +;; > $XDG_DATA_HOME/doom/$PROFILE/@/curr/init.el (replaces $EMACSDIR/init.el) +;; - hook: `doom-before-modules-init-hook' +;; - {$DOOMDIR,$EMACSDIR}/modules/*/*/init.el +;; - hook: `doom-after-modules-init-hook' +;; - hook: `doom-before-modules-config-hook' +;; - {$DOOMDIR,$EMACSDIR}/modules/*/*/config.el +;; - hook: `doom-after-modules-config-hook' +;; - $DOOMDIR/config.el +;; - `custom-file' or $DOOMDIR/custom.el +;; > The rest of `command-line' (Emacs startup) +;; - hook: `after-init-hook' +;; - hook: `emacs-startup-hook' +;; - hook: `window-setup-hook' +;; - hook: `doom-init-ui-hook' +;; - hook: `doom-after-init-hook' +;; > After startup is complete: +;; - On first input: `doom-first-input-hook' +;; - On first switched-to buffer: `doom-first-buffer-hook' +;; - On first opened file: `doom-first-file-hook' +;; +;; This is Doom's heart, where I define all its major constants and variables, +;; set only its sanest global defaults, employ its hackiest (and least +;; offensive) optimizations, and load the minimum for all Doom sessions. ;; ;;; Code: @@ -187,54 +200,11 @@ Defaults to ~/.config/doom, ~/.doom.d or the value of the DOOMDIR envvar; whichever is found first. Must end in a slash.") -(defconst doom-profiles-dir - (if-let (profilesdir (getenv-internal "DOOMPROFILESDIR")) - (expand-file-name "./" profilesdir) - (expand-file-name "profiles/" doom-emacs-dir)) - "Where Doom stores its profiles. - -Profiles are essentially snapshots of Doom Emacs environments. Every time you -update or sync, you create a new generation of a profile (which can be easily -rolled back or switched between with the DOOMPROFILE envvar). Must end in a -slash.") - -(defconst doom-profile-dir - (expand-file-name (concat (or doom-profile "default@latest") "/") - doom-profiles-dir) - "The path to the current, active profile. - -Must end in a slash.") - -(defconst doom-profile-data-dir - (expand-file-name "data/" doom-profile-dir) - "Where file storage/servers for the current, active profile is kept. - -Use this for long-living files that contain shared data that the user would -reasonably want to keep, and/or are required for Emacs to function correctly. -Must end in a slash.") - -(defconst doom-profile-cache-dir - (expand-file-name "cache/" doom-profile-dir) - "Where file caches for the current, active profile is kept. - -Use this for non-essential data files that, when deleted, won't cause breakage -or misbehavior, and can be restored. This includes server binaries or programs -downloaded/installed by packages. Must end in a slash.") - -(defconst doom-profile-init-file - (expand-file-name "init.el" doom-profile-dir) - "TODO") - - -;; -;;; DEPRECATED file/directory vars - -(defconst doom-local-dir +;; DEPRECATED: .local will be removed entirely in 3.0 +(defvar doom-local-dir (if-let (localdir (getenv-internal "DOOMLOCALDIR")) (expand-file-name (file-name-as-directory localdir)) - (if doom-profile - doom-profile-dir - (expand-file-name ".local/" doom-emacs-dir))) + (expand-file-name ".local/" doom-emacs-dir)) "Root directory for local storage. Use this as a storage location for this system's installation of Doom Emacs. @@ -243,32 +213,82 @@ These files should not be shared across systems. By default, it is used by `doom-data-dir' and `doom-cache-dir'. Must end with a slash.") (define-obsolete-variable-alias 'doom-etc-dir 'doom-data-dir "3.0.0") -(defconst doom-data-dir +(defvar doom-data-dir (if doom-profile - doom-profile-data-dir + (if IS-WINDOWS + (expand-file-name "doomemacs/data/" (getenv-internal "APPDATA")) + (expand-file-name "doom/" (or (getenv-internal "XDG_DATA_HOME") "~/.local/share"))) + ;; DEPRECATED: .local will be removed entirely in 3.0 (concat doom-local-dir "etc/")) - "Directory for non-volatile local storage. + "Where Doom stores its global data files. -Use this for files that don't change much, like server binaries, external -dependencies or long-term shared data. Must end with a slash.") +Data files contain shared and long-lived data that Doom, Emacs, and their +packages require to function correctly or at all. Deleting them by hand will +cause breakage, and require user intervention (e.g. a 'doom sync' or 'doom env') +to restore. -(defconst doom-cache-dir +Use this for: server binaries, package source, pulled module libraries, +generated files for profiles, profiles themselves, autoloads/loaddefs, etc. + +For profile-local data files, use `doom-profile-data-dir' instead.") + +(defvar doom-cache-dir (if doom-profile - doom-profile-cache-dir + (if IS-WINDOWS + (expand-file-name "doomemacs/cache/" (getenv-internal "APPDATA")) + (expand-file-name "doom/" (or (getenv-internal "XDG_CACHE_HOME") "~/.cache"))) + ;; DEPRECATED: .local will be removed entirely in 3.0 (concat doom-local-dir "cache/")) - "Directory for volatile local storage. + "Where Doom stores its global cache files. -Use this for files that change often, like cache files. Must end with a slash.") +Cache files represent non-essential data that shouldn't be problematic when +deleted (besides, perhaps, a one-time performance hit), lack portability (and so +shouldn't be copied to other systems/configs), and are regenerated when needed, +without user input (e.g. a 'doom sync'). -(defconst doom-autoloads-file +Some examples: images/data caches, elisp bytecode, natively compiled elisp, +session files, ELPA archives, authinfo files, org-persist, etc. + +For profile-local cache files, use `doom-profile-cache-dir' instead.") + +(defvar doom-state-dir (if doom-profile - doom-profile-init-file - (concat doom-local-dir "autoloads." emacs-version ".el")) - "Where `doom-reload-core-autoloads' stores its core autoloads. + (if IS-WINDOWS + (expand-file-name "doomemacs/state/" (getenv-internal "APPDATA")) + (expand-file-name "doom/" (or (getenv-internal "XDG_STATE_HOME") "~/.local/state"))) + ;; DEPRECATED: .local will be removed entirely in 3.0 + (concat doom-local-dir "state/")) + "Where Doom stores its global state files. -This file is responsible for informing Emacs where to find all of Doom's -autoloaded core functions (in lisp/lib/*.el).") +State files contain non-essential, unportable, but persistent data which, if +lost won't cause breakage, but may be inconvenient as they cannot be +automatically regenerated or restored. For example, a recently-opened file list +is not essential, but losing it means losing this record, and restoring it +requires revisiting all those files. +Use this for: history, logs, user-saved data, autosaves/backup files, known +projects, recent files, bookmarks. + +For profile-local state files, use `doom-profile-state-dir' instead.") + +;;; Profile file/directory variables +(defvar doom-profile-cache-dir + (file-name-concat doom-cache-dir (car doom-profile)) + "For profile-local cache files under `doom-cache-dir'.") + +(defvar doom-profile-data-dir + (file-name-concat doom-data-dir (car doom-profile)) + "For profile-local data files under `doom-data-dir'.") + +(defvar doom-profile-state-dir + (file-name-concat doom-state-dir (car doom-profile)) + "For profile-local state files under `doom-state-dir'.") + +(defconst doom-profile-dir + (file-name-concat doom-profile-data-dir "@" (cdr doom-profile)) + "Where generated files for the active profile are kept.") + +;; DEPRECATED: Will be moved to cli/env (defconst doom-env-file (file-name-concat (if doom-profile doom-profile-dir @@ -329,14 +349,6 @@ users).") ;; depending on font size. (setq frame-inhibit-implied-resize t) - ;; PERF: Emacs supports a "default init file", which is a library named - ;; "default.el" living anywhere in your `load-path' (or `$EMACSLOADPATH'). - ;; It's loaded after $EMACSDIR/init.el, but there really is no reason to - ;; do so. Doom doesn't define one, users shouldn't use one, and it seems - ;; too magical when an explicit `-l FILE' would do. I do away with it for - ;; the *miniscule* savings in file IO spent trying to load it. - (setq inhibit-default-init t) - ;; PERF,UX: Reduce *Message* noise at startup. An empty scratch buffer (or ;; the dashboard) is more than enough, and faster to display. (setq inhibit-startup-screen t diff --git a/profiles/README.org b/profiles/README.org index a179a7fce..124443332 100644 --- a/profiles/README.org +++ b/profiles/README.org @@ -1,17 +1,154 @@ #+title: Doom's profile directory -This directory houses Doom's profiles (both generated or static), which in turn -will contain all "local" data for that profile, including packages, caches, -server files, and so on. It's also where generated files (like autoloads) are -written to. +* Introduction +In order to power Doom's soon-to-be generational package manager, I wrote a +profile system. This system can effectively replace [[https://github.com/plexus/chemacs2][Chemacs]]; permitting you to +switch between multiple Emacs configs on-demand (and those configs don't have to +be Doom configs). -This directory may serve as an alternative to =$EMACSDIR/profiles.el= for -[[https://github.com/doomemacs/doomemacs/commit/5b6b204bcbcf69d541c49ca55a2d5c3604f04dad][declaring profiles]]: each directory here is an implicit profile, so assuming -=$EMACSDIR/profiles/foo/init.el= exists, then ~emacs --profile foo~ will be -equivalent to ~emacs --init-directory $EMACSDIR/profiles/foo~. +While I work on the formal documentation for this system, I've created this +brief guide to walk users through their use. *However, for this to work, Doom +must live in =~/.emacs.d= or =~/.config/emacs=.* I'll refer to this as +=$EMACSDIR= (and your private Doom config, in =~/.doom.d= or =~/.config/doom=, +as =$DOOMDIR=). -#+begin_quote -*Warning:* Generated (or static) profiles will follow the =X@Y= naming -convention. To avoid conflicts, avoid naming any profile you put in here the -same way. For example: =default@latest=, =test@942=, =safe-mode@static=,. -#+end_quote +* How use profiles +1. Declare all your profiles in either: + - One or multiple profile files at: + - =$DOOMDIR/profiles.el= + - =$EMACSDIR/profiles.el= + - =~/.config/doom-profiles.el= + - =~/.doom-profiles.el= + + [[id:f9bce7da-d155-4727-9b6f-b566b5b8d824][Example profiles.el file]]. + + - Or an implicit profile, which are inferred from the sub-directories of: + - =$DOOMDIR/profiles/= + - =$EMACSDIR/profiles/= + + Implicit profiles may have a =.doomprofile= file to apply additional + settings. [[id:ac37ac6f-6082-4c34-b98c-962bc1e528c9][Example .doomprofile]]. + +2. To run ~$ doom sync~ whenever you change the above, to regenerate Doom's + cached profile loader (generated at =$EMACSDIR/profiles/init.X.elc=, where X + is your major Emacs version). + + 3. To launch them: + - Launch the profile you want: ~$ emacs --profile FOO~ + - Use ~bin/doom~ on the profile you want: ~$ doom sync --profile FOO~ + +* Auto-generated profiles +Doom v3's sandbox and transactional package manager are capable of generating +profiles on-the-fly. The former for rapid, isolated testing, and the latter for +rollback/snapshot traversal for disaster recovery purposes. + +These auto-generated profiles will be stored and versioned in: +=$XDG_DATA_HOME/doom/$PROFILE_NAME/@/$PROFILE_VERSION/= + +* Fallback profile +Unlike Chemacs, Doom's profiles has no notion of a "default"/fallback profile. +The fallback profile is the Doom installation doing the bootloading. This +"global" profile is unique in that it won't respect a =.doomprofile= -- in other +words, it's not treated as a normal profile. + +It is this way so that the profiles system imposes no overhead on users that +aren't interested in the profile system (or prefer to use Chemacs). + +However, you can emulate this behavior by registering the "global" profile as a +profile, and setting ~$DOOMPROFILE~ or aliasing ~emacs~, like so: + +#+begin_src emacs-lisp +;; in a profiles.el file +((default) + + ...) +#+end_src + +#+begin_src bash +# in .zshrc or .bash_profile +export DOOMPROFILE=default + +# Or +alias emacs='emacs --profile default' +#+end_src + +* Gotchas +There are two caveats with this profile system: + +- It requires that Doom live in =~/.config/emacs= or =~/.emacs.d=. A + non-standard install location won't work, unless you use Emacs 29's new + =--init-directory DIR= switch and launch Emacs with ~emacs --init-directory + ~/nonstandard/emacs.d --profile NAME~. =bin/doom= is fine with it, though. + +- The profile system can be storage-inefficient. A barebones Doom config + averages at ~1mb without installed packages and ~3.75mb /with/ (straight alone + is 2.6m). A fully-fledged Doom config can average 500mb-1.4gb; the majority of + which are packages, but include server binaries, elisp+native bytecode, and + caches add up too. + + To mitigate this, Doom dedups packages across snapshots of a single profile + (e.g. =profile@23= -> =profile@24=), but it cannot (yet) do this across + profiles (e.g. if =profile1= and =profile2= both install =org=). Even then, + packages whose recipes change (either locally or upstream) may dodge this + deduplication and get cloned anew (to ensure historical integrity) -- though + this shouldn't happen often, but can build up over time. + + So v3 will introduce a ~doom gc~ command, which offers a couple nix.gc-esque + switches to control it. E.g. + + - Acts on the "global" profile: + - ~doom gc --older-than 21d~ + - ~doom gc --keep 10~ + - Act on a specific profile: + - ~doom gc --profile foo ...~ + - Act on all known profiles + - ~doom gc --profiles '*' ...~ + + Users can change defaults from their =init.el= or =cli.el=, or configure ~doom + sync~ to auto-GC by whatever rules they like. And the good doctor will warn + you if you haven't GCed in a while, or you're in excess of some threshold + (which I haven't decided yet). + +* How to switch from Chemacs +1. Delete [[https://github.com/plexus/chemacs2][Chemacs]] from =$EMACSDIR=. + +2. Install Doom there: ~$ git clone https://github.com/doomemacs/doomemacs + ~/.config/emacs~ + +3. Move =~/.emacs-profiles.el= to =~/.config/doom/profiles.el= and transform the + string keys to symbols and adapt =env= entries like so: + + #+begin_src emacs-lisp + ;; ~/.emacs-profiles.el + (("default" (user-emacs-directory . "~/.emacs.default") + (env ("DOOMDIR" . "~/.doom.private"))) + ("spacemacs" (user-emacs-directory . "~/spacemacs")) + ("prelude" (user-emacs-directory . "~/prelude"))) + + ;; ~/.config/emacs/profiles.el + ((default (user-emacs-directory . "~/.emacs.default") + ("DOOMDIR" . "~/.doom.private")) + (spacemacs (user-emacs-directory . "~/spacemacs")) + (prelude (user-emacs-directory . "~/prelude"))) + #+end_src + + A comprehensive example of Doom's profiles.el file can be found + [[id:f9bce7da-d155-4727-9b6f-b566b5b8d824][in docs/examples.org]]. + + *Differences with Chemacs profiles:* + - Keys are symbols, not strings. + - Doom's profiles.el has a syntax for evaluating code, expanding paths, and + appending/prepending to variables (with deferral). See the examples.org + link above. + - Doom's profile system won't install [[https://github.com/raxod502/straight.el][Straight.el]] for you. + - Doom does not have a special "default" profile. If you don't specify a + --profile, it will simply start up the Doom config living in + =~/.config/emacs=. See the "Fallback profile" section below for a + workaround. + +4. Then launch a profile. E.g. ~$ emacs --profile prelude~. + +* But Doom is kinda heavy to be a bootloader... +I agree! To remedy that, I'll soon split Doom up into three projects: its core +(where its bootloader lives), its official modules, and its community +contributed modules. At that point, Doom will be much lighter! diff --git a/profiles/safe-mode@static/init.el b/profiles/safe-mode/init.el similarity index 100% rename from profiles/safe-mode@static/init.el rename to profiles/safe-mode/init.el