summary refs log tree commit diff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2014-10-27 18:09:00 +0100
committerLudovic Courtès <ludo@gnu.org>2014-11-02 21:22:12 +0100
commit05962f2958eb98bad384702455236ff9d2acfb39 (patch)
tree519d31fb05176a3ec0e9918fc746ede76a071c7f
parent50373bab7a084dc28a48df2ca7e16036d8978182 (diff)
downloadguix-05962f2958eb98bad384702455236ff9d2acfb39.tar.gz
packages: Implement grafts.
Thanks to Mark H. Weaver <mhw@netris.org> for insightful discussions
and suggestions.

* guix/packages.scm (<package>)[graft]: New field.
  (patch-and-repack): Invoke 'package-derivation' with #:graft? #f.
  (package-source-derivation): Likewise.  Do not use (%guile-for-build)
  in call to 'patch-and-repack', and we could end up using a grafted
  Guile.
  (expand-input): Likewise, also for 'package-cross-derivation' call.
  (package->bag): Add #:graft? parameter.  Honor it.  Use 'strip-append'
  instead of 'package-full-name'.
  (input-graft, input-cross-graft, bag-grafts, package-grafts): New
  procedures.
  (package-derivation, package-cross-derivation): Add #:graft? parameter
  and honor it.
* gnu/packages/bootstrap.scm (package-with-bootstrap-guile): Add
  recursive call on 'graft'.
* guix/build-system/gnu.scm (package-with-explicit-inputs,
  package-with-extra-configure-variable, static-package): Likewise.
  (gnu-build): Use the ungrafted Guile to avoid full rebuilds.
  (gnu-cross-build): Likewise.
* guix/build-system/cmake.scm (cmake-build): Likewise.
* guix/build-system/glib-or-gtk.scm (glib-or-gtk-build): Likewise.
* guix/build-system/perl.scm (perl-build): Likewise.
* guix/build-system/python.scm (python-build): Likewise.
* guix/build-system/ruby.scm (ruby-build): Likewise.
* guix/build-system/trivial.scm (guile-for-build): Likewise.
* tests/packages.scm ("package-derivation, direct graft",
  "package-cross-derivation, direct graft", "package-grafts,
  indirect grafts", "package-grafts, indirect grafts, cross",
  "package-grafts, indirect grafts, propagated inputs",
  "package-derivation, indirect grafts"): New tests.
  ("bag->derivation", "bag->derivation, cross-compilation"): Wrap in
  'parameterize'.
* doc/guix.texi (Security Updates): New node.
  (Invoking guix build): Document --no-graft.
-rw-r--r--doc/guix.texi63
-rw-r--r--gnu/packages/bootstrap.scm4
-rw-r--r--guix/build-system/cmake.scm4
-rw-r--r--guix/build-system/glib-or-gtk.scm4
-rw-r--r--guix/build-system/gnu.scm30
-rw-r--r--guix/build-system/perl.scm4
-rw-r--r--guix/build-system/python.scm4
-rw-r--r--guix/build-system/ruby.scm4
-rw-r--r--guix/build-system/trivial.scm4
-rw-r--r--guix/packages.scm147
-rw-r--r--guix/scripts/build.scm47
-rw-r--r--tests/packages.scm105
12 files changed, 347 insertions, 73 deletions
diff --git a/doc/guix.texi b/doc/guix.texi
index d3ab9676ee..fbf5bac9b4 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -2569,6 +2569,10 @@ candidates:
 guix build guile --with-source=../guile-2.0.9.219-e1bb7.tar.xz
 @end example
 
+@item --no-grafts
+Do not ``graft'' packages.  In practice, this means that package updates
+available as grafts are not applied.  @xref{Security Updates}, for more
+information on grafts.
 
 @item --derivations
 @itemx -d
@@ -3003,6 +3007,7 @@ For information on porting to other architectures or kernels,
 * System Installation::         Installing the whole operating system.
 * System Configuration::        Configuring a GNU system.
 * Installing Debugging Files::  Feeding the debugger.
+* Security Updates::            Deploying security fixes quickly.
 * Package Modules::             Packages from the programmer's viewpoint.
 * Packaging Guidelines::        Growing the distribution.
 * Bootstrapping::               GNU/Linux built from scratch.
