summary refs log tree commit diff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2018-07-10 14:18:36 +0200
committerLudovic Courtès <ludo@gnu.org>2018-07-13 17:28:39 +0200
commit2ca299caf64489f4e1e665ec1158fb0309b0b565 (patch)
tree29a657a53161916cc64b6d0365c712501d666919
parentbc6e291ef0b3c71c07e50d88d7764e5dd334e8b1 (diff)
downloadguix-2ca299caf64489f4e1e665ec1158fb0309b0b565.tar.gz
Add (guix inferior) and (guix scripts repl).
* guix/inferior.scm, guix/scripts/repl.scm, tests/inferior.scm: New
files.
* Makefile.am (MODULES): Add 'guix/scripts/repl.scm' and
'guix/inferior.scm'.
(SCM_TESTS): Add 'tests/inferior.scm'.
* doc/guix.texi (Invoking guix repl): New node.
-rw-r--r--Makefile.am3
-rw-r--r--doc/guix.texi53
-rw-r--r--guix/inferior.scm197
-rw-r--r--guix/scripts/repl.scm199
-rw-r--r--tests/inferior.scm69
5 files changed, 521 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am
index 618d1653e4..134ab0f142 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -85,6 +85,7 @@ MODULES =					\
   guix/nar.scm					\
   guix/derivations.scm				\
   guix/grafts.scm				\
+  guix/inferior.scm				\
   guix/gnu-maintenance.scm			\
   guix/self.scm					\
   guix/upstream.scm				\
@@ -200,6 +201,7 @@ MODULES =					\
   guix/scripts/substitute.scm			\
   guix/scripts/authenticate.scm			\
   guix/scripts/refresh.scm			\
+  guix/scripts/repl.scm				\
   guix/scripts/system.scm			\
   guix/scripts/system/search.scm		\
   guix/scripts/lint.scm				\
@@ -357,6 +359,7 @@ SCM_TESTS =					\
   tests/profiles.scm				\
   tests/search-paths.scm			\
   tests/syscalls.scm				\
+  tests/inferior.scm				\
   tests/gremlin.scm				\
   tests/bournish.scm				\
   tests/lint.scm				\
diff --git a/doc/guix.texi b/doc/guix.texi
index eaec4c422b..7a5ddefd4e 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -166,6 +166,7 @@ Programming Interface
 * Derivations::                 Low-level interface to package derivations.
 * The Store Monad::             Purely functional interface to the store.
 * G-Expressions::               Manipulating build expressions.
+* Invoking guix repl::          Fiddling with Guix interactively.
 
 Defining Packages
 
@@ -3267,6 +3268,7 @@ package definitions.
 * Derivations::                 Low-level interface to package derivations.
 * The Store Monad::             Purely functional interface to the store.
 * G-Expressions::               Manipulating build expressions.
+* Invoking guix repl::          Fiddling with Guix interactively.
 @end menu
 
 @node Defining Packages
@@ -5544,6 +5546,57 @@ corresponding to @var{obj} for @var{system}, cross-compiling for
 has an associated gexp compiler, such as a @code{<package>}.
 @end deffn
 
