github.scm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016 Ben Woodcroft <donttrustben@gmail.com>
  3. ;;; Copyright © 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
  4. ;;; Copyright © 2018 Eric Bavier <bavier@member.fsf.org>
  5. ;;; Copyright © 2019 Arun Isaac <arunisaac@systemreboot.net>
  6. ;;; Copyright © 2019 Efraim Flashner <efraim@flashner.co.il>
  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 import github)
  23. #:use-module (ice-9 match)
  24. #:use-module (srfi srfi-1)
  25. #:use-module (srfi srfi-26)
  26. #:use-module (srfi srfi-34)
  27. #:use-module (guix utils)
  28. #:use-module (guix i18n)
  29. #:use-module (guix diagnostics)
  30. #:use-module ((guix download) #:prefix download:)
  31. #:use-module ((guix git-download) #:prefix download:)
  32. #:use-module (guix import utils)
  33. #:use-module (guix import json)
  34. #:use-module (json)
  35. #:use-module (guix packages)
  36. #:use-module (guix upstream)
  37. #:use-module (guix http-client)
  38. #:use-module (web uri)
  39. #:export (%github-updater))
  40. (define (find-extension url)
  41. "Return the extension of the archive e.g. '.tar.gz' given a URL, or
  42. false if none is recognized"
  43. (find (lambda (x) (string-suffix? x url))
  44. (list ".tar.gz" ".tar.bz2" ".tar.xz" ".zip" ".tar"
  45. ".tgz" ".tbz" ".love")))
  46. (define (updated-github-url old-package new-version)
  47. ;; Return a url for the OLD-PACKAGE with NEW-VERSION. If no source url in
  48. ;; the OLD-PACKAGE is a GitHub url, then return false.
  49. (define (updated-url url)
  50. (if (string-prefix? "https://github.com/" url)
  51. (let ((ext (or (find-extension url) ""))
  52. (name (package-upstream-name old-package))
  53. (version (package-version old-package))
  54. (prefix (string-append "https://github.com/"
  55. (github-user-slash-repository url)))
  56. (repo (github-repository url)))
  57. (cond
  58. ((string-suffix? (string-append "/tarball/v" version) url)
  59. (string-append prefix "/tarball/v" new-version))
  60. ((string-suffix? (string-append "/tarball/" version) url)
  61. (string-append prefix "/tarball/" new-version))
  62. ((string-suffix? (string-append "/archive/v" version ext) url)
  63. (string-append prefix "/archive/v" new-version ext))
  64. ((string-suffix? (string-append "/archive/" version ext) url)
  65. (string-append prefix "/archive/" new-version ext))
  66. ((string-suffix? (string-append "/archive/" name "-" version ext)
  67. url)
  68. (string-append prefix "/archive/" name "-" new-version ext))
  69. ((string-suffix? (string-append "/releases/download/v" version "/"
  70. name "-" version ext)
  71. url)
  72. (string-append prefix "/releases/download/v" new-version "/" name
  73. "-" new-version ext))
  74. ((string-suffix? (string-append "/releases/download/" version "/"
  75. name "-" version ext)
  76. url)
  77. (string-append prefix "/releases/download/" new-version "/" name
  78. "-" new-version ext))
  79. ((string-suffix? (string-append "/releases/download/" version "/"
  80. repo "-" version ext)
  81. url)
  82. (string-append prefix "/releases/download/" new-version "/" repo
  83. "-" new-version ext))
  84. ((string-suffix? (string-append "/releases/download/" repo "-"
  85. version "/" repo "-" version ext)
  86. url)
  87. (string-append "/releases/download/" repo "-" version "/" repo "-"
  88. version ext))
  89. (#t #f))) ; Some URLs are not recognised.
  90. #f))
  91. (match (package-source old-package)
  92. ((? origin? origin)
  93. (let ((source-uri (origin-uri origin))
  94. (fetch-method (origin-method origin)))
  95. (cond
  96. ((eq? fetch-method download:url-fetch)
  97. (match source-uri
  98. ((? string?)
  99. (updated-url source-uri))
  100. ((source-uri ...)
  101. (find updated-url source-uri))))
  102. ((and (eq? fetch-method download:git-fetch)
  103. (string-prefix? "https://github.com/"
  104. (download:git-reference-url source-uri)))
  105. (download:git-reference-url source-uri))
  106. (else #f))))
  107. (_ #f)))
  108. (define (github-package? package)
  109. "Return true if PACKAGE is a package from GitHub, else false."
  110. (->bool (updated-github-url package "dummy")))
  111. (define (github-repository url)
  112. "Return a string e.g. bedtools2 of the name of the repository, from a string
  113. URL of the form 'https://github.com/arq5x/bedtools2/archive/v2.24.0.tar.gz'"
  114. (match (string-split (uri-path (string->uri url)) #\/)
  115. ((_ owner project . rest)
  116. (string-append (basename project ".git")))))
  117. (define (github-user-slash-repository url)
  118. "Return a string e.g. arq5x/bedtools2 of the owner and the name of the
  119. repository separated by a forward slash, from a string URL of the form
  120. 'https://github.com/arq5x/bedtools2/archive/v2.24.0.tar.gz'"
  121. (match (string-split (uri-path (string->uri url)) #\/)
  122. ((_ owner project . rest)
  123. (string-append owner "/" (basename project ".git")))))
  124. (define %github-token
  125. ;; Token to be passed to Github.com to avoid the 60-request per hour
  126. ;; limit, or #f.
  127. (make-parameter (getenv "GUIX_GITHUB_TOKEN")))
  128. (define (fetch-releases-or-tags url)
  129. "Fetch the list of \"releases\" or, if it's empty, the list of tags for the
  130. repository at URL. Return the corresponding JSON dictionaries (alists),
  131. or #f if the information could not be retrieved.
  132. We look at both /releases and /tags because the \"release\" feature of GitHub
  133. is little used; often, people simply provide a tag. What's confusing is that
  134. tags show up in the \"Releases\" tab of the web UI. For instance,
  135. 'https://github.com/aconchillo/guile-json/releases' shows a number of
  136. \"releases\" (really: tags), whereas
  137. 'https://api.github.com/repos/aconchillo/guile-json/releases' returns the
  138. empty list."
  139. (define release-url
  140. (string-append "https://api.github.com/repos/"
  141. (github-user-slash-repository url)
  142. "/releases"))
  143. (define tag-url
  144. (string-append "https://api.github.com/repos/"
  145. (github-user-slash-repository url)
  146. "/tags"))
  147. (define headers
  148. ;; Ask for version 3 of the API as suggested at
  149. ;; <https://developer.github.com/v3/>.
  150. `((Accept . "application/vnd.github.v3+json")
  151. (user-agent . "GNU Guile")
  152. ,@(if (%github-token)
  153. `((Authorization . ,(string-append "token " (%github-token))))
  154. '())))
  155. (guard (c ((and (http-get-error? c)
  156. (= 404 (http-get-error-code c)))
  157. (warning (G_ "~a is unreachable (~a)~%")
  158. release-url (http-get-error-code c))
  159. '#())) ;return an empty release set
  160. (let* ((port (http-fetch release-url #:headers headers))
  161. (result (json->scm port)))
  162. (close-port port)
  163. (match result
  164. (#()
  165. ;; We got the empty list, presumably because the user didn't use GitHub's
  166. ;; "release" mechanism, but hopefully they did use Git tags.
  167. (json-fetch tag-url #:headers headers))
  168. (x x)))))
  169. (define (latest-released-version url package-name)
  170. "Return a string of the newest released version name given a string URL like
  171. 'https://github.com/arq5x/bedtools2/archive/v2.24.0.tar.gz' and the name of
  172. the package e.g. 'bedtools2'. Return #f if there is no releases"
  173. (define (pre-release? x)
  174. (assoc-ref x "prerelease"))
  175. (define (release->version release)
  176. (let ((tag (or (assoc-ref release "tag_name") ;a "release"
  177. (assoc-ref release "name"))) ;a tag
  178. (name-length (string-length package-name)))
  179. (cond
  180. ;; some tags include the name of the package e.g. "fdupes-1.51"
  181. ;; so remove these
  182. ((and (< name-length (string-length tag))
  183. (string=? (string-append package-name "-")
  184. (substring tag 0 (+ name-length 1))))
  185. (substring tag (+ name-length 1)))
  186. ;; some tags start with a "v" e.g. "v0.25.0"
  187. ;; or with the word "version" e.g. "version.2.1"
  188. ;; where some are just the version number
  189. ((string-prefix? "version" tag)
  190. (if (char-set-contains? char-set:digit (string-ref tag 7))
  191. (substring tag 7)
  192. (substring tag 8)))
  193. ((string-prefix? "v" tag)
  194. (substring tag 1))
  195. ;; Finally, reject tags that don't start with a digit:
  196. ;; they may not represent a release.
  197. ((and (not (string-null? tag))
  198. (char-set-contains? char-set:digit
  199. (string-ref tag 0)))
  200. tag)
  201. (else #f))))
  202. (let* ((json (and=> (fetch-releases-or-tags url)
  203. vector->list)))
  204. (if (eq? json #f)
  205. (if (%github-token)
  206. (error "Error downloading release information through the GitHub
  207. API when using a GitHub token")
  208. (error "Error downloading release information through the GitHub
  209. API. This may be fixed by using an access token and setting the environment
  210. variable GUIX_GITHUB_TOKEN, for instance one procured from
  211. https://github.com/settings/tokens"))
  212. (match (sort (filter-map release->version
  213. (match (remove pre-release? json)
  214. (() json) ; keep everything
  215. (releases releases)))
  216. version>?)
  217. ((latest-release . _) latest-release)
  218. (() #f)))))
  219. (define (latest-release pkg)
  220. "Return an <upstream-source> for the latest release of PKG."
  221. (define (origin-github-uri origin)
  222. (match (origin-uri origin)
  223. ((? string? url)
  224. url) ;surely a github.com URL
  225. ((? download:git-reference? ref)
  226. (download:git-reference-url ref))
  227. ((urls ...)
  228. (find (cut string-contains <> "github.com") urls))))
  229. (let* ((source-uri (origin-github-uri (package-source pkg)))
  230. (name (package-name pkg))
  231. (newest-version (latest-released-version source-uri name)))
  232. (if newest-version
  233. (upstream-source
  234. (package name)
  235. (version newest-version)
  236. (urls (list (updated-github-url pkg newest-version))))
  237. #f))) ; On GitHub but no proper releases
  238. (define %github-updater
  239. (upstream-updater
  240. (name 'github)
  241. (description "Updater for GitHub packages")
  242. (pred github-package?)
  243. (latest latest-release)))