Subject: [bug#43064] [PATCH] gexp: computed-file: Prevent mistakenly overriding default option values.

From: Maxim Cournoyer <[email-masked]>
Date: Thu, 27 Aug 2020 01:12:26 -0400

Because the options are passed as a list rather than actual keyword arguments,
omitting to repeat the default values in that list easily causes the default
values to be lost.

* guix/gexp.scm (%computed-file-default-options): New variable.
(alist->plist): New procedure.
(computed-file-combine-options-with-defaults): New procedure.
(computed-file): Use the above procedures to form the default OPTIONS value.
Update doc.  Use the COMPUTED-FILE-COMBINE-OPTIONS-WITH-DEFAULTS procedure to
combine the user options with the default options, when they aren't
overridden.
* tests/gexp.scm ("computed-file options defaults honored")
("computed-file options defaults overridden"): Add tests.
---
 guix/gexp.scm  | 42 +++++++++++++++++++++++++++++++++++++++---
 tests/gexp.scm | 12 ++++++++++++
 2 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/guix/gexp.scm b/guix/gexp.scm
index 67b6121313..14e07e8fe6 100644
--- a/guix/gexp.scm
+++ b/guix/gexp.scm
@@ -3,6 +3,7 @@
 ;;; Copyright © 2018 Clément Lassieur <[email-masked]>
 ;;; Copyright © 2018 Jan Nieuwenhuizen <[email-masked]>
 ;;; Copyright © 2019, 2020 Mathieu Othacehe <[email-masked]>
+;;; Copyright © 2020 Maxim Cournoyer <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -503,14 +504,49 @@ This is the declarative counterpart of 'text-file'."
   (guile      computed-file-guile)                ;<package>
   (options    computed-file-options))             ;list of arguments
 
+;;; Alist containing the default options for computed-file.
+(define %computed-file-default-options '((#:local-build? . #t)))
+
+(define (alist->plist alist)
+  "Transform an association list into a property list."
+  (fold (lambda (current acc)
+          (match current
+            ((x . y)
+             (append acc (list x y)))))
+        '()
+        alist))
+
+(define (computed-file-combine-options-with-defaults options)
+
+  (define alist->keys
+    (match-lambda
+      (((key . value) ...)
+       key)))
+
+  (define (plist->keys plist)
+    (filter keyword? plist))
+
+  (define (default-keys->plist keys)
+    (append-map (lambda (key)
+                  (list key (assq-ref %computed-file-default-options key)))
+                keys))
+
+  (let ((default-keys (lset-difference
+                       eq?
+                       (alist->keys %computed-file-default-options)
+                       (plist->keys options))))
+    (append options (default-keys->plist default-keys))))
+
 (define* (computed-file name gexp
-                        #:key guile (options '(#:local-build? #t)))
+                        #:key guile (options (alist->plist
+                                              %computed-file-default-options)))
   "Return an object representing the store item NAME, a file or directory
 computed by GEXP.  OPTIONS is a list of additional arguments to pass
-to 'gexp->derivation'.
+to 'gexp->derivation', which defaults to %COMPUTED-FILE-DEFAULT-OPTIONS.
 
 This is the declarative counterpart of 'gexp->derivation'."
-  (%computed-file name gexp guile options))
+  (let ((options* (computed-file-combine-options-with-defaults options)))
+    (%computed-file name gexp guile options*)))
 
 (define-gexp-compiler (computed-file-compiler (file <computed-file>)
                                               system target)
diff --git a/tests/gexp.scm b/tests/gexp.scm
index 1beeb67c21..350065b58d 100644
--- a/tests/gexp.scm
+++ b/tests/gexp.scm
@@ -1,5 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <[email-masked]>
+;;; Copyright © 2020 Maxim Cournoyer <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -62,6 +63,9 @@
                                      #:target target)
                   #:guile-for-build (%guile-for-build)))
 
+(define computed-file-combine-options-with-defaults
+  (@@ (guix gexp) computed-file-combine-options-with-defaults))
+
 (define %extension-package
   ;; Example of a package to use when testing 'with-extensions'.
   (dummy-package "extension"
@@ -1367,6 +1371,14 @@
     (return (and (derivation? drv1) (derivation? drv2)
                  (store-path? item)))))
 
+(test-equal "computed-file options defaults honored"
+  '(#:substitutable? #t #:local-build? #t)
+  (computed-file-combine-options-with-defaults '(#:substitutable? #t)))
+
+(test-equal "computed-file options defaults overridden"
+  '(#:local-build? #f)
+  (computed-file-combine-options-with-defaults '(#:local-build? #f)))
+
 (test-assertm "lower-object, computed-file"
   (let* ((text     (plain-file "foo" "Hello!"))
          (exp      #~(begin
-- 
2.27.0