+@node Invoking guix repl
+@section Invoking @command{guix repl}
+
+@cindex REPL, read-eval-print loop
+The @command{guix repl} command spawns a Guile @dfn{read-eval-print loop}
+(REPL) for interactive programming (@pxref{Using Guile Interactively,,, guile,
+GNU Guile Reference Manual}).  Compared to just launching the @command{guile}
+command, @command{guix repl} guarantees that all the Guix modules and all its
+dependencies are available in the search path.  You can use it this way:
+
+@example
+$ guix repl
+scheme@@(guile-user)> ,use (gnu packages base)
+scheme@@(guile-user)> coreutils
+$1 = #<package coreutils@@8.29 gnu/packages/base.scm:327 3e28300>
+@end example
+
+@cindex inferiors
+In addition, @command{guix repl} implements a simple machine-readable REPL
+protocol for use by @code{(guix inferior)}, a facility to interact with
+@dfn{inferiors}, separate processes running a potentially different revision
+of Guix.
+
+The available options are as follows:
+
+@table @code
+@item --type=@var{type}
+@itemx -t @var{type}
+Start a REPL of the given @var{TYPE}, which can be one of the following:
+
+@table @code
+@item guile
+This is default, and it spawns a standard full-featured Guile REPL.
+@item machine
+Spawn a REPL that uses the machine-readable protocol.  This is the protocol
+that the @code{(guix inferior)} module speaks.
+@end table
+
+@item --listen=@var{endpoint}
+By default, @command{guix repl} reads from standard input and writes to
+standard output.  When this option is passed, it will instead listen for
+connections on @var{endpoint}.  Here are examples of valid options:
+
+@table @code
+@item --listen=tcp:37146
+Accept connections on localhost on port 37146.
+
+@item --listen=unix:/tmp/socket
+Accept connections on the Unix-domain socket @file{/tmp/socket}.
+@end table
+@end table
 
 @c *********************************************************************
 @node Utilities
