2be6e4e338
Fixes #10653
1799 lines
74 KiB
EmacsLisp
1799 lines
74 KiB
EmacsLisp
;;; 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://github.com/quelpa/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://github.com/quelpa/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 :repo "quelpa/quelpa" :fetcher github)
|
|
"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-current-buffer (find-file 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)))
|
|
(kill-buffer)))
|
|
|
|
(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
|