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>
4 ;;; Copyright © 2013, 2014, 2015, 2016 Ludovic Courtès <ludo@gnu.org>
5 ;;; Copyright © 2015, 2016 Mathieu Lirzin <mthl@gnu.org>
6 ;;; Copyright © 2016 Danny Milosavljevic <dannym+a@scratchpost.org>
8 ;;; This file is part of GNU Guix.
10 ;;; GNU Guix is free software; you can redistribute it and/or modify it
11 ;;; under the terms of the GNU General Public License as published by
12 ;;; the Free Software Foundation; either version 3 of the License, or (at
13 ;;; your option) any later version.
15 ;;; GNU Guix is distributed in the hope that it will be useful, but
16 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;;; GNU General Public License for more details.
20 ;;; You should have received a copy of the GNU General Public License
21 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
23 (define-module (guix scripts lint)
24 #:use-module ((guix store) #:hide (close-connection))
25 #:use-module (guix base32)
26 #:use-module (guix download)
27 #:use-module (guix ftp-client)
28 #:use-module (guix http-client)
29 #:use-module (guix packages)
30 #:use-module (guix licenses)
31 #:use-module (guix records)
32 #:use-module (guix ui)
33 #:use-module (guix utils)
34 #:use-module (guix scripts)
35 #:use-module (guix gnu-maintenance)
36 #:use-module (guix monads)
37 #:use-module (guix cve)
38 #:use-module (gnu packages)
39 #:use-module (ice-9 match)
40 #:use-module (ice-9 regex)
41 #:use-module (ice-9 format)
42 #:use-module (web uri)
43 #:use-module ((guix build download)
44 #:select (maybe-expand-mirrors
45 open-connection-for-uri
47 #:use-module (web request)
48 #:use-module (web response)
49 #:use-module (srfi srfi-1)
50 #:use-module (srfi srfi-6) ;Unicode string ports
51 #:use-module (srfi srfi-9)
52 #:use-module (srfi srfi-11)
53 #:use-module (srfi srfi-26)
54 #:use-module (srfi srfi-34)
55 #:use-module (srfi srfi-35)
56 #:use-module (srfi srfi-37)
57 #:use-module (ice-9 rdelim)
59 check-description-style
60 check-inputs-should-be-native
61 check-patch-file-names
66 check-source-file-name
76 lint-checker-description
83 (define* (emit-warning package message #:optional field)
84 ;; Emit a warning about PACKAGE, printing the location of FIELD if it is
85 ;; given, the location of PACKAGE otherwise, the full name of PACKAGE and the
87 (let ((loc (or (package-field-location package field)
88 (package-location package))))
89 (format (guix-warning-port) "~a: ~a: ~a~%"
90 (location->string loc)
91 (package-full-name package)
94 (define (call-with-accumulated-warnings thunk)
95 "Call THUNK, accumulating any warnings in the current state, using the state
97 (let ((port (open-output-string)))
98 (mlet %state-monad ((state (current-state))
99 (result -> (parameterize ((guix-warning-port port))
101 (warning -> (get-output-string port)))
103 (munless (string=? "" warning)
104 (set-current-state (cons warning state)))
107 (define-syntax-rule (with-accumulated-warnings exp ...)
108 "Evaluate EXP and accumulate warnings in the state monad."
109 (call-with-accumulated-warnings
117 (define-record-type* <lint-checker>
118 lint-checker make-lint-checker
120 ;; TODO: add a 'certainty' field that shows how confident we are in the
121 ;; checker. Then allow users to only run checkers that have a certain
122 ;; 'certainty' level.
123 (name lint-checker-name)
124 (description lint-checker-description)
125 (check lint-checker-check))
127 (define (list-checkers-and-exit)
128 ;; Print information about all available checkers and exit.
129 (format #t (_ "Available checkers:~%"))
130 (for-each (lambda (checker)
131 (format #t "- ~a: ~a~%"
132 (lint-checker-name checker)
133 (_ (lint-checker-description checker))))
137 (define (properly-starts-sentence? s)
138 (string-match "^[(\"'`[:upper:][:digit:]]" s))
140 (define (starts-with-abbreviation? s)
141 "Return #t if S starts with what looks like an abbreviation or acronym."
142 (string-match "^[A-Z][A-Z0-9]+\\>" s))
144 (define (check-description-style package)
145 ;; Emit a warning if stylistic issues are found in the description of PACKAGE.
146 (define (check-not-empty description)
147 (when (string-null? description)
148 (emit-warning package
149 (_ "description should not be empty")
152 (define (check-texinfo-markup description)
153 "Check that DESCRIPTION can be parsed as a Texinfo fragment. If the
154 markup is valid return a plain-text version of DESCRIPTION, otherwise #f."
156 (lambda () (texi->plain-text description))
157 (lambda (keys . args)
158 (emit-warning package
159 (_ "Texinfo markup in description is invalid")
163 (define (check-proper-start description)
164 (unless (or (properly-starts-sentence? description)
165 (string-prefix-ci? (package-name package) description))
166 (emit-warning package
167 (_ "description should start with an upper-case letter or digit")
170 (define (check-end-of-sentence-space description)
171 "Check that an end-of-sentence period is followed by two spaces."
173 (reverse (fold-matches
174 "\\. [A-Z]" description '()
176 ;; Filter out matches of common abbreviations.
177 (if (find (lambda (s)
178 (string-suffix-ci? s (match:prefix m)))
179 '("i.e" "e.g" "a.k.a" "resp"))
180 r (cons (match:start m) r)))))))
181 (unless (null? infractions)
182 (emit-warning package
183 (format #f (_ "sentences in description should be followed ~
184 by two spaces; possible infraction~p at ~{~a~^, ~}")
189 (let ((description (package-description package)))
190 (if (string? description)
192 (check-not-empty description)
193 ;; Use raw description for this because Texinfo rendering
194 ;; automatically fixes end of sentence space.
195 (check-end-of-sentence-space description)
196 (and=> (check-texinfo-markup description)
198 (emit-warning package
199 (format #f (_ "invalid description: ~s") description)
202 (define (check-inputs-should-be-native package)
203 ;; Emit a warning if some inputs of PACKAGE are likely to belong to its
205 (let ((inputs (package-inputs package)))
207 (((labels packages . _) ...)
208 (when (member "pkg-config"
209 (map package-name (filter package? packages)))
210 (emit-warning package
211 (_ "pkg-config should probably be a native input")
214 (define (package-name-regexp package)
215 "Return a regexp that matches PACKAGE's name as a word at the beginning of a
217 (make-regexp (string-append "^" (regexp-quote (package-name package))
221 (define (check-synopsis-style package)
222 ;; Emit a warning if stylistic issues are found in the synopsis of PACKAGE.
223 (define (check-not-empty synopsis)
224 (when (string-null? synopsis)
225 (emit-warning package
226 (_ "synopsis should not be empty")
229 (define (check-final-period synopsis)
230 ;; Synopsis should not end with a period, except for some special cases.
231 (when (and (string-suffix? "." synopsis)
232 (not (string-suffix? "etc." synopsis)))
233 (emit-warning package
234 (_ "no period allowed at the end of the synopsis")
237 (define check-start-article
238 ;; Skip this check for GNU packages, as suggested by Karl Berry's reply to
239 ;; <http://lists.gnu.org/archive/html/bug-womb/2014-11/msg00000.html>.
240 (if (false-if-exception (gnu-package? package))
243 (when (or (string-prefix-ci? "A " synopsis)
244 (string-prefix-ci? "An " synopsis))
245 (emit-warning package
246 (_ "no article allowed at the beginning of \
250 (define (check-synopsis-length synopsis)
251 (when (>= (string-length synopsis) 80)
252 (emit-warning package
253 (_ "synopsis should be less than 80 characters long")
256 (define (check-proper-start synopsis)
257 (unless (properly-starts-sentence? synopsis)
258 (emit-warning package
259 (_ "synopsis should start with an upper-case letter or digit")
262 (define (check-start-with-package-name synopsis)
263 (when (and (regexp-exec (package-name-regexp package) synopsis)
264 (not (starts-with-abbreviation? synopsis)))
265 (emit-warning package
266 (_ "synopsis should not start with the package name")
270 (list check-not-empty check-proper-start check-final-period
271 check-start-article check-start-with-package-name
272 check-synopsis-length))
274 (match (package-synopsis package)
275 ((? string? synopsis)
276 (for-each (lambda (proc)
280 (emit-warning package (format #f (_ "invalid synopsis: ~s") invalid)
283 (define* (probe-uri uri #:key timeout)
284 "Probe URI, a URI object, and return two values: a symbol denoting the
285 probing status, such as 'http-response' when we managed to get an HTTP
286 response from URI, and additional details, such as the actual HTTP response.
288 TIMEOUT is the maximum number of seconds (possibly an inexact number) to wait
289 for connections to complete; when TIMEOUT is #f, wait as long as needed."
291 '((User-Agent . "GNU Guile")
296 (match (uri-scheme uri)
300 (let ((port (open-connection-for-uri uri #:timeout timeout))
301 (request (build-request uri #:headers headers)))
306 (write-request request port)
308 (read-response port))
310 (close-connection port))))
312 (case (response-code response)
314 (let ((location (response-location response)))
315 (if (or (not location) (member location visited))
316 (values 'http-response response)
317 (loop location (cons location visited))))) ;follow the redirect
319 (values 'http-response response)))))
322 ((bad-header bad-header-component)
323 ;; This can happen if the server returns an invalid HTTP header,
324 ;; as is the case with the 'Date' header at sqlite.org.
325 (values 'invalid-http-response #f))
326 ((getaddrinfo-error system-error gnutls-error)
329 (apply throw key args))))))
333 (let ((conn (ftp-open (uri-host uri) #:timeout timeout)))
338 (ftp-chdir conn (dirname (uri-path uri)))
339 (ftp-size conn (basename (uri-path uri))))
342 (values 'ftp-response '(ok))))
346 (values 'ftp-response `(error ,@args)))
347 ((getaddrinfo-error system-error gnutls-error)
350 (apply throw key args))))))
352 (values 'unknown-protocol #f)))))
354 (define (validate-uri uri package field)
355 "Return #t if the given URI can be reached, otherwise return #f and emit a
356 warning for PACKAGE mentionning the FIELD."
357 (let-values (((status argument)
358 (probe-uri uri #:timeout 3))) ;wait at most 3 seconds
361 (or (= 200 (response-code argument))
363 (emit-warning package
365 (_ "URI ~a not reachable: ~a (~s)")
367 (response-code argument)
368 (response-reason-phrase argument))
374 (('error port command code message)
375 (emit-warning package
377 (_ "URI ~a not reachable: ~a (~s)")
379 code (string-trim-both message)))
382 (emit-warning package
384 (_ "URI ~a domain not found: ~a")
386 (gai-strerror (car argument)))
390 (emit-warning package
392 (_ "URI ~a unreachable: ~a")
396 (cons status argument))))
399 ((invalid-http-response gnutls-error)
400 ;; Probably a misbehaving server; ignore.
402 ((unknown-protocol) ;nothing we can do
405 (error "internal linter error" status)))))
407 (define (check-home-page package)
408 "Emit a warning if PACKAGE has an invalid 'home-page' field, or if that
409 'home-page' is not reachable."
410 (let ((uri (and=> (package-home-page package) string->uri)))
413 (validate-uri uri package 'home-page))
414 ((not (package-home-page package))
415 (unless (or (string-contains (package-name package) "bootstrap")
416 (string=? (package-name package) "ld-wrapper"))
417 (emit-warning package
418 (_ "invalid value for home page")
421 (emit-warning package (format #f (_ "invalid home page URL: ~s")
422 (package-home-page package))
425 (define (check-patch-file-names package)
426 "Emit a warning if the patches requires by PACKAGE are badly named or if the
427 patch could not be found."
428 (guard (c ((message-condition? c) ;raised by 'search-patch'
429 (emit-warning package (condition-message c)
431 (unless (every (match-lambda ;patch starts with package name?
433 (and=> (string-contains (basename patch)
434 (package-name package))
436 (_ #f)) ;must be an <origin> or something like that.
437 (or (and=> (package-source package) origin-patches)
441 (_ "file names of patches should start with the package name")
442 'patch-file-names))))
444 (define (escape-quotes str)
445 "Replace any quote character in STR by an escaped quote character."
447 (string-fold-right (lambda (chr result)
449 (#\" (cons* #\\ #\"result))
450 (_ (cons chr result))))
454 (define official-gnu-packages*
457 "A memoizing version of 'official-gnu-packages' that returns the empty
458 list when something goes wrong, such as a networking issue."
459 (let ((gnus (false-if-exception (official-gnu-packages))))
462 (define (check-gnu-synopsis+description package)
463 "Make sure that, if PACKAGE is a GNU package, it uses the synopsis and
464 descriptions maintained upstream."
465 (match (find (lambda (descriptor)
466 (string=? (gnu-package-name descriptor)
467 (package-name package)))
468 (official-gnu-packages*))
469 (#f ;not a GNU package, so nothing to do
471 (descriptor ;a genuine GNU package
472 (let ((upstream (gnu-package-doc-summary descriptor))
473 (downstream (package-synopsis package))
474 (loc (or (package-field-location package 'synopsis)
475 (package-location package))))
477 (or (not (string? downstream))
478 (not (string=? upstream downstream))))
479 (format (guix-warning-port)
480 (_ "~a: ~a: proposed synopsis: ~s~%")
481 (location->string loc) (package-full-name package)
484 (let ((upstream (gnu-package-doc-description descriptor))
485 (downstream (package-description package))
486 (loc (or (package-field-location package 'description)
487 (package-location package))))
489 (or (not (string? downstream))
490 (not (string=? (fill-paragraph upstream 100)
491 (fill-paragraph downstream 100)))))
492 (format (guix-warning-port)
493 (_ "~a: ~a: proposed description:~% \"~a\"~%")
494 (location->string loc) (package-full-name package)
495 (fill-paragraph (escape-quotes upstream) 77 7)))))))
497 (define (check-source package)
498 "Emit a warning if PACKAGE has an invalid 'source' field, or if that
499 'source' is not reachable."
500 (define (try-uris uris)
504 (with-accumulated-warnings
505 (validate-uri uri package 'source)))
506 (append-map (cut maybe-expand-mirrors <> %mirrors)
510 (let ((origin (package-source package)))
512 (eqv? (origin-method origin) url-fetch))
513 (let* ((strings (origin-uri origin))
514 (uris (if (list? strings)
515 (map string->uri strings)
516 (list (string->uri strings)))))
518 ;; Just make sure that at least one of the URIs is valid.
520 (lambda () (try-uris uris))
521 (lambda (success? warnings)
522 ;; When everything fails, report all of WARNINGS, otherwise don't
525 ;; XXX: Ideally we'd still allow warnings to be raised if *some*
526 ;; URIs are unreachable, but distinguish that from the error case
527 ;; where *all* the URIs are unreachable.
529 (emit-warning package
530 (_ "all the source URIs are unreachable:")
532 (for-each (lambda (warning)
533 (display warning (guix-warning-port)))
534 (reverse warnings)))))))))
536 (define (check-source-file-name package)
537 "Emit a warning if PACKAGE's origin has no meaningful file name."
538 (define (origin-file-name-valid? origin)
539 ;; Return #t if the source file name contains only a version or is #f;
540 ;; indicates that the origin needs a 'file-name' field.
541 (let ((file-name (origin-actual-file-name origin))
542 (version (package-version package)))
544 (not (or (string-prefix? version file-name)
545 ;; Common in many projects is for the filename to start
546 ;; with a "v" followed by the version,
547 ;; e.g. "v3.2.0.tar.gz".
548 (string-prefix? (string-append "v" version) file-name))))))
550 (let ((origin (package-source package)))
551 (unless (or (not origin) (origin-file-name-valid? origin))
552 (emit-warning package
553 (_ "the source file name should contain the package name")
556 (define (check-derivation package)
557 "Emit a warning if we fail to compile PACKAGE to a derivation."
560 (guard (c ((nix-protocol-error? c)
561 (emit-warning package
562 (format #f (_ "failed to create derivation: ~a")
563 (nix-protocol-error-message c))))
564 ((message-condition? c)
565 (emit-warning package
566 (format #f (_ "failed to create derivation: ~a")
567 (condition-message c)))))
569 ;; Disable grafts since it can entail rebuilds.
570 (package-derivation store package #:graft? #f)
572 ;; If there's a replacement, make sure we can compute its
574 (match (package-replacement package)
577 (package-derivation store replacement #:graft? #f))))))
579 (emit-warning package
580 (format #f (_ "failed to create derivation: ~s~%")
583 (define (check-license package)
584 "Warn about type errors of the 'license' field of PACKAGE."
585 (match (package-license package)
590 (emit-warning package (_ "invalid license field")
593 (define (patch-file-name patch)
594 "Return the basename of PATCH's file name, or #f if the file name could not
600 (and=> (origin-actual-file-name patch) basename))))
602 (define (package-name->cpe-name name)
603 "Do a basic conversion of NAME, a Guix package name, to the corresponding
604 Common Platform Enumeration (CPE) name."
606 ("icecat" "firefox") ;or "firefox_esr"
611 (define (current-vulnerabilities*)
612 "Like 'current-vulnerabilities', but return the empty list upon networking
613 or HTTP errors. This allows network-less operation and makes problems with
614 the NIST server non-fatal.."
615 (guard (c ((http-get-error? c)
616 (warning (_ "failed to retrieve CVE vulnerabilities \
618 (uri->string (http-get-error-uri c))
619 (http-get-error-code c)
620 (http-get-error-reason c))
621 (warning (_ "assuming no CVE vulnerabilities~%"))
623 (catch 'getaddrinfo-error
625 (current-vulnerabilities))
626 (lambda (key errcode)
627 (warning (_ "failed to lookup NIST host: ~a~%")
628 (gai-strerror errcode))
629 (warning (_ "assuming no CVE vulnerabilities~%"))
632 (define package-vulnerabilities
633 (let ((lookup (delay (vulnerabilities->lookup-proc
634 (current-vulnerabilities*)))))
636 "Return a list of vulnerabilities affecting PACKAGE."
638 (package-name->cpe-name (package-name package))
639 (package-version package)))))
641 (define (check-vulnerabilities package)
642 "Check for known vulnerabilities for PACKAGE."
643 (match (package-vulnerabilities package)
646 ((vulnerabilities ...)
647 (let* ((patches (filter-map patch-file-name
648 (or (and=> (package-source package)
651 (unpatched (remove (lambda (vuln)
652 (find (cute string-contains
653 <> (vulnerability-id vuln))
656 (unless (null? unpatched)
657 (emit-warning package
658 (format #f (_ "probably vulnerable to ~a")
659 (string-join (map vulnerability-id unpatched)
664 ;;; Source code formatting.
667 (define (report-tabulations package line line-number)
668 "Warn about tabulations found in LINE."
669 (match (string-index line #\tab)
672 (emit-warning package
673 (format #f (_ "tabulation on line ~a, column ~a")
674 line-number index)))))
676 (define (report-trailing-white-space package line line-number)
677 "Warn about trailing white space in LINE."
678 (unless (or (string=? line (string-trim-right line))
679 (string=? line (string #\page)))
680 (emit-warning package
682 (_ "trailing white space on line ~a")
685 (define (report-long-line package line line-number)
686 "Emit a warning if LINE is too long."
687 ;; Note: We don't warn at 80 characters because sometimes hashes and URLs
688 ;; make it hard to fit within that limit and we want to avoid making too
690 (when (> (string-length line) 90)
691 (emit-warning package
692 (format #f (_ "line ~a is way too long (~a characters)")
693 line-number (string-length line)))))
695 (define %hanging-paren-rx
696 (make-regexp "^[[:blank:]]*[()]+[[:blank:]]*$"))
698 (define (report-lone-parentheses package line line-number)
699 "Emit a warning if LINE contains hanging parentheses."
700 (when (regexp-exec %hanging-paren-rx line)
701 (emit-warning package
703 (_ "line ~a: parentheses feel lonely, \
704 move to the previous or next line")
707 (define %formatting-reporters
708 ;; List of procedures that report formatting issues. These are not separate
709 ;; checkers because they would need to re-read the file.
710 (list report-tabulations
711 report-trailing-white-space
713 report-lone-parentheses))
715 (define* (report-formatting-issues package file starting-line
716 #:key (reporters %formatting-reporters))
717 "Report white-space issues in FILE starting from STARTING-LINE, and report
720 ;; Number of the presumed last line.
721 ;; XXX: Ideally we'd stop at the boundaries of the surrounding sexp, but
722 ;; for now just use this simple heuristic.
723 (+ starting-line 60))
725 (call-with-input-file file
727 (let loop ((line-number 1))
728 (let ((line (read-line port)))
729 (or (eof-object? line)
730 (> line-number last-line)
732 (unless (< line-number starting-line)
733 (for-each (lambda (report)
734 (report package line line-number))
736 (loop (+ 1 line-number)))))))))
738 (define (check-formatting package)
739 "Check the formatting of the source code of PACKAGE."
740 (let ((location (package-location package)))
742 (and=> (search-path %load-path (location-file location))
744 ;; Report issues starting from the line before the 'package'
745 ;; form, which usually contains the 'define' form.
746 (report-formatting-issues package file
747 (- (location-line location) 1)))))))
751 ;;; List of checkers.
758 (description "Validate package descriptions")
759 (check check-description-style))
761 (name 'gnu-description)
762 (description "Validate synopsis & description of GNU packages")
763 (check check-gnu-synopsis+description))
765 (name 'inputs-should-be-native)
766 (description "Identify inputs that should be native inputs")
767 (check check-inputs-should-be-native))
769 (name 'patch-file-names)
770 (description "Validate file names and availability of patches")
771 (check check-patch-file-names))
774 (description "Validate home-page URLs")
775 (check check-home-page))
778 ;; TRANSLATORS: <license> is the name of a data type and must not be
780 (description "Make sure the 'license' field is a <license> \
782 (check check-license))
785 (description "Validate source URLs")
786 (check check-source))
788 (name 'source-file-name)
789 (description "Validate file names of sources")
790 (check check-source-file-name))
793 (description "Report failure to compile a package to a derivation")
794 (check check-derivation))
797 (description "Validate package synopses")
798 (check check-synopsis-style))
801 (description "Check the Common Vulnerabilities and Exposures\
803 (check check-vulnerabilities))
806 (description "Look for formatting issues in the source")
807 (check check-formatting))))
809 (define* (run-checkers package #:optional (checkers %checkers))
810 "Run the given CHECKERS on PACKAGE."
811 (let ((tty? (isatty? (current-error-port)))
812 (name (package-full-name package)))
813 (for-each (lambda (checker)
815 (format (current-error-port) "checking ~a [~a]...\x1b[K\r"
816 name (lint-checker-name checker))
817 (force-output (current-error-port)))
818 ((lint-checker-check checker) package))
821 (format (current-error-port) "\x1b[K")
822 (force-output (current-error-port)))))
826 ;;; Command-line options.
829 (define %default-options
830 ;; Alist of default option values.
834 (display (_ "Usage: guix lint [OPTION]... [PACKAGE]...
835 Run a set of checkers on the specified package; if none is specified,
836 run the checkers on all packages.\n"))
838 -c, --checkers=CHECKER1,CHECKER2...
839 only run the specified checkers"))
841 -h, --help display this help and exit"))
843 -l, --list-checkers display the list of available lint checkers"))
845 -V, --version display version information and exit"))
847 (show-bug-report-information))
851 ;; Specification of the command-line options.
852 ;; TODO: add some options:
853 ;; * --certainty=[low,medium,high]: only run checkers that have at least this
855 (list (option '(#\c "checkers") #t #f
856 (lambda (opt name arg result)
857 (let ((names (map string->symbol (string-split arg #\,))))
858 (for-each (lambda (c)
860 (map lint-checker-name
862 (leave (_ "~a: invalid checker~%") c)))
864 (alist-cons 'checkers
865 (filter (lambda (checker)
866 (member (lint-checker-name checker)
870 (option '(#\h "help") #f #f
874 (option '(#\l "list-checkers") #f #f
876 (list-checkers-and-exit)))
877 (option '(#\V "version") #f #f
879 (show-version-and-exit "guix lint")))))
886 (define (guix-lint . args)
887 (define (parse-options)
888 ;; Return the alist of option values.
889 (args-fold* args %options
890 (lambda (opt name arg result)
891 (leave (_ "~A: unrecognized option~%") name))
893 (alist-cons 'argument arg result))
896 (let* ((opts (parse-options))
897 (args (filter-map (match-lambda
902 (checkers (or (assoc-ref opts 'checkers) %checkers)))
904 (fold-packages (lambda (p r) (run-checkers p checkers)) '())
905 (for-each (lambda (spec)
906 (run-checkers (specification->package spec) checkers))