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 home 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 |