gexp: 'gexp-modules' now consistently deletes duplicates.
[jackhill/guix/guix.git] / guix / gexp.scm
index 05178a5..537875b 100644 (file)
@@ -1,5 +1,7 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2014, 2015, 2016 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2014, 2015, 2016, 2017, 2018 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2018 Clément Lassieur <clement@lassieur.org>
+;;; Copyright © 2018 Jan Nieuwenhuizen <janneke@gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
   #:use-module (guix derivations)
   #:use-module (guix grafts)
   #:use-module (guix utils)
+  #:use-module (rnrs bytevectors)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-9)
   #:use-module (srfi srfi-9 gnu)
   #:use-module (srfi srfi-26)
+  #:use-module (srfi srfi-34)
+  #:use-module (srfi srfi-35)
   #:use-module (ice-9 match)
   #:export (gexp
             gexp?
             with-imported-modules
+            with-extensions
 
             gexp-input
             gexp-input?
@@ -57,6 +63,7 @@
             program-file-name
             program-file-gexp
             program-file-guile
+            program-file-module-path
 
             scheme-file
             scheme-file?
             file-append-base
             file-append-suffix
 
+            load-path-expression
+            gexp-modules
+
             gexp->derivation
             gexp->file
             gexp->script
             text-file*
             mixed-text-file
+            file-union
+            directory-union
             imported-files
             imported-modules
             compiled-modules
 
             define-gexp-compiler
             gexp-compiler?
+            file-like?
             lower-object
 
-            lower-inputs))
+            lower-inputs
+
+            &gexp-error
+            gexp-error?
+            &gexp-input-error
+            gexp-input-error?
+            gexp-error-invalid-input))
 
 ;;; Commentary:
 ;;;
 
 ;; "G expressions".
 (define-record-type <gexp>
-  (make-gexp references modules proc)
+  (make-gexp references modules extensions proc)
   gexp?
   (references gexp-references)                    ;list of <gexp-input>
   (modules    gexp-self-modules)                  ;list of module names
+  (extensions gexp-self-extensions)               ;list of lowerable things
   (proc       gexp-proc))                         ;procedure
 
 (define (write-gexp gexp port)
   (lower      gexp-compiler-lower)
   (expand     gexp-compiler-expand))              ;#f | DRV -> sexp
 
+(define-condition-type &gexp-error &error
+  gexp-error?)
+
+(define-condition-type &gexp-input-error &gexp-error
+  gexp-input-error?
+  (input gexp-error-invalid-input))
+
+
 (define %gexp-compilers
   ;; 'eq?' mapping of record type descriptor to <gexp-compiler>.
   (make-hash-table 20))
@@ -161,6 +189,11 @@ procedure to lower it; otherwise return #f."
   (and=> (hashq-ref %gexp-compilers (struct-vtable object))
          gexp-compiler-lower))
 
+(define (file-like? object)
+  "Return #t if OBJECT leads to a file in the store once unquoted in a
+G-expression; otherwise return #f."
+  (and (struct? object) (->bool (lookup-compiler object))))
+
 (define (lookup-expander object)
   "Search for an expander for OBJECT.  Upon success, return the three argument
 procedure to expand it; otherwise return #f."
@@ -174,8 +207,11 @@ procedure to expand it; otherwise return #f."
 corresponding to OBJ for SYSTEM, cross-compiling for TARGET if TARGET is true.
 OBJ must be an object that has an associated gexp compiler, such as a
 <package>."
-  (let ((lower (lookup-compiler obj)))
-    (lower obj system target)))
+  (match (lookup-compiler obj)
+    (#f
+     (raise (condition (&gexp-input-error (input obj)))))
+    (lower
+     (lower obj system target))))
 
 (define-syntax define-gexp-compiler
   (syntax-rules (=> compiler expander)
@@ -247,8 +283,9 @@ vicinity of DIRECTORY."
           (string-append directory "/" file))
          (else file))))
 
