guix-home-manager/home.scm

home.scm

1
;;; Guix Home Manager.
2
;;;
3
;;; Copyright © 2019 Julien Lepiller <julien@lepiller.eu>
4
;;;
5
;;; This program is free software: you can redistribute it and/or modify
6
;;; it under the terms of the GNU General Public License as published by
7
;;; the Free Software Foundation, either version 3 of the License, or
8
;;; (at your option) any later version.
9
;;;
10
;;; This program is distributed in the hope that it will be useful,
11
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
12
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
;;; GNU General Public License for more details.
14
;;;
15
;;; You should have received a copy of the GNU General Public License
16
;;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18
(define-module (home)
19
  #:use-module (guix build union)
20
  #:use-module (guix build utils)
21
  #:use-module (guix build-system trivial)
22
  #:use-module (guix gexp)
23
  #:use-module (guix licenses)
24
  #:use-module (guix packages)
25
  #:use-module (guix records)
26
  #:use-module (guix ui)
27
  #:use-module (guix utils)
28
  #:use-module (ice-9 match)
29
  #:use-module (ice-9 vlist)
30
  #:use-module (home build utils)
31
  #:use-module (srfi srfi-1)
32
  #:use-module (srfi srfi-9)
33
  #:use-module (srfi srfi-35)
34
  #:export (use-home-modules
35
            home
36
            home?
37
            home-data-directory
38
            home-guix-symlink
39
            home-guix-config-symlink
40
            home-local-symlink
41
            home-cache-symlink
42
            home->derivation
43
            
44
            user-home
45
            user-home-type
46
            user-home-value
47
            
48
            home-type
49
            home-type?
50
            home-type-name
51
            home-type-extensions
52
            home-type-extension-points
53
            home-type-default-value
54
            home-type-description
55
            home-type-location
56
            
57
            home-extension
58
            home-extension?
59
            home-extension-target
60
            home-extension-target-point
61
            home-extension-compute
62
            
63
            home-extension-point
64
            home-extension-point?
65
            home-extension-point-name
66
            home-extension-point-compose
67
            home-extension-point-extend
68
            
69
            root-home-type))
