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