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.
This commit is contained in:
parent
ba948b4fa0
commit
21b679f694
|
@ -6,6 +6,7 @@
|
||||||
(scheme-mode
|
(scheme-mode
|
||||||
.
|
.
|
||||||
((indent-tabs-mode . nil)
|
((indent-tabs-mode . nil)
|
||||||
|
(eval . (put 'eval-when 'scheme-indent-function 1))
|
||||||
(eval . (put 'test-assert 'scheme-indent-function 1))
|
(eval . (put 'test-assert 'scheme-indent-function 1))
|
||||||
(eval . (put 'test-equal 'scheme-indent-function 1))
|
(eval . (put 'test-equal 'scheme-indent-function 1))
|
||||||
(eval . (put 'test-eq '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 'with-monad 'scheme-indent-function 1))
|
||||||
(eval . (put 'mlet* 'scheme-indent-function 2))
|
(eval . (put 'mlet* 'scheme-indent-function 2))
|
||||||
(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)))
|
(emacs-lisp-mode . ((indent-tabs-mode . nil)))
|
||||||
(texinfo-mode . ((indent-tabs-mode . nil)
|
(texinfo-mode . ((indent-tabs-mode . nil)
|
||||||
(fill-column . 72))))
|
(fill-column . 72))))
|
||||||
|
|
|
@ -37,6 +37,7 @@ MODULES = \
|
||||||
guix/download.scm \
|
guix/download.scm \
|
||||||
guix/git-download.scm \
|
guix/git-download.scm \
|
||||||
guix/monads.scm \
|
guix/monads.scm \
|
||||||
|
guix/gexp.scm \
|
||||||
guix/profiles.scm \
|
guix/profiles.scm \
|
||||||
guix/serialization.scm \
|
guix/serialization.scm \
|
||||||
guix/nar.scm \
|
guix/nar.scm \
|
||||||
|
@ -139,6 +140,7 @@ SCM_TESTS = \
|
||||||
tests/snix.scm \
|
tests/snix.scm \
|
||||||
tests/store.scm \
|
tests/store.scm \
|
||||||
tests/monads.scm \
|
tests/monads.scm \
|
||||||
|
tests/gexp.scm \
|
||||||
tests/nar.scm \
|
tests/nar.scm \
|
||||||
tests/union.scm \
|
tests/union.scm \
|
||||||
tests/profiles.scm
|
tests/profiles.scm
|
||||||
|
|
219
doc/guix.texi
219
doc/guix.texi
|
@ -1305,6 +1305,7 @@ package definitions.
|
||||||
* The Store:: Manipulating the package store.
|
* The Store:: Manipulating the package store.
|
||||||
* Derivations:: Low-level interface to package derivations.
|
* Derivations:: Low-level interface to package derivations.
|
||||||
* The Store Monad:: Purely functional interface to the store.
|
* The Store Monad:: Purely functional interface to the store.
|
||||||
|
* G-Expressions:: Manipulating build expressions.
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
@node Defining Packages
|
@node Defining Packages
|
||||||
|
@ -1762,13 +1763,21 @@ to a Bash executable in the store:
|
||||||
"echo hello world > $out\n" '())))
|
"echo hello world > $out\n" '())))
|
||||||
(derivation store "foo"
|
(derivation store "foo"
|
||||||
bash `("-e" ,builder)
|
bash `("-e" ,builder)
|
||||||
|
#:inputs `((,bash) (,builder))
|
||||||
#:env-vars '(("HOME" . "/homeless"))))
|
#:env-vars '(("HOME" . "/homeless"))))
|
||||||
@result{} #<derivation /gnu/store/@dots{}-foo.drv => /gnu/store/@dots{}-foo>
|
@result{} #<derivation /gnu/store/@dots{}-foo.drv => /gnu/store/@dots{}-foo>
|
||||||
@end lisp
|
@end lisp
|
||||||
|
|
||||||
As can be guessed, this primitive is cumbersome to use directly. An
|
As can be guessed, this primitive is cumbersome to use directly. A
|
||||||
improved variant is @code{build-expression->derivation}, which allows
|
better approach is to write build scripts in Scheme, of course! The
|
||||||
the caller to directly pass a Guile expression as the build script:
|
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} @
|
@deffn {Scheme Procedure} build-expression->derivation @var{store} @
|
||||||
@var{name} @var{exp} @
|
@var{name} @var{exp} @
|
||||||
|
@ -1816,20 +1825,6 @@ containing one file:
|
||||||
@result{} #<derivation /gnu/store/@dots{}-goo.drv => @dots{}>
|
@result{} #<derivation /gnu/store/@dots{}-goo.drv => @dots{}>
|
||||||
@end lisp
|
@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
|
@node The Store Monad
|
||||||
@section The Store Monad
|
@section The Store Monad
|
||||||
|
@ -1993,6 +1988,196 @@ Packages}).
|
||||||
@end deffn
|
@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 *********************************************************************
|
@c *********************************************************************
|
||||||
@node Utilities
|
@node Utilities
|
||||||
@chapter Utilities
|
@chapter Utilities
|
||||||
|
|
|
@ -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
|
|
@ -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:
|
Loading…
Reference in New Issue