#include "config.h" #include "references.hh" #include "pathlocks.hh" #include "misc.hh" #include "globals.hh" #include "local-store.hh" #include "util.hh" #include "archive.hh" #include "affinity.hh" #include "builtins.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_BZLIB_H # include #endif /* Includes required for chroot support. */ #if HAVE_SYS_PARAM_H #include #endif #if HAVE_SYS_MOUNT_H #include #endif #if HAVE_SYS_SYSCALL_H #include #endif #if HAVE_SCHED_H #include #endif #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE) #define CLONE_ENABLED defined(CLONE_NEWNS) #if defined(SYS_pivot
aboutsummaryrefslogtreecommitdiff
blob: 306b2c6b6312409bfe7ecaef299d4122f33d77f0 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2014, 2016, 2017, 2018 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 (build-self)
  #:use-module (gnu)
  #:use-module (guix)
  #:use-module (guix ui)
  #:use-module (guix config)
  #:use-module (guix modules)
  #:use-module (guix build-system gnu)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-19)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (rnrs io ports)
  #:use-module (ice-9 match)
  #:use-module (ice-9 popen)
  #:export (build))

;;; Commentary:
;;;
;;; When loaded, this module returns a monadic procedure of at least one
;;; argument: the source tree to build.  It returns a derivation that
;;; builds it.
;;;
;;; This file uses modules provided by the already-installed Guix.  Those
;;; modules may be arbitrarily old compared to the version we want to
;;; build.  Because of that, it must rely on the smallest set of features
;;; that are likely to be provided by the (guix) and (gnu) modules, and by
;;; Guile itself, forever and ever.
;;;
;;; Code:


;;;
;;; Generating (guix config).
;;;
;;; This is copied from (guix self) because we cannot assume (guix self) is
;;; available at this point.
;;;

(define %dependency-variables
  ;; (guix config) variables corresponding to dependencies.
  '(%libgcrypt %libz %xz %gzip %bzip2))

(define %persona-variables
  ;; (guix config) variables that define Guix's persona.
  '(%guix-package-name
    %guix-version
    %guix-bug-report-address
    %guix-home-page-url))

