scripts: Use 'define-command' and have 'guix help' use that.
[jackhill/guix/guix.git] / guix / scripts / pull.scm
index 2b7b991..bb1b560 100644 (file)
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013, 2014, 2015, 2017, 2018, 2019 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013, 2014, 2015, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2017 Marius Bakke <mbakke@fastmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -18,7 +18,7 @@
 ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (guix scripts pull)
-  #:use-module (guix ui)
+  #:use-module ((guix ui) #:hide (display-profile-content))
   #:use-module (guix colors)
   #:use-module (guix utils)
   #:use-module ((guix status) #:select (with-status-verbosity))
   #:use-module (guix memoization)
   #:use-module (guix monads)
   #:use-module (guix channels)
-  #:autoload   (guix inferior) (open-inferior)
+  #:autoload   (guix inferior) (open-inferior
+                                inferior-available-packages
+                                close-inferior)
   #:use-module (guix scripts build)
+  #:use-module (guix scripts describe)
   #:autoload   (guix build utils) (which)
+  #:use-module ((guix build syscalls)
+                #:select (with-file-lock/no-wait))
   #:use-module (guix git)
   #:use-module (git)
   #:use-module (gnu packages)
@@ -55,7 +60,9 @@
   #:use-module (ice-9 match)
   #:use-module (ice-9 vlist)
   #:use-module (ice-9 format)
-  #:export (display-profile-content
+  #:re-export (display-profile-content
+               channel-commit-hyperlink)
+  #:export (channel-list
             guix-pull))
 
 \f
   ;; Alist of default option values.
   `((system . ,(%current-system))
     (substitutes? . #t)
-    (build-hook? . #t)
+    (offload? . #t)
     (print-build-trace? . #t)
     (print-extended-build-trace? . #t)
     (multiplexed-build-output? . #t)
     (graft? . #t)
     (debug . 0)
-    (verbosity . 1)))
+    (verbosity . 1)
+    (authenticate-channels? . #t)
+    (validate-pull . ,ensure-forward-channel-update)))
 
 (define (show-help)
   (display (G_ "Usage: guix pull [OPTION]...
 Download and deploy the latest version of Guix.\n"))
-  (display (G_ "
-      --verbose          produce verbose output"))
   (display (G_ "
   -C, --channels=FILE    deploy the channels defined in FILE"))
   (display (G_ "
@@ -88,6 +95,11 @@ Download and deploy the latest version of Guix.\n"))
       --commit=COMMIT    download the specified COMMIT"))
   (display (G_ "
       --branch=BRANCH    download the tip of the specified BRANCH"))
+  (display (G_ "
+      --allow-downgrades allow downgrades to earlier channel revisions"))
+  (display (G_ "
+      --disable-authentication
+                         disable channel authentication"))
   (display (G_ "
   -N, --news             display news compared to the previous generation"))
   (display (G_ "
@@ -120,10 +132,7 @@ Download and deploy the latest version of Guix.\n"))
 
 (define %options
   ;; Specifications of the command-line options.
-  (cons* (option '("verbose") #f #f
-                 (lambda (opt name arg result)
-                   (alist-cons 'verbose? #t result)))
-         (option '(#\C "channels") #t #f
+  (cons* (option '(#\C "channels") #t #f
                  (lambda (opt name arg result)
                    (alist-cons 'channel-file arg result)))
          (option '(#\l "list-generations") #f #t
@@ -155,6 +164,13 @@ Download and deploy the latest version of Guix.\n"))
          (option '("branch") #t #f
                  (lambda (opt name arg result)
                    (alist-cons 'ref `(branch . ,arg) result)))
+         (option '("allow-downgrades") #f #f
+                 (lambda (opt name arg result)
+                   (alist-cons 'validate-pull warn-about-backward-updates
+                               result)))
+         (option '("disable-authentication") #f #f
+                 (lambda (opt name arg result)
+                   (alist-cons 'authenticate-channels? #f result)))
          (option '(#\p "profile") #t #f
                  (lambda (opt name arg result)
                    (alist-cons 'profile (canonicalize-profile arg)
@@ -165,7 +181,7 @@ Download and deploy the latest version of Guix.\n"))
                                (alist-delete 'system result eq?))))
          (option '(#\n "dry-run") #f #f
                  (lambda (opt name arg result)
-                   (alist-cons 'dry-run? #t (alist-cons 'graft? #f result))))
+                   (alist-cons 'dry-run? #t result)))
          (option '(#\v "verbosity") #t #f
                  (lambda (opt name arg result)
                    (let ((level (string->number* arg)))
@@ -185,6 +201,19 @@ Download and deploy the latest version of Guix.\n"))
 
          %standard-build-options))
 
+(define (warn-about-backward-updates channel start commit relation)
+  "Warn about non-forward updates of CHANNEL from START to COMMIT, without
+aborting."
+  (match relation
+    ((or 'ancestor 'self)
+     #t)
+    ('descendant
+     (warning (G_ "rolling back channel '~a' from ~a to ~a~%")
+              (channel-name channel) start commit))
+    ('unrelated
+     (warning (G_ "moving channel '~a' from ~a to unrelated commit ~a~%")
+              (channel-name channel) start commit))))
+
 (define* (display-profile-news profile #:key concise?
                                current-is-newer?)
   "Display what's up in PROFILE--new packages, and all that.  If
@@ -235,31 +264,48 @@ purposes."
   (define title
     (channel-news-entry-title entry))
 
-  (format port "  ~a~%"
-          (highlight
-           (string-trim-right
-            (texi->plain-text (or (assoc-ref title language)
-                                  (assoc-ref title (%default-message-language))
-                                  ""))))))
+  (let ((title (or (assoc-ref title language)
+                   (assoc-ref title (%default-message-language))
+                   "")))
+    (format port "  ~a~%"
+            (highlight
+             (string-trim-right
+              (catch 'parser-error
+                (lambda ()
+                  (texi->plain-text title))
+
+                ;; When Texinfo markup is invalid, display it as-is.
+                (const title)))))))
 
-(define (display-news-entry entry language port)
-  "Display ENTRY, a <channel-news-entry>, in LANGUAGE, a language code, to
-PORT."
+(define (display-news-entry entry channel language port)
+  "Display ENTRY, a <channel-news-entry> from CHANNEL, in LANGUAGE, a language
+code, to PORT."
   (define body
     (channel-news-entry-body entry))
 
+  (define commit
+    (channel-news-entry-commit entry))
+
   (display-news-entry-title entry language port)
-  (format port (G_ "    commit ~a~%")
-          (channel-news-entry-commit entry))
+  (format port (dim (G_ "    commit ~a~%"))
+          (if (supports-hyperlinks?)
+              (channel-commit-hyperlink channel commit)
+              commit))
   (newline port)
-  (format port "    ~a~%"
-          (indented-string
-           (parameterize ((%text-width (- (%text-width) 4)))
-             (string-trim-right
-              (texi->plain-text (or (assoc-ref body language)
-                                    (assoc-ref body (%default-message-language))
-                                    ""))))
-           4)))
+  (let ((body (or (assoc-ref body language)
+                  (assoc-ref body (%default-message-language))
+                  "")))
+    (format port "~a~%"
+            (indented-string
+             (parameterize ((%text-width (- (%text-width) 4)))
+               (string-trim-right
+                (catch 'parser-error
+                  (lambda ()
+                    (texi->plain-text body))
+                  (lambda _
+                    ;; When Texinfo markup is invalid, display it as-is.
+                    (fill-paragraph body (%text-width))))))
+             4))))
 
 (define* (display-channel-specific-news new old
                                         #:key (port (current-output-port))
@@ -282,7 +328,7 @@ to display."
                    (channel-name channel))
            (for-each (if concise?
                          (cut display-news-entry-title <> language port)
-                         (cut display-news-entry <> language port))
+                         (cut display-news-entry <> channel language port))
                      entries)
            (newline port)
            #t))))))
@@ -304,7 +350,7 @@ to display."
                (new
                 (let ((count (length new)))
                   (format (current-error-port)
-                          (N_ "  ~*One new channel:~%"
+                          (N_ "  ~a new channel:~%"
                               "  ~a new channels:~%" count)
                           count)
                   (for-each display-channel new))))
@@ -314,7 +360,7 @@ to display."
                (removed
                 (let ((count (length removed)))
                   (format (current-error-port)
-                          (N_ "  ~*One channel removed:~%"
+                          (N_ "  ~a channel removed:~%"
                               "  ~a channels removed:~%" count)
                           count)
                   (for-each display-channel removed))))
@@ -369,8 +415,7 @@ previous generation.  Return true if there are news to display."
 
   (display-channel-news profile))
 
-(define* (build-and-install instances profile
-                            #:key use-substitutes? verbose? dry-run?)
+(define* (build-and-install instances profile)
   "Build the tool from SOURCE, and install it in PROFILE.  When DRY-RUN? is
 true, display what would be built without actually building it."
   (define update-profile
@@ -383,29 +428,27 @@ true, display what would be built without actually building it."
   (mlet %store-monad ((manifest (channel-instances->manifest instances)))
     (mbegin %store-monad
       (update-profile profile manifest
-                      #:use-substitutes? use-substitutes?
-                      #:hooks %channel-profile-hooks
-                      #:dry-run? dry-run?)
-      (munless dry-run?
-        (return (newline))
-        (return
-         (let ((more? (list (display-profile-news profile #:concise? #t)
-                            (display-channel-news-headlines profile))))
-           (when (any ->bool more?)
-             (display-hint
-              (G_ "Run @command{guix pull --news} to read all the news.")))))
-        (if guix-command
-            (let ((new (map (cut string-append <> "/bin/guix")
-                            (list (user-friendly-profile profile)
-                                  profile))))
-              ;; Is the 'guix' command previously in $PATH the same as the new
-              ;; one?  If the answer is "no", then suggest 'hash guix'.
-              (unless (member guix-command new)
-                (display-hint (format #f (G_ "After setting @code{PATH}, run
+                      #:hooks %channel-profile-hooks)
+
+      (return
+       (let ((more? (list (display-profile-news profile #:concise? #t)
+                          (display-channel-news-headlines profile))))
+         (newline)
+         (when (any ->bool more?)
+           (display-hint
+            (G_ "Run @command{guix pull --news} to read all the news.")))))
+      (if guix-command
+          (let ((new (map (cut string-append <> "/bin/guix")
+                          (list (user-friendly-profile profile)
+                                profile))))
+            ;; Is the 'guix' command previously in $PATH the same as the new
+            ;; one?  If the answer is "no", then suggest 'hash guix'.
+            (unless (member guix-command new)
+              (display-hint (format #f (G_ "After setting @code{PATH}, run
 @command{hash guix} to make sure your shell refers to @file{~a}.")
-                                      (first new))))
-              (return #f))
-            (return #f))))))
+                                    (first new))))
+            (return #f))
+          (return #f)))))
 
 (define (honor-lets-encrypt-certificates! store)
   "Tell Guile-Git to use the Let's Encrypt certificates."
@@ -420,23 +463,6 @@ true, display what would be built without actually building it."
   (unless (honor-system-x509-certificates!)
     (honor-lets-encrypt-certificates! store)))
 
-(define (report-git-error error)
-  "Report the given Guile-Git error."
-  ;; Prior to Guile-Git commit b6b2760c2fd6dfaa5c0fedb43eeaff06166b3134,
-  ;; errors would be represented by integers.
-  (match error
-    ((? integer? error)                           ;old Guile-Git
-     (leave (G_ "Git error ~a~%") error))
-    ((? git-error? error)                         ;new Guile-Git
-     (leave (G_ "Git error: ~a~%") (git-error-message error)))))
-
-(define-syntax-rule (with-git-error-handling body ...)
-  (catch 'git-error
-    (lambda ()
-      body ...)
-    (lambda (key err)
-      (report-git-error err))))
-
 \f
 ;;;
 ;;; Profile.
@@ -481,6 +507,7 @@ true, display what would be built without actually building it."
   ;; workaround, skip this code when $SUDO_USER is set.  See
   ;; <https://bugs.gnu.org/36785>.
   (unless (or (getenv "SUDO_USER")
+              (not (file-exists? %user-profile-directory))
               (string=? %profile-directory
                         (dirname
                          (canonicalize-profile %user-profile-directory))))
@@ -503,46 +530,6 @@ true, display what would be built without actually building it."
 ;;; Queries.
 ;;;
 
-(define (display-profile-content profile number)
-  "Display the packages in PROFILE, generation NUMBER, in a human-readable
-way and displaying details about the channel's source code."
-  (display-generation profile number)
-  (for-each (lambda (entry)
-              (format #t "  ~a ~a~%"
-                      (manifest-entry-name entry)
-                      (manifest-entry-version entry))
-              (match (assq 'source (manifest-entry-properties entry))
-                (('source ('repository ('version 0)
-                                       ('url url)
-                                       ('branch branch)
-                                       ('commit commit)
-                                       _ ...))
-                 (format #t (G_ "    repository URL: ~a~%") url)
-                 (when branch
-                   (format #t (G_ "    branch: ~a~%") branch))
-                 (format #t (G_ "    commit: ~a~%") commit))
-                (_ #f)))
-
-            ;; Show most recently installed packages last.
-            (reverse
-             (manifest-entries
-              (profile-manifest (if (zero? number)
-                                    profile
-                                    (generation-file-name profile number)))))))
-
-(define (indented-string str indent)
-  "Return STR with each newline preceded by IDENT spaces."
-  (define indent-string
-    (make-list indent #\space))
-
-  (list->string
-   (string-fold-right (lambda (chr result)
-                        (if (eqv? chr #\newline)
-                            (cons chr (append indent-string result))
-                            (cons chr result)))
-                      '()
-                      str)))
-
 (define profile-package-alist
   (mlambda (profile)
     "Return a name/version alist representing the packages in PROFILE."
@@ -599,7 +586,7 @@ Return true when there is more package info to display."
   (define (pretty str column)
     (indented-string (fill-paragraph str (- (%text-width) 4)
                                      column)
-                     4))
+                     4 #:initial-indent? #f))
 
   (define concise/max-item-count
     ;; Maximum number of items to display when CONCISE? is true.
@@ -714,6 +701,9 @@ transformations specified in OPTS (resulting from '--url', '--commit', or
   (define default-file
     (string-append (config-directory) "/channels.scm"))
 
+  (define global-file
+    (string-append %sysconfdir "/guix/channels.scm"))
+
   (define (load-channels file)
     (let ((result (load* file (make-user-module '((guix channels))))))
       (if (and (list? result) (every channel? result))
@@ -725,6 +715,8 @@ transformations specified in OPTS (resulting from '--url', '--commit', or
            (load-channels file))
           ((file-exists? default-file)
            (load-channels default-file))
+          ((file-exists? global-file)
+           (load-channels global-file))
           (else
            %default-channels)))
 
@@ -759,55 +751,69 @@ Use '~/.config/guix/channels.scm' instead."))
         channels)))
 
 \f
-(define (guix-pull . args)
+(define-command (guix-pull . args)
+  (synopsis "pull the latest revision of Guix")
+
   (with-error-handling
     (with-git-error-handling
-     (let* ((opts     (parse-command-line args %options
-                                          (list %default-options)))
-            (channels (channel-list opts))
-            (profile  (or (assoc-ref opts 'profile) %current-profile)))
+     (let* ((opts         (parse-command-line args %options
+                                              (list %default-options)))
+            (substitutes? (assoc-ref opts 'substitutes?))
+            (dry-run?     (assoc-ref opts 'dry-run?))
+            (channels     (channel-list opts))
+            (profile      (or (assoc-ref opts 'profile) %current-profile))
+            (current-channels (profile-channels profile))
+            (validate-pull    (assoc-ref opts 'validate-pull))
+            (authenticate?    (assoc-ref opts 'authenticate-channels?)))
        (cond ((assoc-ref opts 'query)
               (process-query opts profile))
              ((assoc-ref opts 'generation)
               (process-generation-change opts profile))
              (else
               (with-store store
-                (ensure-default-profile)
                 (with-status-verbosity (assoc-ref opts 'verbosity)
                   (parameterize ((%current-system (assoc-ref opts 'system))
                                  (%graft? (assoc-ref opts 'graft?)))
-                    (set-build-options-from-command-line store opts)
-                    (honor-x509-certificates store)
-
-                    (let ((instances (latest-channel-instances store channels)))
-                      (format (current-error-port)
-                              (N_ "Building from this channel:~%"
-                                  "Building from these channels:~%"
-                                  (length instances)))
-                      (for-each (lambda (instance)
-                                  (let ((channel
-                                         (channel-instance-channel instance)))
-                                    (format (current-error-port)
-                                            "  ~10a~a\t~a~%"
-                                            (channel-name channel)
-                                            (channel-url channel)
-                                            (string-take
-                                             (channel-instance-commit instance)
-                                             7))))
-                                instances)
-                      (parameterize ((%guile-for-build
-                                      (package-derivation
-                                       store
-                                       (if (assoc-ref opts 'bootstrap?)
-                                           %bootstrap-guile
-                                           (canonical-package guile-2.2)))))
-                        (run-with-store store
-                          (build-and-install instances profile
-                                             #:dry-run?
-                                             (assoc-ref opts 'dry-run?)
-                                             #:use-substitutes?
-                                             (assoc-ref opts 'substitutes?)
-                                             #:verbose?
-                                             (assoc-ref opts 'verbose?))))))))))))))
+                    (with-build-handler (build-notifier #:use-substitutes?
+                                                        substitutes?
+                                                        #:verbosity
+                                                        (assoc-ref opts 'verbosity)
+                                                        #:dry-run? dry-run?)
+                      (set-build-options-from-command-line store opts)
+                      (ensure-default-profile)
+                      (honor-x509-certificates store)
+
+                      (let ((instances
+                             (latest-channel-instances store channels
+                                                       #:current-channels
+                                                       current-channels
+                                                       #:validate-pull
+                                                       validate-pull
+                                                       #:authenticate?
+                                                       authenticate?)))
+                        (format (current-error-port)
+                                (N_ "Building from this channel:~%"
+                                    "Building from these channels:~%"
+                                    (length instances)))
+                        (for-each (lambda (instance)
+                                    (let ((channel
+                                           (channel-instance-channel instance)))
+                                      (format (current-error-port)
+                                              "  ~10a~a\t~a~%"
+                                              (channel-name channel)
+                                              (channel-url channel)
+                                              (string-take
+                                               (channel-instance-commit instance)
+                                               7))))
+                                  instances)
+                        (parameterize ((%guile-for-build
+                                        (package-derivation
+                                         store
+                                         (if (assoc-ref opts 'bootstrap?)
+                                             %bootstrap-guile
+                                             (default-guile)))))
+                          (with-profile-lock profile
+                            (run-with-store store
+                              (build-and-install instances profile)))))))))))))))
 
 ;;; pull.scm ends here