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