Skip to content
Newer
Older
100644 708 lines (570 sloc) 25.1 KB
d2d69fe @codermattie First commit of Grail 0.4.0 with Git/GitHub
authored Nov 5, 2011
1 ;;;----------------------------------------------------------------------
2 ;; grail-profile.el
3 ;; Primary Author: Mike Mattie
4 ;; Copyright (C) 2010 Mike Mattie
5 ;; License: LGPL-v3
6 ;;----------------------------------------------------------------------
7 (eval-when-compile
8 (require 'cl)
9 (require 'grail-fn))
10
11 (defvar grail-requested-profiles
12 nil
13 "List of grail profiles requested by the user.")
14
15 (defvar grail-failed-profiles
16 nil
17 "List of grail profiles that failed to load.")
18
19 (defvar grail-loaded-profiles
20 nil
21 "List of grail profiles that have loaded.")
22
23 (defun grail-load-requested-profiles ()
24 "grail-load-requested-profiles
25
26 Load the profiles in the request list grail-requested-profiles.
27 "
28 (when grail-requested-profiles
29 (let
30 ((order-sorted (sort grail-requested-profiles (lambda ( a b )
31 (when (< (car a) (car b)) t)
32 )) ))
33 (setq grail-requested-profiles nil)
34
35 (mapc
36 (lambda ( profile-order )
37 (message "grail: loading order %d -> %s" (car profile-order) (cdr profile-order))
38 (mapc
39 (lambda ( profile )
40 (message "grail: loading profile %s" (concat grail-local-profiles profile))
41 (let
42 ((trapped (catch 'grail-trap
43 (load-elisp-if-exists (concat grail-local-profiles profile)))))
44 (if (consp trapped)
45 (progn
46 (push (cons (car profile-order) profile) grail-failed-profiles)
47 (message "grail: profile %s failed to load" profile)
48 (apply 'grail-report-errors (format "grail: profile %s failed to load" profile) trapped))
49 (push profile grail-loaded-profiles)) ))
50 (cdr profile-order)))
51 order-sorted)
52 t)))
53
54 (defun grail-retry-failed-profiles ()
55 "grail-retry-failed-profiles
56
57 Retry the loading of any profiles that previously failed to load.
58 "
59 (interactive)
60
61 (setq grail-requested-profiles grail-failed-profiles)
62 (setq grail-failed-profiles nil)
63 (grail-load-requested-profiles))
64
65 (defun use-grail-profiles ( order &rest request-list )
66 "use-grail-groups: ORDER LIST
67
68 request a list of string quoted groups to be loaded after the configuration
69 files have been loaded.
70 "
71 (push (cons order request-list) grail-requested-profiles))
72
73 ;;----------------------------------------------------------------------
74 ;; installation support routines.
75 ;;----------------------------------------------------------------------
76
77 (defvar grail-save-downloads nil
78 "when t downloaded archive files will be saved to grail-dist-dir")
79
80 (defun grail-recursive-delete-directory ( path )
81 "grail-recursive-delete-directory PATH
82
83 recursively delete the directory PATH. t on success.
84 "
85 (condition-case trapped-error
86 (if (dir-path-if-accessible path)
87 (if (equal 0 (call-process-shell-command "rm" nil nil nil "-r" path))
88 t
89 (throw 'grail-trap
90 '((message "grail-recursive-delete-directory path %s is not a directory or the user does not have permissions" path))))
91 (throw 'grail-trap
92 '((message "grail-recursive-delete-directory path %s is not a directory or the user does not have permissions" path))))
93 (error (throw 'grail-trap
94 (list (message "grail-recursive-delete-directory failed") (format-signal-trap trapped-error)))) ))
95
96 (defun grail-dist-install-directory ( &optional package )
97 "grail-dist-install-directory &optional string:PACKAGE
98
99 Ensure that the installation directory exists. The default is grail-dist-elisp,
100 however for multi-file packages an optional package name can be supplied.
101
102 The path of the installation directory is returned for the installer's use.
103 "
104 (grail-garuntee-dir-path (expand-file-name
105 (concat
106 (if package
107 (concat grail-dist-elisp "/" package)
108 grail-dist-elisp)
109 "/"))))
110
111 (defun grail-download-dir-and-file-path ( name )
112 (let
113 ((dl-dir nil))
114
115 (when (condition-case trapped-error
116 (progn
117 (setq dl-dir (if grail-save-downloads
118 grail-dist-dir
119 (make-temp-file "grail" t)))
120 t)
121 (error
122 (throw 'grail-trap
123 '((message "grail: grail-download-dir-and-file-path could not create a download path for %s" name))) ))
124 (cons dl-dir (expand-file-name (concat dl-dir "/" name))) )))
125
126 (defun grail-cleanup-download ( dl-dir-and-file &optional ignore-save )
127 "grail-cleanup-download
128
129 delete the directory and the downloaded files.
130
131 TODO: save downloads option.
132 "
133 (when dl-dir-and-file
134 (if grail-save-downloads
135 ;; when grail-save-downloads is enabled absolutely do not recursive delete !
136 (when (not ignore-save)
137 (delete-file (cdr dl-dir-and-file)))
138 ;; otherwise it is a temp dir so nuke it
139 (grail-recursive-delete-directory (car dl-dir-and-file))) ))
140
141 ;;----------------------------------------------------------------------
142 ;; async chain.
143 ;;----------------------------------------------------------------------
144
145 (defun grail-process-async-chain ( start-process-fn doesnt-start-fn proc-fail-fn
146 do-after-fn next-fn)
147 "grail-process-async-chain START-PROCESS-FN DOESNT-START-FN PROC-FAIL-FN
148 DO-AFTER-FN NEXT-FN
149
150 create asynchronous processes that can be changed. START-PROCESS-FN
151 creates a process object. This function generates a process sentinel
152 and attaches the sentinel to the process.
153
154 a number of lambdas are supplied in the arguments to fill in the body
155 of the process sentinel.
156
157 DOESNT-START-FN: executed if the process does not start.
158
159 PROC-FAIL-FN : executed if the process returns an error (a non-zero exit code).
160 DO-AFTER-FN : executed when the process exits with success (0 exit code)
161 NEXT-FN : when DO-AFTER-FN returns non-nil this function is executed,
162 typically to chain another async process, but it can do
163 anything.
164
165 With this function processes can be changed by nesting another
166 grail-process-async-chain as the tail, or NEXT-FN function for
167 a sequence of process execution.
168 "
169 (lexical-let
170 ((async-proc (funcall start-process-fn))
171 (no-start doesnt-start-fn)
172 (fail-fn proc-fail-fn)
173 (after-fn do-after-fn)
174 (chain-fn next-fn))
175
176 (if (or (not (processp async-proc))
177 (not (process-status async-proc)))
178 (funcall doesnt-start-fn)
179 (progn
180 ;; setup a lambda process sentinal that does the chaining.
181 (set-process-sentinel async-proc
182 ;; a sentinal that accepts status-change signals
183 (lambda ( bound-proc status-change )
184 (when (memq (process-status bound-proc) '(signal exit))
185 ;; do something when the process exits
186 (if (equal 0 (process-exit-status bound-proc))
187
188 ;; If bound-proc process exits with success call the
189 ;; do-after-exit function (do-after-fn).
190
191 ;; If (do-after-fn) returns non-nil, and the (next-fn)
192 ;; is non-nil run that function.
193 (and (funcall after-fn) (and chain-fn (funcall chain-fn)))
194
195 ;; if the process exits non-zero call (proc-fail-fn)
196 (funcall fail-fn)) ))) ))))
197
198 ;;----------------------------------------------------------------------
199 ;; installation library
200 ;;----------------------------------------------------------------------
201
202 (defun grail-file-url ( name url &optional path )
203 "grail-file-url NAME URL &optional PATH
204
205 install from URL into PATH with name NAME. nil is returned
206 when successful, otherwise an error is thrown.
207 "
208 (condition-case error-trap
209 (let
210 ((install-path (concat (grail-dist-install-directory path) name)))
211
212 (with-temp-buffer
213 (url-insert-file-contents url)
214 (let
215 ((buffer-file-coding-system 'no-conversion))
216 (write-file install-path)))
217
218 (message "grail-file-installer: installed of %s to %s completed" name install-path))
219 nil
220 (error
221 (format "grail-file-installer for %s failed with: %s"
222 name (format-signal-trap error-trap))) ))
223
224 (defun grail-wget-url-async ( url path output-buffer )
225 "grail-wget-url-async URL PATH OUTPUT-BUFFER
226
227 retrieve the URL to PATH, with OUTPUT-BUFFER as the output
228 buffer. The process object created is returned, or nil if a
229 process could not be created.
230 "
231 (condition-case trapped-error
232 (start-process-shell-command "grail-wget" output-buffer
233 "wget"
234 "--progress=dot:binary"
235 (quote-string-for-shell url) "-O" (quote-string-for-shell path))
236 (error
237 (progn
238 (message "grail-wget-url failed %s" (format-signal-trap trapped-error))
239 nil)) ))
240
241 ;;
242 ;; tar
243 ;;
244
245 (defun grail-untar-async ( path target-dir compression output-buffer )
246 "grail-untar-async PATH DIR COMPRESSION OUTPUT-BUFFER
247
248 untar PATH in DIR with output going to OUTPUT-BUFFER.
249 return the process object or nil if there was an error.
250
251 Only files with the \".el\" extension will be extracted.
252 "
253 (condition-case trapped-error
254 (start-process-shell-command "grail-untar" output-buffer
255 "tar"
256 (concat
257 "xv"
258 (cond
259 ((equal "gz" compression) "z")
260 ((equal "bz2" compression) "j")
261 (t (signal error (format "grail: error! unsupported compression %s" compression))))
262 "f")
263 (quote-string-for-shell path)
264 "-C" (quote-string-for-shell target-dir)
265 "--wildcards" (quote-string-for-shell "*.el"))
266 (error
267 (progn
268 (message "grail-untar-async failed %s" (format-signal-trap trapped-error))
269 nil)) ))
270
271 (defun grail-untar-local-archive ( path compression )
272 "grail-untar-local-archive PATH COMPRESSION
273
274 extract the local archive PATH in directory name with COMPRESSION.
275 "
276 (lexical-let
277 ((archive-path path)
278 (grail-buffer (pop-to-buffer (generate-new-buffer "*grail-install*") nil t)))
279
280 (grail-process-async-chain
281 ;; start the untar
282 (lambda ()
283 (grail-untar-async archive-path grail-dist-elisp compression grail-buffer))
284
285 ;; if it doesn't start
286 (lambda ()
287 (message "archive program did not start for %s!" archive-path))
288
289 ;; FIXME: how do we clean up the target directory ?
290 (lambda ()
291 (message "extracting %s failed!" archive-path))
292
293 ;; what to do when it finishes.
294 (lambda ()
295 (message "extracting %s has completed." archive-path))
296
297 ;; no chaining
298 nil)))
299
300 (defun grail-untar-remote-archive ( name url compression )
301 "grail-untar-remote-archive NAME URL COMPRESSION
302
303 Download a tarball from a remote url and install it. It is currently
304 hard-coded for tar, but that could be changed fairly easily.
305 "
306 (save-excursion
307 (lexical-let*
308 ((dl-dir-and-file nil)
309 (old-window (selected-window))
310
311 ;; open a new window but do not put it in recently selected
312 (grail-buffer (pop-to-buffer (generate-new-buffer "*grail-install*") nil t))
313 (grail-window (not (eq old-window (selected-window)))))
314
315 (catch 'abort
316 ;; confirm with the user that they want to install the file.
317 (unless (yes-or-no-p (format "download and install %s? " name))
318 (throw 'abort nil))
319
320 ;; signal the start of the download in the grail buffer.
321 (insert (format "Starting the download of %s\n" url))
322
323 ;; create a temporary directory to download into
324 (unless (setq dl-dir-and-file (grail-download-dir-and-file-path (concat name ".tar." compression)))
325 (throw 'abort "could not create a temporary directory for the download"))
326
327 (lexical-let
328 ((dl-url url)
329 (compression-type compression))
330
331 (grail-process-async-chain
332 ;; start the download with wget
333 (lambda ()
334 (grail-wget-url-async
335 dl-url
336 (cdr dl-dir-and-file)
337 grail-buffer))
338
339 ;; the downloader doesn't start cleanup function
340 (lambda ()
341 (insert "could not start the download! Install aborted.\n")
342 (grail-cleanup-download dl-dir-and-file t))
343
344 ;; the downloader fail cleanup function
345 (lambda ()
346 (grail-cleanup-download dl-dir-and-file t)
347 (message "download of %s failed! Install aborted, and downloads deleted." (cdr dl-dir-and-file)))
348
349 ;; the downloader succeeded function
350 (lambda ()
351 (insert "grail: download completed\n")
352 t)
353
354 ;; the chain function
355 (lambda ()
356 (grail-process-async-chain
357 ;; start the untar
358 (lambda ()
359 (message "starting the untar")
360 (grail-untar-async (cdr dl-dir-and-file) (grail-dist-install-directory) compression-type grail-buffer))
361
362 ;; tar doesn't start cleanup function
363 (lambda ()
364 (insert "could not start tar to extract the downloaded archive. Install aborted, deleting downloads.\n")
365 (grail-cleanup-download dl-dir-and-file t))
366
367 ;; the tar fail cleanup function
368 (lambda ()
369 (insert (format "could not install files in %s from downloaded archive." grail-dist-elisp))
370 (grail-cleanup-download dl-dir-and-file t))
371
372 ;; the tar succeeded function
373 (lambda ()
374 (insert "grail: Installation Completed ! Re-Generating load-path\n")
375 (grail-extend-load-path)
376
377 (insert "grail: cleaning up downloads\n")
378 (grail-cleanup-download dl-dir-and-file)
379
380 (delete-windows-on grail-buffer)
381 (kill-buffer grail-buffer)
382 t)
383
384 ;; terminate the chain.
385 nil))) )
386 ;; return nil if an abort is not thrown.
387 nil))
388 )) ;; save excursion and the defun.
389
390 ;;----------------------------------------------------------------------
391 ;; version control
392 ;;
393 ;; The preferred way to integrate third party packages: a version control
394 ;; checkout.
395 ;;----------------------------------------------------------------------
396
397 ;;
398 ;; cvs
399 ;;
400
401 (defun grail-cvs-async ( url module output-buffer )
402 "grail-cvs-async URL PATH OUTPUT-BUFFER
403
404 "
405 (condition-case trapped-error
406 (let
407 ((default-directory (grail-garuntee-dir-path grail-dist-cvs)))
408
409 (start-process-shell-command "grail-cvs" output-buffer
410 "cvs"
411 "-d"
412 (quote-string-for-shell url)
413 "co"
414 (quote-string-for-shell module)))
415 (error
416 (progn
417 (message "grail-cvs-async failed %s" (format-signal-trap trapped-error))
418 nil)) ))
419
420 (defun grail-cvs-installer ( module url )
421 (let
422 ((grail-buffer (pop-to-buffer (generate-new-buffer "*grail-cvs*") nil t)))
423 (grail-cvs-async url module grail-buffer) ))
424
425 ;;
426 ;; git
427 ;;
428
429 (defun grail-git-async ( url module output-buffer )
430 "grail-git-async URL PATH OUTPUT-BUFFER
431
432 retrieve the URL to PATH, with OUTPUT-BUFFER as the output
433 buffer. The process object created is returned, or nil if a
434 process could not be created.
435 "
436 (condition-case trapped-error
437 (let
438 ((default-directory (grail-garuntee-dir-path grail-dist-git)))
439
440 (start-process-shell-command "grail-git" output-buffer
441 "git"
442 "clone"
443 (quote-string-for-shell url)
444 (quote-string-for-shell module)))
445 (error
446 (progn
447 (message "grail-git-async failed %s" (format-signal-trap trapped-error))
448 nil)) ))
449
450 (defun grail-git-installer ( module url )
451 (let
452 ((grail-buffer (pop-to-buffer (generate-new-buffer "*grail-git*") nil t)))
453 (grail-git-async url module grail-buffer) ))
454
455 (defun grail-svn-async ( url module output-buffer )
456 "grail-svn-async URL PATH OUTPUT-BUFFER
457
458 retrieve the URL to PATH, with OUTPUT-BUFFER as the output
459 buffer. The process object created is returned, or nil if a
460 process could not be created.
461 "
462 (condition-case trapped-error
463 (let
464 ((default-directory (grail-garuntee-dir-path grail-dist-svn)))
465
466 (start-process-shell-command "grail-svn" output-buffer
467 "svn"
468 "checkout"
469 (quote-string-for-shell url)
470 (quote-string-for-shell module)))
471 (error
472 (progn
473 (message "grail-svn-async failed %s" (format-signal-trap trapped-error))
474 nil)) ))
475
476 (defun grail-svn-installer ( module url )
477 (let
478 ((grail-buffer (pop-to-buffer (generate-new-buffer "*grail-svn*") nil t)))
479 (grail-svn-async url module grail-buffer) ))
480
481 (defun grail-package-installer ( module )
482 (package-install module))
483
484 ;;----------------------------------------------------------------------
485 ;; installer front ends
486 ;;----------------------------------------------------------------------
487
488 ;;
489 ;; emacswiki
490 ;;
491
492 ;;
493 ;; emacsmirror
494 ;;
495
496 ;;----------------------------------------------------------------------
497 ;; grail-define-installer
498 ;;----------------------------------------------------------------------
499
500 ;; The arg helpers adapt the installer definition process to specific
501 ;; installers.
502
503 (defun grail-target ( url-pair )
504 (car url-pair))
505
506 (defun grail-url ( url-pair )
507 (cdr url-pair))
508
509 (defun grail-make-pair ( target url )
510 (cons target url))
511
512 ;; From a uniform single URL argument parameter and the dynamic scoped
513 ;; bindings of grail-define-installer they generate the installer
514 ;; function calls with the parameters required by the installer
515 ;; function signatures which vary based upon their specific needs.
516
517 (defun grail-file-args ( install-pair )
518 (cons 'grail-file-url
519 (if install-many
520 ;; When there are multiple install pairs pass the package name
521 ;; as a sub-directory to install the files in.
522
523 ;; When there is a single install pair the target part needs to
524 ;; have the .el extension added.
525 `(,(grail-target install-pair) ,(grail-url install-pair) ,name)
526 `(,(concat (grail-target install-pair) ".el") ,(grail-url install-pair)))))
527
528 (defun grail-tar-args ( install-pair )
529 ;; When installing a local archive only the path and the compression
530 ;; need be known, as the target directory and the like cannot be
531 ;; ascertained without inspecting the archive.
532
533 ;; for a remote archive pass the name, the url, and the
534 ;; compression. The name is used for naming the download. This is
535 ;; especially useful when the downloads are saved.
536 (if (string-match "archived:\\(.*\\)" (grail-url install-pair))
537 `(grail-untar-local-archive ,(concat grail-dist-archive (match-string 1 (grail-url install-pair))) ,compression)
538 `(grail-untar-remote-archive ,(grail-target install-pair) ,(grail-url install-pair) ,compression)))
539
540 (defun grail-cvs-args ( install-pair )
541 `(grail-cvs-installer ,(grail-target install-pair) ,(grail-url install-pair)))
542
543 (defun grail-git-args ( install-pair )
544 `(grail-git-installer ,(grail-target install-pair) ,(grail-url install-pair)))
545
546 (defun grail-svn-args ( install-pair )
547 `(grail-svn-installer ,(grail-target install-pair) ,(grail-url install-pair)))
548
549 (defun grail-decompose-installer-type ( type-spec )
550 "grail-decompose-installer-type SPEC
551
552 Spec is either a single value such as file|cvs, or a pair such
553 as tar:bz2. When a pair is detected return it as a cons cell,
554 or simply return the spec as given.
555 "
556 (let
557 ((split-index (string-match ":" type-spec)))
558
559 (if split-index
560 (cons (substring type-spec 0 split-index) (substring type-spec (match-end 0)))
561 type-spec)))
562
563 (defun grail-define-installer ( name type &rest url-list )
564 "grail-define-installer NAME TYPE &rest URLS
565
566 define a installer for a package NAME.
567
568 The type of the installer indicates the format of the URL.
569
570 TYPE is the format of the URL for handling things like
571 compression,archives, and RCS systems.
572
573 recognized TYPE's : file, tar:bz2, tar:gz, cvs svn git pkg
574
575 download a plain elisp file: (grail-define-installer \"bob\" \"file\" \"URL\")
576 download an tar.bz2 archive: (grail-define-installer \"bob\" \"tar:bz2\" \"URL\")
577 cvs checkout: : (grail-define-installer \"bob\" \"cvs\" \"pserver\")
578 git checkout: : (grail-define-installer \"bob\" \"git\" \"url\")
579 svn checkout: : (grail-define-installer \"bob\" \"svn\" \"url\")
580 ELPA package: : (grail-define-installer \"bob\" \"pkg\")
581
582 Most of the time a single URL suffices. Many packages are a
583 single elisp file, or a single tarball.
584
585 Other packages such as icicles are several elisp files, or
586 possibly several archives.
587
588 In this case a list of cons pairs can be given as the
589 URL. When this happens NAME becomes a sub-directory they are
590 installed to, and the files a list of (name . url) pairs.
591
592 (grail-define-installer PACKAGE \"file\"
593 '(\"foo.el\" . \"URL\")
594 '(\"bar.el\" . \"URL\")
595
596 this would install as:
597 PACKAGE/foo.el
598 PACKAGE/bar.el
599 "
600 (let
601 ((install-many (> (length url-list) 1))
602 (install-type (grail-decompose-installer-type type))
603 (compression nil))
604
605 (when (consp install-type)
606 (setq compression (cdr install-type))
607 (setq install-type (car install-type)))
608
609 ;; do a bit more input checking than usual as the definitions are user inputs.
610
611 (unless (and (stringp name) (> (length name) 0))
612 (throw 'grail-trap
613 '((format "installer expected package name string but got %s instead" (princ name)))))
614
615 (if (string-equal "pkg" type)
616 `(grail-package-installer ',(car url-list)) ;; a package system is the only form that won't have a url.
617 (progn
618 (unless url-list
619 (throw 'grail-trap
620 '((format "grail-define-installer: installer definition for %s must be given urls" name))))
621
622 (let
623 ((installer-calls
624 (mapcar
625 (lambda ( url )
626 (let
627 ;; simpler definitions don't require a target,url pair.
628 ;; make one up to prevent the test for and handling of
629 ;; this case from propagating down the fan-out from
630 ;; grail-define-installer.
631 ((install-pair (if (consp url) url (cons name url))))
632
633 (cond
634 ((string-equal "file" install-type) (grail-file-args install-pair))
635 ((string-equal "cvs" install-type) (grail-cvs-args install-pair))
636 ((string-equal "git" install-type) (grail-git-args install-pair))
637 ((string-equal "svn" install-type) (grail-svn-args install-pair))
638 ((string-match "tar" install-type) (grail-tar-args install-pair))
639
640 (t (throw 'grail-trap
641 '((format "grail-define-installer: I don't have an installer for %s" install-type))))) ))
642 url-list)))
643
644 ;; if there are several call-outs sequence them with and so that
645 ;; a failure terminates the install process. for a single
646 ;; call-out extract the call from the map list and return it.
647 (if install-many
648 (cons 'and installer-calls)
649 (car installer-calls)))) ) ))
650
651 (defun grail-run-installer ( installer )
652 "grail-run-installer installer
653
654 run an installer created by grail-define-installer.
655 "
656 (condition-case trap
657 (eval installer)
658 (error
659 (throw 'grail-trap (list (format "installer error. please report \"%s\" to %s"
660 (format-signal-trap trap)
661 grail-maintainer-email))) )) )
662
663 (defun grail-repair-by-installing ( package installer )
664 "grail-repair-by-installing symbol:PACKAGE list|function:INSTALLER
665
666 Attempt to install PACKAGE and load the missing
667 dependency. INSTALLER is either defined by
668 grail-define-installer or a custom installer function.
669
670 t is returned on success and nil for failure.
671 "
672 (let
673 ((package-name (symbol-name package)))
674
675 (catch 'installer-abort
676 (condition-case install-trap
677
678 ;; run the installer
679 (cond
680 ((functionp installer) (funcall installer))
681 ((listp installer) (grail-run-installer installer))
682 (t (throw 'grail-trap
683 '((format "unhandled installer type: not a function or a list %s"
684 (princ (type-of installer)))))))
685
686 ;; if there wasn't a error update the load path.
687 (grail-extend-load-path)
688
689 (error
690 (message "grail repair of package %s failed with %s" package-name (format-signal-trap install-trap))
691 (throw 'installer-abort nil)))
692
693 ;; try to load it again.
694 (condition-case load-trap
695 (require package)
696 (error
697 (message "repair of package %s : installed ok, but loading failed anyways - %s."
698 package-name (format-signal-trap load-trap))
699 (throw 'installer-abort nil)) )
700
701 (message "installation repair of dependency %s completed :)" package-name)
702 t)))
703
704 (defun grail-load ( package installer )
705 (or (require package nil t) (grail-repair-by-installing package installer)))
706
707 (provide 'grail-profile)
Something went wrong with that request. Please try again.