-(define-syntax-rule (local-file file rest ...)
-  "Return an object representing local file FILE to add to the store; this
+(define-syntax local-file
+  (lambda (s)
+    "Return an object representing local file FILE to add to the store; this
 object can be used in a gexp.  If FILE is a relative file name, it is looked
 up relative to the source file where this form appears.  FILE will be added to
 the store under NAME--by default the base name of FILE.
@@ -261,10 +298,23 @@ When RECURSIVE? is true, call (SELECT?  FILE STAT) for each directory entry,
 where FILE is the entry's absolute file name and STAT is the result of
 'lstat'; exclude entries for which SELECT? does not return true.
 
-This is the declarative counterpart of the 'interned-file' monadic procedure."
-  (%local-file file
-               (delay (absolute-file-name file (current-source-directory)))
-               rest ...))
+This is the declarative counterpart of the 'interned-file' monadic procedure.
+It is implemented as a macro to capture the current source directory where it
+appears."
+    (syntax-case s ()
+      ((_ file rest ...)
+       #'(%local-file file
+                      (delay (absolute-file-name file (current-source-directory)))
+                      rest ...))
+      ((_)
+       #'(syntax-error "missing file name"))
+      (id
+       (identifier? #'id)
+       ;; XXX: We could return #'(lambda (file . rest) ...).  However,
+       ;; (syntax-source #'id) is #f so (current-source-directory) would not
+       ;; work.  Thus, simply forbid this form.
+       #'(syntax-error
+          "'local-file' is a macro and cannot be used like this")))))
 
 (define (local-file-absolute-file-name file)
   "Return the absolute file name for FILE, a <local-file> instance.  A
@@ -286,7 +336,7 @@ This is the declarative counterpart of the 'interned-file' monadic procedure."
   (%plain-file name content references)
   plain-file?
   (name        plain-file-name)                   ;string
-  (content     plain-file-content)                ;string
+  (content     plain-file-content)                ;string or bytevector
   (references  plain-file-references))            ;list (currently unused)
 
 (define (plain-file name content)
@@ -301,73 +351,85 @@ This is the declarative counterpart of 'text-file'."
 (define-gexp-compiler (plain-file-compiler (file <plain-file>) system target)
   ;; "Compile" FILE by adding it to the store.
   (match file
-    (($ <plain-file> name content references)
-     (text-file name content references))))
+    (($ <plain-file> name (and (? string?) content) references)
+     (text-file name content references))
+    (($ <plain-file> name (and (? bytevector?) content) references)
+     (binary-file name content references))))
 
 (define-record-type <computed-file>
-  (%computed-file name gexp options)
+  (%computed-file name gexp guile options)
   computed-file?
   (name       computed-file-name)                 ;string
   (gexp       computed-file-gexp)                 ;gexp
+  (guile      computed-file-guile)                ;<package>
   (options    computed-file-options))             ;list of arguments
 
 (define* (computed-file name gexp
-                        #:key (options '(#:local-build? #t)))
+                        #:key guile (options '(#:local-build? #t)))
   "Return an object representing the store item NAME, a file or directory
 computed by GEXP.  OPTIONS is a list of additional arguments to pass
 to 'gexp->derivation'.
 
 This is the declarative counterpart of 'gexp->derivation'."
-  (%computed-file name gexp options))
+  (%computed-file name gexp guile options))
 
 (define-gexp-compiler (computed-file-compiler (file <computed-file>)
                                               system target)
   ;; Compile FILE by returning a derivation whose build expression is its
   ;; gexp.
   (match file
-    (($ <computed-file> name gexp options)
-     (apply gexp->derivation name gexp options))))
+    (($ <computed-file> name gexp guile options)
+     (if guile
+         (mlet %store-monad ((guile (lower-object guile system
+                                                  #:target target)))
+           (apply gexp->derivation name gexp #:guile-for-build guile
+                  options))
+         (apply gexp->derivation name gexp options)))))
 
 (define-record-type <program-file>
-  (%program-file name gexp guile)
+  (%program-file name gexp guile path)
   program-file?
   (name       program-file-name)                  ;string
   (gexp       program-file-gexp)                  ;gexp
-  (guile      program-file-guile))                ;package
+  (guile      program-file-guile)                 ;package
+  (path       program-file-module-path))          ;list of strings
 
