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