~ruther/guix-local

21b679f6944f4e1f09f949322f5242b761dc22a7 — Ludovic Courtès 12 years ago ba948b4
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.
5 files changed, 837 insertions(+), 18 deletions(-)

M .dir-locals.el
M Makefile.am
M doc/guix.texi
A guix/gexp.scm
A tests/gexp.scm
M .dir-locals.el => .dir-locals.el +8 -1
@@ 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))))

M Makefile.am => Makefile.am +2 -0
@@ 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

M doc/guix.texi => doc/guix.texi +202 -17
@@ 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

A guix/gexp.scm => guix/gexp.scm +391 -0
@@ 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

A tests/gexp.scm => tests/gexp.scm +234 -0
@@ 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: