summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2014-01-22 17:09:21 +0100
committerLudovic Courtès <ludo@gnu.org>2014-01-24 00:01:50 +0100
commitcd4027fa478e20b59e798dd163a54e7ff9c42c98 (patch)
tree5e8345f9800d039432fb98560ebd66a46d9eb024
parentce4a482983abaf7090d098cdda973139cefb56b7 (diff)
nar: Add 'restore-file-set', for use by build hooks.
* guix/nar.scm (&nar-invalid-hash-error, &nar-signature-error): New condition types. (&nar-error): Add 'file' and 'port' fields. (&nar-read-error): Remove 'port' and 'file' fields. (lock-store-file, unlock-store-file, finalize-store-file, temporary-store-directory, restore-file-set): New procedures. * tests/nar.scm (%seed): New variable. (random-text): New procedure. ("restore-file-set (signed, valid)", "restore-file-set (missing signature)", "restore-file-set (corrupt)"): New tests. * po/Makevars (XGETTEXT_OPTIONS): Add '--keyword=message'.nar fixes * po/POTFILES.in: Add guix/nar.scm.
-rw-r--r--guix/nar.scm229
-rw-r--r--po/Makevars13
-rw-r--r--po/POTFILES.in1
-rw-r--r--tests/nar.scm103
4 files changed, 332 insertions, 14 deletions
diff --git a/guix/nar.scm b/guix/nar.scm
index ea119a25fe1..4bc2deb2299 100644
--- a/guix/nar.scm
+++ b/guix/nar.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2012, 2013 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2012, 2013, 2014 Ludovic Courtès <ludo@gnu.org>
3;;; 3;;;
4;;; This file is part of GNU Guix. 4;;; This file is part of GNU Guix.
5;;; 5;;;
@@ -19,23 +19,40 @@
19(define-module (guix nar) 19(define-module (guix nar)
20 #:use-module (guix utils) 20 #:use-module (guix utils)
21 #:use-module (guix serialization) 21 #:use-module (guix serialization)
22 #:use-module ((guix build utils) #:select (with-directory-excursion)) 22 #:use-module ((guix build utils)
23 #:select (delete-file-recursively with-directory-excursion))
24 #:use-module (guix store)
25 #:use-module (guix ui) ; for '_'
26 #:use-module (guix hash)
27 #:use-module (guix pki)
28 #:use-module (guix pk-crypto)
23 #:use-module (rnrs bytevectors) 29 #:use-module (rnrs bytevectors)
24 #:use-module (rnrs io ports) 30 #:use-module (rnrs io ports)
25 #:use-module (srfi srfi-1) 31 #:use-module (srfi srfi-1)
32 #:use-module (srfi srfi-11)
26 #:use-module (srfi srfi-26) 33 #:use-module (srfi srfi-26)
27 #:use-module (srfi srfi-34) 34 #:use-module (srfi srfi-34)
28 #:use-module (srfi srfi-35) 35 #:use-module (srfi srfi-35)
29 #:use-module (ice-9 ftw) 36 #:use-module (ice-9 ftw)
30 #:use-module (ice-9 match) 37 #:use-module (ice-9 match)
31 #:export (nar-error? 38 #:export (nar-error?
39 nar-error-port
40 nar-error-file
41
32 nar-read-error? 42 nar-read-error?
33 nar-read-error-file
34 nar-read-error-port
35 nar-read-error-token 43 nar-read-error-token
36 44
45 nar-invalid-hash-error?
46 nar-invalid-hash-error-expected
47 nar-invalid-hash-error-actual
48
49 nar-signature-error?
50 nar-signature-error-signature
51
37 write-file 52 write-file
38 restore-file)) 53 restore-file
54
55 restore-file-set))
39 56
40;;; Comment: 57;;; Comment:
41;;; 58;;;
@@ -44,15 +61,24 @@
44;;; Code: 61;;; Code:
45 62
46(define-condition-type &nar-error &error ; XXX: inherit from &nix-error ? 63(define-condition-type &nar-error &error ; XXX: inherit from &nix-error ?
47 nar-error?) 64 nar-error?
65 (file nar-error-file) ; file we were restoring, or #f
66 (port nar-error-port)) ; port from which we read
48 67
49(define-condition-type &nar-read-error &nar-error 68(define-condition-type &nar-read-error &nar-error
50 nar-read-error? 69 nar-read-error?
51 (port nar-read-error-port) ; port from which we read
52 (file nar-read-error-file) ; file we were restoring, or #f
53 (token nar-read-error-token)) ; faulty token, or #f 70 (token nar-read-error-token)) ; faulty token, or #f
54 71
72(define-condition-type &nar-signature-error &nar-error
73 nar-signature-error?
74 (signature nar-signature-error-signature)) ; faulty signature or #f
55 75
76(define-condition-type &nar-invalid-hash-error &nar-signature-error
77 nar-invalid-hash-error?
78 (expected nar-invalid-hash-error-expected) ; expected hash (a bytevector)
79 (actual nar-invalid-hash-error-actual)) ; actual hash
80
81
56(define (dump in out size) 82(define (dump in out size)
57 "Copy SIZE bytes from IN to OUT." 83 "Copy SIZE bytes from IN to OUT."
58 (define buf-size 65536) 84 (define buf-size 65536)
@@ -239,4 +265,191 @@ Restore it as FILE."
239 (&message (message "unsupported nar entry type")) 265 (&message (message "unsupported nar entry type"))
240 (&nar-read-error (port port) (file file) (token x)))))))) 266 (&nar-read-error (port port) (file file) (token x))))))))
241 267
268
269;;;
270;;; Restoring a file set into the store.
271;;;
272
273;; The code below accesses the store directly and is meant to be run from
274;; "build hooks", which cannot invoke the daemon's 'import-paths' RPC since
275;; (1) the locks on the files to be restored as already held, and (2) the
276;; $NIX_HELD_LOCKS hackish environment variable cannot be set.
277;;
278;; So we're really duplicating that functionality of the daemon (well, until
279;; most of the daemon is in Scheme :-)). But note that we do use a couple of
280;; RPCs for functionality not available otherwise, like 'valid-path?'.
281
282(define (lock-store-file file)
283 "Acquire exclusive access to FILE, a store file."
284 (call-with-output-file (string-append file ".lock")
285 (cut fcntl-flock <> 'write-lock)))
286
287(define (unlock-store-file file)
288 "Release access to FILE."
289 (call-with-input-file (string-append file ".lock")
290 (cut fcntl-flock <> 'unlock)))
291
292(define* (finalize-store-file source target
293 #:key (references '()) deriver (lock? #t))
294 "Rename SOURCE to TARGET and register TARGET as a valid store item, with
295REFERENCES and DERIVER. When LOCK? is true, acquire exclusive locks on TARGET
296before attempting to register it; otherwise, assume TARGET's locks are already
297held."
298
299 ;; XXX: Currently we have to call out to the daemon to check whether TARGET
300 ;; is valid.
301 (with-store store
302 (unless (valid-path? store target)
303 (when lock?
304 (lock-store-file target))
305
306 (unless (valid-path? store target)
307 ;; If FILE already exists, delete it (it's invalid anyway.)
308 (when (file-exists? target)
309 (delete-file-recursively target))
310
311 ;; Install the new TARGET.
312 (rename-file source target)
313
314 ;; Register TARGET. As a side effect, it resets the timestamps of all
315 ;; its files, recursively. However, it doesn't attempt to deduplicate
316 ;; its files like 'importPaths' does (FIXME).
317 (register-path target
318 #:references references
319 #:deriver deriver))
320
321 (when lock?
322 (unlock-store-file target)))))
323
324(define (temporary-store-directory)
325 "Return the file name of a temporary directory created in the store that is
326protected from garbage collection."
327 (let* ((template (string-append (%store-prefix) "/guix-XXXXXX"))
328 (port (mkstemp! template)))
329 (close-port port)
330 (with-store store
331 (add-temp-root store template))
332
333 ;; There's a small window during which the GC could delete the file. Try
334 ;; again if that happens.
335 (if (file-exists? template)
336 (begin
337 ;; It's up to the caller to create that file or directory.
338 (delete-file template)
339 template)
340 (temporary-store-directory))))
341
342(define* (restore-file-set port
343 #:key (verify-signature? #t) (lock? #t)
344 (log-port (current-error-port)))
345 "Restore the file set read from PORT to the store. The format of the data
346on PORT must be as created by 'export-paths'---i.e., a series of Nar-formatted
347archives with interspersed meta-data joining them together, possibly with a
348digital signature at the end. Log progress to LOG-PORT. Return the list of
349files restored.
350
351When LOCK? is #f, assume locks for the files to be restored are already held.
352This is the case when the daemon calls a build hook.
353
354Note that this procedure accesses the store directly, so it's only meant to be
355used by the daemon's build hooks since they cannot call back to the daemon
356while the locks are held."
357 (define %export-magic
358 ;; Number used to identify genuine file set archives.
359 #x4558494e)
360
361 (define port*
362 ;; Keep that one around, for error conditions.
363 port)
364
365 (define (assert-valid-signature signature hash file)
366 ;; Bail out if SIGNATURE, an sexp, doesn't match HASH, a bytevector
367 ;; containing the expected hash for FILE.
368 (let* ((signature (catch 'gcry-error
369 (lambda ()
370 (string->canonical-sexp signature))
371 (lambda (err . _)
372 (raise (condition
373 (&message
374 (message "signature is not a valid \
375s-expression"))
376 (&nar-signature-error
377 (file file)
378 (signature signature) (port port)))))))
379 (subject (signature-subject signature))
380 (data (signature-signed-data signature)))
381 (if (and data subject)
382 (if (authorized-key? subject)
383 (if (equal? (hash-data->bytevector data) hash)
384 (unless (valid-signature? signature)
385 (raise (condition
386 (&message (message "invalid signature"))
387 (&nar-signature-error
388 (file file) (signature signature) (port port)))))
389 (raise (condition (&message (message "invalid hash"))
390 (&nar-invalid-hash-error
391 (port port) (file file)
392 (signature signature)
393 (expected (hash-data->bytevector data))
394 (actual hash)))))
395 (raise (condition (&message (message "unauthorized public key"))
396 (&nar-signature-error
397 (signature signature) (file file) (port port)))))
398 (raise (condition
399 (&message (message "corrupt signature data"))
400 (&nar-signature-error
401 (signature signature) (file file) (port port)))))))
402
403 (let loop ((n (read-long-long port))
404 (files '()))
405 (case n
406 ((0)
407 (reverse files))
408 ((1)
409 (let-values (((port get-hash)
410 (open-sha256-input-port port)))
411 (let ((temp (temporary-store-directory)))
412 (restore-file port temp)
413 (let ((magic (read-int port)))
414 (unless (= magic %export-magic)
415 (raise (condition
416 (&message (message "corrupt file set archive"))
417 (&nar-read-error
418 (port port*) (file #f) (token #f))))))
419
420 (let ((file (read-store-path port))
421 (refs (read-store-path-list port))
422 (deriver (read-string port))
423 (hash (get-hash))
424 (has-sig? (= 1 (read-int port))))
425 (format log-port
426 (_ "importing file or directory '~a'...~%")
427 file)
428
429 (let ((sig (and has-sig? (read-string port))))
430 (when verify-signature?
431 (if sig
432 (begin
433 (assert-valid-signature sig hash file)
434 (format log-port
435 (_ "found valid signature for '~a'~%")
436 file)
437 (finalize-store-file temp file
438 #:references refs
439 #:deriver deriver
440 #:lock? lock?)
441 (loop (read-long-long port)
442 (cons file files)))
443 (raise (condition
444 (&message (message "imported file lacks \
445a signature"))
446 (&nar-signature-error
447 (port port*) (file file) (signature #f)))))))))))
448 (else
449 ;; Neither 0 nor 1.
450 (raise (condition
451 (&message (message "invalid inter-file archive mark"))
452 (&nar-read-error
453 (port port) (file #f) (token #f))))))))
454
242;;; nar.scm ends here 455;;; nar.scm ends here
diff --git a/po/Makevars b/po/Makevars
index 81fd53ef2cb..ade615a4523 100644
--- a/po/Makevars
+++ b/po/Makevars
@@ -5,11 +5,14 @@ DOMAIN = $(PACKAGE)
5subdir = po 5subdir = po
6top_builddir = .. 6top_builddir = ..
7 7
8# These options get passed to xgettext. 8# These options get passed to xgettext. We want to catch standard
9XGETTEXT_OPTIONS = \ 9# gettext uses, package synopses and descriptions, and SRFI-34 error
10 --language=Scheme --from-code=UTF-8 \ 10# condition messages.
11 --keyword=_ --keyword=N_ \ 11XGETTEXT_OPTIONS = \
12 --keyword=synopsis --keyword=description 12 --language=Scheme --from-code=UTF-8 \
13 --keyword=_ --keyword=N_ \
14 --keyword=synopsis --keyword=description \
15 --keyword=message
13 16
14COPYRIGHT_HOLDER = Ludovic Courtès 17COPYRIGHT_HOLDER = Ludovic Courtès
15 18
diff --git a/po/POTFILES.in b/po/POTFILES.in
index beefdc901b8..b329f21e927 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -15,3 +15,4 @@ guix/scripts/authenticate.scm
15guix/gnu-maintenance.scm 15guix/gnu-maintenance.scm
16guix/ui.scm 16guix/ui.scm
17guix/http-client.scm 17guix/http-client.scm
18guix/nar.scm
diff --git a/tests/nar.scm b/tests/nar.scm
index 6493d76876f..9f21f990c82 100644
--- a/tests/nar.scm
+++ b/tests/nar.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2012, 2013 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2012, 2013, 2014 Ludovic Courtès <ludo@gnu.org>
3;;; 3;;;
4;;; This file is part of GNU Guix. 4;;; This file is part of GNU Guix.
5;;; 5;;;
@@ -18,11 +18,17 @@
18 18
19(define-module (test-nar) 19(define-module (test-nar)
20 #:use-module (guix nar) 20 #:use-module (guix nar)
21 #:use-module (guix store)
22 #:use-module ((guix hash) #:select (open-sha256-input-port))
21 #:use-module (rnrs bytevectors) 23 #:use-module (rnrs bytevectors)
22 #:use-module (rnrs io ports) 24 #:use-module (rnrs io ports)
25 #:use-module (srfi srfi-1)
23 #:use-module (srfi srfi-26) 26 #:use-module (srfi srfi-26)
27 #:use-module (srfi srfi-34)
28 #:use-module (srfi srfi-35)
24 #:use-module (srfi srfi-64) 29 #:use-module (srfi srfi-64)
25 #:use-module (ice-9 ftw) 30 #:use-module (ice-9 ftw)
31 #:use-module (ice-9 regex)
26 #:use-module (ice-9 match)) 32 #:use-module (ice-9 match))
27 33
28;; Test the (guix nar) module. 34;; Test the (guix nar) module.
@@ -156,6 +162,24 @@
156 (string-append (dirname (search-path %load-path "pre-inst-env")) 162 (string-append (dirname (search-path %load-path "pre-inst-env"))
157 "/test-nar-" (number->string (getpid)))) 163 "/test-nar-" (number->string (getpid))))
158 164
165;; XXX: Factorize.
166(define %seed
167 (seed->random-state (logxor (getpid) (car (gettimeofday)))))
168
169(define (random-text)
170 (number->string (random (expt 2 256) %seed) 16))
171
172(define-syntax-rule (let/ec k exp...)
173 ;; This one appeared in Guile 2.0.9, so provide a copy here.
174 (let ((tag (make-prompt-tag)))
175 (call-with-prompt tag
176 (lambda ()
177 (let ((k (lambda args
178 (apply abort-to-prompt tag args))))
179 exp...))
180 (lambda (_ . args)
181 (apply values args)))))
182
159 183
160(test-begin "nar") 184(test-begin "nar")
161 185
@@ -201,6 +225,83 @@
201 (lambda () 225 (lambda ()
202 (rmdir input))))) 226 (rmdir input)))))
203 227
228;; 'restore-file-set' depends on 'open-sha256-input-port', which in turn
229;; relies on a Guile 2.0.10+ feature.
230(test-skip (if (false-if-exception
231 (open-sha256-input-port (%make-void-port "r")))
232 0
233 3))
234
235(test-assert "restore-file-set (signed, valid)"
236 (with-store store
237 (let* ((texts (unfold (cut >= <> 10)
238 (lambda _ (random-text))
239 1+
240 0))
241 (files (map (cut add-text-to-store store "text" <>) texts))
242 (dump (call-with-bytevector-output-port
243 (cut export-paths store files <>))))
244 (delete-paths store files)
245 (and (every (negate file-exists?) files)
246 (let* ((source (open-bytevector-input-port dump))
247 (imported (restore-file-set source)))
248 (and (equal? imported files)
249 (every (lambda (file)
250 (and (file-exists? file)
251 (valid-path? store file)))
252 files)
253 (equal? texts
254 (map (lambda (file)
255 (call-with-input-file file
256 get-string-all))
257 files))))))))
258
259(test-assert "restore-file-set (missing signature)"
260 (let/ec return
261 (with-store store
262 (let* ((file (add-text-to-store store "foo" "Hello, world!"))
263 (dump (call-with-bytevector-output-port
264 (cute export-paths store (list file) <>
265 #:sign? #f))))
266 (delete-paths store (list file))
267 (and (not (file-exists? file))
268 (let ((source (open-bytevector-input-port dump)))
269 (guard (c ((nar-signature-error? c)
270 (let ((message (condition-message c))
271 (port (nar-error-port c)))
272 (return
273 (and (string-match "lacks.*signature" message)
274 (string=? file (nar-error-file c))
275 (eq? source port))))))
276 (restore-file-set source))
277 #f))))))
278
279(test-assert "restore-file-set (corrupt)"
280 (let/ec return
281 (with-store store
282 (let* ((file (add-text-to-store store "foo"
283 (random-text)))
284 (dump (call-with-bytevector-output-port
285 (cute export-paths store (list file) <>))))
286 (delete-paths store (list file))
287
288 ;; Flip a byte in the file contents.
289 (let* ((index 120)
290 (byte (bytevector-u8-ref dump index)))
291 (bytevector-u8-set! dump index (logxor #xff byte)))
292
293 (and (not (file-exists? file))
294 (let ((source (open-bytevector-input-port dump)))
295 (guard (c ((nar-invalid-hash-error? c)
296 (let ((message (condition-message c))
297 (port (nar-error-port c)))
298 (return
299 (and (string-contains message "hash")
300 (string=? file (nar-error-file c))
301 (eq? source port))))))
302 (restore-file-set source))
303 #f))))))
304
204(test-end "nar") 305(test-end "nar")
205 306
206 307