1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
|
;;; package-vc.el --- Manage packages from VC checkouts -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Philip Kaludercic <philipk@posteo.net>
;; Keywords: tools
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; While packages managed by package.el use tarballs for distributing
;; the source code, this extension allows for packages to be fetched
;; and updated directly from a version control system.
;;; TODO:
;; - Allow for automatic updating TODO
;; * Detect merge conflicts TODO
;; * Check if there are upstream changes TODO
;; - Allow finding revisions that bump the version tag TODO
;; * Allow for `package-vc-install' to use the version
;; of the package if already installed.
;; - Allow for ELPA specifications to be respected without TODO
;; endangering the user with arbitrary code execution
;;; Code:
(eval-when-compile (require 'rx))
(require 'package)
(require 'lisp-mnt)
(require 'vc)
(require 'seq)
(require 'xdg)
(defgroup package-vc nil
"Manage packages from VC checkouts."
:group 'package
:version "29.1")
(defcustom package-vc-heusitic-alist
`((,(rx bos "http" (? "s") "://"
(or (: (? "www.") "github.com"
"/" (+ (or alnum "-" "." "_"))
"/" (+ (or alnum "-" "." "_")))
(: "codeberg.org"
"/" (+ (or alnum "-" "." "_"))
"/" (+ (or alnum "-" "." "_")))
(: (? "www.") "gitlab" (+ "." (+ alnum))
"/" (+ (or alnum "-" "." "_"))
"/" (+ (or alnum "-" "." "_")))
(: "git.sr.ht"
"/~" (+ (or alnum "-" "." "_"))
"/" (+ (or alnum "-" "." "_")))
(: "git." (or "savannah" "sv") "." (? "non") "gnu.org/"
(or "r" "git") "/"
(+ (or alnum "-" "." "_")) (? "/")))
(or (? "/") ".git") eos)
. Git)
(,(rx bos "http" (? "s") "://"
(or (: "hg.sr.ht"
"/~" (+ (or alnum "-" "." "_"))
"/" (+ (or alnum "-" "." "_")))
(: "hg." (or "savannah" "sv") "." (? "non") "gnu.org/hgweb/"
(+ (or alnum "-" "." "_")) (? "/")))
eos)
. Hg)
(,(rx bos "http" (? "s") "://"
(or (: "bzr." (or "savannah" "sv") "." (? "non") "gnu.org/r/"
(+ (or alnum "-" "." "_")) (? "/")))
eos)
. Bzr))
"Heuristic mapping URL regular expressions to VC backends."
:type `(alist :key-type (regexp :tag "Regular expression matching URLs")
:value-type (choice :tag "VC Backend"
,@(mapcar (lambda (b) `(const ,b))
vc-handled-backends)))
:version "29.1")
(defcustom package-vc-repository-store
(expand-file-name "emacs/vc-packages" (xdg-data-home))
"Directory used by `package-vc-unpack' to store repositories."
:type 'directory
:version "29.1")
(defun package-vc-commit (pkg)
"Extract the commit of a development package PKG."
(cl-assert (package-vc-p pkg))
;; FIXME: vc should be extended to allow querying the commit of a
;; directory (as is possible when dealing with git repositores).
;; This should be a fallback option.
(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-vc-version (pkg)
"Extract the commit of a development package PKG."
(cl-assert (package-vc-p pkg))
(cl-loop with dir = (package-desc-dir pkg) ;FIXME: dir is nil
for file in (sort (directory-files dir t "\\.el\\'")
(lambda (s1 s2)
(< (length s1) (length s2))))
when (with-temp-buffer
(insert-file-contents file)
(package-strip-rcs-id
(or (lm-header "package-version")
(lm-header "version"))))
return it
finally return "0"))
(defun package-vc-generate-description-file (pkg-desc pkg-file)
"Generate a package description file for PKG-DESC.
The output is written out into PKG-FILE."
(let* ((name (package-desc-name pkg-desc)))
(let ((print-level nil)
(print-quoted t)
(print-length nil))
(write-region
(concat
";;; Generated package description from "
(replace-regexp-in-string
"-pkg\\.el\\'" ".el"
(file-name-nondirectory pkg-file))
" -*- no-byte-compile: t -*-\n"
(prin1-to-string
(nconc
(list 'define-package
(symbol-name name)
(cons 'vc (package-vc-version pkg-desc))
(package-desc-summary pkg-desc)
(let ((requires (package-desc-reqs pkg-desc)))
(list 'quote
;; Turn version lists into string form.
(mapcar
(lambda (elt)
(list (car elt)
(package-version-join (cadr elt))))
requires))))
(package--alist-to-plist-args
(package-desc-extras pkg-desc))))
"\n")
nil pkg-file nil 'silent))))
(defun package-vc-unpack-1 (pkg-desc pkg-dir)
"Install PKG-DESC that is already located in PKG-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))))
(let ((default-directory (file-name-as-directory pkg-dir))
(name (package-desc-name pkg-desc))
(pkg-file (expand-file-name (package--description-file pkg-dir) pkg-dir)))
;; Generate autoloads
(package-generate-autoloads name pkg-dir)
(vc-ignore (concat "/" (file-relative-name
(expand-file-name (format "%s-autoloads.el" name))
default-directory)))
;; Generate package file
(package-vc-generate-description-file pkg-desc pkg-file)
(vc-ignore (concat "/" (file-relative-name pkg-file default-directory)))
;; Detect a manual
(when (executable-find "install-info")
;; Only proceed if we can find an unambiguous TeXinfo file
(let ((texi-files (directory-files pkg-dir t "\\.texi\\'"))
(dir-file (expand-file-name "dir" pkg-dir)))
(when (length= texi-files 1)
(call-process "install-info" nil nil nil
(concat "--dir=" dir-file)
(car texi-files)))
(vc-ignore "/dir"))))
;; Update package-alist.
(let ((new-desc (package-load-descriptor pkg-dir)))
;; Activation has to be done before compilation, so that if we're
;; upgrading and macros have changed we load the new definitions
;; before compiling.
(when (package-activate-1 new-desc :reload :deps)
;; FIXME: Compilation should be done as a separate, optional, step.
;; E.g. for multi-package installs, we should first install all packages
;; and then compile them.
(package--compile new-desc)
(when package-native-compile
(package--native-compile-async new-desc))
;; After compilation, load again any files loaded by
;; `activate-1', so that we use the byte-compiled definitions.
(package--reload-previously-loaded new-desc)))
;; Mark package as selected
(package--save-selected-packages
(cons (package-desc-name pkg-desc)
package-selected-packages)))
(defun package-vc-unpack (pkg-desc)
"Install the package described by PKG-DESC."
(let* ((name (package-desc-name pkg-desc))
(dirname (package-desc-full-name pkg-desc))
(pkg-dir (expand-file-name dirname package-user-dir)))
(setf (package-desc-dir pkg-desc) pkg-dir)
(when (file-exists-p pkg-dir)
(if (yes-or-no-p "Overwrite previous checkout?")
(package--delete-directory pkg-dir pkg-desc)
(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")))
(repo-dir
(if (null dir)
pkg-dir
(unless (file-exists-p package-vc-repository-store)
(make-directory package-vc-repository-store t))
(file-name-concat
package-vc-repository-store
;; FIXME: We aren't sure this directory
;; will be unique, but we can try other
;; names to avoid an unnecessary error.
(file-name-base repo)))))
;; Clone the repository into `repo-dir'.
(make-directory (file-name-directory repo-dir) t)
(unless (setf (car (alist-get :upstream attr))
(vc-clone backend repo repo-dir))
(error "Failed to clone %s from %s" name repo))
(unless (eq pkg-dir repo-dir)
;; Link from the right position in `repo-dir' to the package
;; directory in the ELPA store.
(make-symbolic-link (file-name-concat repo-dir dir) pkg-dir))
(when-let ((default-directory repo-dir)
(rev (or (alist-get :rev attr) branch)))
(vc-retrieve-tag pkg-dir rev)))
(package-vc-unpack-1 pkg-desc pkg-dir)))
(defun package-vc-sourced-packages-list ()
"Generate a list of packages with VC data."
(seq-filter
(lambda (pkg)
(let ((extras (package-desc-extras (cadr pkg))))
(or (alist-get :vc extras)
;; If we have no explicit VC data, we can try a kind of
;; heuristic and use the URL header, that might already be
;; pointing towards a repository, and use that as a backup
(and-let* ((url (alist-get :url extras))
(backend (alist-get url package-vc-heusitic-alist
nil nil #'string-match-p)))
(setf (alist-get :vc (package-desc-extras (cadr pkg)))
(list backend url))
t))))
package-archive-contents))
(defun package-vc-update (pkg-desc)
"Attempt to update the packager PKG-DESC."
(let* ((default-directory (package-desc-dir pkg-desc))
(ret (with-demoted-errors "Error during package update: %S"
(vc-pull)))
(buf (cond
((processp ret) (process-buffer ret))
((bufferp ret) ret))))
(if buf
(with-current-buffer buf
(vc-run-delayed
(package-vc-unpack-1 pkg-desc default-directory)))
(package-vc-unpack-1 pkg-desc default-directory))))
;;;###autoload
(defun package-vc-install (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* ((packages (package-vc-sourced-packages-list))
(input (completing-read
"Fetch package source (name or URL): " packages))
(name (file-name-base input)))
(list input (intern (string-remove-prefix "emacs-" name))))))
(package--archives-initialize)
(package-vc-unpack
(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 'vc
:extras `((:upstream . ,(list nil name-or-url nil nil))
(:rev . ,rev))))
((when-let* ((desc (cadr (assoc name-or-url package-archive-contents
#'string=)))
(upstream (or (alist-get :vc (package-desc-extras desc))
(user-error "Package has no VC data"))))
(package-desc-create
:name (if (stringp name-or-url)
(intern name-or-url)
name-or-url)
:kind 'vc
:extras `((:upstream . ,upstream)
(:rev . ,rev)))))
((user-error "Unknown package to fetch: %s" name-or-url)))))
;;;###autoload
(defalias 'package-checkout #'package-vc-install)
(defun package-vc-link-directory (dir name)
"Install the package NAME in DIR by linking it into the ELPA directory.
If invoked interactively with a prefix argument, the user will be
prompted for the package NAME. Otherwise it will be inferred
from the base name of DIR."
(interactive (let ((dir (read-directory-name "Directory: ")))
(list dir
(if current-prefix-arg
(read-string "Package name: ")
(file-name-base (directory-file-name dir))))))
(unless (vc-responsible-backend dir)
(user-error "Directory %S is not under version control" dir))
(package--archives-initialize)
(let* ((name (file-name-base (directory-file-name dir)))
(pkg-dir (expand-file-name name package-user-dir)))
(make-symbolic-link dir pkg-dir)
(package-vc-unpack-1 (package-desc-create
:name (intern name)
:kind 'vc)
pkg-dir)))
(defun package-vc-refresh (pkg-desc)
"Refresh the installation for PKG-DESC."
(interactive (package-vc-read-pkg "Refresh package: "))
(package-vc-unpack-1 pkg-desc (package-desc-dir pkg-desc)))
(defun package-vc-read-pkg (prompt)
"Query for a source package description with PROMPT."
(cadr (assoc (completing-read
prompt
package-alist
(lambda (pkg) (package-vc-p (cadr pkg)))
t)
package-alist
#'string=)))
;;;###autoload
(defun package-vc-prepare-patch (pkg subject revisions)
"Send a patch to the maintainer of a package PKG.
SUBJECT and REVISIONS are used passed on to `vc-prepare-patch'.
PKG must be a package description."
(interactive
(list (package-vc-read-pkg "Package to prepare a patch for: ")
(and (not vc-prepare-patches-separately)
(read-string "Subject: " "[PATCH] " nil nil t))
(or (log-view-get-marked)
(vc-read-multiple-revisions "Revisions: "))))
(vc-prepare-patch (package-maintainers pkg t)
subject revisions))
(provide 'package-vc)
;;; package-vc.el ends here
|