;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2014, 2015, 2018 David Thompson <davet@gnu.org>
;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2018 Mike Gerwitz <mtg@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (guix scripts environment)
  #:use-module (guix ui)
  #:use-module (guix store)
  #:use-module (guix utils)
  #:use-module ((guix status) #:select (with-status-verbosity))
  #:use-module (guix grafts)
  #:use-module (guix derivations)
  #:use-module (guix packages)
  #:use-module (guix profiles)
  #:use-module (guix search-paths)
  #:use-module (guix build utils)
  #:use-module (guix monads)
  #:use-module ((guix gexp) #:select (lower-object))
  #:use-module (guix scripts)
  #:use-module (guix scripts build)
  #:use-module (guix transformations)
  #:use-module (gnu build linux-container)
  #:use-module (gnu build accounts)
  #:use-module ((guix build syscalls) #:select (set-network-interface-up))
  #:use-module (gnu system linux-container)
  #:use-module (gnu system file-systems)
  #:use-module (gnu packages)
  #:use-module (gnu packages bash)
  #:use-module ((gnu packages bootstrap)
                #:select (bootstrap-executable %bootstrap-guile))
  #:use-module (ice-9 match)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-11)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-37)
  #:use-module (srfi srfi-98)
  #:export (assert-container-features
            guix-environment))

