container: Pass a list of <file-system> objects as things to mount.
[jackhill/guix/guix.git] / guix / scripts / environment.scm
1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2014, 2015 David Thompson <davet@gnu.org>
3 ;;; Copyright © 2015, 2016 Ludovic Courtès <ludo@gnu.org>
4 ;;;
5 ;;; This file is part of GNU Guix.
6 ;;;
7 ;;; GNU Guix is free software; you can redistribute it and/or modify it
8 ;;; under the terms of the GNU General Public License as published by
9 ;;; the Free Software Foundation; either version 3 of the License, or (at
10 ;;; your option) any later version.
11 ;;;
12 ;;; GNU Guix is distributed in the hope that it will be useful, but
13 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ;;; GNU General Public License for more details.
16 ;;;
17 ;;; You should have received a copy of the GNU General Public License
18 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20 (define-module (guix scripts environment)
21 #:use-module (guix ui)
22 #:use-module (guix store)
23 #:use-module (guix grafts)
24 #:use-module (guix derivations)
25 #:use-module (guix packages)
26 #:use-module (guix profiles)
27 #:use-module (guix search-paths)
28 #:use-module (guix build utils)
29 #:use-module (guix monads)
30 #:use-module ((guix gexp) #:select (lower-inputs))
31 #:use-module (guix scripts)
32 #:use-module (guix scripts build)
33 #:use-module (gnu build linux-container)
34 #:use-module (gnu system linux-container)
35 #:use-module (gnu system file-systems)
36 #:use-module (gnu packages)
37 #:use-module (gnu packages bash)
38 #:use-module (gnu packages commencement)
39 #:use-module (gnu packages guile)
40 #:use-module ((gnu packages bootstrap) #:select (%bootstrap-guile))
41 #:use-module (ice-9 format)
42 #:use-module (ice-9 match)
43 #:use-module (ice-9 rdelim)
44 #:use-module (srfi srfi-1)
45 #:use-module (srfi srfi-11)
46 #:use-module (srfi srfi-26)
47 #:use-module (srfi srfi-37)
48 #:use-module (srfi srfi-98)
49 #:export (guix-environment))
50
51 (define (evaluate-profile-search-paths profile search-paths)
52 "Evaluate SEARCH-PATHS, a list of search-path specifications, for the
53 directories in PROFILE, the store path of a profile."
54 (evaluate-search-paths search-paths (list profile)))
55
56 ;; Protect some env vars from purification. Borrowed from nix-shell.
57 (define %precious-variables
58 '("HOME" "USER" "LOGNAME" "DISPLAY" "TERM" "TZ" "PAGER"))
59
60 (define %default-shell
61 (or (getenv "SHELL") "/bin/sh"))
62
63 (define %network-configuration-files
64 '("/etc/resolv.conf"
65 "/etc/nsswitch.conf"
66 "/etc/services"
67 "/etc/hosts"))
68
69 (define (purify-environment)
70 "Unset almost all environment variables. A small number of variables such
71 as 'HOME' and 'USER' are left untouched."
72 (for-each unsetenv
73 (remove (cut member <> %precious-variables)
74 (match (get-environment-variables)
75 (((names . _) ...)
76 names)))))
77
78 (define (create-environment profile paths pure?)
79 "Set the environment variables specified by PATHS for PROFILE. When PURE?
80 is #t, unset the variables in the current environment. Otherwise, augment
81 existing enviroment variables with additional search paths."
82 (when pure? (purify-environment))
83 (for-each (match-lambda
84 ((($ <search-path-specification> variable _ separator) . value)
85 (let ((current (getenv variable)))
86 (setenv variable
87 (if (and current (not pure?))
88 (string-append value separator current)
89 value)))))
90 (evaluate-profile-search-paths profile paths))
91
92 ;; Give users a way to know that they're in 'guix environment', so they can
93 ;; adjust 'PS1' accordingly, for instance. Set it to PROFILE so users can
94 ;; conveniently access its contents.
95 (setenv "GUIX_ENVIRONMENT" profile))
96
97 (define (show-search-paths profile search-paths pure?)
98 "Display SEARCH-PATHS applied to PROFILE. When PURE? is #t, do not augment
99 existing environment variables with additional search paths."
100 (for-each (match-lambda
101 ((search-path . value)
102 (display
103 (search-path-definition search-path value
104 #:kind (if pure? 'exact 'prefix)))
105 (newline)))
106 (evaluate-profile-search-paths profile search-paths)))
107
108 (define (strip-input-name input)
109 "Remove the name element from the tuple INPUT."
110 (match input
111 ((_ package) package)
112 ((_ package output)
113 (list package output))))
114
115 (define (package+propagated-inputs package output)
116 "Return the union of PACKAGE's OUTPUT and its transitive propagated inputs."
117 (cons (list package output)
118 (map strip-input-name
119 (package-transitive-propagated-inputs package))))
120
121 (define (package-or-package+output? expr)
122 "Return #t if EXPR is a package or a 2 element list consisting of a package
123 and an output string."
124 (match expr
125 ((or (? package?) ; bare package object
126 ((? package?) (? string?))) ; package+output tuple
127 #t)
128 (_ #f)))
129
130 (define (package-environment-inputs package)
131 "Return a list of the transitive input packages for PACKAGE."
132 ;; Remove non-package inputs such as origin records.
133 (filter package-or-package+output?
134 (map strip-input-name
135 (bag-transitive-inputs
136 (package->bag package)))))
137
138 (define (show-help)
139 (display (_ "Usage: guix environment [OPTION]... PACKAGE... [-- COMMAND...]
140 Build an environment that includes the dependencies of PACKAGE and execute
141 COMMAND or an interactive shell in that environment.\n"))
142 (display (_ "
143 -e, --expression=EXPR create environment for the package that EXPR
144 evaluates to"))
145 (display (_ "
146 -l, --load=FILE create environment for the package that the code within
147 FILE evaluates to"))
148 (display (_ "
149 --ad-hoc include all specified packages in the environment instead
150 of only their inputs"))
151 (display (_ "
152 --pure unset existing environment variables"))
153 (display (_ "
154 --search-paths display needed environment variable definitions"))
155 (display (_ "
156 -s, --system=SYSTEM attempt to build for SYSTEM--e.g., \"i686-linux\""))
157 (display (_ "
158 -C, --container run command within an isolated container"))
159 (display (_ "
160 -N, --network allow containers to access the network"))
161 (display (_ "
162 --share=SPEC for containers, share writable host file system
163 according to SPEC"))
164 (display (_ "
165 --expose=SPEC for containers, expose read-only host file system
166 according to SPEC"))
167 (display (_ "
168 --bootstrap use bootstrap binaries to build the environment"))
169 (newline)
170 (show-build-options-help)
171 (newline)
172 (display (_ "
173 -h, --help display this help and exit"))
174 (display (_ "
175 -V, --version display version information and exit"))
176 (newline)
177 (show-bug-report-information))
178
179 (define %default-options
180 `((system . ,(%current-system))
181 (substitutes? . #t)
182 (graft? . #t)
183 (max-silent-time . 3600)
184 (verbosity . 0)))
185
186 (define (tag-package-arg opts arg)
187 "Return a two-element list with the form (TAG ARG) that tags ARG with either
188 'ad-hoc' in OPTS has the 'ad-hoc?' key set to #t, or 'inputs' otherwise."
189 ;; Normally, the transitive inputs to a package are added to an environment,
190 ;; but the ad-hoc? flag changes the meaning of a package argument such that
191 ;; the package itself is added to the environment instead.
192 (if (assoc-ref opts 'ad-hoc?)
193 `(ad-hoc-package ,arg)
194 `(package ,arg)))
195
196 (define %options
197 ;; Specification of the command-line options.
198 (cons* (option '(#\h "help") #f #f
199 (lambda args
200 (show-help)
201 (exit 0)))
202 (option '(#\V "version") #f #f
203 (lambda args
204 (show-version-and-exit "guix environment")))
205 (option '("pure") #f #f
206 (lambda (opt name arg result)
207 (alist-cons 'pure #t result)))
208 (option '(#\E "exec") #t #f ; deprecated
209 (lambda (opt name arg result)
210 (alist-cons 'exec (list %default-shell "-c" arg) result)))
211 (option '("search-paths") #f #f
212 (lambda (opt name arg result)
213 (alist-cons 'search-paths #t result)))
214 (option '(#\l "load") #t #f
215 (lambda (opt name arg result)
216 (alist-cons 'load
217 (tag-package-arg result arg)
218 result)))
219 (option '(#\e "expression") #t #f
220 (lambda (opt name arg result)
221 (alist-cons 'expression
222 (tag-package-arg result arg)
223 result)))
224 (option '("ad-hoc") #f #f
225 (lambda (opt name arg result)
226 (alist-cons 'ad-hoc? #t result)))
227 (option '(#\n "dry-run") #f #f
228 (lambda (opt name arg result)
229 (alist-cons 'dry-run? #t (alist-cons 'graft? #f result))))
230 (option '(#\s "system") #t #f
231 (lambda (opt name arg result)
232 (alist-cons 'system arg
233 (alist-delete 'system result eq?))))
234 (option '(#\C "container") #f #f
235 (lambda (opt name arg result)
236 (alist-cons 'container? #t result)))
237 (option '(#\N "network") #f #f
238 (lambda (opt name arg result)
239 (alist-cons 'network? #t result)))
240 (option '("share") #t #f
241 (lambda (opt name arg result)
242 (alist-cons 'file-system-mapping
243 (specification->file-system-mapping arg #t)
244 result)))
245 (option '("expose") #t #f
246 (lambda (opt name arg result)
247 (alist-cons 'file-system-mapping
248 (specification->file-system-mapping arg #f)
249 result)))
250 (option '("bootstrap") #f #f
251 (lambda (opt name arg result)
252 (alist-cons 'bootstrap? #t result)))
253 %standard-build-options))
254
255 (define (pick-all alist key)
256 "Return a list of values in ALIST associated with KEY."
257 (define same-key? (cut eq? key <>))
258
259 (fold (lambda (pair memo)
260 (match pair
261 (((? same-key? k) . v)
262 (cons v memo))
263 (_ memo)))
264 '() alist))
265
266 (define (compact lst)
267 "Remove all #f elements from LST."
268 (filter identity lst))
269
270 (define (options/resolve-packages opts)
271 "Return OPTS with package specification strings replaced by actual
272 packages."
273 (define (package->output package mode)
274 (match package
275 ((? package?)
276 (list mode package "out"))
277 (((? package? package) (? string? output))
278 (list mode package output))))
279
280 (define (packages->outputs packages mode)
281 (match packages
282 ((? package-or-package+output? package) ; single package
283 (list (package->output package mode)))
284 (((? package-or-package+output?) ...) ; many packages
285 (map (cut package->output <> mode) packages))))
286
287 (compact
288 (append-map (match-lambda
289 (('package mode (? string? spec))
290 (let-values (((package output)
291 (specification->package+output spec)))
292 (list (list mode package output))))
293 (('expression mode str)
294 ;; Add all the outputs of the package STR evaluates to.
295 (packages->outputs (read/eval str) mode))
296 (('load mode file)
297 ;; Add all the outputs of the package defined in FILE.
298 (let ((module (make-user-module '())))
299 (packages->outputs (load* file module) mode)))
300 (_ '(#f)))
301 opts)))
302
303 (define* (build-environment derivations opts)
304 "Build the DERIVATIONS required by the environment using the build options
305 in OPTS."
306 (let ((substitutes? (assoc-ref opts 'substitutes?))
307 (dry-run? (assoc-ref opts 'dry-run?)))
308 (mbegin %store-monad
309 (show-what-to-build* derivations
310 #:use-substitutes? substitutes?
311 #:dry-run? dry-run?)
312 (if dry-run?
313 (return #f)
314 (mbegin %store-monad
315 (set-build-options-from-command-line* opts)
316 (built-derivations derivations))))))
317
318 (define (inputs->profile-derivation inputs system bootstrap?)
319 "Return the derivation for a profile consisting of INPUTS for SYSTEM.
320 BOOTSTRAP? specifies whether to use the bootstrap Guile to build the
321 profile."
322 (profile-derivation (packages->manifest inputs)
323 #:system system
324 #:hooks (if bootstrap?
325 '()
326 %default-profile-hooks)))
327
328 (define requisites* (store-lift requisites))
329
330 (define (inputs->requisites inputs)
331 "Convert INPUTS, a list of input tuples or store path strings, into a set of
332 requisite store items i.e. the union closure of all the inputs."
333 (define (input->requisites input)
334 (requisites*
335 (match input
336 ((drv output)
337 (list (derivation->output-path drv output)))
338 ((drv)
339 (list (derivation->output-path drv)))
340 ((? direct-store-path? path)
341 (list path)))))
342
343 (mlet %store-monad ((reqs (sequence %store-monad
344 (map input->requisites inputs))))
345 (return (delete-duplicates (concatenate reqs)))))
346
347 (define (status->exit-code status)
348 "Compute the exit code made from STATUS, a value as returned by 'waitpid',
349 and suitable for 'exit'."
350 ;; See <bits/waitstatus.h>.
351 (or (status:exit-val status)
352 (logior #x80 (status:term-sig status))))
353
354 (define exit/status (compose exit status->exit-code))
355 (define primitive-exit/status (compose primitive-exit status->exit-code))
356
357 (define (launch-environment command inputs paths pure?)
358 "Run COMMAND in a new environment containing INPUTS, using the native search
359 paths defined by the list PATHS. When PURE?, pre-existing environment
360 variables are cleared before setting the new ones."
361 ;; Properly handle SIGINT, so pressing C-c in an interactive terminal
362 ;; application works.
363 (sigaction SIGINT SIG_DFL)
364 (create-environment inputs paths pure?)
365 (match command
366 ((program . args)
367 (apply execlp program program args))))
368
369 (define (launch-environment/fork command inputs paths pure?)
370 "Run COMMAND in a new process with an environment containing INPUTS, using
371 the native search paths defined by the list PATHS. When PURE?, pre-existing
372 environment variables are cleared before setting the new ones."
373 (match (primitive-fork)
374 (0 (launch-environment command inputs paths pure?))
375 (pid (match (waitpid pid)
376 ((_ . status) status)))))
377
378 (define* (launch-environment/container #:key command bash user-mappings
379 profile paths network?)
380 "Run COMMAND within a container that features the software in PROFILE.
381 Environment variables are set according to PATHS, a list of native search
382 paths. The global shell is BASH, a file name for a GNU Bash binary in the
383 store. When NETWORK?, access to the host system network is permitted.
384 USER-MAPPINGS, a list of file system mappings, contains the user-specified
385 host file systems to mount inside the container."
386 (mlet %store-monad ((reqs (inputs->requisites
387 (list (direct-store-path bash) profile))))
388 (return
389 (let* ((cwd (getcwd))
390 (passwd (getpwuid (getuid)))
391 ;; Bind-mount all requisite store items, user-specified mappings,
392 ;; /bin/sh, the current working directory, and possibly networking
393 ;; configuration files within the container.
394 (mappings
395 (append user-mappings
396 ;; Current working directory.
397 (list (file-system-mapping
398 (source cwd)
399 (target cwd)
400 (writable? #t)))
401 ;; When in Rome, do as Nix build.cc does: Automagically
402 ;; map common network configuration files.
403 (if network?
404 (filter-map (lambda (file)
405 (and (file-exists? file)
406 (file-system-mapping
407 (source file)
408 (target file)
409 ;; XXX: On some GNU/Linux
410 ;; systems, /etc/resolv.conf is a
411 ;; symlink to a file in a tmpfs
412 ;; which, for an unknown reason,
413 ;; cannot be bind mounted
414 ;; read-only within the
415 ;; container.
416 (writable?
417 (string=? "/etc/resolv.conf")))))
418 %network-configuration-files)
419 '())
420 ;; Mappings for the union closure of all inputs.
421 (map (lambda (dir)
422 (file-system-mapping
423 (source dir)
424 (target dir)
425 (writable? #f)))
426 reqs)))
427 (file-systems (append %container-file-systems
428 (map mapping->file-system mappings))))
429 (exit/status
430 (call-with-container file-systems
431 (lambda ()
432 ;; Setup global shell.
433 (mkdir-p "/bin")
434 (symlink bash "/bin/sh")
435
436 ;; Set a reasonable default PS1.
437 (setenv "PS1" "\\u@\\h \\w [env]\\$ ")
438
439 ;; Setup directory for temporary files.
440 (mkdir-p "/tmp")
441 (for-each (lambda (var)
442 (setenv var "/tmp"))
443 ;; The same variables as in Nix's 'build.cc'.
444 '("TMPDIR" "TEMPDIR" "TMP" "TEMP"))
445
446 ;; Create a dummy home directory under the same name as on the
447 ;; host.
448 (mkdir-p (passwd:dir passwd))
449 (setenv "HOME" (passwd:dir passwd))
450
451 ;; Create a dummy /etc/passwd to satisfy applications that demand
452 ;; to read it, such as 'git clone' over SSH, a valid use-case when
453 ;; sharing the host's network namespace.
454 (mkdir-p "/etc")
455 (call-with-output-file "/etc/passwd"
456 (lambda (port)
457 (display (string-join (list (passwd:name passwd)
458 "x" ; but there is no shadow
459 "0" "0" ; user is now root
460 (passwd:gecos passwd)
461 (passwd:dir passwd)
462 bash)
463 ":")
464 port)
465 (newline port)))
466
467 ;; For convenience, start in the user's current working
468 ;; directory rather than the root directory.
469 (chdir cwd)
470
471 (primitive-exit/status
472 ;; A container's environment is already purified, so no need to
473 ;; request it be purified again.
474 (launch-environment command profile paths #f)))
475 #:namespaces (if network?
476 (delq 'net %namespaces) ; share host network
477 %namespaces)))))))
478
479 (define (environment-bash container? bootstrap? system)
480 "Return a monadic value in the store monad for the version of GNU Bash
481 needed in the environment for SYSTEM, if any. If CONTAINER? is #f, return #f.
482 If CONTAINER? and BOOTSTRAP?, return the store path for the bootstrap Bash.
483 Otherwise, return the derivation for the Bash package."
484 (with-monad %store-monad
485 (cond
486 ((and container? (not bootstrap?))
487 (package->derivation bash))
488 ;; Use the bootstrap Bash instead.
489 ((and container? bootstrap?)
490 (interned-file
491 (search-bootstrap-binary "bash" system)))
492 (else
493 (return #f)))))
494
495 (define (parse-args args)
496 "Parse the list of command line arguments ARGS."
497 (define (handle-argument arg result)
498 (alist-cons 'package (tag-package-arg result arg) result))
499
500 ;; The '--' token is used to separate the command to run from the rest of
501 ;; the operands.
502 (let-values (((args command) (break (cut string=? "--" <>) args)))
503 (let ((opts (parse-command-line args %options (list %default-options)
504 #:argument-handler handle-argument)))
505 (match command
506 (() opts)
507 (("--") opts)
508 (("--" command ...) (alist-cons 'exec command opts))))))
509
510 (define (assert-container-features)
511 "Check if containers can be created and exit with an informative error
512 message if any test fails."
513 (unless (user-namespace-supported?)
514 (report-error (_ "cannot create container: user namespaces unavailable\n"))
515 (leave (_ "is your kernel version < 3.10?\n")))
516
517 (unless (unprivileged-user-namespace-supported?)
518 (report-error (_ "cannot create container: unprivileged user cannot create user namespaces\n"))
519 (leave (_ "please set /proc/sys/kernel/unprivileged_userns_clone to \"1\"\n")))
520
521 (unless (setgroups-supported?)
522 (report-error (_ "cannot create container: /proc/self/setgroups does not exist\n"))
523 (leave (_ "is your kernel version < 3.19?\n"))))
524
525 ;; Entry point.
526 (define (guix-environment . args)
527 (with-error-handling
528 (let* ((opts (parse-args args))
529 (pure? (assoc-ref opts 'pure))
530 (container? (assoc-ref opts 'container?))
531 (network? (assoc-ref opts 'network?))
532 (bootstrap? (assoc-ref opts 'bootstrap?))
533 (system (assoc-ref opts 'system))
534 (command (or (assoc-ref opts 'exec)
535 ;; Spawn a shell if the user didn't specify
536 ;; anything in particular.
537 (if container?
538 ;; The user's shell is likely not available
539 ;; within the container.
540 '("/bin/sh")
541 (list %default-shell))))
542 (packages (options/resolve-packages opts))
543 (mappings (pick-all opts 'file-system-mapping))
544 (inputs (delete-duplicates
545 (append-map (match-lambda
546 (('ad-hoc-package package output)
547 (package+propagated-inputs package
548 output))
549 (('package package _)
550 (package-environment-inputs package)))
551 packages)))
552 (paths (delete-duplicates
553 (cons $PATH
554 (append-map (match-lambda
555 ((or ((? package? p) _ ...)
556 (? package? p))
557 (package-native-search-paths p))
558 (_ '()))
559 inputs))
560 eq?)))
561
562 (when container? (assert-container-features))
563
564 (with-store store
565 ;; Use the bootstrap Guile when requested.
566 (parameterize ((%graft? (assoc-ref opts 'graft?))
567 (%guile-for-build
568 (package-derivation
569 store
570 (if bootstrap?
571 %bootstrap-guile
572 (canonical-package guile-2.0)))))
573 (set-build-options-from-command-line store opts)
574 (run-with-store store
575 ;; Containers need a Bourne shell at /bin/sh.
576 (mlet* %store-monad ((bash (environment-bash container?
577 bootstrap?
578 system))
579 (prof-drv (inputs->profile-derivation
580 inputs system bootstrap?))
581 (profile -> (derivation->output-path prof-drv)))
582 ;; First build the inputs. This is necessary even for
583 ;; --search-paths. Additionally, we might need to build bash for
584 ;; a container.
585 (mbegin %store-monad
586 (build-environment (if (derivation? bash)
587 (list prof-drv bash)
588 (list prof-drv))
589 opts)
590 (cond
591 ((assoc-ref opts 'dry-run?)
592 (return #t))
593 ((assoc-ref opts 'search-paths)
594 (show-search-paths profile paths pure?)
595 (return #t))
596 (container?
597 (let ((bash-binary
598 (if bootstrap?
599 bash
600 (string-append (derivation->output-path bash)
601 "/bin/sh"))))
602 (launch-environment/container #:command command
603 #:bash bash-binary
604 #:user-mappings mappings
605 #:profile profile
606 #:paths paths
607 #:network? network?)))
608 (else
609 (return
610 (exit/status
611 (launch-environment/fork command profile
612 paths pure?)))))))))))))