diff options
Diffstat (limited to 'lisp/emacs-lisp/package.el')
-rw-r--r-- | lisp/emacs-lisp/package.el | 931 |
1 files changed, 586 insertions, 345 deletions
diff --git a/lisp/emacs-lisp/package.el b/lisp/emacs-lisp/package.el index ab02134bbd8..ef0c5171de6 100644 --- a/lisp/emacs-lisp/package.el +++ b/lisp/emacs-lisp/package.el @@ -101,7 +101,7 @@ ;; Michael Olson <mwolson@member.fsf.org> ;; Sebastian Tennant <sebyte@smolny.plus.com> ;; Stefan Monnier <monnier@iro.umontreal.ca> -;; Vinicius Jose Latorre <viniciusjl@ig.com.br> +;; Vinicius Jose Latorre <viniciusjl.gnu@gmail.com> ;; Phil Hagelberg <phil@hagelb.org> ;;; ToDo: @@ -143,14 +143,15 @@ ;;; Code: +(require 'cl-lib) (eval-when-compile (require 'subr-x)) -(eval-when-compile (require 'cl-lib)) (eval-when-compile (require 'epg)) ;For setf accessors. (require 'seq) (require 'tabulated-list) (require 'macroexp) (require 'url-handlers) +(require 'browse-url) (defgroup package nil "Manager for Emacs Lisp packages." @@ -161,29 +162,34 @@ ;;; Customization options ;;;###autoload (defcustom package-enable-at-startup t - "Whether to activate installed packages when Emacs starts. -If non-nil, packages are activated after reading the init file -and before `after-init-hook'. Activation is not done if -`user-init-file' is nil (e.g. Emacs was started with \"-q\"). + "Whether to make installed packages available when Emacs starts. +If non-nil, packages are made available before reading the init +file (but after reading the early init file). This means that if +you wish to set this variable, you must do so in the early init +file. Regardless of the value of this variable, packages are not +made available if `user-init-file' is nil (e.g. Emacs was started +with \"-q\"). Even if the value is nil, you can type \\[package-initialize] to -activate the package system at any time." +make installed packages available at any time, or you can +call (package-initialize) in your init-file." :type 'boolean :version "24.1") (defcustom package-load-list '(all) - "List of packages for `package-initialize' to load. + "List of packages for `package-initialize' to make available. Each element in this list should be a list (NAME VERSION), or the -symbol `all'. The symbol `all' says to load the latest installed -versions of all packages not specified by other elements. +symbol `all'. The symbol `all' says to make available the latest +installed versions of all packages not specified by other +elements. For an element (NAME VERSION), NAME is a package name (a symbol). VERSION should be t, a string, or nil. -If VERSION is t, the most recent version is activated. -If VERSION is a string, only that version is ever loaded. +If VERSION is t, the most recent version is made available. +If VERSION is a string, only that version is ever made available. Any other version, even if newer, is silently ignored. Hence, the package is \"held\" at that version. -If VERSION is nil, the package is not loaded (it is \"disabled\")." +If VERSION is nil, the package is not made available (it is \"disabled\")." :type '(repeat (choice (const all) (list :tag "Specific package" (symbol :tag "Package name") @@ -247,7 +253,9 @@ number from the archive with the highest priority is selected. When higher versions are available from archives with lower priorities, the user has to select those manually. -Archives not in this list have the priority 0. +Archives not in this list have the priority 0, as have packages +that are already installed. If you use negative priorities for +the archives, they will not be upgraded automatically. See also `package-menu-hide-low-priority'." :type '(alist :key-type (string :tag "Archive name") @@ -324,21 +332,37 @@ default directory." :risky t :version "26.1") -(defcustom package-check-signature - (if (and (require 'epg-config) - (epg-find-configuration 'OpenPGP)) - 'allow-unsigned) +(defcustom package-check-signature 'allow-unsigned "Non-nil means to check package signatures when installing. -The value `allow-unsigned' means to still install a package even if -it is unsigned. +More specifically the value can be: +- nil: package signatures are ignored. +- `allow-unsigned': install a package even if it is unsigned, but + if it is signed, we have the key for it, and OpenGPG is + installed, verify the signature. +- t: accept a package only if it comes with at least one verified signature. +- `all': same as t, except when the package has several signatures, + in which case we verify all the signatures. This also applies to the \"archive-contents\" file that lists the contents of the archive." :type '(choice (const nil :tag "Never") (const allow-unsigned :tag "Allow unsigned") - (const t :tag "Check always")) + (const t :tag "Check always") + (const all :tag "Check all signatures")) :risky t - :version "24.4") + :version "27.1") + +(defun package-check-signature () + "Check whether we have a usable OpenPGP configuration. +If true, and `package-check-signature' is `allow-unsigned', +return `allow-unsigned', otherwise return the value of +`package-check-signature'." + (if (eq package-check-signature 'allow-unsigned) + (progn + (require 'epg-config) + (and (epg-find-configuration 'OpenPGP) + 'allow-unsigned)) + package-check-signature)) (defcustom package-unsigned-archives nil "List of archives where we do not check for package signatures." @@ -482,7 +506,7 @@ This is, approximately, the inverse of `version-to-list'. str-list)))) (if (equal "." (car str-list)) (pop str-list)) - (apply 'concat (nreverse str-list))))) + (apply #'concat (nreverse str-list))))) (defun package-desc-full-name (pkg-desc) (format "%s-%s" @@ -491,9 +515,9 @@ This is, approximately, the inverse of `version-to-list'. (defun package-desc-suffix (pkg-desc) (pcase (package-desc-kind pkg-desc) - (`single ".el") - (`tar ".tar") - (`dir "") + ('single ".el") + ('tar ".tar") + ('dir "") (kind (error "Unknown package kind: %s" kind)))) (defun package-desc--keywords (pkg-desc) @@ -604,6 +628,12 @@ updates `package-alist'." (when (file-directory-p pkg-dir) (package-load-descriptor pkg-dir)))))))) +(defun package--alist () + "Return `package-alist', after computing it if needed." + (or package-alist + (progn (package-load-all-descriptors) + package-alist))) + (defun define-package (_name-string _version-string &optional _docstring _requirements &rest _extra-properties) @@ -676,13 +706,17 @@ PKG-DESC is a `package-desc' object." (defvar Info-directory-list) (declare-function info-initialize "info" ()) +(defvar package--quickstart-pkgs t + "If set to a list, we're computing the set of pkgs to activate.") + (defun package--load-files-for-activation (pkg-desc reload) "Load files for activating a package given by PKG-DESC. Load the autoloads file, and ensure `load-path' is setup. If RELOAD is non-nil, also load all files in the package that correspond to previously loaded files." - (let* ((loaded-files-list (when reload - (package--list-loaded-files (package-desc-dir pkg-desc))))) + (let* ((loaded-files-list + (when reload + (package--list-loaded-files (package-desc-dir pkg-desc))))) ;; Add to load path, add autoloads, and activate the package. (package--activate-autoloads-and-load-path pkg-desc) ;; Call `load' on all files in `package-desc-dir' already present in @@ -718,7 +752,10 @@ correspond to previously loaded files (those returned by (message "Unable to activate package `%s'.\nRequired package `%s-%s' is unavailable" name (car req) (package-version-join (cadr req))) (throw 'exit nil)))) - (package--load-files-for-activation pkg-desc reload) + (if (listp package--quickstart-pkgs) + ;; We're only collecting the set of packages to activate! + (push pkg-desc package--quickstart-pkgs) + (package--load-files-for-activation pkg-desc reload)) ;; Add info node. (when (file-exists-p (expand-file-name "dir" pkg-dir)) ;; FIXME: not the friendliest, but simple. @@ -738,7 +775,8 @@ DIR, sorted by most recently loaded last." (let* ((history (delq nil (mapcar (lambda (x) (let ((f (car x))) - (and f (file-name-sans-extension f)))) + (and (stringp f) + (file-name-sans-extension f)))) load-history))) (dir (file-truename dir)) ;; List all files that have already been loaded. @@ -825,7 +863,7 @@ untar into a directory named DIR; otherwise, signal an error." (tar-untar-buffer)) (defun package--alist-to-plist-args (alist) - (mapcar 'macroexp-quote + (mapcar #'macroexp-quote (apply #'nconc (mapcar (lambda (pair) (list (car pair) (cdr pair))) alist)))) (defun package-unpack (pkg-desc) @@ -834,7 +872,7 @@ untar into a directory named DIR; otherwise, signal an error." (dirname (package-desc-full-name pkg-desc)) (pkg-dir (expand-file-name dirname package-user-dir))) (pcase (package-desc-kind pkg-desc) - (`dir + ('dir (make-directory pkg-dir t) (let ((file-list (directory-files @@ -848,12 +886,12 @@ untar into a directory named DIR; otherwise, signal an error." ;; things simple by ensuring we're one of them. (setf (package-desc-kind pkg-desc) (if (> (length file-list) 1) 'tar 'single)))) - (`tar + ('tar (make-directory package-user-dir t) ;; FIXME: should we delete PKG-DIR if it exists? (let* ((default-directory (file-name-as-directory package-user-dir))) (package-untar-buffer dirname))) - (`single + ('single (let ((el-file (expand-file-name (format "%s.el" name) pkg-dir))) (make-directory pkg-dir t) (package--write-file-no-coding el-file))) @@ -886,7 +924,9 @@ untar into a directory named DIR; otherwise, signal an error." (print-length nil)) (write-region (concat - ";;; -*- no-byte-compile: t -*-\n" + ";;; Generated package description from " + (replace-regexp-in-string "-pkg\\.el\\'" ".el" pkg-file) + " -*- no-byte-compile: t -*-\n" (prin1-to-string (nconc (list 'define-package @@ -961,17 +1001,12 @@ This assumes that `pkg-desc' has already been activated with (defun package-read-from-string (str) "Read a Lisp expression from STR. Signal an error if the entire string was not used." - (let* ((read-data (read-from-string str)) - (more-left - (condition-case nil - ;; The call to `ignore' suppresses a compiler warning. - (progn (ignore (read-from-string - (substring str (cdr read-data)))) - t) - (end-of-file nil)))) - (if more-left - (error "Can't read whole string") - (car read-data)))) + (pcase-let ((`(,expr . ,offset) (read-from-string str))) + (condition-case () + ;; The call to `ignore' suppresses a compiler warning. + (progn (ignore (read-from-string str offset)) + (error "Can't read whole string")) + (end-of-file expr)))) (defun package--prepare-dependencies (deps) "Turn DEPS into an acceptable list of dependencies. @@ -993,7 +1028,9 @@ is wrapped around any parts requiring it." deps)))) (declare-function lm-header "lisp-mnt" (header)) +(declare-function lm-header-multiline "lisp-mnt" (header)) (declare-function lm-homepage "lisp-mnt" (&optional file)) +(declare-function lm-keywords-list "lisp-mnt" (&optional file)) (declare-function lm-maintainer "lisp-mnt" (&optional file)) (declare-function lm-authors "lisp-mnt" (&optional file)) @@ -1009,6 +1046,8 @@ boundaries." (let ((file-name (match-string-no-properties 1)) (desc (match-string-no-properties 2)) (start (line-beginning-position))) + ;; The terminating comment format could be extended to accept a + ;; generic string that is not in English. (unless (search-forward (concat ";;; " file-name ".el ends here")) (error "Package lacks a terminating comment")) ;; Try to include a trailing newline. @@ -1016,23 +1055,24 @@ boundaries." (narrow-to-region start (point)) (require 'lisp-mnt) ;; Use some headers we've invented to drive the process. - (let* ((requires-str (lm-header "package-requires")) - ;; Prefer Package-Version; if defined, the package author + (let* (;; Prefer Package-Version; if defined, the package author ;; probably wants us to use it. Otherwise try Version. (pkg-version (or (package-strip-rcs-id (lm-header "package-version")) (package-strip-rcs-id (lm-header "version")))) + (keywords (lm-keywords-list)) (homepage (lm-homepage))) (unless pkg-version (error "Package lacks a \"Version\" or \"Package-Version\" header")) (package-desc-from-define file-name pkg-version desc - (if requires-str - (package--prepare-dependencies - (package-read-from-string requires-str))) + (and-let* ((require-lines (lm-header-multiline "package-requires"))) + (package--prepare-dependencies + (package-read-from-string (mapconcat #'identity require-lines " ")))) :kind 'single :url homepage + :keywords keywords :maintainer (lm-maintainer) :authors (lm-authors))))) @@ -1170,45 +1210,66 @@ errors signaled by ERROR-FORM or by BODY). (declare (indent defun) (debug t)) (while (keywordp (car body)) (setq body (cdr (cdr body)))) - (macroexp-let2* nil ((url-1 url) - (noerror-1 noerror)) - (let ((url-sym (make-symbol "url")) - (b-sym (make-symbol "b-sym"))) - `(cl-macrolet ((unless-error (body-2 &rest before-body) - (let ((err (make-symbol "err"))) - `(with-temp-buffer - (when (condition-case ,err - (progn ,@before-body t) - ,(list 'error ',error-form - (list 'unless ',noerror-1 - `(signal (car ,err) (cdr ,err))))) - ,@body-2))))) - (if (string-match-p "\\`https?:" ,url-1) - (let ((,url-sym (concat ,url-1 ,file))) - (if ,async - (unless-error nil - (url-retrieve ,url-sym - (lambda (status) - (let ((,b-sym (current-buffer))) - (require 'url-handlers) - (unless-error ,body - (when-let* ((er (plist-get status :error))) - (error "Error retrieving: %s %S" ,url-sym er)) - (with-current-buffer ,b-sym - (goto-char (point-min)) - (unless (search-forward-regexp "^\r?\n\r?" nil 'noerror) - (error "Error retrieving: %s %S" ,url-sym "incomprehensible buffer"))) - (url-insert-buffer-contents ,b-sym ,url-sym) - (kill-buffer ,b-sym) - (goto-char (point-min))))) - nil - 'silent)) - (unless-error ,body (url-insert-file-contents ,url-sym)))) - (unless-error ,body - (let ((url (expand-file-name ,file ,url-1))) - (unless (file-name-absolute-p url) - (error "Location %s is not a url nor an absolute file name" url)) - (insert-file-contents url)))))))) + `(package--with-response-buffer-1 ,url (lambda () ,@body) + :file ,file + :async ,async + :error-function (lambda () ,error-form) + :noerror ,noerror)) + +(defmacro package--unless-error (body &rest before-body) + (declare (debug t) (indent 1)) + (let ((err (make-symbol "err"))) + `(with-temp-buffer + (set-buffer-multibyte nil) + (when (condition-case ,err + (progn ,@before-body t) + (error (funcall error-function) + (unless noerror + (signal (car ,err) (cdr ,err))))) + (funcall ,body))))) + +(cl-defun package--with-response-buffer-1 (url body &key async file error-function noerror &allow-other-keys) + (if (string-match-p "\\`https?:" url) + (let ((url (concat url file))) + (if async + (package--unless-error #'ignore + (url-retrieve + url + (lambda (status) + (let ((b (current-buffer))) + (require 'url-handlers) + (package--unless-error body + (when-let* ((er (plist-get status :error))) + (error "Error retrieving: %s %S" url er)) + (with-current-buffer b + (goto-char (point-min)) + (unless (search-forward-regexp "^\r?\n\r?" nil t) + (error "Error retrieving: %s %S" + url "incomprehensible buffer"))) + (url-insert b) + (kill-buffer b) + (goto-char (point-min))))) + nil + 'silent)) + (package--unless-error body + ;; Copy&pasted from url-insert-file-contents, + ;; except it calls `url-insert' because we want the contents + ;; literally (but there's no url-insert-file-contents-literally). + (let ((buffer (url-retrieve-synchronously url))) + (unless buffer (signal 'file-error (list url "No Data"))) + (when (fboundp 'url-http--insert-file-helper) + ;; XXX: This is HTTP/S specific and should be moved + ;; to url-http instead. See bug#17549. + (url-http--insert-file-helper buffer url)) + (url-insert buffer) + (kill-buffer buffer) + (goto-char (point-min)))))) + (package--unless-error body + (let ((url (expand-file-name file url))) + (unless (file-name-absolute-p url) + (error "Location %s is not a url nor an absolute file name" + url)) + (insert-file-contents-literally url))))) (define-error 'bad-signature "Failed to verify signature") @@ -1229,29 +1290,20 @@ errors." (dolist (sig (epg-context-result-for context 'verify)) (if (eq (epg-signature-status sig) 'good) (push sig good-signatures) - ;; If package-check-signature is allow-unsigned, don't + ;; If `package-check-signature' is allow-unsigned, don't ;; signal error when we can't verify signature because of ;; missing public key. Other errors are still treated as ;; fatal (bug#17625). - (unless (and (eq package-check-signature 'allow-unsigned) + (unless (and (eq (package-check-signature) 'allow-unsigned) (eq (epg-signature-status sig) 'no-pubkey)) (setq had-fatal-error t)))) - (when (or (null good-signatures) had-fatal-error) + (when (or (null good-signatures) + (and (eq (package-check-signature) 'all) + had-fatal-error)) (package--display-verify-error context sig-file) (signal 'bad-signature (list sig-file))) good-signatures))) -(defun package--buffer-string () - (let ((string (buffer-string))) - (when (and buffer-file-coding-system - (> (length string) 0)) - (put-text-property 0 1 'package--cs buffer-file-coding-system string)) - string)) - -(defun package--cs (string) - (and (> (length string) 0) - (get-text-property 0 'package--cs string))) - (defun package--check-signature (location file &optional string async callback unwind) "Check signature of the current buffer. Download the signature file from LOCATION by appending \".sig\" @@ -1271,16 +1323,13 @@ Otherwise, an error is signaled. UNWIND, if provided, is a function to be called after everything else, even if an error is signaled." - (let* ((sig-file (concat file ".sig")) - (string (or string (package--buffer-string))) - (cs (package--cs string))) - ;; Re-encode the downloaded file with the coding-system with which - ;; it was decoded, so we (hopefully) get the exact same bytes back. - (when cs (setq string (encode-coding-string string cs))) + (let ((sig-file (concat file ".sig")) + (string (or string (buffer-string)))) (package--with-response-buffer location :file sig-file :async async :noerror t ;; Connection error is assumed to mean "no sig-file". - :error-form (let ((allow-unsigned (eq package-check-signature 'allow-unsigned))) + :error-form (let ((allow-unsigned + (eq (package-check-signature) 'allow-unsigned))) (when (and callback allow-unsigned) (funcall callback nil)) (when unwind (funcall unwind)) @@ -1289,8 +1338,9 @@ else, even if an error is signaled." ;; OTOH, an error here means "bad signature", which we never ;; suppress. (Bug#22089) (unwind-protect - (let ((sig (package--check-signature-content (buffer-substring (point) (point-max)) - string sig-file))) + (let ((sig (package--check-signature-content + (buffer-substring (point) (point-max)) + string sig-file))) (when callback (funcall callback sig)) sig) (when unwind (funcall unwind)))))) @@ -1451,45 +1501,59 @@ If successful, set `package-archive-contents'." ;; available on disk. (defvar package--initialized nil) -(defvar package--init-file-ensured nil - "Whether we know the init file has package-initialize.") - ;;;###autoload (defun package-initialize (&optional no-activate) "Load Emacs Lisp packages, and activate them. The variable `package-load-list' controls which packages to load. If optional arg NO-ACTIVATE is non-nil, don't activate packages. -If `user-init-file' does not mention `(package-initialize)', add -it to the file. If called as part of loading `user-init-file', set `package-enable-at-startup' to nil, to prevent accidentally loading packages twice. + It is not necessary to adjust `load-path' or `require' the individual packages after calling `package-initialize' -- this is -taken care of by `package-initialize'." +taken care of by `package-initialize'. + +If `package-initialize' is called twice during Emacs startup, +signal a warning, since this is a bad idea except in highly +advanced use cases. To suppress the warning, remove the +superfluous call to `package-initialize' from your init-file. If +you have code which must run before `package-initialize', put +that code in the early init-file." (interactive) + (when (and package--initialized (not after-init-time)) + (lwarn '(package reinitialization) :warning + "Unnecessary call to `package-initialize' in init file")) (setq package-alist nil) - (if after-init-time - (package--ensure-init-file) - ;; If `package-initialize' is before we finished loading the init - ;; file, it's obvious we don't need to ensure-init. - (setq package--init-file-ensured t - ;; And likely we don't need to run it again after init. - package-enable-at-startup nil)) + (setq package-enable-at-startup nil) (package-load-all-descriptors) (package-read-all-archive-contents) - (unless no-activate - (dolist (elt package-alist) - (condition-case err - (package-activate (car elt)) - ;; Don't let failure of activation of a package arbitrarily stop - ;; activation of further packages. - (error (message "%s" (error-message-string err)))))) (setq package--initialized t) + (unless no-activate + (package-activate-all)) ;; This uses `package--mapc' so it must be called after ;; `package--initialized' is t. (package--build-compatibility-table)) +(defvar package-quickstart-file) + +;;;###autoload +(defun package-activate-all () + "Activate all installed packages. +The variable `package-load-list' controls which packages to load." + (setq package-enable-at-startup nil) + (if (file-readable-p package-quickstart-file) + ;; Skip load-source-file-function which would slow us down by a factor + ;; 2 (this assumes we were careful to save this file so it doesn't need + ;; any decoding). + (let ((load-source-file-function nil)) + (load package-quickstart-file nil 'nomessage)) + (dolist (elt (package--alist)) + (condition-case err + (package-activate (car elt)) + ;; Don't let failure of activation of a package arbitrarily stop + ;; activation of further packages. + (error (message "%s" (error-message-string err))))))) ;;;; Populating `package-archive-contents' from archives ;; This subsection populates the variables listed above from the @@ -1544,25 +1608,27 @@ similar to an entry in `package-alist'. Save the cached copy to :error-form (package--update-downloads-in-progress archive) (let* ((location (cdr archive)) (name (car archive)) - (content (package--buffer-string)) - (dir (expand-file-name (format "archives/%s" name) package-user-dir)) + (content (buffer-string)) + (dir (expand-file-name (concat "archives/" name) package-user-dir)) (local-file (expand-file-name file dir))) (when (listp (read content)) (make-directory dir t) - (if (or (not package-check-signature) + (if (or (not (package-check-signature)) (member name package-unsigned-archives)) ;; If we don't care about the signature, save the file and ;; we're done. - (progn (let ((coding-system-for-write - (or (package--cs content) 'utf-8))) - (write-region content nil local-file nil 'silent)) - (package--update-downloads-in-progress archive)) + (progn + (cl-assert (not enable-multibyte-characters)) + (let ((coding-system-for-write 'binary)) + (write-region content nil local-file nil 'silent)) + (package--update-downloads-in-progress archive)) ;; If we care, check it (perhaps async) and *then* write the file. (package--check-signature location file content async ;; This function will be called after signature checking. (lambda (&optional good-sigs) - (let ((coding-system-for-write (or (package--cs content) 'utf-8))) + (cl-assert (not enable-multibyte-characters)) + (let ((coding-system-for-write 'binary)) (write-region content nil local-file nil 'silent)) ;; Write out good signatures into archive-contents.signed file. (when good-sigs @@ -1598,8 +1664,8 @@ downloads in the background." (make-directory package-user-dir t)) (let ((default-keyring (expand-file-name "package-keyring.gpg" data-directory)) - (inhibit-message async)) - (when (and package-check-signature (file-exists-p default-keyring)) + (inhibit-message (or inhibit-message async))) + (when (and (package-check-signature) (file-exists-p default-keyring)) (condition-case-unless-debug error (package-import-keyring default-keyring) (error (message "Cannot import default keyring: %S" (cdr error)))))) @@ -1846,7 +1912,7 @@ if all the in-between dependencies are also in PACKAGE-LIST." (file (concat (package-desc-full-name pkg-desc) (package-desc-suffix pkg-desc)))) (package--with-response-buffer location :file file - (if (or (not package-check-signature) + (if (or (not (package-check-signature)) (member (package-desc-archive pkg-desc) package-unsigned-archives)) ;; If we don't care about the signature, unpack and we're @@ -1854,15 +1920,16 @@ if all the in-between dependencies are also in PACKAGE-LIST." (let ((save-silently t)) (package-unpack pkg-desc)) ;; If we care, check it and *then* write the file. - (let ((content (package--buffer-string))) + (let ((content (buffer-string))) (package--check-signature location file content nil ;; This function will be called after signature checking. (lambda (&optional good-sigs) ;; Signature checked, unpack now. - (with-temp-buffer + (with-temp-buffer ;FIXME: Just use the previous current-buffer. + (set-buffer-multibyte nil) + (cl-assert (not (multibyte-string-p content))) (insert content) - (setq buffer-file-coding-system (package--cs content)) (let ((save-silently t)) (package-unpack pkg-desc))) ;; Here the package has been installed successfully, mark it as @@ -1878,7 +1945,8 @@ if all the in-between dependencies are also in PACKAGE-LIST." ;; Update the old pkg-desc which will be shown on the description buffer. (setf (package-desc-signed pkg-desc) t) ;; Update the new (activated) pkg-desc as well. - (when-let* ((pkg-descs (cdr (assq (package-desc-name pkg-desc) package-alist)))) + (when-let* ((pkg-descs (cdr (assq (package-desc-name pkg-desc) + package-alist)))) (setf (package-desc-signed (car pkg-descs)) t)))))))))) (defun package-installed-p (package &optional min-version) @@ -1887,18 +1955,25 @@ If PACKAGE is a symbol, it is the package name and MIN-VERSION should be a version list. If PACKAGE is a `package-desc' object, MIN-VERSION is ignored." - (unless package--initialized (error "package.el is not yet initialized!")) - (if (package-desc-p package) - (let ((dir (package-desc-dir package))) + (cond + ((package-desc-p package) + (let ((dir (package-desc-dir package))) (and (stringp dir) - (file-exists-p dir))) + (file-exists-p dir)))) + ((and (not package--initialized) + (null min-version) + package-activated-list) + ;; We used the quickstart: make it possible to use package-installed-p + ;; even before package is fully initialized. + (memq package package-activated-list)) + (t (or - (let ((pkg-descs (cdr (assq package package-alist)))) + (let ((pkg-descs (cdr (assq package (package--alist))))) (and pkg-descs (version-list-<= min-version (package-desc-version (car pkg-descs))))) ;; Also check built-in packages. - (package-built-in-p package min-version)))) + (package-built-in-p package min-version))))) (defun package-download-transaction (packages) "Download and install all the packages in PACKAGES. @@ -1908,64 +1983,6 @@ PACKAGES are satisfied, i.e. that PACKAGES is computed using `package-compute-transaction'." (mapc #'package-install-from-archive packages)) -(defun package--ensure-init-file () - "Ensure that the user's init file has `package-initialize'. -`package-initialize' doesn't have to be called, as long as it is -present somewhere in the file, even as a comment. If it is not, -add a call to it along with some explanatory comments." - ;; Don't mess with the init-file from "emacs -Q". - (when (and (stringp user-init-file) - (not package--init-file-ensured) - (file-readable-p user-init-file) - (file-writable-p user-init-file)) - (let* ((buffer (find-buffer-visiting user-init-file)) - buffer-name - (contains-init - (if buffer - (with-current-buffer buffer - (save-excursion - (save-restriction - (widen) - (goto-char (point-min)) - (re-search-forward "(package-initialize\\_>" nil 'noerror)))) - ;; Don't visit the file if we don't have to. - (with-temp-buffer - (insert-file-contents user-init-file) - (goto-char (point-min)) - (re-search-forward "(package-initialize\\_>" nil 'noerror))))) - (unless contains-init - (with-current-buffer (or buffer - (let ((delay-mode-hooks t) - (find-file-visit-truename t)) - (find-file-noselect user-init-file))) - (when buffer - (setq buffer-name (buffer-file-name)) - (set-visited-file-name (file-chase-links user-init-file))) - (save-excursion - (save-restriction - (widen) - (goto-char (point-min)) - (while (and (looking-at-p "[[:blank:]]*\\(;\\|$\\)") - (not (eobp))) - (forward-line 1)) - (insert - "\n" - ";; Added by Package.el. This must come before configurations of\n" - ";; installed packages. Don't delete this line. If you don't want it,\n" - ";; just comment it out by adding a semicolon to the start of the line.\n" - ";; You may delete these explanatory comments.\n" - "(package-initialize)\n") - (unless (looking-at-p "$") - (insert "\n")) - (let ((file-precious-flag t)) - (save-buffer)) - (if buffer - (progn - (set-visited-file-name buffer-name) - (set-buffer-modified-p nil)) - (kill-buffer (current-buffer))))))))) - (setq package--init-file-ensured t)) - ;;;###autoload (defun package-install (pkg &optional dont-select) "Install the package PKG. @@ -2007,7 +2024,9 @@ to install it but still mark it as selected." (package-compute-transaction (list pkg) (package-desc-reqs pkg))) (package-compute-transaction () (list (list pkg)))))) - (package-download-transaction transaction) + (progn + (package-download-transaction transaction) + (package--quickstart-maybe-refresh)) (message "`%s' is already installed" name)))) (defun package-strip-rcs-id (str) @@ -2091,12 +2110,12 @@ If some packages are not installed propose to install them." (cond (available (when (y-or-n-p - (format "%s packages will be installed:\n%s, proceed?" + (format "Packages to install: %d (%s), proceed? " (length available) - (mapconcat #'symbol-name available ", "))) + (mapconcat #'symbol-name available " "))) (mapc (lambda (p) (package-install p 'dont-select)) available))) ((> difference 0) - (message "%s packages are not available (the rest already installed), maybe you need to `M-x package-refresh-contents'" + (message "Packages that are not available: %d (the rest is already installed), maybe you need to `M-x package-refresh-contents'" difference)) (t (message "All your packages are already installed")))))) @@ -2122,16 +2141,12 @@ If NOSAVE is non-nil, the package is not removed from `package-selected-packages'." (interactive (progn - ;; Initialize the package system to get the list of package - ;; symbols for completion. - (unless package--initialized - (package-initialize t)) (let* ((package-table (mapcar (lambda (p) (cons (package-desc-full-name p) p)) (delq nil (mapcar (lambda (p) (unless (package-built-in-p p) p)) - (apply #'append (mapcar #'cdr package-alist)))))) + (apply #'append (mapcar #'cdr (package--alist))))))) (package-name (completing-read "Delete package: " (mapcar #'car package-table) nil t))) @@ -2166,6 +2181,9 @@ If NOSAVE is non-nil, the package is not removed from (add-hook 'post-command-hook #'package-menu--post-refresh) (delete-directory dir t) ;; Remove NAME-VERSION.signed and NAME-readme.txt files. + ;; + ;; NAME-readme.txt files are no longer created, but they + ;; may be left around from an earlier install. (dolist (suffix '(".signed" "readme.txt")) (let* ((version (package-version-join (package-desc-version pkg-desc))) (file (concat (if (string= suffix ".signed") @@ -2179,7 +2197,9 @@ If NOSAVE is non-nil, the package is not removed from (delete pkg-desc pkgs) (unless (cdr pkgs) (setq package-alist (delq pkgs package-alist)))) - (message "Package `%s' deleted." (package-desc-full-name pkg-desc)))))) + (package--quickstart-maybe-refresh) + (message "Package `%s' deleted." + (package-desc-full-name pkg-desc)))))) ;;;###autoload (defun package-reinstall (pkg) @@ -2213,9 +2233,9 @@ will be deleted." (let ((removable (package--removable-packages))) (if removable (when (y-or-n-p - (format "%s packages will be deleted:\n%s, proceed? " + (format "Packages to delete: %d (%s), proceed? " (length removable) - (mapconcat #'symbol-name removable ", "))) + (mapconcat #'symbol-name removable " "))) (mapc (lambda (p) (package-delete (cadr (assq p package-alist)) t)) removable)) @@ -2234,12 +2254,12 @@ will be deleted." ;; Load the package list if necessary (but don't activate them). (unless package--initialized (package-initialize t)) - (let ((packages (append (mapcar 'car package-alist) - (mapcar 'car package-archive-contents) - (mapcar 'car package--builtins)))) + (let ((packages (append (mapcar #'car package-alist) + (mapcar #'car package-archive-contents) + (mapcar #'car package--builtins)))) (unless (memq guess packages) (setq guess nil)) - (setq packages (mapcar 'symbol-name packages)) + (setq packages (mapcar #'symbol-name packages)) (let ((val (completing-read (if guess (format "Describe package (default %s): " @@ -2247,7 +2267,7 @@ will be deleted." "Describe package: ") packages nil t nil nil (when guess (symbol-name guess))))) - (list (intern val)))))) + (list (and (> (length val) 0) (intern val))))))) (if (not (or (package-desc-p package) (and package (symbolp package)))) (message "No package specified") (help-setup-xref (list #'describe-package package) @@ -2274,6 +2294,45 @@ Otherwise no newline is inserted." (declare-function lm-commentary "lisp-mnt" (&optional file)) +(defun package--get-description (desc) + "Return a string containing the long description of the package DESC. +The description is read from the installed package files." + ;; Installed packages have nil for kind, so we look for README + ;; first, then fall back to the Commentary header. + + ;; We don’t include README.md here, because that is often the home + ;; page on a site like github, and not suitable as the package long + ;; description. + (let ((files '("README-elpa" "README-elpa.md" "README" "README.rst" "README.org")) + file + (srcdir (package-desc-dir desc)) + result) + (while (and files + (not result)) + (setq file (pop files)) + (when (file-readable-p (expand-file-name file srcdir)) + ;; Found a README. + (with-temp-buffer + (insert-file-contents (expand-file-name file srcdir)) + (setq result (buffer-string))))) + + (or + result + + ;; Look for Commentary header. + (let ((mainsrcfile (expand-file-name (format "%s.el" (package-desc-name desc)) + srcdir))) + (when (file-readable-p mainsrcfile) + (with-temp-buffer + (insert (or (lm-commentary mainsrcfile) "")) + (goto-char (point-min)) + (when (re-search-forward "^;;; Commentary:\n" nil t) + (replace-match "")) + (while (re-search-forward "^\\(;+ ?\\)" nil t) + (replace-match "")) + (buffer-string)))) + ))) + (defun describe-package-1 (pkg) (require 'lisp-mnt) (let* ((desc (or @@ -2297,17 +2356,17 @@ Otherwise no newline is inserted." (installable (and archive (not built-in))) (status (if desc (package-desc-status desc) "orphan")) (incompatible-reason (package--incompatible-p desc)) - (signed (if desc (package-desc-signed desc)))) + (signed (if desc (package-desc-signed desc))) + (maintainer (cdr (assoc :maintainer extras))) + (authors (cdr (assoc :authors extras)))) (when (string= status "avail-obso") (setq status "available obsolete")) (when incompatible-reason (setq status "incompatible")) - (prin1 name) - (princ " is ") - (princ (if (memq (aref status 0) '(?a ?e ?i ?o ?u)) "an " "a ")) - (princ status) - (princ " package.\n\n") + (princ (format "Package %S is %s.\n\n" name status)) + ;; TODO: Remove the string decorations and reformat the strings + ;; for future l10n. (package--print-help-section "Status") (cond (built-in (insert (propertize (capitalize status) @@ -2422,6 +2481,19 @@ Otherwise no newline is inserted." 'action 'package-keyword-button-action) (insert " ")) (insert "\n")) + (when maintainer + (package--print-help-section "Maintainer") + (package--print-email-button maintainer)) + (when authors + (package--print-help-section + (if (= (length authors) 1) + "Author" + "Authors")) + (package--print-email-button (pop authors)) + ;; If there's more than one author, indent the rest correctly. + (dolist (name authors) + (insert (make-string 13 ?\s)) + (package--print-email-button name))) (let* ((all-pkgs (append (cdr (assq name package-alist)) (cdr (assq name package-archive-contents)) (let ((bi (assq name package--builtins))) @@ -2448,39 +2520,47 @@ Otherwise no newline is inserted." (insert "\n") - (if built-in - ;; For built-in packages, insert the commentary. - (let ((fn (locate-file (format "%s.el" name) load-path - load-file-rep-suffixes)) - (opoint (point))) - (insert (or (lm-commentary fn) "")) - (save-excursion - (goto-char opoint) - (when (re-search-forward "^;;; Commentary:\n" nil t) - (replace-match "")) - (while (re-search-forward "^\\(;+ ?\\)" nil t) - (replace-match "")))) - (let* ((basename (format "%s-readme.txt" name)) - (readme (expand-file-name basename package-user-dir)) - readme-string) - ;; For elpa packages, try downloading the commentary. If that - ;; fails, try an existing readme file in `package-user-dir'. - (cond ((and (package-desc-archive desc) - (package--with-response-buffer (package-archive-base desc) - :file basename :noerror t - (save-excursion - (goto-char (point-max)) - (unless (bolp) - (insert ?\n))) - (write-region nil nil - (expand-file-name readme package-user-dir) - nil 'silent) - (setq readme-string (buffer-string)) - t)) - (insert readme-string)) - ((file-readable-p readme) - (insert-file-contents readme) - (goto-char (point-max)))))))) + (let ((start-of-description (point))) + (if built-in + ;; For built-in packages, get the description from the + ;; Commentary header. + (let ((fn (locate-file (format "%s.el" name) load-path + load-file-rep-suffixes)) + (opoint (point))) + (insert (or (lm-commentary fn) "")) + (save-excursion + (goto-char opoint) + (when (re-search-forward "^;;; Commentary:\n" nil t) + (replace-match "")) + (while (re-search-forward "^\\(;+ ?\\)" nil t) + (replace-match "")))) + + (if (package-installed-p desc) + ;; For installed packages, get the description from the + ;; installed files. + (insert (package--get-description desc)) + + ;; For non-built-in, non-installed packages, get description from + ;; the archive. + (let* ((basename (format "%s-readme.txt" name)) + readme-string) + + (package--with-response-buffer (package-archive-base desc) + :file basename :noerror t + (save-excursion + (goto-char (point-max)) + (unless (bolp) + (insert ?\n))) + (cl-assert (not enable-multibyte-characters)) + (setq readme-string + ;; The readme.txt files are defined to contain utf-8 text. + (decode-coding-region (point-min) (point-max) 'utf-8 t)) + t) + (insert (or readme-string + "This package does not provide a description."))))) + ;; Make URLs in the description into links. + (goto-char start-of-description) + (browse-url-add-buttons)))) (defun package-install-button-action (button) (let ((pkg-desc (button-get button 'package-desc))) @@ -2509,9 +2589,24 @@ Otherwise no newline is inserted." :background "light grey" :foreground "black") 'link))) - (apply 'insert-text-button button-text 'face button-face 'follow-link t + (apply #'insert-text-button button-text 'face button-face 'follow-link t props))) +(defun package--print-email-button (name) + (when (car name) + (insert (car name))) + (when (and (car name) (cdr name)) + (insert " ")) + (when (cdr name) + (insert "<") + (insert-text-button (cdr name) + 'follow-link t + 'action (lambda (_) + (compose-mail + (format "%s <%s>" (car name) (cdr name))))) + (insert ">")) + (insert "\n")) + ;;;; Package menu mode. @@ -2537,7 +2632,7 @@ Otherwise no newline is inserted." (easy-menu-define package-menu-mode-menu package-menu-mode-map "Menu for `package-menu-mode'." - `("Package" + '("Package" ["Describe Package" package-menu-describe-package :help "Display information about this package"] ["Help" package-menu-quick-help :help "Show short key binding help for package-menu-mode"] "--" @@ -2590,8 +2685,12 @@ Letters do not insert themselves; instead, they are commands. ("Description" 0 nil)]) (setq tabulated-list-padding 2) (setq tabulated-list-sort-key (cons "Status" nil)) - (add-hook 'tabulated-list-revert-hook 'package-menu--refresh nil t) - (tabulated-list-init-header)) + (add-hook 'tabulated-list-revert-hook #'package-menu--refresh nil t) + (tabulated-list-init-header) + (setf imenu-prev-index-position-function + #'package--imenu-prev-index-position-function) + (setf imenu-extract-index-name-function + #'package--imenu-extract-index-name-function)) (defmacro package--push (pkg-desc status listname) "Convenience macro for `package-menu--generate'. @@ -2689,9 +2788,9 @@ Installed obsolete packages are always displayed.") (user-error "The current buffer is not a Package Menu")) (setq package-menu--hide-packages (not package-menu--hide-packages)) - (message "%s packages" (if package-menu--hide-packages - "Hiding obsolete or unwanted" - "Displaying all")) + (if package-menu--hide-packages + (message "Hiding obsolete or unwanted packages") + (message "Displaying all packages")) (revert-buffer nil 'no-confirm)) (defun package--remove-hidden (pkg-list) @@ -2717,12 +2816,11 @@ to their archives." ((not package-menu-hide-low-priority) pkg-list) ((eq package-menu-hide-low-priority 'archive) - (let* ((max-priority most-negative-fixnum) - (out)) + (let (max-priority out) (while pkg-list (let ((p (pop pkg-list))) (let ((priority (package-desc-priority p))) - (if (< priority max-priority) + (if (and max-priority (< priority max-priority)) (setq pkg-list nil) (push p out) (setq max-priority priority))))) @@ -2796,7 +2894,7 @@ KEYWORDS should be nil or a list of keywords." (mapcar #'package-menu--print-info-simple info-list)))) (defun package-all-keywords () - "Collect all package keywords" + "Collect all package keywords." (let ((key-list)) (package--mapc (lambda (desc) (setq key-list (append (package-desc--keywords desc) @@ -2853,7 +2951,7 @@ When none are given, the package matches." (defun package-menu--generate (remember-pos packages &optional keywords) "Populate the Package Menu. - If REMEMBER-POS is non-nil, keep point on the same entry. +If REMEMBER-POS is non-nil, keep point on the same entry. PACKAGES should be t, which means to display all known packages, or a list of package names (symbols) to display. @@ -2862,7 +2960,7 @@ shown." (package-menu--refresh packages keywords) (setf (car (aref tabulated-list-format 0)) (if keywords - (let ((filters (mapconcat 'identity keywords ","))) + (let ((filters (mapconcat #'identity keywords ","))) (concat "Package[" filters "]")) "Package")) (if keywords @@ -2955,17 +3053,17 @@ PKG is a `package-desc' object. Return (PKG-DESC [NAME VERSION STATUS DOC])." (let* ((status (package-desc-status pkg)) (face (pcase status - (`"built-in" 'package-status-built-in) - (`"external" 'package-status-external) - (`"available" 'package-status-available) - (`"avail-obso" 'package-status-avail-obso) - (`"new" 'package-status-new) - (`"held" 'package-status-held) - (`"disabled" 'package-status-disabled) - (`"installed" 'package-status-installed) - (`"dependency" 'package-status-dependency) - (`"unsigned" 'package-status-unsigned) - (`"incompat" 'package-status-incompat) + ("built-in" 'package-status-built-in) + ("external" 'package-status-external) + ("available" 'package-status-available) + ("avail-obso" 'package-status-avail-obso) + ("new" 'package-status-new) + ("held" 'package-status-held) + ("disabled" 'package-status-disabled) + ("installed" 'package-status-installed) + ("dependency" 'package-status-dependency) + ("unsigned" 'package-status-unsigned) + ("incompat" 'package-status-incompat) (_ 'font-lock-warning-face)))) ; obsolete. (list pkg `[(,(symbol-name (package-desc-name pkg)) @@ -2988,12 +3086,15 @@ Return (PKG-DESC [NAME VERSION STATUS DOC])." "`package-archive-contents' before the latest refresh.") (defun package-menu-refresh () - "Download the Emacs Lisp package archive. -This fetches the contents of each archive specified in -`package-archives', and then refreshes the package menu." + "In Package Menu, download the Emacs Lisp package archive. +Fetch the contents of each archive specified in +`package-archives', and then refresh the package menu. Signal a +user-error if there is already a refresh running asynchronously." (interactive) (unless (derived-mode-p 'package-menu-mode) (user-error "The current buffer is not a Package Menu")) + (when (and package-menu-async package--downloads-in-progress) + (user-error "Package refresh is already in progress, please wait...")) (setq package-menu--old-archive-contents package-archive-contents) (setq package-menu--new-package-list nil) (package-refresh-contents package-menu-async)) @@ -3015,11 +3116,11 @@ If optional arg BUTTON is non-nil, describe its associated package." (let ((hidden (cl-remove-if-not (lambda (e) (string-match re (symbol-name (car e)))) package-archive-contents))) - (message (substitute-command-keys - (concat "Hiding %s packages, type `\\[package-menu-toggle-hiding]'" - " to toggle or `\\[customize-variable] RET package-hidden-regexps'" - " to customize it")) - (length hidden))))) + (message "Packages to hide: %d. Type `%s' to toggle or `%s' to customize" + (length hidden) + (substitute-command-keys "\\[package-menu-toggle-hidding]") + (substitute-command-keys "\\[customize-variable] RET package-hidden-regexps"))))) + (defun package-menu-describe-package (&optional button) "Describe the current package. @@ -3108,7 +3209,7 @@ The full list of keys can be viewed with \\[describe-mode]." "Return the priority of ARCHIVE. The archive priorities are specified in -`package-archive-priorities'. If not given there, the priority +`package-archive-priorities'. If not given there, the priority defaults to 0." (or (cdr (assoc archive package-archive-priorities)) 0)) @@ -3154,7 +3255,7 @@ Implementation of `package-menu-mark-upgrades'." (setq package-menu--mark-upgrades-pending nil) (let ((upgrades (package-menu--find-upgrades))) (if (null upgrades) - (message "No packages to upgrade.") + (message "No packages to upgrade") (widen) (save-excursion (goto-char (point-min)) @@ -3167,9 +3268,9 @@ Implementation of `package-menu-mark-upgrades'." (package-menu-mark-install)) (t (package-menu-mark-delete)))))) - (message "%d package%s marked for upgrading." - (length upgrades) - (if (= (length upgrades) 1) "" "s"))))) + (message "Packages marked for upgrading: %d" + (length upgrades))))) + (defun package-menu-mark-upgrades () "Mark all upgradable packages in the Package Menu. @@ -3192,17 +3293,12 @@ immediately." PACKAGES is a list of `package-desc' objects. Formats the returned string to be usable in a minibuffer prompt (see `package-menu--prompt-transaction-p')." - (cond - ;; None - ((not packages) "") - ;; More than 1 - ((cdr packages) - (format "these %d packages (%s)" - (length packages) - (mapconcat #'package-desc-full-name packages ", "))) - ;; Exactly 1 - (t (format-message "package `%s'" - (package-desc-full-name (car packages)))))) + ;; The case where `package' is empty is handled in + ;; `package-menu--prompt-transaction-p' below. + (format "%d (%s)" + (length packages) + (mapconcat #'package-desc-full-name packages " "))) + (defun package-menu--prompt-transaction-p (delete install upgrade) "Prompt the user about DELETE, INSTALL, and UPGRADE. @@ -3210,16 +3306,14 @@ DELETE, INSTALL, and UPGRADE are lists of `package-desc' objects. Either may be nil, but not all." (y-or-n-p (concat - (when delete "Delete ") - (package-menu--list-to-prompt delete) - (when (and delete install) - (if upgrade "; " "; and ")) - (when install "Install ") - (package-menu--list-to-prompt install) - (when (and upgrade (or install delete)) "; and ") - (when upgrade "Upgrade ") - (package-menu--list-to-prompt upgrade) - "? "))) + (when delete + (format "Packages to delete: %s. " (package-menu--list-to-prompt delete))) + (when install + (format "Packages to install: %s. " (package-menu--list-to-prompt install))) + (when upgrade + (format "Packages to upgrade: %s. " (package-menu--list-to-prompt upgrade))) + "Proceed? "))) + (defun package-menu--partition-transaction (install delete) "Return an alist describing an INSTALL DELETE transaction. @@ -3255,7 +3349,7 @@ objects removed." (redisplay 'force) (dolist (elt (package--sort-by-dependence delete-list)) (condition-case-unless-debug err - (let ((inhibit-message package-menu-async)) + (let ((inhibit-message (or inhibit-message package-menu-async))) (package-delete elt nil 'nosave)) (error (message "Error trying to delete `%s': %S" (package-desc-full-name elt) @@ -3303,25 +3397,24 @@ Optional argument NOQUERY non-nil means do not ask the user to confirm." (when (or noquery (package-menu--prompt-transaction-p .delete .install .upgrade)) (let ((message-template - (concat "Package menu: Operation %s [" - (when .delete (format "Delet__ %s" (length .delete))) - (when (and .delete .install) "; ") - (when .install (format "Install__ %s" (length .install))) - (when (and .upgrade (or .install .delete)) "; ") - (when .upgrade (format "Upgrad__ %s" (length .upgrade))) + (concat "[ " + (when .delete + (format "Delete %d " (length .delete))) + (when .install + (format "Install %d " (length .install))) + (when .upgrade + (format "Upgrade %d " (length .upgrade))) "]"))) - (message (replace-regexp-in-string "__" "ing" message-template) "started") + (message "Operation %s started" message-template) ;; Packages being upgraded are not marked as selected. (package--update-selected-packages .install .delete) (package-menu--perform-transaction install-list delete-list) (when package-selected-packages (if-let* ((removable (package--removable-packages))) - (message "Package menu: Operation finished. %d packages %s" - (length removable) - (substitute-command-keys - "are no longer needed, type `\\[package-autoremove]' to remove them")) - (message (replace-regexp-in-string "__" "ed" message-template) - "finished")))))))) + (message "Operation finished. Packages that are no longer needed: %d. Type `%s' to remove them" + (length removable) + (substitute-command-keys "\\[package-autoremove]")) + (message "Operation %s finished" message-template)))))))) (defun package-menu--version-predicate (A B) (let ((vA (or (aref (cadr A) 1) '(0))) @@ -3388,11 +3481,10 @@ Store this list in `package-menu--new-package-list'." (defun package-menu--find-and-notify-upgrades () "Notify the user of upgradable packages." (when-let* ((upgrades (package-menu--find-upgrades))) - (message "%d package%s can be upgraded; type `%s' to mark %s for upgrading." - (length upgrades) - (if (= (length upgrades) 1) "" "s") - (substitute-command-keys "\\[package-menu-mark-upgrades]") - (if (= (length upgrades) 1) "it" "them")))) + (message "Packages that can be upgraded: %d; type `%s' to mark for upgrading." + (length upgrades) + (substitute-command-keys "\\[package-menu-mark-upgrades]")))) + (defun package-menu--post-refresh () "If there's a *Packages* buffer, revert it and check for new packages and upgrades. @@ -3488,10 +3580,16 @@ shown." (defun package-menu-filter (keyword) "Filter the *Packages* buffer. Show only those items that relate to the specified KEYWORD. + KEYWORD can be a string or a list of strings. If it is a list, a package will be displayed if it matches any of the keywords. Interactively, it is a list of strings separated by commas. +KEYWORD can also be used to filter by status or archive name by +using keywords like \"arc:gnu\" and \"status:available\". +Statuses available include \"incompat\", \"available\", +\"built-in\" and \"installed\". + To restore the full package list, type `q'." (interactive (list (completing-read-multiple @@ -3507,6 +3605,149 @@ The list is displayed in a buffer named `*Packages*'." (interactive) (list-packages t)) +;;;###autoload +(defun package-get-version () + "Return the version number of the package in which this is used. +Assumes it is used from an Elisp file placed inside the top-level directory +of an installed ELPA package. +The return value is a string (or nil in case we can't find it)." + ;; In a sense, this is a lie, but it does just what we want: precompute + ;; the version at compile time and hardcodes it into the .elc file! + (declare (pure t)) + ;; Hack alert! + (let ((file + (or (if (boundp 'byte-compile-current-file) byte-compile-current-file) + load-file-name + buffer-file-name))) + (cond + ((null file) nil) + ;; Packages are normally installed into directories named "<pkg>-<vers>", + ;; so get the version number from there. + ((string-match "/[^/]+-\\([0-9]\\(?:[0-9.]\\|pre\\|beta\\|alpha\\|snapshot\\)+\\)/[^/]+\\'" file) + (match-string 1 file)) + ;; For packages run straight from the an elpa.git clone, there's no + ;; "-<vers>" in the directory name, so we have to fetch the version + ;; the hard way. + (t + (let* ((pkgdir (file-name-directory file)) + (pkgname (file-name-nondirectory (directory-file-name pkgdir))) + (mainfile (expand-file-name (concat pkgname ".el") pkgdir))) + (when (file-readable-p mainfile) + (require 'lisp-mnt) + (with-temp-buffer + (insert-file-contents mainfile) + (or (lm-header "package-version") + (lm-header "version"))))))))) + +;;;; Quickstart: precompute activation actions for faster start up. + +;; Activating packages via `package-initialize' is costly: for N installed +;; packages, it needs to read all N <pkg>-pkg.el files first to decide +;; which packages to activate, and then again N <pkg>-autoloads.el files. +;; To speed this up, we precompute a mega-autoloads file which is the +;; concatenation of all those <pkg>-autoloads.el, so we can activate +;; all packages by loading this one file (and hence without initializing +;; package.el). + +;; Other than speeding things up, this also offers a bootstrap feature: +;; it lets us activate packages according to `package-load-list' and +;; `package-user-dir' even before those vars are set. + +(defcustom package-quickstart nil + "Precompute activation actions to speed up startup. +This requires the use of `package-quickstart-refresh' every time the +activations need to be changed, such as when `package-load-list' is modified." + :type 'boolean + :version "27.1") + +(defcustom package-quickstart-file + (locate-user-emacs-file "package-quickstart.el") + "Location of the file used to speed up activation of packages at startup." + :type 'file + :version "27.1") + +(defun package--quickstart-maybe-refresh () + (if package-quickstart + ;; FIXME: Delay refresh in case we're installing/deleting + ;; several packages! + (package-quickstart-refresh) + (delete-file package-quickstart-file))) + +(defun package-quickstart-refresh () + "(Re)Generate the `package-quickstart-file'." + (interactive) + (package-initialize 'no-activate) + (require 'info) + (let ((package--quickstart-pkgs ()) + ;; Pretend we haven't activated anything yet! + (package-activated-list ()) + ;; Make sure we can load this file without load-source-file-function. + (coding-system-for-write 'emacs-internal) + (Info-directory-list '(""))) + (dolist (elt package-alist) + (condition-case err + (package-activate (car elt)) + ;; Don't let failure of activation of a package arbitrarily stop + ;; activation of further packages. + (error (message "%s" (error-message-string err))))) + (setq package--quickstart-pkgs (nreverse package--quickstart-pkgs)) + (with-temp-file package-quickstart-file + (emacs-lisp-mode) ;For `syntax-ppss'. + (insert ";;; Quickstart file to activate all packages at startup -*- lexical-binding:t -*-\n") + (insert ";; ¡¡ This file is autogenerated by `package-quickstart-refresh', DO NOT EDIT !!\n\n") + (dolist (pkg package--quickstart-pkgs) + (let* ((file + ;; Prefer uncompiled files (and don't accept .so files). + (let ((load-suffixes '(".el" ".elc"))) + (locate-library (package--autoloads-file-name pkg)))) + (pfile (prin1-to-string file))) + (insert "(let ((load-file-name " pfile "))\n") + (insert-file-contents file) + ;; Fixup the special #$ reader form and throw away comments. + (while (re-search-forward "#\\$\\|^;\\(.*\n\\)" nil 'move) + (unless (nth 8 (syntax-ppss)) + (replace-match (if (match-end 1) "" pfile) t t))) + (unless (bolp) (insert "\n")) + (insert ")\n"))) + (pp `(setq package-activated-list + (append ',(mapcar #'package-desc-name package--quickstart-pkgs) + package-activated-list)) + (current-buffer)) + (let ((info-dirs (butlast Info-directory-list))) + (when info-dirs + (pp `(progn (require 'info) + (info-initialize) + (setq Info-directory-list + (append ',info-dirs Info-directory-list))) + (current-buffer)))) + ;; Use `\s' instead of a space character, so this code chunk is not + ;; mistaken for an actual file-local section of package.el. + (insert " +;; Local\sVariables: +;; version-control: never +;;\sno-byte-compile: t +;; no-update-autoloads: t +;; End: +")))) + +(defun package--imenu-prev-index-position-function () + "Move point to previous line in package-menu buffer. +This function is used as a value for +`imenu-prev-index-position-function'." + (unless (bobp) + (forward-line -1))) + +(defun package--imenu-extract-index-name-function () + "Return imenu name for line at point. +This function is used as a value for +`imenu-extract-index-name-function'. Point should be at the +beginning of the line." + (let ((package-desc (tabulated-list-get-id))) + (format "%s (%s): %s" + (package-desc-name package-desc) + (package-version-join (package-desc-version package-desc)) + (package-desc-summary package-desc)))) + (provide 'package) ;;; package.el ends here |