github.scm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016 Ben Woodcroft <donttrustben@gmail.com>
  3. ;;; Copyright © 2017-2020, 2022 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. ;;; Copyright © 2022 Maxime Devos <maximedevos@telenet.be>
  8. ;;; Copyright © 2022 Hartmut Goebel <h.goebel@crazy-compilers.com>
  9. ;;;
  10. ;;; This file is part of GNU Guix.
  11. ;;;
  12. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  13. ;;; under the terms of the GNU General Public License as published by
  14. ;;; the Free Software Foundation; either version 3 of the License, or (at
  15. ;;; your option) any later version.
  16. ;;;
  17. ;;; GNU Guix is distributed in the hope that it will be useful, but
  18. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  19. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. ;;; GNU General Public License for more details.
  21. ;;;
  22. ;;; You should have received a copy of the GNU General Public License
  23. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  24. (define-module (guix import github)
  25. #:use-module (ice-9 match)
  26. #:use-module (srfi srfi-1)
  27. #:use-module (srfi srfi-26)
  28. #:use-module (srfi srfi-34)
  29. #:use-module (srfi srfi-71)
  30. #:use-module (guix utils)
  31. #:use-module (guix i18n)
  32. #:use-module (guix diagnostics)
  33. #:use-module ((guix ui) #:select (display-hint))
  34. #:use-module ((guix download) #:prefix download:)
  35. #:use-module ((guix git-download) #:prefix download:)
  36. #:autoload (guix build download) (open-connection-for-uri)
  37. #:use-module (guix import utils)
  38. #:use-module (json)
  39. #:use-module (guix packages)
  40. #:use-module (guix upstream)
  41. #:use-module (guix http-client)
  42. #:use-module (web uri)
  43. #:use-module (web response)
  44. #:export (%github-api %github-updater))
  45. ;; For tests.
  46. (define %github-api (make-parameter "https://api.github.com"))
  47. (define (find-extension url)
  48. "Return the extension of the archive e.g. '.tar.gz' given a URL, or
  49. false if none is recognized"
  50. (find (lambda (x) (string-suffix? x url))
  51. (list ".tar.gz" ".tar.bz2" ".tar.xz" ".zip" ".tar"
  52. ".tgz" ".tbz" ".love")))
  53. (define (updated-github-url old-package new-version)
  54. ;; Return a url for the OLD-PACKAGE with NEW-VERSION. If no source url in
  55. ;; the OLD-PACKAGE is a GitHub url, then return false.
  56. (define (updated-url url)
  57. (if (string-prefix? "https://github.com/" url)
  58. (let ((ext (or (find-extension url) ""))
  59. (name (package-upstream-name old-package))
  60. (version (package-version old-package))
  61. (prefix (string-append "https://github.com/"
  62. (github-user-slash-repository url)))
  63. (repo (github-repository url)))
  64. (cond
  65. ((string-suffix? (string-append "/tarball/v" version) url)
  66. (string-append prefix "/tarball/v" new-version))
  67. ((string-suffix? (string-append "/tarball/" version) url)
  68. (string-append prefix "/tarball/" new-version))
  69. ((string-suffix? (string-append "/archive/v" version ext) url)
  70. (string-append prefix "/archive/v" new-version ext))
  71. ((string-suffix? (string-append "/archive/" version ext) url)
  72. (string-append prefix "/archive/" new-version ext))
  73. ((string-suffix? (string-append "/archive/" name "-" version ext)
  74. url)
  75. (string-append prefix "/archive/" name "-" new-version ext))
  76. ((string-suffix? (string-append "/releases/download/v" version "/"
  77. name "-" version ext)
  78. url)
  79. (string-append prefix "/releases/download/v" new-version "/" name
  80. "-" new-version ext))
  81. ((string-suffix? (string-append "/releases/download/" version "/"
  82. name "-" version ext)
  83. url)
  84. (string-append prefix "/releases/download/" new-version "/" name
  85. "-" new-version ext))
  86. ((string-suffix? (string-append "/releases/download/" version "/"
  87. repo "-" version ext)
  88. url)
  89. (string-append prefix "/releases/download/" new-version "/" repo
  90. "-" new-version ext))
  91. ((string-suffix? (string-append "/releases/download/" repo "-"
  92. version "/" repo "-" version ext)
  93. url)
  94. (string-append prefix "/releases/download/" repo "-" new-version "/"
  95. repo "-" new-version ext))
  96. (#t #f))) ; Some URLs are not recognised.
  97. #f))
  98. (match (package-source old-package)
  99. ((? origin? origin)
  100. (let ((source-uri (origin-uri origin))
  101. (fetch-method (origin-method origin)))
  102. (cond
  103. ((eq? fetch-method download:url-fetch)
  104. (match source-uri
  105. ((? string?)
  106. (updated-url source-uri))
  107. ((source-uri ...)
  108. (find updated-url source-uri))))
  109. ((and (eq? fetch-method download:git-fetch)
  110. (string-prefix? "https://github.com/"
  111. (download:git-reference-url source-uri)))
  112. (download:git-reference-url source-uri))
  113. (else #f))))
  114. (_ #f)))
  115. (define (github-package? package)
  116. "Return true if PACKAGE is a package from GitHub, else false."
  117. (->bool (updated-github-url package "dummy")))
  118. (define (github-repository url)
  119. "Return a string e.g. bedtools2 of the name of the repository, from a string
  120. URL of the form '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 (basename project ".git")))))
  124. (define (github-user-slash-repository url)
  125. "Return a string e.g. arq5x/bedtools2 of the owner and the name of the
  126. repository separated by a forward slash, from a string URL of the form
  127. 'https://github.com/arq5x/bedtools2/archive/v2.24.0.tar.gz'"
  128. (match (string-split (uri-path (string->uri url)) #\/)
  129. ((_ owner project . rest)
  130. (string-append owner "/" (basename project ".git")))))
  131. (define %github-token
  132. ;; Token to be passed to Github.com to avoid the 60-request per hour
  133. ;; limit, or #f.
  134. (make-parameter (getenv "GUIX_GITHUB_TOKEN")))
  135. (define %rate-limit-reset-time
  136. ;; Time (seconds since the Epoch, UTC) when the rate limit for GitHub
  137. ;; requests will be reset, or #f if the rate limit hasn't been reached.
  138. #f)
  139. (define (update-rate-limit-reset-time! headers)
  140. "Update the rate limit reset time based on HEADERS, the HTTP response
  141. headers."
  142. (match (assq-ref headers 'x-ratelimit-reset)
  143. ((= string->number (? number? reset))
  144. (set! %rate-limit-reset-time reset)
  145. reset)
  146. (_
  147. ;; This shouldn't happen.
  148. (warning
  149. (G_ "GitHub HTTP response lacks 'X-RateLimit-Reset' header~%"))
  150. 0)))
  151. (define (request-rate-limit-reached?)
  152. "Return true if the rate limit has been reached."
  153. (and %rate-limit-reset-time
  154. (match (< (car (gettimeofday)) %rate-limit-reset-time)
  155. (#t #t)
  156. (#f
  157. (set! %rate-limit-reset-time #f)
  158. #f))))
  159. (define (fetch-releases-or-tags url)
  160. "Fetch the list of \"releases\" or, if it's empty, the list of tags for the
  161. repository at URL. Return the corresponding JSON dictionaries (alists),
  162. or #f if the information could not be retrieved.
  163. We look at both /releases and /tags because the \"release\" feature of GitHub
  164. is little used; often, people simply provide a tag. What's confusing is that
  165. tags show up in the \"Releases\" tab of the web UI. For instance,
  166. 'https://github.com/aconchillo/guile-json/releases' shows a number of
  167. \"releases\" (really: tags), whereas
  168. 'https://api.github.com/repos/aconchillo/guile-json/releases' returns the
  169. empty list."
  170. (define release-url
  171. (string-append (%github-api) "/repos/"
  172. (github-user-slash-repository url)
  173. "/releases"))
  174. (define tag-url
  175. (string-append (%github-api) "/repos/"
  176. (github-user-slash-repository url)
  177. "/tags"))
  178. (define headers
  179. ;; Ask for version 3 of the API as suggested at
  180. ;; <https://developer.github.com/v3/>.
  181. `((Accept . "application/vnd.github.v3+json")
  182. (user-agent . "GNU Guile")
  183. ,@(if (%github-token)
  184. `((Authorization . ,(string-append "token " (%github-token))))
  185. '())))
  186. (and (not (request-rate-limit-reached?))
  187. (guard (c ((and (http-get-error? c)
  188. (= 404 (http-get-error-code c)))
  189. (warning (G_ "~a is unreachable (~a)~%")
  190. (uri->string (http-get-error-uri c))
  191. (http-get-error-code c))
  192. '#()) ;return an empty release set
  193. ((and (http-get-error? c)
  194. (= 403 (http-get-error-code c)))
  195. ;; See
  196. ;; <https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting>.
  197. (match (assq-ref (http-get-error-headers c)
  198. 'x-ratelimit-remaining)
  199. (#f
  200. (raise c))
  201. ((? (compose zero? string->number))
  202. (let ((reset (update-rate-limit-reset-time!
  203. (http-get-error-headers c))))
  204. (warning (G_ "GitHub rate limit exceeded; \
  205. disallowing requests for ~a seconds~%")
  206. (- reset (car (gettimeofday))))
  207. (display-hint (G_ "You can raise the rate limit by
  208. setting the @env{GUIX_GITHUB_TOKEN} environment variable to a token obtained
  209. from @url{https://github.com/settings/tokens} with your GitHub account.
  210. Alternatively, you can wait until your rate limit is reset, or use the
  211. @code{generic-git} updater instead."))
  212. #f)) ;bail out
  213. (_
  214. (raise c)))))
  215. (let ((release-uri (string->uri release-url)))
  216. (call-with-port (open-connection-for-uri release-uri)
  217. (lambda (connection)
  218. (let* ((result (json->scm
  219. (http-fetch release-uri
  220. #:port connection
  221. #:keep-alive? #t
  222. #:headers headers))))
  223. (match result
  224. (#()
  225. ;; We got the empty list, presumably because the user didn't use GitHub's
  226. ;; "release" mechanism, but hopefully they did use Git tags.
  227. (json->scm (http-fetch tag-url
  228. #:port connection
  229. #:keep-alive? #t
  230. #:headers headers)))
  231. (x x)))))))))
  232. (define (latest-released-version url package-name)
  233. "Return the newest released version and its tag given a string URL like
  234. 'https://github.com/arq5x/bedtools2/archive/v2.24.0.tar.gz' and the name of
  235. the package e.g. 'bedtools2'. Return #f (two values) if there are no
  236. releases."
  237. (define (pre-release? x)
  238. (assoc-ref x "prerelease"))
  239. ;; This procedure returns (version . tag) pair, or #f
  240. ;; if RELEASE doesn't seyem to correspond to a version.
  241. (define (release->version release)
  242. (let ((tag (or (assoc-ref release "tag_name") ;a "release"
  243. (assoc-ref release "name"))) ;a tag
  244. (name-length (string-length package-name)))
  245. (cond
  246. ;; some tags include the name of the package e.g. "fdupes-1.51"
  247. ;; so remove these
  248. ((and (< name-length (string-length tag))
  249. (string=? (string-append package-name "-")
  250. (substring tag 0 (+ name-length 1))))
  251. (cons (substring tag (+ name-length 1)) tag))
  252. ;; some tags start with a "v" e.g. "v0.25.0"
  253. ;; or with the word "version" e.g. "version.2.1"
  254. ;; where some are just the version number
  255. ((string-prefix? "version" tag)
  256. (cons (if (char-set-contains? char-set:digit (string-ref tag 7))
  257. (substring tag 7)
  258. (substring tag 8)) tag))
  259. ((string-prefix? "v" tag)
  260. (cons (substring tag 1) tag))
  261. ;; Finally, reject tags that don't start with a digit:
  262. ;; they may not represent a release.
  263. ((and (not (string-null? tag))
  264. (char-set-contains? char-set:digit
  265. (string-ref tag 0)))
  266. (cons tag tag))
  267. (else #f))))
  268. (match (and=> (fetch-releases-or-tags url) vector->list)
  269. (#f (values #f #f))
  270. (json
  271. (match (sort (filter-map release->version
  272. (match (remove pre-release? json)
  273. (() json) ; keep everything
  274. (releases releases)))
  275. (lambda (x y) (version>? (car x) (car y))))
  276. (((latest-version . tag) . _) (values latest-version tag))
  277. (() (values #f #f))))))
  278. (define (latest-release pkg)
  279. "Return an <upstream-source> for the latest release of PKG."
  280. (define (github-uri uri)
  281. (match uri
  282. ((? string? url)
  283. url) ;surely a github.com URL
  284. ((? download:git-reference? ref)
  285. (download:git-reference-url ref))
  286. ((urls ...)
  287. (find (cut string-contains <> "github.com") urls))))
  288. (let* ((original-uri (origin-uri (package-source pkg)))
  289. (source-uri (github-uri original-uri))
  290. (name (package-name pkg))
  291. (newest-version version-tag
  292. (latest-released-version source-uri name)))
  293. (if newest-version
  294. (upstream-source
  295. (package name)
  296. (version newest-version)
  297. (urls (if (download:git-reference? original-uri)
  298. (download:git-reference
  299. (inherit original-uri)
  300. (commit version-tag))
  301. (list (updated-github-url pkg newest-version)))))
  302. #f))) ; On GitHub but no proper releases
  303. (define %github-updater
  304. (upstream-updater
  305. (name 'github)
  306. (description "Updater for GitHub packages")
  307. (pred github-package?)
  308. (latest latest-release)))