purge-python2-packages.scm 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. #!/usr/bin/env -S guile --no-auto-compile -e main
  2. !#
  3. (use-modules (gnu packages)
  4. (gnu packages python)
  5. (guix diagnostics)
  6. (guix graph)
  7. (guix monads)
  8. (guix scripts graph)
  9. (guix scripts style)
  10. (guix store)
  11. (guix packages)
  12. (guix build utils)
  13. (guix utils)
  14. (ice-9 match)
  15. (ice-9 popen)
  16. (ice-9 pretty-print)
  17. (ice-9 textual-ports)
  18. (srfi srfi-1)
  19. (srfi srfi-2)
  20. (srfi srfi-26))
  21. ;;; Commentary:
  22. ;;;
  23. ;;; Find all the leaf (0 dependents) Python 2 packages, delete them,
  24. ;;; and de-register any "python2-variant" property that referred to
  25. ;;; them from a Python 3 same-named package.
  26. (define (all-packages)
  27. (fold-packages (lambda (package result)
  28. (cons package result))
  29. '()))
  30. (define (list-dependents package)
  31. "List the packages dependents on PACKAGE."
  32. (with-store store
  33. (run-with-store store
  34. (mlet* %store-monad ((edges (node-back-edges
  35. %bag-node-type
  36. (package-closure (all-packages)))))
  37. (return (node-transitive-edges (list package) edges))))))
  38. (define (locate-package-via-git name)
  39. "Return the location object corresponding to package NAME, searched via git."
  40. (let* ((input-pipe (open-pipe* OPEN_READ
  41. "git" "grep" "-n" "--column"
  42. (format #f "^(define-public ~a$" name)))
  43. (output (get-string-all input-pipe)) ;file:line:column:match
  44. (exit-val (status:exit-val (close-pipe input-pipe))))
  45. (case exit-val
  46. ((0)
  47. (let ((components (string-split output #\:)))
  48. (location
  49. (first components) ;file
  50. (string->number (second components)) ;1-indexed line
  51. ;; FIXME: Comment discrepancy in (guix diagnostics), which
  52. ;; says the column is 0-indexed.
  53. (and=> (string->number (third components)) 1-)))) ;0-indexed column
  54. ((1) #f) ;no match
  55. (else (error "git grep failed with status" exit-val)))))
  56. (define (delete-package-field field sexp)
  57. "Delete FIELD (a symbol) from a package SEXP."
  58. (match sexp
  59. ;; TODO: Can we come up with a general pattern here?
  60. (('define-public name ('package (fields ...) ...))
  61. `(define-public ,name
  62. (package
  63. ,@(alist-delete field fields))))
  64. (('define-public name ('package/inherit parent (fields ...) ...))
  65. `(define-public ,name
  66. (package/inherit ,parent
  67. ,@(alist-delete field fields))))
  68. (('define-public name ('hidden-package ('package (fields ...) ...)))
  69. `(define-public ,name
  70. (hidden-package
  71. (package ,@(alist-delete field fields)))))
  72. (('define-public name ('hidden-package
  73. ('package/inherit parent (fields ...) ...)))
  74. `(define-public ,name
  75. (hidden-package
  76. (package/inherit ,parent
  77. ,@(alist-delete field fields)))))))
  78. ;;; Potential improvement suggested by daviid (to try).
  79. ;; (define (delete-package-field field sexp)
  80. ;; "Delete FIELD (a symbol) from a package SEXP."
  81. ;; (match sexp
  82. ;; ;; TODO: Can we come up with a general pattern here?
  83. ;; (('define-public name package-def ...)
  84. ;; `(define-public ,name
  85. ;; ,(update-package-def package-def field)))))
  86. ;; (define (update-package-def package-def field)
  87. ;; (match package-def
  88. ;; (('package (fields ...) ...)
  89. ;; `(package
  90. ;; ,@(alist-delete field fields)))
  91. ;; (('package/inherit parent (fields ...) ...)
  92. ;; `(package/inherit ,parent
  93. ;; ,@(alist-delete field fields)))
  94. ;; (('hidden-package ('package (fields ...) ...))
  95. ;; `(hidden-package
  96. ;; (package ,@(alist-delete field fields))))
  97. ;; (('hidden-package ('package/inherit parent (fields ...) ...))
  98. ;; `(hidden-package (package/inherit ,parent
  99. ;; ,@(alist-delete field fields))))))
  100. (define (delete-package-property-helper property fields)
  101. "Return a patched copy of a package fields."
  102. (let* ((properties (assoc-ref fields 'properties))
  103. (new-properties (sexp-filter (remove-node property) properties)))
  104. (alist-replace 'properties new-properties fields)))
  105. (define (delete-package-property property sexp)
  106. "Delete a specific PROPERTY (a symbol) from a package SEXP."
  107. (match sexp
  108. (('define-public name ('package (fields ...) ...))
  109. `(define-public ,name
  110. (package ,@(delete-package-property-helper property fields))))
  111. (('define-public name ('package/inherit parent (fields ...) ...))
  112. `(define-public ,name
  113. (package/inherit ,parent
  114. ,@(delete-package-property-helper property fields))))
  115. (('define-public name ('hidden-package ('package (fields ...) ...)))
  116. `(define-public ,name
  117. (hidden-package
  118. (package ,@(delete-package-property-helper property fields)))))
  119. (('define-public name ('hidden-package
  120. ('package/inherit parent (fields ...) ...)))
  121. `(define-public ,name
  122. (hidden-package
  123. (package/inherit ,parent
  124. ,@(delete-package-property-helper property fields)))))))
  125. (define (delete-leaf-python-package package)
  126. "Delete the package, and de-register any variant property."
  127. (with-directory-excursion (%guix-source-root-directory)
  128. (let* ((name (package-name package))
  129. (py3name (string-replace-substring name "python2" "python"))
  130. ;; XXX: Due to https://issues.guix.gnu.org/55139, and also
  131. ;; the fact that we are editing the sources at run time
  132. ;; without re-evaluating the modules, we rely on git to
  133. ;; locate the package definition.
  134. (source-properties (and=> (locate-package-via-git name)
  135. location->source-properties)))
  136. (format #t "Deleting package ~s...~%" name)
  137. ;; Note: the line is decremented by one so the blank line
  138. ;; separating the package definitions is also removed.
  139. (delete-expression (alist-replace
  140. 'line (1- (assoc-ref source-properties 'line))
  141. source-properties))
  142. ;; Check if a same-named Python 3 package had a python2-variant
  143. ;; entry for it.
  144. (and-let* ((py3package (match (find-packages-by-name py3name)
  145. (() #f)
  146. ((p p* ...)
  147. p)))
  148. (properties (package-properties py3package))
  149. (python2-variant (and=> (assoc-ref properties 'python2-variant)
  150. force))
  151. (source-properties (and=> (locate-package-via-git py3name)
  152. location->source-properties)))
  153. (when (and (eq? python2-variant package))
  154. (format #t "Removing 'python2-variant' property from package ~a...~%"
  155. py3name)
  156. (edit-expression
  157. source-properties
  158. (lambda (expr)
  159. (let ((sexp (call-with-input-string expr read)))
  160. (string-trim ;remove added \n
  161. (call-with-output-string
  162. (lambda (port)
  163. (pretty-print-with-comments
  164. port
  165. (if (= 1 (length properties))
  166. (delete-package-field 'properties sexp)
  167. (delete-package-property 'python2-variant sexp)))))))))
  168. #t)))))
  169. (define (main _)
  170. (define skipped-packages
  171. (fold-packages
  172. (lambda (package lst)
  173. (let ((python-arg (and=> (member #:python
  174. (package-arguments package))
  175. second)))
  176. (if (eq? python-2 python-arg)
  177. (let* ((dependents (list-dependents package))
  178. (name (package-name package))
  179. (dependent-count (length dependents)))
  180. (case dependent-count
  181. ((0)
  182. (delete-leaf-python-package package)
  183. (format #t "Committing '~a' package removal...~%" name)
  184. (with-directory-excursion (%guix-source-root-directory)
  185. (invoke "./etc/committer.scm"))
  186. lst)
  187. (else
  188. (format #t "Skipping package '~a' with ~a dependents...~%"
  189. name dependent-count)
  190. (cons (list name dependent-count) lst))))
  191. lst)))
  192. '()))
  193. (newline)
  194. (format #t "Python 2 purge complete.~%")
  195. (format #t "~a packages were kept due to having dependents:~%"
  196. (length skipped-packages))
  197. ;; Sort the skipped packages by descending number of dependencies.
  198. (define skipped-packages/sorted (sort skipped-packages
  199. (lambda (x y)
  200. (> (second x) (second y)))))
  201. (pretty-print-table (cons (list "Packages" "Dependencies")
  202. skipped-packages/sorted)))
  203. ;;;
  204. ;;; Sexp-munging procedures.
  205. ;;;
  206. ;;; sexp-filter and removed-node co-authored by daviid; thank you!
  207. (define (sexp-filter proc sexp)
  208. "Filter SEXP recursively using PROC."
  209. (match sexp
  210. (() sexp)
  211. (((a rest))
  212. (cons (sexp-filter proc (car sexp))
  213. (sexp-filter proc (cdr sexp))))
  214. ((a rest)
  215. (cons a (list (proc rest))))))
  216. (define (remove-node key)
  217. (lambda (props)
  218. (filter-map (lambda (prop)
  219. (match prop
  220. ((k . v)
  221. (and (not (eq? k key)) prop))))
  222. props)))