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