summaryrefslogtreecommitdiff
path: root/guix/scripts/environment.scm
diff options
context:
space:
mode:
Diffstat (limited to 'guix/scripts/environment.scm')
-rw-r--r--guix/scripts/environment.scm382
1 files changed, 314 insertions, 68 deletions
diff --git a/guix/scripts/environment.scm b/guix/scripts/environment.scm
index 2408420e18..2cc5f366a7 100644
--- a/guix/scripts/environment.scm
+++ b/guix/scripts/environment.scm
@@ -25,13 +25,19 @@
#:use-module (guix profiles)
#:use-module (guix search-paths)
#:use-module (guix utils)
+ #:use-module (guix build utils)
#:use-module (guix monads)
#:use-module ((guix gexp) #:select (lower-inputs))
#:use-module (guix scripts)
#:use-module (guix scripts build)
+ #:use-module (gnu build linux-container)
+ #:use-module (gnu system linux-container)
+ #:use-module (gnu system file-systems)
#:use-module (gnu packages)
+ #:use-module (gnu packages bash)
#:use-module (ice-9 format)
#:use-module (ice-9 match)
+ #:use-module (ice-9 rdelim)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-11)
#:use-module (srfi srfi-26)
@@ -60,6 +66,12 @@ OUTPUT) tuples."
(define %default-shell
(or (getenv "SHELL") "/bin/sh"))
+(define %network-configuration-files
+ '("/etc/resolv.conf"
+ "/etc/nsswitch.conf"
+ "/etc/services"
+ "/etc/hosts"))
+
(define (purify-environment)
"Unset almost all environment variables. A small number of variables such
as 'HOME' and 'USER' are left untouched."
@@ -124,6 +136,18 @@ COMMAND or an interactive shell in that environment.\n"))
--search-paths display needed environment variable definitions"))
(display (_ "
-s, --system=SYSTEM attempt to build for SYSTEM--e.g., \"i686-linux\""))
+ (display (_ "
+ -C, --container run command within an isolated container"))
+ (display (_ "
+ -N, --network allow containers to access the network"))
+ (display (_ "
+ --share=SPEC for containers, share writable host file system
+ according to SPEC"))
+ (display (_ "
+ --expose=SPEC for containers, expose read-only host file system
+ according to SPEC"))
+ (display (_ "
+ --bootstrap use bootstrap binaries to build the environment"))
(newline)
(show-build-options-help)
(newline)
@@ -136,12 +160,21 @@ COMMAND or an interactive shell in that environment.\n"))
(define %default-options
;; Default to opening a new shell.
- `((exec . (,%default-shell))
- (system . ,(%current-system))
+ `((system . ,(%current-system))
(substitutes? . #t)
(max-silent-time . 3600)
(verbosity . 0)))
+(define (tag-package-arg opts arg)
+ "Return a two-element list with the form (TAG ARG) that tags ARG with either
+'ad-hoc' in OPTS has the 'ad-hoc?' key set to #t, or 'inputs' otherwise."
+ ;; Normally, the transitive inputs to a package are added to an environment,
+ ;; but the ad-hoc? flag changes the meaning of a package argument such that
+ ;; the package itself is added to the environment instead.
+ (if (assoc-ref opts 'ad-hoc?)
+ `(ad-hoc-package ,arg)
+ `(package ,arg)))
+
(define %options
;; Specification of the command-line options.
(cons* (option '(#\h "help") #f #f
@@ -162,10 +195,14 @@ COMMAND or an interactive shell in that environment.\n"))
(alist-cons 'search-paths #t result)))
(option '(#\l "load") #t #f
(lambda (opt name arg result)
- (alist-cons 'load arg result)))
+ (alist-cons 'load
+ (tag-package-arg result arg)
+ result)))
(option '(#\e "expression") #t #f
(lambda (opt name arg result)
- (alist-cons 'expression arg result)))
+ (alist-cons 'expression
+ (tag-package-arg result arg)
+ result)))
(option '("ad-hoc") #f #f
(lambda (opt name arg result)
(alist-cons 'ad-hoc? #t result)))
@@ -176,6 +213,25 @@ COMMAND or an interactive shell in that environment.\n"))
(lambda (opt name arg result)
(alist-cons 'system arg
(alist-delete 'system result eq?))))
+ (option '(#\C "container") #f #f
+ (lambda (opt name arg result)
+ (alist-cons 'container? #t result)))
+ (option '(#\N "network") #f #f
+ (lambda (opt name arg result)
+ (alist-cons 'network? #t result)))
+ (option '("share") #t #f
+ (lambda (opt name arg result)
+ (alist-cons 'file-system-mapping
+ (specification->file-system-mapping arg #t)
+ result)))
+ (option '("expose") #t #f
+ (lambda (opt name arg result)
+ (alist-cons 'file-system-mapping
+ (specification->file-system-mapping arg #f)
+ result)))
+ (option '("bootstrap") #f #f
+ (lambda (opt name arg result)
+ (alist-cons 'bootstrap? #t result)))
%standard-build-options))
(define (pick-all alist key)
@@ -189,29 +245,40 @@ COMMAND or an interactive shell in that environment.\n"))
(_ memo)))
'() alist))
+(define (compact lst)
+ "Remove all #f elements from LST."
+ (filter identity lst))
+
(define (options/resolve-packages opts)
"Return OPTS with package specification strings replaced by actual
packages."
- (append-map (match-lambda
- (('package . (? string? spec))
- (let-values (((package output)
- (specification->package+output spec)))
- `((package ,package ,output))))
- (('expression . str)
- ;; Add all the outputs of the package STR evaluates to.
- (match (read/eval str)
- ((? package? package)
- (map (lambda (output)
- `(package ,package ,output))
- (package-outputs package)))))
- (('load . file)
- ;; Add all the outputs of the package defined in FILE.
- (let ((package (load* file (make-user-module '()))))
- (map (lambda (output)
- `(package ,package ,output))
- (package-outputs package))))
- (opt (list opt)))
- opts))
+ (define (package->outputs package mode)
+ (map (lambda (output)
+ (list mode package output))
+ (package-outputs package)))
+
+ (define (packages->outputs packages mode)
+ (match packages
+ ((? package? package)
+ (package->outputs package mode))
+ (((? package? packages) ...)
+ (append-map (cut package->outputs <> mode) packages))))
+
+ (compact
+ (append-map (match-lambda
+ (('package mode (? string? spec))
+ (let-values (((package output)
+ (specification->package+output spec)))
+ (list (list mode package output))))
+ (('expression mode str)
+ ;; Add all the outputs of the package STR evaluates to.
+ (packages->outputs (read/eval str) mode))
+ (('load mode file)
+ ;; Add all the outputs of the package defined in FILE.
+ (let ((module (make-user-module '())))
+ (packages->outputs (load* file module) mode)))
+ (_ '(#f)))
+ opts)))
(define (build-inputs inputs opts)
"Build the derivations in INPUTS, a list of (DERIVATION) or (DERIVATION
@@ -227,14 +294,145 @@ OUTPUT) tuples, using the build options in OPTS."
(if dry-run?
(return #f)
(mbegin %store-monad
- (set-build-options-from-command-line* opts)
(built-derivations derivations)
(return derivations))))))))
+(define requisites* (store-lift requisites))
+
+(define (inputs->requisites inputs)
+ "Convert INPUTS, a list of input tuples or store path strings, into a set of
+requisite store items i.e. the union closure of all the inputs."
+ (define (input->requisites input)
+ (requisites*
+ (match input
+ ((drv output)
+ (derivation->output-path drv output))
+ ((drv)
+ (derivation->output-path drv))
+ ((? direct-store-path? path)
+ path))))
+
+ (mlet %store-monad ((reqs (sequence %store-monad
+ (map input->requisites inputs))))
+ (return (delete-duplicates (concatenate reqs)))))
+
+(define (status->exit-code status)
+ "Compute the exit code made from STATUS, a value as returned by 'waitpid',
+and suitable for 'exit'."
+ ;; See <bits/waitstatus.h>.
+ (or (status:exit-val status)
+ (logior #x80 (status:term-sig status))))
+
+(define exit/status (compose exit status->exit-code))
+(define primitive-exit/status (compose primitive-exit status->exit-code))
+
+(define (launch-environment command inputs paths pure?)
+ "Run COMMAND in a new environment containing INPUTS, using the native search
+paths defined by the list PATHS. When PURE?, pre-existing environment
+variables are cleared before setting the new ones."
+ (create-environment inputs paths pure?)
+ (apply system* command))
+
+(define* (launch-environment/container #:key command bash user-mappings
+ inputs paths network?)
+ "Run COMMAND within a Linux container. The environment features INPUTS, a
+list of derivations to be shared from the host system. Environment variables
+are set according to PATHS, a list of native search paths. The global shell
+is BASH, a file name for a GNU Bash binary in the store. When NETWORK?,
+access to the host system network is permitted. USER-MAPPINGS, a list of file
+system mappings, contains the user-specified host file systems to mount inside
+the container."
+ (mlet %store-monad ((reqs (inputs->requisites
+ (cons (direct-store-path bash) inputs))))
+ (return
+ (let* ((cwd (getcwd))
+ ;; Bind-mount all requisite store items, user-specified mappings,
+ ;; /bin/sh, the current working directory, and possibly networking
+ ;; configuration files within the container.
+ (mappings
+ (append user-mappings
+ ;; Current working directory.
+ (list (file-system-mapping
+ (source cwd)
+ (target cwd)
+ (writable? #t)))
+ ;; When in Rome, do as Nix build.cc does: Automagically
+ ;; map common network configuration files.
+ (if network?
+ (filter-map (lambda (file)
+ (and (file-exists? file)
+ (file-system-mapping
+ (source file)
+ (target file)
+ (writable? #f))))
+ %network-configuration-files)
+ '())
+ ;; Mappings for the union closure of all inputs.
+ (map (lambda (dir)
+ (file-system-mapping
+ (source dir)
+ (target dir)
+ (writable? #f)))
+ reqs)))
+ (file-systems (append %container-file-systems
+ (map mapping->file-system mappings))))
+ (exit/status
+ (call-with-container (map file-system->spec file-systems)
+ (lambda ()
+ ;; Setup global shell.
+ (mkdir-p "/bin")
+ (symlink bash "/bin/sh")
+
+ ;; Setup directory for temporary files.
+ (mkdir-p "/tmp")
+ (for-each (lambda (var)
+ (setenv var "/tmp"))
+ ;; The same variables as in Nix's 'build.cc'.
+ '("TMPDIR" "TEMPDIR" "TMP" "TEMP"))
+
+ ;; From Nix build.cc:
+ ;;
+ ;; Set HOME to a non-existing path to prevent certain
+ ;; programs from using /etc/passwd (or NIS, or whatever)
+ ;; to locate the home directory (for example, wget looks
+ ;; for ~/.wgetrc). I.e., these tools use /etc/passwd if
+ ;; HOME is not set, but they will just assume that the
+ ;; settings file they are looking for does not exist if
+ ;; HOME is set but points to some non-existing path.
+ (setenv "HOME" "/homeless-shelter")
+
+ ;; For convenience, start in the user's current working
+ ;; directory rather than the root directory.
+ (chdir cwd)
+
+ (primitive-exit/status
+ ;; A container's environment is already purified, so no need to
+ ;; request it be purified again.
+ (launch-environment command inputs paths #f)))
+ #:namespaces (if network?
+ (delq 'net %namespaces) ; share host network
+ %namespaces)))))))
+
+(define (environment-bash container? bootstrap? system)
+ "Return a monadic value in the store monad for the version of GNU Bash
+needed in the environment for SYSTEM, if any. If CONTAINER? is #f, return #f.
+If CONTAINER? and BOOTSTRAP?, return the store path for the bootstrap Bash.
+Otherwise, return the derivation for the Bash package."
+ (with-monad %store-monad
+ (cond
+ ((and container? (not bootstrap?))
+ (package->derivation bash))
+ ;; Use the bootstrap Bash instead.
+ ((and container? bootstrap?)
+ (interned-file
+ (search-bootstrap-binary "bash" system)))
+ (else
+ (return #f)))))
+
(define (parse-args args)
"Parse the list of command line arguments ARGS."
(define (handle-argument arg result)
- (alist-cons 'package arg result))
+ (alist-cons 'package (tag-package-arg result arg) result))
;; The '--' token is used to separate the command to run from the rest of
;; the operands.
@@ -245,55 +443,103 @@ OUTPUT) tuples, using the build options in OPTS."
opts
(alist-cons 'exec command opts)))))
+(define (assert-container-features)
+ "Check if containers can be created and exit with an informative error
+message if any test fails."
+ (unless (user-namespace-supported?)
+ (report-error (_ "cannot create container: user namespaces unavailable\n"))
+ (leave (_ "is your kernel version < 3.10?\n")))
+
+ (unless (unprivileged-user-namespace-supported?)
+ (report-error (_ "cannot create container: unprivileged user cannot create user namespaces\n"))
+ (leave (_ "please set /proc/sys/kernel/unprivileged_userns_clone to \"1\"\n")))
+
+ (unless (setgroups-supported?)
+ (report-error (_ "cannot create container: /proc/self/setgroups does not exist\n"))
+ (leave (_ "is your kernel version < 3.19?\n"))))
+
;; Entry point.
(define (guix-environment . args)
(with-error-handling
- (let* ((opts (parse-args args))
- (pure? (assoc-ref opts 'pure))
- (ad-hoc? (assoc-ref opts 'ad-hoc?))
- (command (assoc-ref opts 'exec))
- (packages (pick-all (options/resolve-packages opts) 'package))
- (inputs (if ad-hoc?
- (append-map (match-lambda
- ((package output)
- (package+propagated-inputs package
- output)))
- packages)
- (append-map (compose bag-transitive-inputs
- package->bag
- first)
- packages)))
- (paths (delete-duplicates
- (cons $PATH
- (append-map (match-lambda
- ((label (? package? p) _ ...)
- (package-native-search-paths p))
- (_
- '()))
- inputs))
- eq?)))
+ (let* ((opts (parse-args args))
+ (pure? (assoc-ref opts 'pure))
+ (container? (assoc-ref opts 'container?))
+ (network? (assoc-ref opts 'network?))
+ (bootstrap? (assoc-ref opts 'bootstrap?))
+ (system (assoc-ref opts 'system))
+ (command (or (assoc-ref opts 'exec)
+ ;; Spawn a shell if the user didn't specify
+ ;; anything in particular.
+ (if container?
+ ;; The user's shell is likely not available
+ ;; within the container.
+ '("/bin/sh")
+ (list %default-shell))))
+ (packages (options/resolve-packages opts))
+ (mappings (pick-all opts 'file-system-mapping))
+ (inputs (delete-duplicates
+ (append-map (match-lambda
+ (('ad-hoc-package package output)
+ (package+propagated-inputs package
+ output))
+ (('package package output)
+ (bag-transitive-inputs
+ (package->bag package))))
+ packages)))
+ (paths (delete-duplicates
+ (cons $PATH
+ (append-map (match-lambda
+ ((label (? package? p) _ ...)
+ (package-native-search-paths p))
+ (_
+ '()))
+ inputs))
+ eq?)))
+
+ (when container? (assert-container-features))
+
(with-store store
+ (set-build-options-from-command-line store opts)
(run-with-store store
- (mlet %store-monad ((inputs (lower-inputs
- (map (match-lambda
+ (mlet* %store-monad ((inputs (lower-inputs
+ (map (match-lambda
((label item)
(list item))
((label item output)
(list item output)))
- inputs)
- #:system (assoc-ref opts 'system))))
+ inputs)
+ #:system system))
+ ;; Containers need a Bourne shell at /bin/sh.
+ (bash (environment-bash container?
+ bootstrap?
+ system)))
(mbegin %store-monad
- ;; First build INPUTS. This is necessary even for
- ;; --search-paths.
- (build-inputs inputs opts)
- (cond ((assoc-ref opts 'dry-run?)
- (return #t))
- ((assoc-ref opts 'search-paths)
- (show-search-paths inputs paths pure?)
- (return #t))
- (else
- (create-environment inputs paths pure?)
- (return
- (exit
- (status:exit-val
- (apply system* command)))))))))))))
+ ;; First build the inputs. This is necessary even for
+ ;; --search-paths. Additionally, we might need to build bash
+ ;; for a container.
+ (build-inputs (if (derivation? bash)
+ `((,bash "out") ,@inputs)
+ inputs)
+ opts)
+ (cond
+ ((assoc-ref opts 'dry-run?)
+ (return #t))
+ ((assoc-ref opts 'search-paths)
+ (show-search-paths inputs paths pure?)
+ (return #t))
+ (container?
+ (let ((bash-binary
+ (if bootstrap?
+ bash
+ (string-append (derivation->output-path bash)
+ "/bin/sh"))))
+ (launch-environment/container #:command command
+ #:bash bash-binary
+ #:user-mappings mappings
+ #:inputs inputs
+ #:paths paths
+ #:network? network?)))
+ (else
+ (return
+ (exit/status
+ (launch-environment command inputs paths pure?))))))))))))