build-farm.el 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. ;;; build-farm.el --- Interface for Nix and Guix build farms (Hydra and Cuirass) -*- lexical-binding: t -*-
  2. ;; Copyright © 2015–2018 Alex Kost <alezost@gmail.com>
  3. ;; Author: Alex Kost <alezost@gmail.com>
  4. ;; Version: 0.2.2
  5. ;; URL: https://gitlab.com/alezost-emacs/build-farm
  6. ;; Keywords: tools
  7. ;; Package-Requires: ((emacs "24.4") (bui "1.2.1") (magit-popup "2.1.0"))
  8. ;; This program is free software; you can redistribute it and/or modify
  9. ;; it under the terms of the GNU General Public License as published by
  10. ;; the Free Software Foundation, either version 3 of the License, or
  11. ;; (at your option) any later version.
  12. ;;
  13. ;; This program is distributed in the hope that it will be useful,
  14. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;; GNU General Public License for more details.
  17. ;;
  18. ;; You should have received a copy of the GNU General Public License
  19. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. ;;; Commentary:
  21. ;; This package provides Emacs interface for Hydra and Cuirass (Nix and
  22. ;; Guix build farms):
  23. ;;
  24. ;; https://hydra.nixos.org (Hydra)
  25. ;; https://hydra.gnu.org (Hydra)
  26. ;; https://berlin.guixsd.org (Cuirass)
  27. ;;
  28. ;; Set `build-farm-url' variable to choose what build farm you wish to
  29. ;; use.
  30. ;; The entry point for the available features is "M-x build-farm". It
  31. ;; provides a Magit-like interface for the commands to display builds,
  32. ;; jobsets, evaluations and projects.
  33. ;; Alternatively, you can use the following M-x commands directly:
  34. ;;
  35. ;; - `build-farm-latest-builds'
  36. ;; - `build-farm-queued-builds'
  37. ;; - `build-farm-build'
  38. ;; - `build-farm-jobsets'
  39. ;; - `build-farm-projects'
  40. ;; - `build-farm-project'
  41. ;; - `build-farm-latest-evaluations'
  42. ;; You can press RET in a list (of builds, etc.) to see more info on the
  43. ;; current entry. You can also select several entries in the list (with
  44. ;; "m" key) and press RET to "describe" them.
  45. ;;; Code:
  46. (require 'bui)
  47. (require 'build-farm-utils)
  48. (require 'build-farm-url)
  49. (defgroup build-farm nil
  50. "Interface for Hydra and Cuirass build farms used by Guix and Nix."
  51. :prefix "build-farm-"
  52. :group 'external)
  53. (defgroup build-farm-faces nil
  54. "Faces for build-farm interfaces."
  55. :group 'build-farm
  56. :group 'faces)
  57. ;;; Faces and buttons
  58. (defface build-farm-info-project
  59. '((t :inherit button))
  60. "Face for projects in 'info' buffers."
  61. :group 'build-farm-faces)
  62. (defface build-farm-info-jobset
  63. '((t :inherit button))
  64. "Face for jobsets in 'info' buffers."
  65. :group 'build-farm-faces)
  66. (defface build-farm-info-job
  67. '((t :inherit button))
  68. "Face for jobs in 'info' buffers."
  69. :group 'build-farm-faces)
  70. (defface build-farm-info-system
  71. '((t :inherit button))
  72. "Face for system names in 'info' buffers."
  73. :group 'build-farm-faces)
  74. (defun build-farm-project-button-action (button)
  75. "Display project info for project BUTTON."
  76. (let ((search-type 'id)
  77. (search-value (or (button-get button 'id)
  78. (button-get button 'name)
  79. (button-label button))))
  80. (require 'build-farm-project)
  81. (bui-get-display-entries
  82. 'build-farm-project 'info
  83. (list (build-farm-current-url)
  84. search-type search-value))))
  85. (define-button-type 'build-farm-project
  86. :supertype 'bui
  87. 'face 'build-farm-info-project
  88. 'help-echo "Display project info"
  89. 'action 'build-farm-project-button-action)
  90. (define-button-type 'build-farm-jobset
  91. :supertype 'bui
  92. 'help-echo "Display jobset info"
  93. 'face 'build-farm-info-jobset)
  94. (defun build-farm-info-insert-hydra-jobset (project jobset)
  95. "Insert button for Hydra JOBSET of the PROJECT at point."
  96. (let ((url (build-farm-jobset-url
  97. :root-url (build-farm-current-url)
  98. :project project
  99. :jobset jobset)))
  100. (bui-insert-button jobset 'build-farm-jobset
  101. 'url url
  102. 'help-echo (format "Browse %s" url))))
  103. (defun build-farm-info-insert-cuirass-jobset (jobset)
  104. "Insert button for Cuirass JOBSET at point."
  105. (bui-insert-button
  106. jobset 'build-farm-jobset
  107. 'action (lambda (btn)
  108. (require 'build-farm-jobset)
  109. (bui-get-display-entries
  110. 'build-farm-cuirass-jobset 'info
  111. (list (build-farm-current-url)
  112. 'name
  113. (button-get btn 'jobset))))
  114. 'jobset jobset))
  115. ;;; System types
  116. ;; XXX I don't like this hard-coding very much. But it looks like there
  117. ;; is no way to receive system types from a build farm.
  118. (defvar build-farm-guix-system-types
  119. '("x86_64-linux"
  120. "i686-linux"
  121. "armhf-linux"
  122. "mips64el-linux"
  123. "aarch64-linux")
  124. "List of systems supported by Guix build farms.")
  125. (defvar build-farm-nix-system-types
  126. '("x86_64-linux"
  127. "i686-linux"
  128. "x86_64-darwin"
  129. "aarch64-linux")
  130. "List of systems supported by Nix build farms.")
  131. (defun build-farm-system-types (&optional url)
  132. "Return a list of systems supported by URL.
  133. If URL is nil, use variable `build-farm-url'."
  134. (cl-case (build-farm-url-package-manager url)
  135. (nix build-farm-nix-system-types)
  136. (guix build-farm-guix-system-types)
  137. (t (delete-dups
  138. (append build-farm-nix-system-types
  139. build-farm-guix-system-types)))))
  140. ;;; Cache
  141. (defvar build-farm-cache nil
  142. "Cache of various data received from build farms.")
  143. (defun build-farm-cache-get (url data-type)
  144. "Return DATA-TYPE data from the cache of build farm URL."
  145. (bui-assoc-value build-farm-cache url data-type))
  146. (defun build-farm-cache-set (url data-type data)
  147. "Add DATA-TYPE DATA to the cache of build farm URL."
  148. (let ((data-assoc (cons data-type data))
  149. (url-assoc (assoc url build-farm-cache)))
  150. (if url-assoc
  151. (setf (cdr url-assoc)
  152. (cons data-assoc (cdr url-assoc)))
  153. (setq build-farm-cache
  154. (cons (list url data-assoc)
  155. build-farm-cache)))))
  156. (defun build-farm-clear-cache ()
  157. "Remove all cached data received from the build farms."
  158. (interactive)
  159. (setq build-farm-cache nil)
  160. (message "The build farm cache has been cleared."))
  161. (defvar build-farm-job-regexp
  162. (concat ".+\\."
  163. (regexp-opt (append build-farm-guix-system-types
  164. build-farm-nix-system-types))
  165. "\\'")
  166. "Regexp matching full name of a job (including system).")
  167. (defun build-farm-job-name-specification (name version)
  168. "Return job name specification by NAME and VERSION."
  169. (concat name "-" version))
  170. (bui-define-current-args-accessors build-farm-current
  171. url-1 search-type search-args)
  172. (defun build-farm-current-url ()
  173. "Return build farm URL of the current buffer or default URL."
  174. ;; This procedure may be called from non-"build-farm" buffers.
  175. ;; Return `build-farm-url' in this case.
  176. (let ((entry-type (bui-current-entry-type)))
  177. (if (and entry-type
  178. (string-match-p "\\`build-farm"
  179. (symbol-name entry-type)))
  180. (build-farm-current-url-1)
  181. build-farm-url)))
  182. (defun build-farm-current-url-type ()
  183. "Return build farm type of the current buffer."
  184. (build-farm-url-type (build-farm-current-url)))
  185. (defun build-farm-check-project-support (&optional url)
  186. "Raise an error if URL build farm does not support projects."
  187. (when (eq 'cuirass (build-farm-url-type (or url build-farm-url)))
  188. (error "Cuirass does not have a notion of 'project'.
  189. Please use 'jobsets' instead")))
  190. (defun build-farm-get-entries (root-url entry-type search-type
  191. &rest args)
  192. "Receive ENTRY-TYPE entries from cache or build farm.
  193. See `build-farm-search-url' for the meaning of ROOT-URL,
  194. SEARCH-TYPE and ARGS."
  195. (unless (eq search-type 'fake)
  196. (cond
  197. ((memq entry-type '(project cuirass-jobset))
  198. (cl-case search-type
  199. ((id)
  200. (bui-entries-by-ids
  201. (build-farm-get-entries root-url entry-type 'all)
  202. args))
  203. ((name)
  204. (delq nil
  205. (mapcar (lambda (name)
  206. (bui-entry-by-param
  207. (build-farm-get-entries root-url entry-type 'all)
  208. 'name name))
  209. args)))
  210. ((all)
  211. ;; 'roots' mean: projects for Hydra; jobsets for Cuirass.
  212. (or (build-farm-cache-get root-url 'roots)
  213. (progn
  214. ;; Set 'roots-received' before the actual receiving
  215. ;; because there may be an error during this receiving.
  216. (build-farm-cache-set root-url 'roots-received t)
  217. (let ((entries (apply #'build-farm-get-entries-1
  218. root-url entry-type search-type args)))
  219. (build-farm-cache-set root-url 'roots entries)
  220. entries))))))
  221. (t
  222. (apply #'build-farm-get-entries-1
  223. root-url entry-type search-type args)))))
  224. (defun build-farm-get-entries-1 (root-url entry-type search-type
  225. &rest args)
  226. "Receive ENTRY-TYPE entries from build farm.
  227. See `build-farm-search-url' for the meaning of ROOT-URL,
  228. SEARCH-TYPE and ARGS."
  229. (let* ((url (apply #'build-farm-search-url
  230. root-url entry-type search-type args))
  231. (raw-entries (build-farm-receive-data url))
  232. (raw-entries (cond
  233. ((eq search-type 'id)
  234. ;; We expect multiple entries so wrap a single
  235. ;; ID entry into a list.
  236. (list raw-entries))
  237. (t raw-entries)))
  238. (entries (apply #'build-farm-modify-objects
  239. raw-entries
  240. (build-farm-filters entry-type))))
  241. entries))
  242. (defun build-farm-get-root-entries-once (root-type &optional url)
  243. "Return ROOT-TYPE entries for URL build farm.
  244. ROOT-TYPE should be `project' or `cuirass-jobset' symbol.
  245. If projects have already been received, return them from
  246. `build-farm-cache'. If URL is nil, use variable
  247. `build-farm-url'."
  248. (or url (setq url build-farm-url))
  249. (unless (build-farm-cache-get url 'roots-received)
  250. ;; If there is some error in `build-farm-receive-data', we don't
  251. ;; want to fail, because this procedure is used for minibuffer
  252. ;; readers.
  253. (with-demoted-errors "Error: %S"
  254. (require 'build-farm-project)
  255. (build-farm-get-entries url root-type 'all)))
  256. (build-farm-cache-get url 'roots))
  257. (defun build-farm-get-display (root-url entry-type search-type
  258. &rest args)
  259. "Search for ENTRY-TYPE entries and show results.
  260. ENTRY-TYPE should be `build', `jobset', etc.
  261. See `build-farm-search-url' for the meaning of ROOT-URL,
  262. SEARCH-TYPE and ARGS."
  263. (apply #'bui-list-get-display-entries
  264. (build-farm-symbol entry-type)
  265. root-url
  266. search-type args))
  267. (defun build-farm-message (entries _root-url search-type &rest _)
  268. "Display a message after showing ENTRIES of SEARCH-TYPE."
  269. ;; XXX Add more messages maybe.
  270. (when (null entries)
  271. (if (eq search-type 'fake)
  272. (message "Sorry, this operation is not supported.")
  273. (message "The build farm has returned no results."))))
  274. (defun build-farm-list-describe (&rest ids)
  275. "Describe 'build-farm' entries with IDS (list of identifiers)."
  276. (bui-display-entries
  277. (bui-entries-by-ids (bui-current-entries) ids)
  278. (bui-current-entry-type) 'info
  279. ;; Hydra provides an API to receive a build/jobset by its ID/name,
  280. ;; but only a single one. Thus, to receive info on multiple
  281. ;; builds/jobsets, we have to request a build farm multiple times.
  282. ;; This may take a lot of time, so getting multiple builds/jobsets is
  283. ;; not supported, and we use the 'fake' search type.
  284. (list (build-farm-current-url) 'fake)
  285. 'add))
  286. ;;; Readers
  287. (defun build-farm-project-names (&optional url)
  288. "Return projects for URL build farm."
  289. (mapcar #'bui-entry-id
  290. (build-farm-get-root-entries-once 'project url)))
  291. (cl-defun build-farm-jobset-names (&key url project)
  292. "Return jobsets for PROJECT from URL build farm."
  293. (if (eq 'cuirass (build-farm-url-type url))
  294. (mapcar (lambda (entry)
  295. (bui-entry-non-void-value entry 'name))
  296. (build-farm-get-root-entries-once 'cuirass-jobset url))
  297. (bui-entry-non-void-value
  298. (bui-entry-by-id (build-farm-get-root-entries-once 'project url)
  299. project)
  300. 'jobsets)))
  301. (build-farm-define-readers
  302. :require-match nil
  303. :single-reader build-farm-read-job
  304. :single-prompt "Job: ")
  305. (build-farm-define-readers
  306. :require-match nil
  307. :completions-getter build-farm-system-types
  308. :single-reader build-farm-read-system
  309. :single-prompt "System: ")
  310. (cl-defun build-farm-read-project (&key url prompt initial-input)
  311. "Read project name for Hydra URL from minibuffer.
  312. See `completing-read' for PROMPT and INITIAL-INPUT."
  313. (build-farm-check-project-support url)
  314. (build-farm-completing-read
  315. (or prompt "Project: ")
  316. (build-farm-project-names url)
  317. nil nil initial-input nil nil))
  318. (cl-defun build-farm-read-jobset (&key url project prompt initial-input)
  319. "Read jobset for PROJECT from URL build farm from minibuffer.
  320. See `completing-read' for PROMPT and INITIAL-INPUT."
  321. (build-farm-completing-read
  322. (or prompt "Jobset: ")
  323. (build-farm-jobset-names :url url
  324. :project project)
  325. nil nil initial-input))
  326. ;;; Filters for processing raw entries
  327. (defun build-farm-filter-names (entry name-alist)
  328. "Replace names of ENTRY parameters using NAME-ALIST.
  329. Each element of NAME-ALIST is (OLD-NAME . NEW-NAME) pair."
  330. (mapcar (lambda (param)
  331. (pcase param
  332. (`(,name . ,val)
  333. (let ((new-name (bui-assq-value name-alist name)))
  334. (if new-name
  335. (cons new-name val)
  336. param)))))
  337. entry))
  338. (defun build-farm-filter-boolean (entry params)
  339. "Convert number PARAMS (0/1) of ENTRY to boolean values (nil/t)."
  340. (mapcar (lambda (param)
  341. (pcase param
  342. (`(,name . ,val)
  343. (if (memq name params)
  344. (cons name (build-farm-number->bool val))
  345. param))))
  346. entry))
  347. ;;; Wrappers for defined variables
  348. (defun build-farm-symbol (&rest symbols)
  349. "Return `build-farm-...' symbol.
  350. Where '...' is made from SYMBOLS."
  351. (apply #'bui-make-symbol 'build-farm symbols))
  352. (defun build-farm-symbol-value (entry-type symbol)
  353. "Return SYMBOL's value for ENTRY-TYPE."
  354. (symbol-value (build-farm-symbol entry-type symbol)))
  355. (defun build-farm-search-url (root-url entry-type search-type
  356. &rest args)
  357. "Return URL to receive ENTRY-TYPE entries from build farm.
  358. ROOT-URL is the url of the build farm (from `build-farm-url-alist').
  359. SEARCH-TYPE is one of the types defined by `build-farm-define-entry-type'.
  360. ARGS are passed to the according URL function."
  361. (apply (bui-assq-value (build-farm-symbol-value
  362. entry-type 'search-types)
  363. search-type)
  364. ;; `:root-url' should be the last argument because `args' may
  365. ;; contain non-keyword arguments.
  366. (append args `(:root-url ,root-url))))
  367. (defun build-farm-filters (entry-type)
  368. "Return a list of filters for ENTRY-TYPE."
  369. (build-farm-symbol-value entry-type 'filters))
  370. ;;; Interface definers
  371. (defmacro build-farm-define-entry-type (entry-type &rest args)
  372. "Define general code for ENTRY-TYPE.
  373. Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
  374. Required keywords:
  375. - `:search-types' - default value of the generated
  376. `build-farm-ENTRY-TYPE-search-types' variable.
  377. Optional keywords:
  378. - `:filters' - default value of the generated
  379. `build-farm-ENTRY-TYPE-filters' variable.
  380. - `:filter-names' - if specified, a generated
  381. `build-farm-ENTRY-TYPE-filter-names' function for filtering
  382. these names will be added to `build-farm-ENTRY-TYPE-filters'
  383. variable.
  384. - `:filter-boolean-params' - if specified, a generated
  385. `build-farm-ENTRY-TYPE-filter-boolean' function for filtering
  386. these names will be added to `build-farm-ENTRY-TYPE-filters'
  387. variable.
  388. The rest keyword arguments are passed to
  389. `bui-define-entry-type' macro."
  390. (declare (indent 1))
  391. (let* ((entry-type-str (symbol-name entry-type))
  392. (full-entry-type (build-farm-symbol entry-type))
  393. (prefix (concat "build-farm-" entry-type-str))
  394. (search-types-var (intern (concat prefix "-search-types")))
  395. (filters-var (intern (concat prefix "-filters")))
  396. (get-fun (intern (concat prefix "-get-entries"))))
  397. (bui-plist-let args
  398. ((search-types-val :search-types)
  399. (filters-val :filters)
  400. (filter-names-val :filter-names)
  401. (filter-bool-val :filter-boolean-params))
  402. `(progn
  403. (defvar ,search-types-var ,search-types-val
  404. ,(format "\
  405. Alist of search types and according URL functions.
  406. Functions are used to define URL to receive '%s' entries."
  407. entry-type-str))
  408. (defvar ,filters-var ,filters-val
  409. ,(format "\
  410. List of filters for '%s' parameters.
  411. Each filter is a function that should take an entry as a single
  412. argument, and should also return an entry."
  413. entry-type-str))
  414. ,(when filter-bool-val
  415. (let ((filter-bool-var (intern (concat prefix
  416. "-filter-boolean-params")))
  417. (filter-bool-fun (intern (concat prefix
  418. "-filter-boolean"))))
  419. `(progn
  420. (defvar ,filter-bool-var ,filter-bool-val
  421. ,(format "\
  422. List of '%s' parameters that should be transformed to boolean values."
  423. entry-type-str))
  424. (defun ,filter-bool-fun (entry)
  425. ,(format "\
  426. Run `build-farm-filter-boolean' with `%S' variable."
  427. filter-bool-var)
  428. (build-farm-filter-boolean entry ,filter-bool-var))
  429. (setq ,filters-var
  430. (cons ',filter-bool-fun ,filters-var)))))
  431. ;; Do not move this clause up!: name filtering should be
  432. ;; performed before any other filtering, so this filter should
  433. ;; be consed after the boolean filter.
  434. ,(when filter-names-val
  435. (let* ((filter-names-var (intern (concat prefix
  436. "-filter-names")))
  437. (filter-names-fun filter-names-var))
  438. `(progn
  439. (defvar ,filter-names-var ,filter-names-val
  440. ,(format "\
  441. Alist of '%s' parameter names returned by the build farm API and
  442. names used internally by the elisp code of this package."
  443. entry-type-str))
  444. (defun ,filter-names-fun (entry)
  445. ,(format "\
  446. Run `build-farm-filter-names' with `%S' variable."
  447. filter-names-var)
  448. (build-farm-filter-names entry ,filter-names-var))
  449. (setq ,filters-var
  450. (cons ',filter-names-fun ,filters-var)))))
  451. (defun ,get-fun (root-url search-type &rest args)
  452. ,(format "\
  453. Receive '%s' entries.
  454. See `build-farm-get-entries' for details."
  455. entry-type-str)
  456. (apply #'build-farm-get-entries
  457. root-url ',entry-type search-type args))
  458. (bui-define-groups ,full-entry-type
  459. :parent-group build-farm
  460. :parent-faces-group build-farm-faces)
  461. (bui-define-entry-type ,full-entry-type
  462. :message-function 'build-farm-message
  463. ,@%foreign-args)))))
  464. (defmacro build-farm-define-interface (entry-type buffer-type &rest args)
  465. "Define BUFFER-TYPE interface for displaying ENTRY-TYPE entries.
  466. This macro should be called after calling
  467. `build-farm-define-entry-type' with the same ENTRY-TYPE.
  468. ARGS are passed to `bui-define-interface' macro."
  469. (declare (indent 2))
  470. `(bui-define-interface ,(build-farm-symbol entry-type) ,buffer-type
  471. :get-entries-function ',(build-farm-symbol entry-type 'get-entries)
  472. ,@args))
  473. (provide 'build-farm)
  474. ;;; build-farm.el ends here