substitute-binary: Add a local cache.
* guix/scripts/substitute-binary.scm (%narinfo-cache-directory, %narinfo-ttl, %narinfo-negative-ttl): New variables. (with-atomic-file-output, object->fields, read-narinfo, write-narinfo, narinfo->string, string->narinfo, lookup-narinfo): New procedures. (fetch-narinfo): Adjust to use `read-narinfo'. (guix-substitute-binary): Ensure the existence of %NARINFO-CACHE-DIRECTORY. Use `lookup-narinfo' instead of `fetch-narinfo'.master
parent
63b7c6c1f8
commit
eba783b7b2
|
@ -22,6 +22,7 @@
|
||||||
#:use-module (guix utils)
|
#:use-module (guix utils)
|
||||||
#:use-module (guix config)
|
#:use-module (guix config)
|
||||||
#:use-module (guix nar)
|
#:use-module (guix nar)
|
||||||
|
#:use-module ((guix build utils) #:select (mkdir-p))
|
||||||
#:use-module (ice-9 rdelim)
|
#:use-module (ice-9 rdelim)
|
||||||
#:use-module (ice-9 regex)
|
#:use-module (ice-9 regex)
|
||||||
#:use-module (ice-9 match)
|
#:use-module (ice-9 match)
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
#:use-module (srfi srfi-1)
|
#:use-module (srfi srfi-1)
|
||||||
#:use-module (srfi srfi-9)
|
#:use-module (srfi srfi-9)
|
||||||
#:use-module (srfi srfi-11)
|
#:use-module (srfi srfi-11)
|
||||||
|
#:use-module (srfi srfi-19)
|
||||||
#:use-module (srfi srfi-26)
|
#:use-module (srfi srfi-26)
|
||||||
#:use-module (web uri)
|
#:use-module (web uri)
|
||||||
#:use-module (web client)
|
#:use-module (web client)
|
||||||
|
@ -47,6 +49,36 @@
|
||||||
;;;
|
;;;
|
||||||
;;; Code:
|
;;; Code:
|
||||||
|
|
||||||
|
(define %narinfo-cache-directory
|
||||||
|
;; A local cache of narinfos, to avoid going to the network.
|
||||||
|
(or (and=> (getenv "XDG_CACHE_HOME")
|
||||||
|
(cut string-append <> "/guix/substitute-binary"))
|
||||||
|
(string-append %state-directory "/substitute-binary/cache")))
|
||||||
|
|
||||||
|
(define %narinfo-ttl
|
||||||
|
;; Number of seconds during which cached narinfo lookups are considered
|
||||||
|
;; valid.
|
||||||
|
(* 24 3600))
|
||||||
|
|
||||||
|
(define %narinfo-negative-ttl
|
||||||
|
;; Likewise, but for negative lookups---i.e., cached lookup failures.
|
||||||
|
(* 3 3600))
|
||||||
|
|
||||||
|
(define (with-atomic-file-output file proc)
|
||||||
|
"Call PROC with an output port for the file that is going to replace FILE.
|
||||||
|
Upon success, FILE is atomically replaced by what has been written to the
|
||||||
|
output port, and PROC's result is returned."
|
||||||
|
(let* ((template (string-append file ".XXXXXX"))
|
||||||
|
(out (mkstemp! template)))
|
||||||
|
(with-throw-handler #t
|
||||||
|
(lambda ()
|
||||||
|
(let ((result (proc out)))
|
||||||
|
(close out)
|
||||||
|
(rename-file template file)
|
||||||
|
result))
|
||||||
|
(lambda (key . args)
|
||||||
|
(false-if-exception (delete-file template))))))
|
||||||
|
|
||||||
(define (fields->alist port)
|
(define (fields->alist port)
|
||||||
"Read recutils-style record from PORT and return them as a list of key/value
|
"Read recutils-style record from PORT and return them as a list of key/value
|
||||||
pairs."
|
pairs."
|
||||||
|
@ -72,6 +104,17 @@ pairs."
|
||||||
(let ((args (map (cut assoc-ref alist <>) keys)))
|
(let ((args (map (cut assoc-ref alist <>) keys)))
|
||||||
(apply make args)))
|
(apply make args)))
|
||||||
|
|
||||||
|
(define (object->fields object fields port)
|
||||||
|
"Write OBJECT (typically a record) as a series of recutils-style fields to
|
||||||
|
PORT, according to FIELDS. FIELDS must be a list of field name/getter pairs."
|
||||||
|
(let loop ((fields fields))
|
||||||
|
(match fields
|
||||||
|
(()
|
||||||
|
object)
|
||||||
|
(((field . get) rest ...)
|
||||||
|
(format port "~a: ~a~%" field (get object))
|
||||||
|
(loop rest)))))
|
||||||
|
|
||||||
(define (fetch uri)
|
(define (fetch uri)
|
||||||
"Return a binary input port to URI and the number of bytes it's expected to
|
"Return a binary input port to URI and the number of bytes it's expected to
|
||||||
provide."
|
provide."
|
||||||
|
@ -161,22 +204,113 @@ failure."
|
||||||
(_ deriver))
|
(_ deriver))
|
||||||
system)))
|
system)))
|
||||||
|
|
||||||
|
(define* (read-narinfo port #:optional url)
|
||||||
|
"Read a narinfo from PORT in its standard external form. If URL is true, it
|
||||||
|
must be a string used to build full URIs from relative URIs found while
|
||||||
|
reading PORT."
|
||||||
|
(alist->record (fields->alist port)
|
||||||
|
(narinfo-maker url)
|
||||||
|
'("StorePath" "URL" "Compression"
|
||||||
|
"FileHash" "FileSize" "NarHash" "NarSize"
|
||||||
|
"References" "Deriver" "System")))
|
||||||
|
|
||||||
|
(define (write-narinfo narinfo port)
|
||||||
|
"Write NARINFO to PORT."
|
||||||
|
(define (empty-string-if-false x)
|
||||||
|
(or x ""))
|
||||||
|
|
||||||
|
(define (number-or-empty-string x)
|
||||||
|
(if (number? x)
|
||||||
|
(number->string x)
|
||||||
|
""))
|
||||||
|
|
||||||
|
(object->fields narinfo
|
||||||
|
`(("StorePath" . ,narinfo-path)
|
||||||
|
("URL" . ,(compose uri->string narinfo-uri))
|
||||||
|
("Compression" . ,narinfo-compression)
|
||||||
|
("FileHash" . ,(compose empty-string-if-false
|
||||||
|
narinfo-file-hash))
|
||||||
|
("FileSize" . ,(compose number-or-empty-string
|
||||||
|
narinfo-file-size))
|
||||||
|
("NarHash" . ,(compose empty-string-if-false
|
||||||
|
narinfo-hash))
|
||||||
|
("NarSize" . ,(compose number-or-empty-string
|
||||||
|
narinfo-size))
|
||||||
|
("References" . ,(compose string-join narinfo-references))
|
||||||
|
("Deriver" . ,(compose empty-string-if-false
|
||||||
|
narinfo-deriver))
|
||||||
|
("System" . ,narinfo-system))
|
||||||
|
port))
|
||||||
|
|
||||||
|
(define (narinfo->string narinfo)
|
||||||
|
"Return the external representation of NARINFO."
|
||||||
|
(call-with-output-string (cut write-narinfo narinfo <>)))
|
||||||
|
|
||||||
|
(define (string->narinfo str)
|
||||||
|
"Return the narinfo represented by STR."
|
||||||
|
(call-with-input-string str (cut read-narinfo <>)))
|
||||||
|
|
||||||
(define (fetch-narinfo cache path)
|
(define (fetch-narinfo cache path)
|
||||||
"Return the <narinfo> record for PATH, or #f if CACHE does not hold PATH."
|
"Return the <narinfo> record for PATH, or #f if CACHE does not hold PATH."
|
||||||
(define (download url)
|
(define (download url)
|
||||||
;; Download the `nix-cache-info' from URL, and return its contents as an
|
;; Download the `nix-cache-info' from URL, and return its contents as an
|
||||||
;; list of key/value pairs.
|
;; list of key/value pairs.
|
||||||
(and=> (false-if-exception (fetch (string->uri url)))
|
(false-if-exception (fetch (string->uri url))))
|
||||||
fields->alist))
|
|
||||||
|
|
||||||
(and=> (download (string-append (cache-url cache) "/"
|
(and=> (download (string-append (cache-url cache) "/"
|
||||||
(store-path-hash-part path)
|
(store-path-hash-part path)
|
||||||
".narinfo"))
|
".narinfo"))
|
||||||
(lambda (properties)
|
(cute read-narinfo <> (cache-url cache))))
|
||||||
(alist->record properties (narinfo-maker (cache-url cache))
|
|
||||||
'("StorePath" "URL" "Compression"
|
(define (lookup-narinfo cache path)
|
||||||
"FileHash" "FileSize" "NarHash" "NarSize"
|
"Check locally if we have valid info about PATH, otherwise go to CACHE and
|
||||||
"References" "Deriver" "System")))))
|
check what it has."
|
||||||
|
(define now
|
||||||
|
(current-time time-monotonic))
|
||||||
|
|
||||||
|
(define (->time seconds)
|
||||||
|
(make-time time-monotonic 0 seconds))
|
||||||
|
|
||||||
|
(define (obsolete? date ttl)
|
||||||
|
(time>? (subtract-duration now (make-time time-duration 0 ttl))
|
||||||
|
(->time date)))
|
||||||
|
|
||||||
|
(define cache-file
|
||||||
|
(string-append %narinfo-cache-directory "/"
|
||||||
|
(store-path-hash-part path)))
|
||||||
|
|
||||||
|
(define (cache-entry narinfo)
|
||||||
|
`(narinfo (version 0)
|
||||||
|
(date ,(time-second now))
|
||||||
|
(value ,(and=> narinfo narinfo->string))))
|
||||||
|
|
||||||
|
(let*-values (((valid? cached)
|
||||||
|
(catch 'system-error
|
||||||
|
(lambda ()
|
||||||
|
(call-with-input-file cache-file
|
||||||
|
(lambda (p)
|
||||||
|
(match (read p)
|
||||||
|
(('narinfo ('version 0) ('date date)
|
||||||
|
('value #f))
|
||||||
|
;; A cached negative lookup.
|
||||||
|
(if (obsolete? date %narinfo-negative-ttl)
|
||||||
|
(values #f #f)
|
||||||
|
(values #t #f)))
|
||||||
|
(('narinfo ('version 0) ('date date)
|
||||||
|
('value value))
|
||||||
|
;; A cached positive lookup
|
||||||
|
(if (obsolete? date %narinfo-ttl)
|
||||||
|
(values #f #f)
|
||||||
|
(values #t (string->narinfo value))))))))
|
||||||
|
(lambda _
|
||||||
|
(values #f #f)))))
|
||||||
|
(if valid?
|
||||||
|
cached ; including negative caches
|
||||||
|
(let ((narinfo (fetch-narinfo cache path)))
|
||||||
|
(with-atomic-file-output cache-file
|
||||||
|
(lambda (out)
|
||||||
|
(write (cache-entry narinfo) out)))
|
||||||
|
narinfo))))
|
||||||
|
|
||||||
(define (filtered-port command input)
|
(define (filtered-port command input)
|
||||||
"Return an input port (and PID) where data drained from INPUT is filtered
|
"Return an input port (and PID) where data drained from INPUT is filtered
|
||||||
|
@ -214,6 +348,7 @@ through COMMAND. INPUT must be a file input port."
|
||||||
|
|
||||||
(define (guix-substitute-binary . args)
|
(define (guix-substitute-binary . args)
|
||||||
"Implement the build daemon's substituter protocol."
|
"Implement the build daemon's substituter protocol."
|
||||||
|
(mkdir-p %narinfo-cache-directory)
|
||||||
(match args
|
(match args
|
||||||
(("--query")
|
(("--query")
|
||||||
(let ((cache (open-cache %cache-url)))
|
(let ((cache (open-cache %cache-url)))
|
||||||
|
@ -225,7 +360,7 @@ through COMMAND. INPUT must be a file input port."
|
||||||
;; Return the subset of PATHS available in CACHE.
|
;; Return the subset of PATHS available in CACHE.
|
||||||
(let ((substitutable
|
(let ((substitutable
|
||||||
(if cache
|
(if cache
|
||||||
(par-map (cut fetch-narinfo cache <>)
|
(par-map (cut lookup-narinfo cache <>)
|
||||||
paths)
|
paths)
|
||||||
'())))
|
'())))
|
||||||
(for-each (lambda (narinfo)
|
(for-each (lambda (narinfo)
|
||||||
|
@ -237,7 +372,7 @@ through COMMAND. INPUT must be a file input port."
|
||||||
;; Reply info about PATHS if it's in CACHE.
|
;; Reply info about PATHS if it's in CACHE.
|
||||||
(let ((substitutable
|
(let ((substitutable
|
||||||
(if cache
|
(if cache
|
||||||
(par-map (cut fetch-narinfo cache <>)
|
(par-map (cut lookup-narinfo cache <>)
|
||||||
paths)
|
paths)
|
||||||
'())))
|
'())))
|
||||||
(for-each (lambda (narinfo)
|
(for-each (lambda (narinfo)
|
||||||
|
@ -263,7 +398,7 @@ through COMMAND. INPUT must be a file input port."
|
||||||
(("--substitute" store-path destination)
|
(("--substitute" store-path destination)
|
||||||
;; Download STORE-PATH and add store it as a Nar in file DESTINATION.
|
;; Download STORE-PATH and add store it as a Nar in file DESTINATION.
|
||||||
(let* ((cache (open-cache %cache-url))
|
(let* ((cache (open-cache %cache-url))
|
||||||
(narinfo (fetch-narinfo cache store-path))
|
(narinfo (lookup-narinfo cache store-path))
|
||||||
(uri (narinfo-uri narinfo)))
|
(uri (narinfo-uri narinfo)))
|
||||||
;; Tell the daemon what the expected hash of the Nar itself is.
|
;; Tell the daemon what the expected hash of the Nar itself is.
|
||||||
(format #t "~a~%" (narinfo-hash narinfo))
|
(format #t "~a~%" (narinfo-hash narinfo))
|
||||||
|
|
|
@ -45,9 +45,13 @@ then
|
||||||
rm -rf "$NIX_STATE_DIR/substituter-data"
|
rm -rf "$NIX_STATE_DIR/substituter-data"
|
||||||
mkdir -p "$NIX_STATE_DIR/substituter-data"
|
mkdir -p "$NIX_STATE_DIR/substituter-data"
|
||||||
|
|
||||||
|
# Place for the substituter's cache.
|
||||||
|
XDG_CACHE_HOME="$NIX_STATE_DIR/cache-$$"
|
||||||
|
|
||||||
export NIX_IGNORE_SYMLINK_STORE NIX_STORE_DIR \
|
export NIX_IGNORE_SYMLINK_STORE NIX_STORE_DIR \
|
||||||
NIX_LOCALSTATE_DIR NIX_LOG_DIR NIX_STATE_DIR NIX_DB_DIR \
|
NIX_LOCALSTATE_DIR NIX_LOG_DIR NIX_STATE_DIR NIX_DB_DIR \
|
||||||
NIX_ROOT_FINDER NIX_SETUID_HELPER GUIX_BINARY_SUBSTITUTE_URL
|
NIX_ROOT_FINDER NIX_SETUID_HELPER GUIX_BINARY_SUBSTITUTE_URL \
|
||||||
|
XDG_CACHE_HOME
|
||||||
|
|
||||||
# Do that because store.scm calls `canonicalize-path' on it.
|
# Do that because store.scm calls `canonicalize-path' on it.
|
||||||
mkdir -p "$NIX_STORE_DIR"
|
mkdir -p "$NIX_STORE_DIR"
|
||||||
|
|
|
@ -159,6 +159,12 @@ Deriver: ~a~%"
|
||||||
(%current-system) ; System
|
(%current-system) ; System
|
||||||
(basename d)))) ; Deriver
|
(basename d)))) ; Deriver
|
||||||
|
|
||||||
|
;; Remove entry from the local cache.
|
||||||
|
(false-if-exception
|
||||||
|
(delete-file (string-append (getenv "XDG_CACHE_HOME")
|
||||||
|
"/guix/substitute-binary/"
|
||||||
|
(store-path-hash-part o))))
|
||||||
|
|
||||||
;; Make sure `substitute-binary' correctly communicates the above data.
|
;; Make sure `substitute-binary' correctly communicates the above data.
|
||||||
(set-build-options s #:use-substitutes? #t)
|
(set-build-options s #:use-substitutes? #t)
|
||||||
(and (has-substitutes? s o)
|
(and (has-substitutes? s o)
|
||||||
|
|
Reference in New Issue