aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2014-04-28 23:00:57 +0200
committerLudovic Courtès <ludo@gnu.org>2014-04-28 23:24:17 +0200
commit21b679f6944f4e1f09f949322f5242b761dc22a7 (patch)
tree501a32d37b7765db7e7a8c046fcd99ae40ed4877
parentba948b4fa0a088f42815aa104e651c9d14d8a1ba (diff)
downloadguix-21b679f6944f4e1f09f949322f5242b761dc22a7.tar
guix-21b679f6944f4e1f09f949322f5242b761dc22a7.tar.gz
Add (guix gexp).
* guix/gexp.scm: New file. * tests/gexp.scm: New file. * Makefile.am (MODULES): Add guix/gexp.scm. (SCM_TESTS): Add tests/gexp.scm. * doc/guix.texi (Derivations): Add #:inputs in 'derivation' example. Mark 'build-expression->derivation' as deprecated, refer to "G-Expressions". Remove paragraph about code strata. (G-Expressions): New node.
-rw-r--r--.dir-locals.el9
-rw-r--r--Makefile.am2
-rw-r--r--doc/guix.texi219
-rw-r--r--guix/gexp.scm391
-rw-r--r--tests/gexp.scm234
5 files changed, 837 insertions, 18 deletions
diff --git a/.dir-locals.el b/.dir-locals.el
index 49380fe4ba..a6135b171e 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -6,6 +6,7 @@
(scheme-mode
.
((indent-tabs-mode . nil)
+ (eval . (put 'eval-when 'scheme-indent-function 1))
(eval . (put 'test-assert 'scheme-indent-function 1))
(eval . (put 'test-equal 'scheme-indent-function 1))
(eval . (put 'test-eq 'scheme-indent-function 1))
@@ -31,7 +32,13 @@
(eval . (put 'with-monad 'scheme-indent-function 1))
(eval . (put 'mlet* 'scheme-indent-function 2))
(eval . (put 'mlet 'scheme-indent-function 2))
- (eval . (put 'run-with-store 'scheme-indent-function 1))))
+ (eval . (put 'run-with-store 'scheme-indent-function 1))
+
+ ;; Recognize '~' and '$', as used for gexps, as quotation symbols. This
+ ;; notably allows '(' in Paredit to not insert a space when the preceding
+ ;; symbol is one of these.
+ (eval . (modify-syntax-entry ?~ "'"))
+ (eval . (modify-syntax-entry ?$ "'"))))
(emacs-lisp-mode . ((indent-tabs-mode . nil)))
(texinfo-mode . ((indent-tabs-mode . nil)
(fill-column . 72))))
diff --git a/Makefile.am b/Makefile.am
index 8d425f1be9..d01032f530 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -37,6 +37,7 @@ MODULES = \
guix/download.scm \
guix/git-download.scm \
guix/monads.scm \
+ guix/gexp.scm \
guix/profiles.scm \
guix/serialization.scm \
guix/nar.scm \
@@ -139,6 +140,7 @@ SCM_TESTS = \
tests/snix.scm \
tests/store.scm \
tests/monads.scm \
+ tests/gexp.scm \
tests/nar.scm \
tests/union.scm \
tests/profiles.scm
diff --git a/doc/guix.texi b/doc/guix.texi
index f8d71fdace..9fb226c651 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -1305,6 +1305,7 @@ package definitions.
* The Store:: Manipulating the package store.
* Derivations:: Low-level interface to package derivations.
* The Store Monad:: Purely functional interface to the store.
+* G-Expressions:: Manipulating build expressions.
@end menu
@node Defining Packages
@@ -1762,13 +1763,21 @@ to a Bash executable in the store:
"echo hello world > $out\n" '())))
(derivation store "foo"
bash `("-e" ,builder)
+ #:inputs `((,bash) (,builder))
#:env-vars '(("HOME" . "/homeless"))))
@result{} #<derivation /gnu/store/@dots{}-foo.drv => /gnu/store/@dots{}-foo>
@end lisp
-As can be guessed, this primitive is cumbersome to use directly. An
-improved variant is @code{build-expression->derivation}, which allows
-the caller to directly pass a Guile expression as the build script:
+As can be guessed, this primitive is cumbersome to use directly. A
+better approach is to write build scripts in Scheme, of course! The
+best course of action for that is to write the build code as a
+``G-expression'', and to pass it to @code{gexp->derivation}. For more
+information, @ref{G-Expressions}.
+
+Once upon a time, @code{gexp->derivation} did not exist and constructing
+derivations with build code written in Scheme was achieved with
+@code{build-expression->derivation}, documented below. This procedure
+is now deprecated in favor of the much nicer @code{gexp->derivation}.
@deffn {Scheme Procedure} build-expression->derivation @var{store} @
@var{name} @var{exp} @
@@ -1816,20 +1825,6 @@ containing one file:
@result{} #<derivation /gnu/store/@dots{}-goo.drv => @dots{}>
@end lisp
-@cindex strata of code
-Remember that the build expression passed to
-@code{build-expression->derivation} is run by a separate Guile process
-than the one that calls @code{build-expression->derivation}: it is run
-by a Guile process launched by the daemon, typically in a chroot. So,
-while there is a single language for both the @dfn{host} and the build
-side, there are really two @dfn{strata} of code: the host-side, and the
-build-side code@footnote{The term @dfn{stratum} in this context was
-coined by Manuel Serrano et al. in the context of their work on Hop.}.
-This distinction is important to keep in mind, notably when using
-higher-level constructs such as @var{gnu-build-system} (@pxref{Defining
-Packages}). For this reason, Guix modules that are meant to be used in
-the build stratum are kept in the @code{(guix build @dots{})} name
-space.
@node The Store Monad
@section The Store Monad
@@ -1993,6 +1988,196 @@ Packages}).
@end deffn
+@node G-Expressions
+@section G-Expressions
+
+@cindex G-expression
+@cindex build code quoting
+So we have ``derivations'', which represent a sequence of build actions
+to be performed to produce an item in the store (@pxref{Derivations}).
+Those build actions are performed when asking the daemon to actually
+build the derivations; they are run by the daemon in a container
+(@pxref{Invoking guix-daemon}).
+
+@cindex strata of code
+It should come as no surprise that we like to write those build actions
+in Scheme. When we do that, we end up with two @dfn{strata} of Scheme
+code@footnote{The term @dfn{stratum} in this context was coined by
+Manuel Serrano et al.@: in the context of their work on Hop.}: the
+``host code''---code that defines packages, talks to the daemon,
+etc.---and the ``build code''---code that actually performs build
+actions, such as making directories, invoking @command{make}, etc.
+
+To describe a derivation and its build actions, one typically needs to
+embed build code inside host code. It boils down to manipulating build
+code as data, and Scheme's homoiconicity---code has a direct
+representation as data---comes in handy for that. But we need more than
+Scheme's normal @code{quasiquote} mechanism to construct build
+expressions.
+
+The @code{(guix gexp)} module implements @dfn{G-expressions}, a form of
+S-expressions adapted to build expressions. G-expressions, or
+@dfn{gexps}, consist essentially in three syntactic forms: @code{gexp},
+@code{ungexp}, and @code{ungexp-splicing} (or simply: @code{#~},
+@code{#$}, and @code{#$@@}), which are comparable respectively to
+@code{quasiquote}, @code{unquote}, and @code{unquote-splicing}
+(@pxref{Expression Syntax, @code{quasiquote},, guile, GNU Guile
+Reference Manual}). However, there are major differences:
+
+@itemize
+@item
+Gexps are meant to be written to a file and run or manipulated by other
+processes.
+
+@item
+When a package or derivation is unquoted inside a gexp, the result is as
+if its output file name had been introduced.
+
+@item
+Gexps carry information about the packages or derivations they refer to,
+and these dependencies are automatically added as inputs to the build
+processes that use them.
+@end itemize
+
+To illustrate the idea, here is an example of a gexp:
+
+@example
+(define build-exp
+ #~(begin
+ (mkdir #$output)
+ (chdir #$output)
+ (symlink (string-append #$coreutils "/bin/ls")
+ "list-files")))
+@end example
+
+This gexp can be passed to @code{gexp->derivation}; we obtain a
+derivation that builds a directory containing exactly one symlink to
+@file{/gnu/store/@dots{}-coreutils-8.22/bin/ls}:
+
+@example
+(gexp->derivation "the-thing" build-exp)
+@end example
+
+As one would expect, the @code{"/gnu/store/@dots{}-coreutils"} string is
+substituted to the reference to the @var{coreutils} package in the
+actual build code, and @var{coreutils} is automatically made an input to
+the derivation. Likewise, @code{#$output} (equivalent to @code{(ungexp
+output)}) is replaced by a string containing the derivation's output
+directory name. The syntactic form to construct gexps is summarized
+below.
+
+@deffn {Scheme Syntax} #~@var{exp}
+@deffnx {Scheme Syntax} (gexp @var{exp})
+Return a G-expression containing @var{exp}. @var{exp} may contain one
+or more of the following forms:
+
+@table @code
+@item #$@var{obj}
+@itemx (ungexp @var{obj})
+Introduce a reference to @var{obj}. @var{obj} may be a package or a
+derivation, in which case the @code{ungexp} form is replaced by its
+output file name---e.g., @code{"/gnu/store/@dots{}-coreutils-8.22}.
+
+If @var{obj} is a list, it is traversed and any package or derivation
+references are substituted similarly.
+
+If @var{obj} is another gexp, its contents are inserted and its
+dependencies are added to those of the containing gexp.
+
+If @var{obj} is another kind of object, it is inserted as is.
+
+@item #$@var{package-or-derivation}:@var{output}
+@itemx (ungexp @var{package-or-derivation} @var{output})
+This is like the form above, but referring explicitly to the
+@var{output} of @var{package-or-derivation}---this is useful when
+@var{package-or-derivation} produces multiple outputs (@pxref{Packages
+with Multiple Outputs}).
+
+@item #$output[:@var{output}]
+@itemx (ungexp output [@var{output}])
+Insert a reference to derivation output @var{output}, or to the main
+output when @var{output} is omitted.
+
+This only makes sense for gexps passed to @code{gexp->derivation}.
+
+@item #$@@@var{lst}
+@itemx (ungexp-splicing @var{lst})
+Like the above, but splices the contents of @var{lst} inside the
+containing list.
+
+@end table
+
+G-expressions created by @code{gexp} or @code{#~} are run-time objects
+of the @code{gexp?} type (see below.)
+@end deffn
+
+@deffn {Scheme Procedure} gexp? @var{obj}
+Return @code{#t} if @var{obj} is a G-expression.
+@end deffn
+
+G-expressions are meant to be written to disk, either as code building
+some derivation, or as plain files in the store. The monadic procedures
+below allow you to do that (@pxref{The Store Monad}, for more
+information about monads.)
+
+@deffn {Monadic Procedure} gexp->derivation @var{name} @var{exp} @
+ [#:system (%current-system)] [#:inputs '()] @
+ [#:hash #f] [#:hash-algo #f] @
+ [#:recursive? #f] [#:env-vars '()] [#:modules '()] @
+ [#:references-graphs #f] [#:local-build? #f] @
+ [#:guile-for-build #f]
+Return a derivation @var{name} that runs @var{exp} (a gexp) with
+@var{guile-for-build} (a derivation) on @var{system}.
+
+Make @var{modules} available in the evaluation context of @var{EXP};
+@var{MODULES} is a list of names of Guile modules from the current
+search path to be copied in the store, compiled, and made available in
+the load path during the execution of @var{exp}---e.g., @code{((guix
+build utils) (guix build gnu-build-system))}.
+
+The other arguments are as for @code{derivation}.
+@end deffn
+
+@deffn {Monadic Procedure} gexp->script @var{name} @var{exp}
+Return an executable script @var{name} that runs @var{exp} using
+@var{guile} with @var{modules} in its search path.
+
+The example below builds a script that simply invokes the @command{ls}
+command:
+
+@example
+(use-modules (guix gexp) (gnu packages base))
+
+(gexp->script "list-files"
+ #~(execl (string-append #$coreutils "/bin/ls")
+ "ls"))
+@end example
+
+When ``running'' it through the store (@pxref{The Store Monad,
+@code{run-with-store}}), we obtain a derivation that procedures an
+executable file @file{/gnu/store/@dots{}-list-files} along these lines:
+
+@example
+#!/gnu/store/@dots{}-guile-2.0.11/bin/guile -ds
+!#
+(execl (string-append "/gnu/store/@dots{}-coreutils-8.22"/bin/ls")
+ "ls")
+@end example
+@end deffn
+
+@deffn {Monadic Procedure} gexp->file @var{name} @var{exp}
+Return a derivation that builds a file @var{name} containing @var{exp}.
+
+The resulting file holds references to all the dependencies of @var{exp}
+or a subset thereof.
+@end deffn
+
+Of course, in addition to gexps embedded in ``host'' code, there are
+also modules containing build tools. To make it clear that they are
+meant to be used in the build stratum, these modules are kept in the
+@code{(guix build @dots{})} name space.
+
+
@c *********************************************************************
@node Utilities
@chapter Utilities
diff --git a/guix/gexp.scm b/guix/gexp.scm
new file mode 100644
index 0000000000..9dd83f5370
--- /dev/null
+++ b/guix/gexp.scm
@@ -0,0 +1,391 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2014 Ludovic Courtès <ludo@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix 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 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix 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 Guix. If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (guix gexp)
+ #:use-module ((guix store)
+ #:select (direct-store-path?))
+ #:use-module (guix monads)
+ #:use-module ((guix derivations)
+ #:select (derivation? derivation->output-path
+ %guile-for-build derivation))
+ #:use-module (guix packages)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-26)
+ #:use-module (ice-9 match)
+ #:export (gexp
+ gexp?
+ gexp->derivation
+ gexp->file
+ gexp->script))
+
+;;; Commentary:
+;;;
+;;; This module implements "G-expressions", or "gexps". Gexps are like
+;;; S-expressions (sexps), with two differences:
+;;;
+;;; 1. References (un-quotations) to derivations or packages in a gexp are
+;;; replaced by the corresponding output file name;
+;;;
+;;; 2. Gexps embed information about the derivations they refer to.
+;;;
+;;; Gexps make it easy to write to files Scheme code that refers to store
+;;; items, or to write Scheme code to build derivations.
+;;;
+;;; Code:
+
+;; "G expressions".
+(define-record-type <gexp>
+ (make-gexp references proc)
+ gexp?
+ (references gexp-references) ; ((DRV-OR-PKG OUTPUT) ...)
+ (proc gexp-proc)) ; procedure
+
+;; Reference to one of the derivation's outputs, for gexps used in
+;; derivations.
+(define-record-type <output-ref>
+ (output-ref name)
+ output-ref?
+ (name output-ref-name))
+
+(define raw-derivation
+ (store-lift derivation))
+
+(define (lower-inputs* inputs)
+ "Turn any package from INPUTS into a derivation; return the corresponding
+input list as a monadic value."
+ ;; XXX: This is like 'lower-inputs' but without the "name" part in tuples.
+ (with-monad %store-monad
+ (sequence %store-monad
+ (map (match-lambda
+ (((? package? package) sub-drv ...)
+ (mlet %store-monad ((drv (package->derivation package)))
+ (return `(,drv ,@sub-drv))))
+ (input
+ (return input)))
+ inputs))))
+
+(define* (gexp->derivation name exp
+ #:key
+ (system (%current-system))
+ hash hash-algo recursive?
+ (env-vars '())
+ (modules '())
+ (guile-for-build (%guile-for-build))
+ references-graphs
+ local-build?)
+ "Return a derivation NAME that runs EXP (a gexp) with GUILE-FOR-BUILD (a
+derivation) on SYSTEM.
+
+Make MODULES available in the evaluation context of EXP; MODULES is a list of
+names of Guile modules from the current search path to be copied in the store,
+compiled, and made available in the load path during the execution of
+EXP---e.g., '((guix build utils) (guix build gnu-build-system)).
+
+The other arguments are as for 'derivation'."
+ (define %modules modules)
+ (define outputs (gexp-outputs exp))
+
+ (mlet* %store-monad ((inputs (lower-inputs* (gexp-inputs exp)))
+ (sexp (gexp->sexp exp #:outputs outputs))
+ (builder (text-file (string-append name "-builder")
+ (object->string sexp)))
+ (modules (if (pair? %modules)
+ (imported-modules %modules
+ #:system system
+ #:guile guile-for-build)
+ (return #f)))
+ (compiled (if (pair? %modules)
+ (compiled-modules %modules
+ #:system system
+ #:guile guile-for-build)
+ (return #f)))
+ (guile (if guile-for-build
+ (return guile-for-build)
+ (package->derivation
+ (@ (gnu packages base) guile-final)
+ system))))
+ (raw-derivation name
+ (string-append (derivation->output-path guile)
+ "/bin/guile")
+ `("--no-auto-compile"
+ ,@(if (pair? %modules)
+ `("-L" ,(derivation->output-path modules)
+ "-C" ,(derivation->output-path compiled))
+ '())
+ ,builder)
+ #:outputs outputs
+ #:env-vars env-vars
+ #:system system
+ #:inputs `((,guile)
+ (,builder)
+ ,@(if modules
+ `((,modules) (,compiled) ,@inputs)
+ inputs))
+ #:hash hash #:hash-algo hash-algo #:recursive? recursive?
+ #:references-graphs references-graphs
+ #:local-build? local-build?)))
+
+(define (gexp-inputs exp)
+ "Return the input list for EXP."
+ (define (add-reference-inputs ref result)
+ (match ref
+ (((? derivation?) (? string?))
+ (cons ref result))
+ (((? package?) (? string?))
+ (cons ref result))
+ ((? gexp? exp)
+ (append (gexp-inputs exp) result))
+ (((? string? file))
+ (if (direct-store-path? file)
+ (cons ref result)
+ result))
+ ((refs ...)
+ (fold-right add-reference-inputs result refs))
+ (_
+ ;; Ignore references to other kinds of objects.
+ result)))
+
+ (fold-right add-reference-inputs
+ '()
+ (gexp-references exp)))
+
+(define (gexp-outputs exp)
+ "Return the outputs referred to by EXP as a list of strings."
+ (define (add-reference-output ref result)
+ (match ref
+ (($ <output-ref> name)
+ (cons name result))
+ ((? gexp? exp)
+ (append (gexp-outputs exp) result))
+ (_
+ result)))
+
+ (fold-right add-reference-output
+ '()
+ (gexp-references exp)))
+
+(define* (gexp->sexp exp #:key (outputs '()))
+ "Return (monadically) the sexp corresponding to EXP for the given OUTPUT,
+and in the current monad setting (system type, etc.)"
+ (define (reference->sexp ref)
+ (with-monad %store-monad
+ (match ref
+ (((? derivation? drv) (? string? output))
+ (return (derivation->output-path drv output)))
+ (((? package? p) (? string? output))
+ (package-file p #:output output))
+ (($ <output-ref> output)
+ (match (member output outputs)
+ (#f
+ (error "no such output" output))
+ (_
+ (return `((@ (guile) getenv) ,output)))))
+ ((? gexp? exp)
+ (gexp->sexp exp #:outputs outputs))
+ (((? string? str))
+ (return (if (direct-store-path? str) str ref)))
+ ((refs ...)
+ (sequence %store-monad (map reference->sexp refs)))
+ (x
+ (return x)))))
+
+ (mlet %store-monad
+ ((args (sequence %store-monad
+ (map reference->sexp (gexp-references exp)))))
+ (return (apply (gexp-proc exp) args))))
+
+(define (canonicalize-reference ref)
+ "Return a canonical variant of REF, which adds any missing output part in
+package/derivation references."
+ (match ref
+ ((? package? p)
+ `(,p "out"))
+ ((? derivation? d)
+ `(,d "out"))
+ (((? package?) (? string?))
+ ref)
+ (((? derivation?) (? string?))
+ ref)
+ ((? string? s)
+ (if (direct-store-path? s) `(,s) s))
+ ((refs ...)
+ (map canonicalize-reference refs))
+ (x x)))
+
+(define (syntax-location-string s)
+ "Return a string representing the source code location of S."
+ (let ((props (syntax-source s)))
+ (if props
+ (let ((file (assoc-ref props 'filename))
+ (line (and=> (assoc-ref props 'line) 1+))
+ (column (assoc-ref props 'column)))
+ (if file
+ (simple-format #f "~a:~a:~a"
+ file line column)
+ (simple-format #f "~a:~a" line column)))
+ "<unknown location>")))
+
+(define-syntax gexp
+ (lambda (s)
+ (define (collect-escapes exp)
+ ;; Return all the 'ungexp' present in EXP.
+ (let loop ((exp exp)
+ (result '()))
+ (syntax-case exp (ungexp ungexp-splicing)
+ ((ungexp _)
+ (cons exp result))
+ ((ungexp _ _)
+ (cons exp result))
+ ((ungexp-splicing _ ...)
+ (cons exp result))
+ ((exp0 exp ...)
+ (let ((result (loop #'exp0 result)))
+ (fold loop result #'(exp ...))))
+ (_
+ result))))
+
+ (define (escape->ref exp)
+ ;; Turn 'ungexp' form EXP into a "reference".
+ (syntax-case exp (ungexp ungexp-splicing output)
+ ((ungexp output)
+ #'(output-ref "out"))
+ ((ungexp output name)
+ #'(output-ref name))
+ ((ungexp thing)
+ #'thing)
+ ((ungexp drv-or-pkg out)
+ #'(list drv-or-pkg out))
+ ((ungexp-splicing lst)
+ #'lst)))
+
+ (define (substitute-references exp substs)
+ ;; Return a variant of EXP where all the cars of SUBSTS have been
+ ;; replaced by the corresponding cdr.
+ (syntax-case exp (ungexp ungexp-splicing)
+ ((ungexp _ ...)
+ (match (assoc exp substs)
+ ((_ id)
+ id)
+ (_
+ #'(syntax-error "error: no 'ungexp' substitution"
+ #'ref))))
+ (((ungexp-splicing _ ...) rest ...)
+ (syntax-case exp ()
+ ((exp rest ...)
+ (match (assoc #'exp substs)
+ ((_ id)
+ (with-syntax ((id id))
+ #`(append id
+ #,(substitute-references #'(rest ...) substs))))
+ (_
+ #'(syntax-error "error: no 'ungexp-splicing' substitution"
+ #'ref))))))
+ ((exp0 exp ...)
+ #`(cons #,(substitute-references #'exp0 substs)
+ #,(substitute-references #'(exp ...) substs)))
+ (x #''x)))
+
+ (syntax-case s (ungexp output)
+ ((_ exp)
+ (let* ((escapes (delete-duplicates (collect-escapes #'exp)))
+ (formals (generate-temporaries escapes))
+ (sexp (substitute-references #'exp (zip escapes formals)))
+ (refs (map escape->ref escapes)))
+ #`(make-gexp (map canonicalize-reference (list #,@refs))
+ (lambda #,formals
+ #,sexp)))))))
+
+
+;;;
+;;; Convenience procedures.
+;;;
+
+(define* (gexp->script name exp
+ #:key (modules '())
+ (guile (@ (gnu packages base) guile-final)))
+ "Return an executable script NAME that runs EXP using GUILE with MODULES in
+its search path."
+ (mlet %store-monad ((modules (imported-modules modules))
+ (compiled (compiled-modules modules)))
+ (gexp->derivation name
+ (gexp
+ (call-with-output-file (ungexp output)
+ (lambda (port)
+ (format port
+ "#!~a/bin/guile --no-auto-compile~%!#~%"
+ (ungexp guile))
+ (write
+ '(set! %load-path
+ (cons (ungexp modules) %load-path))
+ port)
+ (write
+ '(set! %load-compiled-path
+ (cons (ungexp compiled)
+ %load-compiled-path))
+ port)
+ (write '(ungexp exp) port)
+ (chmod port #o555)))))))
+
+(define (gexp->file name exp)
+ "Return a derivation that builds a file NAME containing EXP."
+ (gexp->derivation name
+ (gexp
+ (call-with-output-file (ungexp output)
+ (lambda (port)
+ (write '(ungexp exp) port))))))
+
+
+
+;;;
+;;; Syntactic sugar.
+;;;
+
+(eval-when (expand load eval)
+ (define (read-ungexp chr port)
+ "Read an 'ungexp' or 'ungexp-splicing' form from PORT."
+ (define unquote-symbol
+ (match (peek-char port)
+ (#\@
+ (read-char port)
+ 'ungexp-splicing)
+ (_
+ 'ungexp)))
+
+ (match (read port)
+ ((? symbol? symbol)
+ (let ((str (symbol->string symbol)))
+ (match (string-index-right str #\:)
+ (#f
+ `(,unquote-symbol ,symbol))
+ (colon
+ (let ((name (string->symbol (substring str 0 colon)))
+ (output (substring str (+ colon 1))))
+ `(,unquote-symbol ,name ,output))))))
+ (x
+ `(,unquote-symbol ,x))))
+
+ (define (read-gexp chr port)
+ "Read a 'gexp' form from PORT."
+ `(gexp ,(read port)))
+
+ ;; Extend the reader
+ (read-hash-extend #\~ read-gexp)
+ (read-hash-extend #\$ read-ungexp))
+
+;;; gexp.scm ends here
diff --git a/tests/gexp.scm b/tests/gexp.scm
new file mode 100644
index 0000000000..3da5b82e4c
--- /dev/null
+++ b/tests/gexp.scm
@@ -0,0 +1,234 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2014 Ludovic Courtès <ludo@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix 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 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix 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 Guix. If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (test-gexp)
+ #:use-module (guix store)
+ #:use-module (guix monads)
+ #:use-module (guix gexp)
+ #:use-module (guix derivations)
+ #:use-module ((guix packages)
+ #:select (package-derivation %current-system))
+ #:use-module (gnu packages)
+ #:use-module (gnu packages base)
+ #:use-module (gnu packages bootstrap)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-64)
+ #:use-module (rnrs io ports)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 popen))
+
+;; Test the (guix gexp) module.
+
+(define %store
+ (open-connection))
+
+;; For white-box testing.
+(define gexp-inputs (@@ (guix gexp) gexp-inputs))
+(define gexp->sexp (@@ (guix gexp) gexp->sexp))
+
+(define guile-for-build
+ (package-derivation %store %bootstrap-guile))
+
+;; Make it the default.
+(%guile-for-build guile-for-build)
+
+(define (gexp->sexp* exp)
+ (run-with-store %store (gexp->sexp exp)
+ #:guile-for-build guile-for-build))
+
+(define-syntax-rule (test-assertm name exp)
+ (test-assert name
+ (run-with-store %store exp
+ #:guile-for-build guile-for-build)))
+
+
+(test-begin "gexp")
+
+(test-equal "no refs"
+ '(display "hello!")
+ (let ((exp (gexp (display "hello!"))))
+ (and (gexp? exp)
+ (null? (gexp-inputs exp))
+ (gexp->sexp* exp))))
+
+(test-equal "unquote"
+ '(display `(foo ,(+ 2 3)))
+ (let ((exp (gexp (display `(foo ,(+ 2 3))))))
+ (and (gexp? exp)
+ (null? (gexp-inputs exp))
+ (gexp->sexp* exp))))
+
+(test-assert "one input package"
+ (let ((exp (gexp (display (ungexp coreutils)))))
+ (and (gexp? exp)
+ (match (gexp-inputs exp)
+ (((p "out"))
+ (eq? p coreutils)))
+ (equal? `(display ,(derivation->output-path
+ (package-derivation %store coreutils)))
+ (gexp->sexp* exp)))))
+
+(test-assert "same input twice"
+ (let ((exp (gexp (begin
+ (display (ungexp coreutils))
+ (display (ungexp coreutils))))))
+ (and (gexp? exp)
+ (match (gexp-inputs exp)
+ (((p "out"))
+ (eq? p coreutils)))
+ (let ((e `(display ,(derivation->output-path
+ (package-derivation %store coreutils)))))
+ (equal? `(begin ,e ,e) (gexp->sexp* exp))))))
+
+(test-assert "two input packages, one derivation, one file"
+ (let* ((drv (build-expression->derivation
+ %store "foo" 'bar
+ #:guile-for-build (package-derivation %store %bootstrap-guile)))
+ (txt (add-text-to-store %store "foo" "Hello, world!"))
+ (exp (gexp (begin
+ (display (ungexp coreutils))
+ (display (ungexp %bootstrap-guile))
+ (display (ungexp drv))
+ (display (ungexp txt))))))
+ (define (match-input thing)
+ (match-lambda
+ ((drv-or-pkg _ ...)
+ (eq? thing drv-or-pkg))))
+
+ (and (gexp? exp)
+ (= 4 (length (gexp-inputs exp)))
+ (every (lambda (input)
+ (find (match-input input) (gexp-inputs exp)))
+ (list drv coreutils %bootstrap-guile txt))
+ (let ((e0 `(display ,(derivation->output-path
+ (package-derivation %store coreutils))))
+ (e1 `(display ,(derivation->output-path
+ (package-derivation %store %bootstrap-guile))))
+ (e2 `(display ,(derivation->output-path drv)))
+ (e3 `(display ,txt)))
+ (equal? `(begin ,e0 ,e1 ,e2 ,e3) (gexp->sexp* exp))))))
+
+(test-assert "input list"
+ (let ((exp (gexp (display
+ '(ungexp (list %bootstrap-guile coreutils)))))
+ (guile (derivation->output-path
+ (package-derivation %store %bootstrap-guile)))
+ (cu (derivation->output-path
+ (package-derivation %store coreutils))))
+ (and (lset= equal?
+ `((,%bootstrap-guile "out") (,coreutils "out"))
+ (gexp-inputs exp))
+ (equal? `(display '(,guile ,cu))
+ (gexp->sexp* exp)))))
+
+(test-assert "input list splicing"
+ (let* ((inputs (list (list glibc "debug") %bootstrap-guile))
+ (outputs (list (derivation->output-path
+ (package-derivation %store glibc)
+ "debug")
+ (derivation->output-path
+ (package-derivation %store %bootstrap-guile))))
+ (exp (gexp (list (ungexp-splicing (cons (+ 2 3) inputs))))))
+ (and (lset= equal?
+ `((,glibc "debug") (,%bootstrap-guile "out"))
+ (gexp-inputs exp))
+ (equal? (gexp->sexp* exp)
+ `(list ,@(cons 5 outputs))))))
+
+(test-assertm "gexp->file"
+ (mlet* %store-monad ((exp -> (gexp (display (ungexp %bootstrap-guile))))
+ (guile (package-file %bootstrap-guile))
+ (sexp (gexp->sexp exp))
+ (drv (gexp->file "foo" exp))
+ (out -> (derivation->output-path drv))
+ (done (built-derivations (list drv)))
+ (refs ((store-lift references) out)))
+ (return (and (equal? sexp (call-with-input-file out read))
+ (equal? (list guile) refs)))))
+
+(test-assertm "gexp->derivation"
+ (mlet* %store-monad ((file (text-file "foo" "Hello, world!"))
+ (exp -> (gexp
+ (begin
+ (mkdir (ungexp output))
+ (chdir (ungexp output))
+ (symlink
+ (string-append (ungexp %bootstrap-guile)
+ "/bin/guile")
+ "foo")
+ (symlink (ungexp file)
+ (ungexp output "2nd")))))
+ (drv (gexp->derivation "foo" exp))
+ (out -> (derivation->output-path drv))
+ (out2 -> (derivation->output-path drv "2nd"))
+ (done (built-derivations (list drv)))
+ (refs ((store-lift references) out))
+ (refs2 ((store-lift references) out2))
+ (guile (package-file %bootstrap-guile "bin/guile")))
+ (return (and (string=? (readlink (string-append out "/foo")) guile)
+ (string=? (readlink out2) file)
+ (equal? refs (list (dirname (dirname guile))))
+ (equal? refs2 (list file))))))
+
+(test-assertm "gexp->derivation, composed gexps"
+ (mlet* %store-monad ((exp0 -> (gexp (begin
+ (mkdir (ungexp output))
+ (chdir (ungexp output)))))
+ (exp1 -> (gexp (symlink
+ (string-append (ungexp %bootstrap-guile)
+ "/bin/guile")
+ "foo")))
+ (exp -> (gexp (begin (ungexp exp0) (ungexp exp1))))
+ (drv (gexp->derivation "foo" exp))
+ (out -> (derivation->output-path drv))
+ (done (built-derivations (list drv)))
+ (guile (package-file %bootstrap-guile "bin/guile")))
+ (return (string=? (readlink (string-append out "/foo"))
+ guile))))
+
+(test-assertm "gexp->script"
+ (mlet* %store-monad ((n -> (random (expt 2 50)))
+ (exp -> (gexp
+ (system*
+ (string-append (ungexp %bootstrap-guile)
+ "/bin/guile")
+ "-c" (object->string
+ '(display (expt (ungexp n) 2))))))
+ (drv (gexp->script "guile-thing" exp
+ #:guile %bootstrap-guile))
+ (out -> (derivation->output-path drv))
+ (done (built-derivations (list drv))))
+ (let* ((pipe (open-input-pipe out))
+ (str (get-string-all pipe)))
+ (return (and (zero? (close-pipe pipe))
+ (= (expt n 2) (string->number str)))))))
+
+(test-equal "sugar"
+ '(gexp (foo (ungexp bar) (ungexp baz "out")
+ (ungexp (chbouib 42))
+ (ungexp-splicing (list x y z))))
+ '#~(foo #$bar #$baz:out #$(chbouib 42) #$@(list x y z)))
+
+(test-end "gexp")
+
+
+(exit (= (test-runner-fail-count (test-runner-current)) 0))
+
+;; Local Variables:
+;; eval: (put 'test-assertm 'scheme-indent-function 1)
+;; End: