guix-play/guix/scripts/style.scm

649 lines
25 KiB
Scheme
Raw Normal View History

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2021-2023 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;;
;;; This script updates package definitions so they use the "simplified" style
;;; for input lists, as in:
;;;
;;; (package
;;; ;; ...
;;; (inputs (list foo bar baz)))
;;;
;;; Code:
(define-module (guix scripts style)
#:autoload (gnu packages) (specification->package fold-packages)
#:use-module (guix scripts)
#:use-module ((guix scripts build) #:select (%standard-build-options))
#:use-module (guix ui)
#:use-module (guix packages)
#:use-module (guix utils)
#:use-module (guix i18n)
#:use-module (guix diagnostics)
#:use-module (guix read-print)
#:use-module (ice-9 control)
#:use-module (ice-9 match)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-9)
#:use-module (srfi srfi-26)
#:use-module (srfi srfi-34)
#:use-module (srfi srfi-37)
#:export (guix-style))
;;;
;;; Simplifying input expressions.
;;;
(define (label-matches? label name)
"Return true if LABEL matches NAME, a package name."
(or (string=? label name)
(and (string-prefix? "python-" label)
(string-prefix? "python2-" name)
(string=? (string-drop label (string-length "python-"))
(string-drop name (string-length "python2-"))))))
(define* (simplify-inputs location package str inputs
#:key (label-matches? label-matches?))
"Simplify the inputs field of PACKAGE (a string) at LOCATION; its current
value is INPUTS the corresponding source code is STR. Return a string to
replace STR."
(define (simplify-input-expression return)
(match-lambda
((label ('unquote symbol)) symbol)
((label ('unquote symbol) output)
(list 'quasiquote
(list (list 'unquote symbol) output)))
(_
;; Expression doesn't look like a simple input.
(warning location (G_ "~a: complex expression, \
bailing out~%")
package)
(return str))))
(define (simplify-input exp input return)
(define package* package)
(match input
((or ((? string? label) (? package? package))
((? string? label) (? package? package)
(? string?)))
;; If LABEL doesn't match PACKAGE's name, then simplifying would incur
;; a rebuild, and perhaps it would break build-side code relying on
;; this specific label.
(if (label-matches? label (package-name package))
((simplify-input-expression return) exp)
(begin
(warning location (G_ "~a: input label \
'~a' does not match package name, bailing out~%")
package* label)
(return str))))
(_
(warning location (G_ "~a: non-trivial input, \
bailing out~%")
package*)
(return str))))
(define (simplify-expressions exp inputs return)
;; Simplify the expressions in EXP, which correspond to INPUTS, and return
;; a list of expressions. Call RETURN with a string when bailing out.
(let loop ((result '())
(exp exp)
(inputs inputs))
(match exp
(((? blank? head) . rest)
(loop (cons head result) rest inputs))
((head . rest)
(match inputs
((input . inputs)
;; HEAD (an sexp) and INPUT (an input tuple) are correlated.
(loop (cons (simplify-input head input return) result)
rest inputs))
(()
;; If EXP and INPUTS have a different length, that
;; means EXP is a non-trivial input list, for example
;; with input-splicing, conditionals, etc.
(warning location (G_ "~a: input expression is too short~%")
package)
(return str))))
(()
;; It's possible for EXP to contain fewer elements than INPUTS, for
;; example in the case of input splicing. No bailout here. (XXX)
(reverse result)))))
(define inputs-exp
(call-with-input-string str read-with-comments))
(match inputs-exp
(('list _ ...) ;already done
str)
(('modify-inputs _ ...) ;already done
str)
(('quasiquote ;prepending inputs
(exp ...
('unquote-splicing
((and symbol (or 'package-inputs 'package-native-inputs
'package-propagated-inputs))
arg))))
(let/ec return
(object->string*
(let ((things (simplify-expressions exp inputs return)))
`(modify-inputs (,symbol ,arg)
(prepend ,@things)))
(location-column location))))
(('quasiquote ;replacing an input
((and exp ((? string? to-delete) ('unquote replacement)))
('unquote-splicing
('alist-delete (? string? to-delete)
((and symbol
(or 'package-inputs 'package-native-inputs
'package-propagated-inputs))
arg)))))
(let/ec return
(object->string*
(let ((things (simplify-expressions (list exp)
(list (car inputs))
return)))
`(modify-inputs (,symbol ,arg)
(replace ,to-delete ,replacement)))
(location-column location))))
(('quasiquote ;removing an input
(exp ...
('unquote-splicing
('alist-delete (? string? to-delete)
((and symbol
(or 'package-inputs 'package-native-inputs
'package-propagated-inputs))
arg)))))
(let/ec return
(object->string*
(let ((things (simplify-expressions exp inputs return)))
`(modify-inputs (,symbol ,arg)
(delete ,to-delete)
(prepend ,@things)))
(location-column location))))
(('fold 'alist-delete ;removing several inputs
((and symbol
(or 'package-inputs 'package-native-inputs
'package-propagated-inputs))
arg)
('quote ((? string? to-delete) ...)))
(object->string*
`(modify-inputs (,symbol ,arg)
(delete ,@to-delete))
(location-column location)))
(('quasiquote ;removing several inputs and adding others
(exp ...
('unquote-splicing
('fold 'alist-delete
((and symbol
(or 'package-inputs 'package-native-inputs
'package-propagated-inputs))
arg)
('quote ((? string? to-delete) ...))))))
(let/ec return
(object->string*
(let ((things (simplify-expressions exp inputs return)))
`(modify-inputs (,symbol ,arg)
(delete ,@to-delete)
(prepend ,@things)))
(location-column location))))
(('quasiquote (exp ...))
(let/ec return
(object->string*
`(list ,@(simplify-expressions exp inputs return))
(location-column location))))
(_
(warning location (G_ "~a: unsupported input style, \
bailing out~%")
package)
str)))
(define (edit-expression/dry-run properties rewrite-string)
"Like 'edit-expression' but display what would be edited without actually
doing it."
(edit-expression properties
(lambda (str)
(unless (string=? (rewrite-string str) str)
(info (source-properties->location properties)
(G_ "would be edited~%")))
str)))
(define (trivial-package-arguments? package)
"Return true if PACKAGE has zero arguments or only \"trivial\" arguments
guaranteed not to refer to input labels."
(let loop ((arguments (package-arguments package)))
(match arguments
(()
#t)
(((? keyword?) value rest ...)
(and (or (boolean? value) (number? value) (string? value))
(loop rest))))))
(define* (simplify-package-inputs package
#:key (policy 'silent)
(edit-expression edit-expression))
"Edit the source code of PACKAGE to simplify its inputs field if needed.
POLICY is a symbol that defines whether to simplify inputs; it can one of
'silent (change only if the resulting derivation is the same), 'safe (change
only if semantics are known to be unaffected), and 'always (fearlessly
simplify inputs!). Call EDIT-EXPRESSION to actually edit the source of
PACKAGE."
(for-each (lambda (field-name field)
(match (field package)
(()
#f)
(inputs
(match (package-field-location package field-name)
(#f
;; If the location of FIELD-NAME is not found, it may be
;; that PACKAGE inherits from another package.
#f)
(location
(edit-expression
(location->source-properties (absolute-location location))
(lambda (str)
(define matches?
(match policy
('silent
;; Simplify inputs only when the label matches
;; perfectly, such that the resulting derivation
;; is unchanged.
label-matches?)
('safe
;; If PACKAGE has no arguments, labels are known
;; to have no effect: this is a "safe" change, but
;; it may change the derivation.
(if (trivial-package-arguments? package)
(const #t)
label-matches?))
('always
;; Assume it's gonna be alright.
(const #t))))
(simplify-inputs location
(package-name package)
str inputs
#:label-matches? matches?))))))))
'(inputs native-inputs propagated-inputs)
(list package-inputs package-native-inputs
package-propagated-inputs)))
;;;
;;; Gexpifying package arguments.
;;;
(define (unquote->ungexp value)
"Replace 'unquote' and 'unquote-splicing' in VALUE with their gexp
counterpart."
;; Replace 'unquote only on the first quasiquotation level.
(let loop ((value value)
(quotation 1))
(match value
(('unquote x)
(if (= quotation 1)
`(ungexp ,x)
value))
(('unquote-splicing x)
(if (= quotation 1)
`(ungexp-splicing x)
value))
(('quasiquote x)
(list 'quasiquote (loop x (+ quotation 1))))
(('quote x)
(list 'quote (loop x (+ quotation 1))))
((lst ...)
(map (cut loop <> quotation) lst))
(x x))))
(define (gexpify-argument-value value quotation)
"Turn VALUE, an sexp, into its gexp equivalent. QUOTATION is a symbol that
indicates in what quotation context VALUE is to be interpreted: 'quasiquote,
'quote, or 'none."
(match quotation
('none
(match value
(('quasiquote value)
(gexpify-argument-value value 'quasiquote))
(('quote value)
(gexpify-argument-value value 'quote))
(value value)))
('quote
`(gexp ,value))
('quasiquote
`(gexp ,(unquote->ungexp value)))))
(define (quote-argument-value value quotation)
"Quote VALUE, an sexp. QUOTATION is a symbol that indicates in what
quotation context VALUE is to be interpreted: 'quasiquote, 'quote, or 'none."
(define (self-quoting? x)
(or (boolean? x) (number? x) (string? x) (char? x)
(keyword? x)))
(match quotation
('none
(match value
(('quasiquote value)
(quote-argument-value value 'quasiquote))
(('quote value)
(quote-argument-value value 'quote))
(value value)))
('quote
(if (self-quoting? value)
value
(list 'quote value)))
('quasiquote
(match value
(('unquote x) x)
((? self-quoting? x) x)
(_ (list 'quasiquote value))))))
(define %gexp-keywords
;; Package argument keywords that must be followed by a gexp.
'(#:phases #:configure-flags #:make-flags #:strip-flags))
(define (gexpify-argument-tail sexp)
"Gexpify SEXP, an unquoted argument tail."
(match sexp
(('substitute-keyword-arguments lst clauses ...)
`(substitute-keyword-arguments ,lst
,@(map (match-lambda
((((? keyword? keyword) identifier) body)
`((,keyword ,identifier)
,(if (memq keyword %gexp-keywords)
(gexpify-argument-value body 'none)
(quote-argument-value body 'none))))
((((? keyword? keyword) identifier default) body)
`((,keyword ,identifier
,(if (memq keyword %gexp-keywords)
(gexpify-argument-value default 'none)
(quote-argument-value default 'none)))
,(if (memq keyword %gexp-keywords)
(gexpify-argument-value body 'none)
(quote-argument-value body 'none))))
(clause clause))
clauses)))
(_ sexp)))
(define* (gexpify-package-arguments package
#:key
(policy 'none)
(edit-expression edit-expression))
"Rewrite the 'arguments' field of PACKAGE to use gexps where applicable."
(define (gexpify location str)
(match (call-with-input-string str read-with-comments)
((rest ...)
(let ((blanks (take-while blank? rest))
(value (drop-while blank? rest)))
(define-values (quotation arguments tail)
(match value
(('quote (arguments ...)) (values 'quote arguments '()))
(('quasiquote (arguments ... ('unquote-splicing tail)))
(values 'quasiquote arguments tail))
(('quasiquote (arguments ...)) (values 'quasiquote arguments '()))
(('list arguments ...) (values 'none arguments '()))
(arguments (values 'none '() arguments))))
(define (append-tail sexp)
(if (null? tail)
sexp
(let ((tail (gexpify-argument-tail tail)))
(if (null? arguments)
tail
`(append ,sexp ,tail)))))
(let/ec return
(object->string*
(append-tail
`(list ,@(let loop ((arguments arguments)
(result '()))
(match arguments
(() (reverse result))
(((? keyword? keyword) value rest ...)
(when (eq? quotation 'none)
(match value
(('gexp _) ;already gexpified
(return str))
(_ #f)))
(loop rest
(cons* (if (memq keyword %gexp-keywords)
(gexpify-argument-value value
quotation)
(quote-argument-value value quotation))
keyword result)))
(((? blank? blank) rest ...)
(loop rest (cons blank result)))
(_
;; Something like: ,@(package-arguments xyz).
(warning location
(G_ "unsupported argument style; \
bailing out~%"))
(return str))))))
(location-column location)))))
(_
(warning location
(G_ "unsupported argument field; bailing out~%"))
str)))
(unless (null? (package-arguments package))
(match (package-field-location package 'arguments)
(#f
#f)
(location
(edit-expression
(location->source-properties (absolute-location location))
(lambda (str)
(gexpify location str)))))))
;;;
;;; Formatting package definitions.
;;;
(define* (format-package-definition package
#:key policy
(edit-expression edit-expression))
"Reformat the definition of PACKAGE."
(unless (package-definition-location package)
(leave (package-location package)
(G_ "no definition location for package ~a~%")
(package-full-name package)))
(edit-expression
(location->source-properties
(absolute-location (package-definition-location package)))
(lambda (str)
(let ((exp (call-with-input-string str
read-with-comments)))
(object->string* exp
(location-column
(package-definition-location package))
#:format-comment canonicalize-comment
#:format-vertical-space canonicalize-vertical-space)))))
(define (package-location<? p1 p2)
"Return true if P1's location is \"before\" P2's."
(let ((loc1 (package-location p1))
(loc2 (package-location p2)))
(and loc1 loc2
(if (string=? (location-file loc1) (location-file loc2))
(< (location-line loc1) (location-line loc2))
(string<? (location-file loc1) (location-file loc2))))))
;;;
;;; Whole-file formatting.
;;;
(define* (format-whole-file file #:rest rest)
"Reformat all of FILE."
(with-fluids ((%default-port-encoding "UTF-8"))
(let ((lst (call-with-input-file file read-with-comments/sequence
#:guess-encoding #t)))
(with-atomic-file-output file
(lambda (port)
(apply pretty-print-with-comments/splice port lst
#:format-comment canonicalize-comment
#:format-vertical-space canonicalize-vertical-space
rest))))))
;;;
;;; Options.
;;;
(define %options
;; Specification of the command-line options.
(list (find (lambda (option)
(member "load-path" (option-names option)))
%standard-build-options)
(option '(#\n "dry-run") #f #f
(lambda (opt name arg result)
(alist-cons 'dry-run? #t result)))
(option '(#\e "expression") #t #f
(lambda (opt name arg result)
(alist-cons 'expression arg result)))
(option '(#\f "whole-file") #f #f
(lambda (opt name arg result)
(alist-cons 'whole-file? #t result)))
(option '(#\S "styling") #t #f
(lambda (opt name arg result)
(alist-cons 'styling-procedure
(match arg
("inputs" simplify-package-inputs)
("arguments" gexpify-package-arguments)
("format" format-package-definition)
(_ (leave (G_ "~a: unknown styling~%")
arg)))
result)))
(option '("input-simplification") #t #f
(lambda (opt name arg result)
(let ((symbol (string->symbol arg)))
(unless (memq symbol '(silent safe always))
(leave (G_ "~a: invalid input simplification policy~%")
arg))
(alist-cons 'input-simplification-policy symbol
result))))
(option '(#\h "help") #f #f
(lambda args
(leave-on-EPIPE (show-help))
(exit 0)))
(option '(#\l "list-stylings") #f #f
(lambda args
(show-stylings)
(exit 0)))
(option '(#\V "version") #f #f
(lambda args
(show-version-and-exit "guix style")))))
(define (show-stylings)
(display (G_ "Available styling rules:\n"))
(display (G_ "- format: Format the given package definition(s)\n"))
(display (G_ "- inputs: Rewrite package inputs to the “new style”\n"))
(display (G_ "- arguments: Rewrite package arguments to G-expressions\n")))
(define (show-help)
(display (G_ "Usage: guix style [OPTION]... [PACKAGE]...
Update package definitions to the latest style.\n"))
(display (G_ "
-S, --styling=RULE apply RULE, a styling rule"))
(display (G_ "
-l, --list-stylings display the list of available style rules"))
(newline)
(display (G_ "
-n, --dry-run display files that would be edited but do nothing"))
(display (G_ "
-L, --load-path=DIR prepend DIR to the package module search path"))
(display (G_ "
-e, --expression=EXPR consider the package EXPR evaluates to"))
(display (G_ "
--input-simplification=POLICY
follow POLICY for package input simplification, one
of 'silent', 'safe', or 'always'"))
(newline)
(display (G_ "
-f, --whole-file format the entire contents of the given file(s)"))
(newline)
(display (G_ "
-h, --help display this help and exit"))
(display (G_ "
-V, --version display version information and exit"))
(newline)
(show-bug-report-information))
(define %default-options
;; Alist of default option values.
`((input-simplification-policy . silent)
(styling-procedure . ,format-package-definition)))
;;;
;;; Entry point.
;;;
(define-command (guix-style . args)
(category packaging)
(synopsis "update the style of package definitions")
(define (parse-options)
;; Return the alist of option values.
(parse-command-line args %options (list %default-options)
#:build-options? #f))
(let* ((opts (parse-options))
(edit (if (assoc-ref opts 'dry-run?)
edit-expression/dry-run
edit-expression))
(style (assoc-ref opts 'styling-procedure))
(policy (assoc-ref opts 'input-simplification-policy)))
(with-error-handling
(if (assoc-ref opts 'whole-file?)
(let ((files (filter-map (match-lambda
(('argument . file) file)
(_ #f))
opts)))
(unless (eq? format-package-definition style)
(warning (G_ "'--styling' option has no effect in whole-file mode~%")))
(when (null? files)
(warning (G_ "no files specified, nothing to do~%")))
(for-each format-whole-file files))
(let ((packages (filter-map (match-lambda
(('argument . spec)
(specification->package spec))
(('expression . str)
(read/eval str))
(_ #f))
opts)))
(for-each (lambda (package)
(style package #:policy policy
#:edit-expression edit))
;; Sort package by source code location so that we start
;; editing files from the bottom and going upward. That
;; way, the 'location' field of <package> records is not
;; invalidated as we modify files.
(sort (if (null? packages)
(fold-packages cons '() #:select? (const #t))
packages)
(negate package-location<?))))))))