/ 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)