home.scm
| 1 | ;;; Guix Home Manager. |
| 2 | ;;; Copyright © 2014, 2015, 2018 David Thompson <davet@gnu.org> |
| 3 | ;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org> |
| 4 | ;;; Copyright © 2018 Mike Gerwitz <mtg@gnu.org> |
| 5 | ;;; Copyright © 2019 Julien Lepiller <julien@lepiller.eu> |
| 6 | ;;; Copyright © 2020 Jelle Licht <jlicht@fsfe.org> |
| 7 | ;;; |
| 8 | ;;; This program is free software: you can redistribute it and/or modify |
| 9 | ;;; it under the terms of the GNU General Public License as published by |
| 10 | ;;; the Free Software Foundation, either version 3 of the License, or |
| 11 | ;;; (at your option) any later version. |
| 12 | ;;; |
| 13 | ;;; This program is distributed in the hope that it will be useful, |
| 14 | ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | ;;; GNU General Public License for more details. |
| 17 | ;;; |
| 18 | ;;; You should have received a copy of the GNU General Public License |
| 19 | ;;; along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 20 | ;;; This file is part of GNU Guix. |
| 21 | |
| 22 | (define-module (guix scripts home) |
| 23 | #:use-module (guix derivations) |
| 24 | #:use-module (guix grafts) |
| 25 | #:use-module (guix monads) |
| 26 | #:use-module (guix packages) |
| 27 | #:use-module (guix profiles) |
| 28 | #:use-module (guix scripts) |
| 29 | #:use-module (guix scripts build) |
| 30 | #:use-module (guix status) |
| 31 | #:use-module (guix store) |
| 32 | #:use-module (guix ui) |
| 33 | #:use-module (guix utils) |
| 34 | #:use-module (home) |
| 35 | #:use-module (ice-9 match) |
| 36 | #:use-module (srfi srfi-1) |
| 37 | #:use-module (srfi srfi-26) |
| 38 | #:use-module (srfi srfi-35) |
| 39 | #:use-module (srfi srfi-37) |
| 40 | #:use-module (srfi srfi-98) |
| 41 | #:export (guix-home)) |
| 42 | |
| 43 | ;;; |
| 44 | ;;; Command-line options. |
| 45 | ;;; |
| 46 | |
| 47 | (define %options |
| 48 | (list (option '(#\h "help") #f #f |
| 49 | (lambda args |
| 50 | (show-help) |
| 51 | (exit 0))) |
| 52 | (option '(#\V "version") #f #f |
| 53 | (lambda args |
| 54 | (show-version-and-exit "guix edit"))))) |
| 55 | |
| 56 | (define (show-help) |
| 57 | (display (G_ "Usage: guix home [OPTION ...] ACTION [ARG ...] [FILE] |
| 58 | Manage a user home environment according to FILE and ACTION. Some actions |
| 59 | support additional ARGs.\n")) |
| 60 | (display (G_ "The valid values for ACTION are:\n")) |
| 61 | (newline) |
| 62 | (display (G_ "\ |
| 63 | reconfigure switch to or create a new home configuration\n")) |
| 64 | (display (G_ "\ |
| 65 | build build the home configuration without installing anything\n")) |
| 66 | (display (G_ "\ |
| 67 | try try the home configuration without installing anything\n")) |
| 68 | (display (G_ "\ |
| 69 | list-generations list the home generations\n")) |
| 70 | (show-build-options-help) |
| 71 | (display (G_ " |
| 72 | -h, --help display this help and exit")) |
| 73 | (display (G_ " |
| 74 | -V, --version display version information and exit")) |
| 75 | (newline) |
| 76 | (show-bug-report-information)) |
| 77 | |
| 78 | (define %options |
| 79 | ;; Specifications of the command-line options. |
| 80 | (cons* (option '(#\h "help") #f #f |
| 81 | (lambda args |
| 82 | (show-help) |
| 83 | (exit 0))) |
| 84 | (option '(#\V "version") #f #f |
| 85 | (lambda args |
| 86 | (show-version-and-exit "guix system"))) |
| 87 | (option '(#\e "expression") #t #f |
| 88 | (lambda (opt name arg result) |
| 89 | (alist-cons 'expression arg result))) |
| 90 | (option '(#\d "derivation") #f #f |
| 91 | (lambda (opt name arg result) |
| 92 | (alist-cons 'derivations-only? #t result))) |
| 93 | (option '("on-error") #t #f |
| 94 | (lambda (opt name arg result) |
| 95 | (alist-cons 'on-error (string->symbol arg) |
| 96 | result))) |
| 97 | (option '(#\t "file-system-type") #t #f |
| 98 | (lambda (opt name arg result) |
| 99 | (alist-cons 'file-system-type arg |
| 100 | result))) |
| 101 | (option '("image-size") #t #f |
| 102 | (lambda (opt name arg result) |
| 103 | (alist-cons 'image-size (size->number arg) |
| 104 | result))) |
| 105 | (option '(#\N "network") #f #f |
| 106 | (lambda (opt name arg result) |
| 107 | (alist-cons 'container-shared-network? #t result))) |
| 108 | (option '("no-bootloader" "no-grub") #f #f |
| 109 | (lambda (opt name arg result) |
| 110 | (alist-cons 'install-bootloader? #f result))) |
| 111 | (option '("full-boot") #f #f |
| 112 | (lambda (opt name arg result) |
| 113 | (alist-cons 'full-boot? #t result))) |
| 114 | (option '("skip-checks") #f #f |
| 115 | (lambda (opt name arg result) |
| 116 | (alist-cons 'skip-safety-checks? #t result))) |
| 117 | |
| 118 | (option '(#\n "dry-run") #f #f |
| 119 | (lambda (opt name arg result) |
| 120 | (alist-cons 'dry-run? #t (alist-cons 'graft? #f result)))) |
| 121 | (option '(#\v "verbosity") #t #f |
| 122 | (lambda (opt name arg result) |
| 123 | (let ((level (string->number* arg))) |
| 124 | (alist-cons 'verbosity level |
| 125 | (alist-delete 'verbosity result))))) |
| 126 | (option '(#\s "system") #t #f |
| 127 | (lambda (opt name arg result) |
| 128 | (alist-cons 'system arg |
| 129 | (alist-delete 'system result eq?)))) |
| 130 | (option '(#\r "root") #t #f |
| 131 | (lambda (opt name arg result) |
| 132 | (alist-cons 'gc-root arg result))) |
| 133 | %standard-build-options)) |
| 134 | |
| 135 | (define %default-options |
| 136 | ;; Alist of default option values. |
| 137 | `((system . ,(%current-system)) |
| 138 | (substitutes? . #t) |
| 139 | (build-hook? . #t) |
| 140 | (print-build-trace? . #t) |
| 141 | (print-extended-build-trace? . #t) |
| 142 | (multiplexed-build-output? . #t) |
| 143 | (graft? . #t) |
| 144 | (debug . 0) |
| 145 | (verbosity . #f) ;default |
| 146 | (file-system-type . "ext4") |
| 147 | (image-size . guess) |
| 148 | (install-bootloader? . #t))) |
| 149 | |
| 150 | ;;; |
| 151 | ;;; Profiles |
| 152 | ;;; |
| 153 | |
| 154 | (define %user-module |
| 155 | ;; Module in which the machine description file is loaded. |
| 156 | (make-user-module '())) |
| 157 | |
| 158 | (define %home (getenv "HOME")) |
| 159 | |
| 160 | (define %current-home |
| 161 | (string-append %profile-directory "/home")) |
| 162 | |
| 163 | (define (ensure-home-profile data-directory) |
| 164 | "Ensures $HOME is a symlink to the profile. If it is not yet the case, move |
| 165 | it to the @var{data-directory} directory, unless it already exists, in which case |
| 166 | report an error." |
| 167 | (ensure-profile-directory) |
| 168 | |
| 169 | (when (and %home %current-home) |
| 170 | (let ((home (false-if-exception (lstat %home)))) |
| 171 | (cond |
| 172 | ((equal? (stat:type home) 'symlink) |
| 173 | (unless (equal? (readlink %home) %current-home) |
| 174 | (delete-file %home) |
| 175 | (symlink %current-home %home))) |
| 176 | ((false-if-exception (lstat data-directory)) |
| 177 | (leave (G_ "Your $HOME directory (~a) is not a symlink to the home |
| 178 | profile, and it cannot be moved as ~a already exists on the filesystem.~%") |
| 179 | %home data-directory)) |
| 180 | ((not home) |
| 181 | (symlink %current-home %home)) |
| 182 | (else |
| 183 | (rename-file %home data-directory) |
| 184 | (symlink %current-home %home)))))) |
| 185 | |
| 186 | (define (list-generations pattern) |
| 187 | "Display in a human-readable format all the home generations matching |
| 188 | PATTERN, a string. When PATTERN is #f, display all the home generations." |
| 189 | (cond ((not (file-exists? %current-home)) ; XXX: race condition |
| 190 | (raise (condition (&profile-not-found-error |
| 191 | (profile %current-home))))) |
| 192 | ((not pattern) |
| 193 | (for-each (lambda (number) (display-generation %current-home number)) (profile-generations %current-home))) |
| 194 | ((matching-generations pattern %current-home) |
| 195 | => |
| 196 | (lambda (numbers) |
| 197 | (if (null-list? numbers) |
| 198 | (exit 1) |
| 199 | (leave-on-EPIPE |
| 200 | (for-each (lambda (number) (display-generation %current-home number)) numbers))))))) |
| 201 | |
| 202 | ;;; |
| 203 | ;;; Try |
| 204 | ;;; |
| 205 | ;;; Very directly inspired by GNU Guix' (guix scripts environment) |
| 206 | |
| 207 | (define %precious-variables |
| 208 | '("USER" "LOGNAME" "DISPLAY" "TERM" "TZ" "PAGER")) |
| 209 | |
| 210 | (define %default-shell |
| 211 | (or (getenv "SHELL") "/bin/sh")) |
| 212 | |
| 213 | (define (purify-environment white-list) |
| 214 | "Unset all environment variables except those that match the regexps in |
| 215 | WHITE-LIST and those listed in %PRECIOUS-VARIABLES. A small number of |
| 216 | variables such as 'USER' are left untouched." |
| 217 | (for-each unsetenv |
| 218 | (remove (lambda (variable) |
| 219 | (or (member variable %precious-variables) |
| 220 | (find (cut regexp-exec <> variable) |
| 221 | white-list))) |
| 222 | (match (get-environment-variables) |
| 223 | (((names . _) ...) |
| 224 | names))))) |
| 225 | |
| 226 | (define* (create-environment #:optional (white-list '())) |
| 227 | "Unset the variables in the current environment except those that match the |
| 228 | regexps in WHITE-LIST." |
| 229 | (purify-environment white-list)) |
| 230 | |
| 231 | |
| 232 | (define* (launch-environment command new-home #:key (white-list '())) |
| 233 | "Run COMMAND in a new environment containing INPUTS, using the native search |
| 234 | paths defined by the list PATHS. When PURE?, pre-existing environment |
| 235 | variables are cleared before setting the new ones, except those matching the |
| 236 | regexps in WHITE-LIST." |
| 237 | ;; Properly handle SIGINT, so pressing C-c in an interactive terminal |
| 238 | ;; application works. |
| 239 | (sigaction SIGINT SIG_DFL) |
| 240 | (create-environment white-list) |
| 241 | (setenv "HOME" new-home) |
| 242 | (chdir new-home) |
| 243 | (match command |
| 244 | ((program . args) |
| 245 | (apply execlp program program args)))) |
| 246 | |
| 247 | (define* (launch-environment/fork command new-home #:key (white-list '())) |
| 248 | "Run COMMAND in a new process with an environment containing PROFILE, with |
| 249 | the search paths specified by MANIFEST. When PURE?, pre-existing environment |
| 250 | variables are cleared before setting the new ones, except those matching the |
| 251 | regexps in WHITE-LIST." |
| 252 | (match (primitive-fork) |
| 253 | (0 (launch-environment command new-home #:white-list white-list)) |
| 254 | (pid (match (waitpid pid) |
| 255 | ((_ . status) status))))) |
| 256 | |
| 257 | (define (status->exit-code status) |
| 258 | "Compute the exit code made from STATUS, a value as returned by 'waitpid', |
| 259 | and suitable for 'exit'." |
| 260 | ;; See <bits/waitstatus.h>. |
| 261 | (or (status:exit-val status) |
| 262 | (logior #x80 (status:term-sig status)))) |
| 263 | (define exit/status (compose exit status->exit-code)) |
| 264 | |
| 265 | ;;; |
| 266 | ;;; Entry point. |
| 267 | ;;; |
| 268 | |
| 269 | (define* (perform-action action home |
| 270 | #:key |
| 271 | dry-run? derivations-only? |
| 272 | use-substitutes?) |
| 273 | "Perform ACTION for HOME. When DERIVATIONS-ONLY? is true, print the |
| 274 | derivation file name(s) without building anything." |
| 275 | (define println |
| 276 | (cut format #t "~a~%" <>)) |
| 277 | |
| 278 | (with-store store |
| 279 | (let* ((drv (run-with-store store (home->derivation home))) |
| 280 | (profile (derivation->output-path drv))) |
| 281 | (show-what-to-build store (list drv) |
| 282 | #:use-substitutes? use-substitutes? |
| 283 | #:dry-run? dry-run?) |
| 284 | |
| 285 | (unless (or dry-run? derivations-only?) |
| 286 | (begin |
| 287 | (build-derivations store (list drv)) |
| 288 | (case action |
| 289 | ((reconfigure) |
| 290 | (newline) |
| 291 | (format #t (G_ "Activating home...~%")) |
| 292 | (ensure-home-profile (home-data-directory home)) |
| 293 | (let* ((number (generation-number %current-home)) |
| 294 | (generation (generation-file-name %current-home (+ 1 number)))) |
| 295 | (switch-symlinks generation profile) |
| 296 | (switch-symlinks %current-home generation)) |
| 297 | (format #t (G_ "Your home directory has been reconfigured.~%"))) |
| 298 | ((try) |
| 299 | (newline) |
| 300 | (format #t (G_ "Trying out home...~%")) |
| 301 | (exit/status |
| 302 | (launch-environment/fork (list %default-shell "-l") profile))) |
| 303 | (else |
| 304 | (display profile) |
| 305 | (newline)))))))) |
| 306 | |
| 307 | (define (process-action action args opts) |
| 308 | "Process ACTION, a sub-command, with the arguments are listed in ARGS. |
| 309 | ACTION must be one of the sub-commands that takes an operating system |
| 310 | declaration as an argument (a file name.) OPTS is the raw alist of options |
| 311 | resulting from command-line parsing." |
| 312 | (define (ensure-home-configuration file-or-exp obj) |
| 313 | (unless (home? obj) |
| 314 | (leave (G_ "'~a' does not return a home configuration~%") |
| 315 | file-or-exp)) |
| 316 | obj) |
| 317 | |
| 318 | (let* ((file (match args |
| 319 | (() #f) |
| 320 | ((x . _) x))) |
| 321 | (expr (assoc-ref opts 'expression)) |
| 322 | (system (assoc-ref opts 'system)) |
| 323 | (home (ensure-home-configuration |
| 324 | (or file expr) |
| 325 | (cond |
| 326 | ((and expr file) |
| 327 | (leave |
| 328 | (G_ "both file and expression cannot be specified~%"))) |
| 329 | (expr |
| 330 | (read/eval expr)) |
| 331 | (file |
| 332 | (load* file %user-module |
| 333 | #:on-error (assoc-ref opts 'on-error))) |
| 334 | (else |
| 335 | (leave (G_ "no configuration specified~%")))))) |
| 336 | |
| 337 | (dry? (assoc-ref opts 'dry-run?))) |
| 338 | |
| 339 | (with-store store |
| 340 | (set-build-options-from-command-line store opts) |
| 341 | |
| 342 | (set-guile-for-build (default-guile)) |
| 343 | |
| 344 | (case action |
| 345 | (else |
| 346 | (unless (eq? action 'build) |
| 347 | (warn-about-old-distro #:suggested-command |
| 348 | "guix home reconfigure")) |
| 349 | |
| 350 | (perform-action action home |
| 351 | #:dry-run? dry? |
| 352 | #:derivations-only? (assoc-ref opts |
| 353 | 'derivations-only?) |
| 354 | #:use-substitutes? (assoc-ref opts 'substitutes?))))) |
| 355 | (warn-about-disk-space))) |
| 356 | |
| 357 | (define (resolve-subcommand name) |
| 358 | (let ((module (resolve-interface |
| 359 | `(guix scripts home ,(string->symbol name)))) |
| 360 | (proc (string->symbol (string-append "guix-home-" name)))) |
| 361 | (module-ref module proc))) |
| 362 | |
| 363 | (define (process-command command args opts) |
| 364 | "Process COMMAND, one of the 'guix system' sub-commands. ARGS is its |
| 365 | argument list and OPTS is the option alist." |
| 366 | (case command |
| 367 | ;; The following commands do not need to use the store, and they do not need |
| 368 | ;; an operating system configuration file. |
| 369 | ((list-generations) |
| 370 | (let ((pattern (match args |
| 371 | (() #f) |
| 372 | ((pattern) pattern) |
| 373 | (x (leave (G_ "wrong number of arguments~%")))))) |
| 374 | (list-generations pattern))) |
| 375 | ;; The following commands need to use the store, but they do not need an |
| 376 | ;; operating system configuration file. |
| 377 | ;; The following commands need to use the store, and they also |
| 378 | ;; need an operating system configuration file. |
| 379 | (else (process-action command args opts)))) |
| 380 | |
| 381 | (define (guix-home . args) |
| 382 | (define (parse-sub-command arg result) |
| 383 | ;; Parse sub-command ARG and augment RESULT accordingly. |
| 384 | (if (assoc-ref result 'action) |
| 385 | (alist-cons 'argument arg result) |
| 386 | (let ((action (string->symbol arg))) |
| 387 | (case action |
| 388 | ((build reconfigure list-generations try) |
| 389 | (alist-cons 'action action result)) |
| 390 | (else (leave (G_ "~a: unknown action~%") action)))))) |
| 391 | |
| 392 | (define (match-pair car) |
| 393 | ;; Return a procedure that matches a pair with CAR. |
| 394 | (match-lambda |
| 395 | ((head . tail) |
| 396 | (and (eq? car head) tail)) |
| 397 | (_ #f))) |
| 398 | |
| 399 | (define (option-arguments opts) |
| 400 | ;; Extract the plain arguments from OPTS. |
| 401 | (let* ((args (reverse (filter-map (match-pair 'argument) opts))) |
| 402 | (count (length args)) |
| 403 | (action (assoc-ref opts 'action)) |
| 404 | (expr (assoc-ref opts 'expression))) |
| 405 | (define (fail) |
| 406 | (leave (G_ "wrong number of arguments for action '~a'~%") |
| 407 | action)) |
| 408 | |
| 409 | (unless action |
| 410 | (format (current-error-port) |
| 411 | (G_ "guix home: missing command name~%")) |
| 412 | (format (current-error-port) |
| 413 | (G_ "Try 'guix home --help' for more information.~%")) |
| 414 | (exit 1)) |
| 415 | |
| 416 | (case action |
| 417 | ((build reconfigure try) |
| 418 | (unless (= count 1) |
| 419 | (fail)))) |
| 420 | args)) |
| 421 | |
| 422 | (with-error-handling |
| 423 | (let* ((opts (parse-command-line args %options |
| 424 | (list %default-options) |
| 425 | #:argument-handler |
| 426 | parse-sub-command)) |
| 427 | (args (option-arguments opts)) |
| 428 | (command (assoc-ref opts 'action))) |
| 429 | (parameterize ((%graft? (assoc-ref opts 'graft?))) |
| 430 | (with-status-verbosity (or (assoc-ref opts 'verbosity) |
| 431 | (if (eq? command 'build) 2 1)) |
| 432 | (process-command command args opts)))))) |
| 433 | |
| 434 | ;;; home.scm ends here |
| 435 |