aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilip Kaludercic2025-12-26 10:42:57 +0100
committerPhilip Kaludercic2026-01-10 12:38:05 +0100
commit881be95cddcab3cf37373678002c35334c177c97 (patch)
tree7cd20acea8384a3880ee7e63dba4b3de45cd872e
parentd35e705bdcd7e81122ba8357b7fed61075e50498 (diff)
downloademacs-881be95cddcab3cf37373678002c35334c177c97.tar.gz
emacs-881be95cddcab3cf37373678002c35334c177c97.zip
Allow reviewing packages before installaion
* lisp/emacs-lisp/package.el (package-review-policy) (package-review-directory, package-review-diff-command): Add new options. (package--review-p): Add new function to consult 'package-review-policy'. (package-review): Add new function. (package-unpack): Use new functions. (package-install-from-archive): Return package descriptors of installed packages. (package-download-transaction): Handle failure of a incomplete transaction. (package-install): Report if a package installation failed. (package-upgrade): Anticipate a failed package transaction by not deleting a package beforehand. (package-install-from-buffer): Handle the failure to download dependencies or a rejection during the actual package review. * doc/emacs/package.texi: Document feature. * etc/NEWS: Mention new feature.
-rw-r--r--doc/emacs/package.texi11
-rw-r--r--etc/NEWS8
-rw-r--r--lisp/emacs-lisp/package.el310
3 files changed, 272 insertions, 57 deletions
diff --git a/doc/emacs/package.texi b/doc/emacs/package.texi
index 2de3d25e7f9..e6432678c62 100644
--- a/doc/emacs/package.texi
+++ b/doc/emacs/package.texi
@@ -402,6 +402,17 @@ package is somehow unavailable, Emacs signals an error and stops
402installation.) A package's requirements list is shown in its help 402installation.) A package's requirements list is shown in its help
403buffer. 403buffer.
404 404
405@cindex review
406@vindex package-review-policy
407 If you are cautious when it comes to installing and upgrading packages
408from package archives, you can configure @code{package-review-policy} to
409give you a chance to review packages before installing them. By setting
410the user option to @code{t}, you get to review all packages (including
411dependencies), during which you can browse the source code, examine a
412diff between the downloaded package and a previous installation or read
413a changelog. You can also configure @code{package-review-policy} to
414selectively trust or distrust specific packages or archives.
415
405@cindex GNU ELPA 416@cindex GNU ELPA
406@cindex NonGNU ELPA 417@cindex NonGNU ELPA
407 By default, Emacs downloads packages from two archives: 418 By default, Emacs downloads packages from two archives:
diff --git a/etc/NEWS b/etc/NEWS
index 564479d2b1d..6df77525bf6 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2827,6 +2827,14 @@ packages.
2827--- 2827---
2828*** Uninstalling a package now removes its directory from 'load-path'. 2828*** Uninstalling a package now removes its directory from 'load-path'.
2829 2829
2830+++
2831*** Packages can be reviewed before installation or upgrade.
2832The user option 'package-review-policy' can configure which packages
2833the user should be allowed to review before any processing takes place.
2834The package review can include reading the downloaded source code,
2835presenting a diff between the downloaded code and a previous
2836installation or displaying a changelog.
2837
2830** Rcirc 2838** Rcirc
2831 2839
2832+++ 2840+++
diff --git a/lisp/emacs-lisp/package.el b/lisp/emacs-lisp/package.el
index bd5bee0a9ca..fccaf9f9f3e 100644
--- a/lisp/emacs-lisp/package.el
+++ b/lisp/emacs-lisp/package.el
@@ -669,40 +669,201 @@ untar into a directory named DIR; otherwise, signal an error."
669 (apply #'nconc 669 (apply #'nconc
670 (mapcar (lambda (pair) (list (car pair) (cdr pair))) alist)))) 670 (mapcar (lambda (pair) (list (car pair) (cdr pair))) alist))))
671 671
672(defcustom package-review-policy nil
673 "Policy to review incoming packages before installing them.
674Reviewing a package allows you to read the source code without
675installing anything, compare it to previous installations of the package
676and read the changelog. The default value of nil will install packages
677without any additional prompts, while t reviews all packages. By
678setting this user option to a list you can also selectively list what
679packages and archives to review. For the former, an entry of the
680form (archive STRING) will review all packages form the archive
681STRING (see `package-archives'), and an entry of the form (package
682SYMBOL) will review package who's name matches SYMBOL. By prefixing the
683list with a symbol `not' the rules are inverted."
684 :type
685 (let ((choice '(choice :tag "Review all packages form archive"
686 (cons (const archive) (string :tag "Archive name"))
687 (cons (const package) (symbol :tag "Package name")))))
688 `(choice
689 (const :tag "Review all packages" t)
690 (repeat :tag "Review these specific packages and archives" ,choice)
691 (cons :tag "Review the complement of these packages and archives"
692 (const not) (repeat ,choice))))
693 :risky t
694 :version "31.1")
695
696(defcustom package-review-directory temporary-file-directory
697 "Directory to unpack packages for review.
698The value of this user option is used to rebind the variable
699`temporary-file-directory'. The directory doesn't have to exist. If
700that is the case, Emacs creates the directory for you. You can
701therefore set the option to
702
703 (setopt package-review-directory (expand-file-name \"emacs\" (xdg-cache-home)))
704
705if you wish to have Emacs unpack the packages in your home directory, in
706case you are concerned about moving files between file systems."
707 :type 'directory
708 :version "31.1")
709
710(defcustom package-review-diff-command
711 (cons diff-command
712 '("-u" ;unified patch formatting
713 "-N" ;treat absent files as empty
714 "-x" "'*.elc'" ;ignore byte compiled files
715 "-x" "'*-autoloads.el'" ;ignore the autoloads file
716 "-x" "'*-pkg.el'" ;ignore the package description
717 "-x" "'*.info'" ;ignore compiled Info files
718 ))
719 "Configuration how `package-review' should generate a Diff.
720The structure of the value must be (COMMAND . SWITCHES), where
721`diff-command' is rebound to be COMMAND and SWITCHES are passed to
722`diff' as the SWITCHES argument if the user selects a diff-related
723option during review."
724 :type '(cons (string :tag "Diff command")
725 (repeat :tag "Diff arguments" string))
726 :version "31.1")
727
728(defun package--review-p (pkg-desc)
729 "Return non-nil if upgrading PKG-DESC requires a review.
730This package consults `package-review-policy' to determine if the user
731wants to review the package prior to installation. See `package-review'."
732 (let ((archive (package-desc-archive pkg-desc))
733 (name (package-desc-name pkg-desc)))
734 (pcase-exhaustive package-review-policy
735 ((and (pred listp) list)
736 (xor (any (lambda (ent)
737 (pcase ent
738 ((or `(archive . ,(pred (equal archive)))
739 `(package . ,(pred (eq name))))
740 t)
741 (_ nil)))
742 (if (eq (car list) 'not) (cdr list) list))
743 (eq (car list) 'not)))
744 ('t t))))
745
746
747(declare-function mail-text "sendmail" ())
748(declare-function message-goto-body "message" (&optional interactive))
749(declare-function diff-no-select "diff" (old new &optional switches no-async buf))
750
751(defun package-review (pkg-desc pkg-dir old-desc)
752 "Review the installation of PKG-DESC.
753PKG-DIR is the directory where the downloaded source of PKG-DIR have
754been downloaded. OLD-DESC is either a `package-desc' object of the
755previous installation or nil, if there is no prior installation. If the
756review fails, the function throws a symbol `review-failed' with PKG-DESC
757attached."
758 (let ((news (let* ((pkg-dir (package-desc-dir pkg-desc))
759 (file (expand-file-name "news" pkg-dir)))
760 (and (file-regular-p file)
761 (file-readable-p file)
762 file)))
763 (enable-recursive-minibuffers t)
764 (diff-command (car package-review-diff-command)))
765 (while (pcase-exhaustive
766 (car (read-multiple-choice
767 (format "Install \"%s\"?" (package-desc-name pkg-desc))
768 `((?y "yes" "Proceed with installation")
769 (?n "no" "Abort installation")
770 ,@(and old-desc '((?d "diff" "Show the installation diff")
771 (?m "mail" "Send an email to the maintainers")))
772 ,@(and news '((?c "changelog" "Show the changelog")))
773 (?b "browse" "Browse the source"))))
774 (?y nil)
775 (?n
776 (delete-directory pkg-dir t)
777 (throw 'review-failed pkg-desc))
778 (?d
779 (diff (package-desc-dir old-desc) pkg-dir (cdr package-review-diff-command) t)
780 t)
781 (?m
782 (require 'diff) ;for `diff-no-select'
783 (with-temp-buffer
784 (diff-no-select
785 (package-desc-dir old-desc) pkg-dir
786 (cdr package-review-diff-command)
787 t (current-buffer))
788 ;; delete sentinel message
789 (goto-char (point-max))
790 (forward-line -2)
791 (narrow-to-region (point-min) (point))
792 ;; prepare mail buffer
793 (let ((tmp-buf (current-buffer)))
794 (compose-mail (with-demoted-errors "Failed to find maintainers: %S"
795 (package-maintainers pkg-desc)))
796 (pcase mail-user-agent
797 ('sendmail-user-agent (mail-text))
798 (_ (message-goto-body)))
799 (insert-buffer-substring tmp-buf)))
800 t)
801 (?c
802 (view-file news)
803 t)
804 (?b
805 (dired pkg-dir "-R") ;FIXME: Is recursive dired portable?
806 t)))))
807
672(declare-function dired-get-marked-files "dired") 808(declare-function dired-get-marked-files "dired")
673 809
674(defun package-unpack (pkg-desc) 810(defun package-unpack (pkg-desc)
675 "Install the contents of the current buffer as a package." 811 "Install the contents of the current buffer as a package.
812The argument PKG-DESC contains metadata of the yet to be installed
813package. The function returns a `package-desc' object of the actually
814installed package."
676 (let* ((name (package-desc-name pkg-desc)) 815 (let* ((name (package-desc-name pkg-desc))
677 (dirname (package-desc-full-name pkg-desc)) 816 (full-name (package-desc-full-name pkg-desc))
678 (pkg-dir (expand-file-name dirname package-user-dir))) 817 (pkg-dir (expand-file-name full-name package-user-dir))
679 (pcase (package-desc-kind pkg-desc) 818 (review-p (package--review-p pkg-desc))
680 ('dir 819 (unpack-dir (if review-p
681 (make-directory pkg-dir t) 820 (let ((temporary-file-directory package-review-directory))
682 (let ((file-list 821 (make-directory temporary-file-directory t) ;ensure existence
683 (or (and (derived-mode-p 'dired-mode) 822 (expand-file-name
684 (dired-get-marked-files nil 'marked)) 823 full-name
685 (directory-files-recursively default-directory "" nil)))) 824 (make-temp-file "emacs-package-review-" t)))
686 (dolist (source-file file-list) 825 pkg-dir))
687 (let ((target (expand-file-name 826 (old-desc (package--get-activatable-pkg name)))
688 (file-relative-name source-file default-directory) 827 (make-directory unpack-dir t)
689 pkg-dir))) 828 (save-window-excursion
690 (make-directory (file-name-directory target) t) 829 (pcase (package-desc-kind pkg-desc)
691 (copy-file source-file target t))) 830 ('dir
692 ;; Now that the files have been installed, this package is 831 (let ((file-list
693 ;; indistinguishable from a `tar' or a `single'. Let's make 832 (or (and (derived-mode-p 'dired-mode)
694 ;; things simple by ensuring we're one of them. 833 (dired-get-marked-files nil 'marked))
695 (setf (package-desc-kind pkg-desc) 834 (directory-files-recursively default-directory "" nil))))
696 (if (length> file-list 1) 'tar 'single)))) 835 (dolist (source-file file-list)
697 ('tar 836 (let ((target (expand-file-name
698 (make-directory package-user-dir t) 837 (file-relative-name source-file default-directory)
699 (let* ((default-directory (file-name-as-directory package-user-dir))) 838 unpack-dir)))
700 (package-untar-buffer dirname))) 839 (make-directory (file-name-directory target) t)
701 ('single 840 (copy-file source-file target t)))
702 (let ((el-file (expand-file-name (format "%s.el" name) pkg-dir))) 841 ;; Now that the files have been installed, this package is
703 (make-directory pkg-dir t) 842 ;; indistinguishable from a `tar' or a `single'. Let's make
704 (package--write-file-no-coding el-file))) 843 ;; things simple by ensuring we're one of them.
705 (kind (error "Unknown package kind: %S" kind))) 844 (setf (package-desc-kind pkg-desc)
845 (if (length> file-list 1) 'tar 'single))))
846 ('tar
847 (let ((default-directory (file-name-directory unpack-dir)))
848 (package-untar-buffer (file-name-nondirectory unpack-dir))))
849 ('single
850 (let ((el-file (expand-file-name (format "%s.el" name) unpack-dir)))
851 (package--write-file-no-coding el-file)))
852 (kind (error "Unknown package kind: %S" kind))))
853
854 ;; check if the user wants to review this package
855 (when review-p
856 (unwind-protect
857 (progn
858 (save-window-excursion
859 (package-review pkg-desc unpack-dir old-desc))
860 (make-directory package-user-dir t)
861 (rename-file unpack-dir pkg-dir))
862 (let ((temp-dir (file-name-directory unpack-dir)))
863 (when (file-directory-p temp-dir)
864 (delete-directory temp-dir t)))))
865 (cl-assert (file-directory-p pkg-dir))
866
706 (package--make-autoloads-and-stuff pkg-desc pkg-dir) 867 (package--make-autoloads-and-stuff pkg-desc pkg-dir)
707 ;; Update package-alist. 868 ;; Update package-alist.
708 (let ((new-desc (package-load-descriptor pkg-dir))) 869 (let ((new-desc (package-load-descriptor pkg-dir)))
@@ -722,8 +883,9 @@ untar into a directory named DIR; otherwise, signal an error."
722 (package--native-compile-async new-desc)) 883 (package--native-compile-async new-desc))
723 ;; After compilation, load again any files loaded by 884 ;; After compilation, load again any files loaded by
724 ;; `activate-1', so that we use the byte-compiled definitions. 885 ;; `activate-1', so that we use the byte-compiled definitions.
725 (package--reload-previously-loaded new-desc))) 886 (package--reload-previously-loaded new-desc))
726 pkg-dir)) 887
888 new-desc)))
727 889
728(defun package-generate-description-file (pkg-desc pkg-file) 890(defun package-generate-description-file (pkg-desc pkg-file)
729 "Create the foo-pkg.el file PKG-FILE for single-file package PKG-DESC." 891 "Create the foo-pkg.el file PKG-FILE for single-file package PKG-DESC."
@@ -1740,13 +1902,16 @@ if all the in-between dependencies are also in PACKAGE-LIST."
1740 (cdr (assoc (package-desc-archive desc) package-archives))) 1902 (cdr (assoc (package-desc-archive desc) package-archives)))
1741 1903
1742(defun package-install-from-archive (pkg-desc) 1904(defun package-install-from-archive (pkg-desc)
1743 "Download and install a package defined by PKG-DESC." 1905 "Download and install a package defined by PKG-DESC.
1906The function returns the new `package-desc' object of the installed
1907package."
1744 ;; This won't happen, unless the archive is doing something wrong. 1908 ;; This won't happen, unless the archive is doing something wrong.
1745 (when (eq (package-desc-kind pkg-desc) 'dir) 1909 (when (eq (package-desc-kind pkg-desc) 'dir)
1746 (error "Can't install directory package from archive")) 1910 (error "Can't install directory package from archive"))
1747 (let* ((location (package-archive-base pkg-desc)) 1911 (let* ((location (package-archive-base pkg-desc))
1748 (file (concat (package-desc-full-name pkg-desc) 1912 (file (concat (package-desc-full-name pkg-desc)
1749 (package-desc-suffix pkg-desc)))) 1913 (package-desc-suffix pkg-desc)))
1914 new-desc)
1750 (package--with-response-buffer location :file file 1915 (package--with-response-buffer location :file file
1751 (if (or (not (package-check-signature)) 1916 (if (or (not (package-check-signature))
1752 (member (package-desc-archive pkg-desc) 1917 (member (package-desc-archive pkg-desc)
@@ -1754,7 +1919,7 @@ if all the in-between dependencies are also in PACKAGE-LIST."
1754 ;; If we don't care about the signature, unpack and we're 1919 ;; If we don't care about the signature, unpack and we're
1755 ;; done. 1920 ;; done.
1756 (let ((save-silently t)) 1921 (let ((save-silently t))
1757 (package-unpack pkg-desc)) 1922 (setq new-desc (package-unpack pkg-desc)))
1758 ;; If we care, check it and *then* write the file. 1923 ;; If we care, check it and *then* write the file.
1759 (let ((content (buffer-string))) 1924 (let ((content (buffer-string)))
1760 (package--check-signature 1925 (package--check-signature
@@ -1767,7 +1932,7 @@ if all the in-between dependencies are also in PACKAGE-LIST."
1767 (cl-assert (not (multibyte-string-p content))) 1932 (cl-assert (not (multibyte-string-p content)))
1768 (insert content) 1933 (insert content)
1769 (let ((save-silently t)) 1934 (let ((save-silently t))
1770 (package-unpack pkg-desc))) 1935 (setq new-desc (package-unpack pkg-desc))))
1771 ;; Here the package has been installed successfully, mark it as 1936 ;; Here the package has been installed successfully, mark it as
1772 ;; signed if appropriate. 1937 ;; signed if appropriate.
1773 (when good-sigs 1938 (when good-sigs
@@ -1798,15 +1963,27 @@ if all the in-between dependencies are also in PACKAGE-LIST."
1798 (unless (save-excursion 1963 (unless (save-excursion
1799 (goto-char (point-min)) 1964 (goto-char (point-min))
1800 (looking-at-p "[[:space:]]*\\'")) 1965 (looking-at-p "[[:space:]]*\\'"))
1801 (write-region nil nil readme))))))) 1966 (write-region nil nil readme)))))
1967 new-desc))
1802 1968
1803(defun package-download-transaction (packages) 1969(defun package-download-transaction (packages)
1804 "Download and install all the packages in PACKAGES. 1970 "Download and install all the packages in PACKAGES.
1805PACKAGES should be a list of `package-desc'. 1971PACKAGES should be a list of `package-desc'. This function assumes that
1806This function assumes that all package requirements in 1972all package requirements in PACKAGES are satisfied, i.e. that PACKAGES
1807PACKAGES are satisfied, i.e. that PACKAGES is computed 1973is computed using `package-compute-transaction'. The function returns a
1808using `package-compute-transaction'." 1974list of `package-desc' objects that have been installed, or nil if the
1809 (mapc #'package-install-from-archive packages)) 1975transaction had no effect."
1976 (let* ((installed '())
1977 (pkg-desc (catch 'review-failed
1978 (dolist (pkg-desc packages nil)
1979 (push (package-install-from-archive pkg-desc)
1980 installed)))))
1981 (if pkg-desc
1982 (progn
1983 (message "Rejected `%s', reverting transaction." (package-desc-name pkg-desc))
1984 (mapc #'package-delete installed)
1985 nil)
1986 installed)))
1810 1987
1811(defun package--archives-initialize () 1988(defun package--archives-initialize ()
1812 "Make sure the list of installed and remote packages are initialized." 1989 "Make sure the list of installed and remote packages are initialized."
@@ -1855,6 +2032,10 @@ had been enabled."
1855 nil 2032 nil
1856 'interactive))) 2033 'interactive)))
1857 (cl-check-type pkg (or symbol package-desc)) 2034 (cl-check-type pkg (or symbol package-desc))
2035 (when (or (and package-install-upgrade-built-in
2036 (package--active-built-in-p pkg))
2037 (package-installed-p pkg))
2038 (user-error "Package is already installed"))
1858 (package--archives-initialize) 2039 (package--archives-initialize)
1859 (add-hook 'post-command-hook #'package-menu--post-refresh) 2040 (add-hook 'post-command-hook #'package-menu--post-refresh)
1860 (let ((name (if (package-desc-p pkg) 2041 (let ((name (if (package-desc-p pkg)
@@ -1877,11 +2058,11 @@ had been enabled."
1877 (package-compute-transaction (list pkg) 2058 (package-compute-transaction (list pkg)
1878 (package-desc-reqs pkg))) 2059 (package-desc-reqs pkg)))
1879 (package-compute-transaction () (list (list pkg)))))) 2060 (package-compute-transaction () (list (list pkg))))))
1880 (progn 2061 (if (package-download-transaction transaction)
1881 (package-download-transaction transaction) 2062 (progn
1882 (package--quickstart-maybe-refresh) 2063 (package--quickstart-maybe-refresh)
1883 (message "Package `%s' installed." name)))))) 2064 (message "Package `%s' installed" name))
1884 2065 (error "Package `%s' not installed" name))))))
1885 2066
1886(declare-function package-vc-upgrade "package-vc" (pkg)) 2067(declare-function package-vc-upgrade "package-vc" (pkg))
1887 2068
@@ -1900,12 +2081,17 @@ NAME should be a symbol."
1900 ;; `pkg-desc' will be nil when the package is an "active built-in". 2081 ;; `pkg-desc' will be nil when the package is an "active built-in".
1901 (if (and pkg-desc (package-vc-p pkg-desc)) 2082 (if (and pkg-desc (package-vc-p pkg-desc))
1902 (package-vc-upgrade pkg-desc) 2083 (package-vc-upgrade pkg-desc)
1903 (when pkg-desc 2084 (let ((new-desc (cadr (assq name package-archive-contents))))
1904 (package-delete pkg-desc 'force 'dont-unselect)) 2085 (when (or (null new-desc)
1905 (package-install name 2086 (version-list-= (package-desc-version pkg-desc)
1906 ;; An active built-in has never been "selected" 2087 (package-desc-version new-desc)))
1907 ;; before. Mark it as installed explicitly. 2088 (user-error "Cannot upgrade `%s'" name))
1908 (and pkg-desc 'dont-select))))) 2089 (package-install new-desc
2090 ;; An active built-in has never been "selected"
2091 ;; before. Mark it as installed explicitly.
2092 (and pkg-desc 'dont-select))
2093 (when pkg-desc
2094 (package-delete pkg-desc 'force 'dont-unselect))))))
1909 2095
1910(defun package--upgradeable-packages (&optional include-builtins) 2096(defun package--upgradeable-packages (&optional include-builtins)
1911 ;; Initialize the package system to get the list of package 2097 ;; Initialize the package system to get the list of package
@@ -2040,10 +2226,20 @@ Downloads and installs required packages as needed."
2040 (name (package-desc-name pkg-desc))) 2226 (name (package-desc-name pkg-desc)))
2041 ;; Download and install the dependencies. 2227 ;; Download and install the dependencies.
2042 (let* ((requires (package-desc-reqs pkg-desc)) 2228 (let* ((requires (package-desc-reqs pkg-desc))
2043 (transaction (package-compute-transaction nil requires))) 2229 (transaction (package-compute-transaction nil requires))
2044 (package-download-transaction transaction)) 2230 (installed (package-download-transaction transaction)))
2045 ;; Install the package itself. 2231 (when (and (catch 'review-failed
2046 (package-unpack pkg-desc) 2232 ;; Install the package itself.
2233 (package-unpack pkg-desc)
2234 nil)
2235 (or (null transaction) installed))
2236 (mapc #'package-delete installed)
2237 (when installed
2238 (message "Review uninstalled dependencies: %s"
2239 (mapconcat #'package-desc-full-name
2240 installed
2241 ", ")))
2242 (user-error "Installation aborted")))
2047 (unless (package--user-selected-p name) 2243 (unless (package--user-selected-p name)
2048 (package--save-selected-packages 2244 (package--save-selected-packages
2049 (cons name package-selected-packages))) 2245 (cons name package-selected-packages)))