summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am3
-rw-r--r--guix/build/store-copy.scm3
-rw-r--r--guix/store/build-derivations.scm474
3 files changed, 478 insertions, 2 deletions
diff --git a/Makefile.am b/Makefile.am
index 43a98ad906c..664e3da4a2b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -312,7 +312,8 @@ STORE_MODULES = \
312 guix/store/database.scm \ 312 guix/store/database.scm \
313 guix/store/deduplication.scm \ 313 guix/store/deduplication.scm \
314 guix/store/roots.scm \ 314 guix/store/roots.scm \
315 guix/store/environment.scm 315 guix/store/environment.scm \
316 guix/store/build-derivations.scm
316 317
317MODULES += $(STORE_MODULES) 318MODULES += $(STORE_MODULES)
318 319
diff --git a/guix/build/store-copy.scm b/guix/build/store-copy.scm
index 549aa4f28be..73bfe694e08 100644
--- a/guix/build/store-copy.scm
+++ b/guix/build/store-copy.scm
@@ -27,7 +27,8 @@
27 #:use-module (ice-9 rdelim) 27 #:use-module (ice-9 rdelim)
28 #:use-module (ice-9 ftw) 28 #:use-module (ice-9 ftw)
29 #:use-module (ice-9 vlist) 29 #:use-module (ice-9 vlist)
30 #:export (store-info? 30 #:export (<store-info>
31 store-info?
31 store-info 32 store-info
32 store-info-item 33 store-info-item
33 store-info-deriver 34 store-info-deriver
diff --git a/guix/store/build-derivations.scm b/guix/store/build-derivations.scm
new file mode 100644
index 00000000000..506cd406722
--- /dev/null
+++ b/guix/store/build-derivations.scm
@@ -0,0 +1,474 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2017, 2019 Caleb Ristvedt <caleb.ristvedt@cune.org>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19;;; For building derivations.
20
21(define-module (guix store build-derivations)
22 #:use-module (guix store derivations)
23 #:use-module (guix store files)
24 #:use-module (guix store database)
25 #:use-module (guix config)
26 #:use-module (guix build syscalls)
27 #:use-module (ice-9 vlist)
28 #:use-module (ice-9 popen)
29 #:use-module (srfi srfi-1)
30 #:use-module (srfi srfi-9)
31 #:use-module (srfi srfi-26)
32 #:use-module (srfi srfi-11)
33 #:use-module (gcrypt hash)
34 #:use-module (guix serialization)
35 #:use-module (guix base16)
36 #:use-module (guix sets)
37 #:use-module ((guix build utils) #:select (delete-file-recursively
38 mkdir-p
39 copy-recursively))
40 #:use-module (guix build store-copy)
41 #:use-module (gnu system file-systems)
42 #:use-module (ice-9 textual-ports)
43 #:use-module (ice-9 match)
44 #:use-module (rnrs io ports)
45 #:use-module (ice-9 rdelim)
46 #:use-module (ice-9 ftw)
47 #:use-module (ice-9 q)
48 #:use-module (srfi srfi-43)
49 #:use-module (rnrs bytevectors)
50 #:use-module (guix store environment)
51 #:export (builder+environment+inputs
52 build-derivation))
53
54(define (output-paths drv)
55 "Return all store output paths produced by DRV."
56 (match (derivation-outputs drv)
57 (((outid . ($ <derivation-output> output-path)) ...)
58 output-path)))
59
60(define (get-output-specs drv possible-references)
61 "Return a list of <store-info> objects, one for each output of DRV."
62 (map (match-lambda
63 ((outid . ($ <derivation-output> output-path))
64 (let ((references
65 (scan-for-references output-path
66 ;; outputs can reference
67 ;; themselves or other outputs of
68 ;; the same derivation.
69 (append (output-paths drv)
70 possible-references))))
71 (store-info output-path (derivation-file-name drv) references))))
72 (derivation-outputs drv)))
73
74(define (builtin-download drv outputs)
75 "Download DRV outputs OUTPUTS into the store."
76 (setenv "NIX_STORE" %store-directory)
77 ;; XXX: Set _NIX_OPTIONS once client settings are known
78 (execl (string-append %libexecdir "/download")
79 "download"
80 (derivation-file-name drv)
81 ;; We assume this has only a single output
82 (derivation-output-path (cdr (first outputs)))))
83
84;; if a derivation builder name is in here, it is a builtin. For normal
85;; behavior, make sure everything starts with "builtin:". Also, the procedures
86;; stored in here should take two arguments, the derivation and the list of
87;; (output-name . <derivation-output>)s to be built.
88
89(define builtins
90 (let ((builtins-table (make-hash-table 10)))
91 (hash-set! builtins-table
92 "builtin:download"
93 builtin-download)
94 builtins-table))
95
96(define %keep-build-dir? #t)
97
98;; XXX: make this configurable.
99(define %build-group
100 (make-parameter (false-if-exception (getgrnam "guixbuild"))))
101
102(define (get-build-user)
103 ;; XXX: user namespace to make build-user work instead of having to be root?
104 (or (and=> (%build-group)
105 ;; XXX: Acquire a user via lock files once those are properly
106 ;; implemented. For now, avoid conflict with the existing daemon
107 ;; where possible by picking a build user from the end (last)
108 ;; instead of the front.
109 ;; So in the future, replace LAST with ACQUIRE-BUILD-USER
110 (compose passwd:uid getpwnam last group:mem))
111 (getuid)))
112
113(define (get-build-group)
114 (or (and (zero? (getuid))
115 (group:gid %build-group))
116 (getgid)))
117
118(define-record-type <trie-node>
119 (make-trie-node table string-exists?)
120 trie-node?
121 ;; TODO implement skip values. Probably not as big a speed gain as you think
122 ;; it is, since this is I/O-bound.
123 ;; (skip-value node-skip-value set-skip-value!)
124 (table node-table set-node-table!)
125 ;; Technically speaking, it's possible for both CAT and CATTLE to be in a
126 ;; trie at once. Of course, for our purposes, this is
127 (string-exists? node-string-exists? set-string-exists?!))
128
129(define* (add-to-trie trie string #:optional (new-tables-size 2))
130 "Adds STR to TRIE."
131 (let ((str (string->utf8 string)))
132 (let next-node ((position 0)
133 (current-node trie))
134 (if (= position (bytevector-length str))
135 ;; this is it. This is where we need to register that this string is
136 ;; present.
137 (set-string-exists?! current-node #t)
138 (let* ((current-table (node-table current-node))
139 (node (hash-ref current-table
140 (bytevector-u8-ref str position))))
141 (if node
142 (next-node (1+ position)
143 node)
144 (let ((new-node (make-trie-node (make-hash-table new-tables-size)
145 #f)))
146 (hash-set! current-table
147 (bytevector-u8-ref str position)
148 new-node)
149 (next-node (1+ position)
150 new-node))))))))
151
152(define (make-search-trie strings)
153 ;; TODO: make the first few trie levels non-sparse tables to avoid hashing
154 ;; overhead.
155 (let ((root (make-trie-node (make-hash-table) #f)))
156 (for-each (cut add-to-trie root <>)
157 strings)
158 root))
159
160
161(define (remove-from-trie! trie sequence)
162 "Removes SEQUENCE from TRIE. This means that any nodes that are only in the
163path of SEQUENCE are removed. It's an error to use this with a sequence not
164already in TRIE."
165 ;; Hm. Looks like we'll have to recurse all the way down, find where it
166 ;; ends, then stop at the first thing on the way back up that has anything
167 ;; with the same prefix. Or I could do this the right way with an explicit
168 ;; stack. Hm...
169
170 (define (node-stack)
171 (let next ((nodes '())
172 (i 0)
173 (current-node trie))
174 (if (= (bytevector-length sequence) i)
175 (begin
176 ;; it's possible that even though this is the last node of this
177 ;; sequence it can't be deleted. So mark it as not denoting a
178 ;; string.
179 (set-string-exists?! current-node #f)
180 (cons current-node nodes))
181 (let ((next-node (hash-ref (node-table current-node)
182 (bytevector-u8-ref sequence i))))
183 (next (cons current-node nodes)
184 (1+ i)
185 next-node)))))
186
187 (let maybe-delete ((visited-nodes (node-stack))
188 (i (1- (bytevector-length sequence))))
189 (match visited-nodes
190 ((current parent others ...)
191 (when (zero? (hash-count (const #t)
192 (node-table current)))
193
194 (hash-remove! (node-table parent)
195 (bytevector-u8-ref sequence i))
196 (maybe-delete (cdr visited-nodes)
197 (1- i))))
198 ((current)
199 #f))))
200
201(define (scanning-wrapper-port output-port paths)
202 "Creates a wrapper port which passes through bytes to OUTPUT-PORT and
203returns it as well as a procedure which, when called, returns a list of all
204references out of the possibilities enumerated in PATHS that were
205detected. PATHS must not be empty."
206 ;; Not sure if I should be using custom ports or soft ports...
207 (let* ((strings (map store-path-hash-part paths))
208 (string->path (fold (lambda (current prev)
209 (vhash-cons (store-path-hash-part current)
210 current
211 prev))
212 vlist-null
213 paths))
214 (lookback-size (apply max (map (compose bytevector-length string->utf8)
215 strings)))
216 (smallest-length (apply min (map (compose bytevector-length
217 string->utf8)
218 strings)))
219 (lookback-buffer (make-bytevector lookback-size))
220 (search-trie (make-search-trie strings))
221 (buffer-pos 0)
222 (references '()))
223
224 (values
225 (make-custom-binary-output-port
226 "scanning-wrapper"
227 ;; write
228 (lambda (bytes offset count)
229 (define (in-lookback? n)
230 (< n buffer-pos))
231 ;; the "virtual" stuff provides a convenient interface that makes it
232 ;; look like we magically remember the end of the previous buffer.
233 (define (virtual-ref n)
234 (if (in-lookback? n)
235 (bytevector-u8-ref lookback-buffer n)
236 (bytevector-u8-ref bytes (+ (- n buffer-pos)
237 offset))))
238
239
240 (let ((total-length (+ buffer-pos count)))
241
242 (define (virtual-copy! start end target)
243 (let* ((copy-size (- end start)))
244 (let copy-next ((i 0))
245 (unless (= i copy-size)
246 (bytevector-u8-set! target
247 i
248 (virtual-ref (+ start i)))
249 (copy-next (1+ i))))
250 target))
251
252 ;; the gritty reality of that magic
253 (define (remember-end)
254 (let* ((copy-amount (min total-length
255 lookback-size))
256 (start (- total-length copy-amount))
257 (end total-length))
258 (virtual-copy! start end lookback-buffer)
259 (set! buffer-pos copy-amount)))
260
261 (define (attempt-match n trie)
262 (let test-position ((i n)
263 (current-node trie))
264 (if (node-string-exists? current-node)
265 ;; MATCH
266 (virtual-copy! n i (make-bytevector (- i n)))
267 (if (>= i total-length)
268 #f
269 (let ((next-node (hash-ref (node-table current-node)
270 (virtual-ref i))))
271 (if next-node
272 (test-position (1+ i)
273 next-node)
274 #f))))))
275
276
277
278 (define (scan)
279 (let next-char ((i 0))
280 (when (< i (- total-length smallest-length))
281 (let ((match-result (attempt-match i search-trie)))
282 (if match-result
283 (begin
284 (set! references
285 (let ((str-result
286 (cdr (vhash-assoc (utf8->string match-result)
287 string->path))))
288 (format #t "Found reference to: ~a~%" str-result)
289 (cons str-result
290 references)))
291 ;; We're not interested in multiple references, it'd
292 ;; just slow us down.
293 (remove-from-trie! search-trie match-result)
294 (next-char (+ i (bytevector-length match-result))))
295 (next-char (1+ i)))))))
296 (format #t "Scanning chunk of ~a bytes~%" count)
297 (scan)
298 (remember-end)
299 (put-bytevector output-port bytes offset count)
300 count))
301 #f ;; get-position
302 #f ;; set-position
303 (lambda ()
304 (close-port output-port)))
305 (lambda ()
306 references))))
307
308
309;; There are two main approaches we can use here: we can look for the entire
310;; store path of the form "/gnu/store/hashpart-name", which will yield no
311;; false positives and likely be faster due to being more quickly able to rule
312;; out sequences, and we can look for just hashpart, which will be faster to
313;; lookup and may both increase false positives and decrease false negatives
314;; as stuff that gets split up will likely still have the hash part all
315;; together, but adds a chance that 32 random base-32 characters could cause a
316;; false positive, but the chances of that are extremely slim, and an
317;; adversary couldn't really use that.
318(define (scan-for-references file possibilities)
319 "Scans for literal references in FILE as long as they happen to be in
320POSSIBILITIES. Returns the list of references found, the sha256 hash of the
321nar, and the length of the nar."
322 (let*-values (((scanning-port get-references)
323 (scanning-wrapper-port (%make-void-port "w") possibilities)))
324 (write-file file scanning-port)
325 (force-output scanning-port)
326 (get-references)))
327
328(define (copy-outputs drv environment)
329 "Copy output paths produced in ENVIRONMENT from building DRV to the store if
330a fake store was used."
331 (let ((store-dir (assoc-ref (environment-temp-dirs environment)
332 'store-directory)))
333 (when store-dir
334 (for-each
335 (match-lambda
336 ((outid . ($ <derivation-output> output-path))
337 (copy-recursively
338 (string-append store-dir "/" (basename output-path)) output-path)))
339 (derivation-outputs drv)))))
340
341(define (topologically-sorted store-infos)
342 "Returns STORE-INFOS in topological order or throws CYCLE-DETECTED if no
343such order exists."
344 (define path->store-info
345 (let loop ((infos store-infos)
346 (mapping vlist-null))
347 (match infos
348 ((($ <store-info> item deriver references) . tail)
349 (loop tail (vhash-cons item (car infos) mapping)))
350 (()
351 (lambda (path)
352 (let ((pair (vhash-assoc path mapping)))
353 (and pair
354 (cdr pair))))))))
355
356 (define (references-of store-info)
357 ;; We need to pretend that self-references don't exist...
358 (fold (lambda (current prev)
359 (let ((info (path->store-info current)))
360 (or (and (not (equal? info store-info))
361 info
362 (cons info prev))
363 prev)))
364 '()
365 (store-info-references store-info)))
366
367 (reverse
368 (let visit ((infos store-infos)
369 (visited (set))
370 (dependents (set))
371 (result '()))
372 (match infos
373 ((head . tail)
374 (if (set-contains? visited head)
375 (if (set-contains? dependents head)
376 (throw 'cycle-detected head)
377 (visit tail visited dependents result))
378 (call-with-values
379 (lambda ()
380 (visit (references-of head)
381 (set-insert head visited)
382 (set-insert head dependents)
383 result))
384 (lambda (result visited)
385 (visit tail
386 visited
387 dependents
388 (cons head result))))))
389 (()
390 (values result visited))))))
391
392(define (run-builder builder drv environment store-inputs)
393 "Run the builder BUILDER for DRV in ENVIRONMENT, wait for it to finish, and
394return the list of <store-info>s corresponding to its outputs."
395 (match (status:exit-val (call-with-values
396 (lambda ()
397 (run-standard environment builder))
398 wait-for-build))
399 (0
400 ;; XXX: check that the output paths were produced.
401 (copy-outputs drv environment)
402 (delete-environment environment)
403 (get-output-specs drv store-inputs))
404 (exit-value
405 (format #t "Builder exited with status ~A~%" exit-value)
406 (if %keep-build-dir?
407 (format #t "Note: keeping build directories: ~A~%"
408 (match (environment-temp-dirs environment)
409 (((sym . dir) ...)
410 dir)))
411 (delete-environment environment))
412 #f)))
413
414(define* (builder+environment+inputs drv #:optional (chroot? #t))
415 "Return a thunk that performs the build action, the environment it should be
416run in, and the store inputs of that environment."
417 (let*-values (((builtin) (hash-ref builtins (derivation-builder drv)))
418 ((environment store-inputs)
419 ((if builtin
420 builtin-builder-environment
421 (if chroot?
422 chroot-build-environment
423 nonchroot-build-environment))
424 drv #:gid (get-build-group) #:uid (get-build-user)))
425 ((builder) (or
426 (and builtin (lambda ()
427 (builtin drv (derivation-outputs
428 drv))))
429 (lambda ()
430 (let ((prog (derivation-builder drv))
431 (args (derivation-builder-arguments drv)))
432 (apply execl prog prog args))))))
433 (values builder environment store-inputs)))
434
435;; Note: used for testing mostly, daemon should be starting builds directly
436;; and not just waiting for them to finish sequentially...
437(define (%build-derivation drv)
438 "Given a <derivation> DRV, build the derivation unconditionally even if its
439outputs already exist."
440 ;; Make sure store permissions and ownership are intact (test-env creates a
441 ;; store with wrong permissions, for example).
442 (when (and (zero? (getuid)) %build-group)
443 (chown %store-directory 0 %build-group))
444 (chmod %store-directory #o1775)
445 ;; Inputs need to exist regardless of how we're getting the outputs of this
446 ;; derivation.
447 (ensure-input-outputs-exist (derivation-inputs drv))
448 (format #t "Starting build of derivation ~a~%~%" drv)
449 (let*-values (((builder environment store-inputs)
450 (builder+environment+inputs drv (zero? (getuid))))
451 ((output-specs)
452 (run-builder builder drv environment store-inputs)))
453 (if output-specs
454 (register-items (topologically-sorted output-specs))
455 (throw 'derivation-build-failed drv))))
456
457(define (ensure-input-outputs-exist inputs)
458 "Call %build-derivation as necessary, recursively, to make the necessary
459outputs of INPUTS exist."
460 (for-each
461 (lambda (input)
462 (let ((input-drv-path (derivation-input-path input)))
463 (unless (outputs-exist? input-drv-path
464 (derivation-input-sub-derivations input))
465 (%build-derivation (read-derivation-from-file input-drv-path)))))
466 inputs))
467
468(define* (build-derivation drv
469 #:optional (outputs (derivation-output-names drv)))
470 "Given a <derivation> DRV with desired outputs OUTPUTS, build DRV if the
471outputs don't already exist."
472 (unless (outputs-exist? (derivation-file-name drv)
473 outputs)
474 (%build-derivation drv)))