diff options
| author | Ludovic Courtès <ludo@gnu.org> | 2021-01-03 21:14:54 +0100 |
|---|---|---|
| committer | Ludovic Courtès <ludo@gnu.org> | 2021-01-03 21:49:46 +0100 |
| commit | e43958af2764d56de4cd883134a6889b9b61a8f2 (patch) | |
| tree | 498c5e5a9a5db4c9e34ba05e0df37f8801ced2b1 | |
| parent | f44a1e0b528bc22ce0b861136efcee808c9783a6 (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.scm | 27 | ||||
| -rwxr-xr-x | guix/scripts/substitute.scm | 204 |
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 | ||
| 441 | entry 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. |
| 438 | Return two values: a Boolean indicating whether we have valid cached info, and | 454 | Return 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 |
| 503 | return its MAX-LENGTH first elements and its tail." | 536 | return 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 | ||
| 869 | cache." | ||
| 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 |
| 1072 | DESTINATION as a nar file. Verify the substitute against ACL, and verify its | 1142 | DESTINATION as a nar file. Verify the substitute against ACL, and verify its |
| 1073 | hash against what appears in the narinfo. When DEDUPLICATE? is true, and if | 1143 | hash against what appears in the narinfo. When DEDUPLICATE? is true, and if |
| 1074 | DESTINATION is in the store, deduplicate its files. Print a status line on | 1144 | DESTINATION is in the store, deduplicate its files. Print a status line on |
| 1075 | the current output port." | 1145 | the 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 | ||
| 1231 | returned 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) |
