/ ensure.el
ensure.el
1 ;;; ensure.el --- Backpack synchronization mode -*- lexical-binding: t; -*- 2 ;; 3 ;; This file implements the `backpack ensure' command which: 4 ;; 1. Installs elpaca from base-packages (no internet required for elpaca itself) 5 ;; 2. Installs all missing packages needed by enabled gears 6 ;; 3. Builds and byte-compiles packages 7 ;; 4. Does NOT activate packages (that happens in normal mode) 8 ;; 9 ;; Usage: emacs --batch --eval "(setq user-emacs-directory \"/path/to/emacs-backpack/\")" -l ensure.el 10 11 ;; Set backpack to sync mode BEFORE loading backpack.el 12 ;; This variable is checked by backpack.el during initialization 13 (setq backpack-mode 'sync) 14 15 ;; Set up load paths for base-packages before loading backpack.el 16 ;; This is needed because backpack.el requires leaf at load time 17 (add-to-list 'load-path (expand-file-name "base-packages/leaf.el" user-emacs-directory)) 18 (add-to-list 'load-path (expand-file-name "base-packages/leaf-keywords.el" user-emacs-directory)) 19 (add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory)) 20 21 ;; Load backpack.el which sets up all the infrastructure 22 ;; This will also install/build elpaca from base-packages if needed 23 (let ((backpack-file (expand-file-name "lisp/backpack.el" user-emacs-directory))) 24 (load backpack-file nil nil nil t)) 25 26 ;; At this point, elpaca should be loaded from backpack--ensure-elpaca 27 ;; Configure elpaca build steps for sync mode (build everything except activation) 28 (setq elpaca-build-steps backpack--sync-build-steps) 29 30 ;; Load user configuration to get gear declarations 31 (let ((init-file (expand-file-name "init.el" backpack-user-dir))) 32 (when (file-exists-p init-file) 33 (load init-file t))) 34 35 ;; Load all gears (which queues packages via :ensure) 36 (backpack-load-gear-files) 37 38 ;;; Progress reporting during package installation 39 40 (defvar backpack--last-progress-message nil 41 "Last progress message printed, to avoid duplicates.") 42 43 (defun backpack--get-package-status-summary () 44 "Get a summary of package statuses from elpaca queues." 45 (let ((finished 0) 46 (failed 0) 47 (in-progress 0) 48 (blocked 0) 49 (total 0) 50 (current-packages nil) 51 (failed-packages nil)) 52 (dolist (q elpaca--queues) 53 (dolist (entry (elpaca-q<-elpacas q)) 54 (let* ((e (cdr entry)) 55 (status (elpaca--status e)) 56 (pkg-name (elpaca<-package e))) 57 (cl-incf total) 58 (cond 59 ((eq status 'finished) (cl-incf finished)) 60 ((eq status 'failed) 61 (cl-incf failed) 62 (push pkg-name failed-packages)) 63 ((memq status '(blocked queued)) (cl-incf blocked)) 64 (t 65 (cl-incf in-progress) 66 (push (cons pkg-name status) current-packages)))))) 67 (list :finished finished 68 :failed failed 69 :in-progress in-progress 70 :blocked blocked 71 :total total 72 :current current-packages 73 :failed-packages (nreverse failed-packages)))) 74 75 (defun backpack--format-progress (summary) 76 "Format SUMMARY into a progress string." 77 (let* ((finished (plist-get summary :finished)) 78 (failed (plist-get summary :failed)) 79 (total (plist-get summary :total)) 80 (current (plist-get summary :current)) 81 (current-str (if current 82 (mapconcat (lambda (p) 83 (format "%s[%s]" 84 (car p) 85 (symbol-name (cdr p)))) 86 (seq-take current 3) ", ") 87 ""))) 88 (format "[%d/%d] %s%s" 89 (+ finished failed) 90 total 91 (if (> failed 0) (format "(failed: %d) " failed) "") 92 current-str))) 93 94 (defun backpack--print-progress () 95 "Print current elpaca progress." 96 (let* ((summary (backpack--get-package-status-summary)) 97 (msg (backpack--format-progress summary))) 98 (unless (equal msg backpack--last-progress-message) 99 (setq backpack--last-progress-message msg) 100 (message "Installing packages... %s" msg)))) 101 102 (defun backpack--queue-finished-p (q) 103 "Return non-nil if queue Q is finished (complete or all packages processed)." 104 (or (eq (elpaca-q<-status q) 'complete) 105 ;; Also consider finished if all packages are either finished or failed 106 (let ((all-done t)) 107 (dolist (entry (elpaca-q<-elpacas q)) 108 (let ((status (elpaca--status (cdr entry)))) 109 (unless (memq status '(finished failed)) 110 (setq all-done nil)))) 111 all-done))) 112 113 (defun backpack--wait-with-progress () 114 "Wait for elpaca to finish while showing progress." 115 (message "Installing packages...") 116 (when-let* ((q (cl-loop for q in elpaca--queues thereis 117 (and (eq (elpaca-q<-status q) 'incomplete) 118 (elpaca-q<-elpacas q) q)))) 119 (setq elpaca--waiting t) 120 (elpaca-process-queues) 121 (let ((last-print-time 0) 122 (stall-count 0) 123 (last-summary nil)) 124 (condition-case nil 125 (while (not (backpack--queue-finished-p q)) 126 (discard-input) 127 ;; Print progress every 0.5 seconds 128 (let* ((now (float-time)) 129 (summary (backpack--get-package-status-summary))) 130 (when (> (- now last-print-time) 0.5) 131 (setq last-print-time now) 132 (backpack--print-progress) 133 ;; Check for stall (no progress for too long) 134 (if (equal summary last-summary) 135 (cl-incf stall-count) 136 (setq stall-count 0) 137 (setq last-summary summary)) 138 ;; If stalled for 60 iterations (~30 seconds) and we have failures, break 139 (when (and (> stall-count 60) 140 (> (plist-get summary :failed) 0) 141 (= (plist-get summary :in-progress) 0)) 142 (message "Installation stalled with failures, continuing...") 143 (cl-return)))) 144 (sit-for elpaca-wait-interval)) 145 (quit (cl-loop for (_ . e) in (elpaca-q<-elpacas q) do 146 (or (eq (elpaca--status e) 'finished) (elpaca--fail e "User quit")))))) 147 (elpaca-split-queue) 148 (setq elpaca--waiting nil)) 149 ;; Final progress 150 (let* ((summary (backpack--get-package-status-summary)) 151 (failed-pkgs (plist-get summary :failed-packages))) 152 (message "") 153 (message "Installing packages... [%d/%d] Done!" 154 (plist-get summary :finished) 155 (plist-get summary :total)) 156 (when failed-pkgs 157 (message "") 158 (message "WARNING: %d package(s) failed to install:" (length failed-pkgs)) 159 (dolist (pkg failed-pkgs) 160 (message " - %s" pkg)) 161 (message "") 162 (message "You may need to run 'backpack ensure' again or check the package recipes.")))) 163 164 ;; Wait for all packages to be installed/built with progress reporting 165 (backpack--wait-with-progress) 166 167 ;; After packages are built, activate packages marked with enable-on-sync 168 ;; This must happen before their config forms run and before treesit grammars are installed 169 (when backpack--enable-on-sync-packages 170 (message "") 171 (message "Activating packages needed for sync...") 172 (backpack--activate-enable-on-sync-packages)) 173 174 ;; After elpaca-wait completes, run our finalization 175 ;; (elpaca-after-init-hook doesn't run when using elpaca-wait in batch mode) 176 (when backpack--treesit-langs 177 (message "") 178 (message "Installing tree-sitter grammars...") 179 (backpack--install-treesit-grammars)) 180 181 (message "") 182 (message "========================================") 183 (message "Backpack synchronization complete!") 184 (message "You can now start Emacs normally.") 185 (message "========================================") 186 (kill-emacs 0)