-(define* (program-file name gexp #:key (guile #f))
+(define* (program-file name gexp #:key (guile #f) (module-path %load-path))
   "Return an object representing the executable store item NAME that runs
-GEXP.  GUILE is the Guile package used to execute that script.
+GEXP.  GUILE is the Guile package used to execute that script.  Imported
+modules of GEXP are looked up in MODULE-PATH.
 
 This is the declarative counterpart of 'gexp->script'."
-  (%program-file name gexp guile))
+  (%program-file name gexp guile module-path))
 
 (define-gexp-compiler (program-file-compiler (file <program-file>)
                                              system target)
   ;; Compile FILE by returning a derivation that builds the script.
   (match file
-    (($ <program-file> name gexp guile)
+    (($ <program-file> name gexp guile module-path)
      (gexp->script name gexp
+                   #:module-path module-path
                    #:guile (or guile (default-guile))))))
 
 (define-record-type <scheme-file>
-  (%scheme-file name gexp)
+  (%scheme-file name gexp splice?)
   scheme-file?
   (name       scheme-file-name)                  ;string
-  (gexp       scheme-file-gexp))                 ;gexp
+  (gexp       scheme-file-gexp)                  ;gexp
+  (splice?    scheme-file-splice?))              ;Boolean
 
-(define* (scheme-file name gexp)
+(define* (scheme-file name gexp #:key splice?)
   "Return an object representing the Scheme file NAME that contains GEXP.
 
 This is the declarative counterpart of 'gexp->file'."
-  (%scheme-file name gexp))
+  (%scheme-file name gexp splice?))
 
 (define-gexp-compiler (scheme-file-compiler (file <scheme-file>)
                                             system target)
   ;; Compile FILE by returning a derivation that builds the file.
   (match file
-    (($ <scheme-file> name gexp)
-     (gexp->file name gexp))))
+    (($ <scheme-file> name gexp splice?)
+     (gexp->file name gexp #:splice? splice?))))
 
 ;; Appending SUFFIX to BASE's output file name.
 (define-record-type <file-append>
@@ -376,6 +438,14 @@ This is the declarative counterpart of 'gexp->file'."
   (base   file-append-base)                    ;<package> | <derivation> | ...
   (suffix file-append-suffix))                 ;list of strings
 
+(define (write-file-append file port)
+  (match file
+    (($ <file-append> base suffix)
+     (format port "#<file-append ~s ~s>" base
+             (string-join suffix)))))
+
+(set-record-type-printer! <file-append> write-file-append)
+
 (define (file-append base . suffix)
   "Return a <file-append> object that expands to the concatenation of BASE and
 SUFFIX."
@@ -436,25 +506,54 @@ whether this should be considered a \"native\" input or not."
 
 (set-record-type-printer! <gexp-output> write-gexp-output)
 
+(define* (gexp-attribute gexp self-attribute #:optional (equal? equal?))
+  "Recurse on GEXP and the expressions it refers to, summing the items
+returned by SELF-ATTRIBUTE, a procedure that takes a gexp.  Use EQUAL? as the
+second argument to 'delete-duplicates'."
+  (if (gexp? gexp)
+      (delete-duplicates
+       (append (self-attribute gexp)
+               (append-map (match-lambda
+                             (($ <gexp-input> (? gexp? exp))
+                              (gexp-attribute exp self-attribute))
+                             (($ <gexp-input> (lst ...))
+                              (append-map (lambda (item)
+                                            (if (gexp? item)
+                                                (gexp-attribute item
+                                                                self-attribute)
+                                                '()))
+                                          lst))
+                             (_
+                              '()))
+                           (gexp-references gexp)))
+       equal?)
+      '()))                                       ;plain Scheme data type
+
 (define (gexp-modules gexp)
-  "Return the list of Guile module names GEXP relies on."
-  (delete-duplicates
-   (append (gexp-self-modules gexp)
-           (append-map (match-lambda
-                         (($ <gexp-input> (? gexp? exp))
-                          (gexp-modules exp))
-                         (($ <gexp-input> (lst ...))
-                          (append-map (lambda (item)
-                                        (if (gexp? item)
-                                            (gexp-modules item)
-                                            '()))
-                                      lst))
-                         (_
-                          '()))
-                       (gexp-references gexp)))))
-
-(define raw-derivation
-  (store-lift derivation))
+  "Return the list of Guile module names GEXP relies on.  If (gexp? GEXP) is
+false, meaning that GEXP is a plain Scheme object, return the empty list."
+  (define (module=? m1 m2)
+    ;; Return #t when M1 equals M2.  Special-case '=>' specs because their
+    ;; right-hand side may not be comparable with 'equal?': it's typically a
+    ;; file-like object that embeds a gexp, which in turn embeds closure;
+    ;; those closures may be 'eq?' when running compiled code but are unlikely
+    ;; to be 'eq?' when running on 'eval'.  Ignore the right-hand side to
+    ;; avoid this discrepancy.
+    (match m1
+      (((name1 ...) '=> _)
+       (match m2
+         (((name2 ...) '=> _) (equal? name1 name2))
+         (_ #f)))
+      (_
+       (equal? m1 m2))))
+
+  (gexp-attribute gexp gexp-self-modules module=?))
+
+(define (gexp-extensions gexp)
+  "Return the list of Guile extensions (packages) GEXP relies on.  If (gexp?
+GEXP) is false, meaning that GEXP is a plain Scheme object, return the empty
+list."
+  (gexp-attribute gexp gexp-self-extensions))
 
 (define* (lower-inputs inputs
                        #:key system target)
@@ -521,11 +620,19 @@ names and file names suitable for the #:allowed-references argument to
                            (modules '())
                            (module-path %load-path)
                            (guile-for-build (%guile-for-build))
+                           (effective-version "2.2")
                            (graft? (%graft?))
                            references-graphs
                            allowed-references disallowed-references
                            leaked-env-vars
                            local-build? (substitutable? #t)
+
+                           ;; TODO: This parameter is transitional; it's here
+                           ;; to avoid a full rebuild.  Remove it on the next
+                           ;; rebuild cycle.
+                           import-creates-derivation?
+
+                           deprecation-warnings
                            (script-name (string-append name "-builder")))
   "Return a derivation NAME that runs EXP (a gexp) with GUILE-FOR-BUILD (a
 derivation) on SYSTEM; EXP is stored in a file called SCRIPT-NAME.  When
@@ -538,6 +645,9 @@ names of Guile modules searched in MODULE-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)).
 
+EFFECTIVE-VERSION determines the string to use when adding extensions of
+EXP (see 'with-extensions') to the search path---e.g., \"2.2\".
+
 GRAFT? determines whether packages referred to by EXP should be grafted when
 applicable.
 
@@ -561,6 +671,9 @@ refer to.  Any reference to another store item will lead to a build error.
 Similarly for DISALLOWED-REFERENCES, which can list items that must not be
 referenced by the outputs.
 
+DEPRECATION-WARNINGS determines whether to show deprecation warnings while
+compiling modules.  It can be #f, #t, or 'detailed.
+
 The other arguments are as for 'derivation'."
   (define %modules
     (delete-duplicates
@@ -570,7 +683,7 @@ The other arguments are as for 'derivation'."
   (define (graphs-file-names graphs)
     ;; Return a list of (FILE-NAME . STORE-PATH) pairs made from GRAPHS.
     (map (match-lambda
-          ;; TODO: Remove 'derivation?' special cases.
+           ;; TODO: Remove 'derivation?' special cases.
            ((file-name (? derivation? drv))
             (cons file-name (derivation->output-path drv)))
            ((file-name (? derivation? drv) sub-drv)
@@ -579,7 +692,13 @@ The other arguments are as for 'derivation'."
             (cons file-name thing)))
          graphs))
 
-  (mlet* %store-monad (;; The following binding forces '%current-system' and
+  (define (extension-flags extension)
+    `("-L" ,(string-append (derivation->output-path extension)
+                           "/share/guile/site/" effective-version)
+      "-C" ,(string-append (derivation->output-path extension)
+                           "/lib/guile/" effective-version "/site-ccache")))
+
+  (mlet* %store-monad ( ;; The following binding forces '%current-system' and
                        ;; '%current-target-system' to be looked up at >>=
                        ;; time.
                        (graft?    (set-grafting graft?))
@@ -600,17 +719,31 @@ The other arguments are as for 'derivation'."
                                              #:target target))
                        (builder  (text-file script-name
                                             (object->string sexp)))
+                       (extensions -> (gexp-extensions exp))
+                       (exts     (mapm %store-monad
+                                       (lambda (obj)
+                                         (lower-object obj system))
+                                       extensions))
                        (modules  (if (pair? %modules)
                                      (imported-modules %modules
+                                                       #:derivation?
+                                                       import-creates-derivation?
                                                        #:system system
                                                        #:module-path module-path
-                                                       #:guile guile-for-build)
+                                                       #:guile guile-for-build
+                                                       #:deprecation-warnings
+                                                       deprecation-warnings)
                                      (return #f)))
                        (compiled (if (pair? %modules)
                                      (compiled-modules %modules
+                                                       #:derivation?
+                                                       import-creates-derivation?
                                                        #:system system
                                                        #:module-path module-path
-                                                       #:guile guile-for-build)
+                                                       #:extensions extensions
+                                                       #:guile guile-for-build
+                                                       #:deprecation-warnings
+                                                       deprecation-warnings)
                                      (return #f)))
                        (graphs   (if references-graphs
                                      (lower-reference-graphs references-graphs
@@ -637,9 +770,12 @@ The other arguments are as for 'derivation'."
                                      "/bin/guile")
                       `("--no-auto-compile"
                         ,@(if (pair? %modules)
-                              `("-L" ,(derivation->output-path modules)
+                              `("-L" ,(if (derivation? modules)
+                                          (derivation->output-path modules)
+                                          modules)
                                 "-C" ,(derivation->output-path compiled))
                               '())
+                        ,@(append-map extension-flags exts)
                         ,builder)
                       #:outputs outputs
                       #:env-vars env-vars
@@ -649,6 +785,7 @@ The other arguments are as for 'derivation'."
                                  ,@(if modules
                                        `((,modules) (,compiled) ,@inputs)
                                        inputs)
+                                 ,@(map list exts)
                                  ,@(match graphs
                                      (((_ . inputs) ...) inputs)
                                      (_ '())))
@@ -672,41 +809,36 @@ references; otherwise, return only non-native references."
                    result)
            result))
       (($ <gexp-input> (? gexp? exp) _ #f)
-       (if native?
-           (append (gexp-inputs exp #:native? #t)
-                   result)
-           (append (gexp-inputs exp)
-                   result)))
+       (append (gexp-inputs exp #:native? native?)
+               result))
       (($ <gexp-input> (? string? str))
        (if (direct-store-path? str)
            (cons `(,str) result)
            result))
-      (($ <gexp-input> (? struct? thing) output)
-       (if (lookup-compiler thing)
+      (($ <gexp-input> (? struct? thing) output n?)
+       (if (and (eqv? n? native?) (lookup-compiler thing))
            ;; THING is a derivation, or a package, or an origin, etc.
            (cons `(,thing ,output) result)
            result))
       (($ <gexp-input> (lst ...) output n?)
        (fold-right add-reference-inputs result
                    ;; XXX: For now, automatically convert LST to a list of
-                   ;; gexp-inputs.
+                   ;; gexp-inputs.  Inherit N?.
                    (map (match-lambda
-                         ((? gexp-input? x) x)
-                         (x (%gexp-input x "out" (or n? native?))))
+                          ((? gexp-input? x)
+                           (%gexp-input (gexp-input-thing x)
+                                        (gexp-input-output x)
+                                        n?))
+                          (x
+                           (%gexp-input x "out" n?)))
                         lst)))
       (_
        ;; Ignore references to other kinds of objects.
        result)))
 
-  (define (native-input? x)
-    (and (gexp-input? x)
-         (gexp-input-native? x)))
-
   (fold-right add-reference-inputs
               '()
-              (if native?
-                  (filter native-input? (gexp-references exp))
-                  (remove native-input? (gexp-references exp)))))
+              (gexp-references exp)))
 
 (define gexp-native-inputs
   (cut gexp-inputs <> #:native? #t))
@@ -802,6 +934,17 @@ environment."
                          (identifier-syntax modules)))
     body ...))
 
+(define-syntax-parameter current-imported-extensions
+  ;; Current list of extensions.
+  (identifier-syntax '()))
+
+(define-syntax-rule (with-extensions extensions body ...)
+  "Mark the gexps defined in BODY... as requiring EXTENSIONS in their
+execution environment."
+  (syntax-parameterize ((current-imported-extensions
+                         (identifier-syntax extensions)))
+    body ...))
+
 (define-syntax gexp
   (lambda (s)
     (define (collect-escapes exp)
@@ -822,9 +965,9 @@ environment."
            (cons exp result))
           ((ungexp-native-splicing _ ...)
            (cons exp result))
-          ((exp0 exp ...)
+          ((exp0 . exp)
            (let ((result (loop #'exp0 result)))
-             (fold loop result #'(exp ...))))
+             (loop  #'exp result)))
           (_
            result))))
 
@@ -856,9 +999,9 @@ environment."
       (match (assoc exp substs)
         ((_ id)
          id)
-        (_
-         #'(syntax-error "error: no 'ungexp' substitution"
-                         #'ref))))
+        (_                                        ;internal error
+         (with-syntax ((exp exp))
+           #'(syntax-error "error: no 'ungexp' substitution" exp)))))
 
     (define (substitute-ungexp-splicing exp substs)
       (syntax-case exp ()
@@ -870,7 +1013,7 @@ environment."
                         #,(substitute-references #'(rest ...) substs))))
            (_
             #'(syntax-error "error: no 'ungexp-splicing' substitution"
-                            #'ref))))))
+                            exp))))))
 
     (define (substitute-references exp substs)
       ;; Return a variant of EXP where all the cars of SUBSTS have been
@@ -885,9 +1028,9 @@ environment."
          (substitute-ungexp-splicing exp substs))
         (((ungexp-native-splicing _ ...) rest ...)
          (substitute-ungexp-splicing exp substs))
-        ((exp0 exp ...)
+        ((exp0 . exp)
          #`(cons #,(substitute-references #'exp0 substs)
-                 #,(substitute-references #'(exp ...) substs)))
+                 #,(substitute-references #'exp substs)))
         (x #''x)))
 
     (syntax-case s (ungexp output)
@@ -898,6 +1041,7 @@ environment."
               (refs    (map escape->ref escapes)))
          #`(make-gexp (list #,@refs)
                       current-imported-modules
+                      current-imported-extensions
                       (lambda #,formals
                         #,sexp)))))))
 
@@ -906,6 +1050,49 @@ environment."
 ;;; Module handling.
 ;;;
 
+(define %not-slash
+  (char-set-complement (char-set #\/)))
+
+(define (file-mapping->tree mapping)
+  "Convert MAPPING, an alist like:
+
+  ((\"guix/build/utils.scm\" . \"…/utils.scm\"))
+
+to a tree suitable for 'interned-file-tree'."
+  (let ((mapping (map (match-lambda
+                        ((destination . source)
+                         (cons (string-tokenize destination
+                                                %not-slash)
+                               source)))
+                      mapping)))
+    (fold (lambda (pair result)
+            (match pair
+              ((destination . source)
+               (let loop ((destination destination)
+                          (result result))
+                 (match destination
+                   ((file)
+                    (let* ((mode (stat:mode (stat source)))
+                           (type (if (zero? (logand mode #o100))
+                                     'regular
+                                     'executable)))
+                      (alist-cons file
+                                  `(,type (file ,source))
+                                  result)))
+                   ((file rest ...)
+                    (let ((directory (assoc-ref result file)))
+                      (alist-cons file
+                                  `(directory
+                                    ,@(loop rest
+                                            (match directory
+                                              (('directory . entries) entries)
+                                              (#f '()))))
+                                  (if directory
+                                      (alist-delete file result)
+                                      result)))))))))
+          '()
+          mapping)))
+
 (define %utils-module
   ;; This file provides 'mkdir-p', needed to implement 'imported-files' and
   ;; other primitives below.  Note: We give the file name relative to this
@@ -914,18 +1101,32 @@ environment."
   (local-file "build/utils.scm"
               "build-utils.scm"))
 
-(define* (imported-files files
-                         #:key (name "file-import")
-                         (system (%current-system))
-                         (guile (%guile-for-build)))
+(define* (imported-files/derivation files
+                                    #:key (name "file-import")
+                                    (symlink? #f)
+                                    (system (%current-system))
+                                    (guile (%guile-for-build))
+
+                                    ;; XXX: The only reason we have
+                                    ;; #:deprecation-warnings is because (guix
+                                    ;; build utils), which we use here, relies
+                                    ;; on _IO*, which is deprecated in 2.2.  On
+                                    ;; the next full-rebuild cycle, we should
+                                    ;; disable such warnings unconditionally.
+                                    (deprecation-warnings #f))
   "Return a derivation that imports FILES into STORE.  FILES must be a list
-of (FINAL-PATH . FILE-NAME) pairs; each FILE-NAME is read from the file
-system, imported, and appears under FINAL-PATH in the resulting store path."
+of (FINAL-PATH . FILE) pairs.  Each FILE is mapped to FINAL-PATH in the
+resulting store path.  FILE can be either a file name, or a file-like object,
+as returned by 'local-file' for example.  If SYMLINK? is true, create symlinks
+to the source files instead of copying them."
   (define file-pair
     (match-lambda
-     ((final-path . file-name)
+     ((final-path . (? string? file-name))
       (mlet %store-monad ((file (interned-file file-name
                                                (basename final-path))))
+        (return (list final-path file))))
+     ((final-path . file-like)
+      (mlet %store-monad ((file (lower-object file-like system)))
         (return (list final-path file))))))
 
   (mlet %store-monad ((files (sequence %store-monad
@@ -940,7 +1141,8 @@ system, imported, and appears under FINAL-PATH in the resulting store path."
          (for-each (match-lambda
                     ((final-path store-path)
                      (mkdir-p (dirname final-path))
-                     (symlink store-path final-path)))
+                     ((ungexp (if symlink? 'symlink 'copy-file))
+                      store-path final-path)))
                    '(ungexp files)))))
 
     ;; TODO: Pass FILES as an environment variable so that BUILD remains
@@ -949,80 +1151,206 @@ system, imported, and appears under FINAL-PATH in the resulting store path."
     (gexp->derivation name build
                       #:system system
                       #:guile-for-build guile
-                      #:local-build? #t)))
+                      #:local-build? #t
+
+                      ;; TODO: On the next rebuild cycle, set to "no"
+                      ;; unconditionally.
+                      #:env-vars
+                      (case deprecation-warnings
+                        ((#f)
+                         '(("GUILE_WARN_DEPRECATED" . "no")))
+                        ((detailed)
+                         '(("GUILE_WARN_DEPRECATED" . "detailed")))
+                        (else
+                         '())))))
+
+(define* (imported-files files
+                         #:key (name "file-import")
+
+                         ;; TODO: Remove this parameter on the next rebuild
+                         ;; cycle.
+                         (derivation? #f)
+
+                         ;; The following parameters make sense when creating
+                         ;; an actual derivation.
+                         (system (%current-system))
+                         (guile (%guile-for-build))
+                         (deprecation-warnings #f))
+  "Import FILES into the store and return the resulting derivation or store
+file name (a derivation is created if and only if some elements of FILES are
+file-like objects and not local file names.)  FILES must be a list
+of (FINAL-PATH . FILE) pairs.  Each FILE is mapped to FINAL-PATH in the
+resulting store path.  FILE can be either a file name, or a file-like object,
+as returned by 'local-file' for example."
+  (if (or derivation?
+          (any (match-lambda
+                 ((_ . (? struct? source)) #t)
+                 (_ #f))
+               files))
+      (imported-files/derivation files #:name name
+                                 #:symlink? derivation?
+                                 #:system system #:guile guile
+                                 #:deprecation-warnings deprecation-warnings)
+      (interned-file-tree `(,name directory
+                                  ,@(file-mapping->tree files)))))
 
 (define* (imported-modules modules
                            #:key (name "module-import")
+                           (derivation? #f)      ;TODO: remove on next rebuild
                            (system (%current-system))
                            (guile (%guile-for-build))
-                           (module-path %load-path))
+                           (module-path %load-path)
+                           (deprecation-warnings #f))
   "Return a derivation that contains the source files of MODULES, a list of
-module names such as `(ice-9 q)'.  All of MODULES must be in the MODULE-PATH
-search path."
-  ;; TODO: Determine the closure of MODULES, build the `.go' files,
-  ;; canonicalize the source files through read/write, etc.
-  (let ((files (map (lambda (m)
-                      (let ((f (module->source-file-name m)))
-                        (cons f (search-path* module-path f))))
+module names such as `(ice-9 q)'.  All of MODULES must be either names of
+modules to be found in the MODULE-PATH search path, or a module name followed
+by an arrow followed by a file-like object.  For example:
+
+  (imported-modules `((guix build utils)
+                      (guix gcrypt)
+                      ((guix config) => ,(scheme-file …))))
+
+In this example, the first two modules are taken from MODULE-PATH, and the
+last one is created from the given <scheme-file> object."
+  (let ((files (map (match-lambda
+                      (((module ...) '=> file)
+                       (cons (module->source-file-name module)
+                             file))
+                      ((module ...)
+                       (let ((f (module->source-file-name module)))
+                         (cons f (search-path* module-path f)))))
                     modules)))
-    (imported-files files #:name name #:system system
-                    #:guile guile)))
+    (imported-files files #:name name
+                    #:derivation? derivation?
+                    #:system system
+                    #:guile guile
+                    #:deprecation-warnings deprecation-warnings)))
 
 (define* (compiled-modules modules
                            #:key (name "module-import-compiled")
+                           (derivation? #f)      ;TODO: remove on next rebuild
                            (system (%current-system))
                            (guile (%guile-for-build))
-                           (module-path %load-path))
+                           (module-path %load-path)
+                           (extensions '())
+                           (deprecation-warnings #f))
   "Return a derivation that builds a tree containing the `.go' files
 corresponding to MODULES.  All the MODULES are built in a context where
 they can refer to each other."
+  (define total (length modules))
+
+  (define build-utils-hack?
+    ;; To avoid a full rebuild, we limit the fix below to the case where
+    ;; MODULE-PATH is different from %LOAD-PATH.  This happens when building
+    ;; modules for 'compute-guix-derivation' upon 'guix pull'.  TODO: Make
+    ;; this unconditional on the next rebuild cycle.
+    (and (member '(guix build utils) modules)
+         (not (equal? module-path %load-path))))
+
   (mlet %store-monad ((modules (imported-modules modules
+                                                 #:derivation? derivation?
                                                  #:system system
                                                  #:guile guile
                                                  #:module-path
-                                                 module-path)))
+                                                 module-path
+                                                 #:deprecation-warnings
+                                                 deprecation-warnings)))
     (define build
       (gexp
        (begin
          (primitive-load (ungexp %utils-module))  ;for 'mkdir-p'
 
          (use-modules (ice-9 ftw)
+                      (ice-9 format)
+                      (srfi srfi-1)
                       (srfi srfi-26)
                       (system base compile))
 
          (define (regular? file)
            (not (member file '("." ".."))))
 
-         (define (process-directory directory output)
+         (define (process-entry entry output processed)
+           (if (file-is-directory? entry)
+               (let ((output (string-append output "/" (basename entry))))
+                 (mkdir-p output)
+                 (process-directory entry output processed))
+               (let* ((base   (basename entry ".scm"))
+                      (output (string-append output "/" base ".go")))
+                 (format #t "[~2@a/~2@a] Compiling '~a'...~%"
+                         (+ 1 processed) (ungexp total) entry)
+                 (compile-file entry
+                               #:output-file output
+                               #:opts %auto-compilation-options)
+                 (+ 1 processed))))
+
+         (define (process-directory directory output processed)
            (let ((entries (map (cut string-append directory "/" <>)
                                (scandir directory regular?))))
-             (for-each (lambda (entry)
-                         (if (file-is-directory? entry)
-                             (let ((output (string-append output "/"
-                                                          (basename entry))))
-                               (mkdir-p output)
-                               (process-directory entry output))
-                             (let* ((base   (string-drop-right
-                                             (basename entry)
-                                             4)) ;.scm
-                                    (output (string-append output "/" base
-                                                           ".go")))
-                               (compile-file entry
-                                             #:output-file output
-                                             #:opts
-                                             %auto-compilation-options))))
-                       entries)))
+             (fold (cut process-entry <> output <>)
+                   processed
+                   entries)))
+
+         (setvbuf (current-output-port)
+                  (cond-expand (guile-2.2 'line) (else _IOLBF)))
+
+         (ungexp-splicing
+          (if build-utils-hack?
+              (gexp ((define mkdir-p
+                       ;; Capture 'mkdir-p'.
+                       (@ (guix build utils) mkdir-p))))
+              '()))
+
+         ;; Add EXTENSIONS to the search path.
+         ;; TODO: Remove the outer 'ungexp-splicing' on the next rebuild cycle.
+         (ungexp-splicing
+          (if (null? extensions)
+              '()
+              (gexp ((set! %load-path
+                       (append (map (lambda (extension)
+                                      (string-append extension
+                                                     "/share/guile/site/"
+                                                     (effective-version)))
+                                    '((ungexp-native-splicing extensions)))
+                               %load-path))
+                     (set! %load-compiled-path
+                       (append (map (lambda (extension)
+                                      (string-append extension "/lib/guile/"
+                                                     (effective-version)
+                                                     "/site-ccache"))
+                                    '((ungexp-native-splicing extensions)))
+                               %load-compiled-path))))))
 
          (set! %load-path (cons (ungexp modules) %load-path))
+
+         (ungexp-splicing
+          (if build-utils-hack?
+              ;; Above we loaded our own (guix build utils) but now we may
+              ;; need to load a compile a different one.  Thus, force a
+              ;; reload.
+              (gexp ((let ((utils (ungexp
+                                   (file-append modules
+                                                "/guix/build/utils.scm"))))
+                       (when (file-exists? utils)
+                         (load utils)))))
+              '()))
+
          (mkdir (ungexp output))
          (chdir (ungexp modules))
-         (process-directory "." (ungexp output)))))
+         (process-directory "." (ungexp output) 0))))
 
     ;; TODO: Pass MODULES as an environment variable.
     (gexp->derivation name build
                       #:system system
                       #:guile-for-build guile
-                      #:local-build? #t)))
+                      #:local-build? #t
+                      #:env-vars
+                      (case deprecation-warnings
+                        ((#f)
+                         '(("GUILE_WARN_DEPRECATED" . "no")))
+                        ((detailed)
+                         '(("GUILE_WARN_DEPRECATED" . "detailed")))
+                        (else
+                         '())))))
 
 \f
 ;;;
@@ -1030,29 +1358,52 @@ they can refer to each other."
 ;;;
 
 (define (default-guile)
-  ;; Lazily resolve 'guile-final'.  This module must not refer to (gnu …)
+  ;; Lazily resolve 'guile-2.2' (not 'guile-final' because this is for
+  ;; programs returned by 'program-file' and we don't want to keep references
+  ;; to several Guile packages).  This module must not refer to (gnu …)
   ;; modules directly, to avoid circular dependencies, hence this hack.
-  (module-ref (resolve-interface '(gnu packages commencement))
-              'guile-final))
+  (module-ref (resolve-interface '(gnu packages guile))
+              'guile-2.2))
 
-(define (load-path-expression modules)
+(define* (load-path-expression modules #:optional (path %load-path)
+                               #:key (extensions '()))
   "Return as a monadic value a gexp that sets '%load-path' and
-'%load-compiled-path' to point to MODULES, a list of module names."
-  (mlet %store-monad ((modules  (imported-modules modules))
-                      (compiled (compiled-modules modules)))
+'%load-compiled-path' to point to MODULES, a list of module names.  MODULES
+are searched for in PATH."
+  (mlet %store-monad ((modules  (imported-modules modules
+                                                  #:module-path path))
+                      (compiled (compiled-modules modules
+                                                  #:extensions extensions
+                                                  #:module-path path)))
     (return (gexp (eval-when (expand load eval)
                     (set! %load-path
-                      (cons (ungexp modules) %load-path))
+                      (cons (ungexp modules)
+                            (append (map (lambda (extension)
+                                           (string-append extension
+                                                          "/share/guile/site/"
+                                                          (effective-version)))
+                                         '((ungexp-native-splicing extensions)))
+                                    %load-path)))
                     (set! %load-compiled-path
                       (cons (ungexp compiled)
-                            %load-compiled-path)))))))
+                            (append (map (lambda (extension)
+                                           (string-append extension
+                                                          "/lib/guile/"
+                                                          (effective-version)
+                                                          "/site-ccache"))
+                                         '((ungexp-native-splicing extensions)))
+                                    %load-compiled-path))))))))
 
 (define* (gexp->script name exp
-                       #:key (guile (default-guile)))
+                       #:key (guile (default-guile))
+                       (module-path %load-path))
   "Return an executable script NAME that runs EXP using GUILE, with EXP's
-imported modules in its search path."
+imported modules in its search path.  Look up EXP's modules in MODULE-PATH."
   (mlet %store-monad ((set-load-path
-                       (load-path-expression (gexp-modules exp))))
+                       (load-path-expression (gexp-modules exp)
+                                             module-path
+                                             #:extensions
+                                             (gexp-extensions exp))))
     (gexp->derivation name
                       (gexp
                        (call-with-output-file (ungexp output)
@@ -1067,31 +1418,52 @@ imported modules in its search path."
 
                            (write '(ungexp set-load-path) port)
                            (write '(ungexp exp) port)
-                           (chmod port #o555)))))))
-
-(define* (gexp->file name exp #:key (set-load-path? #t))
-  "Return a derivation that builds a file NAME containing EXP.  When
-SET-LOAD-PATH? is true, emit code in the resulting file to set '%load-path'
-and '%load-compiled-path' to honor EXP's imported modules."
-  (match (if set-load-path? (gexp-modules exp) '())
-    (()                                           ;zero modules
-     (gexp->derivation name
-                       (gexp
-                        (call-with-output-file (ungexp output)
-                          (lambda (port)
-                            (write '(ungexp exp) port))))
-                       #:local-build? #t
-                       #:substitutable? #f))
-    ((modules ...)
-     (mlet %store-monad ((set-load-path (load-path-expression modules)))
-       (gexp->derivation name
-                         (gexp
-                          (call-with-output-file (ungexp output)
-                            (lambda (port)
-                              (write '(ungexp set-load-path) port)
-                              (write '(ungexp exp) port))))
-                         #:local-build? #t
-                         #:substitutable? #f)))))
+                           (chmod port #o555))))
+                      #:module-path module-path)))
+
+(define* (gexp->file name exp #:key
+                     (set-load-path? #t)
+                     (module-path %load-path)
+                     (splice? #f))
+  "Return a derivation that builds a file NAME containing EXP.  When SPLICE?
+is true, EXP is considered to be a list of expressions that will be spliced in
+the resulting file.
+
+When SET-LOAD-PATH? is true, emit code in the resulting file to set
+'%load-path' and '%load-compiled-path' to honor EXP's imported modules.
+Lookup EXP's modules in MODULE-PATH."
+  (define modules (gexp-modules exp))
+  (define extensions (gexp-extensions exp))
+
+  (if (or (not set-load-path?)
+          (and (null? modules) (null? extensions)))
+      (gexp->derivation name
+                        (gexp
+                         (call-with-output-file (ungexp output)
+                           (lambda (port)
+                             (for-each (lambda (exp)
+                                         (write exp port))
+                                       '(ungexp (if splice?
+                                                    exp
+                                                    (gexp ((ungexp exp)))))))))
+                        #:local-build? #t
+                        #:substitutable? #f)
+      (mlet %store-monad ((set-load-path
+                           (load-path-expression modules module-path
+                                                 #:extensions extensions)))
+        (gexp->derivation name
+                          (gexp
+                           (call-with-output-file (ungexp output)
+                             (lambda (port)
+                               (write '(ungexp set-load-path) port)
+                               (for-each (lambda (exp)
+                                           (write exp port))
+                                         '(ungexp (if splice?
+                                                      exp
+                                                      (gexp ((ungexp exp)))))))))
+                          #:module-path module-path
+                          #:local-build? #t
+                          #:substitutable? #f))))
 
 (define* (text-file* name #:rest text)
   "Return as a monadic value a derivation that builds a text file containing
@@ -1122,6 +1494,90 @@ This is the declarative counterpart of 'text-file*'."
 
   (computed-file name build))
 
+(define (file-union name files)
+  "Return a <computed-file> that builds a directory containing all of FILES.
+Each item in FILES must be a two-element list where the first element is the
+file name to use in the new directory, and the second element is a gexp
+denoting the target file.  Here's an example:
+
+  (file-union \"etc\"
+              `((\"hosts\" ,(plain-file \"hosts\"
+                                        \"127.0.0.1 localhost\"))
+                (\"bashrc\" ,(plain-file \"bashrc\"
+                                         \"alias ls='ls --color'\"))
+                (\"libvirt/qemu.conf\" ,(plain-file \"qemu.conf\" \"\"))))
+
+This yields an 'etc' directory containing these two files."
+  (computed-file name
+                 (with-imported-modules '((guix build utils))
+                   (gexp
+                    (begin
+                      (use-modules (guix build utils))
+
+                      (mkdir (ungexp output))
+                      (chdir (ungexp output))
+                      (ungexp-splicing
+                       (map (match-lambda
+                              ((target source)
+                               (gexp
+                                (begin
+                                  ;; Stat the source to abort early if it does
+                                  ;; not exist.
+                                  (stat (ungexp source))
+
+                                  (mkdir-p (dirname (ungexp target)))
+                                  (symlink (ungexp source)
+                                           (ungexp target))))))
+                            files)))))))
+
+(define* (directory-union name things
+                          #:key (copy? #f) (quiet? #f)
+                          (resolve-collision 'warn-about-collision))
+  "Return a directory that is the union of THINGS, where THINGS is a list of
+file-like objects denoting directories.  For example:
+
+  (directory-union \"guile+emacs\" (list guile emacs))
+
+yields a directory that is the union of the 'guile' and 'emacs' packages.
+
+Call RESOLVE-COLLISION when several files collide, passing it the list of
+colliding files.  RESOLVE-COLLISION must return the chosen file or #f, in
+which case the colliding entry is skipped altogether.
+
+When HARD-LINKS? is true, create hard links instead of symlinks.  When QUIET?
+is true, the derivation will not print anything."
+  (define symlink
+    (if copy?
+        (gexp (lambda (old new)
+                (if (file-is-directory? old)
+                    (symlink old new)
+                    (copy-file old new))))
+        (gexp symlink)))
+
+  (define log-port
+    (if quiet?
+        (gexp (%make-void-port "w"))
+        (gexp (current-error-port))))
+
+  (match things
+    ((one)
+     ;; Only one thing; return it.
+     one)
+    (_
+     (computed-file name
+                    (with-imported-modules '((guix build union))
+                      (gexp (begin
+                              (use-modules (guix build union)
+                                           (srfi srfi-1)) ;for 'first' and 'last'
+
+                              (union-build (ungexp output)
+                                           '(ungexp things)
+
+                                           #:log-port (ungexp log-port)
+                                           #:symlink (ungexp symlink)
+                                           #:resolve-collision
+                                           (ungexp resolve-collision)))))))))
+
 \f
 ;;;
 ;;; Syntactic sugar.