doom-emacs/bin/doom
Henrik Lissner 5bfe0ba53f
fix(cli): load $DOOMRC relative to $PWD
Rather than doing so relative to bin/doom.
2022-09-26 12:31:27 +02:00

327 lines
14 KiB
Bash
Executable file

#!/usr/bin/env sh
:; # -*- mode: emacs-lisp; lexical-binding: t -*-
:; case "$EMACS" in *term*) EMACS=emacs ;; *) EMACS="${EMACS:-emacs}" ;; esac
:; emacs="$EMACS ${DEBUG:+--debug-init} -q --no-site-file --batch"
:; tmpdir=`$emacs --eval '(princ (temporary-file-directory))' 2>/dev/null`
:; [ -z "$tmpdir" ] && { >&2 echo "Error: failed to run Emacs with command '$EMACS'"; >&2 echo; >&2 echo "Are you sure Emacs is installed and in your \$PATH?"; exit 1; }
:; export __DOOMPID="${__DOOMPID:-$$}"
:; export __DOOMSTEP="$((__DOOMSTEP+1))"
:; export __DOOMGEOM="${__DOOMGEOM:-`tput cols 2>/dev/null`x`tput lines 2>/dev/null`}"
:; export __DOOMGPIPE=${__DOOMGPIPE:-$__DOOMPIPE}
:; export __DOOMPIPE=; [ -t 0 ] || __DOOMPIPE="${__DOOMPIPE}0"; [ -t 1 ] || __DOOMPIPE="${__DOOMPIPE}1"
:; $emacs --load "$0" -- "$@" || exit=$?
:; [ "${exit:-0}" -eq 254 ] && { sh "${tmpdir}/doom.${__DOOMPID}.${__DOOMSTEP}.sh" "$0" "$@" && true; exit="$?"; }
:; exit $exit
;; This magical mess of a shebang is necessary for any script that relies on
;; Doom's CLI framework, because Emacs' tty libraries and capabilities are too
;; immature (borderline non-existent) at the time of writing (28.1). This
;; shebang sets out to accomplish these three goals:
;;
;; 1. To produce a more helpful error if Emacs isn't installed or broken. It
;; must do so without assuming whether $EMACS is a shell command (e.g. 'snap
;; run emacs') or an absolute path (to an emacs executable). I've avoided
;; 'command -v $EMACS' for this reason.
;;
;; 2. To allow this Emacs session to "exit into" a child process (since Elisp
;; lacks an analogue for exec system calls) by calling an auto-generated and
;; self-destructing "exit script" if the parent Emacs process exits with code
;; 254. It takes care to prevent nested child instances from clobbering the
;; exit script.
;;
;; 3. To expose some information about the terminal and session:
;; - $__DOOMGEOM holds the dimensions of the terminal (W . H).
;; - $__DOOMPIPE indicates whether the script has been piped (in and/or out).
;; - $__DOOMGPIPE indicates whether one of this process' parent has been
;; piped to/from.
;; - $__DOOMPID is a unique identifier for the parent script, so
;; child processes can identify which persistent data files (like logs) it
;; has access to.
;; - $__DOOMSTEP counts how many levels deep we are in the dream (appending
;; this to the exit script's filename avoids child processes clobbering the
;; same exit script and causing read errors).
;; - $TMPDIR (or $TEMP and $TMP on Windows) aren't guaranteed to have values,
;; and mktemp isn't available on all systems, but you know what is? Emacs!
;; So I use it to print `temporary-file-directory'. And it seconds as a
;; quick sanity check for Emacs' existence (for goal #1).
;;
;; Other weird facts about this shebang line:
;;
;; - The :; hack exploits properties of : and ; in shell scripting and elisp to
;; allow shell script and elisp to coexist in the same file without either's
;; interpreter throwing foreign syntax errors:
;;
;; - In elisp, ":" is a valid keyword symbol literal; it evaluates to itself
;; and has no side-effect.
;; - In the shell, ":" is a valid command that does nothing and ignores its
;; arguments.
;; - In elisp, ";" begins a comment. I.e. the interpreter ignores everything
;; after it.
;; - In the shell, ";" is a command separator.
;;
;; Put together, plus a strategically placed exit call, the shell will read
;; one part of this file and ignore the rest, while the elisp interpreter will
;; do the opposite.
;; - I intentionally avoid loading site files, so lisp/doom-cli.el can load them
;; by hand later. There, I can suppress and deal with unhelpful warnings (e.g.
;; "package cl is deprecated"), "Loading X...DONE" spam, and any other
;; disasterous side-effects.
;;
;; But be careful not to use -Q! It implies --no-site-lisp, which omits the
;; site-lisp directory from `load-path'.
;; - POSIX-compliancy is paramount: there's no guarantee what /bin/sh will be
;; symlinked to in the esoteric OSes/distros Emacs users use.
;; - The user may have a noexec flag set on /tmp, so pass the exit script to
;; /bin/sh rather than executing them directly.
;; In CLI sessions, prefer correctness over performance.
(setq load-prefer-newer t)
;; Doom's core sets up everything we need; including `doom-*-dir' variables,
;; universal defaults, and autoloads for doom-*-initialize functions.
(condition-case e
(let* ((bin-dir (file-name-directory (file-truename load-file-name)))
(init-file (expand-file-name "../early-init.el" bin-dir)))
(or (and (load init-file nil 'nomessage 'nosuffix)
(featurep 'doom))
(user-error "Failed to load Doom from %s" init-file)))
;; Prevent ugly backtraces for trivial errors
(user-error (message "Error: %s" (cadr e))
(kill-emacs 2)))
;; UX: Abort if the user is using 'doom' as root, unless ~/.emacs.d is owned by
;; root, in which case we assume the user genuinely wants root to be their
;; primary user account for Emacs.
(when (equal 0 (user-real-uid))
(unless (equal 0 (file-attribute-user-id (file-attributes doom-emacs-dir)))
(message
(concat
"Error: this script was executed as root, which is likely not what you want.\n"
"It will cause file permissions errors later, when you run Doom as another\n"
"user.\n\n"
"If this really *is* what you want, then change the owner of your Emacs\n"
"config to root:\n\n"
;; TODO Add cmd.exe/powershell commands
" chown root:root -R " (abbreviate-file-name doom-emacs-dir) "\n\n"
"Aborting..."))
(kill-emacs 2)))
;;
;;; Entry point
(defcli! doom (&args _command)
"A command line interface to Doom Emacs.
Includes package management, diagnostics, unit tests, and byte-compilation.
This tool also makes it trivial to launch Emacs out of a different folder or
with a different private module.
ENVIRONMENT VARIABLES:
`$EMACS'
The Emacs executable or command to use for any Emacs operations in this or
other Doom CLI shell scripts (default: first emacs found in `$PATH').
`$EMACSDIR'
The location of your Doom Emacs installation (defaults to ~/.config/emacs or
~/.emacs.d; whichever is found first). This is *not* your private Doom
configuration. The `--emacsdir' option also sets this variable.
`$DOOMDIR'
The location of your private configuration for Doom Emacs (defaults to
~/.config/doom or ~/.doom.d; whichever it finds first). This is *not* the
place you've cloned doomemacs/doomemacs to. The `--doomdir' option also sets
this variable.
`$DOOMPAGER'
The pager to invoke for large output (default: \"less +g\"). The `--pager'
option also sets this variable.
`$DOOMPROFILE'
Which Doom profile to activate (default: \"_@0\"). The `--profile' option
also sets this variable.
`$DOOMPROFILELOADFILE'
Doom generates a profile loader script on 'doom sync' or 'doom profiles
sync'. By default, this file is written to and loaded from
$EMACSDIR/profiles/load.el. Set this envvar to change that. Note that this
envvar must be in scope for both interactive and non-interactive sessions
for it to be effective. This is especially useful for folks on Nix/Guix, who
have deployed Doom to a read-only directory.
Note: this file *must* end with a .el extension. It will be byte-compiled
after it is generated.
`$DOOMPROFILELOADPATH'
A colon-delimited (semi-colon on Windows) list of profile config files or
directories under which Doom will search for implicit profiles. See
`var:doom-profile-load-path' for its default value.
EXIT CODES:
0 Successful run
1 General internal error
2 Error with Emacs/Doom install or execution context
3 Unrecognized user input error
4 Command not found, or is incorrect/deprecated
5 Invalid, missing, or extra options/arguments
6-49 Reserved for Doom
50-200 Reserved for custom user codes
254 Successful run (but then execute `doom-cli-restart-script')
255 Uncaught internal errors
SEE ALSO:
https://doomemacs.org Homepage
https://docs.doomemacs.org Official documentation
https://discourse.doomemacs.org Discourse (discussion & support forum)
https://doomemacs.org/discord Discord chat server
https://doomemacs.org/roadmap Development roadmap
https://git.doomemacs.org Shortcut to Github org
https://git.doomemacs.org/todo Global issue tracker"
:partial t)
(defcli! :before
((force? ("-!" "--force") "Suppress prompts by auto-accepting their consequences")
(debug? ("-D" "--debug") "Enable debug output")
(verbose? ("-v" "--verbose") "Enable verbose output")
(doomdir ("--doomdir" dir) "Use Doom config living in `DIR' (e.g. ~/.doom.d)")
(emacsdir ("--emacsdir" dir) "Use Doom install living in `DIR' (e.g. ~/.emacs.d)")
(pager ("--pager" cmd) "Pager command to use for large output")
(profile ("--profile" name) "Use profile named NAME")
(bench? ("--benchmark") "Always display the benchmark")
&flags
(color? ("--color") "Whether or not to show ANSI color codes")
&multiple
(loads ("-L" "--load" "--strict-load" file) "Load elisp `FILE' before executing `COMMAND'")
(evals ("-E" "--eval" form) "Evaluate `FORM' before executing commands")
&input input
&context context
&args _)
"OPTIONS:
-E, -eval
Can be used multiple times.
-L, --load, --strict-load
Can be used multiple times to load multiple files. Both -L and --load will
silently fail on missing files, but --strict-load won't.
Warning: files loaded this way load too late to define new commands. To
define commands, do so from `$DOOMDIR'/cli.el, `$DOOMDIR'/init.el, or a
.doomrc file in the current project tree."
(when bench?
(setq doom-cli-benchmark-threshold 'always))
(unless init-file-debug ; debug-mode implies verbose
(when verbose?
(setq doom-print-minimum-level 'info)))
(when color?
(setq doom-print-backend (if (eq color? :yes) 'ansi)))
(when pager
(setq doom-cli-pager pager))
(when force?
(setf (doom-cli-context-suppress-prompts-p context) t))
;; For these settings to take full effect, the script must be restarted:
(when (or debug?
profile
emacsdir
doomdir)
(let (omit)
(when debug?
(setenv "DEBUG" "1")
(setq init-file-debug t)
(push "--debug" omit))
(when profile
(setenv "DOOMPROFILE" profile)
(push "--profile=" omit))
(when emacsdir
(setenv "EMACSDIR" emacsdir)
(push "--emacsdir=" omit))
(when doomdir
(setenv "DOOMDIR" doomdir)
(push "--doomdir=" omit))
(exit! :restart :omit omit)))
;; Load extra files and forms, as per given options.
(dolist (file loads)
(load (doom-path (cdr file))
(not (equal (car file) "--strict-load"))
(not init-file-debug) t))
(dolist (form evals)
(eval (read (cdr form)) t)))
;;
;;; Load user config + commands
;; Load $DOOMDIR/init.el, to read the user's `doom!' block and give users an
;; opportunity to customize the CLI environment, if they like. Otherwise, they
;; can do so in .doomrc or .doomproject.
(load! doom-module-init-file doom-user-dir t)
;; There are a lot of CLIs, and some have expensive initialization, so best we
;; load them lazily.
(defcli-group!
:prefix 'doom
;; Import this for implicit 'X help' commands for your script:
(defcli-alias! ((help h)) (:root :help))
;; And suggest its use when errors occur.
(add-to-list 'doom-help-commands "%p h[elp] %c")
(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")
(defcli-autoload! ((install i)))
(defcli-autoload! ((compile c)))
(defcli-autoload! (clean) "compile")
;; TODO Post-3.0 commands
;; (load! "gc" dir)
;; (load! "module" dir)
;; (load! "nuke" dir)
;; (load! "package" dir)
;; (load! "profile" dir)
;; (defcli-obsolete! ((compile c)) (sync "--compile") "v3.0.0")
;; (defcli-obsolete! ((build b)) (sync "--rebuild") "v3.0.0")
)
(defcli-group! "Diagnostics"
:docs "Commands for troubleshooting and debugging Doom."
(defcli-autoload! ((doctor doc)))
(defcli-autoload! (info))
(defcli-alias! ((version v)) (:root :version)))
(defcli-group! "Development"
:docs "Commands for developing or launching Doom."
(defcli-autoload! (ci))
(defcli-autoload! (make))
(defcli-autoload! (run))
;; FIXME Test framework
;; (load! "test" dir)
)
(let ((cli-file "cli.el"))
(defcli-group! "Module commands"
(doom-context-with 'modules
(dolist (key (doom-module-list))
(when-let (path (doom-module-locate-path (car key) (cdr key) cli-file))
(defcli-group! :prefix (if (cdr key) (format "+%s" (cdr key)))
(doom-load (file-name-sans-extension path))))))))
;; Allow per-project Doom settings in .doom files.
(let (doomrc)
(cond
((setq doomrc (getenv "DOOMRC"))
(load! doomrc default-directory))
((setq doomrc (locate-dominating-file default-directory ".doomrc"))
(load! ".doomrc" doomrc)))))
;;
;;; Let 'er rip
(run! "doom" (cdr (member "--" argv)))
;;; doom ends here, unless...