summaryrefslogtreecommitdiff
path: root/guix/upstream.scm
blob: 52fae1183203b067887c77238f71dc41950277fa (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2010-2022 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2015 Alex Kost <alezost@gmail.com>
;;; Copyright © 2019, 2022 Ricardo Wurmus <rekado@elephly.net>
;;; Copyright © 2021 Sarah Morgensen <iskarian@mgsn.dev>
;;; Copyright © 2021, 2022 Maxime Devos <maximedevos@telenet.be>
;;; Copyright © 2022 Hartmut Goebel <h.goebel@crazy-compilers.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (guix upstream)
  #:use-module (guix records)
  #:use-module (guix utils)
  #:use-module (guix discovery)
  #:use-module ((guix download)
                #:select (download-to-store url-fetch))
  #:use-module (guix git-download)
  #:use-module (guix gnupg)
  #:use-module (guix packages)
  #:use-module (guix diagnostics)
  #:use-module (guix ui)
  #:use-module (guix base32)
  #:use-module (guix gexp)
  #:autoload   (guix git) (latest-repository-commit git-reference->git-checkout)
  #:use-module (guix hash)
  #:use-module (guix store)
  #:use-module ((guix derivations) #:select (built-derivations derivation->output-path))
  #:autoload   (gcrypt hash) (port-sha256)
  #:use-module (guix monads)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-9)
  #:use-module (srfi srfi-11)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (rnrs bytevectors)
  #:use-module (ice-9 match)
  #:export (upstream-source
            upstream-source?
            upstream-source-package
            upstream-source-version
            upstream-source-urls
            upstream-source-signature-urls
            upstream-source-archive-types
            upstream-source-input-changes

            url-predicate
            url-prefix-predicate
            coalesce-sources

            upstream-updater
            upstream-updater?
            upstream-updater-name
            upstream-updater-description
            upstream-updater-predicate
            upstream-updater-import

            upstream-input-change?
            upstream-input-change-name
            upstream-input-change-type
            upstream-input-change-action
            changed-inputs

            %updaters
            lookup-updater

            download-tarball
            package-archive-type
            package-latest-release
            package-latest-release*
            package-update
            update-package-source))

;;; Commentary:
;;;
;;; This module provides tools to represent and manipulate a upstream source
;;; code, and to auto-update package recipes.
;;;
;;; Code:

