el-get-recipes.el 18.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
;;; el-get-recipes.el --- Manage the external elisp bits and pieces you depend upon
;;
;; Copyright (C) 2010-2011 Dimitri Fontaine
;;
;; Author: Dimitri Fontaine <dim@tapoueh.org>
;; URL: http://www.emacswiki.org/emacs/el-get
;; GIT: https://github.com/dimitri/el-get
;; Licence: WTFPL, grab your copy here: http://sam.zoy.org/wtfpl/
;;
;; This file is NOT part of GNU Emacs.
;;
;; Install
13
;;     Please see the README.md file from the same distribution
14 15 16 17 18 19 20 21 22 23

;;; Commentary:
;;
;; el-get-recipes provides the API to manage the el-get recipes.
;;

;; el-get-core provides basic el-get API, intended for developpers of el-get
;; and its methods.  See the methods directory for implementation of them.
;;

24 25
;;; Code:

26
(require 'el-get-core)
27
(require 'el-get-custom)
28
(require 'el-get-byte-compile)
29 30

(defcustom el-get-recipe-path-emacswiki
31
  (expand-file-name "el-get/recipes/emacswiki/" el-get-dir)
32
  "Define where to keep a local copy of emacswiki recipes."
33 34 35
  :group 'el-get
  :type 'directory)

36
(defcustom el-get-recipe-path-elpa
37
  (expand-file-name "el-get/recipes/elpa/" el-get-dir)
38
  "Define where to keep a local copy of elpa recipes."
39 40 41
  :group 'el-get
  :type 'directory)

42
(defvar el-get-recipe-path
43
  (list (concat (file-name-directory el-get-script) "recipes")
44 45 46 47 48 49
        el-get-recipe-path-elpa
        el-get-recipe-path-emacswiki)
  "List of directories in which to look for el-get recipes.

Directories that contain automatically-generated recipes, such as
`el-get-recipe-path-emacswiki' and `el-get-recipe-path-elpa',
50 51 52 53 54 55 56 57
should be placed last in this list.

This variable is not customizable, as it needs to be set before
el-get is loaded, while customizations should be loaded after
el-get, so that they can affect pacakages loaded by el-get.
It is recommended to add new directories using code like:

  (add-to-list 'el-get-recipe-path \"~/.emacs.d/el-get-user/recipes/\")")
58 59

(defcustom el-get-user-package-directory nil
60
  "Define where to look for init-pkgname.el configurations.  Disabled if nil."
61 62 63 64 65 66
  :group 'el-get
  :type '(choice (const :tag "Off" nil) directory))

(defun el-get-load-package-user-init-file (package)
  "Load the user init file for PACKAGE, called init-package.el
and to be found in `el-get-user-package-directory'.  Do nothing
67 68 69 70
when this custom is nil.

Will automatically compile the init file as needed and load the
compiled version."
71
  (when el-get-user-package-directory
72
    (let* ((init-file-name (format "init-%s.el" package))
Ryan C. Thompson's avatar
Ryan C. Thompson committed
73 74 75 76
           (package-init-file
            (expand-file-name init-file-name el-get-user-package-directory))
           (file-name-no-extension (file-name-sans-extension package-init-file))
           (compiled-init-file (concat file-name-no-extension ".elc")))
77
      (when (file-exists-p package-init-file)
Ryan C. Thompson's avatar
Ryan C. Thompson committed
78 79 80 81
        (when el-get-byte-compile
          (el-get-byte-compile-file package-init-file))
        (el-get-verbose-message "el-get: load %S" file-name-no-extension)
        (load file-name-no-extension 'noerror)))))
82 83

(defun el-get-recipe-dirs ()
84
  "Return the elements of `el-get-recipe-path' that actually exist.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

Used to avoid errors when exploring the path for recipes"
  (reduce (lambda (dir result)
            (if (file-directory-p dir) (cons dir result) result))
          el-get-recipe-path :from-end t :initial-value nil))

;; recipe files are elisp data, you can't byte-compile or eval them on their
;; own, but having elisp indenting and colors make sense
(eval-and-compile
  (add-to-list 'auto-mode-alist '("\\.rcp\\'" . emacs-lisp-mode)))

;;
;; recipes
;;
(defun el-get-read-recipe-file (filename)
100
  "Read given FILENAME and return its content (a valid form is expected)."
101 102
  (condition-case err
      (with-temp-buffer
103
        (insert-file-contents filename)
104 105 106
        (read (current-buffer)))
    ((debug error)
     (error "Error reading recipe %s: %S" filename err))))
107 108 109 110

(defun el-get-recipe-filename (package)
  "Return the name of the file that contains the recipe for PACKAGE, if any."
  (let ((package-el  (concat (el-get-as-string package) ".el"))
Ryan C. Thompson's avatar
Ryan C. Thompson committed
111
        (package-rcp (concat (el-get-as-string package) ".rcp")))
112
    (loop for dir in el-get-recipe-path
Ryan C. Thompson's avatar
Ryan C. Thompson committed
113 114 115 116
          for recipe-el  = (expand-file-name package-el dir)
          for recipe-rcp = (expand-file-name package-rcp dir)
          if (file-exists-p recipe-el)  return recipe-el
          if (file-exists-p recipe-rcp) return recipe-rcp)))
117 118 119 120 121

(defun el-get-read-recipe (package)
  "Return the source definition for PACKAGE, from the recipes."
  (let ((filename (el-get-recipe-filename package)))
    (if filename
Ryan C. Thompson's avatar
Ryan C. Thompson committed
122
        (el-get-read-recipe-file filename)
123
      (error "El-get can not find a recipe for package \"%s\"" package))))
124

125 126 127 128 129 130 131 132
(defun el-get-all-recipe-file-names ()
  "Return the list of all file based recipe names.

The result may have duplicates."
  (loop for dir in (el-get-recipe-dirs)
        nconc (mapcar #'file-name-base
                      (directory-files dir nil "^[^.].*\.\\(rcp\\|el\\)$"))))

133
(defun el-get-read-all-recipe-files ()
134
  "Return the list of all file based recipes, formated like `el-get-sources'.
135 136 137

Only consider any given recipe only once even if present in
multiple dirs from `el-get-recipe-path'. The first recipe found
138
is the one considered."
139 140 141 142 143 144 145 146 147 148 149 150
  (let (packages)
    (loop
     for dir in (el-get-recipe-dirs)
     nconc (loop
            for recipe in (directory-files dir nil "^[^.].*\.\\(rcp\\|el\\)$")
            for filename = (concat (file-name-as-directory dir) recipe)
            for pname = (file-name-sans-extension
                         (file-name-nondirectory recipe))
            for package = (el-get-as-symbol pname)
            unless (member package packages)
            do (push package packages)
            and collect (ignore-errors (el-get-read-recipe-file filename))))))
151 152 153 154 155 156

(defun el-get-read-all-recipes ()
  "Return the list of all the recipes, formatted like `el-get-sources'.

  We first look in `el-get-sources' then in each directory listed
in `el-get-recipe-path' in order."
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
  (let* ((s-recipes (mapcar (lambda (s) (cons (plist-get s :name) s))
                            el-get-sources))
         (file-recipes (el-get-read-all-recipe-files)))
    (setq file-recipes
          (mapcar (lambda (f-recipe)
                    (let* ((pkg (plist-get f-recipe :name))
                           (s-recipe (assq pkg s-recipes)))
                      (if (null s-recipe) f-recipe
                        (setq s-recipes (assq-delete-all pkg s-recipes))
                        (el-get-recipe-merge f-recipe (cdr s-recipe)))))
                  file-recipes))
    (append (mapcar #'cdr s-recipes) file-recipes)))

(defun el-get-recipe-merge (r1 r2)
  "Merge recipe R1 into R2.

If R2 has a `:type' it completely replaces R1, otherwise, R1
fields are the default value and R2 may override them."
  (if (plist-get r2 :type)
      r2
    (loop with merged
          for (prop val) on (append r2 r1) by 'cddr
          unless (plist-member merged prop)
          nconc (list prop val) into merged
          finally return merged)))
182 183 184 185

(defun el-get-package-def (package)
  "Return a single `el-get-sources' entry for PACKAGE."
  (let ((source (loop for src in el-get-sources
Ryan C. Thompson's avatar
Ryan C. Thompson committed
186 187
                      when (string= package (el-get-source-name src))
                      return src)))
188 189

    (cond ((or (null source) (symbolp source))
Ryan C. Thompson's avatar
Ryan C. Thompson committed
190 191 192
           ;; not in `el-get-sources', or only mentioned by name
           ;; (compatibility from pre 3.1 era)
           (el-get-read-recipe package))
193

Ryan C. Thompson's avatar
Ryan C. Thompson committed
194 195
          ((null (plist-get source :type))
           ;; we got a list with no :type, that's an override plist
196
           (el-get-recipe-merge (el-get-read-recipe package) source))
Ryan C. Thompson's avatar
Ryan C. Thompson committed
197 198
          ;; none of the previous, must be a full definition
          (t source))))
199 200

(defun el-get-package-method (package-or-source)
201 202 203 204 205 206 207 208 209
  "Return the :type property (called method) of PACKAGE-OR-SOURCE.

If the package is built in to the current major version of Emacs,
return 'builtin."
  (let* ((def (if (or (symbolp package-or-source) (stringp package-or-source))
                  (el-get-package-def package-or-source)
                package-or-source))
         (builtin (plist-get def :builtin)))

210 211 212
    (when (integerp builtin)
      (warn "Integer argument for :builtin is obsolete.  Use strings instead.")
      (setq builtin (number-to-string builtin)))
213
    (if (and builtin (version<= builtin emacs-version))
214
        'builtin
215
      (plist-get def :type))))
216 217 218 219

(defalias 'el-get-package-type #'el-get-package-method)

(defun el-get-package-types-alist (statuses &rest types)
220
  "Return an alist of package names that are of given TYPES.
221 222 223 224

Only consider packages whose status is `member' of STATUSES,
which defaults to installed, required and removed.  Example:

225
  (el-get-package-types-alist \"installed\" 'http 'cvs)"
226
  (loop for src in (apply 'el-get-list-package-names-with-status
Ryan C. Thompson's avatar
Ryan C. Thompson committed
227 228
                          (cond ((stringp statuses) (list statuses))
                                ((null statuses) '("installed" "required"
229
                                                   "removed"))
Ryan C. Thompson's avatar
Ryan C. Thompson committed
230 231 232 233 234
                                (t statuses)))
        for name = (el-get-as-symbol src)
        for type = (el-get-package-type name)
        when (or (null types) (memq 'all types) (memq type types))
        collect (cons name type)))
235

236 237 238 239 240
(defun el-get-package-required-emacs-version (package-or-source)
  (let* ((def (if (or (symbolp package-or-source) (stringp package-or-source))
                  (el-get-package-def package-or-source)
                package-or-source)))
    (el-get-plist-get-with-default
241 242
        def :minimum-emacs-version
      0)))
243

244 245 246 247
(defun el-get-version-to-list (version)
  "Convert VERSION to a standard version list.

Like the builtin `version-to-list', this function accepts a
248
string.  Unlike the builtin, it will also accept a single number,
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
which will be wrapped into a single-element list, or a or a list
of numbers, which will be returned unmodified."
  (cond
   ;; String
   ((stringp version)
    (version-to-list version))
   ;; Single number
   ((numberp version)
    (list version))
   ;; List of numbers
   ((and (listp version)
         (null (remove-if 'numberp version)))
    version)
   (t (error "Unrecognized version specification: %S" version))))

264
(defun el-get-error-unless-required-emacs-version (package-or-source)
265
  "Raise an error if `emacs-major-version' is less than package's requirement."
266
  (let* ((pname (el-get-source-name package-or-source))
267 268
         (required-version (el-get-package-required-emacs-version
                            package-or-source))
269 270
         (required-version-list (el-get-version-to-list required-version)))
    (when (version-list-< (version-to-list emacs-version) required-version-list)
271 272
      (error "Package %s requires Emacs version %s or higher, but the current\
 Emacs is only version %s"
273
             pname required-version emacs-version))))
274

275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
(defun el-get-envpath-prepend (envname head)
  "Prepend HEAD in colon-separated environment variable ENVNAME.
This is effectively the same as doing the following in shell:
    export ENVNAME=HEAD:$ENVNAME

Use this to modify environment variable such as $PATH or $PYTHONPATH."
  (setenv envname (el-get-envpath-prepend-1 (getenv envname) head)))

(defun el-get-envpath-prepend-1 (paths head)
  "Return \"HEAD:PATHS\" omitting duplicates in it."
  (let ((pplist (split-string (or paths "") ":" 'omit-nulls)))
    (mapconcat 'identity
               (remove-duplicates (cons head pplist)
                                  :test #'string= :from-end t)
               ":")))

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
(defvar el-get-check--last-file-or-buffer nil
  "The last file-or-buffer checked.")

(defun el-get-check-redo ()
  "Rerun `el-get-check-recipe' with last recipe."
  (interactive)
  (when el-get-check--last-file-or-buffer
    (el-get-check-recipe
     el-get-check--last-file-or-buffer)))

(defvar el-get-check-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map special-mode-map)
    (define-key map "g" #'el-get-check-redo)
    map)
  "Mode map for `el-get-check-mode'.")

(define-derived-mode el-get-check-mode special-mode "El-Get Check"
  "Special mode for `el-get-check-recipe' buffers.
See Info node `(el-get) Authoring Recipes'.")

312 313 314
(defvar el-get-check-suppressed-warnings ()
  "List of `el-get-check-recipe' warnings to suppress.

315 316
Current possibe elements are:
 `features', `github', `autoloads'")
317

318 319 320 321 322 323 324 325 326 327
(defun el-get-check-recipe-batch-1 (recipe-file)
  (let ((warning-prefix-function
         (lambda (level entry)
           (list level (format "%s:%s" el-get-check--last-file-or-buffer
                               (format (nth 1 entry) ""))))))
    (condition-case err
        (el-get-check-recipe (file-relative-name recipe-file))
      (error (lwarn '(el-get) :emergency "%s" (error-message-string err))
             1))))

328 329
(defun el-get-check-recipe-batch ()
  "emacs -Q -batch -f el-get-check-recipe-batch [-Wno-<warning>...] *.rcp"
330 331
  (assert noninteractive nil
          "`el-get-check-recipe-batch' should only be used with -batch")
332
  (setq vc-handled-backends nil) ; avoid loading VC during batch mode
333 334 335 336
  (loop for arg in command-line-args-left
        if (string-match "^-Wno-\\(.*\\)" arg)
        do (push (intern (match-string 1 arg)) el-get-check-suppressed-warnings)
        else summing
337 338 339 340
        (if (file-directory-p arg)
            (reduce #'+ (directory-files arg t "\\.rcp$" t)
                    :key #'el-get-check-recipe-batch-1 :initial-value 0)
          (el-get-check-recipe-batch-1 arg))
341 342 343
        into errors
        finally (progn (message "%d warning/error(s) total." errors)
                       (kill-emacs (if (zerop errors) 0 1)))))
344

345 346 347 348 349 350 351 352 353 354 355
(defun el-get-check-recipe (file-or-buffer)
  "Check the format of the recipe.
Please run this command before sending a pull request.
Usage: M-x el-get-check-recipe RET

You can run this function from checker script like this:
    test/check-recipe.el PATH/TO/RECIPE.rcp

When used as a lisp function, FILE-OR-BUFFER must be a buffer
object or a file path."
  (interactive (list (current-buffer)))
356
  (setq el-get-check--last-file-or-buffer file-or-buffer)
357 358
  (if (bufferp file-or-buffer)
      (with-current-buffer file-or-buffer
359
        (el-get-check-recipe-in-current-buffer (buffer-file-name)))
360 361
    (with-temp-buffer
      (insert-file-contents file-or-buffer)
362
      (el-get-check-recipe-in-current-buffer file-or-buffer))))
363

364
(eval-and-compile
365 366 367 368 369 370 371
  (unless (fboundp 'file-name-base)     ; new in 24.3
    (defun file-name-base (&optional filename)
      "Return the base name of the FILENAME: no directory, no extension.
FILENAME defaults to `buffer-file-name'."
      (file-name-sans-extension
       (file-name-nondirectory (or filename (buffer-file-name)))))))

372 373 374 375 376 377 378
(defvar el-get-check-warning-buffer)

(defun el-get-check-warning (level message &rest args)
  (declare (indent 1))
  (display-warning '(el-get recipe) (apply #'format message args)
                   level el-get-check-warning-buffer))

379
(defun el-get-check-recipe-in-current-buffer (recipe-file-name)
380
  (let ((inhibit-read-only t)
381
        (numerror 0)
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
        (el-get-check-warning-buffer (get-buffer-create "*el-get check recipe*")))
    (display-buffer el-get-check-warning-buffer)
    (with-current-buffer el-get-check-warning-buffer
      (erase-buffer)
      (el-get-check-mode))
    (let ((recipe (save-excursion
                    (goto-char (point-min))
                    (prog1 (read (current-buffer))
                      (let ((lvl-err (condition-case err
                                         (progn (read (current-buffer))
                                                `(:warning . "Extra data following recipe"))
                                       (end-of-file nil)
                                       (error `(:error . ,(error-message-string err))))))
                        (when lvl-err
                         (incf numerror)
                         (let ((el-get-check--last-file-or-buffer
                                (format "%s:%d:%d" recipe-file-name
                                        (line-number-at-pos) (current-column))))
                           (el-get-check-warning (car lvl-err) (cdr lvl-err)))))))))
      (when (and recipe-file-name
                 (not (string= (file-name-base recipe-file-name)
                               (plist-get recipe :name))))
        (incf numerror)
        (el-get-check-warning :error
          "File name should match recipe name."))
      ;; Check if userspace property is used.
      (loop for key in '(:before :after)
            for alt in '(:prepare :post-init)
            when (plist-get recipe key)
            do (progn
                 (el-get-check-warning :warning
                   "Property %S is for user.  Use %S instead."
                   key alt)
                 (incf numerror)))
      (destructuring-bind (&key type url autoloads feats builtin
                                &allow-other-keys)
          recipe
        ;; let-binding `features' causes `provide' to throw error
        (setq feats (plist-get recipe :features))
        ;; Is github type used?
422 423
        (when (and (not (memq 'github el-get-check-suppressed-warnings))
                   (eq type 'git) (string-match "//github.com/" url))
424 425 426 427
          (el-get-check-warning :warning
            "Use `:type github' for github type recipe")
          (incf numerror))
        ;; Warn when `:autoloads nil' is specified.
428 429
        (when (and (not (memq 'autoloads el-get-check-suppressed-warnings))
                   (null autoloads) (plist-member recipe :autoloads))
430 431
          (el-get-check-warning :warning
            "Are you sure you don't need autoloads?
432
  This property should be used only when the library takes care of
433
  the autoload."))
434 435 436 437 438
        ;; Warn when `:features t' is specified
        (when (and (not (memq 'features el-get-check-suppressed-warnings))
                   feats)
          (el-get-check-warning :warning
            "Are you sure you need features?
439
  If this library has `;;;###autoload' comment (a.k.a autoload cookie),
440
  you don't need `:features'."))
441 442 443 444
        ;; Check if `:builtin' is used with an integer
        (when (integerp builtin)
          (el-get-check-warning :warning
            "Usage of integers for :builtin is obsolete.
445
  Use a version string like \"24.3\" instead.")))
446 447 448 449 450 451 452 453
      ;; Check for required properties.
      (loop for key in '(:description :name)
            unless (plist-get recipe key)
            do (progn
                 (el-get-check-warning :error
                   "Required property %S is not defined." key)
                 (incf numerror)))
      (insert (format "\n%s: %s error(s) found." recipe-file-name numerror)))
454 455
    numerror))

456
(provide 'el-get-recipes)
457 458

;;; el-get-recipes.el ends here