configuration.scm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2015 Andy Wingo <wingo@igalia.com>
  3. ;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
  4. ;;; Copyright © 2017, 2018 Clément Lassieur <clement@lassieur.org>
  5. ;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
  6. ;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  7. ;;; Copyright © 2021 Andrew Tropin <andrew@trop.in>
  8. ;;;
  9. ;;; This file is part of GNU Guix.
  10. ;;;
  11. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  12. ;;; under the terms of the GNU General Public License as published by
  13. ;;; the Free Software Foundation; either version 3 of the License, or (at
  14. ;;; your option) any later version.
  15. ;;;
  16. ;;; GNU Guix is distributed in the hope that it will be useful, but
  17. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  18. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. ;;; GNU General Public License for more details.
  20. ;;;
  21. ;;; You should have received a copy of the GNU General Public License
  22. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  23. (define-module (gnu services configuration)
  24. #:use-module (guix packages)
  25. #:use-module (guix records)
  26. #:use-module (guix gexp)
  27. #:use-module ((guix utils) #:select (source-properties->location))
  28. #:use-module ((guix diagnostics) #:select (formatted-message location-file))
  29. #:use-module ((guix modules) #:select (file-name->module-name))
  30. #:use-module (guix i18n)
  31. #:autoload (texinfo) (texi-fragment->stexi)
  32. #:autoload (texinfo serialize) (stexi->texi)
  33. #:use-module (ice-9 curried-definitions)
  34. #:use-module (ice-9 match)
  35. #:use-module (srfi srfi-1)
  36. #:use-module (srfi srfi-34)
  37. #:use-module (srfi srfi-35)
  38. #:export (configuration-field
  39. configuration-field-name
  40. configuration-field-type
  41. configuration-missing-field
  42. configuration-field-error
  43. configuration-field-serializer
  44. configuration-field-getter
  45. configuration-field-default-value-thunk
  46. configuration-field-documentation
  47. configuration-error?
  48. define-configuration
  49. define-configuration/no-serialization
  50. no-serialization
  51. serialize-configuration
  52. define-maybe
  53. define-maybe/no-serialization
  54. validate-configuration
  55. generate-documentation
  56. configuration->documentation
  57. empty-serializer
  58. serialize-package
  59. filter-configuration-fields
  60. interpose
  61. list-of
  62. list-of-strings?
  63. alist?
  64. serialize-file-like
  65. text-config?
  66. serialize-text-config
  67. generic-serialize-alist-entry
  68. generic-serialize-alist))
  69. ;;; Commentary:
  70. ;;;
  71. ;;; Syntax for creating Scheme bindings to complex configuration files.
  72. ;;;
  73. ;;; Code:
  74. (define-condition-type &configuration-error &error
  75. configuration-error?)
  76. (define (configuration-error message)
  77. (raise (condition (&message (message message))
  78. (&configuration-error))))
  79. (define (configuration-field-error field val)
  80. (configuration-error
  81. (format #f "Invalid value for field ~a: ~s" field val)))
  82. (define (configuration-missing-field kind field)
  83. (configuration-error
  84. (format #f "~a configuration missing required field ~a" kind field)))
  85. (define (configuration-no-default-value kind field)
  86. (configuration-error
  87. (format #f "The field `~a' of the `~a' configuration record \
  88. does not have a default value" field kind)))
  89. (define-record-type* <configuration-field>
  90. configuration-field make-configuration-field configuration-field?
  91. (name configuration-field-name)
  92. (type configuration-field-type)
  93. (getter configuration-field-getter)
  94. (predicate configuration-field-predicate)
  95. (serializer configuration-field-serializer)
  96. (default-value-thunk configuration-field-default-value-thunk)
  97. (documentation configuration-field-documentation))
  98. (define (serialize-configuration config fields)
  99. #~(string-append
  100. #$@(map (lambda (field)
  101. ((configuration-field-serializer field)
  102. (configuration-field-name field)
  103. ((configuration-field-getter field) config)))
  104. fields)))
  105. (define (validate-configuration config fields)
  106. (for-each (lambda (field)
  107. (let ((val ((configuration-field-getter field) config)))
  108. (unless ((configuration-field-predicate field) val)
  109. (configuration-field-error
  110. (configuration-field-name field) val))))
  111. fields))
  112. (define-syntax-rule (id ctx parts ...)
  113. "Assemble PARTS into a raw (unhygienic) identifier."
  114. (datum->syntax ctx (symbol-append (syntax->datum parts) ...)))
  115. (define (define-maybe-helper serialize? prefix syn)
  116. (syntax-case syn ()
  117. ((_ stem)
  118. (with-syntax
  119. ((stem? (id #'stem #'stem #'?))
  120. (maybe-stem? (id #'stem #'maybe- #'stem #'?))
  121. (serialize-stem (if prefix
  122. (id #'stem prefix #'serialize- #'stem)
  123. (id #'stem #'serialize- #'stem)))
  124. (serialize-maybe-stem (if prefix
  125. (id #'stem prefix #'serialize-maybe- #'stem)
  126. (id #'stem #'serialize-maybe- #'stem))))
  127. #`(begin
  128. (define (maybe-stem? val)
  129. (or (eq? val 'disabled) (stem? val)))
  130. #,@(if serialize?
  131. (list #'(define (serialize-maybe-stem field-name val)
  132. (if (stem? val)
  133. (serialize-stem field-name val)
  134. "")))
  135. '()))))))
  136. (define-syntax define-maybe
  137. (lambda (x)
  138. (syntax-case x (no-serialization prefix)
  139. ((_ stem (no-serialization))
  140. (define-maybe-helper #f #f #'(_ stem)))
  141. ((_ stem (prefix serializer-prefix))
  142. (define-maybe-helper #t #'serializer-prefix #'(_ stem)))
  143. ((_ stem)
  144. (define-maybe-helper #t #f #'(_ stem))))))
  145. (define-syntax-rule (define-maybe/no-serialization stem)
  146. (define-maybe stem (no-serialization)))
  147. (define (define-configuration-helper serialize? serializer-prefix syn)
  148. (syntax-case syn ()
  149. ((_ stem (field (field-type def ...) doc custom-serializer ...) ...)
  150. (with-syntax (((field-getter ...)
  151. (map (lambda (field)
  152. (id #'stem #'stem #'- field))
  153. #'(field ...)))
  154. ((field-predicate ...)
  155. (map (lambda (type)
  156. (id #'stem type #'?))
  157. #'(field-type ...)))
  158. ((field-default ...)
  159. (map (match-lambda
  160. ((field-type default-value)
  161. default-value)
  162. ((field-type)
  163. ;; Quote `undefined' to prevent a possibly
  164. ;; unbound warning.
  165. (syntax 'undefined)))
  166. #'((field-type def ...) ...)))
  167. ((field-serializer ...)
  168. (map (lambda (type custom-serializer)
  169. (and serialize?
  170. (match custom-serializer
  171. ((serializer)
  172. serializer)
  173. (()
  174. (if serializer-prefix
  175. (id #'stem
  176. serializer-prefix
  177. #'serialize- type)
  178. (id #'stem #'serialize- type))))))
  179. #'(field-type ...)
  180. #'((custom-serializer ...) ...))))
  181. #`(begin
  182. (define-record-type* #,(id #'stem #'< #'stem #'>)
  183. #,(id #'stem #'% #'stem)
  184. #,(id #'stem #'make- #'stem)
  185. #,(id #'stem #'stem #'?)
  186. (%location #,(id #'stem #'stem #'-location)
  187. (default (and=> (current-source-location)
  188. source-properties->location))
  189. (innate))
  190. #,@(map (lambda (name getter def)
  191. (if (eq? (syntax->datum def) (quote 'undefined))
  192. #`(#,name #,getter)
  193. #`(#,name #,getter (default #,def))))
  194. #'(field ...)
  195. #'(field-getter ...)
  196. #'(field-default ...)))
  197. (define #,(id #'stem #'stem #'-fields)
  198. (list (configuration-field
  199. (name 'field)
  200. (type 'field-type)
  201. (getter field-getter)
  202. (predicate field-predicate)
  203. (serializer field-serializer)
  204. (default-value-thunk
  205. (lambda ()
  206. (display '#,(id #'stem #'% #'stem))
  207. (if (eq? (syntax->datum field-default)
  208. 'undefined)
  209. (configuration-no-default-value
  210. '#,(id #'stem #'% #'stem) 'field)
  211. field-default)))
  212. (documentation doc))
  213. ...))
  214. (define-syntax-rule (stem arg (... ...))
  215. (let ((conf (#,(id #'stem #'% #'stem) arg (... ...))))
  216. (validate-configuration conf
  217. #,(id #'stem #'stem #'-fields))
  218. conf)))))))
  219. (define no-serialization ;syntactic keyword for 'define-configuration'
  220. '(no serialization))
  221. (define-syntax define-configuration
  222. (lambda (s)
  223. (syntax-case s (no-serialization prefix)
  224. ((_ stem (field (field-type def ...) doc custom-serializer ...) ...
  225. (no-serialization))
  226. (define-configuration-helper
  227. #f #f #'(_ stem (field (field-type def ...) doc custom-serializer ...)
  228. ...)))
  229. ((_ stem (field (field-type def ...) doc custom-serializer ...) ...
  230. (prefix serializer-prefix))
  231. (define-configuration-helper
  232. #t #'serializer-prefix #'(_ stem (field (field-type def ...)
  233. doc custom-serializer ...)
  234. ...)))
  235. ((_ stem (field (field-type def ...) doc custom-serializer ...) ...)
  236. (define-configuration-helper
  237. #t #f #'(_ stem (field (field-type def ...) doc custom-serializer ...)
  238. ...))))))
  239. (define-syntax-rule (define-configuration/no-serialization
  240. stem (field (field-type def ...)
  241. doc custom-serializer ...) ...)
  242. (define-configuration stem (field (field-type def ...)
  243. doc custom-serializer ...) ...
  244. (no-serialization)))
  245. (define (empty-serializer field-name val) "")
  246. (define serialize-package empty-serializer)
  247. ;; A little helper to make it easier to document all those fields.
  248. (define (generate-documentation documentation documentation-name)
  249. (define (str x) (object->string x))
  250. (define (package->symbol package)
  251. "Return the first symbol name of a package that matches PACKAGE, else #f."
  252. (let* ((module (file-name->module-name
  253. (location-file (package-location package))))
  254. (symbols (filter-map
  255. identity
  256. (module-map (lambda (symbol var)
  257. (and (equal? package (variable-ref var))
  258. symbol))
  259. (resolve-module module)))))
  260. (if (null? symbols)
  261. #f
  262. (first symbols))))
  263. (define (generate configuration-name)
  264. (match (assq-ref documentation configuration-name)
  265. ((fields . sub-documentation)
  266. `((deftp (% (category "Data Type") (name ,(str configuration-name)))
  267. (para "Available " (code ,(str configuration-name)) " fields are:")
  268. (table
  269. (% (formatter (asis)))
  270. ,@(map
  271. (lambda (f)
  272. (let ((field-name (configuration-field-name f))
  273. (field-type (configuration-field-type f))
  274. (field-docs (cdr (texi-fragment->stexi
  275. (configuration-field-documentation f))))
  276. (default (catch #t
  277. (configuration-field-default-value-thunk f)
  278. (lambda _ '%invalid))))
  279. (define (show-default? val)
  280. (or (string? val) (number? val) (boolean? val)
  281. (package? val)
  282. (and (symbol? val) (not (eq? val '%invalid)))
  283. (and (list? val) (and-map show-default? val))))
  284. (define (show-default val)
  285. (cond
  286. ((package? val)
  287. (symbol->string (package->symbol val)))
  288. (else (str val))))
  289. `(entry (% (heading
  290. (code ,(str field-name))
  291. ,@(if (show-default? default)
  292. `(" (default: "
  293. (code ,(show-default default)) ")")
  294. '())
  295. " (type: " ,(str field-type) ")"))
  296. (para ,@field-docs)
  297. ,@(append-map
  298. generate
  299. (or (assq-ref sub-documentation field-name)
  300. '())))))
  301. fields)))))))
  302. (stexi->texi `(*fragment* . ,(generate documentation-name))))
  303. (define (configuration->documentation configuration-symbol)
  304. "Take CONFIGURATION-SYMBOL, the symbol corresponding to the name used when
  305. defining a configuration record with DEFINE-CONFIGURATION, and output the
  306. Texinfo documentation of its fields."
  307. ;; This is helper for a simple, straight-forward application of
  308. ;; GENERATE-DOCUMENTATION.
  309. (let ((fields-getter (module-ref (current-module)
  310. (symbol-append configuration-symbol
  311. '-fields))))
  312. (display (generate-documentation `((,configuration-symbol ,fields-getter))
  313. configuration-symbol))))
  314. (define* (filter-configuration-fields configuration-fields fields
  315. #:optional negate?)
  316. "Retrieve the fields listed in FIELDS from CONFIGURATION-FIELDS.
  317. If NEGATE? is @code{#t}, retrieve all fields except FIELDS."
  318. (filter (lambda (field)
  319. (let ((member? (member (configuration-field-name field) fields)))
  320. (if (not negate?) member? (not member?))))
  321. configuration-fields))
  322. (define* (interpose ls #:optional (delimiter "\n") (grammar 'infix))
  323. "Same as @code{string-join}, but without join and string, returns an
  324. DELIMITER interposed LS. Support 'infix and 'suffix GRAMMAR values."
  325. (when (not (member grammar '(infix suffix)))
  326. (raise
  327. (formatted-message
  328. (G_ "The GRAMMAR value must be 'infix or 'suffix, but ~a provided.")
  329. grammar)))
  330. (fold-right (lambda (e acc)
  331. (cons e
  332. (if (and (null? acc) (eq? grammar 'infix))
  333. acc
  334. (cons delimiter acc))))
  335. '() ls))
  336. (define (list-of pred?)
  337. "Return a procedure that takes a list and check if all the elements of
  338. the list result in @code{#t} when applying PRED? on them."
  339. (lambda (x)
  340. (if (list? x)
  341. (every pred? x)
  342. #f)))
  343. (define list-of-strings?
  344. (list-of string?))
  345. (define alist? list?)
  346. (define serialize-file-like empty-serializer)
  347. (define (text-config? config)
  348. (list-of file-like?))
  349. (define (serialize-text-config field-name val)
  350. #~(string-append
  351. #$@(interpose
  352. (map
  353. (lambda (e)
  354. #~(begin
  355. (use-modules (ice-9 rdelim))
  356. (with-fluids ((%default-port-encoding "UTF-8"))
  357. (with-input-from-file #$e read-string))))
  358. val)
  359. "\n" 'suffix)))
  360. (define ((generic-serialize-alist-entry serialize-field) entry)
  361. "Apply the SERIALIZE-FIELD procedure on the field and value of ENTRY."
  362. (match entry
  363. ((field . val) (serialize-field field val))))
  364. (define (generic-serialize-alist combine serialize-field fields)
  365. "Generate a configuration from an association list FIELDS.
  366. SERIALIZE-FIELD is a procedure that takes two arguments, it will be
  367. applied on the fields and values of FIELDS using the
  368. @code{generic-serialize-alist-entry} procedure.
  369. COMBINE is a procedure that takes one or more arguments and combines
  370. all the alist entries into one value, @code{string-append} or
  371. @code{append} are usually good candidates for this.
  372. See the @code{serialize-alist} procedure in `@code{(gnu home services
  373. version-control}' for an example usage.)}"
  374. (apply combine
  375. (map (generic-serialize-alist-entry serialize-field) fields)))