;; Protect some env vars from purification.  Borrowed from nix-shell.
(define %precious-variables
  '("HOME" "USER" "LOGNAME" "DISPLAY" "TERM" "TZ" "PAGER"))

(define %default-shell
  (or (getenv "SHELL") "/bin/sh"))

(define (purify-environment white-list)
  "Unset all environment variables except those that match the regexps in
WHITE-LIST and those listed in %PRECIOUS-VARIABLES.  A small number of
variables such as 'HOME' and 'USER' are left untouched."
  (for-each unsetenv
            (remove (lambda (variable)
                      (or (member variable %precious-variables)
                          (find (cut regexp-exec <> variable)
                                white-list)))
                    (match (get-environment-variables)
                      (((names . _) ...)
                       names)))))

(define* (create-environment profile manifest
                             #:key pure? (white-list '()))
  "Set the environment variables specified by MANIFEST for PROFILE.  When
PURE?  is #t, unset the variables in the current environment except those that
match the regexps in WHITE-LIST.  Otherwise, augment existing environment
variables with additional search paths."
  (when pure?
    (purify-environment white-list))
  (for-each (match-lambda
              ((($ <search-path-specification> variable _ separator) . value)
               (let ((current (getenv variable)))
                 (setenv variable
                         (if (and current (not pure?))
                             (if separator
                                 (string-append value separator current)
                                 value)
                             value)))))
            (profile-search-paths profile manifest))

  ;; Give users a way to know that they're in 'guix environment', so they can
  ;; adjust 'PS1' accordingly, for instance.  Set it to PROFILE so users can
  ;; conveniently access its contents.
  (setenv "GUIX_ENVIRONMENT" profile))

(define* (show-search-paths profile manifest #:key pure?)
  "Display the search paths of MANIFEST applied to PROFILE.  When PURE? is #t,
do not augment existing environment variables with additional search paths."
  (for-each (match-lambda
              ((search-path . value)
               (display
                (search-path-definition search-path value
                                        #:kind (if pure? 'exact 'prefix)))
               (newline)))
            (profile-search-paths profile manifest)))

(define (input->manifest-entry input)
  "Return a manifest entry for INPUT, or #f if INPUT does not correspond to a
package."
  (match input
    ((_ (? package? package))
     (package->manifest-entry package))
    ((_ (? package? package) output)
     (package->manifest-entry package output))
    (_
     #f)))

(define (package-environment-inputs package)
  "Return a list of manifest entries corresponding to the transitive input
packages for PACKAGE."
  ;; Remove non-package inputs such as origin records.
  (filter-map input->manifest-entry
              (bag-transitive-inputs (package->bag package))))

(define (show-help)
  (display (G_ "Usage: guix environment [OPTION]... PACKAGE... [-- COMMAND...]
Build an environment that includes the dependencies of PACKAGE and execute
COMMAND or an interactive shell in that environment.\n"))
  (display (G_ "
  -e, --expression=EXPR  create environment for the package that EXPR
                         evaluates to"))
  (display (G_ "
  -l, --load=FILE        create environment for the package that the code within
                         FILE evaluates to"))
  (display (G_ "
  -m, --manifest=FILE    create environment with the manifest from FILE"))
  (display (G_ "
  -p, --profile=PATH     create environment from profile at PATH"))
  (display (G_ "
      --ad-hoc           include all specified packages in the environment instead
                         of only their inputs"))
  (display (G_ "
      --pure             unset existing environment variables"))
  (display (G_ "
  -E, --preserve=REGEXP  preserve environment variables that match REGEXP"))
  (display (G_ "
      --search-paths     display needed environment variable definitions"))
  (display (G_ "
  -s, --system=SYSTEM    attempt to build for SYSTEM--e.g., \"i686-linux\""))
  (display (G_ "
  -r, --root=FILE        make FILE a symlink to the result, and register it
                         as a garbage collector root"))
  (display (G_ "
  -C, --container        run command within an isolated container"))
  (display (G_ "
  -N, --network          allow containers to access the network"))
  (display (G_ "
  -P, --link-profile     link environment profile to ~/.guix-profile within
                         an isolated container"))
  (display (G_ "
  -u, --user=USER        instead of copying the name and home of the current
                         user into an isolated container, use the name USER
                         with home directory /home/USER"))
  (display (G_ "
      --no-cwd           do not share current working directory with an
                         isolated container"))

  (display (G_ "
      --share=SPEC       for containers, share writable host file system
                         according to SPEC"))
  (display (G_ "
      --expose=SPEC      for containers, expose read-only host file system
                         according to SPEC"))
  (display (G_ "
  -v, --verbosity=LEVEL  use the given verbosity LEVEL"))
  (display (G_ "
      --bootstrap        use bootstrap binaries to build the environment"))
  (newline)
  (show-build-options-help)
  (newline)
  (show-transformation-options-help)
  (newline)
  (display (G_ "
  -h, --help             display this help and exit"))
  (display (G_ "
  -V, --version          display version information and exit"))
  (newline)
  (show-bug-report-information))

(define %default-options
  `((system . ,(%current-system))
    (substitutes? . #t)
    (offload? . #t)
    (graft? . #t)
    (print-build-trace? . #t)
    (print-extended-build-trace? . #t)
    (multiplexed-build-output? . #t)
    (debug . 0)
    (verbosity . 1)))

(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
                 (lambda args
                   (show-help)
                   (exit 0)))
         (option '(#\V "version") #f #f
                 (lambda args
                   (show-version-and-exit "guix environment")))
         (option '("pure") #f #f
                 (lambda (opt name arg result)
                   (alist-cons 'pure #t result)))
         (option '(#\E "preserve") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'inherit-regexp
                               (make-regexp* arg)
                               result)))
         (option '("inherit") #t #f               ;deprecated
                 (lambda (opt name arg result)
                   (warning (G_ "'--inherit' is deprecated, \
use '--preserve' instead~%"))
                   (alist-cons 'inherit-regexp
                               (make-regexp* arg)
                               result)))
         (option '("search-paths") #f #f
                 (lambda (opt name arg result)
                   (alist-cons 'search-paths #t result)))
         (option '(#\l "load") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'load
                               (tag-package-arg result arg)
                               result)))
         (option '(#\e "expression") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'expression
                               (tag-package-arg result arg)
                               result)))
         (option '(#\m "manifest") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'manifest
                               arg
                               result)))
         (option '("ad-hoc") #f #f
                 (lambda (opt name arg result)
                   (alist-cons 'ad-hoc? #t result)))
         (option '(#\n "dry-run") #f #f
                 (lambda (opt name arg result)
                   (alist-cons 'dry-run? #t result)))
         (option '(#\s "system") #t #f
                 (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 '(#\P "link-profile") #f #f
                 (lambda (opt name arg result)
                   (alist-cons 'link-profile? #t result)))
         (option '(#\p "profile") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'profile arg
                               (alist-delete 'profile result eq?))))
         (option '(#\u "user") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'user arg
                               (alist-delete 'user result eq?))))
         (option '("no-cwd") #f #f
                 (lambda (opt name arg result)
                   (alist-cons 'no-cwd? #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 '(#\r "root") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'gc-root arg result)))
         (option '(#\v "verbosity") #t #f
                 (lambda (opt name arg result)
                   (let ((level (string->number* arg)))
                     (alist-cons 'verbosity level
                                 (alist-delete 'verbosity result)))))
         (option '("bootstrap") #f #f
                 (lambda (opt name arg result)
                   (alist-cons 'bootstrap? #t result)))

         (append %transformation-options
                 %standard-build-options)))

(define (pick-all alist key)
  "Return a list of values in ALIST associated with KEY."
  (define same-key? (cut eq? key <>))

  (fold (lambda (pair memo)
          (match pair
            (((? same-key? k) . v)
             (cons v memo))
            (_ memo)))
        '() alist))

(define (options/resolve-packages store opts)
  "Return OPTS with package specification strings replaced by manifest entries
for the corresponding packages."
  (define (manifest-entry=? e1 e2)
    (and (eq? (manifest-entry-item e1) (manifest-entry-item e2))
         (string=? (manifest-entry-output e1)
                   (manifest-entry-output e2))))

  (define transform
    (options->transformation opts))

  (define* (package->manifest-entry* package #:optional (output "out"))
    (package->manifest-entry (transform package) output))

  (define (packages->outputs packages mode)
    (match packages
      ((? package? package)
       (if (eq? mode 'ad-hoc-package)
           (list (package->manifest-entry* package))
           (package-environment-inputs package)))
      (((? package? package) (? string? output))
       (if (eq? mode 'ad-hoc-package)
           (list (package->manifest-entry* package output))
           (package-environment-inputs package)))
      ((lst ...)
       (append-map (cut packages->outputs <> mode) lst))))

  (manifest
   (delete-duplicates
    (append-map (match-lambda
                  (('package 'ad-hoc-package (? string? spec))
                   (let-values (((package output)
                                 (specification->package+output spec)))
                     (list (package->manifest-entry* package output))))
                  (('package 'package (? string? spec))
                   (package-environment-inputs
                    (transform (specification->package+output spec))))
                  (('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)))
                  (('manifest . file)
                   (let ((module (make-user-module '((guix profiles) (gnu)))))
                     (manifest-entries (load* file module))))
                  (_ '()))
                opts)
    manifest-entry=?)))

(define (manifest->derivation manifest system bootstrap?)
  "Return the derivation for a profile of MANIFEST.
BOOTSTRAP? specifies whether to use the bootstrap Guile to build the profile."
  (profile-derivation manifest
                      #:system system

                      ;; Packages can have conflicting inputs, or explicit
                      ;; inputs that conflict with implicit inputs (e.g., gcc,
                      ;; gzip, etc.).  Thus, do not error out when we
                      ;; encounter collision.
                      #:allow-collisions? #t

                      #:hooks (if bootstrap?
                                  '()
                                  %default-profile-hooks)
                      #:locales? (not bootstrap?)))

(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)
        (list (derivation->output-path drv output)))
       ((drv)
        (list (derivation->output-path drv)))
       ((? direct-store-path? path)
        (list path)))))

  (mlet %store-monad ((reqs (mapm %store-monad
                                  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 profile manifest
                             #:key pure? (white-list '()))
  "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, except those matching the
regexps in WHITE-LIST."
  ;; Properly handle SIGINT, so pressing C-c in an interactive terminal
  ;; application works.
  (sigaction SIGINT SIG_DFL)
  (create-environment profile manifest
                      #:pure? pure? #:white-list white-list)
  (match command
    ((program . args)
     (apply execlp program program args))))

(define* (launch-environment/fork command profile manifest
                                  #:key pure? (white-list '()))
  "Run COMMAND in a new process with an environment containing PROFILE, with
the search paths specified by MANIFEST.  When PURE?, pre-existing environment
variables are cleared before setting the new ones, except those matching the
regexps in WHITE-LIST."
  (match (primitive-fork)
    (0 (launch-environment command profile manifest
                           #:pure? pure?
                           #:white-list white-list))
    (pid (match (waitpid pid)
           ((_ . status) status)))))

(define* (launch-environment/container #:key command bash user user-mappings
                                       profile manifest link-profile? network?
                                       map-cwd? (white-list '()))
  "Run COMMAND within a container that features the software in PROFILE.
Environment variables are set according to the search paths of MANIFEST.
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.  If USER is not #f, each
target of USER-MAPPINGS will be re-written relative to '/home/USER', and USER
will be used for the passwd entry.  LINK-PROFILE? creates a symbolic link from
~/.guix-profile to the environment profile.

Preserve environment variables whose name matches the one of the regexps in
WHILE-LIST."
  (define (optional-mapping->fs mapping)
    (and (file-exists? (file-system-mapping-source mapping))
         (file-system-mapping->bind-mount mapping)))

  (mlet %store-monad ((reqs (inputs->requisites
                             (list (direct-store-path bash) profile))))
    (return
     (let* ((cwd      (getcwd))
            (home     (getenv "HOME"))
            (uid      (if user 1000 (getuid)))
            (gid      (if user 1000 (getgid)))
            (passwd   (let ((pwd (getpwuid (getuid))))
                        (password-entry
                         (name (or user (passwd:name pwd)))
                         (real-name (if user
                                        ""
                                        (passwd:gecos pwd)))
                         (uid uid) (gid gid) (shell bash)
                         (directory (if user
                                        (string-append "/home/" user)
                                        (passwd:dir pwd))))))
            (groups   (list (group-entry (name "users") (gid gid))
                            (group-entry (gid 65534) ;the overflow GID
                                         (name "overflow"))))
            (home-dir (password-entry-directory passwd))
            (logname  (password-entry-name passwd))
            (environ  (filter (match-lambda
                                ((variable . value)
                                 (find (cut regexp-exec <> variable)
                                       white-list)))
                              (get-environment-variables)))
            ;; 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
              (override-user-mappings
               user home
               (append user-mappings
                       ;; Share current working directory, unless asked not to.
                       (if map-cwd?
                           (list (file-system-mapping
                                  (source cwd)
                                  (target cwd)
                                  (writable? #t)))
                           '())))
              ;; 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
                                  (if network?
                                      (filter-map optional-mapping->fs
                                                  %network-file-mappings)
                                      '())
                                  (map file-system-mapping->bind-mount
                                       mappings))))
       (exit/status
        (call-with-container file-systems
          (lambda ()
            ;; Setup global shell.
            (mkdir-p "/bin")
            (symlink bash "/bin/sh")

            ;; Set a reasonable default PS1.
            (setenv "PS1" "\\u@\\h \\w [env]\\$ ")

            ;; 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"))

            ;; Some programs expect USER and/or LOGNAME to be set.
            (setenv "LOGNAME" logname)
            (setenv "USER" logname)

            ;; Create a dummy home directory.
            (mkdir-p home-dir)
            (setenv "HOME" home-dir)

            ;; If requested, link $GUIX_ENVIRONMENT to $HOME/.guix-profile;
            ;; this allows programs expecting that path to continue working as
            ;; expected within a container.
            (when link-profile? (link-environment profile home-dir))

            ;; Create a dummy /etc/passwd to satisfy applications that demand
            ;; to read it, such as 'git clone' over SSH, a valid use-case when
            ;; sharing the host's network namespace.
            (mkdir-p "/etc")
            (write-passwd (list passwd))
            (write-group groups)

            (unless network?
              ;; When isolated from the network, provide a minimal /etc/hosts
              ;; to resolve "localhost".
              (call-with-output-file "/etc/hosts"
                (lambda (port)
                  (display "127.0.0.1 localhost\n" port)))

              ;; Allow local AF_INET communications.
              (set-network-interface-up "lo"))

            ;; For convenience, start in the user's current working
            ;; directory or, if unmapped, the home directory.
            (chdir (if map-cwd?
                       (override-user-dir user home cwd)
                       home-dir))

            ;; Set environment variables that match WHITE-LIST.
            (for-each (match-lambda
                        ((variable . value)
                         (setenv variable value)))
                      environ)

            (primitive-exit/status
             ;; A container's environment is already purified, so no need to
             ;; request it be purified again.
             (launch-environment command
                                 (if link-profile?
                                     (string-append home-dir "/.guix-profile")
                                     profile)
                                 manifest #:pure? #f)))
          #:guest-uid uid
          #:guest-gid gid
          #:namespaces (if network?
                           (delq 'net %namespaces) ; share host network
                           %namespaces)))))))

(define (user-override-home user)
  "Return home directory for override user USER."
  (string-append "/home/" user))

(define (override-user-mappings user home mappings)
  "If a username USER is provided, rewrite each HOME prefix in file system
mappings MAPPINGS to a home directory determined by 'override-user-dir';
otherwise, return MAPPINGS."
  (if (not user)
      mappings
      (map (lambda (mapping)
             (let ((target (file-system-mapping-target mapping)))
               (if (string-prefix? home target)
                   (file-system-mapping
                    (source    (file-system-mapping-source mapping))
                    (target    (override-user-dir user home target))
                    (writable? (file-system-mapping-writable? mapping)))
                   mapping)))
           mappings)))

(define (override-user-dir user home dir)
  "If username USER is provided, overwrite string prefix HOME in DIR with a
directory determined by 'user-override-home'; otherwise, return DIR."
  (if (and user (string-prefix? home dir))
      (string-append (user-override-home user)
                     (substring dir (string-length home)))
      dir))

(define (link-environment profile home-dir)
  "Create a symbolic link from HOME-DIR/.guix-profile to PROFILE."
  (let ((profile-dir (string-append home-dir "/.guix-profile")))
    (catch 'system-error
      (lambda ()
        (symlink profile profile-dir))
      (lambda args
        (if (= EEXIST (system-error-errno args))
            (leave (G_ "cannot link profile: '~a' already exists within container~%")
                   profile-dir)
            (apply throw args))))))

(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?)
      (lower-object (bootstrap-executable "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 (tag-package-arg result arg) result))

  ;; The '--' token is used to separate the command to run from the rest of
  ;; the operands.
  (let-values (((args command) (break (cut string=? "--" <>) args)))
    (let ((opts (parse-command-line args %options (list %default-options)
                                    #:argument-handler handle-argument)))
      (match command
        (() opts)
        (("--") opts)
        (("--" command ...) (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 (G_ "cannot create container: user namespaces unavailable\n"))
    (leave (G_ "is your kernel version < 3.10?\n")))

  (unless (unprivileged-user-namespace-supported?)
    (report-error (G_ "cannot create container: unprivileged user cannot create user namespaces\n"))
    (leave (G_ "please set /proc/sys/kernel/unprivileged_userns_clone to \"1\"\n")))

  (unless (setgroups-supported?)
    (report-error (G_ "cannot create container: /proc/self/setgroups does not exist\n"))
    (leave (G_ "is your kernel version < 3.19?\n"))))

(define (register-gc-root target root)
  "Make ROOT an indirect root to TARGET.  This is procedure is idempotent."
  (let* ((root (if (string-prefix? "/" root)
                   root
                   (string-append (canonicalize-path (dirname root))
                                  "/" (basename root)))))
    (catch 'system-error
      (lambda ()
        (symlink target root)
        ((store-lift add-indirect-root) root))
      (lambda args
        (if (and (= EEXIST (system-error-errno args))
                 (equal? (false-if-exception (readlink root)) target))
            (with-monad %store-monad
              (return #t))
            (apply throw args))))))


;;;
;;; Entry point.
;;;

(define-command (guix-environment . args)
  (category development)
  (synopsis "spawn one-off software environments")

  (with-error-handling
    (let* ((opts       (parse-args args))
           (pure?      (assoc-ref opts 'pure))
           (container? (assoc-ref opts 'container?))
           (link-prof? (assoc-ref opts 'link-profile?))
           (network?   (assoc-ref opts 'network?))
           (no-cwd?    (assoc-ref opts 'no-cwd?))
           (user       (assoc-ref opts 'user))
           (bootstrap? (assoc-ref opts 'bootstrap?))
           (system     (assoc-ref opts 'system))
           (profile    (assoc-ref opts 'profile))
           (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))))
           (mappings   (pick-all opts 'file-system-mapping))
           (white-list (pick-all opts 'inherit-regexp)))

      (when container? (assert-container-features))

      (when (and (not container?) link-prof?)
        (leave (G_ "'--link-profile' cannot be used without '--container'~%")))
      (when (and (not container?) user)
        (leave (G_ "'--user' cannot be used without '--container'~%")))
      (when (and (not container?) no-cwd?)
        (leave (G_ "--no-cwd cannot be used without --container~%")))


      (with-store store
        (with-build-handler (build-notifier #:use-substitutes?
                                            (assoc-ref opts 'substitutes?)
                                            #:verbosity
                                            (assoc-ref opts 'verbosity)
                                            #:dry-run?
                                            (assoc-ref opts 'dry-run?))
          (with-status-verbosity (assoc-ref opts 'verbosity)
            (define manifest-from-opts
              (options/resolve-packages store opts))

            (define manifest
              (if profile
                  (profile-manifest profile)
                  manifest-from-opts))

            (when (and profile
                       (> (length (manifest-entries manifest-from-opts)) 0))
              (leave (G_ "'--profile' cannot be used with package options~%")))

            (set-build-options-from-command-line store opts)

            ;; Use the bootstrap Guile when requested.
            (parameterize ((%graft? (assoc-ref opts 'graft?))
                           (%guile-for-build
                            (package-derivation
                             store
                             (if bootstrap?
                                 %bootstrap-guile
                                 (default-guile)))))
              (run-with-store store
                ;; Containers need a Bourne shell at /bin/sh.
                (mlet* %store-monad ((bash       (environment-bash container?
                                                                   bootstrap?
                                                                   system))
                                     (prof-drv   (manifest->derivation
                                                  manifest system bootstrap?))
                                     (profile -> (if profile
                                                   (readlink* profile)
                                                   (derivation->output-path prof-drv)))
                                     (gc-root -> (assoc-ref opts 'gc-root)))

                  ;; First build the inputs.  This is necessary even for
                  ;; --search-paths.  Additionally, we might need to build bash for
                  ;; a container.
                  (mbegin %store-monad
                    (built-derivations (if (derivation? bash)
                                           (list prof-drv bash)
                                           (list prof-drv)))
                    (mwhen gc-root
                      (register-gc-root profile gc-root))

                    (cond
                     ((assoc-ref opts 'search-paths)
                      (show-search-paths profile manifest #:pure? pure?)
                      (return #t))
                     (container?
                      (let ((bash-binary
                             (if bootstrap?
                                 (derivation->output-path bash)
                                 (string-append (derivation->output-path bash)
                                                "/bin/sh"))))
                        (launch-environment/container #:command command
                                                      #:bash bash-binary
                                                      #:user user
                                                      #:user-mappings mappings
                                                      #:profile profile
                                                      #:manifest manifest
                                                      #:white-list white-list
                                                      #:link-profile? link-prof?
                                                      #:network? network?
                                                      #:map-cwd? (not no-cwd?))))

                     (else
                      (return
                       (exit/status
                        (launch-environment/fork command profile manifest
                                                 #:white-list white-list
                                                 #:pure? pure?)))))))))))))))
