diff --git a/ChangeLog b/ChangeLog index 44810b1d..c7d3c475 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,41 @@ +2026-07-18 Mats Lidell + +* test/hy-test-dependencies.el (hypb:package-install-advice-for-retry): + Advice for retrying package-install. + +* test/hy-test-helpers.el (hy-test-mocked-feature): Macro for mocking + featurep. + +* test/hypb-tests.el (hypb--users-package-manager): + (hypb--package-el-install): + (hypb--notify-manual-install-needed): + (hypb--ensure-dependency): + (hypb--require-package): Unit tests for the new functions. + +* test/hy-test-dependencies.el (hypb:ask-to-install-package-flag): Set to + nil for not asking about installing dynamic dependencies during test, + i.e. markdown-mode. + +* hsys-consult.el (hsys-consult-require-version): + (hsys-consult--org-roam-call-function): +* hui-treemacs.el (treemacs): +* hypb.el (hypb:activate-interaction-log-mode): Use hypb:require-package. + +* hyrolo.el (hyrolo-any-file-type-problem-p): Use require to check if + markdown-mode is available. Use users package-archives and fail + markdown-mode install if nongnu is not present. + (hyrolo-markdown-mode): Remove not needed require. + (hyrolo-install-markdown-mode): Use hypb:require-package. + +* hypb.el (hypb:ask-to-install-package-flag): Give user control if package + shall be installed when using package.el. + (hypb:users-package-manager): Users package manager. + (hypb:package-el-install): Install package using package.el. + (hypb:notify-manual-install-needed): Notify user manual install is needed. + (hypb:ensure-dependency): Ensure package is available using + hypb:package-el-install or hypb:notify-manual-install-needed. + (hypb:require-package): Use hypb:ensure-dependency. + 2026-07-16 Mats Lidell * Makefile (docker-clean): Fix remove command for docker volumes. diff --git a/hsys-consult.el b/hsys-consult.el index 0343a20c..d4a33d07 100644 --- a/hsys-consult.el +++ b/hsys-consult.el @@ -2,7 +2,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 4-Jul-24 at 09:57:18 -;; Last-Mod: 6-May-26 at 08:17:25 by Bob Weiner +;; Last-Mod: 12-Jul-26 at 00:03:44 by Mats Lidell ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -116,9 +116,7 @@ Requires use of `vertico' for completions." Install `consult' package if not yet installed." (unless hsys-consult-flag (error "`%s' command requires `hsys-consult-flag' set to t" this-command)) - (unless (package-installed-p 'consult) - (package-install 'consult)) - (require 'consult) + (hypb:require-package 'consult) (let ((consult-version (hsys-consult-get-version))) ;; Multi-file support added after consult version "0.32" (when (not (and consult-version (string-greaterp consult-version "0.32"))) @@ -403,9 +401,7 @@ tag around point." "Install Org Roam if necessary and then call an Org Roam FUNC." (unless hsys-consult-flag (error "`%s' command requires `hsys-consult-flag' set to t" this-command)) - (unless (package-installed-p 'org-roam) - (package-install 'org-roam)) - (require 'org-roam) + (hypb:require-package 'org-roam) (unless (file-readable-p org-roam-directory) (make-directory org-roam-directory)) (unless org-roam-db-autosync-mode diff --git a/hui-treemacs.el b/hui-treemacs.el index b17f8b15..5ef6015e 100644 --- a/hui-treemacs.el +++ b/hui-treemacs.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 19-Nov-17 -;; Last-Mod: 7-Mar-26 at 22:39:17 by Bob Weiner +;; Last-Mod: 11-Jul-26 at 23:58:02 by Mats Lidell ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -24,10 +24,7 @@ (require 'package) ;; Always keep this first (require 'seq) -(or (require 'treemacs nil t) - (and (package-installed-p 'treemacs) - (package-activate 'treemacs)) - (hypb:require-package 'treemacs)) +(hypb:require-package 'treemacs) (defvar treemacs-version) diff --git a/hypb.el b/hypb.el index 2c34d3f8..2af9a9fe 100644 --- a/hypb.el +++ b/hypb.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 6-Oct-91 at 03:42:38 -;; Last-Mod: 10-Jul-26 at 17:16:28 by Bob Weiner +;; Last-Mod: 16-Jul-26 at 17:10:28 by Mats Lidell ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -93,6 +93,11 @@ delimiter." :type 'sexp :group 'hyperbole-commands) +(defcustom hypb:ask-to-install-package-flag t + "Non-nil means `hypb:require-package' queries the user before installing it." + :type 'boolean + :group 'hyperbole-commands) + (defvar hypb:mail-address-mode-list '(fundamental-mode prog-mode text-mode) "List of major modes in which mail address implicit buttons are active. @@ -181,8 +186,7 @@ Uses the newer \"nadvice\" elisp library, not \"advice\"." This displays a clean log of Emacs keys used and commands executed." (interactive) ;; Ensure package is installed - (unless (package-installed-p 'interaction-log) - (package-install 'interaction-log)) + (hypb:require-package 'interaction-log) ;; Ensure interaction-log-mode is disabled to removes its command ;; hooks which are replaced below. @@ -1169,23 +1173,46 @@ WINDOW pixelwise." (string-join unreadable-dirs "\n")))) dirs) +(defun hypb:users-package-manager () + "Return the package manager in use. +Current supported package managers are `straight', `elpaca', and `package'." + (cond ((featurep 'straight) 'straight) + ((featurep 'elpaca) 'elpaca) + (t 'package))) + +(defun hypb:package-el-install (package) + "Install PACKAGE using the default package manager `package.el'. +If `hypb:ask-to-install-package-flag' is non-nil query user if package should +be installed." + (when (or (not hypb:ask-to-install-package-flag) + (y-or-n-p (format "Install `%s' to enable this feature? " package))) + (package-install package) + (require package))) + +(defun hypb:notify-manual-install-needed (package manager) + "Prompt with an error that PACKAGE must be manually installed using MANAGER." + (user-error "Package '%s' is required by this command. Use your package manager '%s' to install it" package manager)) + +(defun hypb:ensure-dependency (package) + "Return non-nil if PACKAGE is installed and ready for use. +If PACKAGE is not installed and the package.el package manager is in +use, an install attempt may be made. When any other package manager is +in use, an error is triggered prompting to manually install the PACKAGE." + (or (require package nil t) + (pcase (hypb:users-package-manager) + ('package (hypb:package-el-install package)) + (manager (hypb:notify-manual-install-needed package manager))))) + ;;;###autoload (defun hypb:require-package (package) - "Prompt user to install, if necessary, and require the Emacs PACKAGE-NAME. -PACKAGE-NAME may be a symbol or a string." + "Prompt user to install, if necessary, and require the Emacs PACKAGE. +PACKAGE may be a symbol or a string." (when (stringp package) (setq package (intern package))) (unless (symbolp package) (error "(hypb:require-package): package must be a symbol or string, not '%s'" package)) - (unless (or - ;; Allow for alternative package managers like elpaca that don't - ;; show up with a `package-installed-p' check - (require package nil t) - (package-installed-p package)) - (if (y-or-n-p (format "Install package `%s' required by this command?" package)) - (package-install package) - (keyboard-quit))) - (require package)) + (unless (hypb:ensure-dependency package) + (error "(hypb:require-package): package '%s' could not be found" package))) ;; Adapted from cl--do-remf in "cl-extra.el" but uses 'equal' for comparisons. ;;;###autoload diff --git a/hyrolo.el b/hyrolo.el index 3cba3864..20fb25b6 100644 --- a/hyrolo.el +++ b/hyrolo.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 7-Jun-89 at 22:08:29 -;; Last-Mod: 13-Jun-26 at 13:33:38 by Bob Weiner +;; Last-Mod: 16-Jul-26 at 17:40:26 by Mats Lidell ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -38,8 +38,6 @@ (require 'hsys-consult) (require 'hsys-org) ;; For `hsys-org-cycle-bob-file-list' (require 'hypb) ;; For `hypb:mail-address-regexp' and `hypb:add-to-invisibility-spec' -(eval-when-compile - `(hyrolo-install-markdown-mode)) (require 'outline) (require 'reveal) ;; Avoid any potential library name conflict by giving the load directory. @@ -1074,7 +1072,6 @@ or NAME is invalid, return nil." (define-derived-mode hyrolo-markdown-mode text-mode "Markdown" "Major mode for editing Markdown files." (hyrolo-install-markdown-mode) - (require 'markdown-mode) ;; Don't actually derive from `markdown-mode' to avoid its costly setup ;; but set its parent mode property to `markdown-mode' so `derived-mode-p' checks @@ -3091,19 +3088,17 @@ package is not installed." (hyrolo-get-file-list)))) file-and-major-mode-list files-no-mode-list - files-invalid-suffix-list - package-archives) + files-invalid-suffix-list) ;; 2. Skip this if the markdown-mode package is installed - (unless (package-installed-p 'markdown-mode) + (unless (require 'markdown-mode nil t) ;; 3. If any `hyrolo-file-list' file has a markdown file suffix, (when (delq nil (mapcar (lambda (suffix) (string-match-p (concat "\\(?:" hyrolo-markdown-suffix-regexp "\\)$") suffix)) file-suffixes)) - ;; 4. if not, ensure nongnu is temporarily added to package - ;; source list and then install markdown-mode. + ;; 4. then install markdown-mode. (hyrolo-install-markdown-mode))) ;; 5. Check that each file has an entry in `hyrolo-auto-mode-alist' or `auto-mode-alist', @@ -3231,13 +3226,7 @@ files to search." (defun hyrolo-install-markdown-mode () "Install `markdown-mode' package unless already installed." - (unless (package-installed-p 'markdown-mode) - (unless (assoc "nongnu" package-archives) - (setq package-archives (cl-copy-list package-archives)) - (add-to-list 'package-archives '("nongnu" . "https://elpa.nongnu.org/nongnu/") - t)) - (package-refresh-contents) - (package-install 'markdown-mode))) + (hypb:require-package 'markdown-mode)) (defun hyrolo-isearch-for-regexp (regexp fold-search-flag) "Interactively search forward for the next occurrence of REGEXP. diff --git a/test/hy-test-dependencies.el b/test/hy-test-dependencies.el index 62959ed2..f5a0d481 100644 --- a/test/hy-test-dependencies.el +++ b/test/hy-test-dependencies.el @@ -3,7 +3,7 @@ ;; Author: Mats Lidell ;; ;; Orig-Date: 20-Feb-21 at 23:16:00 -;; Last-Mod: 14-Jul-26 at 09:39:15 by Bob Weiner +;; Last-Mod: 18-Jul-26 at 15:13:20 by Mats Lidell ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -18,6 +18,35 @@ ;;; Code: +(require 'package) + +;; Advice with retry for package-install +(defun hypb:package-install-advice-for-retry (pkg-install &rest args) + "Add a retry layer on top of package-install." + (let ((retries 3) + (attempt 0) + done + result) + (while (not done) + (setq attempt (1+ attempt)) + (condition-case err + (progn + (setq result (apply pkg-install args)) + (setq done t)) + (wrong-type-argument + ;; Reraise if error is not stringp nil + (unless (equal (cdr err) '(stringp nil)) + (signal (car err) (cdr err))) + (if (<= attempt retries) + (message "package-install failed (attempt %d/%d): %s" + attempt retries (error-message-string err)) + (signal (car err) (cdr err)))))) + result)) + +;; Apply advice only for Emacs master branch version used in CI +(unless (version< emacs-version "32.0.50") + (advice-add 'package-install :around #'hypb:package-install-advice-for-retry)) + (declare-function markdown-ts-mode "ext:markdown-ts-mode") ;; Force markdown-mode to be selected first to avoid markdown-ts-mode @@ -26,6 +55,9 @@ (when (fboundp #'markdown-ts-mode) (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode))) +(setq package-pinned-packages + '((markdown-mode . "nongnu"))) + (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")) (package-initialize) @@ -53,5 +85,9 @@ ;; Ignore publishing-related errors that don't affect the tests. (ignore-errors (hsys-org-log-and-fix-version)) +;; Allow dynamic loading with no questions asked of dependencies +;; i.e. markdown-mode +(setq hypb:ask-to-install-package-flag nil) + (provide 'hy-test-dependencies) ;;; hy-test-dependencies.el ends here diff --git a/test/hy-test-helpers.el b/test/hy-test-helpers.el index a9d4305b..b6a71779 100644 --- a/test/hy-test-helpers.el +++ b/test/hy-test-helpers.el @@ -3,7 +3,7 @@ ;; Author: Mats Lidell ;; ;; Orig-Date: 30-Jan-21 at 12:00:00 -;; Last-Mod: 12-Apr-26 at 15:09:22 by Bob Weiner +;; Last-Mod: 15-Jul-26 at 22:00:22 by Mats Lidell ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -150,5 +150,16 @@ and the default WORD-LENGTH is 4." (setq beg (1+ beg))) (not no-face))) +(defmacro hy-test-mocked-feature (feature &rest body) + "Mock FEATURE as being present." + (declare (indent 1)) + `(let ((orig-featurep (symbol-function 'featurep))) + (cl-letf (((symbol-function 'featurep) + (lambda (f &optional subfeature) + (if (eq f ,feature) + t + (funcall orig-featurep f subfeature))))) + ,@body))) + (provide 'hy-test-helpers) ;;; hy-test-helpers.el ends here diff --git a/test/hypb-tests.el b/test/hypb-tests.el index 61494723..4cf70846 100644 --- a/test/hypb-tests.el +++ b/test/hypb-tests.el @@ -3,7 +3,7 @@ ;; Author: Mats Lidell ;; ;; Orig-Date: 5-Apr-21 at 18:53:10 -;; Last-Mod: 8-Mar-26 at 12:42:06 by Bob Weiner +;; Last-Mod: 16-Jul-26 at 17:06:05 by Mats Lidell ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -157,6 +157,55 @@ See Emacs bug#74042 related to usage of texi2any." (should-error (hypb:string-count-matches "a" "a" 0 -1)) (should-error (hypb:string-count-matches "a" "ab" 0 3))) +(ert-deftest hypb--users-package-manager () + "Verify `hypb:users-package-manager'." + (hy-test-mocked-feature 'straight + (should (eq 'straight (hypb:users-package-manager)))) + (hy-test-mocked-feature 'elpaca + (should (eq 'elpaca (hypb:users-package-manager)))) + (should (eq 'package (hypb:users-package-manager)))) + +(ert-deftest hypb--package-el-install () + "Verify `hypb:package-el-install'." + (let ((hypb:ask-to-install-package-flag t)) + (with-mock + (mock (y-or-n-p *) => nil) + (hypb:package-el-install 'hypb-dummy-package-name))) + (let* ((hypb:ask-to-install-package-flag nil) + (err (should-error (hypb:package-el-install 'hypb-dummy-package-name) :type 'error))) + ;;FIXME: Emacs-28 includes a '-' when there is no version + ;;number. Can be removed when we drop support for Emacs 28. + (should (string-match-p (rx "Package " punct "hypb-dummy-package-name" (optional "-") punct " is unavailable") (cadr err))))) + +(ert-deftest hypb--notify-manual-install-needed () + "Verify `hypb:notify-manual-install-needed'. +Verifies it raises a 'need to install' package manager error." + (let ((err (should-error (hypb:notify-manual-install-needed 'la-package 'la-manager) :type 'user-error))) + (should (string-search "la-package" (cadr err))) + (should (string-search "la-manager" (cadr err))))) + +(ert-deftest hypb--ensure-dependency () + "Verify `hypb:ensure-dependency'." + (with-mock + (mock (hypb:users-package-manager) => 'package) + (mock (hypb:package-el-install 'the-package) => t) + (should (hypb:ensure-dependency 'the-package))) + (with-mock + (mock (hypb:users-package-manager) => 'package) + (mock (hypb:package-el-install 'the-package) => nil) + (should-not (hypb:ensure-dependency 'the-package))) + (with-mock + (mock (hypb:users-package-manager) => 'elpaca) + (mock (hypb:notify-manual-install-needed 'the-package 'elpaca) => (user-error "Error")) + (should-error (hypb:ensure-dependency 'the-package) :type 'user-error))) + +(ert-deftest hypb--require-package () + "Verify `hypb:require-package' signals if automatic install fails." + (with-mock + (mock (hypb:ensure-dependency 'package) => nil) + (let ((err (should-error (hypb:require-package 'package) :type 'error))) + (should (string-search "could not be found" (cadr err)))))) + ;; This file can't be byte-compiled without the `el-mock' package (because of ;; the use of the `with-mock' macro), which is not a dependency of Hyperbole. ;; Local Variables: