summary refs log tree commit diff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2013-01-17 22:20:42 +0100
committerLudovic Courtès <ludo@gnu.org>2013-01-17 22:42:39 +0100
commit24e262f086980a13d9d0a27615ed7eaec4aacbff (patch)
tree4c519de6a66020766572e3d419cda80290c81634
parent8ca6cc4b451cab4b686d28f8aedef2c39fab6a17 (diff)
downloadguix-24e262f086980a13d9d0a27615ed7eaec4aacbff.tar.gz
guix-package: Add `--roll-back'.
Based on a patch by Nikita Karetnikov <nikita@karetnikov.org>.

* guix-package.in (profile-regexp): New procedure.
  (latest-profile-number): Remove `%profile-rx', and use
  `profile-regexp' instead.
  (profile-number, roll-back): New procedure.
  (show-help): Add `--roll-back'.
  (%options): Likewise.
  (guix-package)[process-actions]: First check whether `roll-back?' is
  among OPTS, and call `roll-back' if it is, followed by a recursive
  call to `process-actions'.  Emit the "nothing to be done" message only
  when INSTALL or REMOVE is non-empty.
* tests/guix-package.sh (readlink_base): New function.
  Add tests for `--roll-back'.
* doc/guix.texi (Invoking guix-package): Document `--roll-back'.
-rw-r--r--doc/guix.texi7
-rw-r--r--guix-package.in228
-rw-r--r--tests/guix-package.sh39
3 files changed, 180 insertions, 94 deletions
diff --git a/doc/guix.texi b/doc/guix.texi
index 27a00a6ed9..a41560e75c 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -490,6 +490,13 @@ Remove @var{package}.
 @itemx -u @var{regexp}
 Upgrade all the installed packages matching @var{regexp}.
 
+@item --roll-back
+Roll back to the previous @dfn{generation} of the profile---i.e., undo
+the last transaction.
+
+When combined with options such as @code{--install}, roll back occurs
+before any other actions.
+
 @item --profile=@var{profile}
 @itemx -p @var{profile}
 Use @var{profile} instead of the user's default profile.
diff --git a/guix-package.in b/guix-package.in
index c3fc397e5c..5dd4724b53 100644
--- a/guix-package.in
+++ b/guix-package.in
@@ -13,6 +13,7 @@ exec ${GUILE-@GUILE@} -L "@guilemoduledir@" -l "$0"    \
 !#
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2012, 2013 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013 Nikita Karetnikov <nikita@karetnikov.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -89,13 +90,14 @@ exec ${GUILE-@GUILE@} -L "@guilemoduledir@" -l "$0"    \
     (_
      (error "unsupported manifest format" manifest))))
 
+(define (profile-regexp profile)
+  "Return a regular expression that matches PROFILE's name and number."
+  (make-regexp (string-append "^" (regexp-quote (basename profile))
+                              "-([0-9]+)")))
+
 (define (latest-profile-number profile)
   "Return the identifying number of the latest generation of PROFILE.
 PROFILE is the name of the symlink to the current generation."
-  (define %profile-rx
-    (make-regexp (string-append "^" (regexp-quote (basename profile))
-                                "-([0-9]+)")))
-
   (define* (scandir name #:optional (select? (const #t))
                     (entry<? (@ (ice-9 i18n) string-locale<?)))
     ;; XXX: Bug-fix version introduced in Guile v2.0.6-62-g139ce19.
@@ -131,16 +133,17 @@ PROFILE is the name of the symlink to the current generation."
              (sort files entry<?))))
 
   (match (scandir (dirname profile)
-                  (cut regexp-exec %profile-rx <>))
+                  (cute regexp-exec (profile-regexp profile) <>))
     (#f                                         ; no profile directory
      0)
     (()                                         ; no profiles
      0)
     ((profiles ...)                             ; former profiles around
-     (let ((numbers (map (compose string->number
-                                  (cut match:substring <> 1)
-                                  (cut regexp-exec %profile-rx <>))
-                         profiles)))
+     (let ((numbers
+            (map (compose string->number
+                          (cut match:substring <> 1)
+                          (cut regexp-exec (profile-regexp profile) <>))
+                 profiles)))
        (fold (lambda (number highest)
                (if (> number highest)
                    number
@@ -179,6 +182,37 @@ all of PACKAGES, a list of name/version/output/path tuples."
                                      packages)
                                 #:modules '((guix build union))))
 
+(define (profile-number profile)
+  "Return PROFILE's number or 0.  An absolute file name must be used."
+  (or (and=> (false-if-exception (regexp-exec (profile-regexp profile)
+                                              (basename (readlink profile))))
+             (compose string->number (cut match:substring <> 1)))
+      0))
+
+(define (roll-back profile)
+  "Roll back to the previous generation of PROFILE."
+  ;; XXX: Get the previous generation number from the manifest?
+  (let* ((number (profile-number profile))
+         (previous-number (1- number))
+         (previous-profile (format #f "~a/~a-~a-link"
+                                   (dirname profile) profile
+                                   previous-number))
+         (manifest (string-append previous-profile "/manifest")))
+
+    (define (switch-link)
+      ;; Atomically switch PROFILE to the previous profile.
+      (let ((pivot (string-append previous-profile ".new")))
+        (format #t (_ "switching from generation ~a to ~a~%")
+                number previous-number)
+        (symlink previous-profile pivot)
+        (rename-file pivot profile)))
+
+    (if (= number 0)
+        (leave (_ "error: `~a' is not a valid profile~%") profile)
+        (if (file-exists? previous-profile)
+            (switch-link)
+            (leave (_ "error: no previous profile; not rolling back~%"))))))
+
 
 ;;;
 ;;; Command-line options.
