weather.scm 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  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>
  7. ;;;
  8. ;;; This file is part of GNU Guix.
  9. ;;;
  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.
  14. ;;;
  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.
  19. ;;;
  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/>.
  22. (define-module (guix scripts weather)
  23. #:use-module (guix ui)
  24. #:use-module (guix scripts)
  25. #:use-module (guix packages)
  26. #:use-module (guix profiles)
  27. #:use-module (guix derivations)
  28. #:use-module (guix progress)
  29. #:use-module (guix monads)
  30. #:use-module (guix store)
  31. #:use-module (guix grafts)
  32. #:use-module (guix gexp)
  33. #:use-module ((guix build syscalls) #:select (terminal-columns))
  34. #:use-module ((guix build utils) #:select (every*))
  35. #:use-module (guix substitutes)
  36. #:use-module (guix narinfo)
  37. #:use-module (guix http-client)
  38. #:use-module (guix ci)
  39. #:use-module (guix sets)
  40. #:use-module (guix graph)
  41. #:autoload (guix scripts graph) (%bag-node-type)
  42. #:use-module (gnu packages)
  43. #:use-module (web uri)
  44. #:use-module (srfi srfi-1)
  45. #:use-module (srfi srfi-19)
  46. #:use-module (srfi srfi-26)
  47. #:use-module (srfi srfi-34)
  48. #:use-module (srfi srfi-37)
  49. #:use-module (ice-9 match)
  50. #:use-module (ice-9 format)
  51. #:use-module (ice-9 vlist)
  52. #:export (guix-weather))
  53. (define (all-packages)
  54. "Return the list of public packages we are going to query."
  55. (fold-packages (lambda (package result)
  56. (match (package-replacement package)
  57. ((? package? replacement)
  58. (cons* replacement package result))
  59. (#f
  60. (cons package result))))
  61. '()
  62. ;; Dismiss deprecated packages but keep hidden packages.
  63. #:select? (negate package-superseded)))
  64. (define (call-with-progress-reporter reporter proc)
  65. "This is a variant of 'call-with-progress-reporter' that works with monadic
  66. scope."
  67. ;; TODO: Move to a more appropriate place.
  68. (with-monad %store-monad
  69. (start-progress-reporter! reporter)
  70. (mlet* %store-monad ((report -> (lambda ()
  71. (progress-reporter-report! reporter)))
  72. (result (proc report)))
  73. (stop-progress-reporter! reporter)
  74. (return result))))
  75. (define* (package-outputs packages
  76. #:optional (system (%current-system)))
  77. "Return the list of outputs of all of PACKAGES for the given SYSTEM."
  78. (define (lower-object/no-grafts obj system)
  79. (mlet* %store-monad ((previous (set-grafting #f))
  80. (drv (lower-object obj system))
  81. (_ (set-grafting previous)))
  82. (return drv)))
  83. (let ((packages (filter (lambda (package)
  84. (or (not (package? package))
  85. (supported-package? package system)))
  86. packages)))
  87. (format (current-error-port)
  88. (G_ "computing ~h package derivations for ~a...~%")
  89. (length packages) system)
  90. (call-with-progress-reporter (progress-reporter/bar (length packages))
  91. (lambda (report)
  92. (foldm %store-monad
  93. (lambda (package result)
  94. ;; PACKAGE could in fact be a non-package object, for example
  95. ;; coming from a user-specified manifest. Thus, use
  96. ;; 'lower-object' rather than 'package->derivation' here.
  97. (mlet %store-monad ((drv (lower-object/no-grafts package
  98. system)))
  99. (report)
  100. (match (derivation->output-paths drv)
  101. (((names . items) ...)
  102. (return (append items result))))))
  103. '()
  104. packages)))))
  105. (define (call-with-time thunk kont)
  106. "Call THUNK and pass KONT the elapsed time followed by THUNK's return
  107. values."
  108. (let* ((start (current-time time-monotonic))
  109. (result (call-with-values thunk list))
  110. (end (current-time time-monotonic)))
  111. (apply kont (time-difference end start) result)))
  112. (define-syntax-rule (let/time ((time result ... exp)) body ...)
  113. (call-with-time (lambda () exp) (lambda (time result ...) body ...)))
  114. (define (histogram field proc seed lst)
  115. "Return an alist giving a histogram of all the values of FIELD for elements
  116. of LST. FIELD must be a one element procedure that returns a field's value.
  117. For each FIELD value, call PROC with the previous field-specific result.
  118. Example:
  119. (histogram car (lambda (x n) (+ 1 n)) 0 '((a . x)(b . y)(a . z)))
  120. => ((a . 2) (b . 1))
  121. meaning that we have two a's and one b."
  122. (let loop ((lst lst)
  123. (result '()))
  124. (match lst
  125. (()
  126. result)
  127. ((head . tail)
  128. (let ((value (field head)))
  129. (loop tail
  130. (match (assoc-ref result value)
  131. (#f
  132. `((,value . ,(proc head seed)) ,@result))
  133. (previous
  134. `((,value . ,(proc head previous))
  135. ,@(alist-delete value result))))))))))
  136. (define (throughput lst timestamp)
  137. "Return the throughput, in items per second, given the elements of LST,
  138. calling TIMESTAMP to get the \"timestamp\" of each item."
  139. (let ((oldest (reduce min +inf.0 (map build-timestamp lst)))
  140. (now (time-second (current-time time-utc))))
  141. (/ (length lst) (- now oldest) 1.)))
  142. (define (queued-subset queue items)
  143. "Return the subset of ITEMS, a list of store file names, that appears in
  144. QUEUE, a list of builds. Return #f if elements in QUEUE lack information
  145. about the derivations queued, as is the case with Hydra."
  146. (define queued
  147. (append-map (lambda (build)
  148. (match (false-if-exception
  149. (read-derivation-from-file (build-derivation build)))
  150. (#f
  151. '())
  152. (drv
  153. (match (derivation->output-paths drv)
  154. (((names . items) ...) items)))))
  155. queue))
  156. (if (any (negate build-derivation) queue)
  157. #f ;no derivation information
  158. (lset-intersection string=? queued items)))
  159. (define (store-item-system store item)
  160. "Return the system (a string such as \"aarch64-linux\")) ITEM targets,
  161. or #f if it could not be determined."
  162. (match (valid-derivers store item)
  163. ((drv . _)
  164. (and=> (false-if-exception (read-derivation-from-file drv))
  165. derivation-system))
  166. (()
  167. #f)))
  168. (define* (report-server-coverage server items
  169. #:key display-missing?)
  170. "Report the subset of ITEMS available as substitutes on SERVER.
  171. When DISPLAY-MISSING? is true, display the list of missing substitutes.
  172. Return the coverage ratio, an exact number between 0 and 1.
  173. In case ITEMS is an empty list, return 1 instead."
  174. (define MiB (* (expt 2 20) 1.))
  175. ;; TRANSLATORS: it is quite possible zero store items are
  176. ;; looked for.
  177. (format #t (G_ "looking for ~h store items on ~a...~%")
  178. (length items) server)
  179. (let/time ((time narinfos requests-made
  180. (lookup-narinfos
  181. server items
  182. #:make-progress-reporter
  183. (lambda* (total #:key url #:allow-other-keys)
  184. (progress-reporter/bar total)))))
  185. (format #t "~a~%" server)
  186. (let ((obtained (length narinfos))
  187. (requested (length items))
  188. (missing (lset-difference string=?
  189. items (map narinfo-path narinfos)))
  190. (sizes (append-map (lambda (narinfo)
  191. (filter integer?
  192. (narinfo-file-sizes narinfo)))
  193. narinfos))
  194. (time (+ (time-second time)
  195. (/ (time-nanosecond time) 1e9))))
  196. (when (> requested 0)
  197. (format #t (G_ " ~,1f% substitutes available (~h out of ~h)~%")
  198. (* 100. (/ obtained requested 1.))
  199. obtained requested))
  200. (let ((total (/ (reduce + 0 sizes) MiB)))
  201. (match (length sizes)
  202. ((? zero?)
  203. (format #t (G_ " unknown substitute sizes~%")))
  204. (len
  205. (if (= len obtained)
  206. (format #t (G_ " ~,1h MiB of nars (compressed)~%") total)
  207. (format #t (G_ " at least ~,1h MiB of nars (compressed)~%")
  208. total)))))
  209. (format #t (G_ " ~,1h MiB on disk (uncompressed)~%")
  210. (/ (reduce + 0 (map narinfo-size narinfos)) MiB))
  211. (when (> requests-made 0)
  212. (format #t (G_ " ~,3h seconds per request (~,1h seconds in total)~%")
  213. (/ time requests-made 1.) time)
  214. (format #t (G_ " ~,1h requests per second~%")
  215. (/ requests-made time 1.)))
  216. (guard (c ((http-get-error? c)
  217. (if (= 404 (http-get-error-code c))
  218. (format (current-error-port)
  219. (G_ " (continuous integration information \
  220. unavailable)~%"))
  221. (format (current-error-port)
  222. (G_ " '~a' returned ~a (~s)~%")
  223. (uri->string (http-get-error-uri c))
  224. (http-get-error-code c)
  225. (http-get-error-reason c)))))
  226. (let* ((max %query-limit)
  227. (queue (queued-builds server max))
  228. (len (length queue))
  229. (histo (histogram build-system
  230. (lambda (build count)
  231. (+ 1 count))
  232. 0 queue)))
  233. (newline)
  234. (unless (null? missing)
  235. (match (queued-subset queue missing)
  236. (#f #f)
  237. ((= length queued)
  238. (let ((missing (length missing)))
  239. (format #t (G_ " ~,1f% (~h out of ~h) of the missing items \
  240. are queued~%")
  241. (* 100. (/ queued missing))
  242. queued missing)))))
  243. (if (>= len max)
  244. (format #t (G_ " at least ~h queued builds~%") len)
  245. (format #t (G_ " ~h queued builds~%") len))
  246. (for-each (match-lambda
  247. ((system . count)
  248. (format #t (G_ " ~a: ~a (~0,1f%)~%")
  249. system count (* 100. (/ count len)))))
  250. histo))
  251. (let* ((latest (latest-builds server))
  252. (builds/sec (throughput latest build-timestamp)))
  253. (format #t (G_ " build rate: ~1,2f builds per hour~%")
  254. (* builds/sec 3600.))
  255. (for-each (match-lambda
  256. ((system . builds)
  257. (format #t (G_ " ~a: ~,2f builds per hour~%")
  258. system
  259. (* (throughput builds build-timestamp)
  260. 3600.))))
  261. (histogram build-system cons '() latest))))
  262. (when (and display-missing? (not (null? missing)))
  263. (newline)
  264. (format #t (G_ "Substitutes are missing for the following items:~%"))
  265. ;; Display two columns: store items, and their system type.
  266. (format #t "~:{ ~a ~a~%~}"
  267. (zip (map (let ((width (max (- (current-terminal-columns)
  268. 20)
  269. 0)))
  270. (lambda (item)
  271. (if (> (string-length item) width)
  272. item
  273. (string-pad-right item width))))
  274. missing)
  275. (with-store store
  276. (map (lambda (item)
  277. (or (store-item-system store item)
  278. (G_ "unknown system")))
  279. missing)))))
  280. ;; Return the coverage ratio.
  281. (let ((total (length items)))
  282. (if (> total 0)
  283. (/ (- total (length missing)) total)
  284. 1)))))
  285. ;;;
  286. ;;; Command-line options.
  287. ;;;
  288. (define (show-help)
  289. (display (G_ "Usage: guix weather [OPTIONS] [PACKAGES ...]
  290. Report the availability of substitutes.\n"))
  291. (display (G_ "
  292. --substitute-urls=URLS
  293. check for available substitutes at URLS"))
  294. (display (G_ "
  295. -m, --manifest=MANIFEST
  296. look up substitutes for packages specified in MANIFEST"))
  297. (display (G_ "
  298. -c, --coverage[=COUNT]
  299. show substitute coverage for packages with at least
  300. COUNT dependents"))
  301. (display (G_ "
  302. --display-missing display the list of missing substitutes"))
  303. (display (G_ "
  304. -s, --system=SYSTEM consider substitutes for SYSTEM--e.g., \"i686-linux\""))
  305. (newline)
  306. (display (G_ "
  307. -h, --help display this help and exit"))
  308. (display (G_ "
  309. -V, --version display version information and exit"))
  310. (newline)
  311. (show-bug-report-information))
  312. (define %options
  313. (list (option '(#\h "help") #f #f
  314. (lambda args
  315. (show-help)
  316. (exit 0)))
  317. (option '(#\V "version") #f #f
  318. (lambda args
  319. (show-version-and-exit "guix weather")))
  320. (option '("substitute-urls") #t #f
  321. (lambda (opt name arg result . rest)
  322. (let ((urls (string-tokenize arg)))
  323. (for-each (lambda (url)
  324. (unless (string->uri url)
  325. (leave (G_ "~a: invalid URL~%") url)))
  326. urls)
  327. (apply values
  328. (alist-cons 'substitute-urls urls
  329. (alist-delete 'substitute-urls result))
  330. rest))))
  331. (option '(#\m "manifest") #t #f
  332. (lambda (opt name arg result)
  333. (alist-cons 'manifest arg result)))
  334. (option '(#\c "coverage") #f #t
  335. (lambda (opt name arg result)
  336. (alist-cons 'coverage
  337. (if arg (string->number* arg) 0)
  338. result)))
  339. (option '("display-missing") #f #f
  340. (lambda (opt name arg result)
  341. (alist-cons 'display-missing? #t result)))
  342. (option '(#\s "system") #t #f
  343. (lambda (opt name arg result)
  344. (alist-cons 'system arg result)))))
  345. (define %default-options
  346. `((substitute-urls . ,%default-substitute-urls)))
  347. (define (load-manifest file)
  348. "Load the manifest from FILE and return the list of packages it refers to."
  349. (let* ((user-module (make-user-module '((guix profiles) (gnu))))
  350. (manifest (load* file user-module)))
  351. (delete-duplicates (map manifest-entry-item
  352. (manifest-transitive-entries manifest))
  353. eq?)))
  354. ;;;
  355. ;;; Missing package substitutes.
  356. ;;;
  357. (define* (package-partition-boundary pred packages
  358. #:key (system (%current-system)))
  359. "Return the subset of PACKAGES that are at the \"boundary\" between those
  360. that match PRED and those that don't. The returned packages themselves do not
  361. match PRED but they have at least one direct dependency that does.
  362. Note: The assumption is that, if P matches PRED, then all the dependencies of
  363. P match PRED as well."
  364. ;; XXX: Graph theoreticians surely have something to teach us about this...
  365. (let loop ((packages packages)
  366. (result (setq))
  367. (visited vlist-null))
  368. (define (visited? package)
  369. (vhash-assq package visited))
  370. (match packages
  371. ((package . rest)
  372. (cond ((visited? package)
  373. (loop rest result visited))
  374. ((pred package)
  375. (loop rest result (vhash-consq package #t visited)))
  376. (else
  377. (let* ((bag (package->bag package system))
  378. (deps (filter-map (match-lambda
  379. ((label (? package? package) . _)
  380. (and (not (pred package))
  381. package))
  382. (_ #f))
  383. (bag-direct-inputs bag))))
  384. (loop (append deps rest)
  385. (if (null? deps)
  386. (set-insert package result)
  387. result)
  388. (vhash-consq package #t visited))))))
  389. (()
  390. (set->list result)))))
  391. (define (package->output-mapping packages system)
  392. "Return a vhash that maps each item of PACKAGES to its corresponding output
  393. store file names for SYSTEM."
  394. (foldm %store-monad
  395. (lambda (package mapping)
  396. (mlet %store-monad ((drv (package->derivation package system
  397. #:graft? #f)))
  398. (return (vhash-consq package
  399. (match (derivation->output-paths drv)
  400. (((names . outputs) ...)
  401. outputs))
  402. mapping))))
  403. vlist-null
  404. packages))
  405. (define (substitute-oracle server items)
  406. "Return a procedure that, when passed a store item (one of those listed in
  407. ITEMS), returns true if SERVER has a substitute for it, false otherwise."
  408. (define available
  409. (fold (lambda (narinfo set)
  410. (set-insert (narinfo-path narinfo) set))
  411. (set)
  412. (lookup-narinfos server items)))
  413. (cut set-contains? available <>))
  414. (define* (report-package-coverage-per-system server packages system
  415. #:key (threshold 0))
  416. "Report on the subset of PACKAGES that lacks SYSTEM substitutes on SERVER,
  417. sorted by decreasing number of dependents. Do not display those with less
  418. than THRESHOLD dependents."
  419. (mlet* %store-monad ((packages -> (package-closure packages #:system system))
  420. (mapping (package->output-mapping packages system))
  421. (back-edges (node-back-edges %bag-node-type packages)))
  422. (define items
  423. (vhash-fold (lambda (package items result)
  424. (append items result))
  425. '()
  426. mapping))
  427. (define substitutable?
  428. (substitute-oracle server items))
  429. (define substitutable-package?
  430. (lambda (package)
  431. (match (vhash-assq package mapping)
  432. ((_ . items)
  433. (find substitutable? items))
  434. (#f
  435. #f))))
  436. (define missing
  437. (package-partition-boundary substitutable-package? packages
  438. #:system system))
  439. (define missing-count
  440. (length missing))
  441. (if (zero? threshold)
  442. (format #t (N_ "The following ~a package is missing from '~a' for \
  443. '~a':~%"
  444. "The following ~a packages are missing from '~a' for \
  445. '~a':~%"
  446. missing-count)
  447. missing-count server system)
  448. (format #t (N_ "~a package is missing from '~a' for '~a':~%"
  449. "~a packages are missing from '~a' for '~a', among \
  450. which:~%"
  451. missing-count)
  452. missing-count server system))
  453. (for-each (match-lambda
  454. ((package count)
  455. (match (vhash-assq package mapping)
  456. ((_ . items)
  457. (when (>= count threshold)
  458. (format #t " ~4d\t~a@~a\t~{~a ~}~%"
  459. count
  460. (package-name package) (package-version package)
  461. items)))
  462. (#f ;PACKAGE must be an internal thing
  463. #f))))
  464. (sort (zip missing
  465. (map (lambda (package)
  466. (node-reachable-count (list package)
  467. back-edges))
  468. missing))
  469. (match-lambda*
  470. (((_ count1) (_ count2))
  471. (< count2 count1)))))
  472. (return #t)))
  473. (define* (report-package-coverage server packages systems
  474. #:key (threshold 0))
  475. "Report on the substitute coverage for PACKAGES, for each of SYSTEMS, on
  476. SERVER. Display information for packages with at least THRESHOLD dependents."
  477. (with-store store
  478. (run-with-store store
  479. (foldm %store-monad
  480. (lambda (system _)
  481. (report-package-coverage-per-system server packages system
  482. #:threshold threshold))
  483. #f
  484. systems))))
  485. ;;;
  486. ;;; Entry point.
  487. ;;;
  488. (define-command (guix-weather . args)
  489. (synopsis "report on the availability of pre-built package binaries")
  490. (define (package-list opts)
  491. ;; Return the package list specified by OPTS.
  492. (let ((files (filter-map (match-lambda
  493. (('manifest . file) file)
  494. (_ #f))
  495. opts))
  496. (base (filter-map (match-lambda
  497. (('argument . spec)
  498. (specification->package spec))
  499. (_
  500. #f))
  501. opts)))
  502. (if (and (null? files) (null? base))
  503. (all-packages)
  504. (append base (append-map load-manifest files)))))
  505. (with-error-handling
  506. (parameterize ((current-terminal-columns (terminal-columns))
  507. ;; Set grafting upfront in case the user's input depends on
  508. ;; it (e.g., a manifest or code snippet that calls
  509. ;; 'gexp->derivation').
  510. (%graft? #f))
  511. (let* ((opts (parse-command-line args %options
  512. (list %default-options)
  513. #:build-options? #f))
  514. (urls (assoc-ref opts 'substitute-urls))
  515. (systems (match (filter-map (match-lambda
  516. (('system . system) system)
  517. (_ #f))
  518. opts)
  519. (() (list (%current-system)))
  520. (systems systems)))
  521. (packages (package-list opts))
  522. (items (with-store store
  523. (concatenate
  524. (run-with-store store
  525. (mapm %store-monad
  526. (lambda (system)
  527. (package-outputs packages system))
  528. systems))))))
  529. (exit
  530. (every* (lambda (server)
  531. (define coverage
  532. (report-server-coverage server items
  533. #:display-missing?
  534. (assoc-ref opts 'display-missing?)))
  535. (match (assoc-ref opts 'coverage)
  536. (#f #f)
  537. (threshold
  538. ;; PACKAGES may include non-package objects coming from a
  539. ;; manifest. Filter them out.
  540. (report-package-coverage server
  541. (filter package? packages)
  542. systems
  543. #:threshold threshold)))
  544. (= 1 coverage))
  545. urls))))))
  546. ;;; Local Variables:
  547. ;;; eval: (put 'let/time 'scheme-indent-function 1)
  548. ;;; End: