Subject: [bug#37978] [PATCH 1/3] guix: new command "guix time-machine"

From: Konrad Hinsen <[email-masked]>
Date: Fri, 25 Oct 2019 17:42:21 +0200

* guix/scripts/time-machine.scm: New file.
* Makefile.am: (MODULES): Add it.
* guix/scripts/pull.scm: Export function channel-list.
* guix/inferior.scm: New exported function cached-channel-instance.
* doc/guix.texi: Document "guix time-machine".
---
 Makefile.am                   |   1 +
 doc/guix.texi                 |  59 +++++++++++++++++++-
 guix/inferior.scm             |  38 +++++++++----
 guix/scripts/pull.scm         |   1 +
 guix/scripts/time-machine.scm | 102 ++++++++++++++++++++++++++++++++++
 5 files changed, 187 insertions(+), 14 deletions(-)
 create mode 100644 guix/scripts/time-machine.scm

diff --git a/Makefile.am b/Makefile.am
index b1f33946c5..b3f03d44c8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -278,6 +278,7 @@ MODULES =					\
   guix/scripts/container.scm			\
   guix/scripts/container/exec.scm		\
   guix/scripts/deploy.scm			\
+  guix/scripts/time-machine.scm			\
   guix.scm					\
   $(GNU_SYSTEM_MODULES)
 
diff --git a/doc/guix.texi b/doc/guix.texi
index 3b8e5935bb..3d4192cab2 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -197,6 +197,7 @@ Package Management
 * Invoking guix gc::            Running the garbage collector.
 * Invoking guix pull::          Fetching the latest Guix and distribution.
 * Channels::                    Customizing the package collection.
+* Invoking guix time-machine::  Running an older revision of Guix.
 * Inferiors::                   Interacting with another revision of Guix.
 * Invoking guix describe::      Display information about your Guix revision.
 * Invoking guix archive::       Exporting and importing store files.
@@ -2548,6 +2549,7 @@ guix install emacs-guix
 * Invoking guix gc::            Running the garbage collector.
 * Invoking guix pull::          Fetching the latest Guix and distribution.
 * Channels::                    Customizing the package collection.
+* Invoking guix time-machine::  Running an older revision of Guix.
 * Inferiors::                   Interacting with another revision of Guix.
 * Invoking guix describe::      Display information about your Guix revision.
 * Invoking guix archive::       Exporting and importing store files.
@@ -4150,7 +4152,10 @@ say, on another machine, by providing a channel specification in
 @end lisp
 
 The @command{guix describe --format=channels} command can even generate this
-list of channels directly (@pxref{Invoking guix describe}).
+list of channels directly (@pxref{Invoking guix describe}).  The resulting
+file can be used with the -C options of @command{guix pull}
+(@pxref{Invoking guix pull}) or @command{guix time-machine}
+(@pxref{Invoking guix time-machine}).
 
 At this point the two machines run the @emph{exact same Guix}, with access to
 the @emph{exact same packages}.  The output of @command{guix build gimp} on
@@ -4164,6 +4169,57 @@ artifacts with very fine grain, and to reproduce software environments at
 will---some sort of ``meta reproducibility'' capabilities, if you will.
 @xref{Inferiors}, for another way to take advantage of these super powers.
 
+@node Invoking guix time-machine
+@section Invoking @command{guix time-machine}
+
+@cindex @command{guix time-machine}
+@cindex pinning, channels
+@cindex replicating Guix
+@cindex reproducibility, of Guix
+
+The @command{guix time-machine} command provides access to other
+revisions of Guix, for example to install older versions of packages,
+or to reproduce a computation in an identical environment.  The revision
+of Guix to be used is defined by a commit or by a channel
+description file created by @command{guix describe}
+(@pxref{Invoking guix describe}).
+
+The general syntax is:
+
+@example
+guix time-machine @var{options}@dots{} -- @var{command} @var {arg}@dots{}
+@end example
+
+where @var{command} and @var{arg}@dots{} are passed unmodified to the
+@command{guix} command if the specified revision.  The @var{options} that define
+this revision are the same as for @command{guix pull} (@pxref{Invoking guix pull}):
+
+@table @code
+@item --url=@var{url}
+@itemx --commit=@var{commit}
+@itemx --branch=@var{branch}
+Use the @code{guix} channel from the specified @var{url}, at the
+given @var{commit} (a valid Git commit ID represented as a hexadecimal
+string), or @var{branch}.
+
+@item --channels=@var{file}
+@itemx -C @var{file}
+Read the list of channels from @var{file}.  @var{file} must contain
+Scheme code that evaluates to a list of channel objects.
+@xref{Channels} for more information.
+@end table
+
+As for @command{guix pull}, the absence of any options means that the
+the latest commit on the master branch will be used. The command
+
+@example
+guix time-machine -- build hello
+@end example
+
+will thus build the package @code{hello} as defined in the master branch,
+which is in general a newer revison of Guix than you have installed.
+Time travel works in both directions!
+
 @node Inferiors
 @section Inferiors
 
@@ -10582,7 +10638,6 @@ ClientPID: 19419
 ClientCommand: cuirass --cache-directory /var/cache/cuirass @dots{}
 @end example
 
-
 @node System Configuration
 @chapter System Configuration
 
diff --git a/guix/inferior.scm b/guix/inferior.scm
index b8e2f21f42..be50e0ec26 100644
--- a/guix/inferior.scm
+++ b/guix/inferior.scm
@@ -89,6 +89,7 @@
             gexp->derivation-in-inferior
 
             %inferior-cache-directory
+            cached-channel-instance
             inferior-for-channels))
 
 ;;; Commentary:
@@ -635,16 +636,13 @@ failing when GUIX is too old and lacks the 'guix repl' command."
   (make-parameter (string-append (cache-directory #:ensure? #f)
                                  "/inferiors")))
 
