package-x.el 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. ;;; package-x.el --- Package extras
  2. ;; Copyright (C) 2007-2017 Free Software Foundation, Inc.
  3. ;; Author: Tom Tromey <tromey@redhat.com>
  4. ;; Created: 10 Mar 2007
  5. ;; Keywords: tools
  6. ;; Package: package
  7. ;; This file is part of GNU Emacs.
  8. ;; GNU Emacs is free software: you can redistribute it and/or modify
  9. ;; it under the terms of the GNU General Public License as published by
  10. ;; the Free Software Foundation, either version 3 of the License, or
  11. ;; (at your option) any later version.
  12. ;; GNU Emacs is distributed in the hope that it will be useful,
  13. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;; GNU General Public License for more details.
  16. ;; You should have received a copy of the GNU General Public License
  17. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  18. ;;; Commentary:
  19. ;; This file currently contains parts of the package system that many
  20. ;; won't need, such as package uploading.
  21. ;; To upload to an archive, first set `package-archive-upload-base' to
  22. ;; some desired directory. For testing purposes, you can specify any
  23. ;; directory you want, but if you want the archive to be accessible to
  24. ;; others via http, this is typically a directory in the /var/www tree
  25. ;; (possibly one on a remote machine, accessed via Tramp).
  26. ;; Then call M-x package-upload-file, which prompts for a file to
  27. ;; upload. Alternatively, M-x package-upload-buffer uploads the
  28. ;; current buffer, if it's visiting a package file.
  29. ;; Once a package is uploaded, users can access it via the Package
  30. ;; Menu, by adding the archive to `package-archives'.
  31. ;;; Code:
  32. (require 'package)
  33. (defvar gnus-article-buffer)
  34. (defcustom package-archive-upload-base "/path/to/archive"
  35. "The base location of the archive to which packages are uploaded.
  36. This should be an absolute directory name. If the archive is on
  37. another machine, you may specify a remote name in the usual way,
  38. e.g. \"/ssh:foo@example.com:/var/www/packages/\".
  39. See Info node `(emacs)Remote Files'.
  40. Unlike `package-archives', you can't specify a HTTP URL."
  41. :type 'directory
  42. :group 'package
  43. :version "24.1")
  44. (defvar package-update-news-on-upload nil
  45. "Whether uploading a package should also update NEWS and RSS feeds.")
  46. (defun package--encode (string)
  47. "Encode a string by replacing some characters with XML entities."
  48. ;; We need a special case for translating "&" to "&amp;".
  49. (let ((index))
  50. (while (setq index (string-match "[&]" string index))
  51. (setq string (replace-match "&amp;" t nil string))
  52. (setq index (1+ index))))
  53. (while (string-match "[<]" string)
  54. (setq string (replace-match "&lt;" t nil string)))
  55. (while (string-match "[>]" string)
  56. (setq string (replace-match "&gt;" t nil string)))
  57. (while (string-match "[']" string)
  58. (setq string (replace-match "&apos;" t nil string)))
  59. (while (string-match "[\"]" string)
  60. (setq string (replace-match "&quot;" t nil string)))
  61. string)
  62. (defun package--make-rss-entry (title text archive-url)
  63. (let ((date-string (format-time-string "%a, %d %B %Y %T %z")))
  64. (concat "<item>\n"
  65. "<title>" (package--encode title) "</title>\n"
  66. ;; FIXME: should have a link in the web page.
  67. "<link>" archive-url "news.html</link>\n"
  68. "<description>" (package--encode text) "</description>\n"
  69. "<pubDate>" date-string "</pubDate>\n"
  70. "</item>\n")))
  71. (defun package--make-html-entry (title text)
  72. (concat "<li> " (format-time-string "%B %e") " - "
  73. title " - " (package--encode text)
  74. " </li>\n"))
  75. (defun package--update-file (file tag text)
  76. "Update the package archive file named FILE.
  77. FILE should be relative to `package-archive-upload-base'.
  78. TAG is a string that can be found within the file; TEXT is
  79. inserted after its first occurrence in the file."
  80. (setq file (expand-file-name file package-archive-upload-base))
  81. (save-excursion
  82. (let ((old-buffer (find-buffer-visiting file)))
  83. (with-current-buffer (let ((find-file-visit-truename t))
  84. (or old-buffer (find-file-noselect file)))
  85. (goto-char (point-min))
  86. (search-forward tag)
  87. (forward-line)
  88. (insert text)
  89. (let ((file-precious-flag t))
  90. (save-buffer))
  91. (unless old-buffer
  92. (kill-buffer (current-buffer)))))))
  93. (defun package--archive-contents-from-url (archive-url)
  94. "Parse archive-contents file at ARCHIVE-URL.
  95. Return the file contents, as a string, or nil if unsuccessful."
  96. (when archive-url
  97. (with-temp-buffer
  98. (ignore-errors
  99. (url-insert-file-contents (concat archive-url "archive-contents"))
  100. (package-read-from-string
  101. (buffer-substring-no-properties (point-min) (point-max)))))))
  102. (defun package--archive-contents-from-file ()
  103. "Parse the archive-contents at `package-archive-upload-base'"
  104. (let ((file (expand-file-name "archive-contents"
  105. package-archive-upload-base)))
  106. (if (not (file-exists-p file))
  107. ;; No existing archive-contents means a new archive.
  108. (list package-archive-version)
  109. (let ((dont-kill (find-buffer-visiting file)))
  110. (with-current-buffer (let ((find-file-visit-truename t))
  111. (find-file-noselect file))
  112. (prog1
  113. (package-read-from-string
  114. (buffer-substring-no-properties (point-min) (point-max)))
  115. (unless dont-kill
  116. (kill-buffer (current-buffer)))))))))
  117. (defun package-maint-add-news-item (title description archive-url)
  118. "Add a news item to the webpages associated with the package archive.
  119. TITLE is the title of the news item.
  120. DESCRIPTION is the text of the news item."
  121. (interactive "sTitle: \nsText: ")
  122. (package--update-file "elpa.rss"
  123. "<description>"
  124. (package--make-rss-entry title description archive-url))
  125. (package--update-file "news.html"
  126. "New entries go here"
  127. (package--make-html-entry title description)))
  128. (defun package--update-news (package version description archive-url)
  129. "Update the ELPA web pages when a package is uploaded."
  130. (package-maint-add-news-item (concat package " version " version)
  131. description
  132. archive-url))
  133. (declare-function lm-commentary "lisp-mnt" (&optional file))
  134. (defvar tar-data-buffer)
  135. (defun package-upload-buffer-internal (pkg-desc extension &optional archive-url)
  136. "Upload a package whose contents are in the current buffer.
  137. PKG-DESC is the `package-desc'.
  138. EXTENSION is the file extension, a string. It can be either
  139. \"el\" or \"tar\".
  140. The upload destination is given by `package-archive-upload-base'.
  141. If its value is invalid, prompt for a directory.
  142. Optional arg ARCHIVE-URL is the URL of the destination archive.
  143. If it is non-nil, compute the new \"archive-contents\" file
  144. starting from the existing \"archive-contents\" at that URL. In
  145. addition, if `package-update-news-on-upload' is non-nil, call
  146. `package--update-news' to add a news item at that URL.
  147. If ARCHIVE-URL is nil, compute the new \"archive-contents\" file
  148. from the \"archive-contents\" at `package-archive-upload-base',
  149. if it exists."
  150. (let ((package-archive-upload-base package-archive-upload-base))
  151. ;; Check if `package-archive-upload-base' is valid.
  152. (when (or (not (stringp package-archive-upload-base))
  153. (equal package-archive-upload-base
  154. (car-safe
  155. (get 'package-archive-upload-base 'standard-value))))
  156. (setq package-archive-upload-base
  157. (read-directory-name
  158. "Base directory for package archive: ")))
  159. (unless (file-directory-p package-archive-upload-base)
  160. (if (y-or-n-p (format "%s does not exist; create it? "
  161. package-archive-upload-base))
  162. (make-directory package-archive-upload-base t)
  163. (error "Aborted")))
  164. (save-excursion
  165. (save-restriction
  166. (let* ((file-type (package-desc-kind pkg-desc))
  167. (pkg-name (package-desc-name pkg-desc))
  168. (requires (package-desc-reqs pkg-desc))
  169. (desc (if (eq (package-desc-summary pkg-desc)
  170. package--default-summary)
  171. (read-string "Description of package: ")
  172. (package-desc-summary pkg-desc)))
  173. (split-version (package-desc-version pkg-desc))
  174. (commentary
  175. (pcase file-type
  176. (`single (lm-commentary))
  177. (`tar nil))) ;; FIXME: Get it from the README file.
  178. (extras (package-desc-extras pkg-desc))
  179. (pkg-version (package-version-join split-version))
  180. (pkg-buffer (current-buffer)))
  181. ;; `package-upload-file' will error if given a directory,
  182. ;; but we check it here as well just in case.
  183. (when (eq 'dir file-type)
  184. (user-error "Can't upload directory, tar it instead"))
  185. ;; Get archive-contents from ARCHIVE-URL if it's non-nil, or
  186. ;; from `package-archive-upload-base' otherwise.
  187. (let ((contents (or (package--archive-contents-from-url archive-url)
  188. (package--archive-contents-from-file)))
  189. (new-desc (package-make-ac-desc
  190. split-version requires desc file-type extras)))
  191. (if (> (car contents) package-archive-version)
  192. (error "Unrecognized archive version %d" (car contents)))
  193. (let ((elt (assq pkg-name (cdr contents))))
  194. (if elt
  195. (if (version-list-<= split-version
  196. (package--ac-desc-version (cdr elt)))
  197. (error "New package has smaller version: %s" pkg-version)
  198. (setcdr elt new-desc))
  199. (setq contents (cons (car contents)
  200. (cons (cons pkg-name new-desc)
  201. (cdr contents))))))
  202. ;; Now CONTENTS is the updated archive contents. Upload
  203. ;; this and the package itself. For now we assume ELPA is
  204. ;; writable via file primitives.
  205. (let ((print-level nil)
  206. (print-quoted t)
  207. (print-length nil))
  208. (write-region (concat (pp-to-string contents) "\n")
  209. nil
  210. (expand-file-name "archive-contents"
  211. package-archive-upload-base)))
  212. ;; If there is a commentary section, write it.
  213. (when commentary
  214. (write-region commentary nil
  215. (expand-file-name
  216. (concat (symbol-name pkg-name) "-readme.txt")
  217. package-archive-upload-base)))
  218. (set-buffer (if (eq file-type 'tar) tar-data-buffer pkg-buffer))
  219. (write-region (point-min) (point-max)
  220. (expand-file-name
  221. (format "%s-%s.%s" pkg-name pkg-version extension)
  222. package-archive-upload-base)
  223. nil nil nil 'excl)
  224. ;; Write a news entry.
  225. (and package-update-news-on-upload
  226. archive-url
  227. (package--update-news (format "%s.%s" pkg-name extension)
  228. pkg-version desc archive-url))
  229. ;; special-case "package": write a second copy so that the
  230. ;; installer can easily find the latest version.
  231. (if (eq pkg-name 'package)
  232. (write-region (point-min) (point-max)
  233. (expand-file-name
  234. (format "%s.%s" pkg-name extension)
  235. package-archive-upload-base)
  236. nil nil nil 'ask))))))))
  237. (defun package-upload-buffer ()
  238. "Upload the current buffer as a single-file Emacs Lisp package.
  239. If `package-archive-upload-base' does not specify a valid upload
  240. destination, prompt for one."
  241. (interactive)
  242. (save-excursion
  243. (save-restriction
  244. ;; Find the package in this buffer.
  245. (let ((pkg-desc (package-buffer-info)))
  246. (package-upload-buffer-internal pkg-desc "el")))))
  247. (defun package-upload-file (file)
  248. "Upload the Emacs Lisp package FILE to the package archive.
  249. Interactively, prompt for FILE. The package is considered a
  250. single-file package if FILE ends in \".el\", and a multi-file
  251. package if FILE ends in \".tar\".
  252. If `package-archive-upload-base' does not specify a valid upload
  253. destination, prompt for one."
  254. (interactive "fPackage file name: ")
  255. (with-temp-buffer
  256. (insert-file-contents file)
  257. (let ((pkg-desc
  258. (cond
  259. ((string-match "\\.tar\\'" file)
  260. (tar-mode) (package-tar-file-info))
  261. ((string-match "\\.el\\'" file) (package-buffer-info))
  262. (t (error "Unrecognized extension `%s'"
  263. (file-name-extension file))))))
  264. (package-upload-buffer-internal pkg-desc (file-name-extension file)))))
  265. (defun package-gnus-summary-upload ()
  266. "Upload a package contained in the current *Article* buffer.
  267. This should be invoked from the gnus *Summary* buffer."
  268. (interactive)
  269. (with-current-buffer gnus-article-buffer
  270. (package-upload-buffer)))
  271. (provide 'package-x)
  272. ;;; package-x.el ends here