emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

feature/package+vc 04c4c578c7 3/4: Allow for packages to be installed di


From: Philip Kaludercic
Subject: feature/package+vc 04c4c578c7 3/4: Allow for packages to be installed directly from VCS
Date: Mon, 14 Feb 2022 09:00:19 -0500 (EST)

branch: feature/package+vc
commit 04c4c578c71cae77b3b782497808bb2321da3be1
Author: Philip Kaludercic <philipk@posteo.net>
Commit: Philip Kaludercic <philipk@posteo.net>

    Allow for packages to be installed directly from VCS
    
    Packages installed via package-fetch are of the kind 'source, and
    their extra properties may include a :upstream key (a list consisting
    of the VC backend, a remote repository, a branch and a path within the
    repository) and :rev key (indicating a specific revision to checkout).
    
    * package.el (package-devel-dir): Add new option.
    (package-desc): Allow an empty version string to be passed to
    package-desc-from-define.
    (package-desc-full-name): Handle source packages.
    (vc-working-revision): Declare function for package-devel-commit.
    (package-devel-commit): Add function.
    (package-load-descriptor): Detect and handle source packages.
    (package-load-all-descriptors): Use package-devel-dir.
    (vc-clone): Declare function for package-unpack.
    (package-unpack): Handle source packages.
    (package-generate-description-file): Handle source packages by
    ommiting a version number.
    (package-install-from-archive): Check if a package is a source
    package.
    (package-fetch): Add new command
    (package-desc-status): Check for source packages.
    (package--remove-hidden): Hide regular packages from the package list
    if a source package was installed.
    (package-status-from-source): Add new face.
    (package-menu--print-info-simple): Handle source packages.
    (package-menu-mark-delete): Allow deleting source packages.
    (package-menu--status-predicate): Sort source packages after
    dependencies but before unsigned packages.
    (package-menu-filter-by-status): Allow filtering by source packages.
---
 lisp/emacs-lisp/package.el | 261 +++++++++++++++++++++++++++++++++++----------
 1 file changed, 202 insertions(+), 59 deletions(-)

diff --git a/lisp/emacs-lisp/package.el b/lisp/emacs-lisp/package.el
index 6aa82e576d..c3f6174c19 100644
--- a/lisp/emacs-lisp/package.el
+++ b/lisp/emacs-lisp/package.el
@@ -303,6 +303,17 @@ packages in `package-directory-list'."
   :risky t
   :version "24.1")
 
+(defcustom package-devel-dir (expand-file-name "devel" package-user-dir)
+  "Directory containing the user's Emacs Lisp package checkouts.
+The directory name should be absolute.
+Apart from this directory, Emacs also looks for system-wide
+packages in `package-directory-list'."
+  :type 'directory
+  :initialize #'custom-initialize-delay
+  :set-after '(package-user-dir)
+  :risky t
+  :version "29.1")
+
 ;;;###autoload
 (defcustom package-directory-list
   ;; Defaults are subdirs named "elpa" in the site-lisp dirs.
@@ -459,7 +470,7 @@ synchronously."
                  &rest rest-plist
                  &aux
                  (name (intern name-string))
-                 (version (version-to-list version-string))
+                 (version (and version-string (version-to-list 
version-string)))
                  (reqs (mapcar (lambda (elt)
                                  (list (car elt)
                                        (version-to-list (cadr elt))))
@@ -560,7 +571,9 @@ This is, approximately, the inverse of `version-to-list'.
 This is the name of the package with its version appended."
   (format "%s-%s"
           (package-desc-name pkg-desc)
-          (package-version-join (package-desc-version pkg-desc))))
+          (if (eq (package-desc-kind pkg-desc) 'source)
+              "devel"
+            (package-version-join (package-desc-version pkg-desc)))))
 
 (defun package-desc-suffix (pkg-desc)
   "Return file-name extension of package-desc object PKG-DESC.
@@ -666,6 +679,16 @@ are sorted with the highest version first."
               nil)))
       new-pkg-desc)))
 
+(declare-function vc-working-revision "vc" (file &optional backend))
+(defun package-devel-commit (pkg)
+  "Extract the commit of a development package PKG."
+  (cl-assert (eq (package-desc-kind pkg) 'source))
+  (require 'vc)
+  (cl-loop with dir = (package-desc-dir pkg)
+           for file in (directory-files dir t "\\.el\\'" t)
+           when (vc-working-revision file) return it
+           finally return "unknown"))
+
 (defun package-load-descriptor (pkg-dir)
   "Load the package description file in directory PKG-DIR.
 Create a new `package-desc' object, add it to `package-alist' and
@@ -681,6 +704,14 @@ return it."
                              (read (current-buffer)))
                             (error "Can't find define-package in %s" 
pkg-file))))
           (setf (package-desc-dir pkg-desc) pkg-dir)
