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