-(define* (inferior-for-channels channels
-                                #:key
-                                (cache-directory (%inferior-cache-directory))
-                                (ttl (* 3600 24 30)))
-  "Return an inferior for CHANNELS, a list of channels.  Use the cache at
-CACHE-DIRECTORY, where entries can be reclaimed after TTL seconds.  This
-procedure opens a new connection to the build daemon.
-
-This is a convenience procedure that people may use in manifests passed to
-'guix package -m', for instance."
+(define* (cached-channel-instance channels
+                                  #:key
+                                  (cache-directory (%inferior-cache-directory))
+                                  (ttl (* 3600 24 30)))
+  "Return a directory containing a guix filetree defined by CHANNELS, a list of channels.
+The directory is a subdirectory of CACHE-DIRECTORY, where entries can be reclaimed after TTL seconds.
+This procedure opens a new connection to the build daemon."
   (with-store store
     (let ()
       (define instances
@@ -680,7 +678,7 @@ This is a convenience procedure that people may use in manifests passed to
                                           (file-expiration-time ttl))
 
       (if (file-exists? cached)
-          (open-inferior cached)
+          cached
           (run-with-store store
             (mlet %store-monad ((profile
                                  (channel-instances->derivation instances)))
@@ -689,4 +687,20 @@ This is a convenience procedure that people may use in manifests passed to
                 (built-derivations (list profile))
                 (symlink* (derivation->output-path profile) cached)
                 (add-indirect-root* cached)
-                (return (open-inferior cached)))))))))
+                (return cached))))))))
+
+(define* (inferior-for-channels channels
+                                #:key
+                                (cache-directory (%inferior-cache-directory))
+                                (ttl (* 3600 24 30)))
+  "Return an inferior for CHANNELS, a list of channels.  Use the cache at
+CACHE-DIRECTORY, where entries can be reclaimed after TTL seconds.  This
+procedure opens a new connection to the build daemon.
+
+This is a convenience procedure that people may use in manifests passed to
+'guix package -m', for instance."
+  (define cached
+    (cached-channel-instance channels
+                             #:cache-directory cache-directory
+                             #:ttl ttl))
+  (open-inferior cached))
diff --git a/guix/scripts/pull.scm b/guix/scripts/pull.scm
index 92aac6066e..d6173a6acb 100644
--- a/guix/scripts/pull.scm
+++ b/guix/scripts/pull.scm
@@ -56,6 +56,7 @@
   #:use-module (ice-9 vlist)
   #:use-module (ice-9 format)
   #:export (display-profile-content
+            channel-list
             guix-pull))
 
 
diff --git a/guix/scripts/time-machine.scm b/guix/scripts/time-machine.scm
new file mode 100644
index 0000000000..a6598fb0f7
--- /dev/null
+++ b/guix/scripts/time-machine.scm
@@ -0,0 +1,102 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2019 Konrad Hinsen <[email-masked]>
+;;;
+;;; 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 time-machine)
+  #:use-module (guix ui)
+  #:use-module (guix scripts)
+  #:use-module (guix inferior)
+  #:use-module (guix channels)
+  #:use-module ((guix scripts pull) #:select (channel-list))
+  #: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)
+  #:export (guix-time-machine))
+
+
+;;;
+;;; Command-line options.
+;;;
+
+(define (show-help)
+  (display (G_ "Usage: guix time-machine [OPTION] -- COMMAND ARGS...
+Execute COMMAND ARGS... in an older version of Guix.\n"))
+  (display (G_ "
+  -C, --channels=FILE    deploy the channels defined in FILE"))
+  (display (G_ "
+      --url=URL          use the Git repository at URL"))
+  (display (G_ "
+      --commit=COMMIT    use the specified COMMIT"))
+  (display (G_ "
+      --branch=BRANCH    use the tip of the specified BRANCH"))
+  (display (G_ "
+  -h, --help             display this help and exit"))
+  (display (G_ "
+  -V, --version          display version information and exit"))
+  (newline)
+  (show-bug-report-information))
+
+(define %options
+  ;; Specifications of the command-line options.
+  (list (option '(#\C "channels") #t #f
+                (lambda (opt name arg result)
+                  (alist-cons 'channel-file arg result)))
+         (option '("url") #t #f
+                 (lambda (opt name arg result)
+                   (alist-cons 'repository-url arg
+                               (alist-delete 'repository-url result))))
+         (option '("commit") #t #f
+                 (lambda (opt name arg result)
+                   (alist-cons 'ref `(commit . ,arg) result)))
+         (option '("branch") #t #f
+                 (lambda (opt name arg result)
+                   (alist-cons 'ref `(branch . ,arg) result)))
+        (option '(#\h "help") #f #f
+                (lambda args
+                  (show-help)
+                  (exit 0)))
+        (option '(#\V "version") #f #f
+                (lambda args
+                  (show-version-and-exit "guix time-machine")))))
+
+(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-values (((args command) (break (cut string=? "--" <>) args)))
+    (let ((opts (parse-command-line args %options '(()) #:build-options? #f)))
+      (match command
+        (() opts)
+        (("--") opts)
+        (("--" command ...) (alist-cons 'exec command opts))))))
+
+
+;;;
+;;; Entry point.
+;;;
+
+(define (guix-time-machine . args)
+  (with-error-handling
+    (let* ((opts         (parse-args args))
+           (channels     (channel-list opts))
+           (command-line (assoc-ref opts 'exec)))
+      (when command-line
+        (let* ((directory  (cached-channel-instance channels))
+               (executable (string-append directory "/bin/guix")))
+          (apply execl (cons* executable executable command-line)))))))
-- 
2.24.0