summaryrefslogtreecommitdiff
path: root/guix
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2014-01-25 17:07:21 +0100
committerLudovic Courtès <ludo@gnu.org>2014-01-25 17:07:21 +0100
commit200a97e64f29dc904961e99bcbc0f20fef431dd2 (patch)
tree4b8d5c809925320e74efb8c9657037ee6f00d718 /guix
parentfcaa7523d4f37d5b3c4bf459784e826f98252fe8 (diff)
parent1909431c5b6413c496eb93d3d74be3e3e936951b (diff)
Merge branch 'master' into core-updates
Diffstat (limited to 'guix')
-rw-r--r--guix/build/linux-initrd.scm6
-rw-r--r--guix/config.scm.in5
-rw-r--r--guix/derivations.scm44
-rw-r--r--guix/hash.scm42
-rw-r--r--guix/licenses.scm8
-rw-r--r--guix/nar.scm229
-rw-r--r--guix/pki.scm23
-rw-r--r--guix/profiles.scm4
-rw-r--r--guix/scripts/authenticate.scm24
-rw-r--r--guix/scripts/offload.scm380
-rw-r--r--guix/scripts/package.scm5
-rwxr-xr-xguix/scripts/substitute-binary.scm25
-rw-r--r--guix/store.scm82
-rw-r--r--guix/ui.scm16
-rw-r--r--guix/utils.scm66
15 files changed, 899 insertions, 60 deletions
diff --git a/guix/build/linux-initrd.scm b/guix/build/linux-initrd.scm
index cbdb363b4e..ae18a16e11 100644
--- a/guix/build/linux-initrd.scm
+++ b/guix/build/linux-initrd.scm
@@ -1,5 +1,5 @@
;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013, 2014 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -95,7 +95,9 @@
;; Other useful nodes.
(mknod (scope "dev/null") 'char-special #o666 (device-number 1 3))
- (mknod (scope "dev/zero") 'char-special #o666 (device-number 1 5)))
+ (mknod (scope "dev/zero") 'char-special #o666 (device-number 1 5))
+ (chmod (scope "dev/null") #o666)
+ (chmod (scope "dev/zero") #o666))
(define %host-qemu-ipv4-address
(inet-pton AF_INET "10.0.2.10"))
diff --git a/guix/config.scm.in b/guix/config.scm.in
index 3a5c50e00a..5edb4ced30 100644
--- a/guix/config.scm.in
+++ b/guix/config.scm.in
@@ -24,6 +24,7 @@
%store-directory
%state-directory
%config-directory
+ %guix-register-program
%system
%libgcrypt
%nixpkgs
@@ -62,6 +63,10 @@
;; This must match `NIX_CONF_DIR' as defined in `daemon.am'.
(or (getenv "NIX_CONF_DIR") "@guix_sysconfdir@/guix"))
+(define %guix-register-program
+ ;; The 'guix-register' program.
+ (or (getenv "GUIX_REGISTER") "@guix_sbindir@/guix-register"))
+
(define %system
"@guix_system@")
diff --git a/guix/derivations.scm b/guix/derivations.scm
index 3d9f0affbf..cc8e37c973 100644
--- a/guix/derivations.scm
+++ b/guix/derivations.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.
;;;
@@ -532,7 +532,8 @@ the derivation called NAME with hash HASH."
(system (%current-system)) (env-vars '())
(inputs '()) (outputs '("out"))
hash hash-algo hash-mode
- references-graphs)
+ references-graphs
+ local-build?)
"Build a derivation with the given arguments, and return the resulting
<derivation> object. When HASH, HASH-ALGO, and HASH-MODE are given, a
fixed-output derivation is created---i.e., one whose result is known in
@@ -540,7 +541,11 @@ advance, such as a file download.
When REFERENCES-GRAPHS is true, it must be a list of file name/store path
pairs. In that case, the reference graph of each store path is exported in
-the build environment in the corresponding file, in a simple text format."
+the build environment in the corresponding file, in a simple text format.
+
+When LOCAL-BUILD? is true, declare that the derivation is not a good candidate
+for offloading and should rather be built locally. This is the case for small
+derivations where the costs of data transfers would outweigh the benefits."
(define (add-output-paths drv)
;; Return DRV with an actual store path for each of its output and the
;; corresponding environment variable.
@@ -571,16 +576,20 @@ the build environment in the corresponding file, in a simple text format."
;; Some options are passed to the build daemon via the env. vars of
;; derivations (urgh!). We hide that from our API, but here is the place
;; where we kludgify those options.
- (match references-graphs
- (((file . path) ...)
- (let ((value (map (cut string-append <> " " <>)
- file path)))
- ;; XXX: This all breaks down if an element of FILE or PATH contains
- ;; white space.
- `(("exportReferencesGraph" . ,(string-join value " "))
- ,@env-vars)))
- (#f
- env-vars)))
+ (let ((env-vars (if local-build?
+ `(("preferLocalBuild" . "1")
+ ,@env-vars)
+ env-vars)))
+ (match references-graphs
+ (((file . path) ...)
+ (let ((value (map (cut string-append <> " " <>)
+ file path)))
+ ;; XXX: This all breaks down if an element of FILE or PATH contains
+ ;; white space.
+ `(("exportReferencesGraph" . ,(string-join value " "))
+ ,@env-vars)))
+ (#f
+ env-vars))))
(define (env-vars-with-empty-outputs env-vars)
;; Return a variant of ENV-VARS where each OUTPUTS is associated with an
@@ -904,7 +913,8 @@ they can refer to each other."
(env-vars '())
(modules '())
guile-for-build
- references-graphs)
+ references-graphs
+ local-build?)
"Return a derivation that executes Scheme expression EXP as a builder
for derivation NAME. INPUTS must be a list of (NAME DRV-PATH SUB-DRV)
tuples; when SUB-DRV is omitted, \"out\" is assumed. MODULES is a list
@@ -923,7 +933,8 @@ EXP returns #f, the build is considered to have failed.
EXP is built using GUILE-FOR-BUILD (a derivation). When GUILE-FOR-BUILD is
omitted or is #f, the value of the `%guile-for-build' fluid is used instead.
-See the `derivation' procedure for the meaning of REFERENCES-GRAPHS."
+See the `derivation' procedure for the meaning of REFERENCES-GRAPHS and
+LOCAL-BUILD?."
(define guile-drv
(or guile-for-build (%guile-for-build)))
@@ -1046,4 +1057,5 @@ See the `derivation' procedure for the meaning of REFERENCES-GRAPHS."
#:hash hash #:hash-algo hash-algo
#:outputs outputs
- #:references-graphs references-graphs)))
+ #:references-graphs references-graphs
+ #:local-build? local-build?)))
diff --git a/guix/hash.scm b/guix/hash.scm
index 92ecaf78d5..fb85f47586 100644
--- a/guix/hash.scm
+++ b/guix/hash.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.
;;;
@@ -25,7 +25,8 @@
#:use-module (srfi srfi-11)
#:export (sha256
open-sha256-port
- port-sha256))
+ port-sha256
+ open-sha256-input-port))
;;; Commentary:
;;;
@@ -128,4 +129,41 @@ output port."
(close-port out)
(get)))
+(define (open-sha256-input-port port)
+ "Return an input port that wraps PORT and a thunk to get the hash of all the
+data read from PORT. The thunk always returns the same value."
+ (define md
+ (open-sha256-md))
+
+ (define (read! bv start count)
+ (let ((n (get-bytevector-n! port bv start count)))
+ (if (eof-object? n)
+ 0
+ (begin
+ (unless digest
+ (let ((ptr (bytevector->pointer bv start)))
+ (md-write md ptr n)))
+ n))))
+
+ (define digest #f)
+
+ (define (finalize!)
+ (let ((ptr (md-read md 0)))
+ (set! digest (bytevector-copy (pointer->bytevector ptr 32)))
+ (md-close md)))
+
+ (define (get-hash)
+ (unless digest
+ (finalize!))
+ digest)
+
+ (define (unbuffered port)
+ ;; Guile <= 2.0.9 does not support 'setvbuf' on custom binary input ports.
+ ;; If you get a wrong-type-arg error here, the fix is to upgrade Guile. :-)
+ (setvbuf port _IONBF)
+ port)
+
+ (values (unbuffered (make-custom-binary-input-port "sha256" read! #f #f #f))
+ get-hash))
+
;;; hash.scm ends here
diff --git a/guix/licenses.scm b/guix/licenses.scm
index c0a0e60b36..5f1b3c16cf 100644
--- a/guix/licenses.scm
+++ b/guix/licenses.scm
@@ -1,5 +1,5 @@
;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2012, 2014 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2013 Andreas Enge <andreas@enge.fr>
;;; Copyright © 2012, 2013 Nikita Karetnikov <nikita@karetnikov.org>
;;;
@@ -31,6 +31,7 @@
expat
freetype
gpl1 gpl1+ gpl2 gpl2+ gpl3 gpl3+
+ fdl1.3+
isc
ijg
ibmpl1.0
@@ -161,6 +162,11 @@ which may be a file:// URI pointing the package's tree."
"https://www.gnu.org/licenses/gpl.html"
"https://www.gnu.org/licenses/license-list#GNUGPLv3"))
+(define fdl1.3+
+ (license "FDL 1.3+"
+ "https://www.gnu.org/licenses/fdl.html"
+ "https://www.gnu.org/licenses/license-list#FDL"))
+
(define isc
(license "ISC"
"http://directory.fsf.org/wiki/License:ISC"
diff --git a/guix/nar.scm b/guix/nar.scm
index ea119a25fe..4bc2deb229 100644
--- a/guix/nar.scm
+++ b/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
diff --git a/guix/pki.scm b/guix/pki.scm
index 5e4dbadd35..4b90b65a13 100644
--- a/guix/pki.scm
+++ b/guix/pki.scm
@@ -29,8 +29,12 @@
current-acl
public-keys->acl
acl->public-keys
+ authorized-key?
+
signature-sexp
- authorized-key?))
+ signature-subject
+ signature-signed-data
+ valid-signature?))
;;; Commentary:
;;;
@@ -136,4 +140,21 @@ PUBLIC-KEY (see <http://theworld.com/~cme/spki.txt> for examples.)"
(canonical-sexp->string (sign data secret-key))
(canonical-sexp->string public-key))))
+(define (signature-subject sig)
+ "Return the signer's public key for SIG."
+ (find-sexp-token sig 'public-key))
+
+(define (signature-signed-data sig)
+ "Return the signed data from SIG, typically an sexp such as
+ (hash \"sha256\" #...#)."
+ (find-sexp-token sig 'data))
+
+(define (valid-signature? sig)
+ "Return #t if SIG is valid."
+ (let* ((data (signature-signed-data sig))
+ (signature (find-sexp-token sig 'sig-val))
+ (public-key (signature-subject sig)))
+ (and data signature
+ (verify signature data public-key))))
+
;;; pki.scm ends here
diff --git a/guix/profiles.scm b/guix/profiles.scm
index 9b5c5f515c..1ff6c97f9f 100644
--- a/guix/profiles.scm
+++ b/guix/profiles.scm
@@ -1,5 +1,5 @@
;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013, 2014 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2013 Nikita Karetnikov <nikita@karetnikov.org>
;;;
;;; This file is part of GNU Guix.
@@ -238,8 +238,6 @@ the given MANIFEST."
(let ((output (assoc-ref %outputs "out"))
(inputs (map cdr %build-inputs)))
- (format #t "building profile '~a' with ~a packages...~%"
- output (length inputs))
(union-build output inputs
#:log-port (%make-void-port "w"))
(call-with-output-file (string-append output "/manifest")
diff --git a/guix/scripts/authenticate.scm b/guix/scripts/authenticate.scm
index c7a14f7a8b..27580dedff 100644
--- a/guix/scripts/authenticate.scm
+++ b/guix/scripts/authenticate.scm
@@ -1,5 +1,5 @@
;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013, 2014 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -72,23 +72,21 @@
;; Read the signature as produced above, check whether its public key is
;; authorized, and verify the signature, and print the signed data to
;; stdout upon success.
- (let* ((sig+data (read-canonical-sexp signature-file))
- (public-key (find-sexp-token sig+data 'public-key))
- (data (find-sexp-token sig+data 'data))
- (signature (find-sexp-token sig+data 'sig-val)))
- (if (and data signature)
- (if (authorized-key? public-key)
- (if (verify signature data public-key)
- (begin
- (display (bytevector->base16-string
- (hash-data->bytevector data)))
+ (let* ((signature (read-canonical-sexp signature-file))
+ (subject (signature-subject signature))
+ (data (signature-signed-data signature)))
+ (if (and data subject)
+ (if (authorized-key? subject)
+ (if (valid-signature? signature)
+ (let ((hash (hash-data->bytevector data)))
+ (display (bytevector->base16-string hash))
#t) ; success
(leave (_ "error: invalid signature: ~a~%")
(canonical-sexp->string signature)))
(leave (_ "error: unauthorized public key: ~a~%")
- (canonical-sexp->string public-key)))
+ (canonical-sexp->string subject)))
(leave (_ "error: corrupt signature data: ~a~%")
- (canonical-sexp->string sig+data)))))
+ (canonical-sexp->string signature)))))
(("--help")
(display (_ "Usage: guix authenticate OPTION...
Sign or verify the signature on the given file. This tool is meant to
diff --git a/guix/scripts/offload.scm b/guix/scripts/offload.scm
new file mode 100644
index 0000000000..d919ede3c7
--- /dev/null
+++ b/guix/scripts/offload.scm
@@ -0,0 +1,380 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2014 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 (guix scripts offload)
+ #:use-module (guix config)
+ #:use-module (guix records)
+ #:use-module (guix store)
+ #:use-module (guix derivations)
+ #:use-module (guix nar)
+ #:use-module (guix utils)
+ #:use-module ((guix build utils) #:select (which))
+ #:use-module (guix ui)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (ice-9 popen)
+ #:use-module (ice-9 rdelim)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 regex)
+ #:use-module (ice-9 format)
+ #:use-module (rnrs io ports)
+ #:export (build-machine
+ build-requirements
+ guix-offload))
+
+;;; Commentary:
+;;;
+;;; Attempt to offload builds to the machines listed in
+;;; /etc/guix/machines.scm, transferring missing dependencies over SSH, and
+;;; retrieving the build output(s) over SSH upon success.
+;;;
+;;; This command should not be used directly; instead, it is called on-demand
+;;; by the daemon, unless it was started with '--no-build-hook' or a client
+;;; inhibited build hooks.
+;;;
+;;; Code:
+
+
+(define-record-type* <build-machine>
+ build-machine make-build-machine
+ build-machine?
+ (name build-machine-name) ; string
+ (system build-machine-system) ; string
+ (user build-machine-user) ; string
+ (private-key build-machine-private-key ; file name
+ (default (user-lsh-private-key)))
+ (parallel-builds build-machine-parallel-builds ; number
+ (default 1))
+ (speed build-machine-speed ; inexact real
+ (default 1.0))
+ (features build-machine-features ; list of strings
+ (default '())))
+
+(define-record-type* <build-requirements>
+ build-requirements make-build-requirements
+ build-requirements?
+ (system build-requirements-system) ; string
+ (features build-requirements-features ; list of strings
+ (default '())))
+
+(define %machine-file
+ ;; File that lists machines available as build slaves.
+ (string-append %config-directory "/machines.scm"))
+
+(define %lsh-command
+ "lsh")
+
+(define %lshg-command
+ ;; FIXME: 'lshg' fails to pass large amounts of data, see
+ ;; <http://lists.lysator.liu.se/pipermail/lsh-bugs/2014q1/000639.html>.
+ "lsh")
+
+(define (user-lsh-private-key)
+ "Return the user's default lsh private key, or #f if it could not be
+determined."
+ (and=> (getenv "HOME")
+ (cut string-append <> "/.lsh/identity")))
+
+(define %user-module
+ ;; Module in which the machine description file is loaded.
+ (let ((module (make-fresh-user-module)))
+ (module-use! module (resolve-interface '(guix scripts offload)))
+ module))
+
+(define* (build-machines #:optional (file %machine-file))
+ "Read the list of build machines from FILE and return it."
+ (catch #t
+ (lambda ()
+ ;; Avoid ABI incompatibility with the <build-machine> record.
+ (set! %fresh-auto-compile #t)
+
+ (save-module-excursion
+ (lambda ()
+ (set-current-module %user-module)
+ (primitive-load %machine-file))))
+ (lambda args
+ (match args
+ (('system-error . _)
+ (let ((err (system-error-errno args)))
+ ;; Silently ignore missing file since this is a common case.
+ (if (= ENOENT err)
+ '()
+ (leave (_ "failed to open machine file '~a': ~a~%")
+ %machine-file (strerror err)))))
+ (_
+ (leave (_ "failed to load machine file '~a': ~s~%")
+ %machine-file args))))))
+
+(define (open-ssh-gateway machine)
+ "Initiate an SSH connection gateway to MACHINE, and return the PID of the
+running lsh gateway upon success, or #f on failure."
+ (catch 'system-error
+ (lambda ()
+ (let* ((port (open-pipe* OPEN_READ %lsh-command
+ "-l" (build-machine-user machine)
+ "-i" (build-machine-private-key machine)
+ ;; XXX: With lsh 2.1, passing '--write-pid'
+ ;; last causes the PID not to be printed.
+ "--write-pid" "--gateway" "--background" "-z"
+ (build-machine-name machine)))
+ (line (read-line port))
+ (status (close-pipe port)))
+ (if (zero? status)
+ (let ((pid (string->number line)))
+ (if (integer? pid)
+ pid
+ (begin
+ (warning (_ "'~a' did not write its PID on stdout: ~s~%")
+ %lsh-command line)
+ #f)))
+ (begin
+ (warning (_ "failed to initiate SSH connection to '~a':\
+ '~a' exited with ~a~%")
+ (build-machine-name machine)
+ %lsh-command
+ (status:exit-val status))
+ #f))))
+ (lambda args
+ (leave (_ "failed to execute '~a': ~a~%")
+ %lsh-command (strerror (system-error-errno args))))))
+
+(define (remote-pipe machine mode command)
+ "Run COMMAND on MACHINE, assuming an lsh gateway has been set up."
+ (catch 'system-error
+ (lambda ()
+ (apply open-pipe* mode %lshg-command
+ "-l" (build-machine-user machine) "-z"
+ (build-machine-name machine)
+ command))
+ (lambda args
+ (warning (_ "failed to execute '~a': ~a~%")
+ %lshg-command (strerror (system-error-errno args)))
+ #f)))
+
+(define* (offload drv machine
+ #:key print-build-trace? (max-silent-time 3600)
+ (build-timeout 7200))
+ "Perform DRV on MACHINE, assuming DRV and its prerequisites are available
+there. Return a read pipe from where to read the build log."
+ (format (current-error-port) "offloading '~a' to '~a'...~%"
+ (derivation-file-name drv) (build-machine-name machine))
+ (format (current-error-port) "@ build-remote ~a ~a~%"
+ (derivation-file-name drv) (build-machine-name machine))
+
+ ;; FIXME: Protect DRV from garbage collection on MACHINE.
+ (let ((pipe (remote-pipe machine OPEN_READ
+ `("guix" "build"
+ ;; FIXME: more options
+ ,(format #f "--max-silent-time=~a"
+ max-silent-time)
+ ,(derivation-file-name drv)))))
+ pipe))
+
+(define (send-files files machine)
+ "Send the subset of FILES that's missing to MACHINE's store. Return #t on
+success, #f otherwise."
+ (define (missing-files files)
+ ;; Return the subset of FILES not already on MACHINE.
+ (let* ((files (format #f "~{~a~%~}" files))
+ (missing (filtered-port
+ (list (which %lshg-command)
+ "-l" (build-machine-user machine)
+ "-i" (build-machine-private-key machine)
+ (build-machine-name machine)
+ "guix" "archive" "--missing")
+ (open-input-string files))))
+ (string-tokenize (get-string-all missing))))
+
+ (with-store store
+ (guard (c ((nix-protocol-error? c)
+ (warning (_ "failed to export files for '~a': ~s~%")
+ (build-machine-name machine)
+ c)
+ (false-if-exception (close-pipe pipe))
+ #f))
+
+ ;; Compute the subset of FILES missing on MACHINE, and send them in
+ ;; topologically sorted order so that they can actually be imported.
+ (let ((files (missing-files (topologically-sorted store files)))
+ (pipe (remote-pipe machine OPEN_WRITE
+ '("guix" "archive" "--import"))))
+ (format #t (_ "sending ~a store files to '~a'...~%")
+ (length files) (build-machine-name machine))
+ (catch 'system-error
+ (lambda ()
+ (export-paths store files pipe))
+ (lambda args
+ (warning (_ "failed while exporting files to '~a': ~a~%")
+ (build-machine-name machine)
+ (strerror (system-error-errno args)))))
+ (zero? (close-pipe pipe))))))
+
+(define (retrieve-files files machine)
+ "Retrieve FILES from MACHINE's store, and import them."
+ (define host
+ (build-machine-name machine))
+
+ (let ((pipe (remote-pipe machine OPEN_READ
+ `("guix" "archive" "--export" ,@files))))
+ (and pipe
+ (with-store store
+ (guard (c ((nix-protocol-error? c)
+ (warning (_ "failed to import files from '~a': ~s~%")
+ host c)
+ #f))
+ (format (current-error-port) "retrieving ~a files from '~a'...~%"
+ (length files) host)
+
+ ;; We cannot use the 'import-paths' RPC here because we already
+ ;; hold the locks for FILES.
+ (restore-file-set pipe
+ #:log-port (current-error-port)
+ #:lock? #f)
+
+ (zero? (close-pipe pipe)))))))
+
+(define (machine-matches? machine requirements)
+ "Return #t if MACHINE matches REQUIREMENTS."
+ (and (string=? (build-requirements-system requirements)
+ (build-machine-system machine))
+ (lset<= string=?
+ (build-requirements-features requirements)
+ (build-machine-features machine))))
+
+(define (machine-faster? m1 m2)
+ "Return #t if M1 is faster than M2."
+ (> (build-machine-speed m1) (build-machine-speed m2)))
+
+(define (choose-build-machine requirements machines)
+ "Return the best machine among MACHINES fulfilling REQUIREMENTS, or #f."
+ ;; FIXME: Take machine load into account, and/or shuffle MACHINES.
+ (let ((machines (sort (filter (cut machine-matches? <> requirements)
+ machines)
+ machine-faster?)))
+ (match machines
+ ((head . _)
+ head)
+ (_ #f))))
+
+(define* (process-request wants-local? system drv features
+ #:key
+ print-build-trace? (max-silent-time 3600)
+ (build-timeout 7200))
+ "Process a request to build DRV."
+ (let* ((local? (and wants-local? (string=? system (%current-system))))
+ (reqs (build-requirements
+ (system system)
+ (features features)))
+ (machine (choose-build-machine reqs (build-machines))))
+ (if machine
+ (match (open-ssh-gateway machine)
+ ((? integer? pid)
+ (display "# accept\n")
+ (let ((inputs (string-tokenize (read-line)))
+ (outputs (string-tokenize (read-line))))
+ (when (send-files (cons (derivation-file-name drv) inputs)
+ machine)
+ (let ((log (offload drv machine
+ #:print-build-trace? print-build-trace?
+ #:max-silent-time max-silent-time
+ #:build-timeout build-timeout)))
+ (let loop ((line (read-line log)))
+ (if (eof-object? line)
+ (close-pipe log)
+ (begin
+ (display line) (newline)
+ (loop (read-line log))))))
+ (retrieve-files outputs machine)))
+ (format (current-error-port) "done with offloaded '~a'~%"
+ (derivation-file-name drv))
+ (kill pid SIGTERM))
+ (#f
+ (display "# decline\n")))
+ (display "# decline\n"))))
+
+(define-syntax-rule (with-nar-error-handling body ...)
+ "Execute BODY with any &nar-error suitably reported to the user."
+ (guard (c ((nar-error? c)
+ (let ((file (nar-error-file c)))
+ (if (condition-has-type? c &message)
+ (leave (_ "while importing file '~a': ~a~%")
+ file (gettext (condition-message c)))
+ (leave (_ "failed to import file '~a'~%")
+ file)))))
+ body ...))
+
+
+;;;
+;;; Entry point.
+;;;
+
+(define (guix-offload . args)
+ (define request-line-rx
+ ;; The request format. See 'tryBuildHook' method in build.cc.
+ (make-regexp "([01]) ([a-z0-9_-]+) (/[[:graph:]]+.drv) ([[:graph:]]*)"))
+
+ (define not-coma
+ (char-set-complement (char-set #\,)))
+
+ ;; Make sure $HOME really corresponds to the current user. This is
+ ;; necessary since lsh uses that to determine the location of the yarrow
+ ;; seed file, and fails if it's owned by someone else.
+ (and=> (passwd:dir (getpw (getuid)))
+ (cut setenv "HOME" <>))
+
+ (match args
+ ((system max-silent-time print-build-trace? build-timeout)
+ (let ((max-silent-time (string->number max-silent-time))
+ (build-timeout (string->number build-timeout))
+ (print-build-trace? (string=? print-build-trace? "1")))
+ (parameterize ((%current-system system))
+ (let loop ((line (read-line)))
+ (unless (eof-object? line)
+ (cond ((regexp-exec request-line-rx line)
+ =>
+ (lambda (match)
+ (with-nar-error-handling
+ (process-request (equal? (match:substring match 1) "1")
+ (match:substring match 2) ; system
+ (call-with-input-file
+ (match:substring match 3)
+ read-derivation)
+ (string-tokenize
+ (match:substring match 4) not-coma)
+ #:print-build-trace? print-build-trace?
+ #:max-silent-time max-silent-time
+ #:build-timeout build-timeout))))
+ (else
+ (leave (_ "invalid request line: ~s~%") line)))
+ (loop (read-line)))))))
+ (("--version")
+ (show-version-and-exit "guix offload"))
+ (("--help")
+ (format #t (_ "Usage: guix offload SYSTEM PRINT-BUILD-TRACE
+Process build offload requests written on the standard input, possibly
+offloading builds to the machines listed in '~a'.~%")
+ %machine-file)
+ (display (_ "
+This tool is meant to be used internally by 'guix-daemon'.\n"))
+ (show-bug-report-information))
+ (x
+ (leave (_ "invalid arguments: ~{~s ~}~%") x))))
+
+;;; offload.scm ends here
diff --git a/guix/scripts/package.scm b/guix/scripts/package.scm
index 04393abc9a..d41a83de8a 100644
--- a/guix/scripts/package.scm
+++ b/guix/scripts/package.scm
@@ -1032,8 +1032,9 @@ more information.~%"))
(('search regexp)
(let ((regexp (make-regexp regexp regexp/icase)))
- (for-each (cute package->recutils <> (current-output-port))
- (find-packages-by-description regexp))
+ (leave-on-EPIPE
+ (for-each (cute package->recutils <> (current-output-port))
+ (find-packages-by-description regexp)))
#t))
(('search-paths)
diff --git a/guix/scripts/substitute-binary.scm b/guix/scripts/substitute-binary.scm
index 901b3fb064..3aaa1c4284 100755
--- a/guix/scripts/substitute-binary.scm
+++ b/guix/scripts/substitute-binary.scm
@@ -1,5 +1,5 @@
;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013, 2014 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -486,6 +486,29 @@ Internal tool to substitute a pre-built binary to a local build.\n"))
"Implement the build daemon's substituter protocol."
(mkdir-p %narinfo-cache-directory)
(maybe-remove-expired-cached-narinfo)
+
+ ;; Starting from commit 22144afa in Nix, we are allowed to bail out directly
+ ;; when we know we cannot substitute, but we must emit a newline on stdout
+ ;; when everything is alright.
+ (let ((uri (string->uri %cache-url)))
+ (case (uri-scheme uri)
+ ((http)
+ ;; Exit gracefully if there's no network access.
+ (let ((host (uri-host uri)))
+ (catch 'getaddrinfo-error
+ (lambda ()
+ (getaddrinfo host))
+ (lambda (key error)
+ (warning (_ "failed to look up host '~a' (~a), \
+substituter disabled~%")
+ host (gai-strerror error))
+ (exit 0)))))
+ (else #t)))
+
+ ;; Say hello (see above.)
+ (newline)
+ (force-output (current-output-port))
+
(with-networking
(match args
(("--query")
diff --git a/guix/store.scm b/guix/store.scm
index 1012480b39..eca0de7d97 100644
--- a/guix/store.scm
+++ b/guix/store.scm
@@ -33,6 +33,7 @@
#:use-module (ice-9 match)
#:use-module (ice-9 regex)
#:use-module (ice-9 vlist)
+ #:use-module (ice-9 popen)
#:export (%daemon-socket-file
nix-server?
@@ -52,6 +53,7 @@
open-connection
close-connection
+ with-store
set-build-options
valid-path?
query-path-hash
@@ -74,6 +76,7 @@
references
requisites
referrers
+ topologically-sorted
valid-derivers
query-derivation-outputs
live-paths
@@ -85,6 +88,8 @@
current-build-output-port
+ register-path
+
%store-prefix
store-path?
direct-store-path?
@@ -320,6 +325,17 @@ operate, should the disk become full. Return a server object."
"Close the connection to SERVER."
(close (nix-server-socket server)))
+(define-syntax-rule (with-store store exp ...)
+ "Bind STORE to an open connection to the store and evaluate EXPs;
+automatically close the store when the dynamic extent of EXP is left."
+ (let ((store (open-connection)))
+ (dynamic-wind
+ (const #f)
+ (lambda ()
+ exp ...)
+ (lambda ()
+ (false-if-exception (close-connection store))))))
+
(define current-build-output-port
;; The port where build output is sent.
(make-parameter (current-error-port)))
@@ -360,11 +376,11 @@ encoding conversion errors."
(nix-server-socket server))
;; magic cookies from worker-protocol.hh
- (define %stderr-next #x6f6c6d67)
- (define %stderr-read #x64617461) ; data needed from source
- (define %stderr-write #x64617416) ; data for sink
- (define %stderr-last #x616c7473)
- (define %stderr-error #x63787470)
+ (define %stderr-next #x6f6c6d67) ; "olmg", build log
+ (define %stderr-read #x64617461) ; "data", data needed from source
+ (define %stderr-write #x64617416) ; "dat\x16", data for sink
+ (define %stderr-last #x616c7473) ; "alts", we're done
+ (define %stderr-error #x63787470) ; "cxtp", error reporting
(let ((k (read-int p)))
(cond ((= k %stderr-write)
@@ -574,6 +590,40 @@ SEED."
references, recursively)."
(fold-path store cons '() path))
+(define (topologically-sorted store paths)
+ "Return a list containing PATHS and all their references sorted in
+topological order."
+ (define (traverse)
+ ;; Do a simple depth-first traversal of all of PATHS.
+ (let loop ((paths paths)
+ (visited vlist-null)
+ (result '()))
+ (define (visit n)
+ (vhash-cons n #t visited))
+
+ (define (visited? n)
+ (vhash-assoc n visited))
+
+ (match paths
+ ((head tail ...)
+ (if (visited? head)
+ (loop tail visited result)
+ (call-with-values
+ (lambda ()
+ (loop (references store head)
+ (visit head)
+ result))
+ (lambda (visited result)
+ (loop tail
+ visited
+ (cons head result))))))
+ (()
+ (values visited result)))))
+
+ (call-with-values traverse
+ (lambda (_ result)
+ (reverse result))))
+
(define referrers
(operation (query-referrers (store-path path))
"Return the list of path that refer to PATH."
@@ -694,6 +744,28 @@ is true."
(and (export-path server head port #:sign? sign?)
(loop tail)))))))
+(define* (register-path path
+ #:key (references '()) deriver)
+ "Register PATH as a valid store file, with REFERENCES as its list of
+references, and DERIVER as its deriver (.drv that led to it.) Return #t on
+success.
+
+Use with care as it directly modifies the store! This is primarily meant to
+be used internally by the daemon's build hook."
+ ;; Currently this is implemented by calling out to the fine C++ blob.
+ (catch 'system-error
+ (lambda ()
+ (let ((pipe (open-pipe* OPEN_WRITE %guix-register-program)))
+ (and pipe
+ (begin
+ (format pipe "~a~%~a~%~a~%"
+ path (or deriver "") (length references))
+ (for-each (cut format pipe "~a~%" <>) references)
+ (zero? (close-pipe pipe))))))
+ (lambda args
+ ;; Failed to run %GUIX-REGISTER-PROGRAM.
+ #f)))
+
;;;
;;; Store paths.
diff --git a/guix/ui.scm b/guix/ui.scm
index f15419f7a8..d6058f806b 100644
--- a/guix/ui.scm
+++ b/guix/ui.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>
;;; Copyright © 2013 Mark H Weaver <mhw@netris.org>
;;; Copyright © 2013 Nikita Karetnikov <nikita@karetnikov.org>
;;;
@@ -138,7 +138,7 @@ messages."
"Display version information for COMMAND and `(exit 0)'."
(simple-format #t "~a (~a) ~a~%"
command %guix-package-name %guix-version)
- (display (_ "Copyright (C) 2013 the Guix authors
+ (display (_ "Copyright (C) 2014 the Guix authors
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
@@ -404,7 +404,11 @@ WIDTH columns."
(format port "location: ~a~%"
(or (and=> (package-location p) location->string)
(_ "unknown")))
- (format port "home-page: ~a~%" (package-home-page p))
+
+ ;; Note: Starting from version 1.6 or recutils, hyphens are not allowed in
+ ;; field identifiers.
+ (format port "homepage: ~a~%" (package-home-page p))
+
(format port "license: ~a~%"
(match (package-license p)
(((? license? licenses) ...)
@@ -554,13 +558,17 @@ reporting."
(command-files)))
(define (show-guix-help)
+ (define (internal? command)
+ (member command '("substitute-binary" "authenticate" "offload")))
+
(format #t (_ "Usage: guix COMMAND ARGS...
Run COMMAND with ARGS.\n"))
(newline)
(format #t (_ "COMMAND must be one of the sub-commands listed below:\n"))
(newline)
;; TODO: Display a synopsis of each command.
- (format #t "~{ ~a~%~}" (sort (commands) string<?))
+ (format #t "~{ ~a~%~}" (sort (remove internal? (commands))
+ string<?))
(show-bug-report-information))
(define program-name
diff --git a/guix/utils.scm b/guix/utils.scm
index 04a74ee29a..5fda2116de 100644
--- a/guix/utils.scm
+++ b/guix/utils.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>
;;; Copyright © 2013 Mark H Weaver <mhw@netris.org>
;;;
;;; This file is part of GNU Guix.
@@ -34,7 +34,7 @@
#:use-module (ice-9 regex)
#:use-module (ice-9 match)
#:use-module (ice-9 format)
- #:autoload (system foreign) (pointer->procedure)
+ #:use-module (system foreign)
#:export (bytevector->base16-string
base16-string->bytevector
@@ -43,6 +43,7 @@
nixpkgs-derivation*
compile-time-value
+ fcntl-flock
memoize
default-keyword-arguments
substitute-keyword-arguments
@@ -224,6 +225,67 @@ buffered data is lost."
;;;
+;;; Advisory file locking.
+;;;
+
+(define %struct-flock
+ ;; 'struct flock' from <fcntl.h>.
+ (list short ; l_type
+ short ; l_whence
+ size_t ; l_start
+ size_t ; l_len
+ int)) ; l_pid
+
+(define F_SETLKW
+ ;; On Linux-based systems, this is usually 7, but not always
+ ;; (exceptions include SPARC.) On GNU/Hurd, it's 9.
+ (compile-time-value
+ (cond ((string-contains %host-type "sparc") 9) ; sparc-*-linux-gnu
+ ((string-contains %host-type "linux") 7) ; *-linux-gnu
+ (else 9)))) ; *-gnu*
+
+(define F_xxLCK
+ ;; The F_RDLCK, F_WRLCK, and F_UNLCK constants.
+ (compile-time-value
+ (cond ((string-contains %host-type "sparc") #(1 2 3)) ; sparc-*-linux-gnu
+ ((string-contains %host-type "hppa") #(1 2 3)) ; hppa-*-linux-gnu
+ ((string-contains %host-type "linux") #(0 1 2)) ; *-linux-gnu
+ (else #(1 2 3))))) ; *-gnu*
+
+(define fcntl-flock
+ (let* ((ptr (dynamic-func "fcntl" (dynamic-link)))
+ (proc (pointer->procedure int ptr `(,int ,int *))))
+ (lambda (fd-or-port operation)
+ "Perform locking OPERATION on the file beneath FD-OR-PORT. OPERATION
+must be a symbol, one of 'read-lock, 'write-lock, or 'unlock."
+ (define (operation->int op)
+ (case op
+ ((read-lock) (vector-ref F_xxLCK 0))
+ ((write-lock) (vector-ref F_xxLCK 1))
+ ((unlock) (vector-ref F_xxLCK 2))
+ (else (error "invalid fcntl-flock operation" op))))
+
+ (define fd
+ (if (port? fd-or-port)
+ (fileno fd-or-port)
+ fd-or-port))
+
+ ;; XXX: 'fcntl' is a vararg function, but here we happily use the
+ ;; standard ABI; crossing fingers.
+ (let ((err (proc fd
+ F_SETLKW ; lock & wait
+ (make-c-struct %struct-flock
+ (list (operation->int operation)
+ SEEK_SET
+ 0 0 ; whole file
+ 0)))))
+ (or (zero? err)
+
+ ;; Presumably we got EAGAIN or so.
+ (throw 'flock-error fd))))))
+
+
+;;;
;;; Miscellaneous.
;;;