(define %config-variables
  ;; (guix config) variables corresponding to Guix configuration.
  (letrec-syntax ((variables (syntax-rules ()
                               ((_)
                                '())
                               ((_ variable rest ...)
                                (cons `(variable . ,variable)
                                      (variables rest ...))))))
    (variables %localstatedir %storedir %sysconfdir %system)))

(define* (make-config.scm #:key zlib gzip xz bzip2
                          (package-name "GNU Guix")
                          (package-version "0")
                          (bug-report-address "bug-guix@gnu.org")
                          (home-page-url "https://gnu.org/s/guix"))

  ;; Hack so that Geiser is not confused.
  (define defmod 'define-module)

  (scheme-file "config.scm"
               #~(begin
                   (#$defmod (guix config)
                     #:export (%guix-package-name
                               %guix-version
                               %guix-bug-report-address
                               %guix-home-page-url
                               %store-directory
                               %state-directory
                               %store-database-directory
                               %config-directory
                               %libz
                               %gzip
                               %bzip2
                               %xz))

                   ;; XXX: Work around <http://bugs.gnu.org/15602>.
                   (eval-when (expand load eval)
                     #$@(map (match-lambda
                               ((name . value)
                                #~(define-public #$name #$value)))
                             %config-variables)

                     (define %store-directory
                       (or (and=> (getenv "NIX_STORE_DIR") canonicalize-path)
                           %storedir))

                     (define %state-directory
                       ;; This must match `NIX_STATE_DIR' as defined in
                       ;; `nix/local.mk'.
                       (or (getenv "NIX_STATE_DIR")
                           (string-append %localstatedir "/guix")))

                     (define %store-database-directory
                       (or (getenv "NIX_DB_DIR")
                           (string-append %state-directory "/db")))

                     (define %config-directory
                       ;; This must match `GUIX_CONFIGURATION_DIRECTORY' as
                       ;; defined in `nix/local.mk'.
                       (or (getenv "GUIX_CONFIGURATION_DIRECTORY")
                           (string-append %sysconfdir "/guix")))

                     (define %guix-package-name #$package-name)
                     (define %guix-version #$package-version)
                     (define %guix-bug-report-address #$bug-report-address)
                     (define %guix-home-page-url #$home-page-url)

                     (define %gzip
                       #+(and gzip (file-append gzip "/bin/gzip")))
                     (define %bzip2
                       #+(and bzip2 (file-append bzip2 "/bin/bzip2")))
                     (define %xz
                       #+(and xz (file-append xz "/bin/xz")))

                     (define %libz
                       #+(and zlib
                              (file-append zlib "/lib/libz")))))))


;;;
;;; 'gexp->script'.
;;;
;;; This is our own variant of 'gexp->script' with an extra #:module-path
;;; parameter, which was unavailable in (guix gexp) until commit
;;; 1ae16033f34cebe802023922436883867010850f (March 2018.)
;;;

(define (load-path-expression modules path)
  "Return as a monadic value a gexp that sets '%load-path' and
'%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
                                                  #:module-path path)))
    (return (gexp (eval-when (expand load eval)
                    (set! %load-path
                      (cons (ungexp modules) %load-path))
                    (set! %load-compiled-path
                      (cons (ungexp compiled)
                            %load-compiled-path)))))))

(define* (gexp->script name exp
                       #: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."
  (mlet %store-monad ((set-load-path
                       (load-path-expression (gexp-modules exp)
                                             module-path)))
    (gexp->derivation name
                      (gexp
                       (call-with-output-file (ungexp output)
                         (lambda (port)
                           ;; Note: that makes a long shebang.  When the store
                           ;; is /gnu/store, that fits within the 128-byte
                           ;; limit imposed by Linux, but that may go beyond
                           ;; when running tests.
                           (format port
                                   "#!~a/bin/guile --no-auto-compile~%!#~%"
                                   (ungexp guile))

                           (write '(ungexp set-load-path) port)
                           (write '(ungexp exp) port)
                           (chmod port #o555))))
                      #:module-path module-path)))


(define (date-version-string)
  "Return the current date and hour in UTC timezone, for use as a poor
person's version identifier."
  ;; XXX: Replace with a Git commit id.
  (date->string (current-date 0) "~Y~m~d.~H"))

(define guile-gcrypt
  ;; The host Guix may or may not have 'guile-gcrypt', which was introduced in
  ;; August 2018.  If it has it, it's at least version 0.1.0, which is good
  ;; enough.  If it doesn't, specify our own package because the target Guix
  ;; requires it.
  (match (find-best-packages-by-name "guile-gcrypt" #f)
    (()
     (package
       (name "guile-gcrypt")
       (version "0.1.0")
       (home-page "https://notabug.org/cwebber/guile-gcrypt")
       (source (origin
                 (method url-fetch)
                 (uri (string-append home-page "/archive/v" version ".tar.gz"))
                 (sha256
                  (base32
                   "1gir7ifknbmbvjlql5j6wzk7bkb5lnmq80q59ngz43hhpclrk5k3"))
                 (file-name (string-append name "-" version ".tar.gz"))))
       (build-system gnu-build-system)
       (arguments
        ;; The 'bootstrap' phase appeared in 'core-updates', which was merged
        ;; into 'master' ca. June 2018.
        '(#:phases (modify-phases %standard-phases
                     (delete 'bootstrap)
                     (add-before 'configure 'bootstrap
                       (lambda _
                         (unless (zero? (system* "autoreconf" "-vfi"))
                           (error "autoreconf failed"))
                         #t)))))
       (native-inputs
        `(("pkg-config" ,(specification->package "pkg-config"))
          ("autoconf" ,(specification->package "autoconf"))
          ("automake" ,(specification->package "automake"))
          ("texinfo" ,(specification->package "texinfo"))))
       (inputs
        `(("guile" ,(specification->package "guile"))
          ("libgcrypt" ,(specification->package "libgcrypt"))))
       (synopsis "Cryptography library for Guile using Libgcrypt")
       (description
        "Guile-Gcrypt provides a Guile 2.x interface to a subset of the
GNU Libgcrypt crytographic library.  It provides modules for cryptographic
hash functions, message authentication codes (MAC), public-key cryptography,
strong randomness, and more.  It is implemented using the foreign function
interface (FFI) of Guile.")
       (license #f)))                             ;license:gpl3+
    ((package . _)
     package)))

(define* (build-program source version
                        #:optional (guile-version (effective-version))
                        #:key (pull-version 0))
  "Return a program that computes the derivation to build Guix from SOURCE."
  (define select?
    ;; Select every module but (guix config) and non-Guix modules.
    (match-lambda
      (('guix 'config) #f)
      (('guix _ ...)   #t)
      (('gnu _ ...)    #t)
      (_               #f)))

  (define fake-gcrypt-hash
    ;; Fake (gcrypt hash) module; see below.
    (scheme-file "hash.scm"
                 #~(define-module (gcrypt hash)
                     #:export (sha1 sha256))))

  (define fake-git
    (scheme-file "git.scm" #~(define-module (git))))

  (with-imported-modules `(((guix config)
                            => ,(make-config.scm))

                           ;; To avoid relying on 'with-extensions', which was
                           ;; introduced in 0.15.0, provide a fake (gcrypt
                           ;; hash) just so that we can build modules, and
                           ;; adjust %LOAD-PATH later on.
                           ((gcrypt hash) => ,fake-gcrypt-hash)

                           ;; (guix git-download) depends on (git) but only
                           ;; for peripheral functionality.  Provide a dummy
                           ;; (git) to placate it.
                           ((git) => ,fake-git)

                           ,@(source-module-closure `((guix store)
                                                      (guix self)
                                                      (guix derivations)
                                                      (gnu packages bootstrap))
                                                    (list source)
                                                    #:select? select?))
    (gexp->script "compute-guix-derivation"
                  #~(begin
                      (use-modules (ice-9 match))

                      (eval-when (expand load eval)
                        ;; Don't augment '%load-path'.
                        (unsetenv "GUIX_PACKAGE_PATH")

                        ;; (gnu packages …) modules are going to be looked up
                        ;; under SOURCE.  (guix config) is looked up in FRONT.
                        (match (command-line)
                          ((_ source _ ...)
                           (match %load-path
                             ((front _ ...)
                              (unless (string=? front source) ;already done?
                                (set! %load-path
                                  (list source
                                        (string-append #$guile-gcrypt
                                                       "/share/guile/site/"
                                                       (effective-version))
                                        front)))))))

                        ;; Only load Guile-Gcrypt, our own modules, or those
                        ;; of Guile.
                        (match %load-compiled-path
                          ((front _ ... sys1 sys2)
                           (unless (string-prefix? #$guile-gcrypt front)
                             (set! %load-compiled-path
                               (list (string-append #$guile-gcrypt
                                                    "/lib/guile/"
                                                    (effective-version)
                                                    "/site-ccache")
                                     front sys1 sys2))))))

                      (use-modules (guix store)
                                   (guix self)
                                   (guix derivations)
                                   (srfi srfi-1))

                      (define (spin system)
                        (define spin
                          (circular-list "-" "\\" "|" "/" "-" "\\" "|" "/"))

                        (format (current-error-port)
                                "Computing Guix derivation for '~a'...  "
                                system)
                        (let loop ((spin spin))
                          (display (string-append "\b" (car spin))
                                   (current-error-port))
                          (force-output (current-error-port))
                          (sleep 1)
                          (loop (cdr spin))))

                      (match (command-line)
                        ((_ source system version protocol-version)
                         ;; The current input port normally wraps a file
                         ;; descriptor connected to the daemon, or it is
                         ;; connected to /dev/null.  In the former case, reuse
                         ;; the connection such that we inherit build options
                         ;; such as substitute URLs and so on; in the latter
                         ;; case, attempt to open a new connection.
                         (let* ((proto (string->number protocol-version))
                                (store (if (integer? proto)
                                           (port->connection (duplicate-port
                                                              (current-input-port)
                                                              "w+0")
                                                             #:version proto)
                                           (open-connection))))
                           (call-with-new-thread
                            (lambda ()
                              (spin system)))

                           (display
                            (and=>
                             (run-with-store store
                               (guix-derivation source version
                                                #$guile-version
                                                #:pull-version
                                                #$pull-version)
                               #:system system)
                             derivation-file-name))))))
                  #:module-path (list source))))

;; The procedure below is our return value.
(define* (build source
                #:key verbose? (version (date-version-string)) system
                (guile-version (match ((@ (guile) version))
                                 ("2.2.2" "2.2.2")
                                 (_       (effective-version))))
                (pull-version 0)
                #:allow-other-keys
                #:rest rest)
  "Return a derivation that unpacks SOURCE into STORE and compiles Scheme
files."
  ;; Build the build program and then use it as a trampoline to build from
  ;; SOURCE.
  (mlet %store-monad ((build  (build-program source version guile-version
                                             #:pull-version pull-version))
                      (system (if system (return system) (current-system)))
                      (port   ((store-lift nix-server-socket)))
                      (major  ((store-lift nix-server-major-version)))
                      (minor  ((store-lift nix-server-minor-version))))
    (mbegin %store-monad
      (show-what-to-build* (list build))
      (built-derivations (list build))

      ;; Use the port beneath the current store as the stdin of BUILD.  This
      ;; way, we know 'open-pipe*' will not close it on 'exec'.  If PORT is
      ;; not a file port (e.g., it's an SSH channel), then the subprocess's
      ;; stdin will actually be /dev/null.
      (let* ((pipe   (with-input-from-port port
                       (lambda ()
                         (setenv "GUILE_WARN_DEPRECATED" "no") ;be quiet and drive
                         (open-pipe* OPEN_READ
                                     (derivation->output-path build)
                                     source system version
                                     (if (file-port? port)
                                         (number->string
                                          (logior major minor))
                                         "none")))))
             (str    (get-string-all pipe))
             (status (close-pipe pipe)))
        (match str
          ((? eof-object?)
           (error "build program failed" (list build status)))
          ((? derivation-path? drv)
           (mbegin %store-monad
             (return (newline (current-output-port)))
             ((store-lift add-temp-root) drv)
             (return (read-derivation-from-file drv))))
          ("#f"
           ;; Unsupported PULL-VERSION.
           (return #f))
          ((? string? str)
           (raise (condition
                   (&message
                    (message (format #f "You found a bug: the program '~a'
failed to compute the derivation for Guix (version: ~s; system: ~s;
host version: ~s; pull-version: ~s).
Please report it by email to <~a>.~%"
                                     (derivation->output-path build)
                                     version system %guix-version pull-version
                                     %guix-bug-report-address)))))))))))

;; This file is loaded by 'guix pull'; return it the build procedure.
build

;; Local Variables:
;; eval: (put 'with-load-path 'scheme-indent-function 1)
;; End:

;;; build-self.scm ends here
rename(storePath.c_str(), oldPath.c_str()); if (rename(tmpPath.c_str(), storePath.c_str()) == -1) throw SysError(format("moving `%1%' to `%2%'") % tmpPath % storePath); if (pathExists(oldPath)) deletePath(oldPath); } MakeError(NotDeterministic, BuildError) /* Recursively make the file permissions of a path safe for exposure to arbitrary users, but without canonicalising its permissions, timestamp, and user. Throw an exception if a file type that isn't explicitly known to be safe is found. */ static void secureFilePerms(Path path) { struct stat st; if (lstat(path.c_str(), &st)) return; switch(st.st_mode & S_IFMT) { case S_IFLNK: return; case S_IFDIR: for (auto & i : readDirectory(path)) { secureFilePerms(path + "/" + i.name); } /* FALLTHROUGH */ case S_IFREG: chmod(path.c_str(), (st.st_mode & ~S_IFMT) & ~(S_ISUID | S_ISGID | S_IWOTH)); break; default: throw Error(format("file `%1%' has an unsupported type") % path); } } void DerivationGoal::buildDone() { trace("build done"); /* Since we got an EOF on the logger pipe, the builder is presumed to have terminated. In fact, the builder could also have simply have closed its end of the pipe --- just don't do that :-) */ int status; pid_t savedPid; if (hook) { savedPid = hook->pid; status = hook->pid.wait(true); } else { /* !!! this could block! security problem! solution: kill the child */ savedPid = pid; status = pid.wait(true); } debug(format("builder process for `%1%' finished") % drvPath); /* So the child is gone now. */ worker.childTerminated(savedPid); /* Close the read side of the logger pipe. */ if (hook) { hook->builderOut.readSide.close(); hook->fromAgent.readSide.close(); } else builderOut.readSide.close(); /* Close the log file. */ closeLogFile(); /* When running under a build user, make sure that all processes running under that uid are gone. This is to prevent a malicious user from leaving behind a process that keeps files open and modifies them after they have been chown'ed to root. */ if (buildUser.enabled()) buildUser.kill(); bool diskFull = false; try { /* Check the exit status. */ if (!statusOk(status)) { /* Heuristically check whether the build failure may have been caused by a disk full condition. We have no way of knowing whether the build actually got an ENOSPC. So instead, check if the disk is (nearly) full now. If so, we don't mark this build as a permanent failure. */ #if HAVE_STATVFS unsigned long long required = 8ULL * 1024 * 1024; // FIXME: make configurable struct statvfs st; if (statvfs(settings.nixStore.c_str(), &st) == 0 && (unsigned long long) st.f_bavail * st.f_bsize < required) diskFull = true; if (statvfs(tmpDir.c_str(), &st) == 0 && (unsigned long long) st.f_bavail * st.f_bsize < required) diskFull = true; #endif deleteTmpDir(false); /* Move paths out of the chroot for easier debugging of build failures. */ if (useChroot && buildMode == bmNormal) foreach (PathSet::iterator, i, missingPaths) if (pathExists(chrootRootDir + *i)) { try { secureFilePerms(chrootRootDir + *i); rename((chrootRootDir + *i).c_str(), i->c_str()); } catch(Error & e) { printMsg(lvlError, e.msg()); } } if (diskFull) printMsg(lvlError, "note: build failure may have been caused by lack of free disk space"); throw BuildError(format("builder for `%1%' %2%") % drvPath % statusToString(status)); } if (fixedOutput) { /* Replace the output, if it exists, by a fresh copy of itself to make sure that there's no stale file descriptor pointing to it (CVE-2024-27297). */ foreach (DerivationOutputs::iterator, i, drv.outputs) { Path output = chrootRootDir + i->second.path; if (pathExists(output)) { Path pivot = output + ".tmp"; copyFileRecursively(output, pivot, true); int err = rename(pivot.c_str(), output.c_str()); if (err != 0) throw SysError(format("renaming `%1%' to `%2%'") % pivot % output); } } } /* Compute the FS closure of the outputs and register them as being valid. */ registerOutputs(); /* Delete unused redirected outputs (when doing hash rewriting). */ foreach (RedirectedOutputs::iterator, i, redirectedOutputs) if (pathExists(i->second)) deletePath(i->second); /* Delete the chroot (if we were using one). */ autoDelChroot.reset(); /* this runs the destructor */ deleteTmpDir(true); /* Repeat the build if necessary. */ if (curRound++ < nrRounds) { outputLocks.unlock(); buildUser.release(); state = &DerivationGoal::tryToBuild; worker.wakeUp(shared_from_this()); return; } /* It is now safe to delete the lock files, since all future lockers will see that the output paths are valid; they will not create new lock files with the same names as the old (unlinked) lock files. */ outputLocks.setDeletion(true); outputLocks.unlock(); } catch (BuildError & e) { if (!hook) printMsg(lvlError, e.msg()); outputLocks.unlock(); buildUser.release(); BuildResult::Status st = BuildResult::MiscFailure; if (hook && WIFEXITED(status) && WEXITSTATUS(status) == 101) { if (settings.printBuildTrace) printMsg(lvlError, format("@ build-failed %1% - timeout") % drvPath); st = BuildResult::TimedOut; } else if (hook && (!WIFEXITED(status) || WEXITSTATUS(status) != 100)) { if (settings.printBuildTrace) printMsg(lvlError, format("@ hook-failed %1% - %2% %3%") % drvPath % status % e.msg()); } else { if (settings.printBuildTrace) printMsg(lvlError, format("@ build-failed %1% - %2% %3%") % drvPath % 1 % e.msg()); st = statusOk(status) ? BuildResult::OutputRejected : fixedOutput || diskFull ? BuildResult::TransientFailure : BuildResult::PermanentFailure; /* Register the outputs of this build as "failed" so we won't try to build them again (negative caching). However, don't do this for fixed-output derivations, since they're likely to fail for transient reasons (e.g., fetchurl not being able to access the network). Hook errors (like communication problems with the remote machine) shouldn't be cached either. */ if (settings.cacheFailure && !fixedOutput && !diskFull) foreach (DerivationOutputs::iterator, i, drv.outputs) worker.store.registerFailedPath(i->second.path); } done(st, e.msg()); return; } /* Release the build user, if applicable. */ buildUser.release(); if (settings.printBuildTrace) printMsg(lvlError, format("@ build-succeeded %1% -") % drvPath); done(BuildResult::Built); } HookReply DerivationGoal::tryBuildHook() { if (!settings.useBuildHook) return rpDecline; if (!worker.hook) { Strings args = { "offload", settings.thisSystem.c_str(), (format("%1%") % settings.maxSilentTime).str().c_str(), (format("%1%") % settings.printBuildTrace).str().c_str(), (format("%1%") % settings.buildTimeout).str().c_str() }; worker.hook = std::make_shared(settings.guixProgram, args); } /* Tell the hook about system features (beyond the system type) required from the build machine. (The hook could parse the drv file itself, but this is easier.) */ Strings features = tokenizeString(get(drv.env, "requiredSystemFeatures")); foreach (Strings::iterator, i, features) checkStoreName(*i); /* !!! abuse */ /* Send the request to the hook. */ writeLine(worker.hook->toAgent.writeSide, (format("%1% %2% %3% %4%") % (worker.getNrLocalBuilds() < settings.maxBuildJobs ? "1" : "0") % drv.platform % drvPath % concatStringsSep(",", features)).str()); /* Read the first line of input, which should be a word indicating whether the hook wishes to perform the build. */ string reply; while (true) { string s = readLine(worker.hook->fromAgent.readSide); if (string(s, 0, 2) == "# ") { reply = string(s, 2); break; } s += "\n"; writeToStderr(s); } debug(format("hook reply is `%1%'") % reply); if (reply == "decline" || reply == "postpone") return reply == "decline" ? rpDecline : rpPostpone; else if (reply != "accept") throw Error(format("bad hook reply `%1%'") % reply); printMsg(lvlTalkative, format("using hook to build path(s) %1%") % showPaths(missingPaths)); hook = worker.hook; worker.hook.reset(); /* Tell the hook all the inputs that have to be copied to the remote system. This unfortunately has to contain the entire derivation closure to ensure that the validity invariant holds on the remote system. (I.e., it's unfortunate that we have to list it since the remote system *probably* already has it.) */ PathSet allInputs; allInputs.insert(inputPaths.begin(), inputPaths.end()); computeFSClosure(worker.store, drvPath, allInputs); string s; foreach (PathSet::iterator, i, allInputs) { s += *i; s += ' '; } writeLine(hook->toAgent.writeSide, s); /* Tell the hooks the missing outputs that have to be copied back from the remote system. */ s = ""; foreach (PathSet::iterator, i, missingPaths) { s += *i; s += ' '; } writeLine(hook->toAgent.writeSide, s); hook->toAgent.writeSide.close(); /* Create the log file and pipe. */ Path logFile = openLogFile(); set fds; fds.insert(hook->fromAgent.readSide); fds.insert(hook->builderOut.readSide); worker.childStarted(shared_from_this(), hook->pid, fds, false, true); if (settings.printBuildTrace) printMsg(lvlError, format("@ build-started %1% - %2% %3% %4%") % drvPath % drv.platform % logFile % hook->pid); return rpAccept; } void chmod_(const Path & path, mode_t mode) { if (chmod(path.c_str(), mode) == -1) throw SysError(format("setting permissions on `%1%'") % path); } int childEntry(void * arg) { ((DerivationGoal *) arg)->runChild(); return 1; } void DerivationGoal::startBuilder() { auto f = format( buildMode == bmRepair ? "repairing path(s) %1%" : buildMode == bmCheck ? "checking path(s) %1%" : nrRounds > 1 ? "building path(s) %1% (round %2%/%3%)" : "building path(s) %1%"); f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit); startNest(nest, lvlInfo, f % showPaths(missingPaths) % curRound % nrRounds); /* Note: built-in builders are *not* running in a chroot environment so that we can easily implement them in Guile without having it as a derivation input (they are running under a separate build user, though). */ useChroot = settings.useChroot && !isBuiltin(drv); /* Construct the environment passed to the builder. */ env.clear(); /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when PATH is not set. We don't want this, so we fill it in with some dummy value. */ env["PATH"] = "/path-not-set"; /* Set HOME to a non-existing path to prevent certain programs from using /etc/passwd (or NIS, or whatever) to locate the home directory (for example, wget looks for ~/.wgetrc). I.e., these tools use /etc/passwd if HOME is not set, but they will just assume that the settings file they are looking for does not exist if HOME is set but points to some non-existing path. */ Path homeDir = "/homeless-shelter"; env["HOME"] = homeDir; /* Tell the builder where the store is. Usually they shouldn't care, but this is useful for purity checking (e.g., the compiler or linker might only want to accept paths to files in the store or in the build directory). */ env["NIX_STORE"] = settings.nixStore; /* The maximum number of cores to utilize for parallel building. */ env["NIX_BUILD_CORES"] = (format("%d") % settings.buildCores).str(); /* Add all bindings specified in the derivation. */ foreach (StringPairs::iterator, i, drv.env) env[i->first] = i->second; /* Create a temporary directory where the build will take place. */ auto drvName = storePathToName(drvPath); tmpDir = createTempDir("", "guix-build-" + drvName, false, false, 0700); if (useChroot) { /* Make the build directory seen by the build process a sub-directory. That way, "/tmp/guix-build-foo.drv-0" is root-owned, and thus its permissions cannot be changed by the build process, while "/tmp/guix-build-foo.drv-0/top" is owned by the build user. This cannot be done when !useChroot because then $NIX_BUILD_TOP would be inaccessible to the build user by its full file name. If the build user could make the build directory world-writable, then an attacker could create in it a hardlink to a root-owned file such as /etc/shadow. If 'keepFailed' is true, the daemon would then chown that hardlink to the user, giving them write access to that file. */ tmpDir += "/top"; if (mkdir(tmpDir.c_str(), 0700) == 1) throw SysError("creating top-level build directory"); } /* In a sandbox, for determinism, always use the same temporary directory. */ tmpDirInSandbox = useChroot ? canonPath("/tmp", true) + "/guix-build-" + drvName + "-0" : tmpDir; /* For convenience, set an environment pointing to the top build directory. */ env["NIX_BUILD_TOP"] = tmpDirInSandbox; /* Also set TMPDIR and variants to point to this directory. */ env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox; /* Explicitly set PWD to prevent problems with chroot builds. In particular, dietlibc cannot figure out the cwd because the inode of the current directory doesn't appear in .. (because getdents returns the inode of the mount point). */ env["PWD"] = tmpDirInSandbox; /* *Only* if this is a fixed-output derivation, propagate the values of the environment variables specified in the `impureEnvVars' attribute to the builder. This allows for instance environment variables for proxy configuration such as `http_proxy' to be easily passed to downloaders like `fetchurl'. Passing such environment variables from the caller to the builder is generally impure, but the output of fixed-output derivations is by definition pure (since we already know the cryptographic hash of the output). */ if (fixedOutput) { Strings varNames = tokenizeString(get(drv.env, "impureEnvVars")); foreach (Strings::iterator, i, varNames) env[*i] = getEnv(*i); } /* The `exportReferencesGraph' feature allows the references graph to be passed to a builder. This attribute should be a list of pairs [name1 path1 name2 path2 ...]. The references graph of each `pathN' will be stored in a text file `nameN' in the temporary build directory. The text files have the format used by `nix-store --register-validity'. However, the deriver fields are left empty. */ string s = get(drv.env, "exportReferencesGraph"); Strings ss = tokenizeString(s); if (ss.size() % 2 != 0) throw BuildError(format("odd number of tokens in `exportReferencesGraph': `%1%'") % s); for (Strings::iterator i = ss.begin(); i != ss.end(); ) { string fileName = *i++; checkStoreName(fileName); /* !!! abuse of this function */ /* Check that the store path is valid. */ Path storePath = *i++; if (!isInStore(storePath)) throw BuildError(format("`exportReferencesGraph' contains a non-store path `%1%'") % storePath); storePath = toStorePath(storePath); if (!worker.store.isValidPath(storePath)) throw BuildError(format("`exportReferencesGraph' contains an invalid path `%1%'") % storePath); /* If there are derivations in the graph, then include their outputs as well. This is useful if you want to do things like passing all build-time dependencies of some path to a derivation that builds a NixOS DVD image. */ PathSet paths, paths2; computeFSClosure(worker.store, storePath, paths); paths2 = paths; foreach (PathSet::iterator, j, paths2) { if (isDerivation(*j)) { Derivation drv = derivationFromPath(worker.store, *j); foreach (DerivationOutputs::iterator, k, drv.outputs) computeFSClosure(worker.store, k->second.path, paths); } } /* Write closure info to `fileName'. */ writeFile(tmpDir + "/" + fileName, worker.store.makeValidityRegistration(paths, false, false)); } /* If `build-users-group' is not empty, then we have to build as one of the members of that group. */ if (settings.buildUsersGroup != "") { buildUser.acquire(); assert(buildUser.getUID() != 0); assert(buildUser.getGID() != 0); /* Make sure that no other processes are executing under this uid. */ buildUser.kill(); /* Change ownership of the temporary build directory. */ if (chown(tmpDir.c_str(), buildUser.getUID(), buildUser.getGID()) == -1) throw SysError(format("cannot change ownership of '%1%'") % tmpDir); } if (useChroot) { #if CHROOT_ENABLED /* Create a temporary directory in which we set up the chroot environment using bind-mounts. We put it in the store to ensure that we can create hard-links to non-directory inputs in the fake store in the chroot (see below). */ chrootRootDir = drvPath + ".chroot"; if (pathExists(chrootRootDir)) deletePath(chrootRootDir); /* Clean up the chroot directory automatically. */ autoDelChroot = std::shared_ptr(new AutoDelete(chrootRootDir)); printMsg(lvlChatty, format("setting up chroot environment in `%1%'") % chrootRootDir); if (mkdir(chrootRootDir.c_str(), 0750) == -1) throw SysError(format("cannot create ‘%1%’") % chrootRootDir); if (chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1) throw SysError(format("cannot change ownership of ‘%1%’") % chrootRootDir); /* Create a writable /tmp in the chroot. Many builders need this. (Of course they should really respect $TMPDIR instead.) */ Path chrootTmpDir = chrootRootDir + "/tmp"; createDirs(chrootTmpDir); chmod_(chrootTmpDir, 01777); /* Create a /etc/passwd with entries for the build user and the nobody account. The latter is kind of a hack to support Samba-in-QEMU. */ createDirs(chrootRootDir + "/etc"); writeFile(chrootRootDir + "/etc/passwd", (format( "nixbld:x:%1%:%2%:Nix build user:/:/noshell\n" "nobody:x:65534:65534:Nobody:/:/noshell\n") % (buildUser.enabled() ? buildUser.getUID() : getuid()) % (buildUser.enabled() ? buildUser.getGID() : getgid())).str()); /* Declare the build user's group so that programs get a consistent view of the system (e.g., "id -gn"). */ writeFile(chrootRootDir + "/etc/group", (format("nixbld:!:%1%:\n") % (buildUser.enabled() ? buildUser.getGID() : getgid())).str()); /* Create /etc/hosts with localhost entry. */ if (!fixedOutput) writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n"); /* Bind-mount a user-configurable set of directories from the host file system. */ PathSet dirs = tokenizeString(settings.get("build-chroot-dirs", string(DEFAULT_CHROOT_DIRS))); PathSet dirs2 = tokenizeString(settings.get("build-extra-chroot-dirs", string(""))); dirs.insert(dirs2.begin(), dirs2.end()); for (auto & i : dirs) { size_t p = i.find('='); if (p == string::npos) dirsInChroot[i] = i; else dirsInChroot[string(i, 0, p)] = string(i, p + 1); } dirsInChroot[tmpDirInSandbox] = tmpDir; /* Make the closure of the inputs available in the chroot, rather than the whole store. This prevents any access to undeclared dependencies. Directories are bind-mounted, while other inputs are hard-linked (since only directories can be bind-mounted). !!! As an extra security precaution, make the fake store only writable by the build user. */ Path chrootStoreDir = chrootRootDir + settings.nixStore; createDirs(chrootStoreDir); chmod_(chrootStoreDir, 01775); if (chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1) throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir); foreach (PathSet::iterator, i, inputPaths) { struct stat st; if (lstat(i->c_str(), &st)) throw SysError(format("getting attributes of path `%1%'") % *i); if (S_ISDIR(st.st_mode)) dirsInChroot[*i] = *i; else { Path p = chrootRootDir + *i; if (link(i->c_str(), p.c_str()) == -1) { /* Hard-linking fails if we exceed the maximum link count on a file (e.g. 32000 of ext3), which is quite possible after a `nix-store --optimise'. */ if (errno != EMLINK) throw SysError(format("linking `%1%' to `%2%'") % p % *i); StringSink sink; dumpPath(*i, sink); StringSource source(sink.s); restorePath(p, source); } regularInputPaths.insert(*i); } } /* If we're repairing, checking or rebuilding part of a multiple-outputs derivation, it's possible that we're rebuilding a path that is in settings.dirsInChroot (typically the dependencies of /bin/sh). Throw them out. */ for (auto & i : drv.outputs) dirsInChroot.erase(i.second.path); #else throw Error("chroot builds are not supported on this platform"); #endif } else { if (pathExists(homeDir)) throw Error(format("directory `%1%' exists; please remove it") % homeDir); /* We're not doing a chroot build, but we have some valid output paths. Since we can't just overwrite or delete them, we have to do hash rewriting: i.e. in the environment/arguments passed to the build, we replace the hashes of the valid outputs with unique dummy strings; after the build, we discard the redirected outputs corresponding to the valid outputs, and rewrite the contents of the new outputs to replace the dummy strings with the actual hashes. */ if (validPaths.size() > 0) foreach (PathSet::iterator, i, validPaths) addHashRewrite(*i); /* If we're repairing, then we don't want to delete the corrupt outputs in advance. So rewrite them as well. */ if (buildMode == bmRepair) foreach (PathSet::iterator, i, missingPaths) if (worker.store.isValidPath(*i) && pathExists(*i)) { addHashRewrite(*i); redirectedBadOutputs.insert(*i); } } /* Run the builder. */ printMsg(lvlChatty, format("executing builder `%1%'") % drv.builder); /* Create the log file. */ Path logFile = openLogFile(); /* Create a pipe to get the output of the builder. */ builderOut.create(); /* Fork a child to build the package. Note that while we currently use forks to run and wait for the children, it shouldn't be hard to use threads for this on systems where fork() is unavailable or inefficient. If we're building in a chroot, then also set up private namespaces for the build: - The PID namespace causes the build to start as PID 1. Processes outside of the chroot are not visible to those on the inside, but processes inside the chroot are visible from the outside (though with different PIDs). - The private mount namespace ensures that all the bind mounts we do will only show up in this process and its children, and will disappear automatically when we're done. - The private network namespace ensures that the builder cannot talk to the outside world (or vice versa). It only has a private loopback interface. - The IPC namespace prevents the builder from communicating with outside processes using SysV IPC mechanisms (shared memory, message queues, semaphores). It also ensures that all IPC objects are destroyed when the builder exits. - The UTS namespace ensures that builders see a hostname of localhost rather than the actual hostname. */ #if __linux__ if (useChroot) { char stack[32 * 1024]; int flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD; if (!fixedOutput) flags |= CLONE_NEWNET; /* Ensure proper alignment on the stack. On aarch64, it has to be 16 bytes. */ pid = clone(childEntry, (char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~(uintptr_t)0xf), flags, this); if (pid == -1) throw SysError("cloning builder process"); } else #endif { pid = fork(); if (pid == 0) runChild(); } if (pid == -1) throw SysError("unable to fork"); /* parent */ pid.setSeparatePG(true); builderOut.writeSide.close(); worker.childStarted(shared_from_this(), pid, singleton >(builderOut.readSide), true, true); /* Check if setting up the build environment failed. */ string msg = readLine(builderOut.readSide); if (!msg.empty()) throw Error(msg); if (settings.printBuildTrace) { printMsg(lvlError, format("@ build-started %1% - %2% %3% %4%") % drvPath % drv.platform % logFile % pid); } } /* Return true if the operating system kernel part of SYSTEM1 and SYSTEM2 (the bit that comes after the hyphen in system types such as "i686-linux") is the same. */ static bool sameOperatingSystemKernel(const std::string& system1, const std::string& system2) { auto os1 = system1.substr(system1.find("-")); auto os2 = system2.substr(system2.find("-")); return os1 == os2; } void DerivationGoal::runChild() { /* Warning: in the child we should absolutely not make any SQLite calls! */ try { /* child */ _writeToStderr = 0; restoreAffinity(); commonChildInit(builderOut); #if CHROOT_ENABLED if (useChroot) { /* Initialise the loopback interface. */ AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); if (fd == -1) throw SysError("cannot open IP socket"); struct ifreq ifr; strcpy(ifr.ifr_name, "lo"); ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1) throw SysError("cannot set loopback interface flags"); fd.close(); /* Set the hostname etc. to fixed values. */ char hostname[] = "localhost"; if (sethostname(hostname, sizeof(hostname)) == -1) throw SysError("cannot set host name"); char domainname[] = "(none)"; // kernel default if (setdomainname(domainname, sizeof(domainname)) == -1) throw SysError("cannot set domain name"); /* Make all filesystems private. This is necessary because subtrees may have been mounted as "shared" (MS_SHARED). (Systemd does this, for instance.) Even though we have a private mount namespace, mounting filesystems on top of a shared subtree still propagates outside of the namespace. Making a subtree private is local to the namespace, though, so setting MS_PRIVATE does not affect the outside world. */ if (mount(0, "/", 0, MS_REC|MS_PRIVATE, 0) == -1) { throw SysError("unable to make ‘/’ private mount"); } /* Bind-mount chroot directory to itself, to treat it as a different filesystem from /, as needed for pivot_root. */ if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) throw SysError(format("unable to bind mount ‘%1%’") % chrootRootDir); /* Set up a nearly empty /dev, unless the user asked to bind-mount the host /dev. */ Strings ss; if (dirsInChroot.find("/dev") == dirsInChroot.end()) { createDirs(chrootRootDir + "/dev/shm"); createDirs(chrootRootDir + "/dev/pts"); ss.push_back("/dev/full"); #ifdef __linux__ if (pathExists("/dev/kvm")) ss.push_back("/dev/kvm"); #endif ss.push_back("/dev/null"); ss.push_back("/dev/random"); ss.push_back("/dev/tty"); ss.push_back("/dev/urandom"); ss.push_back("/dev/zero"); createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); } /* Fixed-output derivations typically need to access the network, so give them access to /etc/resolv.conf and so on. */ if (fixedOutput) { ss.push_back("/etc/resolv.conf"); ss.push_back("/etc/nsswitch.conf"); ss.push_back("/etc/services"); ss.push_back("/etc/hosts"); } for (auto & i : ss) dirsInChroot[i] = i; /* Bind-mount all the directories from the "host" filesystem that we want in the chroot environment. */ foreach (DirsInChroot::iterator, i, dirsInChroot) { struct stat st; Path source = i->second; Path target = chrootRootDir + i->first; if (source == "/proc") continue; // backwards compatibility if (stat(source.c_str(), &st) == -1) throw SysError(format("getting attributes of path `%1%'") % source); if (S_ISDIR(st.st_mode)) createDirs(target); else { createDirs(dirOf(target)); writeFile(target, ""); } if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1) throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target); } /* Bind a new instance of procfs on /proc to reflect our private PID namespace. */ createDirs(chrootRootDir + "/proc"); if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) throw SysError("mounting /proc"); /* Mount a new tmpfs on /dev/shm to ensure that whatever the builder puts in /dev/shm is cleaned up automatically. */ if (pathExists("/dev/shm") && mount("none", (chrootRootDir + "/dev/shm").c_str(), "tmpfs", 0, 0) == -1) throw SysError("mounting /dev/shm"); /* Mount a new devpts on /dev/pts. Note that this requires the kernel to be compiled with CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case if /dev/ptx/ptmx exists). */ if (pathExists("/dev/pts/ptmx") && !pathExists(chrootRootDir + "/dev/ptmx") && dirsInChroot.find("/dev/pts") == dirsInChroot.end()) { if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == -1) throw SysError("mounting /dev/pts"); createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); /* Make sure /dev/pts/ptmx is world-writable. With some Linux versions, it is created with permissions 0. */ chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); } /* Do the chroot(). */ if (chdir(chrootRootDir.c_str()) == -1) throw SysError(format("cannot change directory to '%1%'") % chrootRootDir); if (mkdir("real-root", 0) == -1) throw SysError("cannot create real-root directory"); if (pivot_root(".", "real-root") == -1) throw SysError(format("cannot pivot old root directory onto '%1%'") % (chrootRootDir + "/real-root")); if (chroot(".") == -1) throw SysError(format("cannot change root directory to '%1%'") % chrootRootDir); if (umount2("real-root", MNT_DETACH) == -1) throw SysError("cannot unmount real root filesystem"); if (rmdir("real-root") == -1) throw SysError("cannot remove real-root directory"); } #endif if (chdir(tmpDirInSandbox.c_str()) == -1) throw SysError(format("changing into `%1%'") % tmpDir); /* Close all other file descriptors. */ closeMostFDs(set()); #if __linux__ /* Change the personality to 32-bit if we're doing an i686-linux build on an x86_64-linux machine. */ struct utsname utsbuf; uname(&utsbuf); if (drv.platform == "i686-linux" && (settings.thisSystem == "x86_64-linux" || (!strcmp(utsbuf.sysname, "Linux") && !strcmp(utsbuf.machine, "x86_64")))) { if (personality(PER_LINUX32) == -1) throw SysError("cannot set i686-linux personality"); } if (drv.platform == "armhf-linux" && (settings.thisSystem == "aarch64-linux" || (!strcmp(utsbuf.sysname, "Linux") && !strcmp(utsbuf.machine, "aarch64")))) { if (personality(PER_LINUX32) == -1) throw SysError("cannot set armhf-linux personality"); } /* Impersonate a Linux 2.6 machine to get some determinism in builds that depend on the kernel version. */ if ((drv.platform == "i686-linux" || drv.platform == "x86_64-linux") && settings.impersonateLinux26) { int cur = personality(0xffffffff); if (cur != -1) personality(cur | 0x0020000 /* == UNAME26 */); } /* Disable address space randomization for improved determinism. */ int cur = personality(0xffffffff); if (cur != -1) personality(cur | ADDR_NO_RANDOMIZE); /* Ask the kernel to eagerly kill us & our children if it runs out of memory, regardless of blame, to preserve ‘real’ user data & state. */ try { writeFile("/proc/self/oom_score_adj", "1000"); // 100% } catch (...) { ignoreException(); } #endif /* Fill in the environment. */ Strings envStrs; foreach (Environment::const_iterator, i, env) envStrs.push_back(rewriteHashes(i->first + "=" + i->second, rewritesToTmp)); /* If we are running in `build-users' mode, then switch to the user we allocated above. Make sure that we drop all root privileges. Note that above we have closed all file descriptors except std*, so that's safe. Also note that setuid() when run as root sets the real, effective and saved UIDs. */ if (buildUser.enabled()) { /* Preserve supplementary groups of the build user, to allow admins to specify groups such as "kvm". */ if (setgroups(buildUser.getSupplementaryGIDs().size(), buildUser.getSupplementaryGIDs().data()) == -1) throw SysError("cannot set supplementary groups of build user"); if (setgid(buildUser.getGID()) == -1 || getgid() != buildUser.getGID() || getegid() != buildUser.getGID()) throw SysError("setgid failed"); if (setuid(buildUser.getUID()) == -1 || getuid() != buildUser.getUID() || geteuid() != buildUser.getUID()) throw SysError("setuid failed"); } restoreSIGPIPE(); /* Indicate that we managed to set up the build environment. */ writeFull(STDERR_FILENO, "\n"); /* Execute the program. This should not return. */ if (isBuiltin(drv)) { try { logType = ltFlat; auto buildDrv = lookupBuiltinBuilder(drv.builder); if (buildDrv != NULL) { /* Check what the output file name is. When doing a 'bmCheck' build, the output file name is different from that specified in DRV due to hash rewriting. */ Path output = drv.outputs["out"].path; auto redirected = redirectedOutputs.find(output); if (redirected != redirectedOutputs.end()) output = redirected->second; buildDrv(drv, drvPath, output); } else throw Error(format("unsupported builtin function '%1%'") % string(drv.builder, 8)); _exit(0); } catch (std::exception & e) { writeFull(STDERR_FILENO, "error: " + string(e.what()) + "\n"); _exit(1); } } /* Fill in the arguments. */ Strings args; string builderBasename = baseNameOf(drv.builder); args.push_back(builderBasename); foreach (Strings::iterator, i, drv.args) args.push_back(rewriteHashes(*i, rewritesToTmp)); /* If DRV targets the same operating system kernel, try to execute it: there might be binfmt_misc set up for user-land emulation of other architectures. However, if it targets a different operating system--e.g., "i586-gnu" vs. "x86_64-linux"--do not try executing it: the ELF file for that OS is likely indistinguishable from a native ELF binary and it would just crash at run time. */ int error; if (sameOperatingSystemKernel(drv.platform, settings.thisSystem)) { execve(drv.builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data()); error = errno; } else { error = ENOEXEC; } /* Right platform? Check this after we've tried 'execve' to allow for transparent emulation of different platforms with binfmt_misc handlers that invoke QEMU. */ if (error == ENOEXEC && !canBuildLocally(drv.platform)) { if (settings.printBuildTrace) printMsg(lvlError, format("@ unsupported-platform %1% %2%") % drvPath % drv.platform); throw Error( format("a `%1%' is required to build `%3%', but I am a `%2%'") % drv.platform % settings.thisSystem % drvPath); } errno = error; throw SysError(format("executing `%1%'") % drv.builder); } catch (std::exception & e) { writeFull(STDERR_FILENO, "while setting up the build environment: " + string(e.what()) + "\n"); _exit(1); } abort(); /* never reached */ } /* Parse a list of reference specifiers. Each element must either be a store path, or the symbolic name of the output of the derivation (such as `out'). */ PathSet parseReferenceSpecifiers(const Derivation & drv, string attr) { PathSet result; Paths paths = tokenizeString(attr); foreach (Strings::iterator, i, paths) { if (isStorePath(*i)) result.insert(*i); else if (drv.outputs.find(*i) != drv.outputs.end()) result.insert(drv.outputs.find(*i)->second.path); else throw BuildError( format("derivation contains an invalid reference specifier `%1%'") % *i); } return result; } void DerivationGoal::registerOutputs() { /* When using a build hook, the build hook can register the output as valid (by doing `nix-store --import'). If so we don't have to do anything here. */ if (hook) { bool allValid = true; foreach (DerivationOutputs::iterator, i, drv.outputs) if (!worker.store.isValidPath(i->second.path)) allValid = false; if (allValid) return; } ValidPathInfos infos; /* Set of inodes seen during calls to canonicalisePathMetaData() for this build's outputs. This needs to be shared between outputs to allow hard links between outputs. */ InodesSeen inodesSeen; Path checkSuffix = "-check"; /* Check whether the output paths were created, and grep each output path to determine what other paths it references. Also make all output paths read-only. */ foreach (DerivationOutputs::iterator, i, drv.outputs) { Path path = i->second.path; if (missingPaths.find(path) == missingPaths.end()) continue; Path actualPath = path; if (useChroot) { actualPath = chrootRootDir + path; } else { Path redirected = redirectedOutputs[path]; if (buildMode == bmRepair && redirectedBadOutputs.find(path) != redirectedBadOutputs.end() && pathExists(redirected)) replaceValidPath(path, redirected); if (buildMode == bmCheck && redirected != "") actualPath = redirected; } struct stat st; if (lstat(actualPath.c_str(), &st) == -1) { if (errno == ENOENT) throw BuildError( format("builder for `%1%' failed to produce output path `%2%'") % drvPath % path); throw SysError(format("getting attributes of path `%1%'") % actualPath); } #ifndef __CYGWIN__ /* Check that the output is not group or world writable, as that means that someone else can have interfered with the build. Also, the output should be owned by the build user. */ if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH))) || (buildUser.enabled() && st.st_uid != buildUser.getUID())) throw BuildError(format("suspicious ownership or permission on `%1%'; rejecting this build output") % path); #endif /* Apply hash rewriting if necessary. */ bool rewritten = false; if (!rewritesFromTmp.empty()) { printMsg(lvlError, format("warning: rewriting hashes in `%1%'; cross fingers") % path); /* Canonicalise first. This ensures that the path we're rewriting doesn't contain a hard link to /etc/shadow or something like that. */ canonicalisePathMetaData(actualPath, buildUser.enabled() ? buildUser.getUID() : -1, inodesSeen); /* FIXME: this is in-memory. */ StringSink sink; dumpPath(actualPath, sink); deletePath(actualPath); sink.s = rewriteHashes(sink.s, rewritesFromTmp); StringSource source(sink.s); restorePath(actualPath, source); rewritten = true; } startNest(nest, lvlTalkative, format("scanning for references inside `%1%'") % path); /* Check that fixed-output derivations produced the right outputs (i.e., the content hash should match the specified hash). */ if (i->second.hash != "") { bool recursive; HashType ht; Hash h; i->second.parseHashInfo(recursive, ht, h); if (!recursive) { /* The output path should be a regular file without execute permission. */ if (!S_ISREG(st.st_mode) || (st.st_mode & S_IXUSR) != 0) throw BuildError( format("output path `%1% should be a non-executable regular file") % path); } /* Check the hash. */ Hash h2 = recursive ? hashPath(ht, actualPath).first : hashFile(ht, actualPath); if (h != h2) { if (settings.printBuildTrace) printMsg(lvlError, format("@ hash-mismatch %1% %2% %3% %4%") % path % i->second.hashAlgo % printHash16or32(h) % printHash16or32(h2)); throw BuildError(format("hash mismatch for store item '%1%'") % path); } } /* Get rid of all weird permissions. This also checks that all files are owned by the build user, if applicable. */ canonicalisePathMetaData(actualPath, buildUser.enabled() && !rewritten ? buildUser.getUID() : -1, inodesSeen); if (useChroot) { if (pathExists(actualPath)) { /* Now that output paths have been canonicalized (in particular there are no setuid files left), move them outside of the chroot and to the store. */ if (buildMode == bmRepair) replaceValidPath(path, actualPath); else if (buildMode != bmCheck && rename(actualPath.c_str(), path.c_str()) == -1) throw SysError(format("moving build output `%1%' from the chroot to the store") % path); } if (buildMode != bmCheck) actualPath = path; } /* For this output path, find the references to other paths contained in it. Compute the SHA-256 NAR hash at the same time. The hash is stored in the database so that we can verify later on whether nobody has messed with the store. */ HashResult hash; PathSet references = scanForReferences(actualPath, allPaths, hash); if (buildMode == bmCheck) { if (!store->isValidPath(path)) continue; ValidPathInfo info = worker.store.queryPathInfo(path); if (hash.first != info.hash) { if (settings.keepFailed) { Path dst = path + checkSuffix; if (pathExists(dst)) deletePath(dst); if (rename(actualPath.c_str(), dst.c_str())) throw SysError(format("renaming `%1%' to `%2%'") % actualPath % dst); throw Error(format("derivation `%1%' may not be deterministic: output `%2%' differs from `%3%'") % drvPath % path % dst); } else throw Error(format("derivation `%1%' may not be deterministic: output `%2%' differs") % drvPath % path); } if (settings.printBuildTrace) printMsg(lvlError, format("@ build-succeeded %1% -") % drvPath); continue; } /* For debugging, print out the referenced and unreferenced paths. */ foreach (PathSet::iterator, i, inputPaths) { PathSet::iterator j = references.find(*i); if (j == references.end()) debug(format("unreferenced input: `%1%'") % *i); else debug(format("referenced input: `%1%'") % *i); } /* Enforce `allowedReferences' and friends. */ auto checkRefs = [&](const string & attrName, bool allowed, bool recursive) { if (drv.env.find(attrName) == drv.env.end()) return; PathSet spec = parseReferenceSpecifiers(drv, get(drv.env, attrName)); PathSet used; if (recursive) { /* Our requisites are the union of the closures of our references. */ for (auto & i : references) /* Don't call computeFSClosure on ourselves. */ if (actualPath != i) computeFSClosure(worker.store, i, used); } else used = references; for (auto & i : used) if (allowed) { if (spec.find(i) == spec.end()) throw BuildError(format("output (`%1%') is not allowed to refer to path `%2%'") % actualPath % i); } else { if (spec.find(i) != spec.end()) throw BuildError(format("output (`%1%') is not allowed to refer to path `%2%'") % actualPath % i); } }; checkRefs("allowedReferences", true, false); checkRefs("allowedRequisites", true, true); checkRefs("disallowedReferences", false, false); checkRefs("disallowedRequisites", false, true); if (curRound == nrRounds) { worker.store.optimisePath(path); // FIXME: combine with scanForReferences() worker.store.markContentsGood(path); } ValidPathInfo info; info.path = path; info.hash = hash.first; info.narSize = hash.second; info.references = references; info.deriver = drvPath; infos.push_back(info); } /* Compare the result with the previous round, and report which path is different, if any.*/ if (curRound > 1 && prevInfos != infos) { assert(prevInfos.size() == infos.size()); for (auto i = prevInfos.begin(), j = infos.begin(); i != prevInfos.end(); ++i, ++j) if (!(*i == *j)) { Path prev = i->path + checkSuffix; if (pathExists(prev)) throw NotDeterministic( format("output ‘%1%’ of ‘%2%’ differs from ‘%3%’ from previous round") % i->path % drvPath % prev); else throw NotDeterministic( format("output ‘%1%’ of ‘%2%’ differs from previous round") % i->path % drvPath); } assert(false); // shouldn't happen } if (settings.keepFailed) { for (auto & i : drv.outputs) { Path prev = i.second.path + checkSuffix; if (pathExists(prev)) deletePath(prev); if (curRound < nrRounds) { Path dst = i.second.path + checkSuffix; if (rename(i.second.path.c_str(), dst.c_str())) throw SysError(format("renaming ‘%1%’ to ‘%2%’") % i.second.path % dst); } } } if (curRound < nrRounds) { prevInfos = infos; return; } /* Register each output path as valid, and register the sets of paths referenced by each of them. If there are cycles in the outputs, this will fail. */ worker.store.registerValidPaths(infos); } string drvsLogDir = "drvs"; Path DerivationGoal::openLogFile() { logSize = 0; if (!settings.keepLog) return ""; string baseName = baseNameOf(drvPath); /* Create a log file. */ Path dir = (format("%1%/%2%/%3%/") % settings.nixLogDir % drvsLogDir % string(baseName, 0, 2)).str(); createDirs(dir); switch (settings.logCompression) { case COMPRESSION_GZIP: { Path logFileName = (format("%1%/%2%.gz") % dir % string(baseName, 2)).str(); AutoCloseFD fd = open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd == -1) throw SysError(format("creating log file `%1%'") % logFileName); closeOnExec(fd); /* Note: FD will be closed by 'gzclose'. */ if (!(gzLogFile = gzdopen(fd.borrow(), "w"))) throw Error(format("cannot open compressed log file `%1%'") % logFileName); gzbuffer(gzLogFile, 32768); gzsetparams(gzLogFile, Z_BEST_COMPRESSION, Z_DEFAULT_STRATEGY); return logFileName; } #if HAVE_BZLIB_H case COMPRESSION_BZIP2: { Path logFileName = (format("%1%/%2%.bz2") % dir % string(baseName, 2)).str(); AutoCloseFD fd = open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd == -1) throw SysError(format("creating log file `%1%'") % logFileName); closeOnExec(fd); if (!(fLogFile = fdopen(fd.borrow(), "w"))) throw SysError(format("opening log file `%1%'") % logFileName); int err; if (!(bzLogFile = BZ2_bzWriteOpen(&err, fLogFile, 9, 0, 0))) throw Error(format("cannot open compressed log file `%1%'") % logFileName); return logFileName; } #endif case COMPRESSION_NONE: { Path logFileName = (format("%1%/%2%") % dir % string(baseName, 2)).str(); fdLogFile = open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fdLogFile == -1) throw SysError(format("creating log file `%1%'") % logFileName); closeOnExec(fdLogFile); return logFileName; } } abort(); } void DerivationGoal::closeLogFile() { if (gzLogFile) { int err; err = gzclose(gzLogFile); gzLogFile = NULL; if (err != Z_OK) throw Error(format("cannot close compressed log file (gzip error = %1%)") % err); } #if HAVE_BZLIB_H else if (bzLogFile) { int err; BZ2_bzWriteClose(&err, bzLogFile, 0, 0, 0); bzLogFile = 0; if (err != BZ_OK) throw Error(format("cannot close compressed log file (BZip2 error = %1%)") % err); } #endif if (fLogFile) { fclose(fLogFile); fLogFile = 0; } fdLogFile.close(); } static void _chown(const Path & path, uid_t uid, gid_t gid) { checkInterrupt(); if (lchown(path.c_str(), uid, gid) == -1) { throw SysError(format("change owner and group of `%1%'") % path); } struct stat st = lstat(path); if (S_ISDIR(st.st_mode)) { for (auto & i : readDirectory(path)) _chown(path + "/" + i.name, uid, gid); } } void DerivationGoal::deleteTmpDir(bool force) { if (tmpDir != "") { // When useChroot is true, tmpDir looks like // "/tmp/guix-build-foo.drv-0/top". Its parent is root-owned. string top; if (useChroot) { if (baseNameOf(tmpDir) != "top") abort(); top = dirOf(tmpDir); } else top = tmpDir; if (settings.keepFailed && !force) { printMsg(lvlError, format("note: keeping build directory `%2%'") % drvPath % top); chmod(tmpDir.c_str(), 0755); // Change the ownership if clientUid is set. Never change the // ownership or the group to "root" for security reasons. if (settings.clientUid != (uid_t) -1 && settings.clientUid != 0) { _chown(tmpDir, settings.clientUid, settings.clientGid != 0 ? settings.clientGid : -1); if (top != tmpDir) { // Rename tmpDir to its parent, with an intermediate step. string pivot = top + ".pivot"; if (rename(top.c_str(), pivot.c_str()) == -1) throw SysError("pivoting failed build tree"); if (rename((pivot + "/top").c_str(), top.c_str()) == -1) throw SysError("renaming failed build tree"); rmdir(pivot.c_str()); } } } else { deletePath(tmpDir); if (top != tmpDir) rmdir(dirOf(tmpDir).c_str()); } tmpDir = ""; } } void DerivationGoal::handleChildOutput(int fd, const string & data) { string prefix; if (settings.multiplexedBuildOutput) { /* Print a prefix that allows clients to determine whether a message comes from the daemon or from a build process, and in the latter case, which build process it comes from. The PID here matches the one given in "@ build-started" traces; it's shorter that the derivation file name, hence this choice. */ prefix = "@ build-log " + std::to_string(pid < 0 ? hook->pid : pid) + " " + std::to_string(data.size()) + "\n"; } if ((hook && fd == hook->builderOut.readSide) || (!hook && fd == builderOut.readSide)) { logSize += data.size(); if (settings.maxLogSize && logSize > settings.maxLogSize) { printMsg(lvlError, format("%1% killed after writing more than %2% bytes of log output") % getName() % settings.maxLogSize); timedOut(); // not really a timeout, but close enough return; } if (verbosity >= settings.buildVerbosity) writeToStderr(prefix + data); if (gzLogFile) { if (data.size() > 0) { int count, err; count = gzwrite(gzLogFile, data.data(), data.size()); if (count == 0) throw Error(format("cannot write to compressed log file (gzip error = %1%)") % gzerror(gzLogFile, &err)); } #if HAVE_BZLIB_H } else if (bzLogFile) { int err; BZ2_bzWrite(&err, bzLogFile, (unsigned char *) data.data(), data.size()); if (err != BZ_OK) throw Error(format("cannot write to compressed log file (BZip2 error = %1%)") % err); #endif } else if (fdLogFile != -1) writeFull(fdLogFile, data); } if (hook && fd == hook->fromAgent.readSide) writeToStderr(prefix + data); } void DerivationGoal::handleEOF(int fd) { worker.wakeUp(shared_from_this()); } PathSet DerivationGoal::checkPathValidity(bool returnValid, bool checkHash) { PathSet result; foreach (DerivationOutputs::iterator, i, drv.outputs) { if (!wantOutput(i->first, wantedOutputs)) continue; bool good = worker.store.isValidPath(i->second.path) && (!checkHash || worker.store.pathContentsGood(i->second.path)); if (good == returnValid) result.insert(i->second.path); } return result; } bool DerivationGoal::pathFailed(const Path & path) { if (!settings.cacheFailure) return false; if (!worker.store.hasPathFailed(path)) return false; printMsg(lvlError, format("builder for `%1%' failed previously (cached)") % path); if (settings.printBuildTrace) printMsg(lvlError, format("@ build-failed %1% - cached") % drvPath); done(BuildResult::CachedFailure); return true; } Path DerivationGoal::addHashRewrite(const Path & path) { string h1 = string(path, settings.nixStore.size() + 1, 32); string h2 = string(printHash32(hashString(htSHA256, "rewrite:" + drvPath + ":" + path)), 0, 32); Path p = settings.nixStore + "/" + h2 + string(path, settings.nixStore.size() + 33); if (pathExists(p)) deletePath(p); assert(path.size() == p.size()); rewritesToTmp[h1] = h2; rewritesFromTmp[h2] = h1; redirectedOutputs[path] = p; printMsg(lvlChatty, format("output '%1%' redirected to '%2%'") % path % p); return p; } void DerivationGoal::done(BuildResult::Status status, const string & msg) { result.status = status; result.errorMsg = msg; amDone(result.success() ? ecSuccess : ecFailed); if (result.status == BuildResult::TimedOut) worker.timedOut = true; if (result.status == BuildResult::PermanentFailure || result.status == BuildResult::CachedFailure) worker.permanentFailure = true; } ////////////////////////////////////////////////////////////////////// class SubstitutionGoal : public Goal { friend class Worker; private: /* The store path that should be realised through a substitute. */ Path storePath; /* Path info returned by the substituter's query info operation. */ SubstitutablePathInfo info; /* Lock on the store path. */ std::shared_ptr outputLock; /* Whether to try to repair a valid path. */ bool repair; /* Location where we're downloading the substitute. Differs from storePath when doing a repair. */ Path destPath; typedef void (SubstitutionGoal::*GoalState)(); GoalState state; /* The substituter. */ std::shared_ptr substituter; /* Either the empty string, or the status phrase returned by the substituter. */ string status; void tryNext(); public: SubstitutionGoal(const Path & storePath, Worker & worker, bool repair = false); ~SubstitutionGoal(); void timedOut(); string key() { /* "a$" ensures substitution goals happen before derivation goals. */ return "a$" + storePathToName(storePath) + "$" + storePath; } void work(); /* The states. */ void init(); void gotInfo(); void referencesValid(); void tryToRun(); void finished(); /* Callback used by the worker to write to the log. */ void handleChildOutput(int fd, const string & data); void handleEOF(int fd); Path getStorePath() { return storePath; } }; SubstitutionGoal::SubstitutionGoal(const Path & storePath, Worker & worker, bool repair) : Goal(worker) , repair(repair) { this->storePath = storePath; state = &SubstitutionGoal::init; name = (format("substitution of `%1%'") % storePath).str(); trace("created"); } SubstitutionGoal::~SubstitutionGoal() { if (substituter) worker.childTerminated(substituter->pid); } void SubstitutionGoal::timedOut() { if (settings.printBuildTrace) printMsg(lvlError, format("@ substituter-failed %1% timeout") % storePath); if (substituter) { pid_t savedPid = substituter->pid; substituter.reset(); worker.childTerminated(savedPid); } amDone(ecFailed); } void SubstitutionGoal::work() { (this->*state)(); } void SubstitutionGoal::init() { trace("init"); worker.store.addTempRoot(storePath); /* If the path already exists we're done. */ if (!repair && worker.store.isValidPath(storePath)) { amDone(ecSuccess); return; } if (settings.readOnlyMode) throw Error(format("cannot substitute path `%1%' - no write access to the store") % storePath); tryNext(); } void SubstitutionGoal::tryNext() { trace("trying substituter"); SubstitutablePathInfos infos; PathSet dummy(singleton(storePath)); worker.store.querySubstitutablePathInfos(dummy, infos); SubstitutablePathInfos::iterator k = infos.find(storePath); if (k == infos.end()) { /* None left. Terminate this goal and let someone else deal with it. */ debug(format("path `%1%' is required, but there is no substituter that can build it") % storePath); /* Hack: don't indicate failure if there were no substituters. In that case the calling derivation should just do a build. */ amDone(ecNoSubstituters); return; } /* Found a substitute. */ info = k->second; /* To maintain the closure invariant, we first have to realise the paths referenced by this one. */ foreach (PathSet::iterator, i, info.references) if (*i != storePath) /* ignore self-references */ addWaitee(worker.makeSubstitutionGoal(*i)); if (waitees.empty()) /* to prevent hang (no wake-up event) */ referencesValid(); else state = &SubstitutionGoal::referencesValid; } void SubstitutionGoal::referencesValid() { trace("all references realised"); if (nrFailed > 0) { debug(format("some references of path `%1%' could not be realised") % storePath); amDone(nrNoSubstituters > 0 || nrIncompleteClosure > 0 ? ecIncompleteClosure : ecFailed); return; } foreach (PathSet::iterator, i, info.references) if (*i != storePath) /* ignore self-references */ assert(worker.store.isValidPath(*i)); state = &SubstitutionGoal::tryToRun; worker.wakeUp(shared_from_this()); } void SubstitutionGoal::tryToRun() { trace("trying to run"); /* Make sure that we are allowed to start a build. Note that even is maxBuildJobs == 0 (no local builds allowed), we still allow a substituter to run. This is because substitutions cannot be distributed to another machine via the build hook. */ if (worker.getNrLocalBuilds() >= (settings.maxBuildJobs == 0 ? 1 : settings.maxBuildJobs)) { worker.waitForBuildSlot(shared_from_this()); return; } /* Maybe a derivation goal has already locked this path (exceedingly unlikely, since it should have used a substitute first, but let's be defensive). */ outputLock.reset(); // make sure this goal's lock is gone if (pathIsLockedByMe(storePath)) { debug(format("restarting substitution of `%1%' because it's locked by another goal") % storePath); worker.waitForAnyGoal(shared_from_this()); return; /* restart in the tryToRun() state when another goal finishes */ } /* Acquire a lock on the output path. */ outputLock = std::shared_ptr(new PathLocks); if (!outputLock->lockPaths(singleton(storePath), "", false)) { worker.waitForAWhile(shared_from_this()); return; } /* Check again whether the path is invalid. */ if (!repair && worker.store.isValidPath(storePath)) { debug(format("store path `%1%' has become valid") % storePath); outputLock->setDeletion(true); amDone(ecSuccess); return; } printMsg(lvlInfo, format("fetching path `%1%'...") % storePath); destPath = repair ? storePath + ".tmp" : storePath; /* Remove the (stale) output path if it exists. */ if (pathExists(destPath)) deletePath(destPath); if (!worker.substituter) { const Strings args = { "substitute", "--substitute" }; const std::map env = { { "_NIX_OPTIONS", settings.pack() + "deduplicate=" + (settings.autoOptimiseStore ? "yes" : "no") } }; worker.substituter = std::make_shared(settings.guixProgram, args, env); } /* Borrow the worker's substituter. */ if (!substituter) substituter.swap(worker.substituter); /* Send the request to the substituter. */ writeLine(substituter->toAgent.writeSide, (format("substitute %1% %2%") % storePath % destPath).str()); set fds; fds.insert(substituter->fromAgent.readSide); fds.insert(substituter->builderOut.readSide); worker.childStarted(shared_from_this(), substituter->pid, fds, true, true); state = &SubstitutionGoal::finished; if (settings.printBuildTrace) /* The second element in the message used to be the name of the substituter but we're left with only one. */ printMsg(lvlError, format("@ substituter-started %1% substitute") % storePath); } void SubstitutionGoal::finished() { trace("substitute finished"); /* Remove the 'guix substitute' process from the list of children. */ worker.childTerminated(substituter->pid); /* If max-jobs > 1, the worker might have created a new 'substitute' process in the meantime. If that is the case, terminate ours; otherwise, give it back to the worker. */ if (worker.substituter) { substituter.reset (); } else { worker.substituter.swap(substituter); } /* Check the exit status and the build result. */ HashResult hash; try { auto statusList = tokenizeString >(status); if (statusList.empty()) { throw SubstError(format("fetching path `%1%' (empty status)") % storePath); } else if (statusList[0] == "hash-mismatch") { if (settings.printBuildTrace) { auto hashType = statusList[1]; auto expectedHash = statusList[2]; auto actualHash = statusList[3]; printMsg(lvlError, format("@ hash-mismatch %1% %2% %3% %4%") % storePath % hashType % expectedHash % actualHash); } throw SubstError(format("hash mismatch for substituted item `%1%'") % storePath); } else if (statusList[0] == "success") { if (!pathExists(destPath)) throw SubstError(format("substitute did not produce path `%1%'") % destPath); std::string hashStr = statusList[1]; size_t n = hashStr.find(':'); if (n == string::npos) throw Error(format("bad hash from substituter: %1%") % hashStr); HashType hashType = parseHashType(string(hashStr, 0, n)); switch (hashType) { case htUnknown: throw Error(format("unknown hash algorithm in `%1%'") % hashStr); case htSHA256: hash.first = parseHash16or32(hashType, string(hashStr, n + 1)); if (!string2Int(statusList[2], hash.second)) throw Error(format("invalid nar size for '%1%' substitute") % storePath); break; default: /* The database only stores SHA256 hashes, so compute it. */ hash = hashPath(htSHA256, destPath); break; } } else throw SubstError(format("fetching path `%1%' (status: '%2%')") % storePath % status); } catch (SubstError & e) { printMsg(lvlInfo, e.msg()); if (settings.printBuildTrace) { printMsg(lvlError, format("@ substituter-failed %1% %2% %3%") % storePath % status % e.msg()); } amDone(ecFailed); return; } if (repair) replaceValidPath(storePath, destPath); /* Note: 'guix substitute' takes care of resetting timestamps and of deduplicating 'destPath', so no need to do it here. */ ValidPathInfo info2; info2.path = storePath; info2.hash = hash.first; info2.narSize = hash.second; info2.references = info.references; info2.deriver = info.deriver; worker.store.registerValidPath(info2); outputLock->setDeletion(true); outputLock.reset(); worker.store.markContentsGood(storePath); printMsg(lvlChatty, format("substitution of path `%1%' succeeded") % storePath); if (settings.printBuildTrace) printMsg(lvlError, format("@ substituter-succeeded %1%") % storePath); amDone(ecSuccess); } void SubstitutionGoal::handleChildOutput(int fd, const string & data) { if (verbosity >= settings.buildVerbosity && fd == substituter->fromAgent.readSide) { writeToStderr(data); /* Don't write substitution output to a log file for now. We probably should, though. */ } if (fd == substituter->builderOut.readSide) { /* DATA may consist of several lines. Process them one by one. */ string input = data; while (!input.empty()) { /* Process up to the first newline. */ size_t end = input.find_first_of("\n"); string trimmed = (end != string::npos) ? input.substr(0, end) : input; /* Update the goal's state accordingly. */ if (status == "") { status = trimmed; worker.wakeUp(shared_from_this()); } else { printMsg(lvlError, format("unexpected substituter message '%1%'") % input); } input = (end != string::npos) ? input.substr(end + 1) : ""; } } } void SubstitutionGoal::handleEOF(int fd) { worker.wakeUp(shared_from_this()); } ////////////////////////////////////////////////////////////////////// static bool working = false; Worker::Worker(LocalStore & store) : store(store) { /* Debugging: prevent recursive workers. */ if (working) abort(); working = true; nrLocalBuilds = 0; lastWokenUp = 0; permanentFailure = false; timedOut = false; } Worker::~Worker() { working = false; /* Explicitly get rid of all strong pointers now. After this all goals that refer to this worker should be gone. (Otherwise we are in trouble, since goals may call childTerminated() etc. in their destructors). */ topGoals.clear(); } GoalPtr Worker::makeDerivationGoal(const Path & path, const StringSet & wantedOutputs, BuildMode buildMode) { GoalPtr goal = derivationGoals[path].lock(); if (!goal) { goal = GoalPtr(new DerivationGoal(path, wantedOutputs, *this, buildMode)); derivationGoals[path] = goal; wakeUp(goal); } else (dynamic_cast(goal.get()))->addWantedOutputs(wantedOutputs); return goal; } GoalPtr Worker::makeSubstitutionGoal(const Path & path, bool repair) { GoalPtr goal = substitutionGoals[path].lock(); if (!goal) { goal = GoalPtr(new SubstitutionGoal(path, *this, repair)); substitutionGoals[path] = goal; wakeUp(goal); } return goal; } static void removeGoal(GoalPtr goal, WeakGoalMap & goalMap) { /* !!! inefficient */ for (WeakGoalMap::iterator i = goalMap.begin(); i != goalMap.end(); ) if (i->second.lock() == goal) { WeakGoalMap::iterator j = i; ++j; goalMap.erase(i); i = j; } else ++i; } void Worker::removeGoal(GoalPtr goal) { nix::removeGoal(goal, derivationGoals); nix::removeGoal(goal, substitutionGoals); if (topGoals.find(goal) != topGoals.end()) { topGoals.erase(goal); /* If a top-level goal failed, then kill all other goals (unless keepGoing was set). */ if (goal->getExitCode() == Goal::ecFailed && !settings.keepGoing) topGoals.clear(); } /* Wake up goals waiting for any goal to finish. */ foreach (WeakGoals::iterator, i, waitingForAnyGoal) { GoalPtr goal = i->lock(); if (goal) wakeUp(goal); } waitingForAnyGoal.clear(); } void Worker::wakeUp(GoalPtr goal) { goal->trace("woken up"); addToWeakGoals(awake, goal); } unsigned Worker::getNrLocalBuilds() { return nrLocalBuilds; } void Worker::childStarted(GoalPtr goal, pid_t pid, const set & fds, bool inBuildSlot, bool respectTimeouts) { Child child; child.goal = goal; child.fds = fds; child.timeStarted = child.lastOutput = time(0); child.inBuildSlot = inBuildSlot; child.respectTimeouts = respectTimeouts; children[pid] = child; if (inBuildSlot) nrLocalBuilds++; } void Worker::childTerminated(pid_t pid, bool wakeSleepers) { assert(pid != -1); /* common mistake */ Children::iterator i = children.find(pid); assert(i != children.end()); if (i->second.inBuildSlot) { assert(nrLocalBuilds > 0); nrLocalBuilds--; } children.erase(pid); if (wakeSleepers) { /* Wake up goals waiting for a build slot. */ foreach (WeakGoals::iterator, i, wantingToBuild) { GoalPtr goal = i->lock(); if (goal) wakeUp(goal); } wantingToBuild.clear(); } } void Worker::waitForBuildSlot(GoalPtr goal) { debug("wait for build slot"); if (getNrLocalBuilds() < settings.maxBuildJobs) wakeUp(goal); /* we can do it right away */ else addToWeakGoals(wantingToBuild, goal); } void Worker::waitForAnyGoal(GoalPtr goal) { debug("wait for any goal"); addToWeakGoals(waitingForAnyGoal, goal); } void Worker::waitForAWhile(GoalPtr goal) { debug("wait for a while"); addToWeakGoals(waitingForAWhile, goal); } void Worker::run(const Goals & _topGoals) { foreach (Goals::iterator, i, _topGoals) topGoals.insert(*i); startNest(nest, lvlDebug, format("entered goal loop")); while (1) { checkInterrupt(); /* Call every wake goal (in the ordering established by CompareGoalPtrs). */ while (!awake.empty() && !topGoals.empty()) { Goals awake2; for (auto & i : awake) { GoalPtr goal = i.lock(); if (goal) awake2.insert(goal); } awake.clear(); for (auto & goal : awake2) { checkInterrupt(); goal->work(); if (topGoals.empty()) break; // stuff may have been cancelled } } if (topGoals.empty()) break; /* Wait for input. */ if (!children.empty() || !waitingForAWhile.empty()) waitForInput(); else { if (awake.empty() && settings.maxBuildJobs == 0) throw Error( "unable to start any build; either increase `--max-jobs' " "or enable distributed builds"); assert(!awake.empty()); } } /* If --keep-going is not set, it's possible that the main goal exited while some of its subgoals were still active. But if --keep-going *is* set, then they must all be finished now. */ assert(!settings.keepGoing || awake.empty()); assert(!settings.keepGoing || wantingToBuild.empty()); assert(!settings.keepGoing || children.empty()); } void Worker::waitForInput() { printMsg(lvlVomit, "waiting for children"); /* Process output from the file descriptors attached to the children, namely log output and output path creation commands. We also use this to detect child termination: if we get EOF on the logger pipe of a build, we assume that the builder has terminated. */ bool useTimeout = false; struct timeval timeout; timeout.tv_usec = 0; time_t before = time(0); /* If we're monitoring for silence on stdout/stderr, or if there is a build timeout, then wait for input until the first deadline for any child. */ assert(sizeof(time_t) >= sizeof(long)); time_t nearest = LONG_MAX; // nearest deadline foreach (Children::iterator, i, children) { if (!i->second.respectTimeouts) continue; if (settings.maxSilentTime != 0) nearest = std::min(nearest, i->second.lastOutput + settings.maxSilentTime); if (settings.buildTimeout != 0) nearest = std::min(nearest, i->second.timeStarted + settings.buildTimeout); } if (nearest != LONG_MAX) { timeout.tv_sec = std::max((time_t) 1, nearest - before); useTimeout = true; printMsg(lvlVomit, format("sleeping %1% seconds") % timeout.tv_sec); } /* If we are polling goals that are waiting for a lock, then wake up after a few seconds at most. */ if (!waitingForAWhile.empty()) { useTimeout = true; if (lastWokenUp == 0) printMsg(lvlError, "waiting for locks or build slots..."); if (lastWokenUp == 0 || lastWokenUp > before) lastWokenUp = before; timeout.tv_sec = std::max((time_t) 1, (time_t) (lastWokenUp + settings.pollInterval - before)); } else lastWokenUp = 0; using namespace std; /* Use select() to wait for the input side of any logger pipe to become `available'. Note that `available' (i.e., non-blocking) includes EOF. */ fd_set fds; FD_ZERO(&fds); int fdMax = 0; foreach (Children::iterator, i, children) { foreach (set::iterator, j, i->second.fds) { FD_SET(*j, &fds); if (*j >= fdMax) fdMax = *j + 1; } } if (select(fdMax, &fds, 0, 0, useTimeout ? &timeout : 0) == -1) { if (errno == EINTR) return; throw SysError("waiting for input"); } time_t after = time(0); /* Process all available file descriptors. */ /* Since goals may be canceled from inside the loop below (causing them go be erased from the `children' map), we have to be careful that we don't keep iterators alive across calls to timedOut(). */ set pids; foreach (Children::iterator, i, children) pids.insert(i->first); foreach (set::iterator, i, pids) { checkInterrupt(); Children::iterator j = children.find(*i); if (j == children.end()) continue; // child destroyed GoalPtr goal = j->second.goal.lock(); assert(goal); set fds2(j->second.fds); foreach (set::iterator, k, fds2) { if (FD_ISSET(*k, &fds)) { unsigned char buffer[4096]; ssize_t rd = read(*k, buffer, sizeof(buffer)); if (rd == -1) { if (errno != EINTR) throw SysError(format("reading from %1%") % goal->getName()); } else if (rd == 0) { debug(format("%1%: got EOF") % goal->getName()); goal->handleEOF(*k); j->second.fds.erase(*k); } else { printMsg(lvlVomit, format("%1%: read %2% bytes") % goal->getName() % rd); string data((char *) buffer, rd); j->second.lastOutput = after; goal->handleChildOutput(*k, data); } } } if (goal->getExitCode() == Goal::ecBusy && settings.maxSilentTime != 0 && j->second.respectTimeouts && after - j->second.lastOutput >= (time_t) settings.maxSilentTime) { printMsg(lvlError, format("%1% timed out after %2% seconds of silence") % goal->getName() % settings.maxSilentTime); goal->timedOut(); } else if (goal->getExitCode() == Goal::ecBusy && settings.buildTimeout != 0 && j->second.respectTimeouts && after - j->second.timeStarted >= (time_t) settings.buildTimeout) { printMsg(lvlError, format("%1% timed out after %2% seconds") % goal->getName() % settings.buildTimeout); goal->timedOut(); } } if (!waitingForAWhile.empty() && lastWokenUp + settings.pollInterval <= after) { lastWokenUp = after; foreach (WeakGoals::iterator, i, waitingForAWhile) { GoalPtr goal = i->lock(); if (goal) wakeUp(goal); } waitingForAWhile.clear(); } } unsigned int Worker::exitStatus() { return timedOut ? 101 : (permanentFailure ? 100 : 1); } ////////////////////////////////////////////////////////////////////// void LocalStore::buildPaths(const PathSet & drvPaths, BuildMode buildMode) { startNest(nest, lvlDebug, format("building %1%") % showPaths(drvPaths)); Worker worker(*this); Goals goals; foreach (PathSet::const_iterator, i, drvPaths) { DrvPathWithOutputs i2 = parseDrvPathWithOutputs(*i); if (isDerivation(i2.first)) goals.insert(worker.makeDerivationGoal(i2.first, i2.second, buildMode)); else goals.insert(worker.makeSubstitutionGoal(*i, buildMode)); } worker.run(goals); PathSet failed; foreach (Goals::iterator, i, goals) if ((*i)->getExitCode() == Goal::ecFailed) { DerivationGoal * i2 = dynamic_cast(i->get()); if (i2) failed.insert(i2->getDrvPath()); else failed.insert(dynamic_cast(i->get())->getStorePath()); } if (!failed.empty()) throw Error(format("build of %1% failed") % showPaths(failed), worker.exitStatus()); } void LocalStore::ensurePath(const Path & path) { /* If the path is already valid, we're done. */ if (isValidPath(path)) return; Worker worker(*this); GoalPtr goal = worker.makeSubstitutionGoal(path); Goals goals = singleton(goal); worker.run(goals); if (goal->getExitCode() != Goal::ecSuccess) throw Error(format("path `%1%' does not exist and cannot be created") % path, worker.exitStatus()); } void LocalStore::repairPath(const Path & path) { Worker worker(*this); GoalPtr goal = worker.makeSubstitutionGoal(path, true); Goals goals = singleton(goal); worker.run(goals); if (goal->getExitCode() != Goal::ecSuccess) { /* Since substituting the path didn't work, if we have a valid deriver, then rebuild the deriver. */ Path deriver = queryDeriver(path); if (deriver != "" && isValidPath(deriver)) { goals.clear(); goals.insert(worker.makeDerivationGoal(deriver, StringSet(), bmRepair)); worker.run(goals); } else throw Error(format("cannot repair path `%1%'") % path, worker.exitStatus()); } } }