+          (when (file-exists-p (expand-file-name
+                                (symbol-name (package-desc-name pkg-desc))
+                                package-devel-dir))
+            ;; XXX: This check seems dirty, there should be a better
+            ;; way to deduce if a package is in the devel directory.
+            (setf (package-desc-kind pkg-desc) 'source)
+            (push (cons :commit (package-devel-commit pkg-desc))
+                  (package-desc-extras pkg-desc)))
           (if (file-exists-p signed-file)
               (setf (package-desc-signed pkg-desc) t))
           pkg-desc)))))
@@ -694,13 +725,13 @@ controls which package subdirectories may be loaded.
 In each valid package subdirectory, this function loads the
 description file containing a call to `define-package', which
 updates `package-alist'."
-  (dolist (dir (cons package-user-dir package-directory-list))
+  (dolist (dir (cl-list* package-user-dir
+                         package-devel-dir
+                         package-directory-list))
     (when (file-directory-p dir)
-      (dolist (subdir (directory-files dir))
-        (unless (equal subdir "..")
-          (let ((pkg-dir (expand-file-name subdir dir)))
-            (when (file-directory-p pkg-dir)
-              (package-load-descriptor pkg-dir))))))))
+      (dolist (pkg-dir (directory-files dir t "^[^.]" t))
+        (when (file-directory-p pkg-dir)
+          (package-load-descriptor pkg-dir))))))
 
 (defun package--alist ()
   "Return `package-alist', after computing it if needed."
@@ -916,12 +947,51 @@ untar into a directory named DIR; otherwise, signal an 
error."
           (apply #'nconc
                  (mapcar (lambda (pair) (list (car pair) (cdr pair))) alist))))
 