diff --git a/guix/inferior.scm b/guix/inferior.scm
new file mode 100644
index 0000000000..629c2c4313
--- /dev/null
+++ b/guix/inferior.scm
@@ -0,0 +1,197 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2018 Ludovic Courtès <ludo@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 inferior)
+  #:use-module (srfi srfi-9)
+  #:use-module (srfi srfi-9 gnu)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 popen)
+  #:export (inferior?
+            open-inferior
+            close-inferior
+            inferior-eval
+            inferior-object?
+
+            inferior-package?
+            inferior-package-name
+            inferior-package-version
+
+            inferior-packages
+            inferior-package-synopsis
+            inferior-package-description))
+
+;;; Commentary:
+;;;
+;;; This module provides a way to spawn Guix "inferior" processes and to talk
+;;; to them.  It allows us, from one instance of Guix, to interact with
+;;; another instance of Guix coming from a different commit.
+;;;
+;;; Code:
+
+;; Inferior Guix process.
+(define-record-type <inferior>
+  (inferior pid socket version)
+  inferior?
+  (pid      inferior-pid)
+  (socket   inferior-socket)
+  (version  inferior-version))                    ;REPL protocol version
+
+(define (inferior-pipe directory command)
+  "Return an input/output pipe on the Guix instance in DIRECTORY.  This runs
+'DIRECTORY/COMMAND repl' if it exists, or falls back to some other method if
+it's an old Guix."
+  (let ((pipe (with-error-to-port (%make-void-port "w")
+                (lambda ()
+                  (open-pipe* OPEN_BOTH
+                              (string-append directory "/" command)
+                              "repl" "-t" "machine")))))
+    (if (eof-object? (peek-char pipe))
+        (begin
+          (close-pipe pipe)
+
+          ;; Older versions of Guix didn't have a 'guix repl' command, so
+          ;; emulate it.
+          (open-pipe* OPEN_BOTH "guile"
+                      "-L" (string-append directory "/share/guile/site/"
+                                          (effective-version))
+                      "-C" (string-append directory "/share/guile/site/"
+                                          (effective-version))
+                      "-C" (string-append directory "/lib/guile/"
+                                          (effective-version) "/site-ccache")
+                      "-c"
+                      (object->string
+                       `(begin
+                          (primitive-load ,(search-path %load-path
+                                                        "guix/scripts/repl.scm"))
+                          ((@ (guix scripts repl) machine-repl))))))
+        pipe)))
+
+(define* (open-inferior directory #:key (command "bin/guix"))
+  "Open the inferior Guix in DIRECTORY, running 'DIRECTORY/COMMAND repl' or
+equivalent.  Return #f if the inferior could not be launched."
+  (define pipe
+    (inferior-pipe directory command))
+
+  (setvbuf pipe _IOLBF)
+  (match (read pipe)
+    (('repl-version 0 rest ...)
+     (let ((result (inferior 'pipe pipe (cons 0 rest))))
+       (inferior-eval '(use-modules (guix)) result)
+       (inferior-eval '(use-modules (gnu)) result)
+       (inferior-eval '(define %package-table (make-hash-table))
+                      result)
+       result))
+    (_
+     #f)))
+
+(define (close-inferior inferior)
+  "Close INFERIOR."
+  (close-pipe (inferior-socket inferior)))
+
+;; Non-self-quoting object of the inferior.
+(define-record-type <inferior-object>
+  (inferior-object address appearance)
+  inferior-object?
+  (address     inferior-object-address)
+  (appearance  inferior-object-appearance))
+
+(define (write-inferior-object object port)
+  (match object
+    (($ <inferior-object> _ appearance)
+     (format port "#<inferior-object ~a>" appearance))))
+
+(set-record-type-printer! <inferior-object> write-inferior-object)
+
+(define (inferior-eval exp inferior)
+  "Evaluate EXP in INFERIOR."
+  (define sexp->object
+    (match-lambda
+      (('value value)
+       value)
+      (('non-self-quoting address string)
+       (inferior-object address string))))
+
+  (write exp (inferior-socket inferior))
+  (newline (inferior-socket inferior))
+  (match (read (inferior-socket inferior))
+    (('values objects ...)
+     (apply values (map sexp->object objects)))
+    (('exception key objects ...)
+     (apply throw key (map sexp->object objects)))))
+
+
+;;;
+;;; Inferior packages.
+;;;
+
+(define-record-type <inferior-package>
+  (inferior-package inferior name version id)
+  inferior-package?
+  (inferior   inferior-package-inferior)
+  (name       inferior-package-name)
+  (version    inferior-package-version)
+  (id         inferior-package-id))
+
+(define (write-inferior-package package port)
+  (match package
+    (($ <inferior-package> _ name version)
+     (format port "#<inferior-package ~a@~a ~a>"
+             name version
+             (number->string (object-address package) 16)))))
+
+(set-record-type-printer! <inferior-package> write-inferior-package)
+
+(define (inferior-packages inferior)
+  "Return the list of packages known to INFERIOR."
+  (let ((result (inferior-eval
+                 '(fold-packages (lambda (package result)
+                                   (let ((id (object-address package)))
+                                     (hashv-set! %package-table id package)
+                                     (cons (list (package-name package)
+                                                 (package-version package)
+                                                 id)
+                                           result)))
+                                 '())
+                 inferior)))
+    (map (match-lambda
+           ((name version id)
+            (inferior-package inferior name version id)))
+         result)))
+
+(define (inferior-package-field package getter)
+  "Return the field of PACKAGE, an inferior package, accessed with GETTER."
+  (let ((inferior (inferior-package-inferior package))
+        (id       (inferior-package-id package)))
+    (inferior-eval `(,getter (hashv-ref %package-table ,id))
+                   inferior)))
+
+(define* (inferior-package-synopsis package #:key (translate? #t))
+  "Return the Texinfo synopsis of PACKAGE, an inferior package.  When
+TRANSLATE? is true, translate it to the current locale's language."
+  (inferior-package-field package
+                          (if translate?
+                              '(compose (@ (guix ui) P_) package-synopsis)
+                              'package-synopsis)))
+
+(define* (inferior-package-description package #:key (translate? #t))
+  "Return the Texinfo description of PACKAGE, an inferior package.  When
+TRANSLATE? is true, translate it to the current locale's language."
+  (inferior-package-field package
+                          (if translate?
+                              '(compose (@ (guix ui) P_) package-description)
+                              'package-description)))
diff --git a/guix/scripts/repl.scm b/guix/scripts/repl.scm
new file mode 100644
index 0000000000..b157833a49
--- /dev/null
+++ b/guix/scripts/repl.scm
@@ -0,0 +1,199 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2018 Ludovic Courtès <ludo@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 repl)
+  #:use-module (guix ui)
+  #:use-module (guix scripts)
+  #:use-module (guix utils)
+  #:use-module (guix packages)
+  #:use-module (gnu packages)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-37)
+  #:use-module (ice-9 match)
+  #:use-module (rnrs bytevectors)
+  #:autoload   (system repl repl) (start-repl)
+  #:autoload   (system repl server)
+                  (make-tcp-server-socket make-unix-domain-server-socket)
+  #:export (machine-repl
+            guix-repl))
+
+;;; Commentary:
+;;;
+;;; This command provides a Guile REPL
+
+(define %default-options
+  `((type . guile)))
+
+(define %options
+  (list (option '(#\h "help") #f #f
+                (lambda args
+                  (show-help)
+                  (exit 0)))
+        (option '(#\V "version") #f #f
+                (lambda args
+                  (show-version-and-exit "guix repl")))
+        (option '(#\t "type") #t #f
+                (lambda (opt name arg result)
+                  (alist-cons 'type (string->symbol arg) result)))
+        (option '("listen") #t #f
+                (lambda (opt name arg result)
+                  (alist-cons 'listen arg result)))))
+
+
+(define (show-help)
+  (display (G_ "Usage: guix repl [OPTIONS...]
+Start a Guile REPL in the Guix execution environment.\n"))
+  (display (G_ "
+  -t, --type=TYPE        start a REPL of the given TYPE"))
+  (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 (self-quoting? x)
+  "Return #t if X is self-quoting."
+  (letrec-syntax ((one-of (syntax-rules ()
+                            ((_) #f)
+                            ((_ pred rest ...)
+                             (or (pred x)
+                                 (one-of rest ...))))))
+    (one-of symbol? string? pair? null? vector?
+            bytevector? number? boolean?)))
+
+(define user-module
+  ;; Module where we execute user code.
+  (let ((module (resolve-module '(guix-user) #f #f #:ensure #t)))
+    (beautify-user-module! module)
+    module))
+
+(define* (machine-repl #:optional
+                       (input (current-input-port))
+                       (output (current-output-port)))
+  "Run a machine-usable REPL over ports INPUT and OUTPUT.
+
+The protocol of this REPL is meant to be machine-readable and provides proper
+support to represent multiple-value returns, exceptions, objects that lack a
+read syntax, and so on.  As such it is more convenient and robust than parsing
+Guile's REPL prompt."
+  (define (value->sexp value)
+    (if (self-quoting? value)
+        `(value ,value)
+        `(non-self-quoting ,(object-address value)
+                           ,(object->string value))))
+
+  (write `(repl-version 0 0) output)
+  (newline output)
+  (force-output output)
+
+  (let loop ()
+    (match (read input)
+      ((? eof-object?) #t)
+      (exp
+       (catch #t
+         (lambda ()
+           (let ((results (call-with-values
+                              (lambda ()
+
+                                (primitive-eval exp))
+                            list)))
+             (write `(values ,@(map value->sexp results))
+                    output)
+             (newline output)
+             (force-output output)))
+         (lambda (key . args)
+           (write `(exception ,key ,@(map value->sexp args)))
+           (newline output)
+           (force-output output)))
+       (loop)))))
+
+(define (call-with-connection spec thunk)
+  "Dynamically-bind the current input and output ports according to SPEC and
+call THUNK."
+  (if (not spec)
+      (thunk)
+
+      ;; Note: the "PROTO:" prefix in SPEC is here so that we can eventually
+      ;; parse things like "fd:123" in a non-ambiguous way.
+      (match (string-index spec #\:)
+        (#f
+         (leave (G_ "~A: invalid listen specification~%") spec))
+        (index
+         (let ((protocol (string-take spec index))
+               (address  (string-drop spec (+ index 1))))
+           (define socket
+             (match protocol
+               ("tcp"
+                (make-tcp-server-socket #:port (string->number address)))
+               ("unix"
+                (make-unix-domain-server-socket #:path address))
+               (_
+                (leave (G_ "~A: unsupported protocol family~%")
+                       protocol))))
+
+           (listen socket 10)
+           (let loop ()
+             (match (accept socket)
+               ((connection . address)
+                (if (= AF_UNIX (sockaddr:fam address))
+                    (info (G_ "accepted connection~%"))
+                    (info (G_ "accepted connection from ~a~%")
+                          (inet-ntop (sockaddr:fam address)
+                                     (sockaddr:addr address))))
+                (dynamic-wind
+                  (const #t)
+                  (lambda ()
+                    (parameterize ((current-input-port connection)
+                                   (current-output-port connection))
+                      (thunk)))
+                  (lambda ()
+                    (false-if-exception (close-port connection))
+                    (info (G_ "connection closed~%"))))))
+             (loop)))))))
+
+
+(define (guix-repl . args)
+  (define opts
+    ;; Return the list of package names.
+    (args-fold* args %options
+                (lambda (opt name arg result)
+                  (leave (G_ "~A: unrecognized option~%") name))
+                (lambda (arg result)
+                  (leave (G_ "~A: extraneous argument~%") arg))
+                %default-options))
+
+  (with-error-handling
+    (let ((type (assoc-ref opts 'type)))
+      (call-with-connection (assoc-ref opts 'listen)
+        (lambda ()
+          (case type
+            ((guile)
+             (save-module-excursion
+              (lambda ()
+                (set-current-module user-module)
+                (start-repl))))
+            ((machine)
+             (machine-repl))
+            (else
+             (leave (G_ "~a: unknown type of REPL~%") type))))))))
+
+;; Local Variables:
+;; eval: (put 'call-with-connection 'scheme-indent-function 1)
+;; End:
diff --git a/tests/inferior.scm b/tests/inferior.scm
new file mode 100644
index 0000000000..5e0f8ae66e
--- /dev/null
+++ b/tests/inferior.scm
@@ -0,0 +1,69 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2018 Ludovic Courtès <ludo@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 (test-inferior)
+  #:use-module (guix inferior)
+  #:use-module (guix packages)
+  #:use-module (gnu packages)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-64))
+
+(define %top-srcdir
+  (dirname (search-path %load-path "guix.scm")))
+
+(define %top-builddir
+  (dirname (search-path %load-compiled-path "guix.go")))
+
+
+(test-begin "inferior")
+
+(test-equal "open-inferior"
+  '(42 #t)
+  (let ((inferior (open-inferior %top-builddir
+                                 #:command "scripts/guix")))
+    (and (inferior? inferior)
+         (let ((a (inferior-eval '(apply * '(6 7)) inferior))
+               (b (inferior-eval '(@ (gnu packages base) coreutils)
+                                 inferior)))
+           (close-inferior inferior)
+           (list a (inferior-object? b))))))
+
+(test-equal "inferior-packages"
+  (take (sort (fold-packages (lambda (package lst)
+                               (alist-cons (package-name package)
+                                           (package-version package)
+                                           lst))
+                             '())
+              (lambda (x y)
+                (string<? (car x) (car y))))
+        10)
+  (let* ((inferior (open-inferior %top-builddir
+                                  #:command "scripts/guix"))
+         (packages (inferior-packages inferior)))
+    (and (every string? (map inferior-package-synopsis packages))
+         (begin
+           (close-inferior inferior)
+           (take (sort (map (lambda (package)
+                              (cons (inferior-package-name package)
+                                    (inferior-package-version package)))
+                            packages)
+                       (lambda (x y)
+                         (string<? (car x) (car y))))
+                 10)))))
+
+(test-end "inferior")