gnu: guix: Ensure the bootstrap binary tarballs aren't GC'd.
[jackhill/guix/guix.git] / guix / lint.scm
CommitLineData
f363c836
CB
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2014 Cyril Roelandt <tipecaml@gmail.com>
3;;; Copyright © 2014, 2015 Eric Bavier <bavier@member.fsf.org>
f4cde9ac 4;;; Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
f363c836
CB
5;;; Copyright © 2015, 2016 Mathieu Lirzin <mthl@gnu.org>
6;;; Copyright © 2016 Danny Milosavljevic <dannym+a@scratchpost.org>
7;;; Copyright © 2016 Hartmut Goebel <h.goebel@crazy-compilers.com>
8;;; Copyright © 2017 Alex Kost <alezost@gmail.com>
9;;; Copyright © 2017 Tobias Geerinckx-Rice <me@tobias.gr>
10;;; Copyright © 2017, 2018 Efraim Flashner <efraim@flashner.co.il>
11;;; Copyright © 2018, 2019 Arun Isaac <arunisaac@systemreboot.net>
12;;;
13;;; This file is part of GNU Guix.
14;;;
15;;; GNU Guix is free software; you can redistribute it and/or modify it
16;;; under the terms of the GNU General Public License as published by
17;;; the Free Software Foundation; either version 3 of the License, or (at
18;;; your option) any later version.
19;;;
20;;; GNU Guix is distributed in the hope that it will be useful, but
21;;; WITHOUT ANY WARRANTY; without even the implied warranty of
22;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23;;; GNU General Public License for more details.
24;;;
25;;; You should have received a copy of the GNU General Public License
26;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
27
28(define-module (guix lint)
f4cde9ac 29 #:use-module (guix store)
f363c836
CB
30 #:use-module (guix base32)
31 #:use-module (guix diagnostics)
32 #:use-module (guix download)
33 #:use-module (guix ftp-client)
34 #:use-module (guix http-client)
35 #:use-module (guix packages)
36 #:use-module (guix i18n)
37 #:use-module (guix licenses)
38 #:use-module (guix records)
39 #:use-module (guix grafts)
40 #:use-module (guix upstream)
41 #:use-module (guix utils)
42 #:use-module (guix memoization)
43 #:use-module (guix scripts)
44 #:use-module ((guix ui) #:select (texi->plain-text fill-paragraph))
45 #:use-module (guix gnu-maintenance)
f363c836 46 #:use-module (guix cve)
55549c7b
LC
47 #:use-module ((guix swh) #:hide (origin?))
48 #:autoload (guix git-download) (git-reference?)
f363c836
CB
49 #:use-module (ice-9 match)
50 #:use-module (ice-9 regex)
51 #:use-module (ice-9 format)
52 #:use-module (web client)
53 #:use-module (web uri)
54 #:use-module ((guix build download)
55 #:select (maybe-expand-mirrors
56 (open-connection-for-uri
f4cde9ac 57 . guix:open-connection-for-uri)))
f363c836
CB
58 #:use-module (web request)
59 #:use-module (web response)
60 #:use-module (srfi srfi-1)
61 #:use-module (srfi srfi-6) ;Unicode string ports
62 #:use-module (srfi srfi-9)
63 #:use-module (srfi srfi-11)
64 #:use-module (srfi srfi-26)
65 #:use-module (srfi srfi-34)
66 #:use-module (srfi srfi-35)
67 #:use-module (ice-9 rdelim)
68 #:export (check-description-style
69 check-inputs-should-be-native
70 check-inputs-should-not-be-an-input-at-all
71 check-patch-file-names
72 check-synopsis-style
73 check-derivation
74 check-home-page
75 check-source
76 check-source-file-name
77 check-source-unstable-tarball
78 check-mirror-url
79 check-github-url
80 check-license
81 check-vulnerabilities
82 check-for-updates
83 check-formatting
55549c7b 84 check-archival
f363c836
CB
85
86 lint-warning
87 lint-warning?
88 lint-warning-package
89 lint-warning-message
90 lint-warning-message-text
91 lint-warning-message-data
92 lint-warning-location
93
38f3176a
CB
94 %local-checkers
95 %network-dependent-checkers
96 %all-checkers
f363c836
CB
97
98 lint-checker
99 lint-checker?
100 lint-checker-name
101 lint-checker-description
102 lint-checker-check))
103
104\f
105;;;
106;;; Warnings
107;;;
108
109(define-record-type* <lint-warning>
110 lint-warning make-lint-warning
111 lint-warning?
112 (package lint-warning-package)
113 (message-text lint-warning-message-text)
114 (message-data lint-warning-message-data
115 (default '()))
116 (location lint-warning-location
117 (default #f)))
118
119(define (lint-warning-message warning)
120 (apply format #f
121 (G_ (lint-warning-message-text warning))
122 (lint-warning-message-data warning)))
123
124(define (package-file package)
125 (location-file
126 (package-location package)))
127
128(define* (%make-warning package message-text
129 #:optional (message-data '())
130 #:key field location)
131 (make-lint-warning
132 package
133 message-text
134 message-data
135 (or location
136 (package-field-location package field)
137 (package-location package))))
138
139(define-syntax make-warning
140 (syntax-rules (G_)
141 ((_ package (G_ message) rest ...)
142 (%make-warning package message rest ...))))
143
144\f
145;;;
146;;; Checkers
147;;;
148
149(define-record-type* <lint-checker>
150 lint-checker make-lint-checker
151 lint-checker?
152 ;; TODO: add a 'certainty' field that shows how confident we are in the
153 ;; checker. Then allow users to only run checkers that have a certain
154 ;; 'certainty' level.
155 (name lint-checker-name)
156 (description lint-checker-description)
157 (check lint-checker-check))
158
159(define (properly-starts-sentence? s)
160 (string-match "^[(\"'`[:upper:][:digit:]]" s))
161
162(define (starts-with-abbreviation? s)
163 "Return #t if S starts with what looks like an abbreviation or acronym."
164 (string-match "^[A-Z][A-Z0-9]+\\>" s))
165
166(define %quoted-identifier-rx
167 ;; A quoted identifier, like 'this'.
168 (make-regexp "['`][[:graph:]]+'"))
169
170(define (check-description-style package)
171 ;; Emit a warning if stylistic issues are found in the description of PACKAGE.
172 (define (check-not-empty description)
173 (if (string-null? description)
174 (list
175 (make-warning package
176 (G_ "description should not be empty")
177 #:field 'description))
178 '()))
179
180 (define (check-texinfo-markup description)
181 "Check that DESCRIPTION can be parsed as a Texinfo fragment. If the
182markup is valid return a plain-text version of DESCRIPTION, otherwise #f."
183 (catch #t
184 (lambda () (texi->plain-text description))
185 (lambda (keys . args)
186 (make-warning package
187 (G_ "Texinfo markup in description is invalid")
188 #:field 'description))))
189
190 (define (check-trademarks description)
191 "Check that DESCRIPTION does not contain '™' or '®' characters. See
192http://www.gnu.org/prep/standards/html_node/Trademarks.html."
193 (match (string-index description (char-set #\™ #\®))
194 ((and (? number?) index)
195 (list
196 (make-warning package
197 (G_ "description should not contain ~
198trademark sign '~a' at ~d")
199 (list (string-ref description index) index)
200 #:field 'description)))
201 (else '())))
202
203 (define (check-quotes description)
204 "Check whether DESCRIPTION contains single quotes and suggest @code."
205 (if (regexp-exec %quoted-identifier-rx description)
206 (list
207 (make-warning package
208 ;; TRANSLATORS: '@code' is Texinfo markup and must be kept
209 ;; as is.
210 (G_ "use @code or similar ornament instead of quotes")
211 #:field 'description))
212 '()))
213
214 (define (check-proper-start description)
215 (if (or (string-null? description)
216 (properly-starts-sentence? description)
217 (string-prefix-ci? (package-name package) description))
218 '()
219 (list
220 (make-warning
221 package
222 (G_ "description should start with an upper-case letter or digit")
223 #:field 'description))))
224
225 (define (check-end-of-sentence-space description)
226 "Check that an end-of-sentence period is followed by two spaces."
227 (let ((infractions
228 (reverse (fold-matches
229 "\\. [A-Z]" description '()
230 (lambda (m r)
231 ;; Filter out matches of common abbreviations.
232 (if (find (lambda (s)
233 (string-suffix-ci? s (match:prefix m)))
234 '("i.e" "e.g" "a.k.a" "resp"))
235 r (cons (match:start m) r)))))))
236 (if (null? infractions)
237 '()
238 (list
239 (make-warning package
240 (G_ "sentences in description should be followed ~
241by two spaces; possible infraction~p at ~{~a~^, ~}")
242 (list (length infractions)
243 infractions)
244 #:field 'description)))))
245
246 (let ((description (package-description package)))
247 (if (string? description)
248 (append
249 (check-not-empty description)
250 (check-quotes description)
251 (check-trademarks description)
252 ;; Use raw description for this because Texinfo rendering
253 ;; automatically fixes end of sentence space.
254 (check-end-of-sentence-space description)
255 (match (check-texinfo-markup description)
256 ((and warning (? lint-warning?)) (list warning))
257 (plain-description
258 (check-proper-start plain-description))))
259 (list
260 (make-warning package
261 (G_ "invalid description: ~s")
262 (list description)
263 #:field 'description)))))
264
265(define (package-input-intersection inputs-to-check input-names)
266 "Return the intersection between INPUTS-TO-CHECK, the list of input tuples
267of a package, and INPUT-NAMES, a list of package specifications such as
268\"glib:bin\"."
269 (match inputs-to-check
270 (((labels packages . outputs) ...)
271 (filter-map (lambda (package output)
272 (and (package? package)
273 (let ((input (string-append
274 (package-name package)
275 (if (> (length output) 0)
276 (string-append ":" (car output))
277 ""))))
278 (and (member input input-names)
279 input))))
280 packages outputs))))
281
282(define (check-inputs-should-be-native package)
283 ;; Emit a warning if some inputs of PACKAGE are likely to belong to its
284 ;; native inputs.
285 (let ((inputs (package-inputs package))
286 (input-names
287 '("pkg-config"
288 "cmake"
289 "extra-cmake-modules"
290 "glib:bin"
291 "intltool"
292 "itstool"
293 "qttools"
e8c6644a 294 "yasm" "nasm" "fasm"
f363c836
CB
295 "python-coverage" "python2-coverage"
296 "python-cython" "python2-cython"
297 "python-docutils" "python2-docutils"
298 "python-mock" "python2-mock"
299 "python-nose" "python2-nose"
300 "python-pbr" "python2-pbr"
301 "python-pytest" "python2-pytest"
302 "python-pytest-cov" "python2-pytest-cov"
303 "python-setuptools-scm" "python2-setuptools-scm"
304 "python-sphinx" "python2-sphinx")))
305 (map (lambda (input)
306 (make-warning
307 package
308 (G_ "'~a' should probably be a native input")
309 (list input)
310 #:field 'inputs))
311 (package-input-intersection inputs input-names))))
312
313(define (check-inputs-should-not-be-an-input-at-all package)
314 ;; Emit a warning if some inputs of PACKAGE are likely to should not be
315 ;; an input at all.
316 (let ((input-names '("python-setuptools"
317 "python2-setuptools"
318 "python-pip"
319 "python2-pip")))
320 (map (lambda (input)
321 (make-warning
322 package
323 (G_ "'~a' should probably not be an input at all")
324 (list input)
325 #:field 'inputs))
326 (package-input-intersection (package-direct-inputs package)
327 input-names))))
328
329(define (package-name-regexp package)
330 "Return a regexp that matches PACKAGE's name as a word at the beginning of a
331line."
332 (make-regexp (string-append "^" (regexp-quote (package-name package))
333 "\\>")
334 regexp/icase))
335
336(define (check-synopsis-style package)
337 ;; Emit a warning if stylistic issues are found in the synopsis of PACKAGE.
338 (define (check-final-period synopsis)
339 ;; Synopsis should not end with a period, except for some special cases.
340 (if (and (string-suffix? "." synopsis)
341 (not (string-suffix? "etc." synopsis)))
342 (list
343 (make-warning package
344 (G_ "no period allowed at the end of the synopsis")
345 #:field 'synopsis))
346 '()))
347
348 (define check-start-article
349 ;; Skip this check for GNU packages, as suggested by Karl Berry's reply to
350 ;; <http://lists.gnu.org/archive/html/bug-womb/2014-11/msg00000.html>.
351 (if (false-if-exception (gnu-package? package))
352 (const '())
353 (lambda (synopsis)
354 (if (or (string-prefix-ci? "A " synopsis)
355 (string-prefix-ci? "An " synopsis))
356 (list
357 (make-warning package
358 (G_ "no article allowed at the beginning of \
359the synopsis")
360 #:field 'synopsis))
361 '()))))
362
363 (define (check-synopsis-length synopsis)
364 (if (>= (string-length synopsis) 80)
365 (list
366 (make-warning package
367 (G_ "synopsis should be less than 80 characters long")
368 #:field 'synopsis))
369 '()))
370
371 (define (check-proper-start synopsis)
372 (if (properly-starts-sentence? synopsis)
373 '()
374 (list
375 (make-warning package
376 (G_ "synopsis should start with an upper-case letter or digit")
377 #:field 'synopsis))))
378
379 (define (check-start-with-package-name synopsis)
380 (if (and (regexp-exec (package-name-regexp package) synopsis)
381 (not (starts-with-abbreviation? synopsis)))
382 (list
383 (make-warning package
384 (G_ "synopsis should not start with the package name")
385 #:field 'synopsis))
386 '()))
387
388 (define (check-texinfo-markup synopsis)
389 "Check that SYNOPSIS can be parsed as a Texinfo fragment. If the
390markup is valid return a plain-text version of SYNOPSIS, otherwise #f."
391 (catch #t
392 (lambda ()
393 (texi->plain-text synopsis)
394 '())
395 (lambda (keys . args)
396 (list
397 (make-warning package
398 (G_ "Texinfo markup in synopsis is invalid")
399 #:field 'synopsis)))))
400
401 (define checks
402 (list check-proper-start
403 check-final-period
404 check-start-article
405 check-start-with-package-name
406 check-synopsis-length
407 check-texinfo-markup))
408
409 (match (package-synopsis package)
410 (""
411 (list
412 (make-warning package
413 (G_ "synopsis should not be empty")
414 #:field 'synopsis)))
415 ((? string? synopsis)
416 (append-map
417 (lambda (proc)
418 (proc synopsis))
419 checks))
420 (invalid
421 (list
422 (make-warning package
423 (G_ "invalid synopsis: ~s")
424 (list invalid)
425 #:field 'synopsis)))))
426
427(define* (probe-uri uri #:key timeout)
428 "Probe URI, a URI object, and return two values: a symbol denoting the
429probing status, such as 'http-response' when we managed to get an HTTP
430response from URI, and additional details, such as the actual HTTP response.
431
432TIMEOUT is the maximum number of seconds (possibly an inexact number) to wait
433for connections to complete; when TIMEOUT is #f, wait as long as needed."
434 (define headers
435 '((User-Agent . "GNU Guile")
436 (Accept . "*/*")))
437
438 (let loop ((uri uri)
439 (visited '()))
440 (match (uri-scheme uri)
441 ((or 'http 'https)
442 (catch #t
443 (lambda ()
444 (let ((port (guix:open-connection-for-uri
445 uri #:timeout timeout))
446 (request (build-request uri #:headers headers)))
447 (define response
448 (dynamic-wind
449 (const #f)
450 (lambda ()
451 (write-request request port)
452 (force-output port)
453 (read-response port))
454 (lambda ()
f4cde9ac 455 (close-port port))))
f363c836
CB
456
457 (case (response-code response)
458 ((302 ; found (redirection)
459 303 ; see other
460 307 ; temporary redirection
461 308) ; permanent redirection
462 (let ((location (response-location response)))
463 (if (or (not location) (member location visited))
464 (values 'http-response response)
465 (loop location (cons location visited))))) ;follow the redirect
466 ((301) ; moved permanently
467 (let ((location (response-location response)))
468 ;; Return RESPONSE, unless the final response as we follow
469 ;; redirects is not 200.
470 (if location
471 (let-values (((status response2)
472 (loop location (cons location visited))))
473 (case status
474 ((http-response)
475 (values 'http-response
476 (if (= 200 (response-code response2))
477 response
478 response2)))
479 (else
480 (values status response2))))
481 (values 'http-response response)))) ;invalid redirect
482 (else
483 (values 'http-response response)))))
484 (lambda (key . args)
485 (case key
486 ((bad-header bad-header-component)
487 ;; This can happen if the server returns an invalid HTTP header,
488 ;; as is the case with the 'Date' header at sqlite.org.
489 (values 'invalid-http-response #f))
490 ((getaddrinfo-error system-error
491 gnutls-error tls-certificate-error)
492 (values key args))
493 (else
494 (apply throw key args))))))
495 ('ftp
496 (catch #t
497 (lambda ()
498 (let ((conn (ftp-open (uri-host uri) #:timeout timeout)))
499 (define response
500 (dynamic-wind
501 (const #f)
502 (lambda ()
503 (ftp-chdir conn (dirname (uri-path uri)))
504 (ftp-size conn (basename (uri-path uri))))
505 (lambda ()
506 (ftp-close conn))))
507 (values 'ftp-response '(ok))))
508 (lambda (key . args)
509 (case key
510 ((ftp-error)
511 (values 'ftp-response `(error ,@args)))
512 ((getaddrinfo-error system-error gnutls-error)
513 (values key args))
514 (else
515 (apply throw key args))))))
516 (_
517 (values 'unknown-protocol #f)))))
518
519(define (tls-certificate-error-string args)
520 "Return a string explaining the 'tls-certificate-error' arguments ARGS."
521 (call-with-output-string
522 (lambda (port)
523 (print-exception port #f
524 'tls-certificate-error args))))
525
526(define (validate-uri uri package field)
527 "Return #t if the given URI can be reached, otherwise return a warning for
7f694149 528PACKAGE mentioning the FIELD."
f363c836
CB
529 (let-values (((status argument)
530 (probe-uri uri #:timeout 3))) ;wait at most 3 seconds
531 (case status
532 ((http-response)
533 (cond ((= 200 (response-code argument))
534 (match (response-content-length argument)
535 ((? number? length)
536 ;; As of July 2016, SourceForge returns 200 (instead of 404)
537 ;; with a small HTML page upon failure. Attempt to detect
538 ;; such malicious behavior.
539 (or (> length 1000)
540 (make-warning package
541 (G_ "URI ~a returned \
542suspiciously small file (~a bytes)")
543 (list (uri->string uri)
544 length)
545 #:field field)))
546 (_ #t)))
547 ((= 301 (response-code argument))
548 (if (response-location argument)
549 (make-warning package
550 (G_ "permanent redirect from ~a to ~a")
551 (list (uri->string uri)
552 (uri->string
553 (response-location argument)))
554 #:field field)
555 (make-warning package
556 (G_ "invalid permanent redirect \
557from ~a")
558 (list (uri->string uri))
559 #:field field)))
560 (else
561 (make-warning package
562 (G_ "URI ~a not reachable: ~a (~s)")
563 (list (uri->string uri)
564 (response-code argument)
565 (response-reason-phrase argument))
566 #:field field))))
567 ((ftp-response)
568 (match argument
569 (('ok) #t)
570 (('error port command code message)
571 (make-warning package
572 (G_ "URI ~a not reachable: ~a (~s)")
573 (list (uri->string uri)
574 code (string-trim-both message))
575 #:field field))))
576 ((getaddrinfo-error)
577 (make-warning package
578 (G_ "URI ~a domain not found: ~a")
579 (list (uri->string uri)
580 (gai-strerror (car argument)))
581 #:field field))
582 ((system-error)
583 (make-warning package
584 (G_ "URI ~a unreachable: ~a")
585 (list (uri->string uri)
586 (strerror
587 (system-error-errno
588 (cons status argument))))
589 #:field field))
590 ((tls-certificate-error)
591 (make-warning package
592 (G_ "TLS certificate error: ~a")
593 (list (tls-certificate-error-string argument))
594 #:field field))
595 ((invalid-http-response gnutls-error)
596 ;; Probably a misbehaving server; ignore.
597 #f)
598 ((unknown-protocol) ;nothing we can do
599 #f)
600 (else
601 (error "internal linter error" status)))))
602
603(define (check-home-page package)
604 "Emit a warning if PACKAGE has an invalid 'home-page' field, or if that
605'home-page' is not reachable."
606 (let ((uri (and=> (package-home-page package) string->uri)))
607 (cond
608 ((uri? uri)
609 (match (validate-uri uri package 'home-page)
610 ((and (? lint-warning? warning) warning)
611 (list warning))
612 (_ '())))
613 ((not (package-home-page package))
614 (if (or (string-contains (package-name package) "bootstrap")
615 (string=? (package-name package) "ld-wrapper"))
616 '()
617 (list
618 (make-warning package
619 (G_ "invalid value for home page")
620 #:field 'home-page))))
621 (else
622 (list
623 (make-warning package
624 (G_ "invalid home page URL: ~s")
625 (list (package-home-page package))
626 #:field 'home-page))))))
627
628(define %distro-directory
629 (mlambda ()
630 (dirname (search-path %load-path "gnu.scm"))))
631
632(define (check-patch-file-names package)
633 "Emit a warning if the patches requires by PACKAGE are badly named or if the
634patch could not be found."
635 (guard (c ((message-condition? c) ;raised by 'search-patch'
636 (list
637 ;; Use %make-warning, as condition-mesasge is already
638 ;; translated.
639 (%make-warning package (condition-message c)
640 #:field 'patch-file-names))))
641 (define patches
642 (or (and=> (package-source package) origin-patches)
643 '()))
644
645 (append
646 (if (every (match-lambda ;patch starts with package name?
647 ((? string? patch)
648 (and=> (string-contains (basename patch)
649 (package-name package))
650 zero?))
651 (_ #f)) ;must be an <origin> or something like that.
652 patches)
653 '()
654 (list
655 (make-warning
656 package
657 (G_ "file names of patches should start with the package name")
658 #:field 'patch-file-names)))
659
660 ;; Check whether we're reaching tar's maximum file name length.
661 (let ((prefix (string-length (%distro-directory)))
662 (margin (string-length "guix-0.13.0-10-123456789/"))
663 (max 99))
664 (filter-map (match-lambda
665 ((? string? patch)
666 (if (> (+ margin (if (string-prefix? (%distro-directory)
667 patch)
668 (- (string-length patch) prefix)
669 (string-length patch)))
670 max)
671 (make-warning
672 package
673 (G_ "~a: file name is too long")
674 (list (basename patch))
675 #:field 'patch-file-names)
676 #f))
677 (_ #f))
678 patches)))))
679
680(define (escape-quotes str)
681 "Replace any quote character in STR by an escaped quote character."
682 (list->string
683 (string-fold-right (lambda (chr result)
684 (match chr
685 (#\" (cons* #\\ #\"result))
686 (_ (cons chr result))))
687 '()
688 str)))
689
690(define official-gnu-packages*
691 (mlambda ()
692 "A memoizing version of 'official-gnu-packages' that returns the empty
693list when something goes wrong, such as a networking issue."
694 (let ((gnus (false-if-exception (official-gnu-packages))))
695 (or gnus '()))))
696
697(define (check-gnu-synopsis+description package)
698 "Make sure that, if PACKAGE is a GNU package, it uses the synopsis and
699descriptions maintained upstream."
700 (match (find (lambda (descriptor)
701 (string=? (gnu-package-name descriptor)
702 (package-name package)))
703 (official-gnu-packages*))
704 (#f ;not a GNU package, so nothing to do
705 '())
706 (descriptor ;a genuine GNU package
707 (append
708 (let ((upstream (gnu-package-doc-summary descriptor))
709 (downstream (package-synopsis package)))
710 (if (and upstream
711 (or (not (string? downstream))
712 (not (string=? upstream downstream))))
713 (list
714 (make-warning package
715 (G_ "proposed synopsis: ~s~%")
716 (list upstream)
717 #:field 'synopsis))
718 '()))
719
720 (let ((upstream (gnu-package-doc-description descriptor))
721 (downstream (package-description package)))
722 (if (and upstream
723 (or (not (string? downstream))
724 (not (string=? (fill-paragraph upstream 100)
725 (fill-paragraph downstream 100)))))
726 (list
727 (make-warning
728 package
729 (G_ "proposed description:~% \"~a\"~%")
730 (list (fill-paragraph (escape-quotes upstream) 77 7))
731 #:field 'description))
732 '()))))))
733
734(define (origin-uris origin)
735 "Return the list of URIs (strings) for ORIGIN."
736 (match (origin-uri origin)
737 ((? string? uri)
738 (list uri))
739 ((uris ...)
740 uris)))
741
742(define (check-source package)
743 "Emit a warning if PACKAGE has an invalid 'source' field, or if that
744'source' is not reachable."
745 (define (warnings-for-uris uris)
674b9df3
LC
746 (let loop ((uris uris)
747 (warnings '()))
748 (match uris
749 (()
750 (reverse warnings))
751 ((uri rest ...)
752 (match (validate-uri uri package 'source)
753 (#t
754 ;; We found a working URL, so stop right away.
755 '())
756 ((? lint-warning? warning)
757 (loop rest (cons warning warnings))))))))
f363c836
CB
758
759 (let ((origin (package-source package)))
760 (if (and origin
761 (eqv? (origin-method origin) url-fetch))
848ae71e
LC
762 (let* ((uris (append-map (cut maybe-expand-mirrors <> %mirrors)
763 (map string->uri (origin-uris origin))))
f363c836
CB
764 (warnings (warnings-for-uris uris)))
765
766 ;; Just make sure that at least one of the URIs is valid.
848ae71e 767 (if (= (length uris) (length warnings))
f363c836
CB
768 ;; When everything fails, report all of WARNINGS, otherwise don't
769 ;; report anything.
770 ;;
771 ;; XXX: Ideally we'd still allow warnings to be raised if *some*
772 ;; URIs are unreachable, but distinguish that from the error case
773 ;; where *all* the URIs are unreachable.
774 (cons*
775 (make-warning package
776 (G_ "all the source URIs are unreachable:")
777 #:field 'source)
778 warnings)
779 '()))
780 '())))
781
782(define (check-source-file-name package)
783 "Emit a warning if PACKAGE's origin has no meaningful file name."
784 (define (origin-file-name-valid? origin)
785 ;; Return #f if the source file name contains only a version or is #f;
786 ;; indicates that the origin needs a 'file-name' field.
787 (let ((file-name (origin-actual-file-name origin))
788 (version (package-version package)))
789 (and file-name
790 ;; Common in many projects is for the filename to start
791 ;; with a "v" followed by the version,
792 ;; e.g. "v3.2.0.tar.gz".
793 (not (string-match (string-append "^v?" version) file-name)))))
794
795 (let ((origin (package-source package)))
796 (if (or (not origin) (origin-file-name-valid? origin))
797 '()
798 (list
799 (make-warning package
800 (G_ "the source file name should contain the package name")
801 #:field 'source)))))
802
803(define (check-source-unstable-tarball package)
804 "Emit a warning if PACKAGE's source is an autogenerated tarball."
805 (define (check-source-uri uri)
806 (if (and (string=? (uri-host (string->uri uri)) "github.com")
807 (match (split-and-decode-uri-path
808 (uri-path (string->uri uri)))
809 ((_ _ "archive" _ ...) #t)
810 (_ #f)))
811 (make-warning package
812 (G_ "the source URI should not be an autogenerated tarball")
813 #:field 'source)
814 #f))
815
816 (let ((origin (package-source package)))
817 (if (and (origin? origin)
818 (eqv? (origin-method origin) url-fetch))
819 (filter-map check-source-uri
820 (origin-uris origin))
821 '())))
822
823(define (check-mirror-url package)
824 "Check whether PACKAGE uses source URLs that should be 'mirror://'."
825 (define (check-mirror-uri uri) ;XXX: could be optimized
826 (let loop ((mirrors %mirrors))
827 (match mirrors
828 (()
829 #f)
830 (((mirror-id mirror-urls ...) rest ...)
831 (match (find (cut string-prefix? <> uri) mirror-urls)
832 (#f
833 (loop rest))
834 (prefix
835 (make-warning package
836 (G_ "URL should be \
837'mirror://~a/~a'")
838 (list mirror-id
839 (string-drop uri (string-length prefix)))
840 #:field 'source)))))))
841
842 (let ((origin (package-source package)))
843 (if (and (origin? origin)
844 (eqv? (origin-method origin) url-fetch))
845 (let ((uris (origin-uris origin)))
846 (filter-map check-mirror-uri uris))
847 '())))
848
849(define* (check-github-url package #:key (timeout 3))
850 "Check whether PACKAGE uses source URLs that redirect to GitHub."
851 (define (follow-redirect url)
852 (let* ((uri (string->uri url))
853 (port (guix:open-connection-for-uri uri #:timeout timeout))
854 (response (http-head uri #:port port)))
855 (close-port port)
856 (case (response-code response)
857 ((301 302)
858 (uri->string (assoc-ref (response-headers response) 'location)))
859 (else #f))))
860
861 (define (follow-redirects-to-github uri)
862 (cond
863 ((string-prefix? "https://github.com/" uri) uri)
864 ((string-prefix? "http" uri)
865 (and=> (follow-redirect uri) follow-redirects-to-github))
866 ;; Do not attempt to follow redirects on URIs other than http and https
867 ;; (such as mirror, file)
868 (else #f)))
869
870 (let ((origin (package-source package)))
871 (if (and (origin? origin)
872 (eqv? (origin-method origin) url-fetch))
873 (filter-map
874 (lambda (uri)
875 (and=> (follow-redirects-to-github uri)
876 (lambda (github-uri)
877 (if (string=? github-uri uri)
878 #f
879 (make-warning
880 package
881 (G_ "URL should be '~a'")
882 (list github-uri)
883 #:field 'source)))))
884 (origin-uris origin))
885 '())))
886
887(define (check-derivation package)
888 "Emit a warning if we fail to compile PACKAGE to a derivation."
889 (define (try system)
890 (catch #t
891 (lambda ()
892 (guard (c ((store-protocol-error? c)
893 (make-warning package
894 (G_ "failed to create ~a derivation: ~a")
895 (list system
896 (store-protocol-error-message c))))
897 ((message-condition? c)
898 (make-warning package
899 (G_ "failed to create ~a derivation: ~a")
900 (list system
901 (condition-message c)))))
902 (with-store store
903 ;; Disable grafts since it can entail rebuilds.
904 (parameterize ((%graft? #f))
905 (package-derivation store package system #:graft? #f)
906
907 ;; If there's a replacement, make sure we can compute its
908 ;; derivation.
909 (match (package-replacement package)
910 (#f #t)
911 (replacement
912 (package-derivation store replacement system
913 #:graft? #f)))))))
914 (lambda args
915 (make-warning package
916 (G_ "failed to create ~a derivation: ~s")
917 (list system args)))))
918
919 (filter lint-warning?
920 (map try (package-supported-systems package))))
921
922(define (check-license package)
923 "Warn about type errors of the 'license' field of PACKAGE."
924 (match (package-license package)
925 ((or (? license?)
926 ((? license?) ...))
927 '())
928 (x
929 (list
930 (make-warning package (G_ "invalid license field")
931 #:field 'license)))))
932
933(define (call-with-networking-fail-safe message error-value proc)
934 "Call PROC catching any network-related errors. Upon a networking error,
935display a message including MESSAGE and return ERROR-VALUE."
936 (guard (c ((http-get-error? c)
937 (warning (G_ "~a: HTTP GET error for ~a: ~a (~s)~%")
938 message
939 (uri->string (http-get-error-uri c))
940 (http-get-error-code c)
941 (http-get-error-reason c))
942 error-value))
943 (catch #t
944 proc
945 (match-lambda*
946 (('getaddrinfo-error errcode)
947 (warning (G_ "~a: host lookup failure: ~a~%")
948 message
949 (gai-strerror errcode))
950 error-value)
951 (('tls-certificate-error args ...)
952 (warning (G_ "~a: TLS certificate error: ~a")
953 message
954 (tls-certificate-error-string args))
955 error-value)
900e0fbc
LC
956 ((and ('system-error _ ...) args)
957 (let ((errno (system-error-errno args)))
958 (if (member errno (list ECONNRESET ECONNABORTED ECONNREFUSED))
959 (let ((details (call-with-output-string
960 (lambda (port)
961 (print-exception port #f (car args)
962 (cdr args))))))
963 (warning (G_ "~a: ~a~%") message details)
964 error-value)
965 (apply throw args))))
f363c836
CB
966 (args
967 (apply throw args))))))
968
969(define-syntax-rule (with-networking-fail-safe message error-value exp ...)
970 (call-with-networking-fail-safe message error-value
971 (lambda () exp ...)))
972
973(define (current-vulnerabilities*)
974 "Like 'current-vulnerabilities', but return the empty list upon networking
975or HTTP errors. This allows network-less operation and makes problems with
976the NIST server non-fatal."
977 (with-networking-fail-safe (G_ "while retrieving CVE vulnerabilities")
978 '()
979 (current-vulnerabilities)))
980
981(define package-vulnerabilities
982 (let ((lookup (delay (vulnerabilities->lookup-proc
983 (current-vulnerabilities*)))))
984 (lambda (package)
985 "Return a list of vulnerabilities affecting PACKAGE."
986 ;; First we retrieve the Common Platform Enumeration (CPE) name and
987 ;; version for PACKAGE, then we can pass them to LOOKUP.
988 (let ((name (or (assoc-ref (package-properties package)
989 'cpe-name)
990 (package-name package)))
991 (version (or (assoc-ref (package-properties package)
992 'cpe-version)
993 (package-version package))))
994 ((force lookup) name version)))))
995
996(define (check-vulnerabilities package)
997 "Check for known vulnerabilities for PACKAGE."
998 (let ((package (or (package-replacement package) package)))
999 (match (package-vulnerabilities package)
1000 (()
1001 '())
1002 ((vulnerabilities ...)
1003 (let* ((patched (package-patched-vulnerabilities package))
1004 (known-safe (or (assq-ref (package-properties package)
1005 'lint-hidden-cve)
1006 '()))
1007 (unpatched (remove (lambda (vuln)
1008 (let ((id (vulnerability-id vuln)))
1009 (or (member id patched)
1010 (member id known-safe))))
1011 vulnerabilities)))
1012 (if (null? unpatched)
1013 '()
1014 (list
1015 (make-warning
1016 package
1017 (G_ "probably vulnerable to ~a")
1018 (list (string-join (map vulnerability-id unpatched)
1019 ", "))))))))))
1020
1021(define (check-for-updates package)
1022 "Check if there is an update available for PACKAGE."
1023 (match (with-networking-fail-safe
58d5f280
LC
1024 (format #f (G_ "while retrieving upstream info for '~a'")
1025 (package-name package))
f363c836
CB
1026 #f
1027 (package-latest-release* package (force %updaters)))
1028 ((? upstream-source? source)
1029 (if (version>? (upstream-source-version source)
1030 (package-version package))
1031 (list
1032 (make-warning package
1033 (G_ "can be upgraded to ~a")
1034 (list (upstream-source-version source))
1035 #:field 'version))
1036 '()))
1037 (#f '()))) ; cannot find newer upstream release
1038
55549c7b
LC
1039
1040(define (check-archival package)
1041 "Check whether PACKAGE's source code is archived on Software Heritage. If
1042it's not, and if its source code is a VCS snapshot, then send a \"save\"
1043request to Software Heritage.
1044
1045Software Heritage imposes limits on the request rate per client IP address.
1046This checker prints a notice and stops doing anything once that limit has been
1047reached."
1048 (define (response->warning url method response)
1049 (if (request-rate-limit-reached? url method)
1050 (list (make-warning package
1051 (G_ "Software Heritage rate limit reached; \
1052try again later")
1053 #:field 'source))
1054 (list (make-warning package
1055 (G_ "'~a' returned ~a")
1056 (list url (response-code response))
1057 #:field 'source))))
1058
1059 (define skip-key (gensym "skip-archival-check"))
1060
1061 (define (skip-when-limit-reached url method)
1062 (or (not (request-rate-limit-reached? url method))
1063 (throw skip-key #t)))
1064
1065 (parameterize ((%allow-request? skip-when-limit-reached))
1066 (catch #t
1067 (lambda ()
1068 (match (and (origin? (package-source package))
1069 (package-source package))
1070 (#f ;no source
1071 '())
1072 ((= origin-uri (? git-reference? reference))
1073 (define url
1074 (git-reference-url reference))
1075 (define commit
1076 (git-reference-commit reference))
1077
1078 (match (if (commit-id? commit)
1079 (or (lookup-revision commit)
1080 (lookup-origin-revision url commit))
1081 (lookup-origin-revision url commit))
1082 ((? revision? revision)
1083 '())
1084 (#f
1085 ;; Revision is missing from the archive, attempt to save it.
1086 (catch 'swh-error
1087 (lambda ()
1088 (save-origin (git-reference-url reference) "git")
1089 (list (make-warning
1090 package
1091 ;; TRANSLATORS: "Software Heritage" is a proper noun
1092 ;; that must remain untranslated. See
1093 ;; <https://www.softwareheritage.org>.
1094 (G_ "scheduled Software Heritage archival")
1095 #:field 'source)))
1096 (lambda (key url method response . _)
1097 (cond ((= 429 (response-code response))
1098 (list (make-warning
1099 package
1100 (G_ "archival rate limit exceeded; \
1101try again later")
1102 #:field 'source)))
1103 (else
1104 (response->warning url method response))))))))
1105 ((? origin? origin)
1106 ;; Since "save" origins are not supported for non-VCS source, all
1107 ;; we can do is tell whether a given tarball is available or not.
1108 (if (origin-sha256 origin) ;XXX: for ungoogled-chromium
1109 (match (lookup-content (origin-sha256 origin) "sha256")
1110 (#f
1111 (list (make-warning package
1112 (G_ "source not archived on Software \
1113Heritage")
1114 #:field 'source)))
1115 ((? content?)
1116 '()))
1117 '()))))
1118 (match-lambda*
1119 ((key url method response)
1120 (response->warning url method response))
1121 ((key . args)
1122 (if (eq? key skip-key)
1123 '()
c2b2c8e9
LC
1124 (with-networking-fail-safe
1125 (G_ "while connecting to Software Heritage")
1126 '()
1127 (apply throw key args))))))))
55549c7b 1128
f363c836
CB
1129\f
1130;;;
1131;;; Source code formatting.
1132;;;
1133
1134(define (report-tabulations package line line-number)
1135 "Warn about tabulations found in LINE."
1136 (match (string-index line #\tab)
7d09f2e8 1137 (#f #f)
f363c836
CB
1138 (index
1139 (make-warning package
1140 (G_ "tabulation on line ~a, column ~a")
1141 (list line-number index)
1142 #:location
1143 (location (package-file package)
1144 line-number
1145 index)))))
1146
1147(define (report-trailing-white-space package line line-number)
1148 "Warn about trailing white space in LINE."
7d09f2e8
LC
1149 (and (not (or (string=? line (string-trim-right line))
1150 (string=? line (string #\page))))
1151 (make-warning package
1152 (G_ "trailing white space on line ~a")
1153 (list line-number)
1154 #:location
1155 (location (package-file package)
1156 line-number
1157 0))))
f363c836
CB
1158
1159(define (report-long-line package line line-number)
1160 "Emit a warning if LINE is too long."
1161 ;; Note: We don't warn at 80 characters because sometimes hashes and URLs
1162 ;; make it hard to fit within that limit and we want to avoid making too
1163 ;; much noise.
7d09f2e8
LC
1164 (and (> (string-length line) 90)
1165 (make-warning package
1166 (G_ "line ~a is way too long (~a characters)")
1167 (list line-number (string-length line))
1168 #:location
1169 (location (package-file package)
1170 line-number
1171 0))))
f363c836
CB
1172
1173(define %hanging-paren-rx
1174 (make-regexp "^[[:blank:]]*[()]+[[:blank:]]*$"))
1175
1176(define (report-lone-parentheses package line line-number)
1177 "Emit a warning if LINE contains hanging parentheses."
7d09f2e8
LC
1178 (and (regexp-exec %hanging-paren-rx line)
1179 (make-warning package
1180 (G_ "parentheses feel lonely, \
f363c836 1181move to the previous or next line")
7d09f2e8
LC
1182 (list line-number)
1183 #:location
1184 (location (package-file package)
1185 line-number
1186 0))))
f363c836
CB
1187
1188(define %formatting-reporters
1189 ;; List of procedures that report formatting issues. These are not separate
1190 ;; checkers because they would need to re-read the file.
1191 (list report-tabulations
1192 report-trailing-white-space
1193 report-long-line
1194 report-lone-parentheses))
1195
1196(define* (report-formatting-issues package file starting-line
1197 #:key (reporters %formatting-reporters))
1198 "Report white-space issues in FILE starting from STARTING-LINE, and report
1199them for PACKAGE."
1200 (define (sexp-last-line port)
1201 ;; Return the last line of the sexp read from PORT or an estimate thereof.
1202 (define &failure (list 'failure))
1203
1204 (let ((start (ftell port))
1205 (start-line (port-line port))
1206 (sexp (catch 'read-error
1207 (lambda () (read port))
1208 (const &failure))))
1209 (let ((line (port-line port)))
1210 (seek port start SEEK_SET)
1211 (set-port-line! port start-line)
1212 (if (eq? sexp &failure)
1213 (+ start-line 60) ;conservative estimate
1214 line))))
1215
1216 (call-with-input-file file
1217 (lambda (port)
1218 (let loop ((line-number 1)
1219 (last-line #f)
1220 (warnings '()))
1221 (let ((line (read-line port)))
1222 (if (or (eof-object? line)
1223 (and last-line (> line-number last-line)))
1224 warnings
1225 (if (and (= line-number starting-line)
1226 (not last-line))
1227 (loop (+ 1 line-number)
1228 (+ 1 (sexp-last-line port))
1229 warnings)
1230 (loop (+ 1 line-number)
1231 last-line
1232 (append
1233 warnings
1234 (if (< line-number starting-line)
1235 '()
7d09f2e8
LC
1236 (filter-map (lambda (report)
1237 (report package line line-number))
1238 reporters)))))))))))
f363c836
CB
1239
1240(define (check-formatting package)
1241 "Check the formatting of the source code of PACKAGE."
1242 (let ((location (package-location package)))
1243 (if location
1244 (and=> (search-path %load-path (location-file location))
1245 (lambda (file)
1246 ;; Report issues starting from the line before the 'package'
1247 ;; form, which usually contains the 'define' form.
1248 (report-formatting-issues package file
1249 (- (location-line location) 1))))
1250 '())))
1251
1252\f
1253;;;
1254;;; List of checkers.
1255;;;
1256
38f3176a 1257(define %local-checkers
f363c836
CB
1258 (list
1259 (lint-checker
1260 (name 'description)
1261 (description "Validate package descriptions")
1262 (check check-description-style))
f363c836
CB
1263 (lint-checker
1264 (name 'inputs-should-be-native)
1265 (description "Identify inputs that should be native inputs")
1266 (check check-inputs-should-be-native))
1267 (lint-checker
1268 (name 'inputs-should-not-be-input)
1269 (description "Identify inputs that shouldn't be inputs at all")
1270 (check check-inputs-should-not-be-an-input-at-all))
f363c836
CB
1271 (lint-checker
1272 (name 'license)
1273 ;; TRANSLATORS: <license> is the name of a data type and must not be
1274 ;; translated.
1275 (description "Make sure the 'license' field is a <license> \
1276or a list thereof")
1277 (check check-license))
f363c836
CB
1278 (lint-checker
1279 (name 'mirror-url)
1280 (description "Suggest 'mirror://' URLs")
1281 (check check-mirror-url))
f363c836
CB
1282 (lint-checker
1283 (name 'source-file-name)
1284 (description "Validate file names of sources")
1285 (check check-source-file-name))
1286 (lint-checker
1287 (name 'source-unstable-tarball)
1288 (description "Check for autogenerated tarballs")
1289 (check check-source-unstable-tarball))
1290 (lint-checker
1291 (name 'derivation)
1292 (description "Report failure to compile a package to a derivation")
1293 (check check-derivation))
38f3176a
CB
1294 (lint-checker
1295 (name 'patch-file-names)
1296 (description "Validate file names and availability of patches")
1297 (check check-patch-file-names))
1298 (lint-checker
1299 (name 'formatting)
1300 (description "Look for formatting issues in the source")
1301 (check check-formatting))))
1302
1303(define %network-dependent-checkers
1304 (list
f363c836
CB
1305 (lint-checker
1306 (name 'synopsis)
1307 (description "Validate package synopses")
1308 (check check-synopsis-style))
38f3176a
CB
1309 (lint-checker
1310 (name 'gnu-description)
1311 (description "Validate synopsis & description of GNU packages")
1312 (check check-gnu-synopsis+description))
1313 (lint-checker
1314 (name 'home-page)
1315 (description "Validate home-page URLs")
1316 (check check-home-page))
1317 (lint-checker
1318 (name 'source)
1319 (description "Validate source URLs")
1320 (check check-source))
1321 (lint-checker
1322 (name 'github-url)
1323 (description "Suggest GitHub URLs")
1324 (check check-github-url))
9efa2c28
LC
1325 (lint-checker
1326 (name 'cve)
1327 (description "Check the Common Vulnerabilities and Exposures\
1328 (CVE) database")
1329 (check check-vulnerabilities))
f363c836
CB
1330 (lint-checker
1331 (name 'refresh)
1332 (description "Check the package for new upstream releases")
55549c7b
LC
1333 (check check-for-updates))
1334 (lint-checker
1335 (name 'archival)
1336 (description "Ensure source code archival on Software Heritage")
1337 (check check-archival))))
38f3176a
CB
1338
1339(define %all-checkers
1340 (append %local-checkers
1341 %network-dependent-checkers))