guix build: Accept multiple '-s' options.
[jackhill/guix/guix.git] / guix / scripts / build.scm
index 3fa3c2c..ba143ad 100644 (file)
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2013 Mark H Weaver <mhw@netris.org>
 ;;;
 ;;; This file is part of GNU Guix.
   #:use-module (srfi srfi-37)
   #:autoload   (gnu packages) (specification->package %package-module-path)
   #:autoload   (guix download) (download-to-store)
+  #:autoload   (guix git-download) (git-reference?)
+  #:autoload   (guix git) (git-checkout?)
+  #:use-module ((guix status) #:select (with-status-verbosity))
+  #:use-module ((guix progress) #:select (current-terminal-columns))
+  #:use-module ((guix build syscalls) #:select (terminal-columns))
   #:export (%standard-build-options
             set-build-options-from-command-line
             set-build-options-from-command-line*
@@ -60,7 +65,7 @@
 
 (define %default-log-urls
   ;; Default base URLs for build logs.
-  '("http://hydra.gnu.org/log"))
+  '("http://ci.guix.info/log"))
 
 ;; XXX: The following procedure cannot be in (guix store) because of the
 ;; dependency on (guix derivations).
@@ -114,7 +119,7 @@ found.  Return #f if no build log was found."
   (let* ((root (if (string-prefix? "/" root)
                    root
                    (string-append (canonicalize-path (dirname root))
-                                  "/" root))))
+                                  "/" (basename root)))))
     (catch 'system-error
       (lambda ()
         (match paths
@@ -221,18 +226,21 @@ matching URIs given in SOURCES."
          obj)))))
 
 (define (evaluate-replacement-specs specs proc)
-  "Parse SPECS, a list of strings like \"guile=guile@2.1\", and invoke PROC on
-each package pair specified by SPECS.  Return the resulting list.  Raise an
-error if an element of SPECS uses invalid syntax, or if a package it refers to
-could not be found."
+  "Parse SPECS, a list of strings like \"guile=guile@2.1\" and return a list
+of package spec/procedure pairs as expected by 'package-input-rewriting/spec'.
+PROC is called with the package to be replaced and its replacement according
+to SPECS.  Raise an error if an element of SPECS uses invalid syntax, or if a
+package it refers to could not be found."
   (define not-equal
     (char-set-complement (char-set #\=)))
 
   (map (lambda (spec)
          (match (string-tokenize spec not-equal)
-           ((old new)
-            (proc (specification->package old)
-                  (specification->package new)))
+           ((spec new)
+            (cons spec
+                  (let ((new (specification->package new)))
+                    (lambda (old)
+                      (proc old new)))))
            (x
             (leave (G_ "invalid replacement specification: ~s~%") spec))))
        specs))
@@ -243,8 +251,10 @@ dependencies according to REPLACEMENT-SPECS.  REPLACEMENT-SPECS is a list of
 strings like \"guile=guile@2.1\" meaning that, any dependency on a package
 called \"guile\" must be replaced with a dependency on a version 2.1 of
 \"guile\"."
-  (let* ((replacements (evaluate-replacement-specs replacement-specs cons))
-         (rewrite      (package-input-rewriting replacements)))
+  (let* ((replacements (evaluate-replacement-specs replacement-specs
+                                                   (lambda (old new)
+                                                     new)))
+         (rewrite      (package-input-rewriting/spec replacements)))
     (lambda (store obj)
       (if (package? obj)
           (rewrite obj)
@@ -255,18 +265,122 @@ called \"guile\" must be replaced with a dependency on a version 2.1 of
 dependencies according to REPLACEMENT-SPECS.  REPLACEMENT-SPECS is a list of
 strings like \"gnutls=gnutls@3.5.4\" meaning that packages are built using the
 current 'gnutls' package, after which version 3.5.4 is grafted onto them."
-  (define (replacement-pair old new)
-    (cons old
-          (package (inherit old) (replacement new))))
+  (define (set-replacement old new)
+    (package (inherit old) (replacement new)))
 
   (let* ((replacements (evaluate-replacement-specs replacement-specs
-                                                   replacement-pair))
-         (rewrite      (package-input-rewriting replacements)))
+                                                   set-replacement))
+         (rewrite      (package-input-rewriting/spec replacements)))
+    (lambda (store obj)
+      (if (package? obj)
+          (rewrite obj)
+          obj))))
+
+(define %not-equal
+  (char-set-complement (char-set #\=)))
+
+(define (package-git-url package)
+  "Return the URL of the Git repository for package, or raise an error if
+the source of PACKAGE is not fetched from a Git repository."
+  (let ((source (package-source package)))
+    (cond ((and (origin? source)
+                (git-reference? (origin-uri source)))
+           (git-reference-url (origin-uri source)))
+          ((git-checkout? source)
+           (git-checkout-url source))
+          (else
+           (leave (G_ "the source of ~a is not a Git reference~%")
+                  (package-full-name package))))))
+
+(define (evaluate-git-replacement-specs specs proc)
+  "Parse SPECS, a list of strings like \"guile=stable-2.2\", and return a list
+of package pairs, where (PROC PACKAGE URL BRANCH-OR-COMMIT) returns the
+replacement package.  Raise an error if an element of SPECS uses invalid
+syntax, or if a package it refers to could not be found."
+  (map (lambda (spec)
+         (match (string-tokenize spec %not-equal)
+           ((spec branch-or-commit)
+            (define (replace old)
+              (let* ((source (package-source old))
+                     (url    (package-git-url old)))
+                (proc old url branch-or-commit)))
+
+            (cons spec replace))
+           (x
+            (leave (G_ "invalid replacement specification: ~s~%") spec))))
+       specs))
+
+(define (transform-package-source-branch replacement-specs)
+  "Return a procedure that, when passed a package, replaces its direct
+dependencies according to REPLACEMENT-SPECS.  REPLACEMENT-SPECS is a list of
+strings like \"guile-next=stable-3.0\" meaning that packages are built using
+'guile-next' from the latest commit on its 'stable-3.0' branch."
+  (define (replace old url branch)
+    (package
+      (inherit old)
+      (version (string-append "git." (string-map (match-lambda
+                                                   (#\/ #\-)
+                                                   (chr chr))
+                                                 branch)))
+      (source (git-checkout (url url) (branch branch)
+                            (recursive? #t)))))
+
+  (let* ((replacements (evaluate-git-replacement-specs replacement-specs
+                                                       replace))
+         (rewrite      (package-input-rewriting/spec replacements)))
+    (lambda (store obj)
+      (if (package? obj)
+          (rewrite obj)
+          obj))))
+
+(define (transform-package-source-commit replacement-specs)
+  "Return a procedure that, when passed a package, replaces its direct
+dependencies according to REPLACEMENT-SPECS.  REPLACEMENT-SPECS is a list of
+strings like \"guile-next=cabba9e\" meaning that packages are built using
+'guile-next' from commit 'cabba9e'."
+  (define (replace old url commit)
+    (package
+      (inherit old)
+      (version (string-append "git."
+                              (if (< (string-length commit) 7)
+                                  commit
+                                  (string-take commit 7))))
+      (source (git-checkout (url url) (commit commit)
+                            (recursive? #t)))))
+
+  (let* ((replacements (evaluate-git-replacement-specs replacement-specs
+                                                       replace))
+         (rewrite      (package-input-rewriting/spec replacements)))
     (lambda (store obj)
       (if (package? obj)
           (rewrite obj)
           obj))))
 
+(define (transform-package-source-git-url replacement-specs)
+  "Return a procedure that, when passed a package, replaces its dependencies
+according to REPLACEMENT-SPECS.  REPLACEMENT-SPECS is a list of strings like
+\"guile-json=https://gitthing.com/…\" meaning that packages are built using
+a checkout of the Git repository at the given URL."
+  (define replacements
+    (map (lambda (spec)
+           (match (string-tokenize spec %not-equal)
+             ((spec url)
+              (cons spec
+                    (lambda (old)
+                      (package
+                        (inherit old)
+                        (source (git-checkout (url url)
+                                              (recursive? #t)))))))))
+         replacement-specs))
+
+  (define rewrite
+    (package-input-rewriting/spec replacements))
+
+  (lambda (store obj)
+    (if (package? obj)
+        (rewrite obj)
+        obj)))
+
 (define %transformations
   ;; Transformations that can be applied to things to build.  The car is the
   ;; key used in the option alist, and the cdr is the transformation
@@ -274,7 +388,10 @@ current 'gnutls' package, after which version 3.5.4 is grafted onto them."
   ;; things to build.
   `((with-source . ,transform-package-source)
     (with-input  . ,transform-package-inputs)
-    (with-graft  . ,transform-package-inputs/graft)))
+    (with-graft  . ,transform-package-inputs/graft)
+    (with-branch . ,transform-package-source-branch)
+    (with-commit . ,transform-package-source-commit)
+    (with-git-url . ,transform-package-source-git-url)))
 
 (define %transformation-options
   ;; The command-line interface to the above transformations.
@@ -288,7 +405,13 @@ current 'gnutls' package, after which version 3.5.4 is grafted onto them."
           (option '("with-input") #t #f
                   (parser 'with-input))
           (option '("with-graft") #t #f
-                  (parser 'with-graft)))))
+                  (parser 'with-graft))
+          (option '("with-branch") #t #f
+                  (parser 'with-branch))
+          (option '("with-commit") #t #f
+                  (parser 'with-commit))
+          (option '("with-git-url") #t #f
+                  (parser 'with-git-url)))))
 
 (define (show-transformation-options-help)
   (display (G_ "
@@ -299,23 +422,38 @@ current 'gnutls' package, after which version 3.5.4 is grafted onto them."
                          replace dependency PACKAGE by REPLACEMENT"))
   (display (G_ "
       --with-graft=PACKAGE=REPLACEMENT
-                         graft REPLACEMENT on packages that refer to PACKAGE")))
+                         graft REPLACEMENT on packages that refer to PACKAGE"))
+  (display (G_ "
+      --with-branch=PACKAGE=BRANCH
+                         build PACKAGE from the latest commit of BRANCH"))
+  (display (G_ "
+      --with-commit=PACKAGE=COMMIT
+                         build PACKAGE from COMMIT"))
+  (display (G_ "
+      --with-git-url=PACKAGE=URL
+                         build PACKAGE from the repository at URL")))
 
 
 (define (options->transformation opts)
   "Return a procedure that, when passed an object to build (package,
 derivation, etc.), applies the transformations specified by OPTS."
   (define applicable
-    ;; List of applicable transformations as symbol/procedure pairs.
+    ;; List of applicable transformations as symbol/procedure pairs in the
+    ;; order in which they appear on the command line.
     (filter-map (match-lambda
-                  ((key . transform)
-                   (match (filter-map (match-lambda
-                                        ((k . arg)
-                                         (and (eq? k key) arg)))
-                                      opts)
-                     (()   #f)
-                     (args (cons key (transform args))))))
-                %transformations))
+                  ((key . value)
+                   (match (any (match-lambda
+                                 ((k . proc)
+                                  (and (eq? k key) proc)))
+                               %transformations)
+                     (#f
+                      #f)
+                     (transform
+                      ;; XXX: We used to pass TRANSFORM a list of several
+                      ;; arguments, but we now pass only one, assuming that
+                      ;; transform composes well.
+                      (cons key (transform (list value)))))))
+                (reverse opts)))
 
   (lambda (store obj)
     (fold (match-lambda*
@@ -364,14 +502,14 @@ options handled by 'set-build-options-from-command-line', and listed in
                          mark the build as failed after SECONDS of silence"))
   (display (G_ "
       --timeout=SECONDS  mark the build as failed after SECONDS of activity"))
-  (display (G_ "
-      --verbosity=LEVEL  use the given verbosity LEVEL"))
   (display (G_ "
       --rounds=N         build N times in a row to detect non-determinism"))
   (display (G_ "
   -c, --cores=N          allow the use of up to N CPU cores for the build"))
   (display (G_ "
-  -M, --max-jobs=N       allow at most N build jobs")))
+  -M, --max-jobs=N       allow at most N build jobs"))
+  (display (G_ "
+      --debug=LEVEL      produce debugging output at LEVEL")))
 
 (define (set-build-options-from-command-line store opts)
   "Given OPTS, an alist as returned by 'args-fold' given
@@ -390,7 +528,11 @@ options handled by 'set-build-options-from-command-line', and listed in
                      #:max-silent-time (assoc-ref opts 'max-silent-time)
                      #:timeout (assoc-ref opts 'timeout)
                      #:print-build-trace (assoc-ref opts 'print-build-trace?)
-                     #:verbosity (assoc-ref opts 'verbosity)))
+                     #:print-extended-build-trace?
+                     (assoc-ref opts 'print-extended-build-trace?)
+                     #:multiplexed-build-output?
+                     (assoc-ref opts 'multiplexed-build-output?)
+                     #:verbosity (assoc-ref opts 'debug)))
 
 (define set-build-options-from-command-line*
   (store-lift set-build-options-from-command-line))
@@ -464,12 +606,12 @@ options handled by 'set-build-options-from-command-line', and listed in
                   (apply values
                          (alist-cons 'timeout (string->number* arg) result)
                          rest)))
-        (option '("verbosity") #t #f
+        (option '("debug") #t #f
                 (lambda (opt name arg result . rest)
-                  (let ((level (string->number arg)))
+                  (let ((level (string->number* arg)))
                     (apply values
-                           (alist-cons 'verbosity level
-                                       (alist-delete 'verbosity result))
+                           (alist-cons 'debug level
+                                       (alist-delete 'debug result))
                            rest))))
         (option '(#\c "cores") #t #f
                 (lambda (opt name arg result . rest)
@@ -493,13 +635,15 @@ options handled by 'set-build-options-from-command-line', and listed in
 
 (define %default-options
   ;; Alist of default option values.
-  `((system . ,(%current-system))
-    (build-mode . ,(build-mode normal))
+  `((build-mode . ,(build-mode normal))
     (graft? . #t)
     (substitutes? . #t)
     (build-hook? . #t)
     (print-build-trace? . #t)
-    (verbosity . 0)))
+    (print-extended-build-trace? . #t)
+    (multiplexed-build-output? . #t)
+    (verbosity . 2)
+    (debug . 0)))
 
 (define (show-help)
   (display (G_ "Usage: guix build [OPTION]... PACKAGE-OR-DERIVATION...
@@ -528,6 +672,8 @@ Build the given PACKAGE-OR-DERIVATION and return their output paths.\n"))
   -r, --root=FILE        make FILE a symlink to the result, and register it
                          as a garbage collector root"))
   (display (G_ "
+  -v, --verbosity=LEVEL  use the given verbosity LEVEL"))
+  (display (G_ "
   -q, --quiet            do not show the build log"))
   (display (G_ "
       --log-file         return the log file names for the given derivations"))
@@ -582,8 +728,7 @@ must be one of 'package', 'all', or 'transitive'~%")
                           rest)))
          (option '(#\s "system") #t #f
                  (lambda (opt name arg result)
-                   (alist-cons 'system arg
-                               (alist-delete 'system result eq?))))
+                   (alist-cons 'system arg result)))
          (option '("target") #t #f
                  (lambda (opt name arg result)
                    (alist-cons 'target arg
@@ -603,9 +748,15 @@ must be one of 'package', 'all', or 'transitive'~%")
          (option '(#\r "root") #t #f
                  (lambda (opt name arg result)
                    (alist-cons 'gc-root arg result)))
+         (option '(#\v "verbosity") #t #f
+                 (lambda (opt name arg result)
+                   (let ((level (string->number* arg)))
+                     (alist-cons 'verbosity level
+                                 (alist-delete 'verbosity result)))))
          (option '(#\q "quiet") #f #f
                  (lambda (opt name arg result)
-                   (alist-cons 'quiet? #t result)))
+                   (alist-cons 'verbosity 0
+                               (alist-delete 'verbosity result))))
          (option '("log-file") #f #f
                  (lambda (opt name arg result)
                    (alist-cons 'log-file? #t result)))
@@ -617,7 +768,7 @@ must be one of 'package', 'all', or 'transitive'~%")
   "Read the arguments from OPTS and return a list of high-level objects to
 build---packages, gexps, derivations, and so on."
   (define (validate-type x)
-    (unless (or (package? x) (derivation? x) (gexp? x) (procedure? x))
+    (unless (or (derivation? x) (file-like? x) (gexp? x) (procedure? x))
       (leave (G_ "~s: not something we can build~%") x)))
 
   (define (ensure-list x)
@@ -658,50 +809,71 @@ build."
        (cut package-cross-derivation <> <> triplet <>))))
 
   (define src    (assoc-ref opts 'source))
-  (define system (assoc-ref opts 'system))
   (define graft? (assoc-ref opts 'graft?))
+  (define systems
+    (match (filter-map (match-lambda
+                         (('system . system) system)
+                         (_ #f))
+                       opts)
+      (()      (list (%current-system)))
+      (systems systems)))
+
+  (define things-to-build
+    (map (cut transform store <>)
+         (options->things-to-build opts)))
+
+  (define (compute-derivation obj system)
+    ;; Compute the derivation of OBJ for SYSTEM.
+    (match obj
+      ((? package? p)
+       (let ((p (or (and graft? (package-replacement p)) p)))
+         (match src
+           (#f
+            (list (package->derivation store p system)))
+           (#t
+            (match (package-source p)
+              (#f
+               (format (current-error-port)
+                       (G_ "~a: warning: \
+package '~a' has no source~%")
+                       (location->string (package-location p))
+                       (package-name p))
+               '())
+              (s
+               (list (package-source-derivation store s)))))
+           (proc
+            (map (cut package-source-derivation store <>)
+                 (proc p))))))
+      ((? derivation? drv)
+       (list drv))
+      ((? procedure? proc)
+       (list (run-with-store store
+               (mbegin %store-monad
+                 (set-guile-for-build (default-guile))
+                 (proc))
+               #:system system)))
+      ((? file-like? obj)
+       (list (run-with-store store
+               (lower-object obj system
+                             #:target (assoc-ref opts 'target))
+               #:system system)))
+      ((? gexp? gexp)
+       (list (run-with-store store
+               (mbegin %store-monad
+                 (set-guile-for-build (default-guile))
+                 (gexp->derivation "gexp" gexp
+                                   #:system system))
+               #:system system)))))
 
   ;; We may get 'unbound-variable' errors while evaluating the 'inputs' fields
   ;; of user packages.  Since 'guix build' is the primary tool for people
   ;; testing new packages, report such errors gracefully.
   (with-unbound-variable-handling
    (parameterize ((%graft? graft?))
-     (append-map (match-lambda
-                   ((? package? p)
-                    (let ((p (or (and graft? (package-replacement p)) p)))
-                      (match src
-                        (#f
-                         (list (package->derivation store p system)))
-                        (#t
-                         (match (package-source p)
-                           (#f
-                            (format (current-error-port)
-                                    (G_ "~a: warning: \
-package '~a' has no source~%")
-                                    (location->string (package-location p))
-                                    (package-name p))
-                            '())
-                           (s
-                            (list (package-source-derivation store s)))))
-                        (proc
-                         (map (cut package-source-derivation store <>)
-                              (proc p))))))
-                   ((? derivation? drv)
-                    (list drv))
-                   ((? procedure? proc)
-                    (list (run-with-store store
-                            (mbegin %store-monad
-                              (set-guile-for-build (default-guile))
-                              (proc))
-                            #:system system)))
-                   ((? gexp? gexp)
-                    (list (run-with-store store
-                            (mbegin %store-monad
-                              (set-guile-for-build (default-guile))
-                              (gexp->derivation "gexp" gexp
-                                                #:system system))))))
-                 (map (cut transform store <>)
-                      (options->things-to-build opts))))))
+     (append-map (lambda (system)
+                   (append-map (cut compute-derivation <> system)
+                               things-to-build))
+                 systems))))
 
 (define (show-build-log store file urls)
   "Show the build log for FILE, falling back to remote logs from URLS if
@@ -722,63 +894,59 @@ needed."
     (parse-command-line args %options
                         (list %default-options)))
 
-  (define quiet?
-    (assoc-ref opts 'quiet?))
-
   (with-error-handling
     ;; Ask for absolute file names so that .drv file names passed from the
     ;; user to 'read-derivation' are absolute when it returns.
     (with-fluids ((%file-port-name-canonicalization 'absolute))
-      (with-store store
-        ;; Set the build options before we do anything else.
-        (set-build-options-from-command-line store opts)
-
-        (parameterize ((current-build-output-port (if quiet?
-                                                      (%make-void-port "w")
-                                                      (build-output-port #:verbose? #t))))
-          (let* ((mode  (assoc-ref opts 'build-mode))
-                 (drv   (options->derivations store opts))
-                 (urls  (map (cut string-append <> "/log")
-                             (if (assoc-ref opts 'substitutes?)
-                                 (or (assoc-ref opts 'substitute-urls)
-                                     ;; XXX: This does not necessarily match the
-                                     ;; daemon's substitute URLs.
-                                     %default-substitute-urls)
-                                 '())))
-                 (items (filter-map (match-lambda
-                                      (('argument . (? store-path? file))
-                                       file)
-                                      (_ #f))
-                                    opts))
-                 (roots (filter-map (match-lambda
-                                      (('gc-root . root) root)
-                                      (_ #f))
-                                    opts)))
-
-            (unless (or (assoc-ref opts 'log-file?)
-                        (assoc-ref opts 'derivations-only?))
-              (show-what-to-build store drv
-                                  #:use-substitutes?
-                                  (assoc-ref opts 'substitutes?)
-                                  #:dry-run? (assoc-ref opts 'dry-run?)
-                                  #:mode mode))
-
-            (cond ((assoc-ref opts 'log-file?)
-                   (for-each (cut show-build-log store <> urls)
-                             (delete-duplicates
-                              (append (map derivation-file-name drv)
-                                      items))))
-                  ((assoc-ref opts 'derivations-only?)
-                   (format #t "~{~a~%~}" (map derivation-file-name drv))
-                   (for-each (cut register-root store <> <>)
-                             (map (compose list derivation-file-name) drv)
-                             roots))
-                  ((not (assoc-ref opts 'dry-run?))
-                   (and (build-derivations store drv mode)
-                        (for-each show-derivation-outputs drv)
-                        (for-each (cut register-root store <> <>)
-                                  (map (lambda (drv)
-                                         (map cdr
-                                              (derivation->output-paths drv)))
-                                       drv)
-                                  roots))))))))))
+      (with-status-verbosity (assoc-ref opts 'verbosity)
+        (with-store store
+          ;; Set the build options before we do anything else.
+          (set-build-options-from-command-line store opts)
+
+          (parameterize ((current-terminal-columns (terminal-columns)))
+            (let* ((mode  (assoc-ref opts 'build-mode))
+                   (drv   (options->derivations store opts))
+                   (urls  (map (cut string-append <> "/log")
+                               (if (assoc-ref opts 'substitutes?)
+                                   (or (assoc-ref opts 'substitute-urls)
+                                       ;; XXX: This does not necessarily match the
+                                       ;; daemon's substitute URLs.
+                                       %default-substitute-urls)
+                                   '())))
+                   (items (filter-map (match-lambda
+                                        (('argument . (? store-path? file))
+                                         file)
+                                        (_ #f))
+                                      opts))
+                   (roots (filter-map (match-lambda
+                                        (('gc-root . root) root)
+                                        (_ #f))
+                                      opts)))
+
+              (unless (or (assoc-ref opts 'log-file?)
+                          (assoc-ref opts 'derivations-only?))
+                (show-what-to-build store drv
+                                    #:use-substitutes?
+                                    (assoc-ref opts 'substitutes?)
+                                    #:dry-run? (assoc-ref opts 'dry-run?)
+                                    #:mode mode))
+
+              (cond ((assoc-ref opts 'log-file?)
+                     (for-each (cut show-build-log store <> urls)
+                               (delete-duplicates
+                                (append (map derivation-file-name drv)
+                                        items))))
+                    ((assoc-ref opts 'derivations-only?)
+                     (format #t "~{~a~%~}" (map derivation-file-name drv))
+                     (for-each (cut register-root store <> <>)
+                               (map (compose list derivation-file-name) drv)
+                               roots))
+                    ((not (assoc-ref opts 'dry-run?))
+                     (and (build-derivations store drv mode)
+                          (for-each show-derivation-outputs drv)
+                          (for-each (cut register-root store <> <>)
+                                    (map (lambda (drv)
+                                           (map cdr
+                                                (derivation->output-paths drv)))
+                                         drv)
+                                    roots)))))))))))