build-farm-build.el 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. ;;; build-farm-build.el --- Interface for builds -*- lexical-binding: t -*-
  2. ;; Copyright © 2015–2018 Alex Kost <alezost@gmail.com>
  3. ;; This program is free software; you can redistribute it and/or modify
  4. ;; it under the terms of the GNU General Public License as published by
  5. ;; the Free Software Foundation, either version 3 of the License, or
  6. ;; (at your option) any later version.
  7. ;;
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;;
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; This file provides an interface for displaying builds of a build farm
  17. ;; in 'list' and 'info' buffers.
  18. ;;; Code:
  19. (require 'cl-lib)
  20. (require 'bui)
  21. (require 'build-farm)
  22. (require 'build-farm-utils)
  23. (require 'build-farm-url)
  24. (build-farm-define-entry-type build
  25. :search-types '((id . build-farm-build-url)
  26. (latest . build-farm-build-latest-api-url)
  27. (queue . build-farm-build-queue-api-url))
  28. :filters '(build-farm-build-filter-status)
  29. :filter-names '((nixname . name)
  30. (buildstatus . build-status)
  31. (buildmetrics . build-metrics)
  32. (buildoutputs . outputs)
  33. (drvpath . derivation)
  34. (releasename . release-name)
  35. (starttime . start-time)
  36. (stoptime . stop-time)
  37. (timestamp . queued-time))
  38. :filter-boolean-params '(finished busy)
  39. :titles '((queued-time . "Queued at")
  40. (start-time . "Started at")
  41. (stop-time . "Stopped at")))
  42. (defcustom build-farm-number-of-builds 64
  43. "Default number of builds to display.
  44. This variable is used by '\\[build-farm-latest-builds]' and'
  45. '\\[build-farm-queued-builds]' commands. If nil, always prompt
  46. for the number of builds."
  47. :type 'integer
  48. :group 'build-farm-build)
  49. (defun build-farm-set-number-of-builds (number)
  50. "Set `build-farm-number-of-builds' to NUMBER."
  51. (interactive (list (build-farm-build-read-number)))
  52. (setq build-farm-number-of-builds number))
  53. (defun build-farm-build-read-number (&optional prompt)
  54. "Read from minibuffer (using PROMPT) a number of builds."
  55. (read-number (or prompt "Number of builds: ")
  56. build-farm-number-of-builds))
  57. (defun build-farm-build-read-number-maybe (&optional prompt)
  58. "Read from minibuffer (using PROMPT) a number of builds.
  59. If `current-prefix-arg' is specified, just return
  60. `build-farm-number-of-builds' without reading."
  61. (if (or current-prefix-arg
  62. (null build-farm-number-of-builds))
  63. (build-farm-build-read-number prompt)
  64. build-farm-number-of-builds))
  65. (cl-defun build-farm-build-latest-prompt-args (&key project jobset
  66. job system)
  67. "Prompt for and return a list of 'latest builds' arguments."
  68. (let* ((number (build-farm-build-read-number-maybe))
  69. (url (build-farm-current-url))
  70. (url-type (build-farm-url-type url))
  71. (project (unless (eq 'cuirass url-type)
  72. (if current-prefix-arg
  73. (build-farm-read-project
  74. :url url
  75. :initial-input project)
  76. project)))
  77. (jobset (if current-prefix-arg
  78. (build-farm-read-jobset
  79. :url url
  80. :project project
  81. :initial-input jobset)
  82. jobset))
  83. (job-or-name (if current-prefix-arg
  84. (build-farm-read-job nil job)
  85. job))
  86. (job (and job-or-name
  87. (string-match-p build-farm-job-regexp
  88. job-or-name)
  89. job-or-name))
  90. (system (if (and (not job)
  91. (or current-prefix-arg
  92. (and job-or-name (not system))))
  93. (if job-or-name
  94. (build-farm-while-null
  95. (build-farm-read-system
  96. (concat job-or-name ".") system))
  97. (build-farm-read-system nil system))
  98. system))
  99. (job (or job
  100. (and job-or-name
  101. (concat job-or-name "." system))))
  102. ;; Job specification already includes system, so we don't need
  103. ;; system, if job is specified.
  104. (system (unless job system)))
  105. (list number
  106. :project project
  107. :jobset jobset
  108. :job job
  109. :system system)))
  110. (defun build-farm-build-button-action (button &optional type)
  111. "Display latest builds according to BUTTON.
  112. Additional parameters are taken from BUTTON. If TYPE is
  113. specified, it should be one of the following symbols: `project',
  114. `jobset', `job' or `system'. This TYPE defines what parameter
  115. BUTTON label is used for."
  116. (let* ((label (button-label button))
  117. (args (build-farm-build-latest-prompt-args
  118. :project (or (button-get button 'project)
  119. (and (eq type 'project) label))
  120. :jobset (or (button-get button 'jobset)
  121. (and (eq type 'jobset) label))
  122. :job (or (button-get button 'job)
  123. (and (eq type 'job) label))
  124. :system (or (button-get button 'system)
  125. (and (eq type 'system) label)))))
  126. (apply #'build-farm-get-display
  127. (build-farm-current-url)
  128. 'build 'latest args)))
  129. (defmacro build-farm-define-build-button (type)
  130. "Define button and action function for it for TYPE.
  131. See `build-farm-build-button-action' for the meaning of TYPE.
  132. Button name is `build-farm-TYPE'.
  133. Function name is `build-farm-build-button-TYPE-action'."
  134. (let* ((type-str (symbol-name type))
  135. (btn-name (intern (concat "build-farm-" type-str)))
  136. (fun-name (intern (concat "build-farm-build-button-"
  137. type-str "-action")))
  138. (face-name (intern (concat "build-farm-info-" type-str))))
  139. `(progn
  140. (defun ,fun-name (button)
  141. "Display latest builds according to BUTTON."
  142. (build-farm-build-button-action button ',type))
  143. (define-button-type ',btn-name
  144. :supertype 'bui
  145. 'action #',fun-name
  146. 'help-echo ,(concat "Show latest builds for this " type-str
  147. " (with prefix, prompt for all parameters)")
  148. 'face ',face-name))))
  149. ;; 'project' and 'jobset' buttons for latest builds are not generated,
  150. ;; because these buttons already exist: they are used to display the
  151. ;; according Info interfaces.
  152. (build-farm-define-build-button job)
  153. (build-farm-define-build-button system)
  154. (cl-defun build-farm-info-insert-builds-button
  155. (&key project jobset job system)
  156. "Insert 'Builds' button for PROJECT, JOBSET, JOB, SYSTEM."
  157. (bui-insert-action-button
  158. "Builds"
  159. #'build-farm-build-button-action
  160. (concat "Show latest builds"
  161. (let ((thing (cond (job "job")
  162. (system "system")
  163. (jobset "jobset")
  164. (project "project"))))
  165. (if thing
  166. (concat " for this " thing)
  167. ""))
  168. " (with prefix, prompt for all parameters)")
  169. 'project project
  170. 'jobset jobset
  171. 'job job
  172. 'system system))
  173. (cl-defun build-farm-info-insert-system-button
  174. (system &key project jobset job)
  175. "Insert button to display builds for SYSTEM, PROJECT, JOBSET, JOB."
  176. (bui-insert-button system 'build-farm-system
  177. 'project project
  178. 'jobset jobset
  179. 'job job
  180. 'system system))
  181. (declare-function guix-build-log-mode "guix-build-log" t)
  182. (defun build-farm-build-view-log (id &optional root-url)
  183. "View build log of a build ID from ROOT-URL."
  184. (let ((pkg-manager (build-farm-url-package-manager root-url))
  185. (url (or root-url (build-farm-current-url))))
  186. (if (eq pkg-manager 'nix)
  187. ;; Logs from hydra.nixos.org (which are actually kept on
  188. ;; amazonaws) are stored in a compressed form that is not
  189. ;; supported by Emacs yet: a raw log page returns
  190. ;; "Content-Encoding: br" heading ("Brotli" compression). So
  191. ;; instead of opening the log in Emacs (it would be displayed as
  192. ;; an arbitrary binary data), open it in a browser.
  193. (browse-url (build-farm-build-log-url id :root-url url))
  194. (browse-url-emacs (build-farm-build-log-url
  195. id :root-url url :raw t))
  196. (when (and (eq pkg-manager 'guix)
  197. (require 'guix-build-log nil t))
  198. (guix-build-log-mode)))))
  199. ;;; Filters for processing raw entries
  200. (defun build-farm-build-filter-status (entry)
  201. "Add 'status' parameter to 'hydra-build' ENTRY."
  202. (let ((status (if (bui-entry-non-void-value entry 'finished)
  203. (build-farm-build-status-number->name
  204. (bui-entry-non-void-value entry 'build-status))
  205. (if (bui-entry-non-void-value entry 'busy)
  206. 'running
  207. 'scheduled))))
  208. (cons `(status . ,status)
  209. entry)))
  210. ;;; Build status
  211. (defface build-farm-build-status-running
  212. '((t :inherit bold))
  213. "Face used if a build is not finished."
  214. :group 'build-farm-build-faces)
  215. (defface build-farm-build-status-scheduled
  216. '((t))
  217. "Face used if a build is scheduled."
  218. :group 'build-farm-build-faces)
  219. (defface build-farm-build-status-succeeded
  220. '((t :inherit success))
  221. "Face used if a build succeeded."
  222. :group 'build-farm-build-faces)
  223. (defface build-farm-build-status-cancelled
  224. '((t :inherit warning))
  225. "Face used if a build was cancelled."
  226. :group 'build-farm-build-faces)
  227. (defface build-farm-build-status-failed
  228. '((t :inherit error))
  229. "Face used if a build failed."
  230. :group 'build-farm-build-faces)
  231. (defface build-farm-build-status-unknown
  232. '((t))
  233. "Face used if a build has an unknown status."
  234. :group 'build-farm-build-faces)
  235. (defvar build-farm-build-status-alist
  236. ;; "buildstatus" numbers can be looked at:
  237. ;; <https://github.com/NixOS/hydra/blob/master/src/root/common.tt>.
  238. '((0 . succeeded)
  239. (1 . failed-build)
  240. (2 . failed-dependency)
  241. (3 . failed-other)
  242. (6 . failed-output)
  243. (4 . cancelled))
  244. "Alist of build status numbers and status names.
  245. Status numbers are returned by build farms APIs; names (symbols)
  246. are used internally by the elisp code of this package.")
  247. (defun build-farm-build-status-number->name (number)
  248. "Convert build status NUMBER to a name.
  249. See `build-farm-build-status-alist'."
  250. (bui-assq-value build-farm-build-status-alist number))
  251. (defun build-farm-build-status-string (status)
  252. "Return a human readable string for build STATUS."
  253. (cl-case status
  254. (scheduled
  255. (bui-get-string "Scheduled" 'build-farm-build-status-scheduled))
  256. (running
  257. (bui-get-string "Running" 'build-farm-build-status-running))
  258. (succeeded
  259. (bui-get-string "Succeeded" 'build-farm-build-status-succeeded))
  260. (cancelled
  261. (bui-get-string "Cancelled" 'build-farm-build-status-cancelled))
  262. (failed-build
  263. (build-farm-build-status-fail-string))
  264. (failed-dependency
  265. (build-farm-build-status-fail-string "dependency"))
  266. (failed-other
  267. (build-farm-build-status-fail-string "other"))
  268. (failed-output
  269. (build-farm-build-status-fail-string "with output"))
  270. (t
  271. (bui-get-string "Unknown" 'build-farm-build-status-unknown))))
  272. (defun build-farm-build-status-fail-string (&optional reason)
  273. "Return a string for a failed build using REASON."
  274. (let ((base (bui-get-string "Failed" 'build-farm-build-status-failed)))
  275. (if reason
  276. (concat base " (" reason ")")
  277. base)))
  278. (defun build-farm-build-finished? (entry)
  279. "Return non-nil, if build ENTRY was finished."
  280. (bui-entry-non-void-value entry 'finished))
  281. (defun build-farm-build-running? (entry)
  282. "Return non-nil, if build ENTRY is running."
  283. (eq (bui-entry-non-void-value entry 'status)
  284. 'running))
  285. (defun build-farm-build-scheduled? (entry)
  286. "Return non-nil, if build ENTRY is scheduled."
  287. (eq (bui-entry-non-void-value entry 'status)
  288. 'scheduled))
  289. (defun build-farm-build-succeeded? (entry)
  290. "Return non-nil, if build ENTRY succeeded."
  291. (eq (bui-entry-non-void-value entry 'status)
  292. 'succeeded))
  293. (defun build-farm-build-cancelled? (entry)
  294. "Return non-nil, if build ENTRY was cancelled."
  295. (eq (bui-entry-non-void-value entry 'status)
  296. 'cancelled))
  297. (defun build-farm-build-failed? (entry)
  298. "Return non-nil, if build ENTRY failed."
  299. (memq (bui-entry-non-void-value entry 'status)
  300. '(failed-build failed-dependency failed-other)))
  301. ;;; Build 'info'
  302. (build-farm-define-interface build info
  303. :mode-name "Build-Info"
  304. :buffer-name "*Farm Build Info*"
  305. :format '((name nil (simple bui-info-heading))
  306. nil
  307. build-farm-build-info-insert-more-button
  308. build-farm-build-info-insert-url
  309. nil
  310. (queued-time format (time))
  311. (start-time format (time))
  312. (stop-time format (time))
  313. (release-name format (format))
  314. (status format build-farm-build-info-insert-status)
  315. (priority format (format))
  316. (derivation simple (indent bui-file))
  317. (outputs simple (build-farm-build-info-insert-outputs))
  318. nil
  319. (project format build-farm-build-info-insert-project)
  320. (jobset format build-farm-build-info-insert-jobset)
  321. (job format build-farm-build-info-insert-job)
  322. (system format build-farm-build-info-insert-system)))
  323. (defvar build-farm-build-info-output-format "%-6s "
  324. "String for formatting output names of builds.
  325. It should be a '%s'-sequence.")
  326. (defun build-farm-build-info-insert-more-button (entry)
  327. "Insert 'More info' button for build ENTRY at point."
  328. (when (and (eq 'hydra (build-farm-current-url-type))
  329. (bui-void-value? (bui-entry-value entry 'start-time)))
  330. (bui-insert-action-button
  331. "More info"
  332. (lambda (btn)
  333. (build-farm-build-info-update-build (button-get btn 'id)))
  334. "Receive more info on the current build"
  335. 'id (bui-entry-id entry))
  336. (bui-newline 2)))
  337. (defun build-farm-build-info-update-build (id)
  338. "Update build with ID in the current build info buffer."
  339. (let ((new-entry (car (bui-get-entries
  340. 'build-farm-build 'info
  341. (list (build-farm-current-url) 'id id)))))
  342. (or new-entry
  343. ;; Actually, this shouldn't happen.
  344. (error "Couldn't receive more info for build %d" id))
  345. (setf (bui-item-entries bui-item)
  346. (bui-replace-entry (bui-current-entries) id new-entry))
  347. (bui-redisplay)))
  348. (defun build-farm-build-info-insert-project (project entry)
  349. "Insert PROJECT for build ENTRY at point."
  350. (bui-insert-button project 'build-farm-project)
  351. (bui-insert-indent)
  352. (build-farm-info-insert-builds-button
  353. :project (bui-entry-non-void-value entry 'project)))
  354. (defun build-farm-build-info-insert-jobset (jobset entry)
  355. "Insert JOBSET for build ENTRY at point."
  356. (if (eq 'hydra (build-farm-current-url-type))
  357. (build-farm-info-insert-hydra-jobset
  358. (bui-entry-non-void-value entry 'project)
  359. jobset)
  360. (build-farm-info-insert-cuirass-jobset jobset))
  361. (bui-insert-indent)
  362. (build-farm-info-insert-builds-button
  363. :project (bui-entry-non-void-value entry 'project)
  364. :jobset (bui-entry-non-void-value entry 'jobset)))
  365. (defun build-farm-build-info-insert-job (job entry)
  366. "Insert JOB for build ENTRY at point."
  367. (insert job)
  368. (bui-insert-indent)
  369. (build-farm-info-insert-builds-button
  370. :project (bui-entry-non-void-value entry 'project)
  371. :jobset (bui-entry-non-void-value entry 'jobset)
  372. :job (bui-entry-non-void-value entry 'job)))
  373. (defun build-farm-build-info-insert-system (system entry)
  374. "Insert SYSTEM for build ENTRY at point."
  375. (insert system)
  376. (bui-insert-indent)
  377. (build-farm-info-insert-builds-button
  378. :system (bui-entry-non-void-value entry 'system)))
  379. (defun build-farm-build-info-insert-url (entry)
  380. "Insert URL for the build ENTRY."
  381. (bui-insert-button (build-farm-build-url
  382. (bui-entry-id entry)
  383. :root-url (build-farm-current-url))
  384. 'bui-url)
  385. (when (build-farm-build-finished? entry)
  386. (bui-insert-indent)
  387. (bui-insert-action-button
  388. "Build log"
  389. (lambda (btn)
  390. (build-farm-build-view-log (button-get btn 'id)))
  391. "View build log"
  392. 'id (bui-entry-id entry)))
  393. (bui-newline))
  394. (defun build-farm-build-info-insert-status (status &optional _)
  395. "Insert a string with build STATUS."
  396. (insert (build-farm-build-status-string status)))
  397. (defun build-farm-build-info-insert-outputs (outputs)
  398. "Insert build OUTPUTS at point."
  399. (bui-insert-non-nil outputs
  400. (dolist (output outputs)
  401. (bui-newline)
  402. (bui-insert-indent)
  403. (build-farm-build-info-insert-output output))))
  404. (defun build-farm-build-info-insert-output (output)
  405. "Insert build OUTPUT at point."
  406. (let* ((name (symbol-name (car output)))
  407. (alist (cdr output))
  408. (file-name (bui-assq-value alist 'path)))
  409. (bui-format-insert name nil
  410. build-farm-build-info-output-format)
  411. (bui-insert-button file-name 'bui-file)))
  412. ;;; Build 'list'
  413. (build-farm-define-interface build list
  414. :describe-function 'build-farm-list-describe
  415. :mode-name "Build-List"
  416. :buffer-name "*Farm Builds*"
  417. :format '((name nil 30 t)
  418. (system nil 16 t)
  419. (status build-farm-build-list-get-status 20 t)
  420. (project nil 10 t)
  421. (jobset nil 17 t)
  422. (queued-time bui-list-get-time 20 t))
  423. :hint 'build-farm-build-list-hint)
  424. (let ((map build-farm-build-list-mode-map))
  425. (define-key map (kbd "B") 'build-farm-build-list-latest-builds)
  426. (define-key map (kbd "L") 'build-farm-build-list-view-log))
  427. (defvar build-farm-build-list-default-hint
  428. '(("\\[build-farm-build-list-latest-builds]")
  429. " show latest builds of the current job;\n"
  430. ("\\[build-farm-build-list-view-log]") " show build log;\n"))
  431. (defun build-farm-build-list-hint ()
  432. "Return hint string for a build-list buffer."
  433. (bui-format-hints
  434. build-farm-build-list-default-hint
  435. (bui-default-hint)))
  436. (defun build-farm-build-list-get-status (status &optional _)
  437. "Return a string for build STATUS."
  438. (build-farm-build-status-string status))
  439. (defun build-farm-build-list-latest-builds (number &rest args)
  440. "Display latest NUMBER of builds of the current job.
  441. Interactively, use `build-farm-number-of-builds' variable for
  442. NUMBER. With prefix argument, prompt for it and for the other
  443. ARGS."
  444. (interactive
  445. (let ((entry (bui-list-current-entry)))
  446. (build-farm-build-latest-prompt-args
  447. :project (bui-entry-non-void-value entry 'project)
  448. :jobset (bui-entry-non-void-value entry 'jobset)
  449. :job (bui-entry-non-void-value entry 'job)
  450. :system (bui-entry-non-void-value entry 'system))))
  451. (apply #'build-farm-get-display
  452. (build-farm-current-url) 'build 'latest number args))
  453. (defun build-farm-build-list-view-log ()
  454. "View build log of the current build."
  455. (interactive)
  456. (build-farm-build-view-log (bui-list-current-id)))
  457. ;;; Interactive commands
  458. ;;;###autoload
  459. (defun build-farm-latest-builds (number &rest args)
  460. "Display latest NUMBER of builds.
  461. ARGS are the same arguments as for `build-farm-build-latest-api-url'.
  462. Interactively, use `build-farm-number-of-builds' variable for
  463. NUMBER. With prefix argument, prompt for it and for the other
  464. ARGS."
  465. (interactive (build-farm-build-latest-prompt-args))
  466. (apply #'build-farm-get-display
  467. build-farm-url 'build 'latest number args))
  468. ;;;###autoload
  469. (defun build-farm-queued-builds (number)
  470. "Display the NUMBER of queued builds.
  471. Interactively, use `build-farm-number-of-builds' variable for
  472. NUMBER. With prefix argument, prompt for it."
  473. (interactive
  474. (list (build-farm-build-read-number-maybe)))
  475. (build-farm-get-display build-farm-url 'build 'queue number))
  476. ;;;###autoload
  477. (defun build-farm-build (id)
  478. "Find build by its ID and display it."
  479. (interactive "nBuild ID: ")
  480. (build-farm-get-display build-farm-url 'build 'id id))
  481. (provide 'build-farm-build)
  482. ;;; build-farm-build.el ends here