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 |