Mirror of GNU Guix
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

565 lines
21 KiB

  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2014 Cyril Roelandt <tipecaml@gmail.com>
  3. ;;; Copyright © 2014 Eric Bavier <bavier@member.fsf.org>
  4. ;;; Copyright © 2013, 2014, 2015 Ludovic Courtès <ludo@gnu.org>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (guix scripts lint)
  21. #:use-module (guix base32)
  22. #:use-module (guix download)
  23. #:use-module (guix ftp-client)
  24. #:use-module (guix packages)
  25. #:use-module (guix records)
  26. #:use-module (guix ui)
  27. #:use-module (guix utils)
  28. #:use-module (guix gnu-maintenance)
  29. #:use-module (gnu packages)
  30. #:use-module (ice-9 match)
  31. #:use-module (ice-9 regex)
  32. #:use-module (ice-9 format)
  33. #:use-module (web uri)
  34. #:use-module ((guix build download)
  35. #:select (maybe-expand-mirrors
  36. open-connection-for-uri))
  37. #:use-module (web request)
  38. #:use-module (web response)
  39. #:use-module (srfi srfi-1)
  40. #:use-module (srfi srfi-9)
  41. #:use-module (srfi srfi-11)
  42. #:use-module (srfi srfi-26)
  43. #:use-module (srfi srfi-37)
  44. #:export (guix-lint
  45. check-description-style
  46. check-inputs-should-be-native
  47. check-patches
  48. check-synopsis-style
  49. check-home-page
  50. check-source))
  51. ;;;
  52. ;;; Helpers
  53. ;;;
  54. (define* (emit-warning package message #:optional field)
  55. ;; Emit a warning about PACKAGE, printing the location of FIELD if it is
  56. ;; given, the location of PACKAGE otherwise, the full name of PACKAGE and the
  57. ;; provided MESSAGE.
  58. (let ((loc (or (package-field-location package field)
  59. (package-location package))))
  60. (format (guix-warning-port) "~a: ~a: ~a~%"
  61. (location->string loc)
  62. (package-full-name package)
  63. message)))
  64. ;;;
  65. ;;; Checkers
  66. ;;;
  67. (define-record-type* <lint-checker>
  68. lint-checker make-lint-checker
  69. lint-checker?
  70. ;; TODO: add a 'certainty' field that shows how confident we are in the
  71. ;; checker. Then allow users to only run checkers that have a certain
  72. ;; 'certainty' level.
  73. (name lint-checker-name)
  74. (description lint-checker-description)
  75. (check lint-checker-check))
  76. (define (list-checkers-and-exit)
  77. ;; Print information about all available checkers and exit.
  78. (format #t (_ "Available checkers:~%"))
  79. (for-each (lambda (checker)
  80. (format #t "- ~a: ~a~%"
  81. (lint-checker-name checker)
  82. (_ (lint-checker-description checker))))
  83. %checkers)
  84. (exit 0))
  85. (define (properly-starts-sentence? s)
  86. (string-match "^[(\"'[:upper:][:digit:]]" s))
  87. (define (starts-with-abbreviation? s)
  88. "Return #t if S starts with what looks like an abbreviation or acronym."
  89. (string-match "^[A-Z][A-Z0-9]+\\>" s))
  90. (define (check-description-style package)
  91. ;; Emit a warning if stylistic issues are found in the description of PACKAGE.
  92. (define (check-not-empty description)
  93. (when (string-null? description)
  94. (emit-warning package
  95. (_ "description should not be empty")
  96. 'description)))
  97. (define (check-proper-start description)
  98. (unless (or (properly-starts-sentence? description)
  99. (string-prefix-ci? (package-name package) description))
  100. (emit-warning package
  101. (_ "description should start with an upper-case letter or digit")
  102. 'description)))
  103. (define (check-end-of-sentence-space description)
  104. "Check that an end-of-sentence period is followed by two spaces."
  105. (let ((infractions
  106. (reverse (fold-matches
  107. "\\. [A-Z]" description '()
  108. (lambda (m r)
  109. ;; Filter out matches of common abbreviations.
  110. (if (find (lambda (s)
  111. (string-suffix-ci? s (match:prefix m)))
  112. '("i.e" "e.g" "a.k.a" "resp"))
  113. r (cons (match:start m) r)))))))
  114. (unless (null? infractions)
  115. (emit-warning package
  116. (format #f (_ "sentences in description should be followed ~
  117. by two spaces; possible infraction~p at ~{~a~^, ~}")
  118. (length infractions)
  119. infractions)
  120. 'description))))
  121. (let ((description (package-description package)))
  122. (when (string? description)
  123. (check-not-empty description)
  124. (check-proper-start description)
  125. (check-end-of-sentence-space description))))
  126. (define (check-inputs-should-be-native package)
  127. ;; Emit a warning if some inputs of PACKAGE are likely to belong to its
  128. ;; native inputs.
  129. (let ((inputs (package-inputs package)))
  130. (match inputs
  131. (((labels packages . _) ...)
  132. (when (member "pkg-config"
  133. (map package-name (filter package? packages)))
  134. (emit-warning package
  135. (_ "pkg-config should probably be a native input")
  136. 'inputs))))))
  137. (define (package-name-regexp package)
  138. "Return a regexp that matches PACKAGE's name as a word at the beginning of a
  139. line."
  140. (make-regexp (string-append "^" (regexp-quote (package-name package))
  141. "\\>")
  142. regexp/icase))
  143. (define (check-synopsis-style package)
  144. ;; Emit a warning if stylistic issues are found in the synopsis of PACKAGE.
  145. (define (check-not-empty synopsis)
  146. (when (string-null? synopsis)
  147. (emit-warning package
  148. (_ "synopsis should not be empty")
  149. 'synopsis)))
  150. (define (check-final-period synopsis)
  151. ;; Synopsis should not end with a period, except for some special cases.
  152. (when (and (string-suffix? "." synopsis)
  153. (not (string-suffix? "etc." synopsis)))
  154. (emit-warning package
  155. (_ "no period allowed at the end of the synopsis")
  156. 'synopsis)))
  157. (define check-start-article
  158. ;; Skip this check for GNU packages, as suggested by Karl Berry's reply to
  159. ;; <http://lists.gnu.org/archive/html/bug-womb/2014-11/msg00000.html>.
  160. (if (false-if-exception (gnu-package? package))
  161. (const #t)
  162. (lambda (synopsis)
  163. (when (or (string-prefix-ci? "A " synopsis)
  164. (string-prefix-ci? "An " synopsis))
  165. (emit-warning package
  166. (_ "no article allowed at the beginning of \
  167. the synopsis")
  168. 'synopsis)))))
  169. (define (check-synopsis-length synopsis)
  170. (when (>= (string-length synopsis) 80)
  171. (emit-warning package
  172. (_ "synopsis should be less than 80 characters long")
  173. 'synopsis)))
  174. (define (check-proper-start synopsis)
  175. (unless (properly-starts-sentence? synopsis)
  176. (emit-warning package
  177. (_ "synopsis should start with an upper-case letter or digit")
  178. 'synopsis)))
  179. (define (check-start-with-package-name synopsis)
  180. (when (and (regexp-exec (package-name-regexp package) synopsis)
  181. (not (starts-with-abbreviation? synopsis)))
  182. (emit-warning package
  183. (_ "synopsis should not start with the package name")
  184. 'synopsis)))
  185. (let ((synopsis (package-synopsis package)))
  186. (when (string? synopsis)
  187. (check-not-empty synopsis)
  188. (check-proper-start synopsis)
  189. (check-final-period synopsis)
  190. (check-start-article synopsis)
  191. (check-start-with-package-name synopsis)
  192. (check-synopsis-length synopsis))))
  193. (define (probe-uri uri)
  194. "Probe URI, a URI object, and return two values: a symbol denoting the
  195. probing status, such as 'http-response' when we managed to get an HTTP
  196. response from URI, and additional details, such as the actual HTTP response."
  197. (define headers
  198. '((User-Agent . "GNU Guile")
  199. (Accept . "*/*")))
  200. (let loop ((uri uri)
  201. (visited '()))
  202. (match (uri-scheme uri)
  203. ((or 'http 'https)
  204. (catch #t
  205. (lambda ()
  206. (let ((port (open-connection-for-uri uri))
  207. (request (build-request uri #:headers headers)))
  208. (define response
  209. (dynamic-wind
  210. (const #f)
  211. (lambda ()
  212. (write-request request port)
  213. (force-output port)
  214. (read-response port))
  215. (lambda ()
  216. (close port))))
  217. (case (response-code response)
  218. ((301 302 307)
  219. (let ((location (response-location response)))
  220. (if (or (not location) (member location visited))
  221. (values 'http-response response)
  222. (loop location (cons location visited))))) ;follow the redirect
  223. (else
  224. (values 'http-response response)))))
  225. (lambda (key . args)
  226. (case key
  227. ((bad-header bad-header-component)
  228. ;; This can happen if the server returns an invalid HTTP header,
  229. ;; as is the case with the 'Date' header at sqlite.org.
  230. (values 'invalid-http-response #f))
  231. ((getaddrinfo-error system-error gnutls-error)
  232. (values key args))
  233. (else
  234. (apply throw key args))))))
  235. ('ftp
  236. (catch #t
  237. (lambda ()
  238. (let ((port (ftp-open (uri-host uri) 21)))
  239. (define response
  240. (dynamic-wind
  241. (const #f)
  242. (lambda ()
  243. (ftp-chdir port (dirname (uri-path uri)))
  244. (ftp-size port (basename (uri-path uri))))
  245. (lambda ()
  246. (ftp-close port))))
  247. (values 'ftp-response #t)))
  248. (lambda (key . args)
  249. (case key
  250. ((or ftp-error)
  251. (values 'ftp-response #f))
  252. ((getaddrinfo-error system-error gnutls-error)
  253. (values key args))
  254. (else
  255. (apply throw key args))))))
  256. (_
  257. (values 'unknown-protocol #f)))))
  258. (define (validate-uri uri package field)
  259. "Return #t if the given URI can be reached, otherwise emit a
  260. warning for PACKAGE mentionning the FIELD."
  261. (let-values (((status argument)
  262. (probe-uri uri)))
  263. (case status
  264. ((http-response)
  265. (or (= 200 (response-code argument))
  266. (emit-warning package
  267. (format #f
  268. (_ "URI ~a not reachable: ~a (~s)")
  269. (uri->string uri)
  270. (response-code argument)
  271. (response-reason-phrase argument))
  272. field)))
  273. ((ftp-response)
  274. (when (not argument)
  275. (emit-warning package
  276. (format #f
  277. (_ "URI ~a not reachable")
  278. (uri->string uri)))))
  279. ((getaddrinfo-error)
  280. (emit-warning package
  281. (format #f
  282. (_ "URI ~a domain not found: ~a")
  283. (uri->string uri)
  284. (gai-strerror (car argument)))
  285. field)
  286. #f)
  287. ((system-error)
  288. (emit-warning package
  289. (format #f
  290. (_ "URI ~a unreachable: ~a")
  291. (uri->string uri)
  292. (strerror
  293. (system-error-errno
  294. (cons status argument))))
  295. field)
  296. #f)
  297. ((invalid-http-response gnutls-error)
  298. ;; Probably a misbehaving server; ignore.
  299. #f)
  300. ((unknown-protocol) ;nothing we can do
  301. #f)
  302. (else
  303. (error "internal linter error" status)))))
  304. (define (check-home-page package)
  305. "Emit a warning if PACKAGE has an invalid 'home-page' field, or if that
  306. 'home-page' is not reachable."
  307. (let ((uri (and=> (package-home-page package) string->uri)))
  308. (cond
  309. ((uri? uri)
  310. (validate-uri uri package 'home-page))
  311. ((not (package-home-page package))
  312. (unless (or (string-contains (package-name package) "bootstrap")
  313. (string=? (package-name package) "ld-wrapper"))
  314. (emit-warning package
  315. (_ "invalid value for home page")
  316. 'home-page)))
  317. (else
  318. (emit-warning package (format #f (_ "invalid home page URL: ~s")
  319. (package-home-page package))
  320. 'home-page)))))
  321. (define (check-patches package)
  322. ;; Emit a warning if the patches requires by PACKAGE are badly named.
  323. (let ((patches (and=> (package-source package) origin-patches))
  324. (name (package-name package))
  325. (full-name (package-full-name package)))
  326. (when (and patches
  327. (any (match-lambda
  328. ((? string? patch)
  329. (let ((filename (basename patch)))
  330. (not (or (eq? (string-contains filename name) 0)
  331. (eq? (string-contains filename full-name)
  332. 0)))))
  333. (_
  334. ;; This must be an <origin> or something like that.
  335. #f))
  336. patches))
  337. (emit-warning package
  338. (_ "file names of patches should start with \
  339. the package name")
  340. 'patches))))
  341. (define (escape-quotes str)
  342. "Replace any quote character in STR by an escaped quote character."
  343. (list->string
  344. (string-fold-right (lambda (chr result)
  345. (match chr
  346. (#\" (cons* #\\ #\"result))
  347. (_ (cons chr result))))
  348. '()
  349. str)))
  350. (define official-gnu-packages*
  351. (memoize
  352. (lambda ()
  353. "A memoizing version of 'official-gnu-packages' that returns the empty
  354. list when something goes wrong, such as a networking issue."
  355. (let ((gnus (false-if-exception (official-gnu-packages))))
  356. (or gnus '())))))
  357. (define (check-gnu-synopsis+description package)
  358. "Make sure that, if PACKAGE is a GNU package, it uses the synopsis and
  359. descriptions maintained upstream."
  360. (match (find (lambda (descriptor)
  361. (string=? (gnu-package-name descriptor)
  362. (package-name package)))
  363. (official-gnu-packages*))
  364. (#f ;not a GNU package, so nothing to do
  365. #t)
  366. (descriptor ;a genuine GNU package
  367. (let ((upstream (gnu-package-doc-summary descriptor))
  368. (downstream (package-synopsis package))
  369. (loc (or (package-field-location package 'synopsis)
  370. (package-location package))))
  371. (unless (and upstream (string=? upstream downstream))
  372. (format (guix-warning-port)
  373. (_ "~a: ~a: proposed synopsis: ~s~%")
  374. (location->string loc) (package-full-name package)
  375. upstream)))
  376. (let ((upstream (gnu-package-doc-description descriptor))
  377. (downstream (package-description package))
  378. (loc (or (package-field-location package 'description)
  379. (package-location package))))
  380. (when (and upstream
  381. (not (string=? (fill-paragraph upstream 100)
  382. (fill-paragraph downstream 100))))
  383. (format (guix-warning-port)
  384. (_ "~a: ~a: proposed description:~% \"~a\"~%")
  385. (location->string loc) (package-full-name package)
  386. (fill-paragraph (escape-quotes upstream) 77 7)))))))
  387. (define (check-source package)
  388. "Emit a warning if PACKAGE has an invalid 'source' field, or if that
  389. 'source' is not reachable."
  390. (let ((origin (package-source package)))
  391. (when (and origin
  392. (eqv? (origin-method origin) url-fetch))
  393. (let* ((strings (origin-uri origin))
  394. (uris (if (list? strings)
  395. (map string->uri strings)
  396. (list (string->uri strings)))))
  397. ;; Just make sure that at least one of the URIs is valid.
  398. (any (cut validate-uri <> package 'source)
  399. (append-map (cut maybe-expand-mirrors <> %mirrors)
  400. uris))))))
  401. ;;;
  402. ;;; List of checkers.
  403. ;;;
  404. (define %checkers
  405. (list
  406. (lint-checker
  407. (name 'description)
  408. (description "Validate package descriptions")
  409. (check check-description-style))
  410. (lint-checker
  411. (name 'gnu-description)
  412. (description "Validate synopsis & description of GNU packages")
  413. (check check-gnu-synopsis+description))
  414. (lint-checker
  415. (name 'inputs-should-be-native)
  416. (description "Identify inputs that should be native inputs")
  417. (check check-inputs-should-be-native))
  418. (lint-checker
  419. (name 'patch-filenames)
  420. (description "Validate file names of patches")
  421. (check check-patches))
  422. (lint-checker
  423. (name 'home-page)
  424. (description "Validate home-page URLs")
  425. (check check-home-page))
  426. (lint-checker
  427. (name 'source)
  428. (description "Validate source URLs")
  429. (check check-source))
  430. (lint-checker
  431. (name 'synopsis)
  432. (description "Validate package synopses")
  433. (check check-synopsis-style))))
  434. (define (run-checkers package checkers)
  435. ;; Run the given CHECKERS on PACKAGE.
  436. (let ((tty? (isatty? (current-error-port)))
  437. (name (package-full-name package)))
  438. (for-each (lambda (checker)
  439. (when tty?
  440. (format (current-error-port) "checking ~a [~a]...\r"
  441. name (lint-checker-name checker))
  442. (force-output (current-error-port)))
  443. ((lint-checker-check checker) package))
  444. checkers)))
  445. ;;;
  446. ;;; Command-line options.
  447. ;;;
  448. (define %default-options
  449. ;; Alist of default option values.
  450. '())
  451. (define (show-help)
  452. (display (_ "Usage: guix lint [OPTION]... [PACKAGE]...
  453. Run a set of checkers on the specified package; if none is specified, run the checkers on all packages.\n"))
  454. (display (_ "
  455. -c, --checkers=CHECKER1,CHECKER2...
  456. only run the specificed checkers"))
  457. (display (_ "
  458. -h, --help display this help and exit"))
  459. (display (_ "
  460. -l, --list-checkers display the list of available lint checkers"))
  461. (display (_ "
  462. -V, --version display version information and exit"))
  463. (newline)
  464. (show-bug-report-information))
  465. (define %options
  466. ;; Specification of the command-line options.
  467. ;; TODO: add some options:
  468. ;; * --certainty=[low,medium,high]: only run checkers that have at least this
  469. ;; 'certainty'.
  470. (list (option '(#\c "checkers") #t #f
  471. (lambda (opt name arg result)
  472. (let ((names (map string->symbol (string-split arg #\,))))
  473. (for-each (lambda (c)
  474. (unless (memq c
  475. (map lint-checker-name
  476. %checkers))
  477. (leave (_ "~a: invalid checker~%") c)))
  478. names)
  479. (alist-cons 'checkers
  480. (filter (lambda (checker)
  481. (member (lint-checker-name checker)
  482. names))
  483. %checkers)
  484. result))))
  485. (option '(#\h "help") #f #f
  486. (lambda args
  487. (show-help)
  488. (exit 0)))
  489. (option '(#\l "list-checkers") #f #f
  490. (lambda args
  491. (list-checkers-and-exit)))
  492. (option '(#\V "version") #f #f
  493. (lambda args
  494. (show-version-and-exit "guix lint")))))
  495. ;;;
  496. ;;; Entry Point
  497. ;;;
  498. (define (guix-lint . args)
  499. (define (parse-options)
  500. ;; Return the alist of option values.
  501. (args-fold* args %options
  502. (lambda (opt name arg result)
  503. (leave (_ "~A: unrecognized option~%") name))
  504. (lambda (arg result)
  505. (alist-cons 'argument arg result))
  506. %default-options))
  507. (let* ((opts (parse-options))
  508. (args (filter-map (match-lambda
  509. (('argument . value)
  510. value)
  511. (_ #f))
  512. (reverse opts)))
  513. (checkers (or (assoc-ref opts 'checkers) %checkers)))
  514. (if (null? args)
  515. (fold-packages (lambda (p r) (run-checkers p checkers)) '())
  516. (for-each (lambda (spec)
  517. (run-checkers (specification->package spec) checkers))
  518. args))))