finder.scm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. (define-module (fiasco finder)
  2. #:use-module (ice-9 control)
  3. #:use-module (ice-9 match)
  4. #:use-module (ice-9 popen)
  5. #:use-module (ice-9 regex)
  6. #:use-module (ice-9 textual-ports)
  7. #:use-module (gnu packages)
  8. #:use-module (guix base32)
  9. #:use-module (guix build utils)
  10. #:use-module (guix download)
  11. #:use-module ((guix build download)
  12. #:select (url-fetch)
  13. #:prefix build:)
  14. #:use-module (guix download)
  15. #:use-module (guix packages)
  16. #:use-module (guix scripts download)
  17. #:use-module (guix scripts hash)
  18. #:use-module (guix store)
  19. #:use-module (guix ui)
  20. #:use-module (srfi srfi-1)
  21. #:use-module (srfi srfi-9)
  22. #:use-module (srfi srfi-19)
  23. #:export (result
  24. result?
  25. result-package-name
  26. result-package-version
  27. result-guix-hash
  28. result-upstream-hash
  29. result-hash-ok?
  30. result-safe-to-update?
  31. result-date
  32. result->package
  33. results-dir
  34. results-file
  35. results-file->results
  36. results->results-file
  37. purge-deprecated-results!
  38. find-problematic-packages))
  39. ;;; Commentary: Finds GitHub packages whose hash got broken.
  40. ;;; Requirements: tar and diff command line tools.
  41. ;; Workaround Geiser bug #83 (see:
  42. ;; https://github.com/jaor/geiser/issues/83)
  43. (guix-warning-port (current-warning-port))
  44. ;;;
  45. ;;; Parameters to configure.
  46. ;;;
  47. (define substitute-urls
  48. (make-parameter (cons* "https://berlin.guixsd.org"
  49. "https://bayfront.guixsd.org"
  50. %default-substitute-urls)))
  51. (define results-dir
  52. (make-parameter (string-append (getenv "HOME") "/src/guile-hacks/fiasco")))
  53. (define results-file
  54. (make-parameter (string-append (results-dir) "/results.txt")))
  55. (define tar-diff-dir
  56. (make-parameter (string-append (results-dir) "/tar-diffs")))
  57. ;;;
  58. ;;; Data structures and supporting functions.
  59. ;;;
  60. (define-record-type <result>
  61. (make-result package-name package-version guix-hash
  62. upstream-hash hash-ok? safe-to-update? date)
  63. result?
  64. (package-name result-package-name)
  65. (package-version result-package-version)
  66. (guix-hash result-guix-hash)
  67. (upstream-hash result-upstream-hash)
  68. (hash-ok? result-hash-ok?)
  69. (safe-to-update? result-safe-to-update?)
  70. (date result-date))
  71. (define (result->sexp result)
  72. (list (result-package-name result)
  73. (result-package-version result)
  74. (result-guix-hash result)
  75. (result-upstream-hash result)
  76. (result-hash-ok? result)
  77. (result-safe-to-update? result)
  78. (result-date result)))
  79. (define (sexp->result sexp)
  80. (match sexp
  81. ((package-name package-version guix-hash
  82. upstream-hash safe-to-update? result-hash-ok? date)
  83. (make-result package-name package-version guix-hash
  84. upstream-hash safe-to-update? result-hash-ok? date))))
  85. (define (results-file->results file)
  86. "Read the results from FILE and return the list of result records."
  87. (with-input-from-file file
  88. (lambda ()
  89. (let loop ((line (read (current-input-port))))
  90. (if (eof-object? line)
  91. '()
  92. (cons (sexp->result line)
  93. (loop (read (current-input-port)))))))))
  94. (define (result-package-exist? result)
  95. "Return the package referred to by RESULT or #f if it doesn't exist."
  96. (let* ((name (result-package-name result))
  97. (version (result-package-version result))
  98. (packages (find-best-packages-by-name name version)))
  99. (not (null? packages))))
  100. (define (result->package result)
  101. "Return the package referred to by RESULT or null if it doesn't exist."
  102. (let* ((name (result-package-name result))
  103. (version (result-package-version result))
  104. (packages (find-best-packages-by-name name version)))
  105. (if (null? packages)
  106. (begin
  107. (warn (format #f "The package ~a, version ~a is no longer in Guix"
  108. name version))
  109. '())
  110. (first packages))))
  111. (define (results->results-file results file)
  112. "Overwrite the FILE content with the RESULTS."
  113. (with-output-to-file file
  114. (lambda ()
  115. (for-each (lambda (result)
  116. (write (result->sexp result) (current-output-port))
  117. (display "\n" (current-output-port)))
  118. results))))
  119. (define (result<? result1 result2)
  120. "Predicate to sort results alphabetically by name and versions."
  121. (let ((name1 (result-package-name result1))
  122. (name2 (result-package-name result2))
  123. (version1 (result-package-version result1))
  124. (version2 (result-package-version result2)))
  125. (or (string<? name1 name2)
  126. (and (string=? name1 name2)
  127. (string<? version1 version2)))))
  128. (define (purge-deprecated-results! file)
  129. "Overwrite FILE after purging the results of Guix packages no longer
  130. available."
  131. (let* ((all-results (results-file->results file))
  132. (valid-results
  133. (sort (filter result-package-exist? all-results) result<?)))
  134. (results->results-file valid-results file)))
  135. ;;;
  136. ;;; Functions and procedures.
  137. ;;;
  138. (define (package<? package1 package2)
  139. "Predicate to sort packages alphabetically by name and versions."
  140. (or (string<? (package-name package1) (package-name package2))
  141. (and (string=? (package-name package1) (package-name package2))
  142. (string<? (package-version package1) (package-version package2)))))
  143. (define (problematic-uri? uri)
  144. (define (contains-github-archive? uri)
  145. (regexp-match? (string-match "github.com/.*/archive/" uri)))
  146. ;; URI can be a string or a list of string.
  147. (match uri
  148. ((uri1 uri2 ...) ;match list of strings
  149. (not (null? (filter contains-github-archive? uri))))
  150. (uri1 ;match string
  151. (contains-github-archive? uri1))))
  152. (define (problematic-github-package? package)
  153. (let ((source (package-source package)))
  154. (and (origin? source)
  155. (eq? (origin-method source) url-fetch)
  156. (problematic-uri? (origin-uri source)))))
  157. (define (problematic-github-packages)
  158. "Return the list of all the potentially problematic GitHub packages in Guix."
  159. (sort (fold-packages (lambda (p r)
  160. (if (problematic-github-package? p)
  161. (cons p r)
  162. r))
  163. '())
  164. package<?))
  165. (define* (already-checked-packages #:optional (file (results-file)))
  166. "List of already checked packages."
  167. (if (file-exists? file)
  168. (filter package? (map result->package
  169. (results-file->results file)))
  170. '()))
  171. (define (origin->nix-base32-bash origin)
  172. (bytevector->nix-base32-string (origin-sha256 origin)))
  173. (define (origin->download-uri-suffix origin)
  174. "Form the suffix part of the URI of a downloadable substitute file."
  175. (let ((file-name (origin-actual-file-name origin))
  176. (hash (origin->nix-base32-bash origin)))
  177. (string-append "/file/" file-name "/sha256/" hash)))
  178. (define* (download-substitute package file)
  179. "Download the substitute of PACKAGE and return it as FILE, or #f if
  180. the substitute could not be downloaded."
  181. (let* ((origin (package-source package))
  182. (download-uri-suffix (origin->download-uri-suffix origin)))
  183. (let/ec return
  184. (for-each (lambda (url)
  185. ;; Do not verify certificate to work around bug#28810.
  186. (let* ((uri (string-append url download-uri-suffix))
  187. (file (build:url-fetch uri file
  188. #:verify-certificate? #f)))
  189. (when file
  190. (return file)))) ;abort loop
  191. (substitute-urls))
  192. (warn "Failed to download a substitute for package: "
  193. (package-name package))
  194. #f)))
  195. (define (file-hash file)
  196. "Return the nix-base32 string corresponding to the sha256 hash of FILE."
  197. (and file
  198. (string-trim-both (with-output-to-string
  199. (lambda ()
  200. (guix-hash file))))))
  201. (define (compare-tar-archives archive1 archive2)
  202. "Return #f if the archives content is the same. Otherwise, a string
  203. detailing the differences is returned."
  204. (let* ((tmpdir (tmpnam))
  205. (subdir1 (string-append tmpdir "/archive1"))
  206. (subdir2 (string-append tmpdir "/archive2"))
  207. (name1 (basename archive1))
  208. (name2 (basename archive2))
  209. (diff-file (string-append (tar-diff-dir) "/"
  210. name1 "-" name2 ".diff")))
  211. (define (untar archive-file dest-dir)
  212. (unless (zero? (system* "tar" "-C" dest-dir "-xf" archive-file))
  213. (error "Failed to extract archive: " archive-file)))
  214. (mkdir-p subdir1)
  215. (mkdir-p subdir2)
  216. (mkdir-p (tar-diff-dir))
  217. (untar archive1 subdir1)
  218. (untar archive2 subdir2)
  219. ;; Use --no-dereference to prevent diff failing on broken
  220. ;; symlinks that archives may contain (e.g. antlr3).
  221. (let* ((input-pipe (open-pipe* OPEN_READ
  222. "diff" "-r" "--no-dereference"
  223. subdir1 subdir2))
  224. (output (get-string-all input-pipe))
  225. (exit-val (status:exit-val (close-pipe input-pipe))))
  226. (case exit-val
  227. ((0) #f)
  228. ((1)
  229. (with-output-to-file diff-file
  230. (lambda ()
  231. (display output)))
  232. (format #t "Diff saved to ~a:~%~a~%" diff-file output))
  233. (else (error "diff failed comparing the folders: " subdir1 subdir2
  234. "exit status: " exit-val))))))
  235. (define (hash-ok? hash1 hash2)
  236. (and (string? hash1)
  237. (string? hash2)
  238. (string=? hash1 hash2)))
  239. (define (check-package-hash package)
  240. "Verify the hash of a package and return a <result> object. Assumes
  241. the definition of PACKAGE contains an origin using the url-fetch
  242. method and a base32 encoded sha256 hash."
  243. (let* ((date (date->string (current-date)))
  244. (name (package-name package))
  245. (version (package-version package))
  246. (origin (package-source package))
  247. (tmpdir (tmpnam))
  248. (tmpdir! (mkdir-p tmpdir))
  249. (file-name (origin-actual-file-name origin))
  250. (upstream-archive (string-append tmpdir "/upstream-" file-name))
  251. (substitute-archive (string-append tmpdir "/substitute-" file-name))
  252. (uri (origin-uri origin))
  253. (guix-hash (origin->nix-base32-bash origin))
  254. (upstream-hash (file-hash (build:url-fetch uri upstream-archive)))
  255. (hash-ok? (hash-ok? upstream-hash guix-hash))
  256. (substitute (and upstream-hash ;stop if false
  257. (not hash-ok?)
  258. (download-substitute package
  259. substitute-archive)))
  260. (safe-to-update?
  261. (if hash-ok?
  262. #f ;false here means 'no need to update'
  263. (and substitute ;stop here if we don't have a substitute
  264. (not (compare-tar-archives upstream-archive
  265. substitute-archive))))))
  266. (make-result name version guix-hash upstream-hash hash-ok?
  267. safe-to-update? date)))
  268. ;;;
  269. ;;; Main program
  270. ;;;
  271. (define (find-problematic-packages)
  272. "Find and print the names of the potentially problematic GitHub packages."
  273. (define (print-packages packages)
  274. (for-each (lambda (name)
  275. (format #t "~a~%" name))
  276. (map package-name packages))
  277. (format #t "~%"))
  278. (define (verify-package-hash package)
  279. (format #t "~%~a verifying package hash...~%" (package-name package))
  280. (let* ((result (check-package-hash package))
  281. (name (result-package-name result))
  282. (guix-hash (result-guix-hash result))
  283. (upstream-hash (result-upstream-hash result))
  284. (hash-ok? (result-hash-ok? result)))
  285. (format #t "~a Guix hash: ~s~%" name guix-hash)
  286. (format #t "~a upstream hash: ~s~%" name upstream-hash)
  287. (if hash-ok?
  288. (format #t "~a hash OK~%" name)
  289. (format #t "~a hash NOK~%" name))
  290. (cond
  291. (hash-ok? #t) ;no-op
  292. ((result-safe-to-update? result)
  293. (format #t "~a hash can be safely updated~%" name))
  294. (else (format #t "~a requires manual verification~%" name)))
  295. ;; Append result to results file.
  296. (let ((results-file (open-file (results-file) "a")))
  297. (dynamic-wind
  298. (lambda () #f)
  299. (lambda ()
  300. (write (result->sexp result) results-file)
  301. (display "\n" results-file))
  302. (lambda () (close results-file))))))
  303. (let* ((problematic-github-packages (problematic-github-packages))
  304. (already-checked-packages (already-checked-packages)))
  305. (format #t "Number of potentially problematic GitHub packages: ~a~%"
  306. (length problematic-github-packages))
  307. ;;(print-packages problematic-github-packages)
  308. (unless (null? already-checked-packages)
  309. (format #t "Skipping ~a already checked packages~%"
  310. (length already-checked-packages)))
  311. (for-each verify-package-hash
  312. (lset-difference eq? problematic-github-packages
  313. already-checked-packages))))