70
71
;;;
72
;;; Service-like home configuration mechanism
73
;;;
74
75
(define &no-default-value
76
  '(no default value))
77
78
(define-record-type* <home-extension> home-extension make-home-extension
79
  home-extension?
80
  (target home-extension-target ; home-type
81
          (default #f))
82
  (point  home-extension-target-point ; symbol
83
          (default #f))
84
  ;; A function that takes the user-home value as parameter and returns the
85
  ;; value to be passed to the extension point.
86
  (compute  home-extension-compute))
87
88
(define-record-type* <home-extension-point> home-extension-point
89
  make-home-extension-point home-extension-point?
90
  (name home-extension-point-name ; symbol
91
        (default #f))
92
  ;; A function that composes extensions. It is passed a list of extension values
93
  ;; and returns one extension value
94
  (compose home-extension-point-compose)
95
  ;; A function that extends the original service value with the result of
96
  ;; the composition.  It is passed two arguments: the user-home value and the
97
  ;; result of the composition.  It returns a new user-home value.
98
  (extend home-extension-point-extend)) ; any(value) -> any(compose) -> any(value)
99
100
(define-record-type* <home-type> home-type make-home-type
101
  home-type?
102
  (name             home-type-name) ; symbol
103
  (extensions       home-type-extensions ; list home-extension
104
                    (default #f))
105
  (extension-points home-type-extension-points ; list home-extension-point
106
                    (default #f))
107
  (default-value    home-type-default-value ; any
108
                    (default &no-default-value))
109
  (description      home-type-description ; string
110
                    (default #f))
111
  (location         home-type-location
112
                    (default (and=> (current-source-location)
113
                                    source-properties->location))
114
                    (innate)))
115
116
(define-record-type <user-home>
117
  (make-user-home type value)
118
  user-home?
119
  (type  user-home-type)
120
  (value user-home-value))
121
122
(define-syntax user-home
123
  (syntax-rules ()
124
    "Return a user-home instance of TYPE.  The user-home value is VALUE or, if
125
omitted, TYPE's default value."
126
    ((_ type value)
127
     (make-user-home type value))
128
    ((_ type)
129
     (%user-home-with-default-value (current-source-location)
130
                                    type))))
131
132
(define (%user-home-with-default-value location type)
133
  "Return an instance of user-home type TYPE with its default value, if any.  If
134
TYPE does not have a default value, an error is raised."
135
  ;; TODO: Currently this is a run-time error but with a little bit macrology
136
  ;; we could turn it into an expansion-time error.
137
  (let ((default (home-type-default-value type)))
138
    (if (eq? default &no-default-value)
139
        (let ((location (source-properties->location location)))
140
          (raise
141
           (condition
142
            (&missing-value-home-error (type type) (location location))
143
            (&message
144
             (message (format #f (G_ "~a: no value specified \
145
for home configuration of type '~a'")
146
                              (location->string location)
147
                              (home-type-name type)))))))
148
        (make-user-home type default))))
149
150
(define-condition-type &home-error &error
151
  home-error?)
152
153
(define-condition-type &missing-value-home-error &home-error
154
  missing-value-home-error?
155
  (type     missing-value-home-error-type)
156
  (location missing-value-home-error-location))
157
158
(define-condition-type &missing-target-home-error &home-error
159
   missing-target-home-error?
160
   (home         missing-target-home-error-home)
161
   (target-type  missing-target-home-error-target-type))
162
163
(define-condition-type &ambiguous-target-home-error &home-error
164
   ambiguous-target-home-error?
165
   (home         missing-target-home-error-home)
166
   (target-type  missing-target-home-error-target-type))
167
168
(define root-home-type
169
  (home-type
170
    (name 'root)
171
    (extension-points
172
      (list (home-extension-point
173
          (compose (lambda (l) (apply append l)))
174
          (extend (lambda (v l) (apply append v l))))))
175
    (default-value '())
176
    (description "The root home type")))
177
178
;;;
179
;;; Home composition
180
;;;
181
182
(define (missing-target-error home target-type)
183
  (raise
184
    (condition (&missing-target-home-error
185
                 (home home)
186
                 (target-type target-type))
187
               (&message
188
                 (message
189
                   (format #f (G_ "no target of type '~a' for home configuration '~a'")
190
                           (home-type-name target-type)
191
                           (home-type-name
192
                             (user-home-type home))))))))
193
194
(define (home-back-edges homes)
195
  "Return a procedure which, when passed a user-home from HOMES, returns the
196
list of <user-home> that extend it."
197
  (define (add-edges home edges)
198
    (define (add-edge extension edges)
199
      (define (add-extension-edge target edges)
200
        (vhash-consq target home edges))
201
      (let ((target-type (home-extension-target extension))
202
            (target-point (home-extension-target-point extension)))
203
        (match (filter
204
                 (lambda (home)
205
                   (if target-type
206
                       (eq? (user-home-type home) target-type)
207
                       (and target-point
208
                            (not (null? (filter
209
                                          (lambda (extension-point)
210
                                            (eq?
211
                                              (home-extension-point-name extension-point)
212
                                              target-point))
213
                                          (home-type-extension-points
214
                                            (user-home-type home))))))))
215
                 homes)
216
          ((target ...)
217
           (fold add-extension-edge edges target))
218
          (()
219
           (missing-target-error home target-type)))))
220
    (let ((extensions (home-type-extensions (user-home-type home))))
221
      (if extensions
222
        (fold add-edge edges (home-type-extensions (user-home-type home)))
223
        edges)))
224
225
  (let ((edges (fold add-edges vlist-null homes)))
226
    (lambda (node)
227
      (reverse (vhash-foldq* cons '() node edges)))))
228
229
(define (instantiate-missing-user-homes homes)
230
  "Return HOMES, a list of user-home, augmented with any user-home targeted by
231
extensions and missing from HOMES.  Only home types with a default value can
232
be instantiated; other missing user-homes lead to a '&missing-target-home-error'."
233
  (let loop ((homes homes))
234
    (define (not-present? target)
235
      (let ((target-type (home-extension-target target)))
236
        (and target-type
237
            (null? (filter (lambda (home)
238
                             (eq? (user-home-type home) target-type))
239
                           homes)))))
240
    (define additions
241
      (append-map
242
        (lambda (home)
243
          (let ((extensions (home-type-extensions (user-home-type home))))
244
            (if extensions
245
              (map (lambda (extension) (home-extension-target extension))
246
                   (filter not-present? extensions))
247
              '())))
248
        homes))
249
250
    (define (uniq lst)
251
      (let loop ((types lst) (result '()))
252
        (match types
253
          ((type types ...)
254
           (if (member type types)
255
               (loop types result)
256
               (loop types (cons type result))))
257
          (() result))))
258
259
    (define generated
260
      (map
261
        (lambda (type)
262
          (let ((default (home-type-default-value type)))
263
            (if (eq? default &no-default-value)
264
                (missing-target-error #f target-type)
265
                (make-user-home type default))))
266
        (uniq additions)))
267
268
    (if (null? generated)
269
        homes
270
        (loop (append homes generated)))))
271
272
(define (fold-home target-type homes)
273
  "fold HOMES by successively resolving extension points, until we get only
274
one user-home of type TARGET-TYPE."
275
  (define dependents
276
    (home-back-edges homes))
277
  
278
  (define (apply-extension target)
279
    (define (update-extension extensions point home)
280
      (let* ((point-name (home-extension-point-name point))
281
             (matched-extensions
282
               (filter (lambda (extension)
283
                         (if point-name
284
                             (eq? (home-extension-target-point extension)
285
                                  point-name)
286
                             (eq? (home-extension-target extension)
287
                                  (user-home-type target))))
288
                       (home-type-extensions (user-home-type home)))))
289
        (if (null? matched-extensions)
290
            extensions
291
            (append
292
              (map
293
                (lambda (extension)
294
                  `(,point-name ,((home-extension-compute extension)
295
                                  (user-home-value home))))
296
                matched-extensions)
297
              extensions))))
298
    (lambda (home)
299
      (let loop ((extensions '()) (points (home-type-extension-points
300
                                            (user-home-type target))))
301
        (match points
302
          (() extensions)
303
          ((point points ...)
304
           (loop
305
             (update-extension extensions point home)
306
             points))))))
307
308
  (define (merge-extensions points extensions)
309
    (let loop ((points points) (extensions extensions))
310
      (match points
311
        (() extensions)
312
        (((point value) points ...)
313
         (loop points (acons point
314
                             (let ((v (assoc-ref extensions point)))
315
                               (if v (cons value v) (list value)))
316
                             extensions))))))
317
318
  (define (compose-extension target)
319
    (lambda (extension)
320
      (match extension
321
        ((point-name values ...)
322
         (match (filter
323
                  (lambda (extension-point)
324
                      (eq? (home-extension-point-name extension-point) point-name))
325
                    (home-type-extension-points (user-home-type target)))
326
           ((point)
327
            (list
328
              point-name
329
              ((home-extension-point-compose point)
330
               values))))))))
331
332
  (match (filter (lambda (home)
333
                   (eq? (user-home-type home) target-type))
334
                 homes)
335
    ((sink)
336
     (let loop ((sink sink))
337
       (let* ((dependents (map loop (dependents sink)))
338
              (extensions (map (apply-extension sink) dependents))
339
              (extensions (fold merge-extensions '() extensions))
340
              (extensions (map (compose-extension sink) extensions)))
341
         (user-home
342
           (user-home-type sink)
343
           (let ((extension-points (home-type-extension-points (user-home-type sink))))
344
             (if extension-points
345
               (fold (lambda (extension-point value)
346
                       (let* ((extend (home-extension-point-extend extension-point))
347
                              (name (home-extension-point-name extension-point))
348
                              (extension-value (assoc-ref extensions name)))
349
                         (if extension-value
350
                             (extend value extension-value)
351
                             value)))
352
                     (user-home-value sink)
353
                     (home-type-extension-points (user-home-type sink)))
354
               (user-home-value sink)))))))
355
    (()
356
     (raise
357
       (condition (&missing-target-home-error
358
                    (home #f)
359
                    (target-type target-type))
360
                  (&message
361
                    (message (format #f (G_ "home configuration of type '~a' not found")
362
                                     (home-type-name target-type)))))))
363
    (x
364
      (raise
365
        (condition (&ambiguous-target-home-error
366
                    (home #f)
367
                    (target-type target-type))
368
                   (&message
369
                    (message (format #f (G_ "home configuration of type '~a' not found")
370
                                     (home-type-name target-type)))))))))
371
372
;;;
373
;;; Home generation
374
;;;
375
376
(define-syntax use-home-modules
377
  (syntax-rules ()
378
    ((_ modules ...)
379
     (use-modules (home modules) ...))))
380
381
(define-record-type* <home> home
382
  make-home
383
  home?
384
  (data-directory home-data-directory)
385
  (base-configuration home-base-configuration (thunked)
386
    (default
387
      (base-home-configuration
388
        (guix-symlink
389
          (string-append (home-data-directory this-record) "/.guix-profile"))
390
        (guix-config-symlink
391
          (string-append (home-data-directory this-record) "/.config/guix"))
392
        (local-symlink
393
          (string-append (home-data-directory this-record) "/.local"))
394
        (cache-symlink
395
          (string-append (home-data-directory this-record) "/.cache")))))
396
  (configurations home-configurations
397
                  (default (list (user-home root-home-type)))))
398
399
(define-record-type* <base-home-configuration> base-home-configuration
400
  make-base-home-configuration
401
  base-home-configuration?
402
  (guix-symlink        base-home-configuration-guix-symlink)
403
  (guix-config-symlink base-home-configuration-guix-config-symlink)
404
  (local-symlink       base-home-configuration-local-symlink)
405
  (cache-symlink       base-home-configuration-cache-symlink))
406
407
(define base-home-type
408
  (home-type
409
    (name 'guix)
410
    (extensions
411
      (list
412
        (home-extension
413
          (target root-home-type)
414
          (compute (lambda (config)
415
                     `((".guix-profile"
416
                        ,(base-home-configuration-guix-symlink config))
417
                       (".config/guix"
418
                        ,(base-home-configuration-guix-config-symlink config))
419
                       (".local"
420
                        ,(base-home-configuration-local-symlink config))
421
                       (".cache"
422
                        ,(base-home-configuration-cache-symlink config))))))))))
423
424
(define (home->derivation home)
425
  (define builder
426
    (with-imported-modules
427
      '((guix build utils) (home build utils) (ice-9 match))
428
      #~(begin
429
          (use-modules (guix build utils) (home build utils) (ice-9 match))
430
          #$(let* ((homes (instantiate-missing-user-homes
431
                            (cons
432
                              (user-home base-home-type (home-base-configuration home))
433
                              (home-configurations home))))
434
                   (root (fold-home root-home-type homes))
435
                   (configs (user-home-value root)))
436
              #~(for-each
437
                  (lambda (config)
438
                    (match config
439
                      ((home-name target)
440
                       (mkdir-p (dirname (home-file #$output home-name)))
441
                       (symlink target (home-file #$output home-name)))))
442
                  '(#$@configs))))))
443
  (gexp->derivation "home" builder
444
                    #:substitutable? #f
445
                    #:local-build? #t))
446