@@ -4280,6 +4285,64 @@ the load.  To check whether a package has a @code{debug} output, use
 @command{guix package --list-available} (@pxref{Invoking guix package}).
 
 
+@node Security Updates
+@section Security Updates
+
+@indentedblock
+Note: As of version @value{VERSION}, the feature described in this
+section is experimental.
+@end indentedblock
+
+@cindex security updates
+Occasionally, important security vulnerabilities are discovered in core
+software packages and must be patched.  Guix follows a functional
+package management discipline (@pxref{Introduction}), which implies
+that, when a package is changed, @emph{every package that depends on it}
+must be rebuilt.  This can significantly slow down the deployment of
+fixes in core packages such as libc or Bash, since basically the whole
+distribution would need to be rebuilt.  Using pre-built binaries helps
+(@pxref{Substitutes}), but deployment may still take more time than
+desired.
+
+@cindex grafts
+To address that, Guix implements @dfn{grafts}, a mechanism that allows
+for fast deployment of critical updates without the costs associated
+with a whole-distribution rebuild.  The idea is to rebuild only the
+package that needs to be patched, and then to ``graft'' it onto packages
+explicitly installed by the user and that were previously referring to
+the original package.  The cost of grafting is typically very low, and
+order of magnitudes lower than a full rebuild of the dependency chain.
+
+@cindex replacements of packages, for grafts
+For instance, suppose a security update needs to be applied to Bash.
+Guix developers will provide a package definition for the ``fixed''
+Bash, say @var{bash-fixed}, in the usual way (@pxref{Defining
+Packages}).  Then, the original package definition is augmented with a
+@code{replacement} field pointing to the package containing the bug fix:
+
+@example
+(define bash
+  (package
+    (name "bash")
+    ;; @dots{}
+    (replacement bash-fixed)))
+@end example
+
+From there on, any package depending directly or indirectly on Bash that
+is installed will automatically be ``rewritten'' to refer to
+@var{bash-fixed} instead of @var{bash}.  This grafting process takes
+time proportional to the size of the package, but expect less than a
+minute for an ``average'' package on a recent machine.
+
+Currently, the graft and the package it replaces (@var{bash-fixed} and
+@var{bash} in the example above) must have the exact same @code{name}
+and @code{version} fields.  This restriction mostly comes from the fact
+that grafting works by patching files, including binary files, directly.
+Other restrictions may apply: for instance, when adding a graft to a
+package providing a shared library, the original shared library and its
+replacement must have the same @code{SONAME} and be binary-compatible.
+
+
 @node Package Modules
 @section Package Modules
 
