upstream.scm 25 KB

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