upstream.scm 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2015 Alex Kost <alezost@gmail.com>
  4. ;;; Copyright © 2019 Ricardo Wurmus <rekado@elephly.net>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; 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 GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (guix upstream)
  21. #:use-module (guix records)
  22. #:use-module (guix utils)
  23. #:use-module (guix discovery)
  24. #:use-module ((guix download)
  25. #:select (download-to-store url-fetch))
  26. #:use-module (guix gnupg)
  27. #:use-module (guix packages)
  28. #:use-module (guix diagnostics)
  29. #:use-module (guix ui)
  30. #:use-module (guix base32)
  31. #:use-module (guix gexp)
  32. #:use-module (guix store)
  33. #:use-module ((guix derivations) #:select (built-derivations derivation->output-path))
  34. #:autoload (gcrypt hash) (port-sha256)
  35. #:use-module (guix monads)
  36. #:use-module (srfi srfi-1)
  37. #:use-module (srfi srfi-9)
  38. #:use-module (srfi srfi-11)
  39. #:use-module (srfi srfi-26)
  40. #:use-module (srfi srfi-34)
  41. #:use-module (srfi srfi-35)
  42. #:use-module (rnrs bytevectors)
  43. #:use-module (ice-9 match)
  44. #:use-module (ice-9 regex)
  45. #:export (upstream-source
  46. upstream-source?
  47. upstream-source-package
  48. upstream-source-version
  49. upstream-source-urls
  50. upstream-source-signature-urls
  51. upstream-source-archive-types
  52. upstream-source-input-changes
  53. url-predicate
  54. url-prefix-predicate
  55. coalesce-sources
  56. upstream-updater
  57. upstream-updater?
  58. upstream-updater-name
  59. upstream-updater-description
  60. upstream-updater-predicate
  61. upstream-updater-latest
  62. upstream-input-change?
  63. upstream-input-change-name
  64. upstream-input-change-type
  65. upstream-input-change-action
  66. changed-inputs
  67. %updaters
  68. lookup-updater
  69. download-tarball
  70. package-latest-release
  71. package-latest-release*
  72. package-update
  73. update-package-source))
  74. ;;; Commentary:
  75. ;;;
  76. ;;; This module provides tools to represent and manipulate a upstream source
  77. ;;; code, and to auto-update package recipes.
  78. ;;;
  79. ;;; Code:
  80. ;; Representation of upstream's source. There can be several URLs--e.g.,
  81. ;; tar.gz, tar.gz, etc. There can be correspond signature URLs, one per
  82. ;; source URL.
  83. (define-record-type* <upstream-source>
  84. upstream-source make-upstream-source
  85. upstream-source?
  86. (package upstream-source-package) ;string
  87. (version upstream-source-version) ;string
  88. (urls upstream-source-urls) ;list of strings
  89. (signature-urls upstream-source-signature-urls ;#f | list of strings
  90. (default #f))
  91. (input-changes upstream-source-input-changes
  92. (default '()) (thunked)))
  93. ;; Representation of an upstream input change.
  94. (define-record-type* <upstream-input-change>
  95. upstream-input-change make-upstream-input-change
  96. upstream-input-change?
  97. (name upstream-input-change-name) ;string
  98. (type upstream-input-change-type) ;symbol: regular | native | propagated
  99. (action upstream-input-change-action)) ;symbol: add | remove
  100. (define (changed-inputs package package-sexp)
  101. "Return a list of input changes for PACKAGE based on the newly imported
  102. S-expression PACKAGE-SEXP."
  103. (match package-sexp
  104. ((and expr ('package fields ...))
  105. (let* ((input->name (match-lambda ((name pkg . out) name)))
  106. (new-regular
  107. (match expr
  108. ((path *** ('inputs
  109. ('quasiquote ((label ('unquote sym)) ...)))) label)
  110. (_ '())))
  111. (new-native
  112. (match expr
  113. ((path *** ('native-inputs
  114. ('quasiquote ((label ('unquote sym)) ...)))) label)
  115. (_ '())))
  116. (new-propagated
  117. (match expr
  118. ((path *** ('propagated-inputs
  119. ('quasiquote ((label ('unquote sym)) ...)))) label)
  120. (_ '())))
  121. (current-regular
  122. (map input->name (package-inputs package)))
  123. (current-native
  124. (map input->name (package-native-inputs package)))
  125. (current-propagated
  126. (map input->name (package-propagated-inputs package))))
  127. (append-map
  128. (match-lambda
  129. ((action type names)
  130. (map (lambda (name)
  131. (upstream-input-change
  132. (name name)
  133. (type type)
  134. (action action)))
  135. names)))
  136. `((add regular
  137. ,(lset-difference equal?
  138. new-regular current-regular))
  139. (remove regular
  140. ,(lset-difference equal?
  141. current-regular new-regular))
  142. (add native
  143. ,(lset-difference equal?
  144. new-native current-native))
  145. (remove native
  146. ,(lset-difference equal?
  147. current-native new-native))
  148. (add propagated
  149. ,(lset-difference equal?
  150. new-propagated current-propagated))
  151. (remove propagated
  152. ,(lset-difference equal?
  153. current-propagated new-propagated))))))
  154. (_ '())))
  155. (define* (url-predicate matching-url?)
  156. "Return a predicate that returns true when passed a package whose source is
  157. an <origin> with the URL-FETCH method, and one of its URLs passes
  158. MATCHING-URL?."
  159. (lambda (package)
  160. (match (package-source package)
  161. ((? origin? origin)
  162. (and (eq? (origin-method origin) url-fetch)
  163. (match (origin-uri origin)
  164. ((? string? url)
  165. (matching-url? url))
  166. (((? string? urls) ...)
  167. (any matching-url? urls))
  168. (_
  169. #f))))
  170. (_ #f))))
  171. (define (url-prefix-predicate prefix)
  172. "Return a predicate that returns true when passed a package where one of its
  173. source URLs starts with PREFIX."
  174. (url-predicate (cut string-prefix? prefix <>)))
  175. (define (upstream-source-archive-types release)
  176. "Return the available types of archives for RELEASE---a list of strings such
  177. as \"gz\" or \"xz\"."
  178. (map file-extension (upstream-source-urls release)))
  179. (define (coalesce-sources sources)
  180. "Coalesce the elements of SOURCES, a list of <upstream-source>, that
  181. correspond to the same version."
  182. (define (same-version? r1 r2)
  183. (string=? (upstream-source-version r1) (upstream-source-version r2)))
  184. (define (release>? r1 r2)
  185. (version>? (upstream-source-version r1) (upstream-source-version r2)))
  186. (fold (lambda (release result)
  187. (match result
  188. ((head . tail)
  189. (if (same-version? release head)
  190. (cons (upstream-source
  191. (inherit release)
  192. (urls (append (upstream-source-urls release)
  193. (upstream-source-urls head)))
  194. (signature-urls
  195. (let ((one (upstream-source-signature-urls release))
  196. (two (upstream-source-signature-urls head)))
  197. (and one two (append one two)))))
  198. tail)
  199. (cons release result)))
  200. (()
  201. (list release))))
  202. '()
  203. (sort sources release>?)))
  204. ;;;
  205. ;;; Auto-update.
  206. ;;;
  207. (define-record-type* <upstream-updater>
  208. upstream-updater make-upstream-updater
  209. upstream-updater?
  210. (name upstream-updater-name)
  211. (description upstream-updater-description)
  212. (pred upstream-updater-predicate)
  213. (latest upstream-updater-latest))
  214. (define (importer-modules)
  215. "Return the list of importer modules."
  216. (cons (resolve-interface '(guix gnu-maintenance))
  217. (all-modules (map (lambda (entry)
  218. `(,entry . "guix/import"))
  219. %load-path)
  220. #:warn warn-about-load-error)))
  221. (define %updaters
  222. ;; The list of publically-known updaters.
  223. (delay (fold-module-public-variables (lambda (obj result)
  224. (if (upstream-updater? obj)
  225. (cons obj result)
  226. result))
  227. '()
  228. (importer-modules))))
  229. ;; Tests need to mock this variable so mark it as "non-declarative".
  230. (set! %updaters %updaters)
  231. (define* (lookup-updater package
  232. #:optional (updaters (force %updaters)))
  233. "Return an updater among UPDATERS that matches PACKAGE, or #f if none of
  234. them matches."
  235. (find (match-lambda
  236. (($ <upstream-updater> name description pred latest)
  237. (pred package)))
  238. updaters))
  239. (define* (package-latest-release package
  240. #:optional
  241. (updaters (force %updaters)))
  242. "Return an upstream source to update PACKAGE, a <package> object, or #f if
  243. none of UPDATERS matches PACKAGE. When several updaters match PACKAGE, try
  244. them until one of them returns an upstream source. It is the caller's
  245. responsibility to ensure that the returned source is newer than the current
  246. one."
  247. (any (match-lambda
  248. (($ <upstream-updater> name description pred latest)
  249. (and (pred package)
  250. (latest package))))
  251. updaters))
  252. (define* (package-latest-release* package
  253. #:optional
  254. (updaters (force %updaters)))
  255. "Like 'package-latest-release', but ensure that the return source is newer
  256. than that of PACKAGE."
  257. (match (package-latest-release package updaters)
  258. ((and source ($ <upstream-source> name version))
  259. (and (version>? version (package-version package))
  260. source))
  261. (_
  262. #f)))
  263. (define (uncompressed-tarball name tarball)
  264. "Return a derivation that decompresses TARBALL."
  265. (define (ref package)
  266. (module-ref (resolve-interface '(gnu packages compression))
  267. package))
  268. (define compressor
  269. (cond ((or (string-suffix? ".gz" tarball)
  270. (string-suffix? ".tgz" tarball))
  271. (file-append (ref 'gzip) "/bin/gzip"))
  272. ((string-suffix? ".bz2" tarball)
  273. (file-append (ref 'bzip2) "/bin/bzip2"))
  274. ((string-suffix? ".xz" tarball)
  275. (file-append (ref 'xz) "/bin/xz"))
  276. ((string-suffix? ".lz" tarball)
  277. (file-append (ref 'lzip) "/bin/lzip"))
  278. (else
  279. (error "unknown archive type" tarball))))
  280. (gexp->derivation (file-sans-extension name)
  281. #~(begin
  282. (copy-file #+tarball #+name)
  283. (and (zero? (system* #+compressor "-d" #+name))
  284. (copy-file #+(file-sans-extension name)
  285. #$output)))))
  286. (define* (download-tarball store url signature-url
  287. #:key (key-download 'interactive))
  288. "Download the tarball at URL to the store; check its OpenPGP signature at
  289. SIGNATURE-URL, unless SIGNATURE-URL is false. On success, return the tarball
  290. file name; return #f on failure (network failure or authentication failure).
  291. KEY-DOWNLOAD specifies a download policy for missing OpenPGP keys; allowed
  292. values: 'interactive' (default), 'always', and 'never'."
  293. (let ((tarball (download-to-store store url)))
  294. (if (not signature-url)
  295. tarball
  296. (let* ((sig (download-to-store store signature-url))
  297. ;; Sometimes we get a signature over the uncompressed tarball.
  298. ;; In that case, decompress the tarball in the store so that we
  299. ;; can check the signature.
  300. (data (if (string-prefix? (basename url)
  301. (basename signature-url))
  302. tarball
  303. (run-with-store store
  304. (mlet %store-monad ((drv (uncompressed-tarball
  305. (basename url) tarball)))
  306. (mbegin %store-monad
  307. (built-derivations (list drv))
  308. (return (derivation->output-path drv))))))))
  309. (let-values (((status data)
  310. (if sig
  311. (gnupg-verify* sig data
  312. #:key-download key-download)
  313. (values 'missing-signature data))))
  314. (match status
  315. ('valid-signature
  316. tarball)
  317. ('missing-signature
  318. (warning (G_ "failed to download detached signature from ~a~%")
  319. signature-url)
  320. #f)
  321. ('invalid-signature
  322. (warning (G_ "signature verification failed for '~a' (key: ~a)~%")
  323. url data)
  324. #f)
  325. ('missing-key
  326. (warning (G_ "missing public key ~a for '~a'~%")
  327. data url)
  328. #f)))))))
  329. (define-gexp-compiler (upstream-source-compiler (source <upstream-source>)
  330. system target)
  331. "Download SOURCE from its first URL and lower it as a fixed-output
  332. derivation that would fetch it."
  333. (mlet* %store-monad ((url -> (first (upstream-source-urls source)))
  334. (signature
  335. -> (and=> (upstream-source-signature-urls source)
  336. first))
  337. (tarball ((store-lift download-tarball) url signature)))
  338. (unless tarball
  339. (raise (formatted-message (G_ "failed to fetch source from '~a'")
  340. url)))
  341. ;; Instead of returning TARBALL, return a fixed-output derivation that
  342. ;; would be able to re-download it. In practice, since TARBALL is already
  343. ;; in the store, no extra download will happen, but having the derivation
  344. ;; in store improves provenance tracking.
  345. (let ((hash (call-with-input-file tarball port-sha256)))
  346. (url-fetch url 'sha256 hash (store-path-package-name tarball)
  347. #:system system))))
  348. (define (find2 pred lst1 lst2)
  349. "Like 'find', but operate on items from both LST1 and LST2. Return two
  350. values: the item from LST1 and the item from LST2 that match PRED."
  351. (let loop ((lst1 lst1) (lst2 lst2))
  352. (match lst1
  353. ((head1 . tail1)
  354. (match lst2
  355. ((head2 . tail2)
  356. (if (pred head1 head2)
  357. (values head1 head2)
  358. (loop tail1 tail2)))))
  359. (()
  360. (values #f #f)))))
  361. (define* (package-update/url-fetch store package source
  362. #:key key-download)
  363. "Return the version, tarball, and SOURCE, to update PACKAGE to
  364. SOURCE, an <upstream-source>."
  365. (match source
  366. (($ <upstream-source> _ version urls signature-urls)
  367. (let*-values (((archive-type)
  368. (match (and=> (package-source package) origin-uri)
  369. ((? string? uri)
  370. (let ((type (or (file-extension (basename uri)) "")))
  371. ;; Sometimes we have URLs such as
  372. ;; "https://github.com/…/tarball/v0.1", in which case
  373. ;; we must not consider "1" as the extension.
  374. (and (or (string-contains type "z")
  375. (string=? type "tar"))
  376. type)))
  377. (_
  378. "gz")))
  379. ((url signature-url)
  380. ;; Try to find a URL that matches ARCHIVE-TYPE.
  381. (find2 (lambda (url sig-url)
  382. ;; Some URIs lack a file extension, like
  383. ;; 'https://crates.io/???/0.1/download'. In that
  384. ;; case, pick the first URL.
  385. (or (not archive-type)
  386. (string-suffix? archive-type url)))
  387. urls
  388. (or signature-urls (circular-list #f)))))
  389. ;; If none of URLS matches ARCHIVE-TYPE, then URL is #f; in that case,
  390. ;; pick up the first element of URLS.
  391. (let ((tarball (download-tarball store
  392. (or url (first urls))
  393. (and (pair? signature-urls)
  394. (or signature-url
  395. (first signature-urls)))
  396. #:key-download key-download)))
  397. (values version tarball source))))))
  398. (define %method-updates
  399. ;; Mapping of origin methods to source update procedures.
  400. `((,url-fetch . ,package-update/url-fetch)))
  401. (define* (package-update store package
  402. #:optional (updaters (force %updaters))
  403. #:key (key-download 'interactive))
  404. "Return the new version, the file name of the new version tarball, and input
  405. changes for PACKAGE; return #f (three values) when PACKAGE is up-to-date.
  406. KEY-DOWNLOAD specifies a download policy for missing OpenPGP keys; allowed
  407. values: 'always', 'never', and 'interactive' (default)."
  408. (match (package-latest-release* package updaters)
  409. ((? upstream-source? source)
  410. (let ((method (match (package-source package)
  411. ((? origin? origin)
  412. (origin-method origin))
  413. (_
  414. #f))))
  415. (match (assq method %method-updates)
  416. (#f
  417. (raise (make-compound-condition
  418. (formatted-message (G_ "cannot download for \
  419. this method: ~s")
  420. method)
  421. (condition
  422. (&error-location
  423. (location (package-location package)))))))
  424. ((_ . update)
  425. (update store package source
  426. #:key-download key-download)))))
  427. (#f
  428. (values #f #f #f))))
  429. (define* (update-package-source package source hash)
  430. "Modify the source file that defines PACKAGE to refer to SOURCE, an
  431. <upstream-source> whose tarball has SHA256 HASH (a bytevector). Return the
  432. new version string if an update was made, and #f otherwise."
  433. (define (update-expression expr replacements)
  434. ;; Apply REPLACEMENTS to package expression EXPR, a string. REPLACEMENTS
  435. ;; must be a list of replacement pairs, either bytevectors or strings.
  436. (fold (lambda (replacement str)
  437. (match replacement
  438. (((? bytevector? old-bv) . (? bytevector? new-bv))
  439. (string-replace-substring
  440. str
  441. (bytevector->nix-base32-string old-bv)
  442. (bytevector->nix-base32-string new-bv)))
  443. ((old . new)
  444. (string-replace-substring str old new))))
  445. expr
  446. replacements))
  447. (let ((name (package-name package))
  448. (version (upstream-source-version source))
  449. (version-loc (package-field-location package 'version)))
  450. (if version-loc
  451. (let* ((loc (package-location package))
  452. (old-version (package-version package))
  453. (old-hash (content-hash-value
  454. (origin-hash (package-source package))))
  455. (old-url (match (origin-uri (package-source package))
  456. ((? string? url) url)
  457. (_ #f)))
  458. (new-url (match (upstream-source-urls source)
  459. ((first _ ...) first)))
  460. (file (and=> (location-file loc)
  461. (cut search-path %load-path <>))))
  462. (if file
  463. ;; Be sure to use absolute filename. Replace the URL directory
  464. ;; when OLD-URL is available; this is useful notably for
  465. ;; mirror://cpan/ URLs where the directory may change as a
  466. ;; function of the person who uploads the package. Note that
  467. ;; package definitions usually concatenate fragments of the URL,
  468. ;; which is why we only attempt to replace a subset of the URL.
  469. (let ((properties (assq-set! (location->source-properties loc)
  470. 'filename file))
  471. (replacements `((,old-version . ,version)
  472. (,old-hash . ,hash)
  473. ,@(if (and old-url new-url)
  474. `((,(dirname old-url) .
  475. ,(dirname new-url)))
  476. '()))))
  477. (and (edit-expression properties
  478. (cut update-expression <> replacements))
  479. version))
  480. (begin
  481. (warning (G_ "~a: could not locate source file")
  482. (location-file loc))
  483. #f)))
  484. (warning (package-location package)
  485. (G_ "~a: no `version' field in source; skipping~%")
  486. name))))
  487. ;;; upstream.scm ends here