diff --git a/gnu/packages/bootstrap.scm b/gnu/packages/bootstrap.scm
index e617093fb3..33b61aa0be 100644
--- a/gnu/packages/bootstrap.scm
+++ b/gnu/packages/bootstrap.scm
@@ -146,7 +146,9 @@ check whether everything is alright."
       (native-inputs (map rewritten-input
                           (package-native-inputs p)))
       (propagated-inputs (map rewritten-input
-                              (package-propagated-inputs p)))))))
+                              (package-propagated-inputs p)))
+      (replacement (and=> (package-replacement p)
+                          package-with-bootstrap-guile))))))
 
 (define* (glibc-dynamic-linker
           #:optional (system (or (and=> (%current-target-system)
diff --git a/guix/build-system/cmake.scm b/guix/build-system/cmake.scm
index 85acc2d0b3..0425e9fb39 100644
--- a/guix/build-system/cmake.scm
+++ b/guix/build-system/cmake.scm
@@ -125,11 +125,11 @@ provides a 'CMakeLists.txt' file as its build system."
   (define guile-for-build
     (match guile
       ((? package?)
-       (package-derivation store guile system))
+       (package-derivation store guile system #:graft? #f))
       (#f                                         ; the default
        (let* ((distro (resolve-interface '(gnu packages commencement)))
               (guile  (module-ref distro 'guile-final)))
-         (package-derivation store guile system)))))
+         (package-derivation store guile system #:graft? #f)))))
 
   (build-expression->derivation store name builder
                                 #:system system
diff --git a/guix/build-system/glib-or-gtk.scm b/guix/build-system/glib-or-gtk.scm
index 078d5f6e8a..51e0c419e3 100644
--- a/guix/build-system/glib-or-gtk.scm
+++ b/guix/build-system/glib-or-gtk.scm
@@ -168,11 +168,11 @@
   (define guile-for-build
     (match guile
       ((? package?)
-       (package-derivation store guile system))
+       (package-derivation store guile system #:graft? #f))
       (#f                                         ; the default
        (let* ((distro (resolve-interface '(gnu packages commencement)))
               (guile  (module-ref distro 'guile-final)))
-         (package-derivation store guile system)))))
+         (package-derivation store guile system #:graft? #f)))))
 
   (build-expression->derivation store name builder
                                 #:system system
diff --git a/guix/build-system/gnu.scm b/guix/build-system/gnu.scm
index 3cb9f6ae94..c675155a6a 100644
--- a/guix/build-system/gnu.scm
+++ b/guix/build-system/gnu.scm
@@ -91,6 +91,13 @@ builder, or the distro's final Guile when GUILE is #f."
          `(#:guile ,guile
            #:implicit-inputs? #f
            ,@args)))
+      (replacement
+       (let ((replacement (package-replacement p)))
+         (and replacement
+              (package-with-explicit-inputs replacement inputs loc
+                                            #:native-inputs
+                                            native-inputs
+                                            #:guile guile))))
       (native-inputs
        (let ((filtered (duplicate-filter native-inputs*)))
         `(,@(call native-inputs*)
@@ -132,6 +139,11 @@ flags for VARIABLE, the associated value is augmented."
                                  (substring flag ,len))
                                 flag))
                           ,flags)))))))
+      (replacement
+       (let ((replacement (package-replacement p)))
+         (and replacement
+              (package-with-extra-configure-variable replacement
+                                                     variable value))))
       (inputs (rewritten-inputs (package-inputs p)))
       (propagated-inputs (rewritten-inputs (package-propagated-inputs p))))))
 
@@ -155,7 +167,8 @@ use `--strip-all' as the arguments to `strip'."
          ((#:strip-flags flags)
           (if strip-all?
               ''("--strip-all")
-              flags)))))))
+              flags)))))
+    (replacement (and=> (package-replacement p) static-package))))
 
 (define* (dist-package p source)
   "Return a package that runs takes source files from the SOURCE directory,
@@ -290,9 +303,11 @@ are allowed to refer to."
   (define canonicalize-reference
     (match-lambda
      ((? package? p)
-      (derivation->output-path (package-derivation store p system)))
+      (derivation->output-path (package-derivation store p system
+                                                   #:graft? #f)))
      (((? package? p) output)
-      (derivation->output-path (package-derivation store p system)
+      (derivation->output-path (package-derivation store p system
+                                                   #:graft? #f)
                                output))
      ((? string? output)
       output)))
@@ -328,11 +343,12 @@ are allowed to refer to."
   (define guile-for-build
     (match guile
       ((? package?)
-       (package-derivation store guile system))
+       (package-derivation store guile system #:graft? #f))
       (#f                                         ; the default
        (let* ((distro (resolve-interface '(gnu packages commencement)))
               (guile  (module-ref distro 'guile-final)))
-         (package-derivation store guile system)))))
+         (package-derivation store guile system
+                             #:graft? #f)))))
 
   (build-expression->derivation store name builder
                                 #:system system
@@ -472,11 +488,11 @@ platform."
   (define guile-for-build
     (match guile
       ((? package?)
-       (package-derivation store guile system))
+       (package-derivation store guile system #:graft? #f))
       (#f                                         ; the default
        (let* ((distro (resolve-interface '(gnu packages commencement)))
               (guile  (module-ref distro 'guile-final)))
-         (package-derivation store guile system)))))
+         (package-derivation store guile system #:graft? #f)))))
 
   (build-expression->derivation store name builder
                                 #:system system
diff --git a/guix/build-system/perl.scm b/guix/build-system/perl.scm
index 1a968f4150..c488adb500 100644
--- a/guix/build-system/perl.scm
+++ b/guix/build-system/perl.scm
@@ -114,11 +114,11 @@ provides a `Makefile.PL' file as its build system."
   (define guile-for-build
     (match guile
       ((? package?)
-       (package-derivation store guile system))
+       (package-derivation store guile system #:graft? #f))
       (#f                                         ; the default
        (let* ((distro (resolve-interface '(gnu packages commencement)))
               (guile  (module-ref distro 'guile-final)))
-         (package-derivation store guile system)))))
+         (package-derivation store guile system #:graft? #f)))))
 
   (build-expression->derivation store name builder
                                 #:system system
diff --git a/guix/build-system/python.scm b/guix/build-system/python.scm
index 3cd537c752..78348e9cf7 100644
--- a/guix/build-system/python.scm
+++ b/guix/build-system/python.scm
@@ -160,11 +160,11 @@ provides a 'setup.py' file as its build system."
   (define guile-for-build
     (match guile
       ((? package?)
-       (package-derivation store guile system))
+       (package-derivation store guile system #:graft? #f))
       (#f                                         ; the default
        (let* ((distro (resolve-interface '(gnu packages commencement)))
               (guile  (module-ref distro 'guile-final)))
-         (package-derivation store guile system)))))
+         (package-derivation store guile system #:graft? #f)))))
 
   (build-expression->derivation store name builder
                                 #:inputs inputs
diff --git a/guix/build-system/ruby.scm b/guix/build-system/ruby.scm
index e4e115f657..d2dd6a48cc 100644
--- a/guix/build-system/ruby.scm
+++ b/guix/build-system/ruby.scm
@@ -99,11 +99,11 @@
   (define guile-for-build
     (match guile
       ((? package?)
-       (package-derivation store guile system))
+       (package-derivation store guile system #:graft? #f))
       (#f
        (let* ((distro (resolve-interface '(gnu packages commencement)))
               (guile  (module-ref distro 'guile-final)))
-         (package-derivation store guile system)))))
+         (package-derivation store guile system #:graft? #f)))))
 
   (build-expression->derivation store name builder
                                 #:inputs inputs
diff --git a/guix/build-system/trivial.scm b/guix/build-system/trivial.scm
index 07adbe75fa..350b1df553 100644
--- a/guix/build-system/trivial.scm
+++ b/guix/build-system/trivial.scm
@@ -28,11 +28,11 @@
 (define (guile-for-build store guile system)
   (match guile
     ((? package?)
-     (package-derivation store guile system))
+     (package-derivation store guile system #:graft? #f))
     (#f                                         ; the default
      (let* ((distro (resolve-interface '(gnu packages commencement)))
             (guile  (module-ref distro 'guile-final)))
-       (package-derivation store guile system)))))
+       (package-derivation store guile system #:graft? #f)))))
 
 (define* (lower name
                 #:key source inputs native-inputs outputs system target
diff --git a/guix/packages.scm b/guix/packages.scm
index 97a82a4682..698a4c8097 100644
--- a/guix/packages.scm
+++ b/guix/packages.scm
@@ -26,6 +26,7 @@
   #:use-module (ice-9 match)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-9 gnu)
+  #:use-module (srfi srfi-11)
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-34)
   #:use-module (srfi srfi-35)
@@ -65,6 +66,7 @@
             package-outputs
             package-native-search-paths
             package-search-paths
+            package-replacement
             package-synopsis
             package-description
             package-license
@@ -85,6 +87,7 @@
             package-derivation
             package-cross-derivation
             package-output
+            package-grafts
 
             %supported-systems
 
@@ -97,6 +100,7 @@
             &package-cross-build-system-error
             package-cross-build-system-error?
 
+            %graft?
             package->bag
             bag->derivation
             bag-transitive-inputs
@@ -211,6 +215,8 @@ corresponds to the arguments expected by `set-path-environment-variable'."
                                                   ; inputs
   (native-search-paths package-native-search-paths (default '()))
   (search-paths package-search-paths (default '()))
+  (replacement package-replacement                ; package | #f
+               (default #f) (thunked))
 
   (synopsis package-synopsis)                    ; one-line description
   (description package-description)              ; one or two paragraphs
@@ -445,8 +451,8 @@ IMPORTED-MODULES specify modules to use/import for use by SNIPPET."
                                (and (member name (cons decompression-type
                                                        '("tar" "xz" "patch")))
                                     (list name
-                                          (package-derivation store p
-                                                              system)))))
+                                          (package-derivation store p system
+                                                              #:graft? #f)))))
                              (or inputs (%standard-patch-inputs))))
         (modules (delete-duplicates (cons '(guix build utils) modules))))
 
@@ -472,12 +478,10 @@ IMPORTED-MODULES specify modules to use/import for use by SNIPPET."
      ;; Patches and/or a snippet.
      (let ((source (method store uri 'sha256 sha256 name
                            #:system system))
-           (guile  (match (or guile-for-build (%guile-for-build)
-                              (default-guile))
+           (guile  (match (or guile-for-build (default-guile))
                      ((? package? p)
-                      (package-derivation store p system))
-                     ((? derivation? drv)
-                      drv))))
+                      (package-derivation store p system
+                                          #:graft? #f)))))
        (patch-and-repack store source patches
                          #:inputs inputs
                          #:snippet snippet
@@ -617,8 +621,9 @@ information in exceptions."
 
   (define derivation
     (if cross-system
-        (cut package-cross-derivation store <> cross-system system)
-        (cut package-derivation store <> system)))
+        (cut package-cross-derivation store <> cross-system system
+             #:graft? #f)
+        (cut package-derivation store <> system #:graft? #f)))
 
   (match input
     (((? string? name) (? package? package))
@@ -643,20 +648,27 @@ information in exceptions."
                         (package package)
                         (input   x)))))))
 
+(define %graft?
+  ;; Whether to honor package grafts by default.
+  (make-parameter #t))
+
 (define* (package->bag package #:optional
                        (system (%current-system))
-                       (target (%current-target-system)))
+                       (target (%current-target-system))
+                       #:key (graft? (%graft?)))
   "Compile PACKAGE into a bag for SYSTEM, possibly cross-compiled to TARGET,
 and return it."
   ;; Bind %CURRENT-SYSTEM and %CURRENT-TARGET-SYSTEM so that thunked field
   ;; values can refer to it.
   (parameterize ((%current-system system)
                  (%current-target-system target))
-    (match package
+    (match (if graft?
+               (or (package-replacement package) package)
+               package)
       (($ <package> name version source build-system
                     args inputs propagated-inputs native-inputs self-native-input?
                     outputs)
-       (or (make-bag build-system (package-full-name package)
+       (or (make-bag build-system (string-append name "-" version)
                      #:system system
                      #:target target
                      #:source source
@@ -676,6 +688,77 @@ and return it."
                        (&package-error
                         (package package))))))))))
 
+(define (input-graft store system)
+  "Return a procedure that, given an input referring to a package with a
+graft, returns a pair with the original derivation and the graft's derivation,
+and returns #f for other inputs."
+  (match-lambda
+   ((label (? package? package) sub-drv ...)
+    (let ((replacement (package-replacement package)))
+      (and replacement
+           (let ((orig (package-derivation store package system
+                                           #:graft? #f))
+                 (new  (package-derivation store replacement system)))
+             (graft
+               (origin orig)
+               (replacement new)
+               (origin-output (match sub-drv
+                                (() "out")
+                                ((output) output)))
+               (replacement-output origin-output))))))
+   (x
+    #f)))
+
+(define (input-cross-graft store target system)
+  "Same as 'input-graft', but for cross-compilation inputs."
+  (match-lambda
+   ((label (? package? package) sub-drv ...)
+    (let ((replacement (package-replacement package)))
+      (and replacement
+           (let ((orig (package-cross-derivation store package target system
+                                                 #:graft? #f))
+                 (new  (package-cross-derivation store replacement
+                                                 target system)))
+             (graft
+               (origin orig)
+               (replacement new)
+               (origin-output (match sub-drv
+                                (() "out")
+                                ((output) output)))
+               (replacement-output origin-output))))))
+   (_
+    #f)))
+
+(define* (bag-grafts store bag)
+  "Return the list of grafts applicable to BAG.  Each graft is a <graft>
+record."
+  (let ((target (bag-target bag))
+        (system (bag-system bag)))
+    (define native-grafts
+      (filter-map (input-graft store system)
+                  (append (bag-transitive-build-inputs bag)
+                          (bag-transitive-target-inputs bag)
+                          (if target
+                              '()
+                              (bag-transitive-host-inputs bag)))))
+
+    (define target-grafts
+      (if target
+          (filter-map (input-cross-graft store target system)
+                      (bag-transitive-host-inputs bag))
+          '()))
+
+    (append native-grafts target-grafts)))
+
+(define* (package-grafts store package
+                         #:optional (system (%current-system))
+                         #:key target)
+  "Return the list of grafts applicable to PACKAGE as built for SYSTEM and
+TARGET."
+  (let* ((package (or (package-replacement package) package))
+         (bag     (package->bag package system target)))
+    (bag-grafts store bag)))
+
 (define* (bag->derivation store bag
                           #:optional context)
   "Return the derivation to build BAG for SYSTEM.  Optionally, CONTEXT can be
@@ -743,23 +826,47 @@ This is an internal procedure."
            (bag-arguments bag))))
 
 (define* (package-derivation store package
-                             #:optional (system (%current-system)))
+                             #:optional (system (%current-system))
+                             #:key (graft? (%graft?)))
   "Return the <derivation> object of PACKAGE for SYSTEM."
 
   ;; Compute the derivation and cache the result.  Caching is important
   ;; because some derivations, such as the implicit inputs of the GNU build
   ;; system, will be queried many, many times in a row.
-  (cached package system
-          (bag->derivation store (package->bag package system #f)
-                           package)))
+  (cached package (cons system graft?)
+          (let* ((bag (package->bag package system #f #:graft? graft?))
+                 (drv (bag->derivation store bag package)))
+            (if graft?
+                (match (bag-grafts store bag)
+                  (()
+                   drv)
+                  (grafts
+                   (let ((guile (package-derivation store (default-guile)
+                                                    system #:graft? #f)))
+                     (graft-derivation store (bag-name bag) drv grafts
+                                       #:system system
+                                       #:guile guile))))
+                drv))))
 
 (define* (package-cross-derivation store package target
-                                   #:optional (system (%current-system)))
+                                   #:optional (system (%current-system))
+                                   #:key (graft? (%graft?)))
   "Cross-build PACKAGE for TARGET (a GNU triplet) from host SYSTEM (a Guix
 system identifying string)."
-  (cached package (cons system target)
-          (bag->derivation store (package->bag package system target)
-                           package)))
+  (cached package (list system target graft?)
+          (let* ((bag (package->bag package system target #:graft? graft?))
+                 (drv (bag->derivation store bag package)))
+            (if graft?
+                (match (bag-grafts store bag)
+                  (()
+                   drv)
+                  (grafts
+                   (graft-derivation store (bag-name bag) drv grafts
+                                     #:system system
+                                     #:guile
+                                     (package-derivation store (default-guile)
+                                                         system #:graft? #f))))
+                drv))))
 
 (define* (package-output store package
                          #:optional (output "out") (system (%current-system)))
diff --git a/guix/scripts/build.scm b/guix/scripts/build.scm
index cde2a25613..7b7f419f3a 100644
--- a/guix/scripts/build.scm
+++ b/guix/scripts/build.scm
@@ -202,6 +202,7 @@ options handled by 'set-build-options-from-command-line', and listed in
 (define %default-options
   ;; Alist of default option values.
   `((system . ,(%current-system))
+    (graft? . #t)
     (substitutes? . #t)
     (build-hook? . #t)
     (print-build-trace? . #t)
@@ -223,6 +224,8 @@ Build the given PACKAGE-OR-DERIVATION and return their output paths.\n"))
       --with-source=SOURCE
                          use SOURCE when building the corresponding package"))
   (display (_ "
+      --no-grafts        do not graft packages"))
+  (display (_ "
   -d, --derivations      return the derivation paths of the given packages"))
   (display (_ "
   -r, --root=FILE        make FILE a symlink to the result, and register it
@@ -278,6 +281,10 @@ Build the given PACKAGE-OR-DERIVATION and return their output paths.\n"))
          (option '("with-source") #t #f
                  (lambda (opt name arg result)
                    (alist-cons 'with-source arg result)))
+         (option '("no-grafts") #f #f
+                 (lambda (opt name arg result)
+                   (alist-cons 'graft? #f
+                               (alist-delete 'graft? result eq?))))
 
          %standard-build-options))
 
@@ -290,26 +297,28 @@ build."
       (triplet
        (cut package-cross-derivation <> <> triplet <>))))
 
-  (define src? (assoc-ref opts 'source?))
-  (define sys  (assoc-ref opts 'system))
+  (define src?   (assoc-ref opts 'source?))
+  (define sys    (assoc-ref opts 'system))
+  (define graft? (assoc-ref opts 'graft?))
 
-  (let ((opts (options/with-source store
-                                   (options/resolve-packages store opts))))
-    (filter-map (match-lambda
-                 (('argument . (? package? p))
-                  (if src?
-                      (let ((s (package-source p)))
-                        (package-source-derivation store s))
-                      (package->derivation store p sys)))
-                 (('argument . (? derivation? drv))
-                  drv)
-                 (('argument . (? derivation-path? drv))
-                  (call-with-input-file drv read-derivation))
-                 (('argument . (? store-path?))
-                  ;; Nothing to do; maybe for --log-file.
-                  #f)
-                 (_ #f))
-                opts)))
+  (parameterize ((%graft? graft?))
+    (let ((opts (options/with-source store
+                                     (options/resolve-packages store opts))))
+      (filter-map (match-lambda
+                   (('argument . (? package? p))
+                    (if src?
+                        (let ((s (package-source p)))
+                          (package-source-derivation store s))
+                        (package->derivation store p sys)))
+                   (('argument . (? derivation? drv))
+                    drv)
+                   (('argument . (? derivation-path? drv))
+                    (call-with-input-file drv read-derivation))
+                   (('argument . (? store-path?))
+                    ;; Nothing to do; maybe for --log-file.
+                    #f)
+                   (_ #f))
+                  opts))))
 
 (define (options/resolve-packages store opts)
   "Return OPTS with package specification strings replaced by actual
diff --git a/tests/packages.scm b/tests/packages.scm
index 44cdb35c4b..4f700b712f 100644
--- a/tests/packages.scm
+++ b/tests/packages.scm
@@ -33,8 +33,9 @@
   #:use-module (guix build-system gnu)
   #:use-module (gnu packages)
   #:use-module (gnu packages base)
+  #:use-module (gnu packages guile)
   #:use-module (gnu packages bootstrap)
-  #:use-module (srfi srfi-11)
+  #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-34)
   #:use-module (srfi srfi-64)
@@ -47,10 +48,6 @@
 (define %store
   (open-connection-for-tests))
 
-
-
-(test-begin "packages")
-
 (define-syntax-rule (dummy-package name* extra-fields ...)
   (package (name name*) (version "0") (source #f)
            (build-system gnu-build-system)
@@ -58,6 +55,9 @@
            (home-page #f) (license #f)
            extra-fields ...))
 
+
+(test-begin "packages")
+
 (test-assert "printer with location"
   (string-match "^#<package foo-0 foo.scm:42 [[:xdigit:]]+>$"
                 (with-output-to-string
@@ -375,6 +375,80 @@
       (package-cross-derivation %store p "mips64el-linux-gnu")
       #f)))
 
+(test-equal "package-derivation, direct graft"
+  (package-derivation %store gnu-make)
+  (let ((p (package (inherit coreutils)
+             (replacement gnu-make))))
+    (package-derivation %store p)))
+
+(test-equal "package-cross-derivation, direct graft"
+  (package-cross-derivation %store gnu-make "mips64el-linux-gnu")
+  (let ((p (package (inherit coreutils)
+             (replacement gnu-make))))
+    (package-cross-derivation %store p "mips64el-linux-gnu")))
+
+(test-assert "package-grafts, indirect grafts"
+  (let* ((new   (dummy-package "dep"
+                  (arguments '(#:implicit-inputs? #f))))
+         (dep   (package (inherit new) (version "0.0")))
+         (dep*  (package (inherit dep) (replacement new)))
+         (dummy (dummy-package "dummy"
+                  (arguments '(#:implicit-inputs? #f))
+                  (inputs `(("dep" ,dep*))))))
+    (equal? (package-grafts %store dummy)
+            (list (graft
+                    (origin (package-derivation %store dep))
+                    (replacement (package-derivation %store new)))))))
+
+(test-assert "package-grafts, indirect grafts, cross"
+  (let* ((new    (dummy-package "dep"
+                   (arguments '(#:implicit-inputs? #f))))
+         (dep    (package (inherit new) (version "0.0")))
+         (dep*   (package (inherit dep) (replacement new)))
+         (dummy  (dummy-package "dummy"
+                   (arguments '(#:implicit-inputs? #f))
+                   (inputs `(("dep" ,dep*)))))
+         (target "mips64el-linux-gnu"))
+    (equal? (package-grafts %store dummy #:target target)
+            (list (graft
+                    (origin (package-cross-derivation %store dep target))
+                    (replacement
+                     (package-cross-derivation %store new target)))))))
+
+(test-assert "package-grafts, indirect grafts, propagated inputs"
+  (let* ((new   (dummy-package "dep"
+                  (arguments '(#:implicit-inputs? #f))))
+         (dep   (package (inherit new) (version "0.0")))
+         (dep*  (package (inherit dep) (replacement new)))
+         (prop  (dummy-package "propagated"
+                  (propagated-inputs `(("dep" ,dep*)))
+                  (arguments '(#:implicit-inputs? #f))))
+         (dummy (dummy-package "dummy"
+                  (arguments '(#:implicit-inputs? #f))
+                  (inputs `(("prop" ,prop))))))
+    (equal? (package-grafts %store dummy)
+            (list (graft
+                    (origin (package-derivation %store dep))
+                    (replacement (package-derivation %store new)))))))
+
+(test-assert "package-derivation, indirect grafts"
+  (let* ((new   (dummy-package "dep"
+                  (arguments '(#:implicit-inputs? #f))))
+         (dep   (package (inherit new) (version "0.0")))
+         (dep*  (package (inherit dep) (replacement new)))
+         (dummy (dummy-package "dummy"
+                  (arguments '(#:implicit-inputs? #f))
+                  (inputs `(("dep" ,dep*)))))
+         (guile (package-derivation %store (canonical-package guile-2.0)
+                                    #:graft? #f)))
+    (equal? (package-derivation %store dummy)
+            (graft-derivation %store "dummy-0"
+                              (package-derivation %store dummy #:graft? #f)
+                              (package-grafts %store dummy)
+
+                              ;; Use the same Guile as 'package-derivation'.
+                              #:guile guile))))
+
 (test-equal "package->bag"
   `("foo86-hurd" #f (,(package-source gnu-make))
     (,(canonical-package glibc)) (,(canonical-package coreutils)))
@@ -406,17 +480,20 @@
        (eq? package dep)))))
 
 (test-assert "bag->derivation"
-  (let ((bag (package->bag gnu-make))
-        (drv (package-derivation %store gnu-make)))
-    (parameterize ((%current-system "foox86-hurd")) ;should have no effect
-      (equal? drv (bag->derivation %store bag)))))
+  (parameterize ((%graft? #f))
+    (let ((bag (package->bag gnu-make))
+          (drv (package-derivation %store gnu-make)))
+      (parameterize ((%current-system "foox86-hurd")) ;should have no effect
+        (equal? drv (bag->derivation %store bag))))))
 
 (test-assert "bag->derivation, cross-compilation"
-  (let ((bag (package->bag gnu-make (%current-system) "mips64el-linux-gnu"))
-        (drv (package-cross-derivation %store gnu-make "mips64el-linux-gnu")))
-    (parameterize ((%current-system "foox86-hurd") ;should have no effect
-                   (%current-target-system "foo64-linux-gnu"))
-      (equal? drv (bag->derivation %store bag)))))
+  (parameterize ((%graft? #f))
+    (let* ((target "mips64el-linux-gnu")
+           (bag    (package->bag gnu-make (%current-system) target))
+           (drv    (package-cross-derivation %store gnu-make target)))
+      (parameterize ((%current-system "foox86-hurd") ;should have no effect
+                     (%current-target-system "foo64-linux-gnu"))
+        (equal? drv (bag->derivation %store bag))))))
 
 (unless (false-if-exception (getaddrinfo "www.gnu.org" "80" AI_NUMERICSERV))
   (test-skip 1))