;;; guix-hydra-build.el --- Interface for Hydra builds -*- lexical-binding: t -*-
;; Copyright © 2015 Alex Kost <alezost@gmail.com>
;; This file is part of GNU Guix.
;; GNU Guix is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Guix is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This file provides an interface for displaying Hydra builds in
;; 'list' and 'info' buffers.
;;; Code:
(require 'cl-lib)
(require 'guix-buffer)
(require 'guix-list)
(require 'guix-info)
(require 'guix-hydra)
(require 'guix-build-log)
(require 'guix-utils)
(guix-hydra-define-entry-type hydra-build
:search-types '((latest . guix-hydra-build-latest-api-url)
(queue . guix-hydra-build-queue-api-url))
:filters '(guix-hydra-build-filter-status)
:filter-names '((nixname . name)
(buildstatus . build-status)
(timestamp . time))
:filter-boolean-params '(finished busy))
(defun guix-hydra-build-get-display (search-type &rest args)
"Search for Hydra builds and show results."
(apply #'guix-list-get-display-entries
'hydra-build search-type args))
(cl-defun guix-hydra-build-latest-prompt-args (&key project jobset
job system)
"Prompt for and return a list of 'latest builds' arguments."
(let* ((number (read-number "Number of latest builds: "))
(project (if current-prefix-arg
(guix-hydra-read-project nil project)
project))
(jobset (if current-prefix-arg
(guix-hydra-read-jobset nil jobset)
jobset))
(job-or-name (if current-prefix-arg
(guix-hydra-read-job nil job)
job))
(job (and job-or-name
(string-match-p guix-hydra-job-regexp
job-or-name)
job-or-name))
(system (if (and (not job)
(or current-prefix-arg
(and job-or-name (not system))))
(if job-or-name
(guix-while-null
(guix-hydra-read-system
(concat job-or-name ".") system))
(guix-hydra-read-system nil system))
system))
(job (or job
(and job-or-name
(concat job-or-name "." system)))))
(list number
:project project
:jobset jobset
:job job
:system system)))
(defun guix-hydra-build-view-log (id)
"View build log of a hydra build ID."
(guix-build-log-find-file (guix-hydra-build-log-url id)))
;;; Defining URLs
(defun guix-hydra-build-url (id)
"Return Hydra URL of a build ID."
(guix-hydra-url "build/" (number-to-string id)))
(defun guix-hydra-build-log-url (id)
"Return Hydra URL of the log file of a build ID."
(concat (guix-hydra-build-url id) "/log/raw"))
(cl-defun guix-hydra-build-latest-api-url
(number &key project jobset job system)
"Return Hydra API URL to receive latest NUMBER of builds."
(guix-hydra-api-url "latestbuilds"
`(("nr" . ,number)
("project" . ,project)
("jobset" . ,jobset)
("job" . ,job)
("system" . ,system))))
(defun guix-hydra-build-queue-api-url (number)
"Return Hydra API URL to receive the NUMBER of queued builds."
(guix-hydra-api-url "queue"
`(("nr" . ,number))))
;;; Filters for processing raw entries
(defun guix-hydra-build-filter-status (entry)
"Add 'status' parameter to 'hydra-build' ENTRY."
(let ((status (if (guix-entry-value entry 'finished)
(guix-hydra-build-status-number->name
(guix-entry-value entry 'build-status))
(if (guix-entry-value entry 'busy)
'running
'scheduled))))
(cons `(status . ,status)
entry)))
;;; Build status
(defface guix-hydra-build-status-running
'((t :inherit bold))
"Face used if hydra build is not finished."
:group 'guix-hydra-build-faces)
(defface guix-hydra-build-status-scheduled
'((t))
"Face used if hydra build is scheduled."
:group 'guix-hydra-build-faces)
(defface guix-hydra-build-status-succeeded
'((t :inherit success))
"Face used if hydra build succeeded."
:group 'guix-hydra-build-faces)
(defface guix-hydra-build-status-cancelled
'((t :inherit warning))
"Face used if hydra build was cancelled."
:group 'guix-hydra-build-faces)
(defface guix-hydra-build-status-failed
'((t :inherit error))
"Face used if hydra build failed."
:group 'guix-hydra-build-faces)
(defvar guix-hydra-build-status-alist
'((0 . succeeded)
(1 . failed-build)
(2 . failed-dependency)
(3 . failed-other)
(4 . cancelled))
"Alist of hydra build status numbers and status names.
Status numbers are returned by Hydra API, names (symbols) are
used internally by the elisp code of this package.")
(defun guix-hydra-build-status-number->name (number)
"Convert build status number to a name.
See `guix-hydra-build-status-alist'."
(guix-assq-value guix-hydra-build-status-alist number))
(defun guix-hydra-build-status-string (status)
"Return a human readable string for build STATUS."
(cl-case status
(scheduled
(guix-get-string "Scheduled" 'guix-hydra-build-status-scheduled))
(running
(guix-get-string "Running" 'guix-hydra-build-status-running))
(succeeded
(guix-get-string "Succeeded" 'guix-hydra-build-status-succeeded))
(cancelled
(guix-get-string "Cancelled" 'guix-hydra-build-status-cancelled))
(failed-build
(guix-hydra-build-status-fail-string))
(failed-dependency
(guix-hydra-build-status-fail-string "dependency"))
(failed-other
(guix-hydra-build-status-fail-string "other"))))
(defun guix-hydra-build-status-fail-string (&optional reason)
"Return a string for a failed build."
(let ((base (guix-get-string "Failed" 'guix-hydra-build-status-failed)))
(if reason
(concat base " (" reason ")")
base)))
(defun guix-hydra-build-finished? (entry)
"Return non-nil, if hydra build was finished."
(guix-entry-value entry 'finished))
(defun guix-hydra-build-running? (entry)
"Return non-nil, if hydra build is running."
(eq (guix-entry-value entry 'status)
'running))
(defun guix-hydra-build-scheduled? (entry)
"Return non-nil, if hydra build is scheduled."
(eq (guix-entry-value entry 'status)
'scheduled))
(defun guix-hydra-build-succeeded? (entry)
"Return non-nil, if hydra build succeeded."
(eq (guix-entry-value entry 'status)
'succeeded))
(defun guix-hydra-build-cancelled? (entry)
"Return non-nil, if hydra build was cancelled."
(eq (guix-entry-value entry 'status)
'cancelled))
(defun guix-hydra-build-failed? (entry)
"Return non-nil, if hydra build failed."
(memq (guix-entry-value entry 'status)
'(failed-build failed-dependency failed-other)))
;;; Hydra build 'info'
(guix-hydra-info-define-interface hydra-build
:mode-name "Hydra-Build-Info"
:buffer-name "*Guix Hydra Build Info*"
:format '((name ignore (simple guix-info-heading))
ignore
guix-hydra-build-info-insert-url
(time format (time))
(status format guix-hydra-build-info-insert-status)
(project format (format guix-hydra-build-project))
(jobset format (format guix-hydra-build-jobset))
(job format (format guix-hydra-build-job))
(system format (format guix-hydra-build-system))
(priority format (format))))
(defface guix-hydra-build-info-project
'((t :inherit link))
"Face for project names."
:group 'guix-hydra-build-info-faces)
(defface guix-hydra-build-info-jobset
'((t :inherit link))
"Face for jobsets."
:group 'guix-hydra-build-info-faces)
(defface guix-hydra-build-info-job
'((t :inherit link))
"Face for jobs."
:group 'guix-hydra-build-info-faces)
(defface guix-hydra-build-info-system
'((t :inherit link))
"Face for system names."
:group 'guix-hydra-build-info-faces)
(defmacro guix-hydra-build-define-button (name)
"Define `guix-hydra-build-NAME' button."
(let* ((name-str (symbol-name name))
(button-name (intern (concat "guix-hydra-build-" name-str)))
(face-name (intern (concat "guix-hydra-build-info-" name-str)))
(keyword (intern (concat ":" name-str))))
`(define-button-type ',button-name
:supertype 'guix
'face ',face-name
'help-echo ,(format "\
Show latest builds for this %s (with prefix, prompt for all parameters)"
name-str)
'action (lambda (btn)
(let ((args (guix-hydra-build-latest-prompt-args
,keyword (button-label btn))))
(apply #'guix-hydra-build-get-display
'latest args))))))
(guix-hydra-build-define-button project)
(guix-hydra-build-define-button jobset)
(guix-hydra-build-define-button job)
(guix-hydra-build-define-button system)
(defun guix-hydra-build-info-insert-url (entry)
"Insert Hydra URL for the build ENTRY."
(guix-insert-button (guix-hydra-build-url (guix-entry-id entry))
'guix-url)
(when (guix-hydra-build-finished? entry)
(guix-info-insert-indent)
(guix-info-insert-action-button
"Build log"
(lambda (btn)
(guix-hydra-build-view-log (button-get btn 'id)))
"View build log"
'id (guix-entry-id entry))))
(defun guix-hydra-build-info-insert-status (status &optional _)
"Insert a string with build STATUS."
(insert (guix-hydra-build-status-string status)))
;;; Hydra build 'list'
(guix-hydra-list-define-interface hydra-build
:mode-name "Hydra-Build-List"
:buffer-name "*Guix Hydra Build List*"
:format '((name nil 30 t)
(system nil 16 t)
(status guix-hydra-build-list-get-status 20 t)
(project nil 10 t)
(jobset nil 17 t)
(time guix-list-get-time 20 t)))
(let ((map guix-hydra-build-list-mode-map))
(define-key map (kbd "B") 'guix-hydra-build-list-latest-builds)
(define-key map (kbd "L") 'guix-hydra-build-list-view-log))
(defun guix-hydra-build-list-get-status (status &optional _)
"Return a string for build STATUS."
(guix-hydra-build-status-string status))
(defun guix-hydra-build-list-latest-builds (number &rest args)
"Display latest NUMBER of Hydra builds of the current job.
Interactively, prompt for NUMBER. With prefix argument, prompt
for all ARGS."
(interactive
(let ((entry (guix-list-current-entry)))
(guix-hydra-build-latest-prompt-args
:project (guix-entry-value entry 'project)
:jobset (guix-entry-value entry 'name)
:job (guix-entry-value entry 'job)
:system (guix-entry-value entry 'system))))
(apply #'guix-hydra-latest-builds number args))
(defun guix-hydra-build-list-view-log ()
"View build log of the current Hydra build."
(interactive)
(guix-hydra-build-view-log (guix-list-current-id)))
;;; Interactive commands
;;;###autoload
(defun guix-hydra-latest-builds (number &rest args)
"Display latest NUMBER of Hydra builds.
ARGS are the same arguments as for `guix-hydra-build-latest-api-url'.
Interactively, prompt for NUMBER. With prefix argument, prompt
for all ARGS."
(interactive (guix-hydra-build-latest-prompt-args))
(apply #'guix-hydra-build-get-display
'latest number args))
;;;###autoload
(defun guix-hydra-queued-builds (number)
"Display the NUMBER of queued Hydra builds."
(interactive "NNumber of queued builds: ")
(guix-hydra-build-get-display 'queue number))
(provide 'guix-hydra-build)
;;; guix-hydra-build.el ends here