summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2021-01-03 21:14:54 +0100
committerLudovic Courtès <ludo@gnu.org>2021-01-03 21:49:46 +0100
commite43958af2764d56de4cd883134a6889b9b61a8f2 (patch)
tree498c5e5a9a5db4c9e34ba05e0df37f8801ced2b1
parentf44a1e0b528bc22ce0b861136efcee808c9783a6 (diff)
DRAFT substitute: Fetch digests and restore store items from digests.wip-digests
DRAFT: Tests missing, compression support missing. * guix/scripts/substitute.scm (digest-cache-file, cache-digest!) (digest-request, lookup-digest): New procedures. (fetch-narinfos)[%not-slash]: New variable. [handle-digest-response, handle-response]: New procedures. [do-fetch]: Append digest requests to narinfo requests. Pass 'handle-response' to 'http-multiple-get' instead of 'handle-narinfo-response'. (process-substitution): Rename to... (process-substitution/nar): ... this. Make 'narinfo' a parameter. (http-fetch-files, nar-hash) (process-substitution, process-substitution/digest): New procedures. (guix-substitute): Pass #:delete-entry to 'maybe-remove-expired-cache-entries'. * guix/digests.scm (sexp->digest): New procedure.
-rw-r--r--guix/digests.scm27
-rwxr-xr-xguix/scripts/substitute.scm204
2 files changed, 217 insertions, 14 deletions
diff --git a/guix/digests.scm b/guix/digests.scm
index 9b09b010f6f..68f8219469a 100644
--- a/guix/digests.scm
+++ b/guix/digests.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2020, 2021 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;;;
@@ -41,7 +41,8 @@
41 file-digest 41 file-digest
42 restore-digest 42 restore-digest
43 43
44 digest->sexp)) 44 digest->sexp
45 sexp->digest))
45 46
46;;; Commentary: 47;;; Commentary:
47;;; 48;;;
@@ -232,3 +233,25 @@ false."
232 233
233 `(digest (version 0) 234 `(digest (version 0)
234 ,(->sexp digest))) 235 ,(->sexp digest)))
236
237(define (sexp->digest sexp)
238 "Return a digest deserialized from SEXP."
239 (define (->digest sexp)
240 (match sexp
241 (('x size (algorithm hash) _ ...)
242 (digest 'executable size (list algorithm hash)))
243 (('f size (algorithm hash) _ ...)
244 (digest 'regular size
245 (list algorithm (nix-base32-string->bytevector hash))))
246 (('d entries ...)
247 (digest 'directory 0
248 (map (match-lambda
249 ((name digest)
250 (digest-entry name (->digest digest))))
251 entries)))
252 (('l target)
253 (digest 'symlink 0 target))))
254
255 (match sexp
256 (('digest ('version 0) sexp)
257 (->digest sexp))))
diff --git a/guix/scripts/substitute.scm b/guix/scripts/substitute.scm
index 8084c89ae5c..b1c2c6c575c 100755
--- a/guix/scripts/substitute.scm
+++ b/guix/scripts/substitute.scm
@@ -1,5 +1,5 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
3;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org> 3;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
4;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com> 4;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
5;;; 5;;;
@@ -28,7 +28,8 @@
28 #:use-module (guix records) 28 #:use-module (guix records)
29 #:use-module (guix diagnostics) 29 #:use-module (guix diagnostics)
30 #:use-module (guix i18n) 30 #:use-module (guix i18n)
31 #:use-module ((guix serialization) #:select (restore-file dump-file)) 31 #:use-module ((guix serialization)
32 #:select (restore-file write-file dump-file dump-port*))
32 #:autoload (guix store deduplication) (dump-file/deduplicate) 33 #:autoload (guix store deduplication) (dump-file/deduplicate)
33 #:autoload (guix scripts discover) (read-substitute-urls) 34 #:autoload (guix scripts discover) (read-substitute-urls)
34 #:use-module (gcrypt hash) 35 #:use-module (gcrypt hash)
@@ -43,7 +44,7 @@
43 (open-connection-for-uri 44 (open-connection-for-uri
44 . guix:open-connection-for-uri) 45 . guix:open-connection-for-uri)
45 store-path-abbreviation byte-count->string)) 46 store-path-abbreviation byte-count->string))
46 #:use-module (guix progress) 47 #:use-module ((guix progress) #:hide (dump-port*))
47 #:use-module ((guix build syscalls) 48 #:use-module ((guix build syscalls)
48 #:select (set-thread-name)) 49 #:select (set-thread-name))
49 #:use-module (ice-9 rdelim) 50 #:use-module (ice-9 rdelim)
@@ -66,6 +67,8 @@
66 #:use-module (web request) 67 #:use-module (web request)
67 #:use-module (web response) 68 #:use-module (web response)
68 #:use-module (guix http-client) 69 #:use-module (guix http-client)
70 #:autoload (guix digests) (digest->sexp sexp->digest restore-digest
71 digest-type digest-size digest-content)
69 #:export (narinfo-signature->canonical-sexp 72 #:export (narinfo-signature->canonical-sexp
70 73
71 narinfo? 74 narinfo?
@@ -433,6 +436,19 @@ entry is stored in a sub-directory specific to CACHE-URL."
433 (bytevector->base32-string (sha256 (string->utf8 cache-url))) 436 (bytevector->base32-string (sha256 (string->utf8 cache-url)))
434 "/" hash-part)))) 437 "/" hash-part))))
435 438
439(define (digest-cache-file cache-url path)
440 "Return the name of the local file that contains an entry for PATH. The
441entry is stored in a sub-directory specific to CACHE-URL."
442 ;; The daemon does not sanitize its input, so PATH could be something like
443 ;; "/gnu/store/foo". Gracefully handle that.
444 (match (store-path-hash-part path)
445 (#f
446 (leave (G_ "'~a' does not name a store item~%") path))
447 ((? string? hash-part)
448 (string-append %narinfo-cache-directory "/"
449 (bytevector->base32-string (sha256 (string->utf8 cache-url)))
450 "/" hash-part ".digest"))))
451
436(define (cached-narinfo cache-url path) 452(define (cached-narinfo cache-url path)
437 "Check locally if we have valid info about PATH coming from CACHE-URL. 453 "Check locally if we have valid info about PATH coming from CACHE-URL.
438Return two values: a Boolean indicating whether we have valid cached info, and 454Return two values: a Boolean indicating whether we have valid cached info, and
@@ -498,6 +514,23 @@ indicates that PATH is unavailable at CACHE-URL."
498 (headers '((User-Agent . "GNU Guile")))) 514 (headers '((User-Agent . "GNU Guile"))))
499 (build-request (string->uri url) #:method 'GET #:headers headers))) 515 (build-request (string->uri url) #:method 'GET #:headers headers)))
500 516
517(define (cache-digest! cache-url path data)
518 "Cache DATA, a bytevector, as the digest for PATH obtained from CACHE-URL."
519 (define now
520 (current-time time-monotonic))
521
522 (let ((file (digest-cache-file cache-url path)))
523 (mkdir-p (dirname file))
524 (with-atomic-file-output file
525 (lambda (out)
526 (put-bytevector out data)))))
527
528(define (digest-request cache-url path)
529 "Return an HTTP request for the digest of PATH at CACHE-URL."
530 (let ((url (string-append cache-url "/digest/" (store-path-hash-part path)))
531 (headers '((User-Agent . "GNU Guile"))))
532 (build-request (string->uri url) #:method 'GET #:headers headers)))
533
501(define (at-most max-length lst) 534(define (at-most max-length lst)
502 "If LST is shorter than MAX-LENGTH, return it and the empty list; otherwise 535 "If LST is shorter than MAX-LENGTH, return it and the empty list; otherwise
503return its MAX-LENGTH first elements and its tail." 536return its MAX-LENGTH first elements and its tail."
@@ -686,20 +719,45 @@ port to it, or, if connection failed, print a warning and return #f. Pass
686 %narinfo-transient-error-ttl)) 719 %narinfo-transient-error-ttl))
687 result)))) 720 result))))
688 721
722 (define %not-slash
723 (char-set-complement (char-set #\/)))
724
725 (define (handle-digest-response request response port result)
726 (when (= 200 (response-code response))
727 (let ((len (response-content-length response)))
728 (match (string-tokenize (uri-path (request-uri request))
729 %not-slash)
730 (("digest" hash-part)
731 (let* ((data (if len
732 (get-bytevector-n port len)
733 (read-to-eof port)))
734 (digest (sexp->digest
735 (read (open-bytevector-input-port data)))))
736 (cache-digest! url (hash-part->path hash-part) data)))
737 (_ #f))))
738 result)
739
740 (define (handle-response request response port result)
741 (if (string-contains (uri-path (request-uri request))
742 "/digest/")
743 (handle-digest-response request response port result)
744 (handle-narinfo-response request response port result)))
745
689 (define (do-fetch uri) 746 (define (do-fetch uri)
690 (case (and=> uri uri-scheme) 747 (case (and=> uri uri-scheme)
691 ((http https) 748 ((http https)
692 ;; Note: Do not check HTTPS server certificates to avoid depending 749 ;; Note: Do not check HTTPS server certificates to avoid depending
693 ;; on the X.509 PKI. We can do it because we authenticate 750 ;; on the X.509 PKI. We can do it because we authenticate
694 ;; narinfos, which provides a much stronger guarantee. 751 ;; narinfos, which provides a much stronger guarantee.
695 (let* ((requests (map (cut narinfo-request url <>) paths)) 752 (let* ((requests (append (map (cut narinfo-request url <>) paths)
753 (map (cut digest-request url <>) paths)))
696 (result (call-with-cached-connection uri 754 (result (call-with-cached-connection uri
697 (lambda (port) 755 (lambda (port)
698 (if port 756 (if port
699 (begin 757 (begin
700 (update-progress!) 758 (update-progress!)
701 (http-multiple-get uri 759 (http-multiple-get uri
702 handle-narinfo-response '() 760 handle-response '()
703 requests 761 requests
704 #:open-connection 762 #:open-connection
705 open-connection-for-uri/cached 763 open-connection-for-uri/cached
@@ -806,6 +864,18 @@ was found."
806 ((answer) answer) 864 ((answer) answer)
807 (_ #f))) 865 (_ #f)))
808 866
867(define (lookup-digest cache-url path)
868 "Return the digest for PATH in CACHE-URL or #f if it could not be found in
869cache."
870 (catch 'system-error
871 (lambda ()
872 (call-with-input-file (digest-cache-file cache-url path)
873 (compose sexp->digest read)))
874 (lambda args
875 (if (= ENOENT (system-error-errno args))
876 #f
877 (apply throw args)))))
878
809(define (cached-narinfo-expiration-time file) 879(define (cached-narinfo-expiration-time file)
810 "Return the expiration time for FILE, which is a cached narinfo." 880 "Return the expiration time for FILE, which is a cached narinfo."
811 (catch 'system-error 881 (catch 'system-error
@@ -1065,18 +1135,14 @@ server certificates."
1065 "Bind PORT with EXP... to a socket connected to URI." 1135 "Bind PORT with EXP... to a socket connected to URI."
1066 (call-with-cached-connection uri (lambda (port) exp ...))) 1136 (call-with-cached-connection uri (lambda (port) exp ...)))
1067 1137
1068(define* (process-substitution store-item destination 1138(define* (process-substitution/nar store-item narinfo destination
1069 #:key cache-urls acl 1139 #:key cache-urls
1070 deduplicate? print-build-trace?) 1140 deduplicate? print-build-trace?)
1071 "Substitute STORE-ITEM (a store file name) from CACHE-URLS, and write it to 1141 "Substitute STORE-ITEM (a store file name) from CACHE-URLS, and write it to
1072DESTINATION as a nar file. Verify the substitute against ACL, and verify its 1142DESTINATION as a nar file. Verify the substitute against ACL, and verify its
1073hash against what appears in the narinfo. When DEDUPLICATE? is true, and if 1143hash against what appears in the narinfo. When DEDUPLICATE? is true, and if
1074DESTINATION is in the store, deduplicate its files. Print a status line on 1144DESTINATION is in the store, deduplicate its files. Print a status line on
1075the current output port." 1145the current output port."
1076 (define narinfo
1077 (lookup-narinfo cache-urls store-item
1078 (cut valid-narinfo? <> acl)))
1079
1080 (define destination-in-store? 1146 (define destination-in-store?
1081 (string-prefix? (string-append (%store-prefix) "/") 1147 (string-prefix? (string-append (%store-prefix) "/")
1082 destination)) 1148 destination))
@@ -1160,6 +1226,115 @@ the current output port."
1160 (bytevector->nix-base32-string expected) 1226 (bytevector->nix-base32-string expected)
1161 (bytevector->nix-base32-string actual))))))) 1227 (bytevector->nix-base32-string actual)))))))
1162 1228
1229(define (http-fetch-files base-url files+digests)
1230 "Fetch the files in FILES+DIGESTS, a list of file name/digest pairs as
1231returned by 'restore-digest'.scm"
1232 (define (content-uri digest)
1233 (match (digest-content digest)
1234 (((algorithm hash) _ ...)
1235 (string->uri
1236 (string-append base-url "/content/" algorithm "/"
1237 (bytevector->base32-string hash))))))
1238
1239 (define (content-request digest)
1240 (build-request (content-uri digest)
1241 #:method 'GET
1242 #:headers '((User-Agent . "GNU Guile"))))
1243
1244 (define request->file
1245 (fold (lambda (file+digest result)
1246 (match file+digest
1247 ((file . digest)
1248 (vhash-consq (content-request digest) file
1249 result))))
1250 vlist-null
1251 files+digests))
1252
1253 (define total-size
1254 (match files+digests
1255 (((_ . digests) ...)
1256 (fold (lambda (digest size)
1257 (+ size (digest-size digest)))
1258 0
1259 digests))))
1260
1261 ;; TODO: decompression
1262 ;; TODO: progress report
1263 (http-multiple-get (string->uri base-url)
1264 (lambda (request response port result)
1265 (match (vhash-assq request request->file)
1266 ((digest . file)
1267 ;; TODO: deduplicate
1268 (with-atomic-file-output file
1269 (lambda (output)
1270 (let ((len (response-content-length response)))
1271 (dump-port* port output len))))
1272 (chmod file (if (eq? (digest-type digest) 'regular)
1273 #o444
1274 #o555))
1275 (utime file 1 1 0 0))))
1276 #t
1277 (vhash-fold-right (lambda (file request result)
1278 (cons request result))
1279 '()
1280 request->file)))
1281
1282(define (nar-hash file algorithm)
1283 "Return the ALGORITHM hash of FILE."
1284 (let-values (((port get-hash) (open-hash-port algorithm)))
1285 (write-file file port)
1286 (force-output port)
1287 (let ((hash (get-hash)))
1288 (close-port port)
1289 hash)))
1290
1291(define* (process-substitution/digest store-item narinfo destination
1292 #:key digest
1293 deduplicate? print-build-trace?)
1294 (define destination-in-store?
1295 (string-prefix? (string-append (%store-prefix) "/")
1296 destination))
1297
1298 (let ((missing-files (restore-digest digest destination)))
1299 (unless (null? missing-files)
1300 (http-fetch-files (narinfo-uri-base narinfo) missing-files)))
1301
1302
1303 (let*-values (((algorithm expected)
1304 (narinfo-hash-algorithm+value narinfo))
1305 ((actual) (nar-hash destination algorithm)))
1306 (if (bytevector=? actual expected)
1307 ;; Tell the daemon that we're done.
1308 (format (current-output-port) "success ~a ~a~%"
1309 (narinfo-hash narinfo) (narinfo-size narinfo))
1310 ;; The actual data has a different hash than that in NARINFO.
1311 (format (current-output-port) "hash-mismatch ~a ~a ~a~%"
1312 (hash-algorithm-name algorithm)
1313 (bytevector->nix-base32-string expected)
1314 (bytevector->nix-base32-string actual)))))
1315
1316(define* (process-substitution store-item destination
1317 #:key cache-urls acl
1318 deduplicate? print-build-trace?)
1319 (define narinfo
1320 (lookup-narinfo cache-urls store-item
1321 (cut valid-narinfo? <> acl)))
1322
1323 (define digest
1324 (and narinfo
1325 (lookup-digest (narinfo-uri-base narinfo) store-item)))
1326
1327
1328 (if digest
1329 (process-substitution/digest store-item narinfo destination
1330 #:digest digest
1331 #:deduplicate? deduplicate?
1332 #:print-build-trace? print-build-trace?)
1333 (process-substitution/nar store-item narinfo destination
1334 #:cache-urls cache-urls
1335 #:deduplicate? deduplicate?
1336 #:print-build-trace? print-build-trace?)))
1337
1163 1338
1164;;; 1339;;;
1165;;; Entry point. 1340;;; Entry point.
@@ -1301,6 +1476,11 @@ default value."
1301 cached-narinfo-files 1476 cached-narinfo-files
1302 #:entry-expiration 1477 #:entry-expiration
1303 cached-narinfo-expiration-time 1478 cached-narinfo-expiration-time
1479 #:delete-entry
1480 (lambda (file)
1481 (delete-file* file)
1482 (delete-file*
1483 (string-append file ".digest")))
1304 #:cleanup-period 1484 #:cleanup-period
1305 %narinfo-expired-cache-entry-removal-delay) 1485 %narinfo-expired-cache-entry-removal-delay)
1306 (check-acl-initialized) 1486 (check-acl-initialized)