diff options
| author | Giacomo Leidi <therewasa@fishinthecalculator.me> | 2026-03-08 17:27:13 +0100 |
|---|---|---|
| committer | Liliana Marie Prikler <liliana.prikler@gmail.com> | 2026-04-12 17:23:42 +0200 |
| commit | 0b8e838208a26fb8d7785b24fe26a4f9c9f744ac (patch) | |
| tree | a00916cc8df1e360cf1d082bde415d2a82bedad9 | |
| parent | 2abfd1370fbdd2e6e859c7bd0f491c9f7bda4f0b (diff) | |
services: configuration: Add environment variable serializer.
This patch implements a general API to serialize configuration records
to list of pairs representing environment variables. The car of each
pair represents the variable name and the cdr the variable value.
* gnu/services/configuration/environment-variables.scm: New file.
(serialize-string-environment-variable)
(serialize-maybe-string-environment-variable)
(serialize-boolean-environment-variable)
(serialize-maybe-boolean-environment-variable)
(serialize-number-environment-variable)
(serialize-maybe-number-environment-variable): New variables.
(serialize-environment-variables): New variable.
* gnu/services/configuration/utils.scm: New file.
(uglify-snake-case): New variable.
* tests/services/configuration.scm: Add tests for environment serializer.
(wrong type for a field): Adjust error location.
* doc/guix.texi: Document it.
Change-Id: I81a166576f94d3c8f5bf78c82a02183689a3091c
Signed-off-by: Liliana Marie Prikler <liliana.prikler@gmail.com>
| -rw-r--r-- | doc/guix.texi | 57 | ||||
| -rw-r--r-- | gnu/services/configuration/environment-variables.scm | 143 | ||||
| -rw-r--r-- | gnu/services/configuration/utils.scm | 35 | ||||
| -rw-r--r-- | tests/services/configuration.scm | 129 |
4 files changed, 362 insertions, 2 deletions
diff --git a/doc/guix.texi b/doc/guix.texi index 733c86afac1..198ecb3413b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi | |||
| @@ -51663,6 +51663,63 @@ phone-number = 0 | |||
| 51663 | is-married = true | 51663 | is-married = true |
| 51664 | @end example | 51664 | @end example |
| 51665 | 51665 | ||
| 51666 | @subsubsection Serializing to environment variables | ||
| 51667 | @cindex environment variables, serialization of configuration records | ||
| 51668 | |||
| 51669 | There are services which expect their configuration as environment variables. | ||
| 51670 | The @code{(gnu services configuration environment-variables)} module provides | ||
| 51671 | facilities to serialize configuration records from | ||
| 51672 | @code{(gnu services configuration)} to list of pairs representing environment | ||
| 51673 | variables. | ||
| 51674 | |||
| 51675 | For example this configuration record: | ||
| 51676 | |||
| 51677 | @lisp | ||
| 51678 | (define-configuration/no-serialization server | ||
| 51679 | (ssh-port | ||
| 51680 | (number 22) | ||
| 51681 | "The public SSH port of the server.") | ||
| 51682 | (fqdn | ||
| 51683 | (maybe-string) | ||
| 51684 | "The fully qualified domain name of the server.") | ||
| 51685 | (active? | ||
| 51686 | (boolean #f) | ||
| 51687 | "Whether or not the server should be activated.")) | ||
| 51688 | |||
| 51689 | (define my-server | ||
| 51690 | (server | ||
| 51691 | (ssh-port 20022) | ||
| 51692 | (active? #t))) | ||
| 51693 | @end lisp | ||
| 51694 | |||
| 51695 | with this call: | ||
| 51696 | |||
| 51697 | @lisp | ||
| 51698 | (serialize-environment-variables my-server server-fields | ||
| 51699 | #:true-value "1" | ||
| 51700 | #:false-value "0") | ||
| 51701 | @end lisp | ||
| 51702 | |||
| 51703 | would yield: | ||
| 51704 | |||
| 51705 | @lisp | ||
| 51706 | '(("SSH_PORT" . "20022") | ||
| 51707 | ("ACTIVE" . "1")) | ||
| 51708 | @end lisp | ||
| 51709 | |||
| 51710 | @anchor{serialize-environment-variables-procedure} | ||
| 51711 | @deffn {Procedure} serialize-environment-variables @var{config} @var{fields} @ | ||
| 51712 | [@var{selection} #f] [@var{negate?} #f] [#:prefix #f] @ | ||
| 51713 | [#:true-value "true"] [#:false-value "false"] | ||
| 51714 | Serializes the fields whose name is included in SELECTION from CONFIG, a | ||
| 51715 | configuration from @code{(gnu services configuration)}, and FIELDS, the | ||
| 51716 | list of its field records, to a list of pairs. When NEGATE? is #t all services | ||
| 51717 | not included in SELECTION will be serialized. Each pair represents an | ||
| 51718 | environment variable. The first element of each pair is the variable name, the | ||
| 51719 | second is the value. When PREFIX is a string it is prepended to the variable | ||
| 51720 | name. TRUE-VALUE and FALSE-VALUE will be used as a representation for | ||
| 51721 | respectfully @code{#t} and @code{#f}. | ||
| 51722 | @end deffn | ||
| 51666 | 51723 | ||
| 51667 | @c ********************************************************************* | 51724 | @c ********************************************************************* |
| 51668 | @cindex troubleshooting, Guix System | 51725 | @cindex troubleshooting, Guix System |
diff --git a/gnu/services/configuration/environment-variables.scm b/gnu/services/configuration/environment-variables.scm new file mode 100644 index 00000000000..c22b732defe --- /dev/null +++ b/gnu/services/configuration/environment-variables.scm | |||
| @@ -0,0 +1,143 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | ||
| 2 | ;;; Copyright © 2026 Giacomo Leidi <goodoldpaul@autistici.org> | ||
| 3 | ;;; | ||
| 4 | ;;; This file is part of GNU Guix. | ||
| 5 | ;;; | ||
| 6 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | ;;; under the terms of the GNU General Public License as published by | ||
| 8 | ;;; the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | ;;; your option) any later version. | ||
| 10 | ;;; | ||
| 11 | ;;; GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | ;;; GNU General Public License for more details. | ||
| 15 | ;;; | ||
| 16 | ;;; You should have received a copy of the GNU General Public License | ||
| 17 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | ||
| 18 | |||
| 19 | (define-module (gnu services configuration environment-variables) | ||
| 20 | #:use-module (gnu services configuration) | ||
| 21 | #:use-module (gnu services configuration utils) | ||
| 22 | #:use-module (guix diagnostics) | ||
| 23 | #:use-module (guix i18n) | ||
| 24 | #:use-module (ice-9 match) | ||
| 25 | #:use-module (srfi srfi-1) | ||
| 26 | #:export (serialize-string-environment-variable | ||
| 27 | serialize-boolean-environment-variable | ||
| 28 | serialize-number-environment-variable | ||
| 29 | serialize-maybe-string-environment-variable | ||
| 30 | serialize-maybe-boolean-environment-variable | ||
| 31 | serialize-maybe-number-environment-variable | ||
| 32 | |||
| 33 | serialize-environment-variables)) | ||
| 34 | |||
| 35 | (define* (field-name->environment-variable field-name | ||
| 36 | #:key prefix | ||
| 37 | (uglify uglify-snake-case)) | ||
| 38 | "Serializes FIELD-NAME, a field name from @code{(gnu services configuration)}, | ||
| 39 | to an environment variable name through UGLIFY, by default a procedure that is | ||
| 40 | passed FIELD-NAME, and returns a snake case string representation of | ||
| 41 | the field name. Trailing @code{?} in the name are dropped and @code{-} get | ||
| 42 | replaced by @code{_}. When PREFIX is a string, it is prepended to the result. | ||
| 43 | The result of UGLIFY is then upcased and returned. | ||
| 44 | |||
| 45 | For example the procedure would convert @code{'a-field} to @code{\"A_FIELD\"}." | ||
| 46 | (let ((variable (string-upcase | ||
| 47 | (uglify field-name)))) | ||
| 48 | (if (string? prefix) | ||
| 49 | (string-append prefix variable) | ||
| 50 | variable))) | ||
| 51 | |||
| 52 | (define* (serialize-string-environment-variable field-name value | ||
| 53 | #:key prefix | ||
| 54 | #:allow-other-keys) | ||
| 55 | (cons (field-name->environment-variable field-name #:prefix prefix) | ||
| 56 | value)) | ||
| 57 | |||
| 58 | (define* (serialize-maybe-string-environment-variable field-name value | ||
| 59 | #:key prefix | ||
| 60 | #:allow-other-keys) | ||
| 61 | (if (maybe-value-set? value) | ||
| 62 | (serialize-string-environment-variable field-name value #:prefix prefix) | ||
| 63 | #f)) | ||
| 64 | |||
| 65 | (define* (serialize-boolean-environment-variable field-name value | ||
| 66 | #:key prefix | ||
| 67 | (true-value "true") | ||
| 68 | (false-value "false") | ||
| 69 | #:allow-other-keys) | ||
| 70 | (serialize-string-environment-variable | ||
| 71 | field-name (if value true-value false-value) | ||
| 72 | #:prefix prefix)) | ||
| 73 | |||
| 74 | (define* (serialize-maybe-boolean-environment-variable field-name value | ||
| 75 | #:key prefix | ||
| 76 | #:allow-other-keys) | ||
| 77 | (if (maybe-value-set? value) | ||
| 78 | (serialize-boolean-environment-variable field-name value #:prefix prefix) | ||
| 79 | #f)) | ||
| 80 | |||
| 81 | (define* (serialize-number-environment-variable field-name value | ||
| 82 | #:key prefix | ||
| 83 | #:allow-other-keys) | ||
| 84 | (cons (field-name->environment-variable field-name #:prefix prefix) | ||
| 85 | (number->string value))) | ||
| 86 | |||
| 87 | (define* (serialize-maybe-number-environment-variable field-name value | ||
| 88 | #:key prefix | ||
| 89 | #:allow-other-keys) | ||
| 90 | (if (maybe-value-set? value) | ||
| 91 | (serialize-number-environment-variable field-name value #:prefix prefix) | ||
| 92 | #f)) | ||
| 93 | |||
| 94 | (define (environment-variable-serializer field) | ||
| 95 | (define type (configuration-field-type field)) | ||
| 96 | (match type | ||
| 97 | ('string serialize-string-environment-variable) | ||
| 98 | ('maybe-string serialize-maybe-string-environment-variable) | ||
| 99 | ('number serialize-number-environment-variable) | ||
| 100 | ('integer serialize-number-environment-variable) | ||
| 101 | ('positive serialize-number-environment-variable) | ||
| 102 | ('maybe-number serialize-maybe-number-environment-variable) | ||
| 103 | ('maybe-integer serialize-maybe-number-environment-variable) | ||
| 104 | ('maybe-positive serialize-maybe-number-environment-variable) | ||
| 105 | ('boolean serialize-boolean-environment-variable) | ||
| 106 | ('maybe-boolean serialize-boolean-environment-variable) | ||
| 107 | (_ | ||
| 108 | (raise | ||
| 109 | (formatted-message | ||
| 110 | (G_ "Unknown environment-variable field type: ~a") | ||
| 111 | type))))) | ||
| 112 | |||
| 113 | (define* (serialize-environment-variables config fields | ||
| 114 | #:optional selection negate? | ||
| 115 | #:key prefix | ||
| 116 | (true-value "true") | ||
| 117 | (false-value "false")) | ||
| 118 | "Serializes the fields whose name is included in SELECTION from CONFIG, a | ||
| 119 | configuration from @code{(gnu services configuration)}, and FIELDS, the | ||
| 120 | list of its field records, to a list of pairs. When NEGATE? is #t all services | ||
| 121 | not included in SELECTION will be serialized. Each pair represents an | ||
| 122 | environment variable. The first element of each pair is the variable name, the | ||
| 123 | second is the value. When PREFIX is a string it is prepended to the variable | ||
| 124 | name. TRUE-VALUE and FALSE-VALUE will be used as a representation for | ||
| 125 | respectfully @code{#t} and @code{#f}." | ||
| 126 | (define selected-names | ||
| 127 | (or selection | ||
| 128 | (map configuration-field-name fields))) | ||
| 129 | (define filtered | ||
| 130 | (filter-configuration-fields fields selected-names negate?)) | ||
| 131 | (define getters | ||
| 132 | (map configuration-field-getter filtered)) | ||
| 133 | (define names | ||
| 134 | (map configuration-field-name filtered)) | ||
| 135 | (define serializers | ||
| 136 | (map environment-variable-serializer filtered)) | ||
| 137 | |||
| 138 | (filter-map (match-lambda ((serializer name getter) | ||
| 139 | (serializer name (getter config) | ||
| 140 | #:prefix prefix | ||
| 141 | #:true-value true-value | ||
| 142 | #:false-value false-value))) | ||
| 143 | (zip serializers names getters))) | ||
diff --git a/gnu/services/configuration/utils.scm b/gnu/services/configuration/utils.scm new file mode 100644 index 00000000000..cca2f3df0b6 --- /dev/null +++ b/gnu/services/configuration/utils.scm | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | ||
| 2 | ;;; Copyright © 2026 Giacomo Leidi <goodoldpaul@autistici.org> | ||
| 3 | ;;; | ||
| 4 | ;;; This file is part of GNU Guix. | ||
| 5 | ;;; | ||
| 6 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | ;;; under the terms of the GNU General Public License as published by | ||
| 8 | ;;; the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | ;;; your option) any later version. | ||
| 10 | ;;; | ||
| 11 | ;;; GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | ;;; GNU General Public License for more details. | ||
| 15 | ;;; | ||
| 16 | ;;; You should have received a copy of the GNU General Public License | ||
| 17 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | ||
| 18 | |||
| 19 | (define-module (gnu services configuration utils) | ||
| 20 | #:use-module (ice-9 string-fun) | ||
| 21 | #:export (uglify-snake-case)) | ||
| 22 | |||
| 23 | (define (uglify-snake-case field-name) | ||
| 24 | "Serializes FIELD-NAME, a field name from @code{(gnu services configuration)}, | ||
| 25 | to a downcased, snake case string representation of the field name. Trailing | ||
| 26 | @code{?} in the name are dropped and dashes are replaced with underscores. | ||
| 27 | |||
| 28 | For example the procedure would convert @code{'A-Field?} to @code{\"a_field\"}." | ||
| 29 | (define str (symbol->string field-name)) | ||
| 30 | (string-downcase | ||
| 31 | (string-replace-substring | ||
| 32 | (if (string-suffix? "?" str) | ||
| 33 | (string-drop-right str 1) | ||
| 34 | str) | ||
| 35 | "-" "_"))) | ||
diff --git a/tests/services/configuration.scm b/tests/services/configuration.scm index f9012b159d5..0c2a0810e12 100644 --- a/tests/services/configuration.scm +++ b/tests/services/configuration.scm | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | ;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz> | 3 | ;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz> |
| 4 | ;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org> | 4 | ;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org> |
| 5 | ;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu> | 5 | ;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu> |
| 6 | ;;; Copyright © 2026 Giacomo Leidi <therewasa@fishinthecalculator.me> | ||
| 6 | ;;; | 7 | ;;; |
| 7 | ;;; This file is part of GNU Guix. | 8 | ;;; This file is part of GNU Guix. |
| 8 | ;;; | 9 | ;;; |
| @@ -21,6 +22,7 @@ | |||
| 21 | 22 | ||
| 22 | (define-module (tests services configuration) | 23 | (define-module (tests services configuration) |
| 23 | #:use-module (gnu services configuration) | 24 | #:use-module (gnu services configuration) |
| 25 | #:use-module (gnu services configuration environment-variables) | ||
| 24 | #:use-module (guix diagnostics) | 26 | #:use-module (guix diagnostics) |
| 25 | #:use-module (guix gexp) | 27 | #:use-module (guix gexp) |
| 26 | #:autoload (guix i18n) (G_) | 28 | #:autoload (guix i18n) (G_) |
| @@ -48,14 +50,14 @@ | |||
| 48 | (port-configuration-port (port-configuration))) | 50 | (port-configuration-port (port-configuration))) |
| 49 | 51 | ||
| 50 | (test-equal "wrong type for a field" | 52 | (test-equal "wrong type for a field" |
| 51 | '("configuration.scm" 59 11) ;error location | 53 | '("configuration.scm" 61 11) ;error location |
| 52 | (guard (c ((configuration-error? c) | 54 | (guard (c ((configuration-error? c) |
| 53 | (let ((loc (error-location c))) | 55 | (let ((loc (error-location c))) |
| 54 | (list (basename (location-file loc)) | 56 | (list (basename (location-file loc)) |
| 55 | (location-line loc) | 57 | (location-line loc) |
| 56 | (location-column loc))))) | 58 | (location-column loc))))) |
| 57 | (port-configuration | 59 | (port-configuration |
| 58 | ;; This is line 58; the test relies on line/column numbers! | 60 | ;; This is line 60; the test relies on line/column numbers! |
| 59 | (port "This is not a number!")))) | 61 | (port "This is not a number!")))) |
| 60 | 62 | ||
| 61 | (define-configuration port-configuration-cs | 63 | (define-configuration port-configuration-cs |
| @@ -363,3 +365,126 @@ | |||
| 363 | (config-with-maybe-string/no-serialization-name | 365 | (config-with-maybe-string/no-serialization-name |
| 364 | (config-with-maybe-string/no-serialization | 366 | (config-with-maybe-string/no-serialization |
| 365 | (name "foo"))))) | 367 | (name "foo"))))) |
| 368 | |||
| 369 | |||
| 370 | ;;; | ||
| 371 | ;;; environment-variables serializer | ||
| 372 | ;;; | ||
| 373 | |||
| 374 | (define-configuration/no-serialization env-config | ||
| 375 | (port (number 80) "") | ||
| 376 | (count maybe-number "") | ||
| 377 | (name string "") | ||
| 378 | (url maybe-string "") | ||
| 379 | (active? (boolean #f) "")) | ||
| 380 | |||
| 381 | (test-group "environment variables serializer" | ||
| 382 | |||
| 383 | (test-equal "basic serialization" | ||
| 384 | '(("PORT" . "70") | ||
| 385 | ("COUNT" . "80") | ||
| 386 | ("NAME" . "Hello World") | ||
| 387 | ("URL" . "https://example.org") | ||
| 388 | ("ACTIVE" . "true")) | ||
| 389 | (serialize-environment-variables | ||
| 390 | (env-config | ||
| 391 | (port 70) | ||
| 392 | (name "Hello World") | ||
| 393 | (url "https://example.org") | ||
| 394 | (active? #t) | ||
| 395 | (count 80)) | ||
| 396 | env-config-fields)) | ||
| 397 | |||
| 398 | (test-equal "field selection" | ||
| 399 | '(("PORT" . "80")) | ||
| 400 | (serialize-environment-variables | ||
| 401 | (env-config | ||
| 402 | (name "Hello World")) | ||
| 403 | env-config-fields | ||
| 404 | '(port))) | ||
| 405 | |||
| 406 | (test-equal "field negative selection" | ||
| 407 | '(("NAME" . "Hello World") | ||
| 408 | ("ACTIVE" . "false")) | ||
| 409 | (serialize-environment-variables | ||
| 410 | (env-config | ||
| 411 | (name "Hello World")) | ||
| 412 | env-config-fields | ||
| 413 | '(port) | ||
| 414 | #t)) | ||
| 415 | |||
| 416 | (test-equal "variable prefixes" | ||
| 417 | '(("TEST_PORT" . "80") | ||
| 418 | ("TEST_NAME" . "Hello World") | ||
| 419 | ("TEST_ACTIVE" . "false")) | ||
| 420 | (serialize-environment-variables | ||
| 421 | (env-config | ||
| 422 | (name "Hello World")) | ||
| 423 | env-config-fields | ||
| 424 | #:prefix "TEST_")) | ||
| 425 | |||
| 426 | (test-equal "boolean serialization" | ||
| 427 | '(("PORT" . "80") | ||
| 428 | ("NAME" . "Hello World") | ||
| 429 | ("ACTIVE" . "1")) | ||
| 430 | (serialize-environment-variables | ||
| 431 | (env-config | ||
| 432 | (name "Hello World") | ||
| 433 | (active? #t)) | ||
| 434 | env-config-fields | ||
| 435 | #:true-value "1" | ||
| 436 | #:false-value "0")) | ||
| 437 | |||
| 438 | (test-equal "full record serialization" | ||
| 439 | '(("TEST_COUNT" . "800") | ||
| 440 | ("TEST_NAME" . "Hello World") | ||
| 441 | ("TEST_URL" . "https://example.org") | ||
| 442 | ("TEST_ACTIVE" . "1")) | ||
| 443 | (serialize-environment-variables | ||
| 444 | (env-config | ||
| 445 | (port 90) | ||
| 446 | (name "Hello World") | ||
| 447 | (count 800) | ||
| 448 | (url "https://example.org") | ||
| 449 | (active? #t)) | ||
| 450 | env-config-fields | ||
| 451 | '(port) | ||
| 452 | #t | ||
| 453 | #:prefix "TEST_" | ||
| 454 | #:true-value "1" | ||
| 455 | #:false-value "0"))) | ||
| 456 | |||
| 457 | (define-configuration another-env-config | ||
| 458 | (port | ||
| 459 | (number 80) | ||
| 460 | "" | ||
| 461 | (serializer serialize-number-environment-variable)) | ||
| 462 | (count | ||
| 463 | (maybe-number) | ||
| 464 | "" | ||
| 465 | (serializer serialize-maybe-number-environment-variable)) | ||
| 466 | (name | ||
| 467 | (string) | ||
| 468 | "" | ||
| 469 | (serializer serialize-string-environment-variable)) | ||
| 470 | (url | ||
| 471 | (maybe-string) | ||
| 472 | "" | ||
| 473 | (serializer serialize-maybe-string-environment-variable)) | ||
| 474 | (active? | ||
| 475 | (boolean #f) | ||
| 476 | "" | ||
| 477 | (serializer serialize-boolean-environment-variable))) | ||
| 478 | |||
| 479 | (test-group "environment variables serializer, with field serializers" | ||
| 480 | |||
| 481 | (test-assert "full record serialization" | ||
| 482 | (gexp? | ||
| 483 | (serialize-configuration | ||
| 484 | (another-env-config | ||
| 485 | (port 90) | ||
| 486 | (name "Hello World") | ||
| 487 | (count 800) | ||
| 488 | (url "https://example.org") | ||
| 489 | (active? #t)) | ||
| 490 | another-env-config-fields)))) | ||
