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.master
parent
ce4a482983
commit
cd4027fa47
229
guix/nar.scm
229
guix/nar.scm
|
@ -1,5 +1,5 @@
|
|||
;;; GNU Guix --- Functional package management for GNU
|
||||
;;; Copyright © 2012, 2013 Ludovic Courtès <ludo@gnu.org>
|
||||
;;; Copyright © 2012, 2013, 2014 Ludovic Courtès <ludo@gnu.org>
|
||||
;;;
|
||||
;;; This file is part of GNU Guix.
|
||||
;;;
|
||||
|
@ -19,23 +19,40 @@
|
|||
(define-module (guix nar)
|
||||
#:use-module (guix utils)
|
||||
#:use-module (guix serialization)
|
||||
#:use-module ((guix build utils) #:select (with-directory-excursion))
|
||||
#:use-module ((guix build utils)
|
||||
#:select (delete-file-recursively with-directory-excursion))
|
||||
#:use-module (guix store)
|
||||
#:use-module (guix ui) ; for '_'
|
||||
#:use-module (guix hash)
|
||||
#:use-module (guix pki)
|
||||
#:use-module (guix pk-crypto)
|
||||
#:use-module (rnrs bytevectors)
|
||||
#:use-module (rnrs io ports)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-11)
|
||||
#:use-module (srfi srfi-26)
|
||||
#:use-module (srfi srfi-34)
|
||||
#:use-module (srfi srfi-35)
|
||||
#:use-module (ice-9 ftw)
|
||||
#:use-module (ice-9 match)
|
||||
#:export (nar-error?
|
||||
nar-error-port
|
||||
nar-error-file
|
||||
|
||||
nar-read-error?
|
||||
nar-read-error-file
|
||||
nar-read-error-port
|
||||
nar-read-error-token
|
||||
|
||||
nar-invalid-hash-error?
|
||||
nar-invalid-hash-error-expected
|
||||
nar-invalid-hash-error-actual
|
||||
|
||||
nar-signature-error?
|
||||
nar-signature-error-signature
|
||||
|
||||
write-file
|
||||
restore-file))
|
||||
restore-file
|
||||
|
||||
restore-file-set))
|
||||
|
||||
;;; Comment:
|
||||
;;;
|
||||
|
@ -44,15 +61,24 @@
|
|||
;;; Code:
|
||||
|
||||
(define-condition-type &nar-error &error ; XXX: inherit from &nix-error ?
|
||||
nar-error?)
|
||||
nar-error?
|
||||
(file nar-error-file) ; file we were restoring, or #f
|
||||
(port nar-error-port)) ; port from which we read
|
||||
|
||||
(define-condition-type &nar-read-error &nar-error
|
||||
nar-read-error?
|
||||
(port nar-read-error-port) ; port from which we read
|
||||
(file nar-read-error-file) ; file we were restoring, or #f
|
||||
(token nar-read-error-token)) ; faulty token, or #f
|
||||
|
||||
(define-condition-type &nar-signature-error &nar-error
|
||||
nar-signature-error?
|
||||
(signature nar-signature-error-signature)) ; faulty signature or #f
|
||||
|
||||
(define-condition-type &nar-invalid-hash-error &nar-signature-error
|
||||
nar-invalid-hash-error?
|
||||
(expected nar-invalid-hash-error-expected) ; expected hash (a bytevector)
|
||||
(actual nar-invalid-hash-error-actual)) ; actual hash
|
||||
|
||||
|
||||
(define (dump in out size)
|
||||
"Copy SIZE bytes from IN to OUT."
|
||||
(define buf-size 65536)
|
||||
|
@ -239,4 +265,191 @@ Restore it as FILE."
|
|||
(&message (message "unsupported nar entry type"))
|
||||
(&nar-read-error (port port) (file file) (token x))))))))
|
||||
|
||||
|
||||
;;;
|
||||
;;; Restoring a file set into the store.
|
||||
;;;
|
||||
|
||||
;; The code below accesses the store directly and is meant to be run from
|
||||
;; "build hooks", which cannot invoke the daemon's 'import-paths' RPC since
|
||||
;; (1) the locks on the files to be restored as already held, and (2) the
|
||||
;; $NIX_HELD_LOCKS hackish environment variable cannot be set.
|
||||
;;
|
||||
;; So we're really duplicating that functionality of the daemon (well, until
|
||||
;; most of the daemon is in Scheme :-)). But note that we do use a couple of
|
||||
;; RPCs for functionality not available otherwise, like 'valid-path?'.
|
||||
|
||||
(define (lock-store-file file)
|
||||
"Acquire exclusive access to FILE, a store file."
|
||||
(call-with-output-file (string-append file ".lock")
|
||||
(cut fcntl-flock <> 'write-lock)))
|
||||
|
||||
(define (unlock-store-file file)
|
||||
"Release access to FILE."
|
||||
(call-with-input-file (string-append file ".lock")
|
||||
(cut fcntl-flock <> 'unlock)))
|
||||
|
||||
(define* (finalize-store-file source target
|
||||
#:key (references '()) deriver (lock? #t))
|
||||
"Rename SOURCE to TARGET and register TARGET as a valid store item, with
|
||||
REFERENCES and DERIVER. When LOCK? is true, acquire exclusive locks on TARGET
|
||||
before attempting to register it; otherwise, assume TARGET's locks are already
|
||||
held."
|
||||
|
||||
;; XXX: Currently we have to call out to the daemon to check whether TARGET
|
||||
;; is valid.
|
||||
(with-store store
|
||||
(unless (valid-path? store target)
|
||||
(when lock?
|
||||
(lock-store-file target))
|
||||
|
||||
(unless (valid-path? store target)
|
||||
;; If FILE already exists, delete it (it's invalid anyway.)
|
||||
(when (file-exists? target)
|
||||
(delete-file-recursively target))
|
||||
|
||||
;; Install the new TARGET.
|
||||
(rename-file source target)
|
||||
|
||||
;; Register TARGET. As a side effect, it resets the timestamps of all
|
||||
;; its files, recursively. However, it doesn't attempt to deduplicate
|
||||
;; its files like 'importPaths' does (FIXME).
|
||||
(register-path target
|
||||
#:references references
|
||||
#:deriver deriver))
|
||||
|
||||
(when lock?
|
||||
(unlock-store-file target)))))
|
||||
|
||||
(define (temporary-store-directory)
|
||||
"Return the file name of a temporary directory created in the store that is
|
||||
protected from garbage collection."
|
||||
(let* ((template (string-append (%store-prefix) "/guix-XXXXXX"))
|
||||
(port (mkstemp! template)))
|
||||
(close-port port)
|
||||
(with-store store
|
||||
(add-temp-root store template))
|
||||
|
||||
;; There's a small window during which the GC could delete the file. Try
|
||||
;; again if that happens.
|
||||
(if (file-exists? template)
|
||||
(begin
|
||||
;; It's up to the caller to create that file or directory.
|
||||
(delete-file template)
|
||||
template)
|
||||
(temporary-store-directory))))
|
||||
|
||||
(define* (restore-file-set port
|
||||
#:key (verify-signature? #t) (lock? #t)
|
||||
(log-port (current-error-port)))
|
||||
"Restore the file set read from PORT to the store. The format of the data
|
||||
on PORT must be as created by 'export-paths'---i.e., a series of Nar-formatted
|
||||
archives with interspersed meta-data joining them together, possibly with a
|
||||
digital signature at the end. Log progress to LOG-PORT. Return the list of
|
||||
files restored.
|
||||
|
||||
When LOCK? is #f, assume locks for the files to be restored are already held.
|
||||
This is the case when the daemon calls a build hook.
|
||||
|
||||
Note that this procedure accesses the store directly, so it's only meant to be
|
||||
used by the daemon's build hooks since they cannot call back to the daemon
|
||||
while the locks are held."
|
||||
(define %export-magic
|
||||
;; Number used to identify genuine file set archives.
|
||||
#x4558494e)
|
||||
|
||||
(define port*
|
||||
;; Keep that one around, for error conditions.
|
||||
port)
|
||||
|
||||
(define (assert-valid-signature signature hash file)
|
||||
;; Bail out if SIGNATURE, an sexp, doesn't match HASH, a bytevector
|
||||
;; containing the expected hash for FILE.
|
||||
(let* ((signature (catch 'gcry-error
|
||||
(lambda ()
|
||||
(string->canonical-sexp signature))
|
||||
(lambda (err . _)
|
||||
(raise (condition
|
||||
(&message
|
||||
(message "signature is not a valid \
|
||||
s-expression"))
|
||||
(&nar-signature-error
|
||||
(file file)
|
||||
(signature signature) (port port)))))))
|
||||
(subject (signature-subject signature))
|
||||
(data (signature-signed-data signature)))
|
||||
(if (and data subject)
|
||||
(if (authorized-key? subject)
|
||||
(if (equal? (hash-data->bytevector data) hash)
|
||||
(unless (valid-signature? signature)
|
||||
(raise (condition
|
||||
(&message (message "invalid signature"))
|
||||
(&nar-signature-error
|
||||
(file file) (signature signature) (port port)))))
|
||||
(raise (condition (&message (message "invalid hash"))
|
||||
(&nar-invalid-hash-error
|
||||
(port port) (file file)
|
||||
(signature signature)
|
||||
(expected (hash-data->bytevector data))
|
||||
(actual hash)))))
|
||||
(raise (condition (&message (message "unauthorized public key"))
|
||||
(&nar-signature-error
|
||||
(signature signature) (file file) (port port)))))
|
||||
(raise (condition
|
||||
(&message (message "corrupt signature data"))
|
||||
(&nar-signature-error
|
||||
(signature signature) (file file) (port port)))))))
|
||||
|
||||
(let loop ((n (read-long-long port))
|
||||
(files '()))
|
||||
(case n
|
||||
((0)
|
||||
(reverse files))
|
||||
((1)
|
||||
(let-values (((port get-hash)
|
||||
(open-sha256-input-port port)))
|
||||
(let ((temp (temporary-store-directory)))
|
||||
(restore-file port temp)
|
||||
(let ((magic (read-int port)))
|
||||
(unless (= magic %export-magic)
|
||||
(raise (condition
|
||||
(&message (message "corrupt file set archive"))
|
||||
(&nar-read-error
|
||||
(port port*) (file #f) (token #f))))))
|
||||
|
||||
(let ((file (read-store-path port))
|
||||
(refs (read-store-path-list port))
|
||||
(deriver (read-string port))
|
||||
(hash (get-hash))
|
||||
(has-sig? (= 1 (read-int port))))
|
||||
(format log-port
|
||||
(_ "importing file or directory '~a'...~%")
|
||||
file)
|
||||
|
||||
(let ((sig (and has-sig? (read-string port))))
|
||||
(when verify-signature?
|
||||
(if sig
|
||||
(begin
|
||||
(assert-valid-signature sig hash file)
|
||||
(format log-port
|
||||
(_ "found valid signature for '~a'~%")
|
||||
file)
|
||||
(finalize-store-file temp file
|
||||
#:references refs
|
||||
#:deriver deriver
|
||||
#:lock? lock?)
|
||||
(loop (read-long-long port)
|
||||
(cons file files)))
|
||||
(raise (condition
|
||||
(&message (message "imported file lacks \
|
||||
a signature"))
|
||||
(&nar-signature-error
|
||||
(port port*) (file file) (signature #f)))))))))))
|
||||
(else
|
||||
;; Neither 0 nor 1.
|
||||
(raise (condition
|
||||
(&message (message "invalid inter-file archive mark"))
|
||||
(&nar-read-error
|
||||
(port port) (file #f) (token #f))))))))
|
||||
|
||||
;;; nar.scm ends here
|
||||
|
|
|
@ -5,11 +5,14 @@ DOMAIN = $(PACKAGE)
|
|||
subdir = po
|
||||
top_builddir = ..
|
||||
|
||||
# These options get passed to xgettext.
|
||||
# These options get passed to xgettext. We want to catch standard
|
||||
# gettext uses, package synopses and descriptions, and SRFI-34 error
|
||||
# condition messages.
|
||||
XGETTEXT_OPTIONS = \
|
||||
--language=Scheme --from-code=UTF-8 \
|
||||
--keyword=_ --keyword=N_ \
|
||||
--keyword=synopsis --keyword=description
|
||||
--keyword=synopsis --keyword=description \
|
||||
--keyword=message
|
||||
|
||||
COPYRIGHT_HOLDER = Ludovic Courtès
|
||||
|
||||
|
|
|
@ -15,3 +15,4 @@ guix/scripts/authenticate.scm
|
|||
guix/gnu-maintenance.scm
|
||||
guix/ui.scm
|
||||
guix/http-client.scm
|
||||
guix/nar.scm
|
||||
|
|
103
tests/nar.scm
103
tests/nar.scm
|
@ -1,5 +1,5 @@
|
|||
;;; GNU Guix --- Functional package management for GNU
|
||||
;;; Copyright © 2012, 2013 Ludovic Courtès <ludo@gnu.org>
|
||||
;;; Copyright © 2012, 2013, 2014 Ludovic Courtès <ludo@gnu.org>
|
||||
;;;
|
||||
;;; This file is part of GNU Guix.
|
||||
;;;
|
||||
|
@ -18,11 +18,17 @@
|
|||
|
||||
(define-module (test-nar)
|
||||
#:use-module (guix nar)
|
||||
#:use-module (guix store)
|
||||
#:use-module ((guix hash) #:select (open-sha256-input-port))
|
||||
#:use-module (rnrs bytevectors)
|
||||
#:use-module (rnrs io ports)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-26)
|
||||
#:use-module (srfi srfi-34)
|
||||
#:use-module (srfi srfi-35)
|
||||
#:use-module (srfi srfi-64)
|
||||
#:use-module (ice-9 ftw)
|
||||
#:use-module (ice-9 regex)
|
||||
#:use-module (ice-9 match))
|
||||
|
||||
;; Test the (guix nar) module.
|
||||
|
@ -156,6 +162,24 @@
|
|||
(string-append (dirname (search-path %load-path "pre-inst-env"))
|
||||
"/test-nar-" (number->string (getpid))))
|
||||
|
||||
;; XXX: Factorize.
|
||||
(define %seed
|
||||
(seed->random-state (logxor (getpid) (car (gettimeofday)))))
|
||||
|
||||
(define (random-text)
|
||||
(number->string (random (expt 2 256) %seed) 16))
|
||||
|
||||
(define-syntax-rule (let/ec k exp...)
|
||||
;; This one appeared in Guile 2.0.9, so provide a copy here.
|
||||
(let ((tag (make-prompt-tag)))
|
||||
(call-with-prompt tag
|
||||
(lambda ()
|
||||
(let ((k (lambda args
|
||||
(apply abort-to-prompt tag args))))
|
||||
exp...))
|
||||
(lambda (_ . args)
|
||||
(apply values args)))))
|
||||
|
||||
|
||||
(test-begin "nar")
|
||||
|
||||
|
@ -201,6 +225,83 @@
|
|||
(lambda ()
|
||||
(rmdir input)))))
|
||||
|
||||
;; 'restore-file-set' depends on 'open-sha256-input-port', which in turn
|
||||
;; relies on a Guile 2.0.10+ feature.
|
||||
(test-skip (if (false-if-exception
|
||||
(open-sha256-input-port (%make-void-port "r")))
|
||||
0
|
||||
3))
|
||||
|
||||
(test-assert "restore-file-set (signed, valid)"
|
||||
(with-store store
|
||||
(let* ((texts (unfold (cut >= <> 10)
|
||||
(lambda _ (random-text))
|
||||
1+
|
||||
0))
|
||||
(files (map (cut add-text-to-store store "text" <>) texts))
|
||||
(dump (call-with-bytevector-output-port
|
||||
(cut export-paths store files <>))))
|
||||
(delete-paths store files)
|
||||
(and (every (negate file-exists?) files)
|
||||
(let* ((source (open-bytevector-input-port dump))
|
||||
(imported (restore-file-set source)))
|
||||
(and (equal? imported files)
|
||||
(every (lambda (file)
|
||||
(and (file-exists? file)
|
||||
(valid-path? store file)))
|
||||
files)
|
||||
(equal? texts
|
||||
(map (lambda (file)
|
||||
(call-with-input-file file
|
||||
get-string-all))
|
||||
files))))))))
|
||||
|
||||
(test-assert "restore-file-set (missing signature)"
|
||||
(let/ec return
|
||||
(with-store store
|
||||
(let* ((file (add-text-to-store store "foo" "Hello, world!"))
|
||||
(dump (call-with-bytevector-output-port
|
||||
(cute export-paths store (list file) <>
|
||||
#:sign? #f))))
|
||||
(delete-paths store (list file))
|
||||
(and (not (file-exists? file))
|
||||
(let ((source (open-bytevector-input-port dump)))
|
||||
(guard (c ((nar-signature-error? c)
|
||||
(let ((message (condition-message c))
|
||||
(port (nar-error-port c)))
|
||||
(return
|
||||
(and (string-match "lacks.*signature" message)
|
||||
(string=? file (nar-error-file c))
|
||||
(eq? source port))))))
|
||||
(restore-file-set source))
|
||||
#f))))))
|
||||
|
||||
(test-assert "restore-file-set (corrupt)"
|
||||
(let/ec return
|
||||
(with-store store
|
||||
(let* ((file (add-text-to-store store "foo"
|
||||
(random-text)))
|
||||
(dump (call-with-bytevector-output-port
|
||||
(cute export-paths store (list file) <>))))
|
||||
(delete-paths store (list file))
|
||||
|
||||
;; Flip a byte in the file contents.
|
||||
(let* ((index 120)
|
||||
(byte (bytevector-u8-ref dump index)))
|
||||
(bytevector-u8-set! dump index (logxor #xff byte)))
|
||||
|
||||
(and (not (file-exists? file))
|
||||
(let ((source (open-bytevector-input-port dump)))
|
||||
(guard (c ((nar-invalid-hash-error? c)
|
||||
(let ((message (condition-message c))
|
||||
(port (nar-error-port c)))
|
||||
(return
|
||||
(and (string-contains message "hash")
|
||||
(string=? file (nar-error-file c))
|
||||
(eq? source port))))))
|
||||
(restore-file-set source))
|
||||
#f))))))
|
||||
|
||||
(test-end "nar")
|
||||
|
||||
|
||||
|
|
Reference in New Issue