diff mbox series

[bug#42849,3/3] installer: Run the installation inside a container.

Message ID 20200813123419.263639-3-othacehe@gnu.org
State Accepted
Headers show
Series [bug#42849,1/3] install: Factorize cow-store procedure. | expand

Checks

Context Check Description
cbaines/comparison success View comparision
cbaines/git branch success View Git branch
cbaines/applying patch success View Laminar job
cbaines/comparison success View comparision
cbaines/git branch success View Git branch
cbaines/applying patch success View Laminar job

Commit Message

Mathieu Othacehe Aug. 13, 2020, 12:34 p.m. UTC
When the store overlay is mounted, other processes such as kmscon, udev
and guix-daemon may open files from the store, preventing the
underlying install support from being umounted. See:
https://lists.gnu.org/archive/html/guix-devel/2018-12/msg00161.html.

To avoid this situation, mount the store overlay inside a container,
and run the installation from within that container.

* gnu/services/base.scm (guix-shepherd-service): Support an optional PID
argument passed to the "start" method. If that argument is passed, ensure that
guix-daemon enters the given PID MNT namespace.
* gnu/installer/final.scm (umount-cow-store): Remove it,
(install-system): run the installation from within a container.
* gnu/installer/newt/final.scm (run-install-shell): Remove the display hack.
---
 gnu/installer/final.scm      | 125 +++++++++++++++++------------------
 gnu/installer/newt/final.scm |   7 --
 gnu/services/base.scm        |  60 ++++++++++-------
 3 files changed, 99 insertions(+), 93 deletions(-)

Comments

Ludovic Courtès Aug. 30, 2020, 8:40 p.m. UTC | #1
Hi,

Mathieu Othacehe <othacehe@gnu.org> skribis:

> When the store overlay is mounted, other processes such as kmscon, udev
> and guix-daemon may open files from the store, preventing the
> underlying install support from being umounted. See:
> https://lists.gnu.org/archive/html/guix-devel/2018-12/msg00161.html.
>
> To avoid this situation, mount the store overlay inside a container,
> and run the installation from within that container.
>
> * gnu/services/base.scm (guix-shepherd-service): Support an optional PID
> argument passed to the "start" method. If that argument is passed, ensure that
> guix-daemon enters the given PID MNT namespace.
> * gnu/installer/final.scm (umount-cow-store): Remove it,
> (install-system): run the installation from within a container.
> * gnu/installer/newt/final.scm (run-install-shell): Remove the display hack.

Smart!

> +    ;; When the store overlay is mounted, other processes such as kmscon, udev
> +    ;; and guix-daemon may open files from the store, preventing the
> +    ;; underlying install support from being umounted. See:
> +    ;; https://lists.gnu.org/archive/html/guix-devel/2018-12/msg00161.html.
> +    ;;
> +    ;; To avoid this situation, mount the store overlay inside a container,
> +    ;; and run the installation from within that container.
> +    (zero?
> +     (call-with-container '()
> +       (lambda ()
> +         (dynamic-wind
> +           (lambda ()
> +             ;; Save the database, so that it can be restored once the
> +             ;; cow-store is umounted.
> +             (copy-file database-file saved-database)
> +             (mount-cow-store (%installer-target-dir) backing-directory))
> +           (lambda ()
> +             ;; We need to drag the guix-daemon to the container MNT
> +             ;; namespace, so that it can operate on the cow-store.
> +             (stop-service 'guix-daemon)
> +             (start-service 'guix-daemon (list (number->string (getpid))))
> +
> +             (setvbuf (current-output-port) 'none)
> +             (setvbuf (current-error-port) 'none)
> +
> +             ;; If there are any connected clients, assume that we are running
> +             ;; installation tests. In that case, dump the standard and error
> +             ;; outputs to syslog.
> +             (set! ret
> +                   (if (not (null? (current-clients)))
> +                       (with-output-to-file "/dev/console"
> +                         (lambda ()
> +                           (with-error-to-file "/dev/console"
> +                             (lambda ()
> +                               (run-command install-command
> +                                            #:locale locale)))))
> +                       (run-command install-command #:locale locale))))
> +           (lambda ()
> +             ;; Restart guix-daemon so that it does no keep the MNT namespace
> +             ;; alive.
> +             (restart-service 'guix-daemon)
> +             (copy-file saved-database database-file)
> +
> +             ;; Finally umount the cow-store and exit the container.
> +             (umount-cow-store (%installer-target-dir) backing-directory)
> +             (assert-exit ret))))

Should ‘mount-cow-store’ also make an overlay for /var/guix/db?  That
way, changes to that directory would go to /mnt/var/guix/db and the
original database would remain unchanged.

> --- a/gnu/services/base.scm
> +++ b/gnu/services/base.scm
> @@ -1558,36 +1558,50 @@ proxy of 'guix-daemon'...~%")
>             (provision '(guix-daemon))
>             (requirement '(user-processes))
>             (actions (list shepherd-set-http-proxy-action))
> -           (modules '((srfi srfi-1)))
> +           (modules '((srfi srfi-1)
> +                      (ice-9 match)))
>             (start
> -            #~(lambda _
> +            #~(lambda args
>                  (define proxy
>                    ;; HTTP/HTTPS proxy.  The 'http_proxy' variable is set by
>                    ;; the 'set-http-proxy' action.
>                    (or (getenv "http_proxy") #$http-proxy))
>  
>                  (fork+exec-command
> -                 (cons* #$(file-append guix "/bin/guix-daemon")
> -                        "--build-users-group" #$build-group
> -                        "--max-silent-time" #$(number->string max-silent-time)
> -                        "--timeout" #$(number->string timeout)
> -                        "--log-compression" #$(symbol->string log-compression)
> -                        #$@(if use-substitutes?
> -                               '()
> -                               '("--no-substitutes"))
> -                        "--substitute-urls" #$(string-join substitute-urls)
> -                        #$@extra-options
> -
> -                        ;; Add CHROOT-DIRECTORIES and all their dependencies
> -                        ;; (if these are store items) to the chroot.
> -                        (append-map (lambda (file)
> -                                      (append-map (lambda (directory)
> -                                                    (list "--chroot-directory"
> -                                                          directory))
> -                                                  (call-with-input-file file
> -                                                    read)))
> -                                    '#$(map references-file
> -                                            chroot-directories)))
> +                 ;; When running the installer, we need guix-daemon to operate
> +                 ;; from within the same MNT namespace as the installation
> +                 ;; container. In that case only, enter the namespace of the
> +                 ;; process PID passed as start argument.
> +                 (append
> +                  (match args
> +                    ((pid)
> +                     (list #$(file-append util-linux "/bin/nsenter")
> +                           "-t" pid "-m"))

We should use ‘container-excursion’ instead of nsenter.

> +                    (else '()))
> +                  (cons* #$(file-append guix "/bin/guix-daemon")
> +                         "--build-users-group" #$build-group
> +                         "--max-silent-time"
> +                         #$(number->string max-silent-time)
> +                         "--timeout" #$(number->string timeout)
> +                         "--log-compression"
> +                         #$(symbol->string log-compression)
> +                         #$@(if use-substitutes?
> +                                '()
> +                                '("--no-substitutes"))
> +                         "--substitute-urls" #$(string-join substitute-urls)
> +                         #$@extra-options
> +
> +                         ;; Add CHROOT-DIRECTORIES and all their dependencies
> +                         ;; (if these are store items) to the chroot.
> +                         (append-map
> +                          (lambda (file)
> +                            (append-map (lambda (directory)
> +                                          (list "--chroot-directory"
> +                                                directory))
> +                                        (call-with-input-file file
> +                                          read)))
> +                          '#$(map references-file

Hmm, that seems quite complex, and it’s not great that we have to tweak
guix-daemon-service “just” for this.

*scratches head*

Is there a way we can identify processes that have open overlay files,
so we could terminate them?

Alternately, something that might simplify the code would be to always
run guix-daemon in a separate mount namespace.  We could add a
‘fork+exec-command/container’ procedure in (gnu build shepherd) to help
with that.

That way, all we’d need to do is to run ‘guix system init’ in that same
mount namespace, which can be achieved using ‘container-excursion’.

Too bad we can’t use setns for a process other than the calling process.
:-/

Thoughts?

Ludo’.
Mathieu Othacehe Aug. 31, 2020, 6:44 a.m. UTC | #2
> Should ‘mount-cow-store’ also make an overlay for /var/guix/db?  That
> way, changes to that directory would go to /mnt/var/guix/db and the
> original database would remain unchanged.

I took the lazy path because it's just one file that keeps reasonably
small. Adding an extra overlay for /var/guix/db would make
sense here.

>
> Hmm, that seems quite complex, and it’s not great that we have to tweak
> guix-daemon-service “just” for this.

Yes I can't say I'm satisfied with all of this but I'm trying different
angles for this problem since months, with no proper outcome.

> Is there a way we can identify processes that have open overlay files,
> so we could terminate them?

That's the current approach but it breaks very ofter because kmscon,
udev or any other processes that can't be killed, opens an overlay file.
I'd really like to avoid relying on this kind of solution.

> Alternately, something that might simplify the code would be to always
> run guix-daemon in a separate mount namespace.  We could add a
> ‘fork+exec-command/container’ procedure in (gnu build shepherd) to help
> with that.
>
> That way, all we’d need to do is to run ‘guix system init’ in that same
> mount namespace, which can be achieved using ‘container-excursion’.

Yes I tried that at first but there's a catch. While running guix-daemon
in it's own mount namespace, it won't 'see' the mounted file-systems
such as /mnt.

So that would mean that we would have to do spawn a containerized
process that would:

* Join guix-daemon mnt namespace
* Call "with-mounted-partitions"
* Mount the cow-store
* Run 'guix system init'

In this is end it still seem overly complex, but I can give it another
try. WDYT?

Thanks a lot for reviewing this!

Mathieu
Ludovic Courtès Sept. 1, 2020, 8:48 a.m. UTC | #3
Hi Mathieu!

Mathieu Othacehe <othacehe@gnu.org> skribis:

>> Should ‘mount-cow-store’ also make an overlay for /var/guix/db?  That
>> way, changes to that directory would go to /mnt/var/guix/db and the
>> original database would remain unchanged.
>
> I took the lazy path because it's just one file that keeps reasonably
> small. Adding an extra overlay for /var/guix/db would make
> sense here.

Yeah, no big deal.

>> Hmm, that seems quite complex, and it’s not great that we have to tweak
>> guix-daemon-service “just” for this.
>
> Yes I can't say I'm satisfied with all of this but I'm trying different
> angles for this problem since months, with no proper outcome.

Yeah…  (I must say I really appreciate your commitment tackling hard
problems like this one, kudos!)

>> Is there a way we can identify processes that have open overlay files,
>> so we could terminate them?
>
> That's the current approach but it breaks very ofter because kmscon,
> udev or any other processes that can't be killed, opens an overlay file.
> I'd really like to avoid relying on this kind of solution.

OK, makes sense!

>> Alternately, something that might simplify the code would be to always
>> run guix-daemon in a separate mount namespace.  We could add a
>> ‘fork+exec-command/container’ procedure in (gnu build shepherd) to help
>> with that.
>>
>> That way, all we’d need to do is to run ‘guix system init’ in that same
>> mount namespace, which can be achieved using ‘container-excursion’.
>
> Yes I tried that at first but there's a catch. While running guix-daemon
> in it's own mount namespace, it won't 'see' the mounted file-systems
> such as /mnt.
>
> So that would mean that we would have to do spawn a containerized
> process that would:
>
> * Join guix-daemon mnt namespace
> * Call "with-mounted-partitions"
> * Mount the cow-store
> * Run 'guix system init'
>
> In this is end it still seem overly complex, but I can give it another
> try. WDYT?

It does seem complex indeed.

So perhaps we can settle on the solution you sent, but let’s see if we
can move complexity out of sight.  For example, if we can arrange to
have a ‘fork+exec-command/container’ procedure that can be passed the
PID of a namespace, such that the ‘start’ method of guix-daemon is just
a few more lines its current definition, I’ll be happy.

How does that sound?

Thank you!

Ludo’.
Mathieu Othacehe Sept. 2, 2020, 3:15 p.m. UTC | #4
Hey Ludo,

> So perhaps we can settle on the solution you sent, but let’s see if we
> can move complexity out of sight.  For example, if we can arrange to
> have a ‘fork+exec-command/container’ procedure that can be passed the
> PID of a namespace, such that the ‘start’ method of guix-daemon is just
> a few more lines its current definition, I’ll be happy.
>
> How does that sound?

Sounds fine! I added a "fork+exec-command/container" in (gnu build
shepherd) module, that uses "container-excursion" to enter the
namespaces of the process passed as argument.

I also took your other remarks into account and pushed this
serie. Thanks a lot for diving into this harsh stuff :).

Locally, the installer tests behave fine, but I'll monitor the CI to see
how it goes.

Now, the only installation test failure I'm aware of is
https://issues.guix.gnu.org/41948. The good news, is that I have a Guile
patch that seem to solve it[1].

Thanks,

Mathieu

[1]: https://lists.gnu.org/archive/html/bug-guile/2020-08/msg00023.html
Ludovic Courtès Sept. 2, 2020, 8:17 p.m. UTC | #5
Hi!

Mathieu Othacehe <othacehe@gnu.org> skribis:

>> So perhaps we can settle on the solution you sent, but let’s see if we
>> can move complexity out of sight.  For example, if we can arrange to
>> have a ‘fork+exec-command/container’ procedure that can be passed the
>> PID of a namespace, such that the ‘start’ method of guix-daemon is just
>> a few more lines its current definition, I’ll be happy.
>>
>> How does that sound?
>
> Sounds fine! I added a "fork+exec-command/container" in (gnu build
> shepherd) module, that uses "container-excursion" to enter the
> namespaces of the process passed as argument.
>
> I also took your other remarks into account and pushed this
> serie. Thanks a lot for diving into this harsh stuff :).

Awesome!

> Locally, the installer tests behave fine, but I'll monitor the CI to see
> how it goes.
>
> Now, the only installation test failure I'm aware of is
> https://issues.guix.gnu.org/41948. The good news, is that I have a Guile
> patch that seem to solve it[1].

Yes, it’s still on my radar (started looking into it the other day,
without any tangible result).

Ludo’.
Ludovic Courtès Sept. 2, 2020, 9:25 p.m. UTC | #6
Hey,

Mathieu Othacehe <othacehe@gnu.org> skribis:

> Sounds fine! I added a "fork+exec-command/container" in (gnu build
> shepherd) module, that uses "container-excursion" to enter the
> namespaces of the process passed as argument.

There’s a catch that I just discovered as I reconfigured berlin:

--8<---------------cut here---------------start------------->8---
root@berlin ~/maintenance/hydra# herd restart guix-daemon
Service cuirass-web has been stopped. 
Service cuirass has been stopped.
Service guix-publish has been stopped.
Service guix-daemon has been stopped. 
herd: exception caught while executing 'start' on service 'guix-daemon':
Unbound variable: fork+exec-command/container
root@berlin ~/maintenance/hydra# herd restart guix-daemon
Service guix-daemon is not running.   
herd: exception caught while executing 'start' on service 'guix-daemon':
Unbound variable: fork+exec-command/container
--8<---------------cut here---------------end--------------->8---

The workaround I found, which works nicely, is to run:

  herd eval root "(reload-module (resolve-module '(gnu build shepherd)))"

and then:

  herd restart guix-daemon
  herd restart guix-publish
  …

Not sure if there’s anything we can or should do.  It’s probably not too
common to restart guix-daemon after an upgrade, though.

Ludo’.
diff mbox series

Patch

diff --git a/gnu/installer/final.scm b/gnu/installer/final.scm
index 685aa81d89..a19018dc85 100644
--- a/gnu/installer/final.scm
+++ b/gnu/installer/final.scm
@@ -26,6 +26,8 @@ 
   #:use-module (guix build syscalls)
   #:use-module (guix build utils)
   #:use-module (gnu build accounts)
+  #:use-module (gnu build install)
+  #:use-module (gnu build linux-container)
   #:use-module ((gnu system shadow) #:prefix sys:)
   #:use-module (rnrs io ports)
   #:use-module (srfi srfi-1)
@@ -133,49 +135,18 @@  USERS."
                        (_ #f))))))
               pids)))
 
-(define (umount-cow-store)
-  "Remove the store overlay and the bind-mount on /tmp created by the
-cow-store service.  This procedure is very fragile and a better approach would
-be much appreciated."
-  (catch #t
-    (lambda ()
-      (let ((tmp-dir "/remove"))
-        (syslog "Unmounting cow-store.~%")
-
-        (mkdir-p tmp-dir)
-        (mount (%store-directory) tmp-dir "" MS_MOVE)
-
-        ;; The guix-daemon has possibly opened files from the cow-store,
-        ;; restart it.
-        (restart-service 'guix-daemon)
-
-        (syslog "Killing cow users.")
-
-        ;; Kill all processes started while the cow-store was active (logins
-        ;; on other TTYs for instance).
-        (kill-cow-users tmp-dir)
-
-        ;; Try to umount the store overlay. Some process such as udevd
-        ;; workers might still be active, so do some retries.
-        (let loop ((try 5))
-          (syslog "Umount try ~a~%" (- 5 try))
-          (sleep 1)
-          (let ((umounted? (false-if-exception (umount tmp-dir))))
-            (if (and (not umounted?) (> try 0))
-                (loop (- try 1))
-                (if umounted?
-                    (syslog "Umounted ~a successfully.~%" tmp-dir)
-                    (syslog "Failed to umount ~a.~%" tmp-dir)))))
-
-        (umount "/tmp")))
-    (lambda args
-      (syslog "~a~%" args))))
-
 (define* (install-system locale #:key (users '()))
   "Create /etc/shadow and /etc/passwd on the installation target for USERS.
 Start COW-STORE service on target directory and launch guix install command in
 a subshell.  LOCALE must be the locale name under which that command will run,
 or #f.  Return #t on success and #f on failure."
+  (define backing-directory
+    ;; Sub-directory used as the backing store for copy-on-write.
+    "/tmp/guix-inst")
+
+  (define (assert-exit x)
+    (primitive-exit (if x 0 1)))
+
   (let* ((options         (catch 'system-error
                             (lambda ()
                               ;; If this file exists, it can provide
@@ -188,7 +159,11 @@  or #f.  Return #t on success and #f on failure."
                                         "--fallback")
                                   options
                                   (list (%installer-configuration-file)
-                                        (%installer-target-dir)))))
+                                        (%installer-target-dir))))
+         (database-dir    "/var/guix/db")
+         (database-file   (string-append database-dir "/db.sqlite"))
+         (saved-database  (string-append database-dir "/db.save"))
+         (ret             #f))
     (mkdir-p (%installer-target-dir))
 
     ;; We want to initialize user passwords but we don't want to store them in
@@ -198,27 +173,51 @@  or #f.  Return #t on success and #f on failure."
     ;; passwords that we've put in there.
     (create-user-database users (%installer-target-dir))
 
-    (dynamic-wind
-      (lambda ()
-        (start-service 'cow-store (list (%installer-target-dir))))
-      (lambda ()
-        ;; If there are any connected clients, assume that we are running
-        ;; installation tests. In that case, dump the standard and error
-        ;; outputs to syslog.
-        (if (not (null? (current-clients)))
-            (with-output-to-file "/dev/console"
-              (lambda ()
-                (with-error-to-file "/dev/console"
-                  (lambda ()
-                    (setvbuf (current-output-port) 'none)
-                    (setvbuf (current-error-port) 'none)
-                    (run-command install-command #:locale locale)))))
-            (run-command install-command #:locale locale)))
-      (lambda ()
-        (stop-service 'cow-store)
-        ;; Remove the store overlay created at cow-store service start.
-        ;; Failing to do that will result in further umount calls to fail
-        ;; because the target device is seen as busy. See:
-        ;; https://lists.gnu.org/archive/html/guix-devel/2018-12/msg00161.html.
-        (umount-cow-store)
-        #f))))
+    ;; When the store overlay is mounted, other processes such as kmscon, udev
+    ;; and guix-daemon may open files from the store, preventing the
+    ;; underlying install support from being umounted. See:
+    ;; https://lists.gnu.org/archive/html/guix-devel/2018-12/msg00161.html.
+    ;;
+    ;; To avoid this situation, mount the store overlay inside a container,
+    ;; and run the installation from within that container.
+    (zero?
+     (call-with-container '()
+       (lambda ()
+         (dynamic-wind
+           (lambda ()
+             ;; Save the database, so that it can be restored once the
+             ;; cow-store is umounted.
+             (copy-file database-file saved-database)
+             (mount-cow-store (%installer-target-dir) backing-directory))
+           (lambda ()
+             ;; We need to drag the guix-daemon to the container MNT
+             ;; namespace, so that it can operate on the cow-store.
+             (stop-service 'guix-daemon)
+             (start-service 'guix-daemon (list (number->string (getpid))))
+
+             (setvbuf (current-output-port) 'none)
+             (setvbuf (current-error-port) 'none)
+
+             ;; If there are any connected clients, assume that we are running
+             ;; installation tests. In that case, dump the standard and error
+             ;; outputs to syslog.
+             (set! ret
+                   (if (not (null? (current-clients)))
+                       (with-output-to-file "/dev/console"
+                         (lambda ()
+                           (with-error-to-file "/dev/console"
+                             (lambda ()
+                               (run-command install-command
+                                            #:locale locale)))))
+                       (run-command install-command #:locale locale))))
+           (lambda ()
+             ;; Restart guix-daemon so that it does no keep the MNT namespace
+             ;; alive.
+             (restart-service 'guix-daemon)
+             (copy-file saved-database database-file)
+
+             ;; Finally umount the cow-store and exit the container.
+             (umount-cow-store (%installer-target-dir) backing-directory)
+             (assert-exit ret))))
+       #:namespaces '(mnt)
+       #:jail? #f))))
diff --git a/gnu/installer/newt/final.scm b/gnu/installer/newt/final.scm
index fa8d6fea71..89684c4d8a 100644
--- a/gnu/installer/newt/final.scm
+++ b/gnu/installer/newt/final.scm
@@ -102,13 +102,6 @@  a specific step, or restart the installer."))
                             #:key (users '()))
   (clear-screen)
   (newt-suspend)
-  ;; XXX: Force loading 'bold' font files before mouting the
-  ;; cow-store. Otherwise, if the file is loaded by kmscon after the cow-store
-  ;; in mounted, it will be necessary to kill kmscon to umount to cow-store.
-  (display
-   (colorize-string
-    (format #f (G_ "Installing Guix System ...~%"))
-    (color BOLD)))
   (let ((install-ok? (install-system locale #:users users)))
     (newt-resume)
     install-ok?))
diff --git a/gnu/services/base.scm b/gnu/services/base.scm
index 491f35702a..f62fd861ca 100644
--- a/gnu/services/base.scm
+++ b/gnu/services/base.scm
@@ -1558,36 +1558,50 @@  proxy of 'guix-daemon'...~%")
            (provision '(guix-daemon))
            (requirement '(user-processes))
            (actions (list shepherd-set-http-proxy-action))
-           (modules '((srfi srfi-1)))
+           (modules '((srfi srfi-1)
+                      (ice-9 match)))
            (start
-            #~(lambda _
+            #~(lambda args
                 (define proxy
                   ;; HTTP/HTTPS proxy.  The 'http_proxy' variable is set by
                   ;; the 'set-http-proxy' action.
                   (or (getenv "http_proxy") #$http-proxy))
 
                 (fork+exec-command
-                 (cons* #$(file-append guix "/bin/guix-daemon")
-                        "--build-users-group" #$build-group
-                        "--max-silent-time" #$(number->string max-silent-time)
-                        "--timeout" #$(number->string timeout)
-                        "--log-compression" #$(symbol->string log-compression)
-                        #$@(if use-substitutes?
-                               '()
-                               '("--no-substitutes"))
-                        "--substitute-urls" #$(string-join substitute-urls)
-                        #$@extra-options
-
-                        ;; Add CHROOT-DIRECTORIES and all their dependencies
-                        ;; (if these are store items) to the chroot.
-                        (append-map (lambda (file)
-                                      (append-map (lambda (directory)
-                                                    (list "--chroot-directory"
-                                                          directory))
-                                                  (call-with-input-file file
-                                                    read)))
-                                    '#$(map references-file
-                                            chroot-directories)))
+                 ;; When running the installer, we need guix-daemon to operate
+                 ;; from within the same MNT namespace as the installation
+                 ;; container. In that case only, enter the namespace of the
+                 ;; process PID passed as start argument.
+                 (append
+                  (match args
+                    ((pid)
+                     (list #$(file-append util-linux "/bin/nsenter")
+                           "-t" pid "-m"))
+                    (else '()))
+                  (cons* #$(file-append guix "/bin/guix-daemon")
+                         "--build-users-group" #$build-group
+                         "--max-silent-time"
+                         #$(number->string max-silent-time)
+                         "--timeout" #$(number->string timeout)
+                         "--log-compression"
+                         #$(symbol->string log-compression)
+                         #$@(if use-substitutes?
+                                '()
+                                '("--no-substitutes"))
+                         "--substitute-urls" #$(string-join substitute-urls)
+                         #$@extra-options
+
+                         ;; Add CHROOT-DIRECTORIES and all their dependencies
+                         ;; (if these are store items) to the chroot.
+                         (append-map
+                          (lambda (file)
+                            (append-map (lambda (directory)
+                                          (list "--chroot-directory"
+                                                directory))
+                                        (call-with-input-file file
+                                          read)))
+                          '#$(map references-file
+                                  chroot-directories))))
 
                  #:environment-variables
                  (append (list #$@(if tmpdir