weather.scm 24 KB

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