;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2021-2023 Ludovic Courtès ;;; ;;; 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 . ;;; 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))))) ;;; ;;; 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 records is not ;; invalidated as we modify files. (sort (if (null? packages) (fold-packages cons '() #:select? (const #t)) packages) (negate package-location