@@ -197,6 +231,8 @@ Install, remove, or upgrade PACKAGES in a single transaction.\n"))
   -r, --remove=PACKAGE   remove PACKAGE"))
   (display (_ "
   -u, --upgrade=REGEXP   upgrade all the installed packages matching REGEXP"))
+  (display (_ "
+      --roll-back        roll back to the previous generation"))
   (newline)
   (display (_ "
   -p, --profile=PROFILE  use PROFILE instead of the user's default profile"))
@@ -237,6 +273,9 @@ Install, remove, or upgrade PACKAGES in a single transaction.\n"))
         (option '(#\r "remove") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'remove arg result)))
+        (option '("roll-back") #f #f
+                (lambda (opt name arg result)
+                  (alist-cons 'roll-back? #t result)))
         (option '(#\p "profile") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'profile arg
@@ -362,87 +401,96 @@ Install, remove, or upgrade PACKAGES in a single transaction.\n"))
 
   (define (process-actions opts)
     ;; Process any install/remove/upgrade action from OPTS.
-    (let* ((dry-run? (assoc-ref opts 'dry-run?))
-           (verbose? (assoc-ref opts 'verbose?))
-           (profile  (assoc-ref opts 'profile))
-           (install  (filter-map (match-lambda
-                                  (('install . (? store-path?))
-                                   #f)
-                                  (('install . package)
-                                   (find-package package))
-                                  (_ #f))
-                                 opts))
-           (drv      (filter-map (match-lambda
-                                  ((name version sub-drv
-                                         (? package? package))
-                                   (package-derivation (%store) package))
-                                  (_ #f))
-                                 install))
-           (install* (append
-                      (filter-map (match-lambda
-                                   (('install . (? store-path? path))
-                                    (let-values (((name version)
-                                                  (package-name->name+version
-                                                   (store-path-package-name
-                                                    path))))
-                                      `(,name ,version #f ,path)))
-                                   (_ #f))
-                                  opts)
-                      (map (lambda (tuple drv)
-                             (match tuple
-                               ((name version sub-drv _)
-                                (let ((output-path
-                                       (derivation-path->output-path
-                                        drv sub-drv)))
-                                  `(,name ,version ,sub-drv ,output-path)))))
-                           install drv)))
-           (remove   (filter-map (match-lambda
-                                  (('remove . package)
-                                   package)
-                                  (_ #f))
-                                 opts))
-           (packages (append install*
-                             (fold (lambda (package result)
-                                     (match package
-                                       ((name _ ...)
-                                        (alist-delete name result))))
-                                   (fold alist-delete
-                                         (manifest-packages
-                                          (profile-manifest profile))
-                                         remove)
-                                   install*))))
-
-      (when (equal? (assoc-ref opts 'profile) %current-profile)
-        (ensure-default-profile))
-
-      (show-what-to-build drv dry-run?)
-
-      (or dry-run?
-          (and (build-derivations (%store) drv)
-               (let* ((prof-drv (profile-derivation (%store) packages))
-                      (prof     (derivation-path->output-path prof-drv))
-                      (old-drv  (profile-derivation
-                                 (%store) (manifest-packages
-                                           (profile-manifest profile))))
-                      (old-prof (derivation-path->output-path old-drv))
-                      (number   (latest-profile-number profile))
-                      (name     (format #f "~a/~a-~a-link"
-                                        (dirname profile)
-                                        (basename profile) (+ 1 number))))
-                 (if (string=? old-prof prof)
-                     (format (current-error-port) (_ "nothing to be done~%"))
-                     (and (parameterize ((current-build-output-port
-                                          ;; Output something when Guile
-                                          ;; needs to be built.
-                                          (if (or verbose? (guile-missing?))
-                                              (current-error-port)
-                                              (%make-void-port "w"))))
-                            (build-derivations (%store) (list prof-drv)))
-                          (begin
-                            (symlink prof name)
-                            (when (file-exists? profile)
-                              (delete-file profile))
-                            (symlink name profile)))))))))
+
+    (define dry-run? (assoc-ref opts 'dry-run?))
+    (define verbose? (assoc-ref opts 'verbose?))
+    (define profile  (assoc-ref opts 'profile))
+
+    ;; First roll back if asked to.
+    (if (and (assoc-ref opts 'roll-back?) (not dry-run?))
+        (begin
+          (roll-back profile)
+          (process-actions (alist-delete 'roll-back? opts)))
+        (let* ((install  (filter-map (match-lambda
+                                      (('install . (? store-path?))
+                                       #f)
+                                      (('install . package)
+                                       (find-package package))
+                                      (_ #f))
+                                     opts))
+               (drv      (filter-map (match-lambda
+                                      ((name version sub-drv
+                                             (? package? package))
+                                       (package-derivation (%store) package))
+                                      (_ #f))
+                                     install))
+               (install* (append
+                          (filter-map (match-lambda
+                                       (('install . (? store-path? path))
+                                        (let-values (((name version)
+                                                      (package-name->name+version
+                                                       (store-path-package-name
+                                                        path))))
+                                          `(,name ,version #f ,path)))
+                                       (_ #f))
+                                      opts)
+                          (map (lambda (tuple drv)
+                                 (match tuple
+                                   ((name version sub-drv _)
+                                    (let ((output-path
+                                           (derivation-path->output-path
+                                            drv sub-drv)))
+                                      `(,name ,version ,sub-drv ,output-path)))))
+                               install drv)))
+               (remove   (filter-map (match-lambda
+                                      (('remove . package)
+                                       package)
+                                      (_ #f))
+                                     opts))
+               (packages (append install*
+                                 (fold (lambda (package result)
+                                         (match package
+                                           ((name _ ...)
+                                            (alist-delete name result))))
+                                       (fold alist-delete
+                                             (manifest-packages
+                                              (profile-manifest profile))
+                                             remove)
+                                       install*))))
+
+          (when (equal? profile %current-profile)
+            (ensure-default-profile))
+
+          (show-what-to-build drv dry-run?)
+
+          (or dry-run?
+              (and (build-derivations (%store) drv)
+                   (let* ((prof-drv (profile-derivation (%store) packages))
+                          (prof     (derivation-path->output-path prof-drv))
+                          (old-drv  (profile-derivation
+                                     (%store) (manifest-packages
+                                               (profile-manifest profile))))
+                          (old-prof (derivation-path->output-path old-drv))
+                          (number   (latest-profile-number profile))
+                          (name     (format #f "~a/~a-~a-link"
+                                            (dirname profile)
+                                            (basename profile) (+ 1 number))))
+                     (if (string=? old-prof prof)
+                         (when (or (pair? install) (pair? remove))
+                           (format (current-error-port)
+                                   (_ "nothing to be done~%")))
+                         (and (parameterize ((current-build-output-port
+                                              ;; Output something when Guile
+                                              ;; needs to be built.
+                                              (if (or verbose? (guile-missing?))
+                                                  (current-error-port)
+                                                  (%make-void-port "w"))))
+                                (build-derivations (%store) (list prof-drv)))
+                              (begin
+                                (symlink prof name)
+                                (when (file-exists? profile)
+                                  (delete-file profile))
+                                (symlink name profile))))))))))
 
   (define (process-query opts)
     ;; Process any query specified by OPTS.  Return #t when a query was
diff --git a/tests/guix-package.sh b/tests/guix-package.sh
index e5b8db7088..fd778f4f4f 100644
--- a/tests/guix-package.sh
+++ b/tests/guix-package.sh
@@ -22,6 +22,11 @@
 
 guix-package --version
 
+readlink_base ()
+{
+    basename `readlink "$1"`
+}
+
 profile="t-profile-$$"
 rm -f "$profile"
 
@@ -34,8 +39,7 @@ test -L "$profile" && test -L "$profile-1-link"
 test -f "$profile/bin/guile"
 
 # Installing the same package a second time does nothing.
-guix-package --bootstrap -p "$profile"						\
-    -i `guix-build -e '(@@ (distro packages base) %bootstrap-guile)'`
+guix-package --bootstrap -p "$profile" -i "$boot_guile"
 test -L "$profile" && test -L "$profile-1-link"
 ! test -f "$profile-2-link"
 test -f "$profile/bin/guile"
@@ -43,8 +47,8 @@ test -f "$profile/bin/guile"
 # Check whether we have network access.
 if guile -c '(getaddrinfo "www.gnu.org" "80" AI_NUMERICSERV)' 2> /dev/null
 then
-    guix-package --bootstrap -p "$profile"						\
-	-i `guix-build -e '(@@ (distro packages base) gnu-make-boot0)'`
+    boot_make="`guix-build -e '(@@ (distro packages base) gnu-make-boot0)'`"
+    guix-package --bootstrap -p "$profile" -i "$boot_make"
     test -L "$profile-2-link"
     test -f "$profile/bin/make" && test -f "$profile/bin/guile"
 
@@ -68,6 +72,29 @@ then
     guix-package --bootstrap -p "$profile" -r "guile-bootstrap"
     test -L "$profile-3-link"
     test -f "$profile/bin/make" && ! test -f "$profile/bin/guile"
+
+    # Roll back.
+    guix-package --roll-back -p "$profile"
+    test "`readlink_base "$profile"`" = "$profile-2-link"
+    test -x "$profile/bin/guile" && test -x "$profile/bin/make"
+    guix-package --roll-back -p "$profile"
+    test "`readlink_base "$profile"`" = "$profile-1-link"
+    test -x "$profile/bin/guile" && ! test -x "$profile/bin/make"
+
+    # Failed attempt to roll back because there's no previous generation.
+    if guix-package --roll-back -p "$profile";
+    then false; else true; fi
+
+    # Reinstall after roll-back to generation 1.
+    guix-package --bootstrap -p "$profile" -i "$boot_make"
+    test "`readlink_base "$profile"`" = "$profile-4-link"
+    test -x "$profile/bin/guile" && test -x "$profile/bin/make"
+
+    # Roll-back to generation 3[*], and install---all at once.
+    # [*] FIXME: Eventually, this should roll-back to generation 1.
+    guix-package --bootstrap -p "$profile" --roll-back -i "$boot_guile"
+    test "`readlink_base "$profile"`" = "$profile-5-link"
+    test -x "$profile/bin/guile" && test -x "$profile/bin/make"
 fi
 
 # Make sure the `:' syntax works.
@@ -88,3 +115,7 @@ mkdir -p "$HOME"
 guix-package --bootstrap -i "$boot_guile"
 test -L "$HOME/.guix-profile"
 test -f "$HOME/.guix-profile/bin/guile"
+
+# Failed attempt to roll back.
+if guix-package --bootstrap --roll-back;
+then false; else true; fi