+(declare-function vc-clone "vc" (backend remote &optional directory))
+
 (defun package-unpack (pkg-desc)
   "Install the contents of the current buffer as a package."
   (let* ((name (package-desc-name pkg-desc))
          (dirname (package-desc-full-name pkg-desc))
          (pkg-dir (expand-file-name dirname package-user-dir)))
     (pcase (package-desc-kind pkg-desc)
+      ('source
+       (setq pkg-dir (expand-file-name (symbol-name name) package-devel-dir))
+       (when (file-exists-p pkg-dir)
+         (if (and (called-interactively-p 'interactive)
+                  (yes-or-no-p "Overwrite previous checkout?"))
+             (delete-directory pkg-dir t)
+           (error "There already exists a checkout for %s" name)))
+       (pcase-let* ((attr (package-desc-extras pkg-desc))
+                    (`(,backend ,repo ,dir ,branch)
+                     (or (alist-get :upstream attr)
+                         (error "Source package has no repository"))))
+         (require 'vc)
+         (make-directory (file-name-directory (file-name-directory pkg-dir)) t)
+         (unless (setf (car (alist-get :upstream attr))
+                       (vc-clone backend repo pkg-dir))
+           (error "Failed to clone %s from %s" name repo))
+         (when-let ((rev (or (alist-get :rev attr) branch)))
+           (vc-retrieve-tag pkg-dir rev))
+         (when dir (setq pkg-dir (file-name-concat pkg-dir dir)))
+         ;; In case the package was installed directly from source, the
+         ;; dependency list wasn't know beforehand, and they might have
+         ;; to be installed explicitly.
+         (let (deps)
+           (dolist (file (directory-files pkg-dir t "\\.el\\'" t))
+             (with-temp-buffer
+               (insert-file-contents file)
+               (when-let* ((require-lines (lm-header-multiline 
"package-requires")))
+                 (thread-last
+                   (mapconcat #'identity require-lines " ")
+                   package-read-from-string
+                   package--prepare-dependencies
+                   (nconc deps)
+                   (setq deps)))))
+           (dolist (dep deps)
+             (cl-callf version-to-list (cadr dep)))
+           (package-download-transaction
+            (package-compute-transaction nil (delete-dups deps))))))
       ('dir
        (make-directory pkg-dir t)
        (let ((file-list
@@ -935,7 +1005,7 @@ untar into a directory named DIR; otherwise, signal an 
error."
          ;; indistinguishable from a `tar' or a `single'. Let's make
          ;; things simple by ensuring we're one of them.
          (setf (package-desc-kind pkg-desc)
-               (if (> (length file-list) 1) 'tar 'single))))
+               (if (length> file-list 1) 'tar 'single))))
       ('tar
        (make-directory package-user-dir t)
        (let* ((default-directory (file-name-as-directory package-user-dir)))
@@ -948,8 +1018,9 @@ untar into a directory named DIR; otherwise, signal an 
error."
     (package--make-autoloads-and-stuff pkg-desc pkg-dir)
     ;; Update package-alist.
     (let ((new-desc (package-load-descriptor pkg-dir)))
-      (unless (equal (package-desc-full-name new-desc)
-                     (package-desc-full-name pkg-desc))
+      (unless (or (equal (package-desc-full-name new-desc)
+                         (package-desc-full-name pkg-desc))
+                  (eq (package-desc-kind pkg-desc) 'source))
         (error "The retrieved package (`%s') doesn't match what the archive 
offered (`%s')"
                (package-desc-full-name new-desc) (package-desc-full-name 
pkg-desc)))
       ;; Activation has to be done before compilation, so that if we're
@@ -983,7 +1054,8 @@ untar into a directory named DIR; otherwise, signal an 
error."
          (nconc
           (list 'define-package
                 (symbol-name name)
-                (package-version-join (package-desc-version pkg-desc))
+                (and (not (eq (package-desc-kind pkg-desc) 'source))
+                     (package-version-join (package-desc-version pkg-desc)))
                 (package-desc-summary pkg-desc)
                 (let ((requires (package-desc-reqs pkg-desc)))
                   (list 'quote
@@ -1995,50 +2067,52 @@ if all the in-between dependencies are also in 
PACKAGE-LIST."
   (cdr (assoc (package-desc-archive desc) package-archives)))
 
 (defun package-install-from-archive (pkg-desc)
-  "Download and install a tar package defined by PKG-DESC."
+  "Download and install a package defined by PKG-DESC."
   ;; This won't happen, unless the archive is doing something wrong.
   (when (eq (package-desc-kind pkg-desc) 'dir)
     (error "Can't install directory package from archive"))
-  (let* ((location (package-archive-base pkg-desc))
-         (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))
-              (member (package-desc-archive pkg-desc)
-                      package-unsigned-archives))
-          ;; If we don't care about the signature, unpack and we're
-          ;; done.
-          (let ((save-silently t))
-            (package-unpack pkg-desc))
-        ;; If we care, check it and *then* write the file.
-        (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 ;FIXME: Just use the previous current-buffer.
-               (set-buffer-multibyte nil)
-               (cl-assert (not (multibyte-string-p content)))
-               (insert content)
-               (let ((save-silently t))
-                 (package-unpack pkg-desc)))
-             ;; Here the package has been installed successfully, mark it as
-             ;; signed if appropriate.
-             (when good-sigs
-               ;; Write out good signatures into NAME-VERSION.signed file.
-               (write-region (mapconcat #'epg-signature-to-string good-sigs 
"\n")
-                             nil
-                             (expand-file-name
-                              (concat (package-desc-full-name pkg-desc) 
".signed")
-                              package-user-dir)
-                             nil 'silent)
-               ;; 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))))
-                 (setf (package-desc-signed (car pkg-descs)) t))))))))))
+  (if (eq (package-desc-kind pkg-desc) 'source)
+      (package-unpack pkg-desc)
+    (let* ((location (package-archive-base pkg-desc))
+           (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))
+                (member (package-desc-archive pkg-desc)
+                        package-unsigned-archives))
+            ;; If we don't care about the signature, unpack and we're
+            ;; done.
+            (let ((save-silently t))
+              (package-unpack pkg-desc))
+          ;; If we care, check it and *then* write the file.
+          (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 ;FIXME: Just use the previous current-buffer.
+                 (set-buffer-multibyte nil)
+                 (cl-assert (not (multibyte-string-p content)))
+                 (insert content)
+                 (let ((save-silently t))
+                   (package-unpack pkg-desc)))
+               ;; Here the package has been installed successfully, mark it as
+               ;; signed if appropriate.
+               (when good-sigs
+                 ;; Write out good signatures into NAME-VERSION.signed file.
+                 (write-region (mapconcat #'epg-signature-to-string good-sigs 
"\n")
+                               nil
+                               (expand-file-name
+                                (concat (package-desc-full-name pkg-desc) 
".signed")
+                                package-user-dir)
+                               nil 'silent)
+                 ;; 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))))
+                   (setf (package-desc-signed (car pkg-descs)) t)))))))))))
 
 ;;;###autoload
 (defun package-installed-p (package &optional min-version)
@@ -2132,6 +2206,61 @@ to install it but still mark it as selected."
           (message  "Package `%s' installed." name))
       (message "`%s' is already installed" name))))
 
+;;;###autoload
+(defun package-fetch (name-or-url &optional name rev)
+  "Fetch the source of NAME-OR-URL.
+If NAME-OR-URL is a URL, then the package will be downloaded from
+the repository indicated by the URL.  The function will try to
+guess the name of the package using `file-name-base'.  This can
+be overridden by manually passing the optional NAME.  Otherwise
+NAME-OR-URL is taken to be a package name, and the package
+metadata will be consulted for the URL.  An explicit revision can
+be requested using REV."
+  (interactive
+   (progn
+     ;; Initialize the package system to get the list of package
+     ;; symbols for completion.
+     (package--archives-initialize)
+     (let* ((input (completing-read
+                    "Fetch package source (name or URL): "
+                    package-archive-contents))
+            (name (file-name-base input)))
+       (list input (intern (string-remove-prefix "emacs-" name))))))
+  (package--archives-initialize)
+  (package-install
+   (cond
+    ((and (stringp name-or-url)
+          (url-type (url-generic-parse-url name-or-url)))
+     (package-desc-create
+      :name (or name (intern (file-name-base name-or-url)))
+      :kind 'source
+      :extras `((:upstream . ,(list nil name-or-url nil nil))
+                (:rev . ,rev))))
+    ((when-let* ((desc (cadr (assoc name-or-url package-archive-contents
+                                    #'string=)))
+                 (spec (or (alist-get :vc (package-desc-extras desc))
+                           (user-error "Package has no VC header"))))
+       (unless (string-match
+                (rx bos
+                    (group (+ alnum))
+                    (+ blank) (group (+ (not blank)))
+                    (? (+ blank) (group (+ (not blank)))
+                       (? (+ blank) (group (+ (not blank)))))
+                    eos)
+                spec)
+         (user-error "Invalid repository specification %S" spec))
+       (package-desc-create
+        :name (if (stringp name-or-url)
+                  (intern name-or-url)
+                name-or-url)
+        :kind 'source
+        :extras `((:upstream . ,(list (intern (match-string 1 spec))
+                                      (match-string 2 spec)
+                                      (match-string 3 spec)
+                                      (match-string 4 spec)))
+                  (:rev . ,rev)))))
+    ((user-error "Unknown package to fetch: %s" name-or-url)))))
+
 (defun package-strip-rcs-id (str)
   "Strip RCS version ID from the version string STR.
 If the result looks like a dotted numeric version, return it.
@@ -2940,6 +3069,7 @@ of these dependencies, similar to the list returned by
          (signed (or (not package-list-unsigned)
                      (package-desc-signed pkg-desc))))
     (cond
+     ((eq (package-desc-kind pkg-desc) 'source) "source")
      ((eq dir 'builtin) "built-in")
      ((and lle (null held)) "disabled")
      ((stringp held)
@@ -3028,8 +3158,9 @@ to their archives."
           (if (not installed)
               filtered-by-priority
             (let ((ins-version (package-desc-version installed)))
-              (cl-remove-if (lambda (p) (version-list-= (package-desc-version 
p)
-                                                   ins-version))
+              (cl-remove-if (lambda (p) (or (version-list-= 
(package-desc-version p)
+                                                            ins-version)
+                                            (eq (package-desc-kind installed) 
'source)))
                             filtered-by-priority))))))))
 
 (defcustom package-hidden-regexps nil
@@ -3231,6 +3362,11 @@ Return (PKG-DESC [NAME VERSION STATUS DOC])."
   "Face used on the status and version of installed packages."
   :version "25.1")
 
+(defface package-status-from-source
+  '((t :inherit font-lock-negation-char-face))
+  "Face used on the status and version of installed packages."
+  :version "29.1")
+
 (defface package-status-dependency
   '((t :inherit package-status-installed))
   "Face used on the status and version of dependency packages."
@@ -3268,6 +3404,7 @@ Return (PKG-DESC [NAME VERSION STATUS DOC])."
                  ("held"      'package-status-held)
                  ("disabled"  'package-status-disabled)
                  ("installed" 'package-status-installed)
+                 ("source"    'package-status-from-source)
                  ("dependency" 'package-status-dependency)
                  ("unsigned"  'package-status-unsigned)
                  ("incompat"  'package-status-incompat)
@@ -3279,9 +3416,12 @@ Return (PKG-DESC [NAME VERSION STATUS DOC])."
              follow-link t
              package-desc ,pkg
              action package-menu-describe-package)
-            ,(propertize (package-version-join
-                          (package-desc-version pkg))
-                         'font-lock-face face)
+            ,(propertize
+              (if (eq (package-desc-kind pkg) 'source)
+                  (package-devel-commit pkg)
+                (package-version-join
+                 (package-desc-version pkg)))
+              'font-lock-face face)
             ,(propertize status 'font-lock-face face)
             ,@(if (cdr package-archives)
                   (list (propertize (or (package-desc-archive pkg) "")
@@ -3356,7 +3496,7 @@ If optional arg BUTTON is non-nil, describe its 
associated package."
   (interactive "p" package-menu-mode)
   (package--ensure-package-menu-mode)
   (if (member (package-menu-get-status)
-              '("installed" "dependency" "obsolete" "unsigned"))
+              '("installed" "source" "dependency" "obsolete" "unsigned"))
       (tabulated-list-put-tag "D" t)
     (forward-line)))
 
@@ -3674,6 +3814,8 @@ This is used for `tabulated-list-format' in 
`package-menu-mode'."
           ((string= sB "installed") nil)
           ((string= sA "dependency") t)
           ((string= sB "dependency") nil)
+          ((string= sA "source") t)
+          ((string= sB "source") nil)
           ((string= sA "unsigned") t)
           ((string= sB "unsigned") nil)
           ((string= sA "held") t)
@@ -3969,6 +4111,7 @@ packages."
                                         "held"
                                         "incompat"
                                         "installed"
+                                        "source"
                                         "new"
                                         "unsigned")))
                package-menu-mode)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]