;; Representation of upstream's source.  There can be several URLs--e.g.,
;; tar.gz, tar.gz, etc.  There can be correspond signature URLs, one per
;; source URL.
(define-record-type* <upstream-source>
  upstream-source make-upstream-source
  upstream-source?
  (package        upstream-source-package)        ;string
  (version        upstream-source-version)        ;string
  (urls           upstream-source-urls)           ;list of strings|git-reference
  (signature-urls upstream-source-signature-urls  ;#f | list of strings
                  (default #f))
  (input-changes  upstream-source-input-changes
                  (default '()) (thunked)))

;; Representation of an upstream input change.
(define-record-type* <upstream-input-change>
  upstream-input-change make-upstream-input-change
  upstream-input-change?
  (name    upstream-input-change-name)    ;string
  (type    upstream-input-change-type)    ;symbol: regular | native | propagated
  (action  upstream-input-change-action)) ;symbol: add | remove

(define (changed-inputs package package-sexp)
  "Return a list of input changes for PACKAGE based on the newly imported
S-expression PACKAGE-SEXP."
  (match package-sexp
    ((and expr ('package fields ...))
     (let* ((input->name (match-lambda ((name pkg . out) name)))
            (new-regular
             (match expr
               ((path *** ('inputs
                           ('quasiquote ((label ('unquote sym)) ...)))) label)
               ((path *** ('inputs
                           ('list sym ...))) (map symbol->string sym))
               (_ '())))
            (new-native
             (match expr
               ((path *** ('native-inputs
                           ('quasiquote ((label ('unquote sym)) ...)))) label)
               ((path *** ('native-inputs
                           ('list sym ...))) (map symbol->string sym))
               (_ '())))
            (new-propagated
             (match expr
               ((path *** ('propagated-inputs
                           ('quasiquote ((label ('unquote sym)) ...)))) label)
               ((path *** ('propagated-inputs
                           ('list sym ...))) (map symbol->string sym))
               (_ '())))
            (current-regular
             (map input->name (package-inputs package)))
            (current-native
             (map input->name (package-native-inputs package)))
            (current-propagated
             (map input->name (package-propagated-inputs package))))
       (append-map
        (match-lambda
          ((action type names)
           (map (lambda (name)
                  (upstream-input-change
                   (name name)
                   (type type)
                   (action action)))
                names)))
        `((add regular
           ,(lset-difference equal?
                             new-regular current-regular))
          (remove regular
           ,(lset-difference equal?
                             current-regular new-regular))
          (add native
           ,(lset-difference equal?
                             new-native current-native))
          (remove native
           ,(lset-difference equal?
                             current-native new-native))
          (add propagated
           ,(lset-difference equal?
                             new-propagated current-propagated))
          (remove propagated
           ,(lset-difference equal?
                             current-propagated new-propagated))))))
    (_ '())))

(define* (url-predicate matching-url?)
  "Return a predicate that returns true when passed a package whose source is
an <origin> with the URL-FETCH method, and one of its URLs passes
MATCHING-URL?."
  (lambda (package)
    (match (package-source package)
      ((? origin? origin)
       (and (eq? (origin-method origin) url-fetch)
            (match (origin-uri origin)
              ((? string? url)
               (matching-url? url))
              (((? string? urls) ...)
               (any matching-url? urls))
              (_
               #f))))
      (_ #f))))

(define (url-prefix-predicate prefix)
  "Return a predicate that returns true when passed a package where one of its
source URLs starts with PREFIX."
  (url-predicate (cut string-prefix? prefix <>)))

(define (upstream-source-archive-types release)
  "Return the available types of archives for RELEASE---a list of strings such
as \"gz\" or \"xz\"."
  (map file-extension (upstream-source-urls release)))

(define (coalesce-sources sources)
  "Coalesce the elements of SOURCES, a list of <upstream-source>, that
correspond to the same version."
  (define (same-version? r1 r2)
    (string=? (upstream-source-version r1) (upstream-source-version r2)))

  (define (release>? r1 r2)
    (version>? (upstream-source-version r1) (upstream-source-version r2)))

  (fold (lambda (release result)
          (match result
            ((head . tail)
             (if (same-version? release head)
                 (cons (upstream-source
                        (inherit release)
                        (urls (append (upstream-source-urls release)
                                      (upstream-source-urls head)))
                        (signature-urls
                         (let ((one (upstream-source-signature-urls release))
                               (two (upstream-source-signature-urls head)))
                           (and one two (append one two)))))
                       tail)
                 (cons release result)))
            (()
             (list release))))
        '()
        (sort sources release>?)))


;;;
;;; Auto-update.
;;;

(define-record-type* <upstream-updater>
  upstream-updater make-upstream-updater
  upstream-updater?
  (name        upstream-updater-name)
  (description upstream-updater-description)
  (pred        upstream-updater-predicate)
  (import      upstream-updater-import))

(define (importer-modules)
  "Return the list of importer modules."
  (cons (resolve-interface '(guix gnu-maintenance))
        (all-modules (map (lambda (entry)
                            `(,entry . "guix/import"))
                          %load-path)
                     #:warn warn-about-load-error)))

(define %updaters
  ;; The list of publically-known updaters, alphabetically sorted.
  (delay
    (sort (fold-module-public-variables (lambda (obj result)
                                          (if (upstream-updater? obj)
                                              (cons obj result)
                                              result))
                                        '()
                                        (importer-modules))
          (lambda (updater1 updater2)
            (string<? (symbol->string (upstream-updater-name updater1))
                      (symbol->string (upstream-updater-name updater2)))))))

;; Tests need to mock this variable so mark it as "non-declarative".
(set! %updaters %updaters)

(define* (lookup-updater package
                         #:optional (updaters (force %updaters)))
  "Return an updater among UPDATERS that matches PACKAGE, or #f if none of
them matches."
  (find (match-lambda
          (($ <upstream-updater> name description pred import)
           (pred package)))
        updaters))

(define* (package-latest-release package
                                 #:optional
                                 (updaters (force %updaters))
                                 #:key (version #f))
  "Return an upstream source to update PACKAGE, a <package> object, or #f if
none of UPDATERS matches PACKAGE.  When several updaters match PACKAGE, try
them until one of them returns an upstream source.  It is the caller's
responsibility to ensure that the returned source is newer than the current
one."
  (any (match-lambda
         (($ <upstream-updater> name description pred import)
          (and (pred package)
               (import package #:version version))))
       updaters))

(define* (package-latest-release* package
                                  #:optional
                                  (updaters (force %updaters)))
  "Like 'package-latest-release', but ensure that the return source is newer
than that of PACKAGE."
  (match (package-latest-release package updaters)
    ((and source ($ <upstream-source> name version))
     (and (version>? version (package-version package))
          source))
    (_
     #f)))

(define (uncompressed-tarball name tarball)
  "Return a derivation that decompresses TARBALL."
  (define (ref package)
    (module-ref (resolve-interface '(gnu packages compression))
                package))

  (define compressor
    (cond ((or (string-suffix? ".gz" tarball)
               (string-suffix? ".tgz" tarball))
           (file-append (ref 'gzip) "/bin/gzip"))
          ((string-suffix? ".bz2" tarball)
           (file-append (ref 'bzip2) "/bin/bzip2"))
          ((string-suffix? ".xz" tarball)
           (file-append (ref 'xz) "/bin/xz"))
          ((string-suffix? ".lz" tarball)
           (file-append (ref 'lzip) "/bin/lzip"))
          (else
           (error "unknown archive type" tarball))))

  (gexp->derivation (file-sans-extension name)
                    #~(begin
                        (copy-file #+tarball #+name)
                        (and (zero? (system* #+compressor "-d" #+name))
                             (copy-file #+(file-sans-extension name)
                                        #$output)))))

(define* (download-tarball store url signature-url
                           #:key (key-download 'interactive))
  "Download the tarball at URL to the store; check its OpenPGP signature at
SIGNATURE-URL, unless SIGNATURE-URL is false.  On success, return the tarball
file name; return #f on failure (network failure or authentication failure).
KEY-DOWNLOAD specifies a download policy for missing OpenPGP keys; allowed
values: 'interactive' (default), 'always', and 'never'."
  (let ((tarball (download-to-store store url)))
    (if (not signature-url)
        tarball
        (let* ((sig  (download-to-store store signature-url))

               ;; Sometimes we get a signature over the uncompressed tarball.
               ;; In that case, decompress the tarball in the store so that we
               ;; can check the signature.
               (data (if (string-prefix? (basename url)
                                         (basename signature-url))
                         tarball
                         (run-with-store store
                           (mlet %store-monad ((drv (uncompressed-tarball
                                                     (basename url) tarball)))
                             (mbegin %store-monad
                               (built-derivations (list drv))
                               (return (derivation->output-path drv))))))))
          (let-values (((status data)
                        (if sig
                            (gnupg-verify* sig data
                                           #:key-download key-download)
                            (values 'missing-signature data))))
            (match status
              ('valid-signature
               tarball)
              ('missing-signature
               (warning (G_ "failed to download detached signature from ~a~%")
                        signature-url)
               #f)
              ('invalid-signature
               (warning (G_ "signature verification failed for '~a' (key: ~a)~%")
                        url data)
               #f)
              ('missing-key
               (warning (G_ "missing public key ~a for '~a'~%")
                        data url)
               #f)))))))

(define (upstream-source-compiler/url-fetch source system)
  "Lower SOURCE, an <upstream-source> pointing to a tarball, as a
fixed-output derivation that would fetch it, and verify its authenticity."
  (mlet* %store-monad ((url -> (first (upstream-source-urls source)))
                       (signature
                        -> (and=> (upstream-source-signature-urls source)
                                  first))
                       (tarball ((store-lift download-tarball) url signature)))
    (unless tarball
      (raise (formatted-message (G_ "failed to fetch source from '~a'")
                                url)))

    ;; Instead of returning TARBALL, return a fixed-output derivation that
    ;; would be able to re-download it.  In practice, since TARBALL is already
    ;; in the store, no extra download will happen, but having the derivation
    ;; in store improves provenance tracking.
    (let ((hash (call-with-input-file tarball port-sha256)))
      (url-fetch url 'sha256 hash (store-path-package-name tarball)
                 #:system system))))

(define (upstream-source-compiler/git-fetch source system)
  "Lower SOURCE, an <upstream-source> using git, as a fixed-output
derivation that would fetch it."
  (mlet* %store-monad ((reference -> (upstream-source-urls source))
                       (checkout
                        (lower-object
                         (git-reference->git-checkout reference)
                         system)))
    ;; Like in 'upstream-source-compiler/url-fetch', return a fixed-output
    ;; derivation instead of CHECKOUT.
    (git-fetch reference 'sha256
               (file-hash* checkout #:recursive? #true #:select? (const #true))
               (git-file-name (upstream-source-package source)
                              (upstream-source-version source))
               #:system system)))

(define-gexp-compiler (upstream-source-compiler (source <upstream-source>)
                                                system target)
  "Download SOURCE, lower it as a fixed-output derivation that would fetch it,
and verify its authenticity if possible."
  (if (git-reference? (upstream-source-urls source))
      (upstream-source-compiler/git-fetch source system)
      (upstream-source-compiler/url-fetch source system)))

(define (find2 pred lst1 lst2)
  "Like 'find', but operate on items from both LST1 and LST2.  Return two
values: the item from LST1 and the item from LST2 that match PRED."
  (let loop ((lst1 lst1) (lst2 lst2))
    (match lst1
      ((head1 . tail1)
       (match lst2
         ((head2 . tail2)
          (if (pred head1 head2)
              (values head1 head2)
              (loop tail1 tail2)))))
      (()
       (values #f #f)))))

(define (package-archive-type package)
  "If PACKAGE's source is a tarball or zip archive, return its archive type--a
string such as \"xz\".  Otherwise return #f."
  (match (and=> (package-source package) origin-actual-file-name)
    (#f #f)
    (file
     (let ((extension (file-extension file)))
       ;; FILE might be "example-1.2-checkout", in which case we want to
       ;; ignore the extension.
       (and (or (string-contains extension "z")
                (string-contains extension "tar"))
            extension)))))

(define* (package-update/url-fetch store package source
                                   #:key key-download)
  "Return the version, tarball, and SOURCE, to update PACKAGE to
SOURCE, an <upstream-source>."
  (match source
    (($ <upstream-source> _ version urls signature-urls)
     (let*-values (((archive-type)
                    (package-archive-type package))
                   ((url signature-url)
                    ;; Try to find a URL that matches ARCHIVE-TYPE.
                    (find2 (lambda (url sig-url)
                             ;; Some URIs lack a file extension, like
                             ;; 'https://crates.io/???/0.1/download'.  In that
                             ;; case, pick the first URL.
                             (or (not archive-type)
                                 (string-suffix? archive-type url)))
                           urls
                           (or signature-urls (circular-list #f)))))
       ;; If none of URLS matches ARCHIVE-TYPE, then URL is #f; in that case,
       ;; pick up the first element of URLS.
       (let ((tarball (download-tarball store
                                        (or url (first urls))
                                        (and (pair? signature-urls)
                                             (or signature-url
                                                 (first signature-urls)))
                                        #:key-download key-download)))
         (values version tarball source))))))


(define* (package-update/git-fetch store package source #:key key-download)
  "Return the version, checkout, and SOURCE, to update PACKAGE to
SOURCE, an <upstream-source>."
  ;; TODO: it would be nice to authenticate commits, e.g. with
  ;; "guix git authenticate" or a list of permitted signing keys.
  (define ref (upstream-source-urls source)) ; a <git-reference>
  (values (upstream-source-version source)
          (latest-repository-commit
           store
           (git-reference-url ref)
           #:ref `(tag-or-commit . ,(git-reference-commit ref))
           #:recursive? (git-reference-recursive? ref))
          source))

(define %method-updates
  ;; Mapping of origin methods to source update procedures.
  `((,url-fetch . ,package-update/url-fetch)
    (,git-fetch . ,package-update/git-fetch)))

(define* (package-update store package
                         #:optional (updaters (force %updaters))
                         #:key (key-download 'interactive) (version #f))
  "Return the new version, the file name of the new version tarball, and input
changes for PACKAGE; return #f (three values) when PACKAGE is up-to-date;
raise an error when the updater could not determine available releases.
KEY-DOWNLOAD specifies a download policy for missing OpenPGP keys; allowed
values: 'always', 'never', and 'interactive' (default).

When VERSION is specified, update PACKAGE to that version, even if that is a
downgrade."
  (match (package-latest-release package updaters #:version version)
    ((? upstream-source? source)
     (if (or (version>? (upstream-source-version source)
                        (package-version package))
             (and version
                  (begin
                    (warning (package-location package)
                             (G_ "downgrading '~a' from ~a to ~a~%")
                             (package-name package)
                             (package-version package)
                             (upstream-source-version source))
                    #t)))
         (let ((method (match (package-source package)
                         ((? origin? origin)
                          (origin-method origin))
                         (_
                          #f))))
           (match (assq method %method-updates)
             (#f
              (raise (make-compound-condition
                      (formatted-message (G_ "cannot download for \
this method: ~s")
                                         method)
                      (condition
                       (&error-location
                        (location (package-location package)))))))
             ((_ . update)
              (update store package source
                      #:key-download key-download))))
         (values #f #f #f)))
    (#f
     ;; Warn rather than abort so that other updates can still take place.
     (if version
         (warning (G_ "updater failed to find release ~a@~a~%")
                  (package-name package) version)
         (warning (G_ "updater failed to determine available releases for ~a~%")
                  (package-name package)))
     (values #f #f #f))))

(define* (update-package-source package source hash)
  "Modify the source file that defines PACKAGE to refer to SOURCE, an
<upstream-source> whose tarball has SHA256 HASH (a bytevector).  Return the
new version string if an update was made, and #f otherwise."
  (define (update-expression expr replacements)
    ;; Apply REPLACEMENTS to package expression EXPR, a string.  REPLACEMENTS
    ;; must be a list of replacement pairs, either bytevectors or strings.
    (fold (lambda (replacement str)
            (match replacement
              (((? bytevector? old-bv) . (? bytevector? new-bv))
               (string-replace-substring
                str
                (bytevector->nix-base32-string old-bv)
                (bytevector->nix-base32-string new-bv)))
              ((old . new)
               (string-replace-substring str old new))))
          expr
          replacements))

  (let ((name        (package-name package))
        (version     (upstream-source-version source))
        (version-loc (package-field-location package 'version)))
    (if version-loc
        (let* ((loc         (package-location package))
               (old-version (package-version package))
               (old-hash    (content-hash-value
                             (origin-hash (package-source package))))
               (old-url     (match (origin-uri (package-source package))
                              ((? string? url) url)
                              ((? git-reference? ref)
                               (git-reference-url ref))
                              (_ #f)))
               (new-url     (match (upstream-source-urls source)
                              ((first _ ...) first)
                              ((? git-reference? ref)
                               (git-reference-url ref))
                              (_ #f)))
               (old-commit  (match (origin-uri (package-source package))
                              ((? git-reference? ref)
                               (git-reference-commit ref))
                              (_ #f)))
               (new-commit  (match (upstream-source-urls source)
                              ((? git-reference? ref)
                               (git-reference-commit ref))
                              (_ #f)))
               (file        (and=> (location-file loc)
                                   (cut search-path %load-path <>))))
          (if file
              ;; Be sure to use absolute filename.  Replace the URL directory
              ;; when OLD-URL is available; this is useful notably for
              ;; mirror://cpan/ URLs where the directory may change as a
              ;; function of the person who uploads the package.  Note that
              ;; package definitions usually concatenate fragments of the URL,
              ;; which is why we only attempt to replace a subset of the URL.
              (let ((properties (assq-set! (location->source-properties loc)
                                           'filename file))
                    (replacements `((,old-version . ,version)
                                    (,old-hash . ,hash)
                                    ,@(if (and old-commit new-commit)
                                          `((,old-commit . ,new-commit))
                                          '())
                                    ,@(if (and old-url new-url)
                                          `((,(dirname old-url) .
                                             ,(dirname new-url)))
                                          '()))))
                (and (edit-expression properties
                                      (cut update-expression <> replacements))
                     version))
              (begin
                (warning (G_ "~a: could not locate source file")
                         (location-file loc))
                #f)))
        (warning (package-location package)
                 (G_ "~a: no `version' field in source; skipping~%")
                 name))))

;;; upstream.scm ends here