guix home: Add 'container' command.

* guix/scripts/home.scm (show-help, %options): Add '--network',
'--share', and '--expose'.
(not-config?, user-shell, spawn-home-container): New procedures.
(%default-system-profile): New variable.
(perform-action): Add #:file-system-mappings, #:container-command,
and #:network?; honor them.
(process-action): Adjust accordingly.
(guix-home)[parse-sub-command]: Add "container".
[parse-args]: New procedure.
Use it instead of 'parse-command-line'.
* tests/guix-home.sh: Add tests.
* doc/guix.texi (Declaring the Home Environment): Mention 'guix home
container' as a way to test configuration.
(Invoking guix home): Document it.
This commit is contained in:
Ludovic Courtès 2022-03-13 22:44:54 +01:00
parent cff9fee82a
commit 094a2cfbe4
No known key found for this signature in database
GPG Key ID: 090B11993D9AEBB5
3 changed files with 347 additions and 38 deletions

View File

@ -38071,6 +38071,21 @@ be confused with Shepherd services (@pxref{Shepherd Services}). Using this exte
mechanism and some Scheme code that glues things together gives the user
the freedom to declare their own, very custom, home environments.
@cindex container, for @command{guix home}
Once the configuration looks good, you can first test it in a throw-away
``container'':
@example
guix home container config.scm
@end example
The command above spawns a shell where your home environment is running.
The shell runs in a container, meaning it's isolated from the rest of
the system, so it's a good way to try out your configuration---you can
see if configuration bits are missing or misbehaving, if daemons get
started, and so on. Once you exit that shell, you're back to the prompt
of your original shell ``in the real world''.
Once you have a configuration file that suits your needs, you can
reconfigure your home by running:
@ -38699,6 +38714,49 @@ As for @command{guix search}, the result is written in
@code{recutils} format, which makes it easy to filter the output
(@pxref{Top, GNU recutils databases,, recutils, GNU recutils manual}).
@cindex container, for @command{guix home}
@item container
Spawn a shell in an isolated environment---a
@dfn{container}---containing your home as specified by @var{file}.
For example, this is how you would start an interactive shell in a
container with your home:
@example
guix home container config.scm
@end example
This is a throw-away container where you can lightheartedly fiddle with
files; any changes made within the container, any process started---all
this disappears as soon as you exit that shell.
As with @command{guix shell}, several options control that container:
@table @option
@item --network
@itemx -N
Enable networking within the container (it is disabled by default).
@item --expose=@var{source}[=@var{target}]
@itemx --share=@var{source}[=@var{target}]
As with @command{guix shell}, make directory @var{source} of the host
system available as @var{target} inside the container---read-only if you
pass @option{--expose}, and writable if you pass @option{--share}
(@pxref{Invoking guix shell, @option{--expose} and @option{--share}}).
@end table
Additionally, you can run a command in that container, instead of
spawning an interactive shell. For instance, here is how you would
check which Shepherd services are started in a throw-away home
container:
@example
guix home container config.scm -- herd status
@end example
The command to run in the container must come after @code{--} (double
hyphen).
@item reconfigure
Build the home environment described in @var{file}, and switch to it.
Switching means that the activation script will be evaluated and (in

View File

@ -24,11 +24,24 @@
#:use-module (gnu packages admin)
#:use-module ((gnu services) #:hide (delete))
#:use-module (gnu packages)
#:autoload (gnu packages base) (coreutils)
#:autoload (gnu packages bash) (bash)
#:autoload (gnu packages gnupg) (guile-gcrypt)
#:autoload (gnu packages shells) (fish gash zsh)
#:use-module (gnu home)
#:use-module (gnu home services)
#:autoload (gnu home services shepherd) (home-shepherd-service-type
home-shepherd-configuration-services
shepherd-service-requirement)
#:autoload (guix modules) (source-module-closure)
#:autoload (gnu build linux-container) (call-with-container %namespaces)
#:autoload (gnu system linux-container) (eval/container)
#:autoload (gnu system file-systems) (file-system-mapping
file-system-mapping-source
file-system-mapping->bind-mount
specification->file-system-mapping
%network-file-mappings)
#:autoload (guix self) (make-config.scm)
#:use-module (guix channels)
#:use-module (guix derivations)
#:use-module (guix ui)
@ -55,6 +68,7 @@
#:use-module (srfi srfi-26)
#:use-module (srfi srfi-35)
#:use-module (srfi srfi-37)
#:use-module (srfi srfi-71)
#:use-module (ice-9 match)
#:export (guix-home))
@ -106,6 +120,16 @@ Some ACTIONS support additional ARGS.\n"))
(display (G_ "
--allow-downgrades for 'reconfigure', allow downgrades to earlier
channel revisions"))
(newline)
(display (G_ "
-N, --network allow containers to access the network"))
(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"))
(newline)
(display (G_ "
-v, --verbosity=LEVEL use the given verbosity LEVEL"))
(display (G_ "
@ -154,6 +178,21 @@ Some ACTIONS support additional ARGS.\n"))
(lambda (opt name arg result)
(alist-cons 'graph-backend arg result)))
;; Container options.
(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)))
%standard-build-options))
(define %default-options
@ -168,6 +207,146 @@ Some ACTIONS support additional ARGS.\n"))
(validate-reconfigure . ,ensure-forward-reconfigure)
(graph-backend . "graphviz")))
;;;
;;; Container.
;;;
(define not-config?
;; Select (guix …) and (gnu …) modules, except (guix config).
(match-lambda
(('guix 'config) #f)
(('guix _ ...) #t)
(('gnu _ ...) #t)
(_ #f)))
(define (user-shell)
(match (and=> (or (getenv "SHELL")
(passwd:shell (getpwuid (getuid))))
basename)
("zsh" (file-append zsh "/bin/zsh"))
("fish" (file-append fish "/bin/fish"))
("gash" (file-append gash "/bin/gash"))
(_ (file-append bash "/bin/bash"))))
(define %default-system-profile
;; The "system" profile available when running 'guix home container'. The
;; activation script currently expects to run "env -0" (XXX), so provide
;; Coreutils by default.
(delay (profile
(name "home-system-profile")
(content (packages->manifest (list coreutils))))))
(define* (spawn-home-container home
#:key
network?
(command '())
(mappings '())
(system-profile
(force %default-system-profile)))
"Spawn a login shell within a container running HOME, a home environment.
When COMMAND is a non-empty list, execute it in the container and exit
immediately. Return the exit status of the process in the container."
(define passwd (getpwuid (getuid)))
(define home-directory (or (getenv "HOME") (passwd:dir passwd)))
(define host (gethostname))
(define uid 1000)
(define gid 1000)
(define user-name (passwd:name passwd))
(define user-real-name (passwd:gecos passwd))
(define (optional-mapping mapping)
(and (file-exists? (file-system-mapping-source mapping))
mapping))
(define network-mappings
(if network?
(filter-map optional-mapping %network-file-mappings)
'()))
(eval/container
(with-extensions (list guile-gcrypt)
(with-imported-modules `(((guix config) => ,(make-config.scm))
,@(source-module-closure
'((gnu build accounts)
(guix profiles)
(guix build utils)
(guix build syscalls))
#:select? not-config?))
#~(begin
(use-modules (guix build utils)
(gnu build accounts)
((guix build syscalls)
#:select (set-network-interface-up)))
(define shell
#$(user-shell))
(define term
#$(getenv "TERM"))
(define passwd
(password-entry
(name #$user-name)
(real-name #$user-real-name)
(uid #$uid) (gid #$gid) (shell shell)
(directory #$home-directory)))
(define groups
(list (group-entry (name "users") (gid #$gid))
(group-entry (gid 65534) ;the overflow GID
(name "overflow"))))
;; (guix profiles) loads (guix utils), which calls 'getpw' from the
;; top level. Thus, arrange so that it's loaded after /etc/passwd
;; has been created.
(module-autoload! (current-module)
'(guix profiles) '(load-profile))
;; Create /etc/passwd for applications that need it, such as mcron.
(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)
(chmod port #o444))))
;; Set PATH for things that the activation script might expect, such
;; as "env".
(load-profile #$system-profile)
(mkdir-p #$home-directory)
(setenv "HOME" #$home-directory)
(setenv "GUIX_NEW_HOME" #$home)
(primitive-load (string-append #$home "/activate"))
(setenv "GUIX_NEW_HOME" #f)
(when term
;; Preserve TERM for proper interactive use.
(setenv "TERM" term))
(chdir #$home-directory)
;; Invoke SHELL with argv[0] starting with "-": that's how shells
;; figure out that they are login shells!
(execl shell (string-append "-" (basename shell))
#$@(match command
(() #~())
((_ ...)
#~("-c" #$(string-join command))))))))
#:namespaces (if network?
(delq 'net %namespaces) ; share host network
%namespaces)
#:mappings (append network-mappings mappings)
#:guest-uid uid
#:guest-gid gid))
;;;
;;; Actions.
@ -208,7 +387,12 @@ Some ACTIONS support additional ARGS.\n"))
derivations-only?
use-substitutes?
(graph-backend "graphviz")
(validate-reconfigure ensure-forward-reconfigure))
(validate-reconfigure ensure-forward-reconfigure)
;; Container options.
(file-system-mappings '())
(container-command '())
network?)
"Perform ACTION for home environment. "
(define println
@ -237,24 +421,37 @@ Some ACTIONS support additional ARGS.\n"))
(he-out-path -> (derivation->output-path he-drv)))
(if (or dry-run? derivations-only?)
(return #f)
(begin
(for-each (compose println derivation->output-path) drvs)
(case action
((reconfigure)
(let* ((number (generation-number %guix-home))
(generation (generation-file-name
%guix-home (+ 1 number))))
(case action
((reconfigure)
(let* ((number (generation-number %guix-home))
(generation (generation-file-name
%guix-home (+ 1 number))))
(switch-symlinks generation he-out-path)
(switch-symlinks %guix-home generation)
(setenv "GUIX_NEW_HOME" he-out-path)
(primitive-load (string-append he-out-path "/activate"))
(setenv "GUIX_NEW_HOME" #f)
(return he-out-path)))
(else
(newline)
(return he-out-path)))))))))
(switch-symlinks generation he-out-path)
(switch-symlinks %guix-home generation)
(setenv "GUIX_NEW_HOME" he-out-path)
(primitive-load (string-append he-out-path "/activate"))
(setenv "GUIX_NEW_HOME" #f)
(return he-out-path)))
((container)
(mlet %store-monad ((status (spawn-home-container
he
#:network? network?
#:mappings file-system-mappings
#:command
container-command)))
(match (status:exit-val status)
(0 (return #t))
((? integer? n) (return (exit n)))
(#f
(if (status:term-sig status)
(leave (G_ "process terminated with signal ~a~%")
(status:term-sig status))
(leave (G_ "process stopped with signal ~a~%")
(status:stop-sig status)))))))
(else
(for-each (compose println derivation->output-path) drvs)
(return he-out-path))))))))
(define (process-action action args opts)
"Process ACTION, a sub-command, with the arguments are listed in ARGS.
@ -293,6 +490,10 @@ resulting from command-line parsing."
(else
(leave (G_ "no configuration specified~%")))))))
(mappings (filter-map (match-lambda
(('file-system-mapping . mapping) mapping)
(_ #f))
opts))
(dry? (assoc-ref opts 'dry-run?)))
(with-store store
@ -315,7 +516,11 @@ resulting from command-line parsing."
#:validate-reconfigure
(assoc-ref opts 'validate-reconfigure)
#:graph-backend
(assoc-ref opts 'graph-backend))))))
(assoc-ref opts 'graph-backend)
#:network? (assoc-ref opts 'network?)
#:file-system-mappings mappings
#:container-command
(or (assoc-ref opts 'container-command) '()))))))
(warn-about-disk-space)))
@ -404,7 +609,7 @@ deploy the home environment described by these files.\n")
list-generations describe
delete-generations roll-back
switch-generation search
import)
import container)
(alist-cons 'action action result))
(else (leave (G_ "~a: unknown action~%") action))))))
@ -442,11 +647,28 @@ deploy the home environment described by these files.\n")
(fail))))
args))
(define (parse-args args)
;; Parse the list of command line arguments ARGS.
;; The '--' token is used to separate the command to run from the rest of
;; the operands.
(let* ((args rest (break (cut string=? "--" <>) args))
(opts (parse-command-line args %options (list %default-options)
#:argument-handler
parse-sub-command)))
(match rest
(() opts)
(("--") opts)
(("--" command ...)
(match (assoc-ref opts 'action)
('container
(alist-cons 'container-command command opts))
(_
(leave (G_ "~a: extraneous command~%")
(string-join command))))))))
(with-error-handling
(let* ((opts (parse-command-line args %options
(list %default-options)
#:argument-handler
parse-sub-command))
(let* ((opts (parse-args args))
(args (option-arguments opts))
(command (assoc-ref opts 'action)))
(parameterize ((%graft? (assoc-ref opts 'graft?)))

View File

@ -26,6 +26,16 @@ set -e
guix home --version
container_supported ()
{
if guile -c '((@ (guix scripts environment) assert-container-features))'
then
return 0
else
return 1
fi
}
NIX_STORE_DIR="$(guile -c '(use-modules (guix config))(display %storedir)')"
localstatedir="$(guile -c '(use-modules (guix config))(display %localstatedir)')"
GUIX_DAEMON_SOCKET="$localstatedir/guix/daemon-socket/socket"
@ -47,20 +57,6 @@ trap 'chmod -Rf +w "$test_directory"; rm -rf "$test_directory"' EXIT
(
cd "$test_directory" || exit 77
HOME="$test_directory"
export HOME
#
# Test 'guix home reconfigure'.
#
echo "# This file will be overridden and backed up." > "$HOME/.bashrc"
mkdir "$HOME/.config"
echo "This file will be overridden too." > "$HOME/.config/test.conf"
echo "This file will stay around." > "$HOME/.config/random-file"
echo -n "# dot-bashrc test file for guix home" > "dot-bashrc"
cat > "home.scm" <<'EOF'
(use-modules (guix gexp)
(gnu home)
@ -93,6 +89,8 @@ trap 'chmod -Rf +w "$test_directory"; rm -rf "$test_directory"' EXIT
"# the content of bashrc-test-config.sh"))))))))
EOF
echo -n "# dot-bashrc test file for guix home" > "dot-bashrc"
# Check whether the graph commands work as expected.
guix home extension-graph "home.scm" | grep 'label = "home-activation"'
guix home extension-graph "home.scm" | grep 'label = "home-symlink-manager"'
@ -101,6 +99,37 @@ EOF
# There are no Shepherd services so the one below must fail.
! guix home shepherd-graph "home.scm"
if container_supported
then
# Run the home in a container.
guix home container home.scm -- true
! guix home container home.scm -- false
test "$(guix home container home.scm -- echo '$HOME')" = "$HOME"
guix home container home.scm -- cat '~/.config/test.conf' | \
grep "the content of"
guix home container home.scm -- test -h '~/.bashrc'
test "$(guix home container home.scm -- id -u)" = 1000
! guix home container home.scm -- test -f '$HOME/sample/home.scm'
guix home container home.scm --expose="$PWD=$HOME/sample" -- \
test -f '$HOME/sample/home.scm'
! guix home container home.scm --expose="$PWD=$HOME/sample" -- \
rm -v '$HOME/sample/home.scm'
else
echo "'guix home container' test SKIPPED" >&2
fi
HOME="$test_directory"
export HOME
#
# Test 'guix home reconfigure'.
#
echo "# This file will be overridden and backed up." > "$HOME/.bashrc"
mkdir "$HOME/.config"
echo "This file will be overridden too." > "$HOME/.config/test.conf"
echo "This file will stay around." > "$HOME/.config/random-file"
guix home reconfigure "${test_directory}/home.scm"
test -d "${HOME}/.guix-home"
test -h "${HOME}/.bash_profile"