1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
3 ;;; Copyright © 2017 Ricardo Wurmus <rekado@elephly.net>
4 ;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
5 ;;; Copyright © 2020 Simon Tournier <zimon.toutoune@gmail.com>
6 ;;; Copyright © 2021 Maxime Devos <maximedevos@telenet.be>
8 ;;; This file is part of GNU Guix.
10 ;;; GNU Guix is free software; you can redistribute it and/or modify it
11 ;;; under the terms of the GNU General Public License as published by
12 ;;; the Free Software Foundation; either version 3 of the License, or (at
13 ;;; your option) any later version.
15 ;;; GNU Guix is distributed in the hope that it will be useful, but
16 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;;; GNU General Public License for more details.
20 ;;; You should have received a copy of the GNU General Public License
21 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
23 (define-module (guix scripts weather)
24 #:use-module (guix ui)
25 #:use-module (guix scripts)
26 #:use-module (guix packages)
27 #:use-module (guix profiles)
28 #:use-module (guix derivations)
29 #:use-module (guix progress)
30 #:use-module (guix monads)
31 #:use-module (guix store)
32 #:use-module (guix grafts)
33 #:use-module (guix gexp)
34 #:use-module ((guix build syscalls) #:select (terminal-columns))
35 #:use-module ((guix build utils) #:select (every*))
36 #:use-module (guix substitutes)
37 #:use-module (guix narinfo)
38 #:use-module (guix http-client)
39 #:use-module (guix ci)
40 #:use-module (guix sets)
41 #:use-module (guix graph)
42 #:autoload (guix scripts graph) (%bag-node-type)
43 #:use-module (gnu packages)
44 #:use-module (web uri)
45 #:use-module (srfi srfi-1)
46 #:use-module (srfi srfi-19)
47 #:use-module (srfi srfi-26)
48 #:use-module (srfi srfi-34)
49 #:use-module (srfi srfi-37)
50 #:use-module (ice-9 match)
51 #:use-module (ice-9 format)
52 #:use-module (ice-9 vlist)
53 #:export (guix-weather))
55 (define (all-packages)
56 "Return the list of public packages we are going to query."
57 (fold-packages (lambda (package result)
58 (match (package-replacement package)
59 ((? package? replacement)
60 (cons* replacement package result))
62 (cons package result))))
65 ;; Dismiss deprecated packages but keep hidden packages.
66 #:select? (negate package-superseded)))
68 (define (call-with-progress-reporter reporter proc)
69 "This is a variant of 'call-with-progress-reporter' that works with monadic
71 ;; TODO: Move to a more appropriate place.
72 (with-monad %store-monad
73 (start-progress-reporter! reporter)
74 (mlet* %store-monad ((report -> (lambda ()
75 (progress-reporter-report! reporter)))
76 (result (proc report)))
77 (stop-progress-reporter! reporter)
80 (define* (package-outputs packages
81 #:optional (system (%current-system)))
82 "Return the list of outputs of all of PACKAGES for the given SYSTEM."
83 (define (lower-object/no-grafts obj system)
84 (mlet* %store-monad ((previous (set-grafting #f))
85 (drv (lower-object obj system))
86 (_ (set-grafting previous)))
89 (let ((packages (filter (lambda (package)
90 (or (not (package? package))
91 (supported-package? package system)))
93 (format (current-error-port)
94 (G_ "computing ~h package derivations for ~a...~%")
95 (length packages) system)
97 (call-with-progress-reporter (progress-reporter/bar (length packages))
100 (lambda (package result)
101 ;; PACKAGE could in fact be a non-package object, for example
102 ;; coming from a user-specified manifest. Thus, use
103 ;; 'lower-object' rather than 'package->derivation' here.
104 (mlet %store-monad ((drv (lower-object/no-grafts package
107 (match (derivation->output-paths drv)
108 (((names . items) ...)
109 (return (append items result))))))
113 (define (call-with-time thunk kont)
114 "Call THUNK and pass KONT the elapsed time followed by THUNK's return
116 (let* ((start (current-time time-monotonic))
117 (result (call-with-values thunk list))
118 (end (current-time time-monotonic)))
119 (apply kont (time-difference end start) result)))
121 (define-syntax-rule (let/time ((time result ... exp)) body ...)
122 (call-with-time (lambda () exp) (lambda (time result ...) body ...)))
124 (define (histogram field proc seed lst)
125 "Return an alist giving a histogram of all the values of FIELD for elements
126 of LST. FIELD must be a one element procedure that returns a field's value.
127 For each FIELD value, call PROC with the previous field-specific result.
130 (histogram car (lambda (x n) (+ 1 n)) 0 '((a . x)(b . y)(a . z)))
133 meaning that we have two a's and one b."
140 (let ((value (field head)))
142 (match (assoc-ref result value)
144 `((,value . ,(proc head seed)) ,@result))
146 `((,value . ,(proc head previous))
147 ,@(alist-delete value result))))))))))
149 (define (throughput lst timestamp)
150 "Return the throughput, in items per second, given the elements of LST,
151 calling TIMESTAMP to get the \"timestamp\" of each item."
152 (let ((oldest (reduce min +inf.0 (map build-timestamp lst)))
153 (now (time-second (current-time time-utc))))
154 (/ (length lst) (- now oldest) 1.)))
156 (define (queued-subset queue items)
157 "Return the subset of ITEMS, a list of store file names, that appears in
158 QUEUE, a list of builds. Return #f if elements in QUEUE lack information
159 about the derivations queued, as is the case with Hydra."
161 (append-map (lambda (build)
162 (match (false-if-exception
163 (read-derivation-from-file (build-derivation build)))
167 (match (derivation->output-paths drv)
168 (((names . items) ...) items)))))
171 (if (any (negate build-derivation) queue)
172 #f ;no derivation information
173 (lset-intersection string=? queued items)))
175 (define (store-item-system store item)
176 "Return the system (a string such as \"aarch64-linux\")) ITEM targets,
177 or #f if it could not be determined."
178 (match (valid-derivers store item)
180 (and=> (false-if-exception (read-derivation-from-file drv))
185 (define* (report-server-coverage server items
186 #:key display-missing?)
187 "Report the subset of ITEMS available as substitutes on SERVER.
188 When DISPLAY-MISSING? is true, display the list of missing substitutes.
189 Return the coverage ratio, an exact number between 0 and 1.
190 In case ITEMS is an empty list, return 1 instead."
191 (define MiB (* (expt 2 20) 1.))
193 ;; TRANSLATORS: it is quite possible zero store items are
195 (format #t (G_ "looking for ~h store items on ~a...~%")
196 (length items) server)
198 (let/time ((time narinfos requests-made
201 #:make-progress-reporter
202 (lambda* (total #:key url #:allow-other-keys)
203 (progress-reporter/bar total)))))
204 (format #t "~a~%" server)
205 (let ((obtained (length narinfos))
206 (requested (length items))
207 (missing (lset-difference string=?
208 items (map narinfo-path narinfos)))
209 (sizes (append-map (lambda (narinfo)
211 (narinfo-file-sizes narinfo)))
213 (time (+ (time-second time)
214 (/ (time-nanosecond time) 1e9))))
215 (when (> requested 0)
216 (format #t (G_ " ~,1f% substitutes available (~h out of ~h)~%")
217 (* 100. (/ obtained requested 1.))
219 (let ((total (/ (reduce + 0 sizes) MiB)))
220 (match (length sizes)
222 (format #t (G_ " unknown substitute sizes~%")))
225 (format #t (G_ " ~,1h MiB of nars (compressed)~%") total)
226 (format #t (G_ " at least ~,1h MiB of nars (compressed)~%")
228 (format #t (G_ " ~,1h MiB on disk (uncompressed)~%")
229 (/ (reduce + 0 (map narinfo-size narinfos)) MiB))
230 (when (> requests-made 0)
231 (format #t (G_ " ~,3h seconds per request (~,1h seconds in total)~%")
232 (/ time requests-made 1.) time)
233 (format #t (G_ " ~,1h requests per second~%")
234 (/ requests-made time 1.)))
236 (guard (c ((http-get-error? c)
237 (if (= 404 (http-get-error-code c))
238 (format (current-error-port)
239 (G_ " (continuous integration information \
241 (format (current-error-port)
242 (G_ " '~a' returned ~a (~s)~%")
243 (uri->string (http-get-error-uri c))
244 (http-get-error-code c)
245 (http-get-error-reason c)))))
246 (let* ((max %query-limit)
247 (queue (queued-builds server max))
249 (histo (histogram build-system
250 (lambda (build count)
254 (unless (null? missing)
255 (match (queued-subset queue missing)
258 (let ((missing (length missing)))
259 (format #t (G_ " ~,1f% (~h out of ~h) of the missing items \
261 (* 100. (/ queued missing))
265 (format #t (G_ " at least ~h queued builds~%") len)
266 (format #t (G_ " ~h queued builds~%") len))
267 (for-each (match-lambda
269 (format #t (G_ " ~a: ~a (~0,1f%)~%")
270 system count (* 100. (/ count len)))))
273 (let* ((latest (latest-builds server))
274 (builds/sec (throughput latest build-timestamp)))
275 (format #t (G_ " build rate: ~1,2f builds per hour~%")
276 (* builds/sec 3600.))
277 (for-each (match-lambda
279 (format #t (G_ " ~a: ~,2f builds per hour~%")
281 (* (throughput builds build-timestamp)
283 (histogram build-system cons '() latest))))
285 (when (and display-missing? (not (null? missing)))
287 (format #t (G_ "Substitutes are missing for the following items:~%"))
289 ;; Display two columns: store items, and their system type.
290 (format #t "~:{ ~a ~a~%~}"
291 (zip (map (let ((width (max (- (current-terminal-columns)
295 (if (> (string-length item) width)
297 (string-pad-right item width))))
301 (or (store-item-system store item)
302 (G_ "unknown system")))
305 ;; Return the coverage ratio.
306 (let ((total (length items)))
308 (/ (- total (length missing)) total)
313 ;;; Command-line options.
317 (display (G_ "Usage: guix weather [OPTIONS] [PACKAGES ...]
318 Report the availability of substitutes.\n"))
320 --substitute-urls=URLS
321 check for available substitutes at URLS"))
323 -m, --manifest=MANIFEST
324 look up substitutes for packages specified in MANIFEST"))
326 -c, --coverage[=COUNT]
327 show substitute coverage for packages with at least
330 --display-missing display the list of missing substitutes"))
332 -s, --system=SYSTEM consider substitutes for SYSTEM--e.g., \"i686-linux\""))
335 -h, --help display this help and exit"))
337 -V, --version display version information and exit"))
339 (show-bug-report-information))
342 (list (option '(#\h "help") #f #f
346 (option '(#\V "version") #f #f
348 (show-version-and-exit "guix weather")))
350 (option '("substitute-urls") #t #f
351 (lambda (opt name arg result . rest)
352 (let ((urls (string-tokenize arg)))
353 (for-each (lambda (url)
354 (unless (string->uri url)
355 (leave (G_ "~a: invalid URL~%") url)))
358 (alist-cons 'substitute-urls urls
359 (alist-delete 'substitute-urls result))
361 (option '(#\m "manifest") #t #f
362 (lambda (opt name arg result)
363 (alist-cons 'manifest arg result)))
364 (option '(#\c "coverage") #f #t
365 (lambda (opt name arg result)
366 (alist-cons 'coverage
367 (if arg (string->number* arg) 0)
369 (option '("display-missing") #f #f
370 (lambda (opt name arg result)
371 (alist-cons 'display-missing? #t result)))
372 (option '(#\s "system") #t #f
373 (lambda (opt name arg result)
374 (alist-cons 'system arg result)))))
376 (define %default-options
377 `((substitute-urls . ,%default-substitute-urls)))
379 (define (load-manifest file)
380 "Load the manifest from FILE and return the list of packages it refers to."
381 (let* ((user-module (make-user-module '((guix profiles) (gnu))))
382 (manifest (load* file user-module)))
383 (delete-duplicates (map manifest-entry-item
384 (manifest-transitive-entries manifest))
389 ;;; Missing package substitutes.
392 (define* (package-partition-boundary pred packages
393 #:key (system (%current-system)))
394 "Return the subset of PACKAGES that are at the \"boundary\" between those
395 that match PRED and those that don't. The returned packages themselves do not
396 match PRED but they have at least one direct dependency that does.
398 Note: The assumption is that, if P matches PRED, then all the dependencies of
399 P match PRED as well."
400 ;; XXX: Graph theoreticians surely have something to teach us about this...
401 (let loop ((packages packages)
403 (visited vlist-null))
404 (define (visited? package)
405 (vhash-assq package visited))
409 (cond ((visited? package)
410 (loop rest result visited))
412 (loop rest result (vhash-consq package #t visited)))
414 (let* ((bag (package->bag package system))
415 (deps (filter-map (match-lambda
416 ((label (? package? package) . _)
417 (and (not (pred package))
420 (bag-direct-inputs bag))))
421 (loop (append deps rest)
423 (set-insert package result)
425 (vhash-consq package #t visited))))))
427 (set->list result)))))
429 (define (package->output-mapping packages system)
430 "Return a vhash that maps each item of PACKAGES to its corresponding output
431 store file names for SYSTEM."
433 (lambda (package mapping)
434 (mlet %store-monad ((drv (package->derivation package system
436 (return (vhash-consq package
437 (match (derivation->output-paths drv)
438 (((names . outputs) ...)
444 (define (substitute-oracle server items)
445 "Return a procedure that, when passed a store item (one of those listed in
446 ITEMS), returns true if SERVER has a substitute for it, false otherwise."
448 (fold (lambda (narinfo set)
449 (set-insert (narinfo-path narinfo) set))
451 (lookup-narinfos server items)))
453 (cut set-contains? available <>))
455 (define* (report-package-coverage-per-system server packages system
457 "Report on the subset of PACKAGES that lacks SYSTEM substitutes on SERVER,
458 sorted by decreasing number of dependents. Do not display those with less
459 than THRESHOLD dependents."
460 (mlet* %store-monad ((packages -> (package-closure packages #:system system))
461 (mapping (package->output-mapping packages system))
462 (back-edges (node-back-edges %bag-node-type packages)))
464 (vhash-fold (lambda (package items result)
465 (append items result))
469 (define substitutable?
470 (substitute-oracle server items))
472 (define substitutable-package?
474 (match (vhash-assq package mapping)
476 (find substitutable? items))
481 (package-partition-boundary substitutable-package? packages
484 (define missing-count
487 (if (zero? threshold)
488 (format #t (N_ "The following ~a package is missing from '~a' for \
490 "The following ~a packages are missing from '~a' for \
493 missing-count server system)
494 (format #t (N_ "~a package is missing from '~a' for '~a':~%"
495 "~a packages are missing from '~a' for '~a', among \
498 missing-count server system))
500 (for-each (match-lambda
502 (match (vhash-assq package mapping)
504 (when (>= count threshold)
505 (format #t " ~4d\t~a@~a\t~{~a ~}~%"
507 (package-name package) (package-version package)
509 (#f ;PACKAGE must be an internal thing
512 (map (lambda (package)
513 (node-reachable-count (list package)
517 (((_ count1) (_ count2))
518 (< count2 count1)))))
521 (define* (report-package-coverage server packages systems
523 "Report on the substitute coverage for PACKAGES, for each of SYSTEMS, on
524 SERVER. Display information for packages with at least THRESHOLD dependents."
526 (run-with-store store
529 (report-package-coverage-per-system server packages system
530 #:threshold threshold))
539 (define-command (guix-weather . args)
540 (synopsis "report on the availability of pre-built package binaries")
542 (define (package-list opts)
543 ;; Return the package list specified by OPTS.
544 (let ((files (filter-map (match-lambda
545 (('manifest . file) file)
548 (base (filter-map (match-lambda
550 (specification->package spec))
554 (if (and (null? files) (null? base))
556 (append base (append-map load-manifest files)))))
559 (parameterize ((current-terminal-columns (terminal-columns))
561 ;; Set grafting upfront in case the user's input depends on
562 ;; it (e.g., a manifest or code snippet that calls
563 ;; 'gexp->derivation').
565 (let* ((opts (parse-command-line args %options
566 (list %default-options)
567 #:build-options? #f))
568 (urls (assoc-ref opts 'substitute-urls))
569 (systems (match (filter-map (match-lambda
570 (('system . system) system)
573 (() (list (%current-system)))
575 (packages (package-list opts))
576 (items (with-store store
578 (run-with-store store
581 (package-outputs packages system))
584 (every* (lambda (server)
586 (report-server-coverage server items
588 (assoc-ref opts 'display-missing?)))
589 (match (assoc-ref opts 'coverage)
592 ;; PACKAGES may include non-package objects coming from a
593 ;; manifest. Filter them out.
594 (report-package-coverage server
595 (filter package? packages)
597 #:threshold threshold)))
603 ;;; eval: (put 'let/time 'scheme-indent-function 1)