diff options
Diffstat (limited to 'configs/shared/emacs/.emacs.d/elpa/quelpa-0.0.1/quelpa.el')
-rw-r--r-- | configs/shared/emacs/.emacs.d/elpa/quelpa-0.0.1/quelpa.el | 1799 |
1 files changed, 1799 insertions, 0 deletions
diff --git a/configs/shared/emacs/.emacs.d/elpa/quelpa-0.0.1/quelpa.el b/configs/shared/emacs/.emacs.d/elpa/quelpa-0.0.1/quelpa.el new file mode 100644 index 000000000000..0994a7cf768d --- /dev/null +++ b/configs/shared/emacs/.emacs.d/elpa/quelpa-0.0.1/quelpa.el @@ -0,0 +1,1799 @@ +;;; quelpa.el --- Emacs Lisp packages built directly from source + +;; Copyright 2014-2018, Steckerhalter +;; Copyright 2014-2015, Vasilij Schneidermann <v.schneidermann@gmail.com> + +;; Author: steckerhalter +;; URL: https://framagit.org/steckerhalter/quelpa +;; Version: 0.0.1 +;; Package-Requires: ((emacs "24.3")) +;; Keywords: package management build source elpa + +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Your personal local Emacs Lisp Package Archive (ELPA) with packages +;; built on-the-fly directly from source. + +;; See the README for more info: +;; https://framagit.org/steckerhalter/quelpa/blob/master/README.md + +;;; Requirements: + +;; Emacs 24.3.1 + +;;; Code: + +(require 'cl-lib) +(require 'help-fns) +(require 'url-parse) +(require 'package) +(require 'lisp-mnt) + +;; --- customs / variables --------------------------------------------------- + +(defgroup quelpa nil + "Build and install packages from source code" + :group 'package) + +(defcustom quelpa-upgrade-p nil + "When non-nil, `quelpa' will try to upgrade packages. +The global value can be overridden for each package by supplying +the `:upgrade' argument." + :group 'quelpa + :type 'boolean) + +(defcustom quelpa-stable-p nil + "When non-nil, try to build stable packages like MELPA does." + :group 'quelpa + :type 'boolean) + +(defcustom quelpa-verbose t + "When non-nil, `quelpa' prints log messages." + :group 'quelpa + :type 'boolean) + +(defcustom quelpa-before-hook nil + "List of functions to be called before quelpa." + :group 'quelpa + :type 'hook) + +(defcustom quelpa-after-hook nil + "List of functions to be called after quelpa." + :group 'quelpa + :type 'hook) + +(defcustom quelpa-dir (expand-file-name "quelpa" user-emacs-directory) + "Where quelpa builds and stores packages." + :group 'quelpa + :type 'string) + +(defcustom quelpa-melpa-dir (expand-file-name "melpa" quelpa-dir) + "Where the melpa repo cloned to." + :group 'quelpa + :type 'string) + +(defcustom quelpa-build-dir (expand-file-name "build" quelpa-dir) + "Where quelpa builds packages." + :group 'quelpa + :type 'string) + +(defcustom quelpa-packages-dir (expand-file-name "packages" quelpa-dir) + "Where quelpa puts built packages." + :group 'quelpa + :type 'string) + +(defcustom quelpa-melpa-recipe-stores (list (expand-file-name + "recipes" + quelpa-melpa-dir)) + "Recipe stores where quelpa finds default recipes for packages. +A store can either be a string pointing to a directory with +recipe files or a list with recipes." + :group 'quelpa + :type '(repeat + (choice directory + (repeat + :tag "List of recipes" + (restricted-sexp :tag "Recipe" + :match-alternatives (listp)))))) + +(defcustom quelpa-persistent-cache-file (expand-file-name "cache" quelpa-dir) + "Location of the persistent cache file." + :group 'quelpa + :type 'string) + +(defcustom quelpa-persistent-cache-p t + "Non-nil when quelpa's cache is saved on and read from disk." + :group 'quelpa + :type 'boolean) + +(defcustom quelpa-checkout-melpa-p t + "If non-nil the MELPA git repo is cloned when quelpa is initialized." + :group 'quelpa + :type 'boolean) + +(defcustom quelpa-update-melpa-p t + "If non-nil the MELPA git repo is updated when quelpa is initialized. +If nil the update is disabled and the repo is only updated on +`quelpa-upgrade' or `quelpa-self-upgrade'." + :group 'quelpa + :type 'boolean) + +(defcustom quelpa-melpa-repo-url "https://github.com/melpa/melpa.git" + "The melpa git repository url." + :group 'quelpa + :type 'string) + +(defcustom quelpa-self-upgrade-p t + "If non-nil upgrade quelpa itself when doing a + `quelpa-upgrade', otherwise only upgrade the packages in the + quelpa cache." + :group 'quelpa + :type 'boolean) + +(defvar quelpa-initialized-p nil + "Non-nil when quelpa has been initialized.") + +(defvar quelpa-cache nil + "The `quelpa' command stores processed pkgs/recipes in the cache.") + +(defvar quelpa-recipe '(quelpa :url "https://framagit.org/steckerhalter/quelpa.git" :fetcher git) + "The recipe for quelpa.") + +;; --- compatibility for legacy `package.el' in Emacs 24.3 ------------------- + +(defun quelpa-setup-package-structs () + "Setup the struct `package-desc' when not available. +`package-desc-from-legacy' is provided to convert the legacy +vector desc into a valid PACKAGE-DESC." + (unless (functionp 'package-desc-p) + (cl-defstruct + (package-desc + (:constructor + ;; convert legacy package desc into PACKAGE-DESC + package-desc-from-legacy + (pkg-info kind + &aux + (name (intern (aref pkg-info 0))) + (version (version-to-list (aref pkg-info 3))) + (summary (if (string= (aref pkg-info 2) "") + "No description available." + (aref pkg-info 2))) + (reqs (aref pkg-info 1)) + (kind kind)))) + name + version + (summary "No description available.") + reqs + kind + archive + dir + extras + signed))) + +;; --- package building ------------------------------------------------------ + +(defun quelpa-package-type (file) + "Determine the package type of FILE. +Return `tar' for tarball packages, `single' for single file +packages, or nil, if FILE is not a package." + (let ((ext (file-name-extension file))) + (cond + ((string= ext "tar") 'tar) + ((string= ext "el") 'single) + (:else nil)))) + +(defun quelpa-get-package-desc (file) + "Extract and return the PACKAGE-DESC struct from FILE. +On error return nil." + (let* ((kind (quelpa-package-type file)) + (desc (with-demoted-errors "Error getting PACKAGE-DESC: %s" + (with-temp-buffer + (pcase kind + (`single (insert-file-contents file) + (package-buffer-info)) + (`tar (insert-file-contents-literally file) + (tar-mode) + (if (help-function-arglist 'package-tar-file-info) + ;; legacy `package-tar-file-info' requires an arg + (package-tar-file-info file) + (with-no-warnings (package-tar-file-info))))))))) + (pcase desc + ((pred package-desc-p) desc) + ((pred vectorp) (package-desc-from-legacy desc kind))))) + +(defun quelpa-archive-file-name (archive-entry) + "Return the path of the file in which the package for ARCHIVE-ENTRY is stored." + (let* ((name (car archive-entry)) + (pkg-info (cdr archive-entry)) + (version (package-version-join (aref pkg-info 0))) + (flavour (aref pkg-info 3))) + (expand-file-name + (format "%s-%s.%s" name version (if (eq flavour 'single) "el" "tar")) + quelpa-packages-dir))) + +(defun quelpa-version>-p (name version) + "Return non-nil if VERSION of pkg with NAME is newer than what is currently installed." + (not (or (not version) + (let ((pkg-desc (cdr (assq name package-alist)))) + (and pkg-desc + (version-list-<= + (version-to-list version) + (if (functionp 'package-desc-vers) + (package-desc-vers pkg-desc) ;old implementation + (package-desc-version (car pkg-desc)))))) + ;; Also check built-in packages. + (package-built-in-p name (version-to-list version))))) + +(defun quelpa-checkout (rcp dir) + "Return the version of the new package given a RCP. +Return nil if the package is already installed and should not be upgraded." + (pcase-let ((`(,name . ,config) rcp) + (quelpa-build-stable quelpa-stable-p)) + (unless (or (and (assq name package-alist) (not quelpa-upgrade-p)) + (and (not config) + (quelpa-message t "no recipe found for package `%s'" name))) + (let ((version (condition-case err + (quelpa-build-checkout name config dir) + (error "Failed to checkout `%s': `%s'" + name (error-message-string err))))) + (when (quelpa-version>-p name version) + version))))) + +(defun quelpa-build (rcp) + "Build a package from the given recipe RCP. +Uses the `package-build' library to get the source code and build +an elpa compatible package in `quelpa-build-dir' storing it in +`quelpa-packages-dir'. Return the path to the created file or nil +if no action is necessary (like when the package is installed +already and should not be upgraded etc)." + (let* ((name (car rcp)) + (build-dir (expand-file-name (symbol-name name) quelpa-build-dir)) + (version (quelpa-checkout rcp build-dir))) + (when version + (quelpa-archive-file-name + (quelpa-build-package (symbol-name name) + version + (quelpa-build--config-file-list (cdr rcp)) + build-dir + quelpa-packages-dir))))) + +;; --- package-build.el integration ------------------------------------------ + +(defun quelpa-file-version (file-path type version time-stamp) + "Return version of file at FILE-PATH." + (if (eq type 'directory) + time-stamp + (cl-letf* ((package-strip-rcs-id-orig (symbol-function 'package-strip-rcs-id)) + ((symbol-function 'package-strip-rcs-id) + (lambda (str) + (or (funcall package-strip-rcs-id-orig (lm-header "package-version")) + (funcall package-strip-rcs-id-orig (lm-header "version")) + "0")))) + (concat (mapconcat + #'number-to-string + (package-desc-version (quelpa-get-package-desc file-path)) ".") + (pcase version + (`original "") + (_ (concat "pre0." time-stamp))))))) + +(defun quelpa-directory-files (path) + "Return list of directory files from PATH recursively." + (let ((result '())) + (mapc + (lambda (file) + (if (file-directory-p file) + (progn + ;; When directory is not empty. + (when (cddr (directory-files file)) + (dolist (subfile (quelpa-directory-files file)) + (add-to-list 'result subfile)))) + (add-to-list 'result file))) + (mapcar + (lambda (file) (expand-file-name file path)) + ;; Without first two entries because they are always "." and "..". + (cddr (directory-files path)))) + result)) + +(defun quelpa-expand-source-file-list (file-path config) + "Return list of source files from FILE-PATH corresponding to +CONFIG." + (let ((source-files + (mapcar + (lambda (file) (expand-file-name file file-path)) + (quelpa-build--expand-source-file-list file-path config)))) + ;; Replace any directories in the source file list with the filenames of the + ;; files they contain (so that these files can subsequently be hashed). + (dolist (file source-files source-files) + (when (file-directory-p file) + (setq source-files (remove file source-files)) + (setq source-files (append source-files + (quelpa-directory-files file))))))) + +(defun quelpa-slurp-file (file) + "Return the contents of FILE as a string, or nil if no such +file exists." + (when (file-exists-p file) + (with-temp-buffer + (set-buffer-multibyte nil) + (setq-local buffer-file-coding-system 'binary) + (insert-file-contents-literally file) + (buffer-substring-no-properties (point-min) (point-max))))) + +(defun quelpa-check-hash (name config file-path dir &optional fetcher) + "Check if hash of FILE-PATH is different as in STAMP-FILE. +If it is different save the new hash and timestamp to STAMP-FILE +and return TIME-STAMP, otherwise return OLD-TIME-STAMP." + (unless (file-directory-p dir) + (make-directory dir)) + (let* (files + hashes + new-stamp-info + new-content-hash + (time-stamp + (replace-regexp-in-string "\\.0+" "." (format-time-string "%Y%m%d.%H%M%S"))) + (stamp-file (concat (expand-file-name (symbol-name name) dir) ".stamp")) + (old-stamp-info (quelpa-build--read-from-file stamp-file)) + (old-content-hash (cdr old-stamp-info)) + (old-time-stamp (car old-stamp-info)) + (type (if (file-directory-p file-path) 'directory 'file)) + (version (plist-get config :version))) + + (if (not (file-exists-p file-path)) + (error "`%s' does not exist" file-path) + (if (eq type 'directory) + (setq files (quelpa-expand-source-file-list file-path config) + hashes (mapcar + (lambda (file) + (secure-hash + 'sha1 (concat file (quelpa-slurp-file file)))) files) + new-content-hash (secure-hash 'sha1 (mapconcat 'identity hashes ""))) + (setq new-content-hash (secure-hash 'sha1 (quelpa-slurp-file file-path))))) + + (setq new-stamp-info (cons time-stamp new-content-hash)) + (if (and old-content-hash + (string= new-content-hash old-content-hash)) + (quelpa-file-version file-path type version old-time-stamp) + (unless (eq fetcher 'url) + (delete-directory dir t) + (make-directory dir) + (if (eq type 'file) + (copy-file file-path dir t t t t) + (copy-directory file-path dir t t t))) + (quelpa-build--dump new-stamp-info stamp-file) + (quelpa-file-version file-path type version time-stamp)))) + +;; --- package-build fork ------------------------------------------ + +(defcustom quelpa-build-verbose t + "When non-nil, then print additional progress information." + :type 'boolean) + +(defcustom quelpa-build-stable nil + "When non-nil, then try to build packages from versions-tagged code." + :type 'boolean) + +(defcustom quelpa-build-timeout-executable + (let ((prog (or (executable-find "timeout") + (executable-find "gtimeout")))) + (when (and prog + (string-match-p "^ *-k" + (shell-command-to-string (concat prog " --help")))) + prog)) + "Path to a GNU coreutils \"timeout\" command if available. +This must be a version which supports the \"-k\" option." + :type '(file :must-match t)) + +(defcustom quelpa-build-timeout-secs 600 + "Wait this many seconds for external processes to complete. + +If an external process takes longer than specified here to +complete, then it is terminated. This only has an effect +if `quelpa-build-timeout-executable' is non-nil." + :type 'number) + +(defcustom quelpa-build-tar-executable + (or (executable-find "gtar") + (executable-find "tar")) + "Path to a (preferably GNU) tar command. +Certain package names (e.g. \"@\") may not work properly with a BSD tar." + :type '(file :must-match t)) + +(defcustom quelpa-build-version-regexp "^[rRvV]?\\(.*\\)$" + "Default pattern for matching valid version-strings within repository tags. +The string in the capture group should be parsed as valid by `version-to-list'." + :type 'string) + +;;; Internal Variables + +(defconst quelpa-build-default-files-spec + '("*.el" "*.el.in" "dir" + "*.info" "*.texi" "*.texinfo" + "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo" + (:exclude ".dir-locals.el" "test.el" "tests.el" "*-test.el" "*-tests.el")) + "Default value for :files attribute in recipes.") + +;;; Utilities + +(defun quelpa-build--message (format-string &rest args) + "Behave like `message' if `quelpa-build-verbose' is non-nil. +Otherwise do nothing." + (when quelpa-build-verbose + (apply 'message format-string args))) + +(defun quelpa-build--slurp-file (file) + "Return the contents of FILE as a string, or nil if no such file exists." + (when (file-exists-p file) + (with-temp-buffer + (insert-file-contents file) + (buffer-substring-no-properties (point-min) (point-max))))) + +(defun quelpa-build--string-rtrim (str) + "Remove trailing whitespace from `STR'." + (replace-regexp-in-string "[ \t\n\r]+$" "" str)) + +(defun quelpa-build--trim (str &optional chr) + "Return a copy of STR without any trailing CHR (or space if unspecified)." + (if (equal (elt str (1- (length str))) (or chr ? )) + (substring str 0 (1- (length str))) + str)) + +;;; Version Handling + +(defun quelpa-build--valid-version (str &optional regexp) + "Apply to STR the REGEXP if defined, \ +then pass the string to `version-to-list' and return the result, \ +or nil if the version cannot be parsed." + (when (and regexp (string-match regexp str)) + (setq str (match-string 1 str))) + (ignore-errors (version-to-list str))) + +(defun quelpa-build--parse-time (str) + "Parse STR as a time, and format as a YYYYMMDD.HHMM string." + ;; We remove zero-padding the HH portion, as it is lost + ;; when stored in the archive-contents + (setq str (substring-no-properties str)) + (let ((time (date-to-time + (if (string-match "\ +^\\([0-9]\\{4\\}\\)/\\([0-9]\\{2\\}\\)/\\([0-9]\\{2\\}\\) \ +\\([0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}\\)$" str) + (concat (match-string 1 str) "-" (match-string 2 str) "-" + (match-string 3 str) " " (match-string 4 str)) + str)))) + (concat (format-time-string "%Y%m%d." time) + (format "%d" (string-to-number (format-time-string "%H%M" time)))))) + +(defun quelpa-build--find-parse-time (regexp &optional bound) + "Find REGEXP in current buffer and format as a time-based version string. +An optional second argument bounds the search; it is a buffer +position. The match found must not end after that position." + (and (re-search-backward regexp bound t) + (quelpa-build--parse-time (match-string-no-properties 1)))) + +(defun quelpa-build--find-parse-time-newest (regexp &optional bound) + "Find REGEXP in current buffer and format as a time-based version string. +An optional second argument bounds the search; it is a buffer +position. The match found must not end after that position." + (save-match-data + (let (cur matches) + (while (setq cur (quelpa-build--find-parse-time regexp bound)) + (push cur matches)) + (car (nreverse (sort matches 'string<)))))) + +(defun quelpa-build--find-version-newest (regexp &optional bound) + "Find the newest version matching REGEXP before point. +An optional second argument bounds the search; it is a buffer +position. The match found must not before after that position." + (let ((tags (split-string + (buffer-substring-no-properties + (or bound (point-min)) (point)) + "\n"))) + (setq tags (append + (mapcar + ;; Because the default `version-separator' is ".", + ;; version-strings like "1_4_5" will be parsed + ;; wrongly as (1 -4 4 -4 5), so we set + ;; `version-separator' to "_" below and run again. + (lambda (tag) + (when (quelpa-build--valid-version tag regexp) + (list (quelpa-build--valid-version tag regexp) tag))) + tags) + (mapcar + ;; Check for valid versions again, this time using + ;; "_" as a separator instead of "." to catch + ;; version-strings like "1_4_5". Since "_" is + ;; otherwise treated as a snapshot separator by + ;; `version-regexp-alist', we don't have to worry + ;; about the incorrect version list above—(1 -4 4 -4 + ;; 5)—since it will always be treated as older by + ;; `version-list-<'. + (lambda (tag) + (let ((version-separator "_")) + (when (quelpa-build--valid-version tag regexp) + (list (quelpa-build--valid-version tag regexp) tag)))) + tags))) + (setq tags (cl-remove-if nil tags)) + ;; Returns a list like ((0 1) ("v0.1")); the first element is used + ;; for comparison and for `package-version-join', and the second + ;; (the original tag) is used by git/hg/etc. + (car (nreverse (sort tags (lambda (v1 v2) (version-list-< (car v1) (car v2)))))))) + +;;; Run Process + +(defun quelpa-build--run-process (dir command &rest args) + "In DIR (or `default-directory' if unset) run COMMAND with ARGS. +Output is written to the current buffer." + (let ((default-directory (file-name-as-directory (or dir default-directory))) + (argv (nconc (unless (eq system-type 'windows-nt) + (list "env" "LC_ALL=C")) + (if quelpa-build-timeout-executable + (nconc (list quelpa-build-timeout-executable + "-k" "60" (number-to-string + quelpa-build-timeout-secs) + command) + args) + (cons command args))))) + (unless (file-directory-p default-directory) + (error "Can't run process in non-existent directory: %s" default-directory)) + (let ((exit-code (apply 'process-file + (car argv) nil (current-buffer) t + (cdr argv)))) + (or (zerop exit-code) + (error "Command '%s' exited with non-zero status %d: %s" + argv exit-code (buffer-string)))))) + +(defun quelpa-build--run-process-match (regexp dir prog &rest args) + "Run PROG with args and return the first match for REGEXP in its output. +PROG is run in DIR, or if that is nil in `default-directory'." + (with-temp-buffer + (apply 'quelpa-build--run-process dir prog args) + (goto-char (point-min)) + (re-search-forward regexp) + (match-string-no-properties 1))) + +;;; Checkout +;;;; Common + +(defun quelpa-build-checkout (package-name config working-dir) + "Check out source for PACKAGE-NAME with CONFIG under WORKING-DIR. +In turn, this function uses the :fetcher option in the CONFIG to +choose a source-specific fetcher function, which it calls with +the same arguments. + +Returns the package version as a string." + (let ((fetcher (plist-get config :fetcher))) + (quelpa-build--message "Fetcher: %s" fetcher) + (unless (eq fetcher 'wiki) + (quelpa-build--message "Source: %s\n" + (or (plist-get config :repo) + (plist-get config :url)))) + (funcall (intern (format "quelpa-build--checkout-%s" fetcher)) + package-name config (file-name-as-directory working-dir)))) + +(defun quelpa-build--princ-exists (dir) + "Print a message that the contents of DIR will be updated." + (quelpa-build--message "Updating %s" dir)) + +(defun quelpa-build--princ-checkout (repo dir) + "Print a message that REPO will be checked out into DIR." + (quelpa-build--message "Cloning %s to %s" repo dir)) + +;;;; Wiki + +(defvar quelpa-build--last-wiki-fetch-time 0 + "The time at which an emacswiki URL was last requested. +This is used to avoid exceeding the rate limit of 1 request per 2 +seconds; the server cuts off after 10 requests in 20 seconds.") + +(defvar quelpa-build--wiki-min-request-interval 3 + "The shortest permissible interval between successive requests for Emacswiki URLs.") + +(defmacro quelpa-build--with-wiki-rate-limit (&rest body) + "Rate-limit BODY code passed to this macro to match EmacsWiki's rate limiting." + (let ((elapsed (cl-gensym))) + `(let ((,elapsed (- (float-time) quelpa-build--last-wiki-fetch-time))) + (when (< ,elapsed quelpa-build--wiki-min-request-interval) + (let ((wait (- quelpa-build--wiki-min-request-interval ,elapsed))) + (quelpa-build--message + "Waiting %.2f secs before hitting Emacswiki again" wait) + (sleep-for wait))) + (unwind-protect + (progn ,@body) + (setq quelpa-build--last-wiki-fetch-time (float-time)))))) + +(require 'mm-decode) +(defvar url-http-response-status) +(defvar url-http-end-of-headers) + +(defun quelpa-build--url-copy-file (url newname &optional ok-if-already-exists) + "Copy URL to NEWNAME. Both args must be strings. +Returns the http request's header as a string. +Like `url-copy-file', but it produces an error if the http response is not 200. +Signals a `file-already-exists' error if file NEWNAME already exists, +unless a third argument OK-IF-ALREADY-EXISTS is supplied and non-nil. +A number as third arg means request confirmation if NEWNAME already exists." + (if (and (file-exists-p newname) + (not ok-if-already-exists)) + (error "Opening output file: File already exists, %s" newname)) + (let ((buffer (url-retrieve-synchronously url)) + (headers nil) + (handle nil)) + (if (not buffer) + (error "Opening input file: No such file or directory, %s" url)) + (with-current-buffer buffer + (unless (= 200 url-http-response-status) + (error "HTTP error %s fetching %s" url-http-response-status url)) + (setq handle (mm-dissect-buffer t)) + (mail-narrow-to-head) + (setq headers (buffer-string))) + (mm-save-part-to-file handle newname) + (kill-buffer buffer) + (mm-destroy-parts handle) + headers)) + +(defun quelpa-build--grab-wiki-file (filename) + "Download FILENAME from emacswiki, returning its last-modified time." + (let ((download-url + (format "https://www.emacswiki.org/emacs/download/%s" filename)) + headers) + (quelpa-build--with-wiki-rate-limit + (setq headers (quelpa-build--url-copy-file download-url filename t))) + (when (zerop (nth 7 (file-attributes filename))) + (error "Wiki file %s was empty - has it been removed?" filename)) + (quelpa-build--parse-time + (with-temp-buffer + (insert headers) + (mail-fetch-field "last-modified"))))) + +(defun quelpa-build--checkout-wiki (name config dir) + "Checkout package NAME with config CONFIG from the EmacsWiki into DIR." + (unless quelpa-build-stable + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (unless (file-exists-p dir) + (make-directory dir)) + (let ((files (or (plist-get config :files) + (list (format "%s.el" name)))) + (default-directory dir)) + (car (nreverse (sort (mapcar 'quelpa-build--grab-wiki-file files) + 'string-lessp))))))) + +;;;; Darcs + +(defun quelpa-build--darcs-repo (dir) + "Get the current darcs repo for DIR." + (quelpa-build--run-process-match "Default Remote: \\(.*\\)" + dir "darcs" "show" "repo")) + +(defun quelpa-build--checkout-darcs (name config dir) + "Check package NAME with config CONFIG out of darcs into DIR." + (let ((repo (plist-get config :url))) + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (cond + ((and (file-exists-p (expand-file-name "_darcs" dir)) + (string-equal (quelpa-build--darcs-repo dir) repo)) + (quelpa-build--princ-exists dir) + (quelpa-build--run-process dir "darcs" "pull" "--all")) + (t + (when (file-exists-p dir) + (delete-directory dir t)) + (quelpa-build--princ-checkout repo dir) + (quelpa-build--run-process nil "darcs" "get" repo dir))) + (if quelpa-build-stable + (let* ((min-bound (goto-char (point-max))) + (tag-version + (and (quelpa-build--run-process dir "darcs" "show" "tags") + (or (quelpa-build--find-version-newest + (or (plist-get config :version-regexp) + quelpa-build-version-regexp) + min-bound) + (error "No valid stable versions found for %s" name))))) + (quelpa-build--run-process dir "darcs" "obliterate" + "--all" "--from-tag" + (cadr tag-version)) + ;; Return the parsed version as a string + (package-version-join (car tag-version))) + (apply 'quelpa-build--run-process + dir "darcs" "changes" "--max-count" "1" + (quelpa-build--expand-source-file-list dir config)) + (quelpa-build--find-parse-time "\ +\\([a-zA-Z]\\{3\\} [a-zA-Z]\\{3\\} \ +\\( \\|[0-9]\\)[0-9] [0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\} \ +[A-Za-z]\\{3\\} [0-9]\\{4\\}\\)"))))) + +;;;; Fossil + +(defun quelpa-build--fossil-repo (dir) + "Get the current fossil repo for DIR." + (quelpa-build--run-process-match "\\(.*\\)" dir "fossil" "remote-url")) + +(defun quelpa-build--checkout-fossil (name config dir) + "Check package NAME with config CONFIG out of fossil into DIR." + (unless quelpa-build-stable + (let ((repo (plist-get config :url))) + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (cond + ((and (or (file-exists-p (expand-file-name ".fslckout" dir)) + (file-exists-p (expand-file-name "_FOSSIL_" dir))) + (string-equal (quelpa-build--fossil-repo dir) repo)) + (quelpa-build--princ-exists dir) + (quelpa-build--run-process dir "fossil" "update")) + (t + (when (file-exists-p dir) + (delete-directory dir t)) + (quelpa-build--princ-checkout repo dir) + (make-directory dir) + (quelpa-build--run-process dir "fossil" "clone" repo "repo.fossil") + (quelpa-build--run-process dir "fossil" "open" "repo.fossil"))) + (quelpa-build--run-process dir "fossil" "timeline" "-n" "1" "-t" "ci") + (or (quelpa-build--find-parse-time "\ +=== \\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} ===\n\ +[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}\\) ") + (error "No valid timestamps found!")))))) + +;;;; Svn + +(defun quelpa-build--svn-repo (dir) + "Get the current svn repo for DIR." + (quelpa-build--run-process-match "URL: \\(.*\\)" dir "svn" "info")) + +(defun quelpa-build--checkout-svn (name config dir) + "Check package NAME with config CONFIG out of svn into DIR." + (unless quelpa-build-stable + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (let ((repo (quelpa-build--trim (plist-get config :url) ?/)) + (bound (goto-char (point-max)))) + (cond + ((and (file-exists-p (expand-file-name ".svn" dir)) + (string-equal (quelpa-build--svn-repo dir) repo)) + (quelpa-build--princ-exists dir) + (quelpa-build--run-process dir "svn" "up")) + (t + (when (file-exists-p dir) + (delete-directory dir t)) + (quelpa-build--princ-checkout repo dir) + (quelpa-build--run-process nil "svn" "checkout" repo dir))) + (apply 'quelpa-build--run-process dir "svn" "info" + (quelpa-build--expand-source-file-list dir config)) + (or (quelpa-build--find-parse-time-newest "\ +Last Changed Date: \\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} \ +[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}\\( [+-][0-9]\\{4\\}\\)?\\)" + bound) + (error "No valid timestamps found!")))))) + +;;;; Cvs + +(defun quelpa-build--cvs-repo (dir) + "Get the current CVS root and repository for DIR. + +Return a cons cell whose `car' is the root and whose `cdr' is the repository." + (apply 'cons + (mapcar (lambda (file) + (quelpa-build--string-rtrim + (quelpa-build--slurp-file (expand-file-name file dir)))) + '("CVS/Root" "CVS/Repository")))) + +(defun quelpa-build--checkout-cvs (name config dir) + "Check package NAME with config CONFIG out of cvs into DIR." + (unless quelpa-build-stable + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (let ((root (quelpa-build--trim (plist-get config :url) ?/)) + (repo (or (plist-get config :module) (symbol-name name))) + (bound (goto-char (point-max))) + latest) + (cond + ((and (file-exists-p (expand-file-name "CVS" dir)) + (equal (quelpa-build--cvs-repo dir) (cons root repo))) + (quelpa-build--princ-exists dir) + (quelpa-build--run-process dir "cvs" "update" "-dP")) + (t + (when (file-exists-p dir) + (delete-directory dir t)) + (quelpa-build--princ-checkout (format "%s from %s" repo root) dir) + ;; CVS insists on relative paths as target directory for checkout (for + ;; whatever reason), and puts "CVS" directories into every subdirectory + ;; of the current working directory given in the target path. To get CVS + ;; to just write to DIR, we need to execute CVS from the parent + ;; directory of DIR, and specific DIR as relative path. Hence all the + ;; following mucking around with paths. CVS is really horrid. + (let ((dir (directory-file-name dir))) + (quelpa-build--run-process (file-name-directory dir) + "env" "TZ=UTC" "cvs" "-z3" + "-d" root "checkout" + "-d" (file-name-nondirectory dir) + repo)))) + (apply 'quelpa-build--run-process dir "cvs" "log" + (quelpa-build--expand-source-file-list dir config)) + + ;; `cvs log` does not provide a way to view the previous N + ;; revisions, so instead of parsing the entire log we examine + ;; the Entries file, which looks like this: + ;; + ;; /.cvsignore/1.2/Thu Sep 1 12:42:02 2005// + ;; /CHANGES/1.1/Tue Oct 4 11:47:54 2005// + ;; /GNUmakefile/1.8/Tue Oct 4 11:47:54 2005// + ;; /Makefile/1.14/Tue Oct 4 11:47:54 2005// + ;; + (insert-file-contents (concat dir "/CVS/Entries")) + (setq latest + (car + (sort + (split-string (buffer-substring-no-properties (point) (point-max)) "\n") + (lambda (x y) + (when (string-match "^\\/[^\\/]*\\/[^\\/]*\\/\\([^\\/]*\\)\\/\\/$" x) + (setq x (quelpa-build--parse-time (match-string 1 x)))) + (when (string-match "^\\/[^\\/]*\\/[^\\/]*\\/\\([^\\/]*\\)\\/\\/$" y) + (setq y (quelpa-build--parse-time (match-string 1 y)))) + (version-list-<= (quelpa-build--valid-version y) + (quelpa-build--valid-version x)))))) + (when (string-match "^\\/[^\\/]*\\/[^\\/]*\\/\\([^\\/]*\\)\\/\\/$" latest) + (setq latest (match-string 1 latest))) + (or (quelpa-build--parse-time latest) + (error "No valid timestamps found!")))))) + +;;;; Git + +(defun quelpa-build--git-repo (dir) + "Get the current git repo for DIR." + (quelpa-build--run-process-match + "Fetch URL: \\(.*\\)" dir "git" "remote" "show" "-n" "origin")) + +(defun quelpa-build--checkout-git (name config dir) + "Check package NAME with config CONFIG out of git into DIR." + (let ((repo (plist-get config :url)) + (commit (or (plist-get config :commit) + (let ((branch (plist-get config :branch))) + (when branch + (concat "origin/" branch)))))) + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (goto-char (point-max)) + (cond + ((and (file-exists-p (expand-file-name ".git" dir)) + (string-equal (quelpa-build--git-repo dir) repo)) + (quelpa-build--princ-exists dir) + (quelpa-build--run-process dir "git" "fetch" "--all" "--tags")) + (t + (when (file-exists-p dir) + (delete-directory dir t)) + (quelpa-build--princ-checkout repo dir) + (quelpa-build--run-process nil "git" "clone" repo dir))) + (if quelpa-build-stable + (let* ((min-bound (goto-char (point-max))) + (tag-version + (and (quelpa-build--run-process dir "git" "tag") + (or (quelpa-build--find-version-newest + (or (plist-get config :version-regexp) + quelpa-build-version-regexp) + min-bound) + (error "No valid stable versions found for %s" name))))) + ;; Using reset --hard here to comply with what's used for + ;; unstable, but maybe this should be a checkout? + (quelpa-build--update-git-to-ref + dir (concat "tags/" (cadr tag-version))) + ;; Return the parsed version as a string + (package-version-join (car tag-version))) + (quelpa-build--update-git-to-ref + dir (or commit (concat "origin/" (quelpa-build--git-head-branch dir)))) + (apply 'quelpa-build--run-process + dir "git" "log" "--first-parent" "-n1" "--pretty=format:'\%ci'" + (quelpa-build--expand-source-file-list dir config)) + (quelpa-build--find-parse-time "\ +\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} \ +[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}\\( [+-][0-9]\\{4\\}\\)?\\)"))))) + +(defun quelpa-build--git-head-branch (dir) + "Get the current git repo for DIR." + (or (ignore-errors + (quelpa-build--run-process-match + "HEAD branch: \\(.*\\)" dir "git" "remote" "show" "origin")) + "master")) + +(defun quelpa-build--git-head-sha (dir) + "Get the current head SHA for DIR." + (ignore-errors + (quelpa-build--run-process-match + "\\(.*\\)" dir "git" "rev-parse" "HEAD"))) + +(defun quelpa-build--update-git-to-ref (dir ref) + "Update the git repo in DIR so that HEAD is REF." + (quelpa-build--run-process dir "git" "reset" "--hard" ref) + (quelpa-build--run-process dir "git" "submodule" "sync" "--recursive") + (quelpa-build--run-process dir "git" "submodule" "update" "--init" "--recursive")) + +(defun quelpa-build--checkout-github (name config dir) + "Check package NAME with config CONFIG out of github into DIR." + (let ((url (format "https://github.com/%s.git" (plist-get config :repo)))) + (quelpa-build--checkout-git name (plist-put (copy-sequence config) :url url) dir))) + +(defun quelpa-build--checkout-gitlab (name config dir) + "Check package NAME with config CONFIG out of gitlab into DIR." + (let ((url (format "https://gitlab.com/%s.git" (plist-get config :repo)))) + (quelpa-build--checkout-git name (plist-put (copy-sequence config) :url url) dir))) + +;;;; Bzr + +(defun quelpa-build--bzr-repo (dir) + "Get the current bzr repo for DIR." + (quelpa-build--run-process-match "parent branch: \\(.*\\)" dir "bzr" "info")) + +(defun quelpa-build--checkout-bzr (name config dir) + "Check package NAME with config CONFIG out of bzr into DIR." + (let ((repo (quelpa-build--run-process-match + "\\(?:branch root\\|repository branch\\): \\(.*\\)" + nil "bzr" "info" (plist-get config :url)))) + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (goto-char (point-max)) + (cond + ((and (file-exists-p (expand-file-name ".bzr" dir)) + (string-equal (quelpa-build--bzr-repo dir) repo)) + (quelpa-build--princ-exists dir) + (quelpa-build--run-process dir "bzr" "merge" "--force")) + (t + (when (file-exists-p dir) + (delete-directory dir t)) + (quelpa-build--princ-checkout repo dir) + (quelpa-build--run-process nil "bzr" "branch" repo dir))) + (if quelpa-build-stable + (let ((bound (goto-char (point-max))) + (regexp (or (plist-get config :version-regexp) + quelpa-build-version-regexp)) + tag-version) + (quelpa-build--run-process dir "bzr" "tags") + (goto-char bound) + (ignore-errors (while (re-search-forward "\\ +.*") + (replace-match ""))) + (setq tag-version + (or (quelpa-build--find-version-newest regexp bound) + (error "No valid stable versions found for %s" name))) + (quelpa-build--run-process dir + "bzr" "revert" "-r" + (concat "tag:" (cadr tag-version))) + ;; Return the parsed version as a string + (package-version-join (car tag-version))) + (apply 'quelpa-build--run-process dir "bzr" "log" "-l1" + (quelpa-build--expand-source-file-list dir config)) + (quelpa-build--find-parse-time "\ +\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} \ +[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}\\( [+-][0-9]\\{4\\}\\)?\\)"))))) + +;;;; Hg + +(defun quelpa-build--hg-repo (dir) + "Get the current hg repo for DIR." + (quelpa-build--run-process-match "default = \\(.*\\)" dir "hg" "paths")) + +(defun quelpa-build--checkout-hg (name config dir) + "Check package NAME with config CONFIG out of hg into DIR." + (let ((repo (plist-get config :url))) + (with-current-buffer (get-buffer-create "*quelpa-build-checkout*") + (goto-char (point-max)) + (cond + ((and (file-exists-p (expand-file-name ".hg" dir)) + (string-equal (quelpa-build--hg-repo dir) repo)) + (quelpa-build--princ-exists dir) + (quelpa-build--run-process dir "hg" "pull") + (quelpa-build--run-process dir "hg" "update")) + (t + (when (file-exists-p dir) + (delete-directory dir t)) + (quelpa-build--princ-checkout repo dir) + (quelpa-build--run-process nil "hg" "clone" repo dir))) + (if quelpa-build-stable + (let ((min-bound (goto-char (point-max))) + (regexp (or (plist-get config :version-regexp) + quelpa-build-version-regexp)) + tag-version) + (quelpa-build--run-process dir "hg" "tags") + ;; The output of `hg tags` shows the ref of the tag as well + ;; as the tag itself, e.g.: + ;; + ;; tip 1696:73ad80e8fea1 + ;; 1.2.8 1691:464af57fd2b7 + ;; + ;; So here we remove that second column before passing the + ;; buffer contents to `quelpa-build--find-version-newest'. + ;; This isn't strictly necessary for Mercurial since the + ;; colon in "1691:464af57fd2b7" means that won't be parsed + ;; as a valid version-string, but it's an example of how to + ;; do it in case it's necessary elsewhere. + (goto-char min-bound) + (ignore-errors (while (re-search-forward "\\ +.*") + (replace-match ""))) + (setq tag-version + (or (quelpa-build--find-version-newest regexp min-bound) + (error "No valid stable versions found for %s" name))) + (quelpa-build--run-process dir "hg" "update" (cadr tag-version)) + ;; Return the parsed version as a string + (package-version-join (car tag-version))) + (apply 'quelpa-build--run-process + dir "hg" "log" "--style" "compact" "-l1" + (quelpa-build--expand-source-file-list dir config)) + (quelpa-build--find-parse-time "\ +\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} \ +[0-9]\\{2\\}:[0-9]\\{2\\}\\( [+-][0-9]\\{4\\}\\)?\\)"))))) + +(defun quelpa-build--checkout-bitbucket (name config dir) + "Check package NAME with config CONFIG out of bitbucket into DIR." + (let ((url (format "https://bitbucket.com/%s" (plist-get config :repo)))) + (quelpa-build--checkout-hg name (plist-put (copy-sequence config) :url url) dir))) + +;;; Utilities + +(defun quelpa-build--dump (data file &optional pretty-print) + "Write DATA to FILE as a Lisp sexp. +Optionally PRETTY-PRINT the data." + (with-temp-file file + (quelpa-build--message "File: %s" file) + (if pretty-print + (pp data (current-buffer)) + (print data (current-buffer))))) + +(defun quelpa-build--write-pkg-file (pkg-file pkg-info) + "Write PKG-FILE containing PKG-INFO." + (with-temp-file pkg-file + (pp + `(define-package + ,(aref pkg-info 0) + ,(aref pkg-info 3) + ,(aref pkg-info 2) + ',(mapcar + (lambda (elt) + (list (car elt) + (package-version-join (cadr elt)))) + (aref pkg-info 1)) + ;; Append our extra information + ,@(cl-mapcan (lambda (entry) + (let ((value (cdr entry))) + (when (or (symbolp value) (listp value)) + ;; We must quote lists and symbols, + ;; because Emacs 24.3 and earlier evaluate + ;; the package information, which would + ;; break for unquoted symbols or lists + (setq value (list 'quote value))) + (list (car entry) value))) + (when (> (length pkg-info) 4) + (aref pkg-info 4)))) + (current-buffer)) + (princ ";; Local Variables:\n;; no-byte-compile: t\n;; End:\n" + (current-buffer)))) + +(defun quelpa-build--read-from-file (file) + "Read and return the Lisp data stored in FILE, or nil if no such file exists." + (when (file-exists-p file) + (car (read-from-string (quelpa-build--slurp-file file))))) + +(defun quelpa-build--create-tar (file dir &optional files) + "Create a tar FILE containing the contents of DIR, or just FILES if non-nil." + (when (eq system-type 'windows-nt) + (setq file (replace-regexp-in-string "^\\([a-z]\\):" "/\\1" file))) + (apply 'process-file + quelpa-build-tar-executable nil + (get-buffer-create "*quelpa-build-checkout*") + nil "-cvf" + file + "--exclude=.svn" + "--exclude=CVS" + "--exclude=.git" + "--exclude=_darcs" + "--exclude=.fslckout" + "--exclude=_FOSSIL_" + "--exclude=.bzr" + "--exclude=.hg" + (or (mapcar (lambda (fn) (concat dir "/" fn)) files) (list dir)))) + +(defun quelpa-build--find-package-commentary (file-path) + "Get commentary section from FILE-PATH." + (when (file-exists-p file-path) + (with-temp-buffer + (insert-file-contents file-path) + (lm-commentary)))) + +(defun quelpa-build--write-pkg-readme (target-dir commentary file-name) + "In TARGET-DIR, write COMMENTARY to a -readme.txt file prefixed with FILE-NAME." + (when commentary + (with-temp-buffer + (insert commentary) + ;; Adapted from `describe-package-1'. + (goto-char (point-min)) + (save-excursion + (when (re-search-forward "^;;; Commentary:\n" nil t) + (replace-match "")) + (while (re-search-forward "^\\(;+ ?\\)" nil t) + (replace-match "")) + (goto-char (point-min)) + (when (re-search-forward "\\`\\( *\n\\)+" nil t) + (replace-match ""))) + (delete-trailing-whitespace) + (let ((coding-system-for-write buffer-file-coding-system)) + (write-region nil nil + (quelpa-build--readme-file-name target-dir file-name)))))) + +(defun quelpa-build--readme-file-name (target-dir file-name) + "Name of the readme file in TARGET-DIR for the package FILE-NAME." + (expand-file-name (concat file-name "-readme.txt") + target-dir)) + +(defun quelpa-build--update-or-insert-version (version) + "Ensure current buffer has a \"Package-Version: VERSION\" header." + (goto-char (point-min)) + (if (let ((case-fold-search t)) + (re-search-forward "^;+* *Package-Version *: *" nil t)) + (progn + (move-beginning-of-line nil) + (search-forward "V" nil t) + (backward-char) + (insert "X-Original-") + (move-beginning-of-line nil)) + ;; Put the new header in a sensible place if we can + (re-search-forward "^;+* *\\(Version\\|Package-Requires\\|Keywords\\|URL\\) *:" + nil t) + (forward-line)) + (insert (format ";; Package-Version: %s" version)) + (newline)) + +(defun quelpa-build--ensure-ends-here-line (file-path) + "Add a 'FILE-PATH ends here' trailing line if missing." + (save-excursion + (goto-char (point-min)) + (let ((trailer (concat ";;; " + (file-name-nondirectory file-path) + " ends here"))) + (unless (search-forward trailer nil t) + (goto-char (point-max)) + (newline) + (insert trailer) + (newline))))) + +(defun quelpa-build--get-package-info (file-path) + "Get a vector of package info from the docstrings in FILE-PATH." + (when (file-exists-p file-path) + (ignore-errors + (with-temp-buffer + (insert-file-contents file-path) + ;; next few lines are a hack for some packages that aren't + ;; commented properly. + (quelpa-build--update-or-insert-version "0") + (quelpa-build--ensure-ends-here-line file-path) + (cl-flet ((package-strip-rcs-id (str) "0")) + (quelpa-build--package-buffer-info-vec)))))) + +(defun quelpa-build--get-pkg-file-info (file-path) + "Get a vector of package info from \"-pkg.el\" file FILE-PATH." + (when (file-exists-p file-path) + (let ((package-def (quelpa-build--read-from-file file-path))) + (if (eq 'define-package (car package-def)) + (let* ((pkgfile-info (cdr package-def)) + (descr (nth 2 pkgfile-info)) + (rest-plist (cl-subseq pkgfile-info (min 4 (length pkgfile-info)))) + (extras (let (alist) + (while rest-plist + (unless (memq (car rest-plist) '(:kind :archive)) + (let ((value (cadr rest-plist))) + (when value + (push (cons (car rest-plist) + (if (eq (car-safe value) 'quote) + (cadr value) + value)) + alist)))) + (setq rest-plist (cddr rest-plist))) + alist))) + (when (string-match "[\r\n]" descr) + (error "Illegal multi-line package description in %s" file-path)) + (vector + (nth 0 pkgfile-info) + (mapcar + (lambda (elt) + (unless (symbolp (car elt)) + (error "Invalid package name in dependency: %S" (car elt))) + (list (car elt) (version-to-list (cadr elt)))) + (eval (nth 3 pkgfile-info))) + descr + (nth 1 pkgfile-info) + extras)) + (error "No define-package found in %s" file-path))))) + +(defun quelpa-build--merge-package-info (pkg-info name version) + "Return a version of PKG-INFO updated with NAME, VERSION and info from CONFIG. +If PKG-INFO is nil, an empty one is created." + (let ((merged (or (copy-sequence pkg-info) + (vector name nil "No description available." version)))) + (aset merged 0 name) + (aset merged 3 version) + merged)) + +(defun quelpa-build--archive-entry (pkg-info type) + "Return the archive-contents cons cell for PKG-INFO and TYPE." + (let ((name (intern (aref pkg-info 0))) + (requires (aref pkg-info 1)) + (desc (or (aref pkg-info 2) "No description available.")) + (version (aref pkg-info 3)) + (extras (and (> (length pkg-info) 4) + (aref pkg-info 4)))) + (cons name + (vector (version-to-list version) + requires + desc + type + extras)))) + +;;; Recipes + +(defun quelpa-build-expand-file-specs (dir specs &optional subdir allow-empty) + "In DIR, expand SPECS, optionally under SUBDIR. +The result is a list of (SOURCE . DEST), where SOURCE is a source +file path and DEST is the relative path to which it should be copied. + +If the resulting list is empty, an error will be reported. Pass t +for ALLOW-EMPTY to prevent this error." + (let ((default-directory dir) + (prefix (if subdir (format "%s/" subdir) "")) + (lst)) + (dolist (entry specs lst) + (setq lst + (if (consp entry) + (if (eq :exclude (car entry)) + (cl-nset-difference lst + (quelpa-build-expand-file-specs + dir (cdr entry) nil t) + :key 'car + :test 'equal) + (nconc lst + (quelpa-build-expand-file-specs + dir + (cdr entry) + (concat prefix (car entry)) + t))) + (nconc + lst (mapcar (lambda (f) + (let ((destname))) + (cons f + (concat prefix + (replace-regexp-in-string + "\\.in\\'" + "" + (file-name-nondirectory f))))) + (file-expand-wildcards entry)))))) + (when (and (null lst) (not allow-empty)) + (error "No matching file(s) found in %s: %s" dir specs)) + lst)) + +(defun quelpa-build--config-file-list (config) + "Get the :files spec from CONFIG, or return `quelpa-build-default-files-spec'." + (let ((file-list (plist-get config :files))) + (cond + ((null file-list) + quelpa-build-default-files-spec) + ((eq :defaults (car file-list)) + (append quelpa-build-default-files-spec (cdr file-list))) + (t + file-list)))) + +(defun quelpa-build--expand-source-file-list (dir config) + "Shorthand way to expand paths in DIR for source files listed in CONFIG." + (mapcar 'car + (quelpa-build-expand-file-specs + dir (quelpa-build--config-file-list config)))) + +(defun quelpa-build--generate-info-files (files source-dir target-dir) + "Create .info files from any .texi files listed in FILES. + +The source and destination file paths are expanded in SOURCE-DIR +and TARGET-DIR respectively. + +Any of the original .texi(nfo) files found in TARGET-DIR are +deleted." + (dolist (spec files) + (let* ((source-file (car spec)) + (source-path (expand-file-name source-file source-dir)) + (dest-file (cdr spec)) + (info-path (expand-file-name + (concat (file-name-sans-extension dest-file) ".info") + target-dir))) + (when (string-match ".texi\\(nfo\\)?$" source-file) + (when (not (file-exists-p info-path)) + (with-current-buffer (get-buffer-create "*quelpa-build-info*") + (ignore-errors + (quelpa-build--run-process + (file-name-directory source-path) + "makeinfo" + source-path + "-o" + info-path) + (quelpa-build--message "Created %s" info-path)))) + (quelpa-build--message "Removing %s" + (expand-file-name dest-file target-dir)) + (delete-file (expand-file-name dest-file target-dir)))))) + +;;; Info Manuals + +(defun quelpa-build--generate-dir-file (files target-dir) + "Create dir file from any .info files listed in FILES in TARGET-DIR." + (dolist (spec files) + (let* ((source-file (car spec)) + (dest-file (cdr spec)) + (info-path (expand-file-name + (concat (file-name-sans-extension dest-file) ".info") + target-dir))) + (when (and (or (string-match ".info$" source-file) + (string-match ".texi\\(nfo\\)?$" source-file)) + (file-exists-p info-path)) + (with-current-buffer (get-buffer-create "*quelpa-build-info*") + (ignore-errors + (quelpa-build--run-process + nil + "install-info" + (concat "--dir=" (expand-file-name "dir" target-dir)) + info-path))))))) + +;;; Utilities + +(defun quelpa-build--copy-package-files (files source-dir target-dir) + "Copy FILES from SOURCE-DIR to TARGET-DIR. +FILES is a list of (SOURCE . DEST) relative filepath pairs." + (cl-loop for (source-file . dest-file) in files + do (quelpa-build--copy-file + (expand-file-name source-file source-dir) + (expand-file-name dest-file target-dir)))) + +(defun quelpa-build--copy-file (file newname) + "Copy FILE to NEWNAME and create parent directories for NEWNAME if they don't exist." + (let ((newdir (file-name-directory newname))) + (unless (file-exists-p newdir) + (make-directory newdir t))) + (cond + ((file-regular-p file) + (quelpa-build--message "%s -> %s" file newname) + (copy-file file newname)) + ((file-directory-p file) + (quelpa-build--message "%s => %s" file newname) + (copy-directory file newname)))) + +(defun quelpa-build--find-source-file (target files) + "Search for source of TARGET in FILES." + (car (rassoc target files))) + +(defun quelpa-build--package-buffer-info-vec () + "Return a vector of package info. +`package-buffer-info' returns a vector in older Emacs versions, +and a cl struct in Emacs HEAD. This wrapper normalises the results." + (let ((desc (package-buffer-info)) + (keywords (lm-keywords-list))) + (if (fboundp 'package-desc-create) + (let ((extras (package-desc-extras desc))) + (when (and keywords (not (assq :keywords extras))) + ;; Add keywords to package properties, if not already present + (push (cons :keywords keywords) extras)) + (vector (package-desc-name desc) + (package-desc-reqs desc) + (package-desc-summary desc) + (package-desc-version desc) + extras)) + ;; The regexp and the processing is taken from `lm-homepage' in Emacs 24.4 + (let* ((page (lm-header "\\(?:x-\\)?\\(?:homepage\\|url\\)")) + (homepage (if (and page (string-match "^<.+>$" page)) + (substring page 1 -1) + page)) + extras) + (when keywords (push (cons :keywords keywords) extras)) + (when homepage (push (cons :url homepage) extras)) + (vector (aref desc 0) + (aref desc 1) + (aref desc 2) + (aref desc 3) + extras))))) + +;;; Building + +;;;###autoload +(defun quelpa-build-package (package-name version file-specs source-dir target-dir) + "Create PACKAGE-NAME with VERSION. + +The information in FILE-SPECS is used to gather files from +SOURCE-DIR. + +The resulting package will be stored as a .el or .tar file in +TARGET-DIR, depending on whether there are multiple files. + +Argument FILE-SPECS is a list of specs for source files, which +should be relative to SOURCE-DIR. The specs can be wildcards, +and optionally specify different target paths. They extended +syntax is currently only documented in the MELPA README. You can +simply pass `quelpa-build-default-files-spec' in most cases. + +Returns the archive entry for the package." + (when (symbolp package-name) + (setq package-name (symbol-name package-name))) + (let ((files (quelpa-build-expand-file-specs source-dir file-specs))) + (unless (equal file-specs quelpa-build-default-files-spec) + (when (equal files (quelpa-build-expand-file-specs + source-dir quelpa-build-default-files-spec nil t)) + (quelpa-build--message "Note: %s :files spec is equivalent to the default." + package-name))) + (cond + ((not version) + (error "Unable to check out repository for %s" package-name)) + ((= 1 (length files)) + (quelpa-build--build-single-file-package + package-name version (caar files) source-dir target-dir)) + ((< 1 (length files)) + (quelpa-build--build-multi-file-package + package-name version files source-dir target-dir)) + (t (error "Unable to find files matching recipe patterns"))))) + +(defun quelpa-build--build-single-file-package + (package-name version file source-dir target-dir) + (let* ((pkg-source (expand-file-name file source-dir)) + (pkg-target (expand-file-name + (concat package-name "-" version ".el") + target-dir)) + (pkg-info (quelpa-build--merge-package-info + (quelpa-build--get-package-info pkg-source) + package-name + version))) + (unless (string-equal (downcase (concat package-name ".el")) + (downcase (file-name-nondirectory pkg-source))) + (error "Single file %s does not match package name %s" + (file-name-nondirectory pkg-source) package-name)) + (if (file-exists-p pkg-target) + (quelpa-build--message "Skipping rebuild of %s" pkg-target) + (copy-file pkg-source pkg-target) + (let ((enable-local-variables nil) + (make-backup-files nil)) + (with-temp-buffer + (insert-file-contents pkg-target) + (quelpa-build--update-or-insert-version version) + (quelpa-build--ensure-ends-here-line pkg-source) + (write-file pkg-target nil) + (condition-case err + (quelpa-build--package-buffer-info-vec) + (error + (quelpa-build--message "Warning: %S" err))))) + + (quelpa-build--write-pkg-readme + target-dir + (quelpa-build--find-package-commentary pkg-source) + package-name)) + (quelpa-build--archive-entry pkg-info 'single))) + +(defun quelpa-build--build-multi-file-package + (package-name version files source-dir target-dir) + (let ((tmp-dir (file-name-as-directory (make-temp-file package-name t)))) + (unwind-protect + (let* ((pkg-dir-name (concat package-name "-" version)) + (pkg-tmp-dir (expand-file-name pkg-dir-name tmp-dir)) + (pkg-file (concat package-name "-pkg.el")) + (pkg-file-source (or (quelpa-build--find-source-file pkg-file files) + pkg-file)) + (file-source (concat package-name ".el")) + (pkg-source (or (quelpa-build--find-source-file file-source files) + file-source)) + (pkg-info (quelpa-build--merge-package-info + (let ((default-directory source-dir)) + (or (quelpa-build--get-pkg-file-info pkg-file-source) + ;; some packages (like magit) provide name-pkg.el.in + (quelpa-build--get-pkg-file-info + (expand-file-name (concat pkg-file ".in") + (file-name-directory pkg-source))) + (quelpa-build--get-package-info pkg-source))) + package-name + version))) + (quelpa-build--copy-package-files files source-dir pkg-tmp-dir) + (quelpa-build--write-pkg-file (expand-file-name + pkg-file + (file-name-as-directory pkg-tmp-dir)) + pkg-info) + + (quelpa-build--generate-info-files files source-dir pkg-tmp-dir) + (quelpa-build--generate-dir-file files pkg-tmp-dir) + + (let ((default-directory tmp-dir)) + (quelpa-build--create-tar + (expand-file-name (concat package-name "-" version ".tar") + target-dir) + pkg-dir-name)) + + (let ((default-directory source-dir)) + (quelpa-build--write-pkg-readme + target-dir + (quelpa-build--find-package-commentary pkg-source) + package-name)) + (quelpa-build--archive-entry pkg-info 'tar)) + (delete-directory tmp-dir t nil)))) + +(defun quelpa-build--checkout-file (name config dir) + "Build according to a PATH with config CONFIG into DIR as NAME. +Generic local file handler for package-build.el. + +Handles the following cases: + +local file: + +Installs a single-file package from a local file. Use the :path +attribute with a PATH like \"/path/to/file.el\". + +local directory: + +Installs a multi-file package from a local directory. Use +the :path attribute with a PATH like \"/path/to/dir\"." + (quelpa-check-hash name config (expand-file-name (plist-get config :path)) dir)) + +(defun quelpa-build--checkout-url (name config dir) + "Build according to an URL with config CONFIG into DIR as NAME. +Generic URL handler for package-build.el. + +Handles the following cases: + +local file: + +Installs a single-file package from a local file. Use the :url +attribute with an URL like \"file:///path/to/file.el\". + +remote file: + +Installs a single-file package from a remote file. Use the :url +attribute with an URL like \"http://domain.tld/path/to/file.el\"." + (let* ((url (plist-get config :url)) + (remote-file-name (file-name-nondirectory + (url-filename (url-generic-parse-url url)))) + (local-path (expand-file-name remote-file-name dir)) + (mm-attachment-file-modes (default-file-modes))) + (unless (string= (file-name-extension url) "el") + (error "<%s> does not end in .el" url)) + (unless (file-directory-p dir) + (make-directory dir)) + (url-copy-file url local-path t) + (quelpa-check-hash name config local-path dir 'url))) + +;; --- helpers --------------------------------------------------------------- + +(defun quelpa-message (wait format-string &rest args) + "Log a message with FORMAT-STRING and ARGS when `quelpa-verbose' is non-nil. +If WAIT is nil don't wait after showing the message. If it is a +number, wait so many seconds. If WAIT is t wait the default time. +Return t in each case." + (when quelpa-verbose + (message "Quelpa: %s" (apply 'format format-string args)) + (when (or (not noninteractive) wait) ; no wait if emacs is noninteractive + (sit-for (or (and (numberp wait) wait) 1.5) t))) + t) + +(defun quelpa-read-cache () + "Read from `quelpa-persistent-cache-file' in `quelpa-cache'." + (when (and quelpa-persistent-cache-p + (file-exists-p quelpa-persistent-cache-file)) + (with-temp-buffer + (insert-file-contents-literally quelpa-persistent-cache-file) + (setq quelpa-cache + (read (buffer-substring-no-properties (point-min) (point-max))))))) + +(defun quelpa-save-cache () + "Write `quelpa-cache' to `quelpa-persistent-cache-file'." + (when quelpa-persistent-cache-p + (let (print-level print-length) + (with-temp-file quelpa-persistent-cache-file + (insert (prin1-to-string quelpa-cache)))))) + +(defun quelpa-update-cache (cache-item) + ;; try removing existing recipes by name + (setq quelpa-cache (cl-remove (car cache-item) + quelpa-cache :key #'car)) + (push cache-item quelpa-cache) + (setq quelpa-cache + (cl-sort quelpa-cache #'string< + :key (lambda (item) (symbol-name (car item)))))) + +(defun quelpa-parse-stable (cache-item) + ;; in case :stable doesn't originate from PLIST, shadow the + ;; default value anyways + (when (plist-member (cdr cache-item) :stable) + (setq quelpa-stable-p (plist-get (cdr cache-item) :stable))) + (when (and quelpa-stable-p (not (plist-get (cdr cache-item) :stable))) + (setf (cdr (last cache-item)) '(:stable t)))) + +(defun quelpa-checkout-melpa () + "Fetch or update the melpa source code from Github. +If there is no error return non-nil. +If there is an error but melpa is already checked out return non-nil. +If there is an error and no existing checkout return nil." + (or (and (null quelpa-update-melpa-p) + (file-exists-p (expand-file-name ".git" quelpa-melpa-dir))) + (condition-case err + (quelpa-build--checkout-git + 'package-build + `(:url ,quelpa-melpa-repo-url :files ("*")) + quelpa-melpa-dir) + (error "failed to checkout melpa git repo: `%s'" (error-message-string err))))) + +(defun quelpa-get-melpa-recipe (name) + "Read recipe with NAME for melpa git checkout. +Return the recipe if it exists, otherwise nil." + (cl-loop for store in quelpa-melpa-recipe-stores + if (stringp store) + for file = (assoc-string name (directory-files store nil "^[^\.]+")) + when file + return (with-temp-buffer + (insert-file-contents-literally + (expand-file-name file store)) + (read (buffer-string))) + else + for rcp = (assoc-string name store) + when rcp + return rcp)) + +(defun quelpa-setup-p () + "Setup what we need for quelpa. +Return non-nil if quelpa has been initialized properly." + (catch 'quit + (dolist (dir (list quelpa-packages-dir quelpa-build-dir)) + (unless (file-exists-p dir) (make-directory dir t))) + (unless quelpa-initialized-p + (quelpa-read-cache) + (quelpa-setup-package-structs) + (if quelpa-checkout-melpa-p + (unless (quelpa-checkout-melpa) (throw 'quit nil))) + (setq quelpa-initialized-p t)) + t)) + +(defun quelpa-shutdown () + "Do things that need to be done after running quelpa." + (quelpa-save-cache) + ;; remove the packages dir because we are done with the built pkgs + (ignore-errors (delete-directory quelpa-packages-dir t))) + +(defun quelpa-arg-rcp (arg) + "Given recipe or package name, return an alist '(NAME . RCP). +If RCP cannot be found it will be set to nil" + (pcase arg + (`(,a . nil) (quelpa-get-melpa-recipe (car arg))) + (`(,a . ,_) arg) + ((pred symbolp) (quelpa-get-melpa-recipe arg)))) + +(defun quelpa-parse-plist (plist) + "Parse the optional PLIST argument of `quelpa'. +Recognized keywords are: + +:upgrade + +If t, `quelpa' tries to do an upgrade. + +:stable + +If t, `quelpa' tries building the stable version of a package." + (while plist + (let ((key (car plist)) + (value (cadr plist))) + (pcase key + (:upgrade (setq quelpa-upgrade-p value)) + (:stable (setq quelpa-stable-p value)))) + (setq plist (cddr plist)))) + +(defun quelpa-package-install-file (file) + "Workaround problem with `package-install-file'. +`package-install-file' uses `insert-file-contents-literally' +which causes problems when the file inserted has crlf line +endings (Windows). So here we replace that with +`insert-file-contents' for non-tar files." + (if (eq system-type 'windows-nt) + (cl-letf* ((insert-file-contents-literally-orig + (symbol-function 'insert-file-contents-literally)) + ((symbol-function 'insert-file-contents-literally) + (lambda (file) + (if (string-match "\\.tar\\'" file) + (funcall insert-file-contents-literally-orig file) + (insert-file-contents file))))) + (package-install-file file)) + (package-install-file file))) + +(defun quelpa-package-install (arg) + "Build and install package from ARG (a recipe or package name). +If the package has dependencies recursively call this function to +install them." + (let* ((rcp (quelpa-arg-rcp arg)) + (file (and rcp (quelpa-build rcp)))) + (when file + (let* ((pkg-desc (quelpa-get-package-desc file)) + (requires (package-desc-reqs pkg-desc))) + (when requires + (mapc (lambda (req) + (unless (or (equal 'emacs (car req)) + (package-installed-p (car req) (cadr req))) + (quelpa-package-install (car req)))) + requires)) + (quelpa-package-install-file file))))) + +(defun quelpa-interactive-candidate () + "Query the user for a recipe and return the name." + (when (quelpa-setup-p) + (let ((recipes (cl-loop + for store in quelpa-melpa-recipe-stores + if (stringp store) + ;; this regexp matches all files except dotfiles + append (directory-files store nil "^[^.].+$") + else if (listp store) + append store))) + (intern (completing-read "Choose MELPA recipe: " + recipes nil t))))) + +;; --- public interface ------------------------------------------------------ + +;;;###autoload +(defun quelpa-expand-recipe (recipe-name) + "Expand a given recipe name into full recipe. +If called interactively, let the user choose a recipe name and +insert the result into the current buffer." + (interactive (list (quelpa-interactive-candidate))) + (when (quelpa-setup-p) + (let* ((recipe (quelpa-get-melpa-recipe recipe-name))) + (when recipe + (if (called-interactively-p 'any) + (prin1 recipe (current-buffer))) + recipe)))) + +;;;###autoload +(defun quelpa-self-upgrade (&optional args) + "Upgrade quelpa itself. +ARGS are additional options for the quelpa recipe." + (interactive) + (when (quelpa-setup-p) + (quelpa (append quelpa-recipe args) :upgrade t))) + +;;;###autoload +(defun quelpa-upgrade () + "Upgrade all packages found in `quelpa-cache'. +This provides an easy way to upgrade all the packages for which +the `quelpa' command has been run in the current Emacs session." + (interactive) + (when (quelpa-setup-p) + (let ((quelpa-upgrade-p t)) + (when quelpa-self-upgrade-p + (quelpa-self-upgrade)) + (setq quelpa-cache + (cl-remove-if-not #'package-installed-p quelpa-cache :key #'car)) + (mapc (lambda (item) + (when (package-installed-p (car (quelpa-arg-rcp item))) + (quelpa item))) + quelpa-cache)))) + +;;;###autoload +(defun quelpa (arg &rest plist) + "Build and install a package with quelpa. +ARG can be a package name (symbol) or a melpa recipe (list). +PLIST is a plist that may modify the build and/or fetch process. +If called interactively, `quelpa' will prompt for a MELPA package +to install. + +When `quelpa' is called interactively with a prefix argument (e.g +C-u M-x quelpa) it will try to upgrade the given package even if +the global var `quelpa-upgrade-p' is set to nil." + + (interactive (list (quelpa-interactive-candidate))) + (run-hooks 'quelpa-before-hook) + (when (quelpa-setup-p) ;if init fails we do nothing + (let* ((quelpa-upgrade-p (if current-prefix-arg t quelpa-upgrade-p)) ;shadow `quelpa-upgrade-p' + (quelpa-stable-p quelpa-stable-p) ;shadow `quelpa-stable-p' + (cache-item (if (symbolp arg) (list arg) arg))) + (quelpa-parse-plist plist) + (quelpa-parse-stable cache-item) + (quelpa-package-install arg) + (quelpa-update-cache cache-item))) + (quelpa-shutdown) + (run-hooks 'quelpa-after-hook)) + +(provide 'quelpa) + +;;; quelpa.el ends here |