summary refs log tree commit diff
path: root/guix/scripts/package.scm
diff options
context:
space:
mode:
Diffstat (limited to 'guix/scripts/package.scm')
-rw-r--r--guix/scripts/package.scm695
1 files changed, 407 insertions, 288 deletions
diff --git a/guix/scripts/package.scm b/guix/scripts/package.scm
index 5c7c165cbb..008ae53b47 100644
--- a/guix/scripts/package.scm
+++ b/guix/scripts/package.scm
@@ -25,6 +25,7 @@
   #:use-module (guix packages)
   #:use-module (guix utils)
   #:use-module (guix config)
+  #:use-module (guix records)
   #:use-module ((guix build utils) #:select (directory-exists? mkdir-p))
   #:use-module ((guix ftp-client) #:select (ftp-open))
   #:use-module (ice-9 ftw)
@@ -33,6 +34,7 @@
   #:use-module (ice-9 regex)
   #:use-module (ice-9 vlist)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-9)
   #:use-module (srfi srfi-11)
   #:use-module (srfi srfi-19)
   #:use-module (srfi srfi-26)
@@ -49,10 +51,10 @@
 
 
 ;;;
-;;; User environment.
+;;; User profile.
 ;;;
 
-(define %user-environment-directory
+(define %user-profile-directory
   (and=> (getenv "HOME")
          (cut string-append <> "/.guix-profile")))
 
@@ -67,30 +69,125 @@
   ;; coexist with Nix profiles.
   (string-append %profile-directory "/guix-profile"))
 
+
+;;;
+;;; Manifests.
+;;;
+
+(define-record-type <manifest>
+  (manifest entries)
+  manifest?
+  (entries manifest-entries))                     ; list of <manifest-entry>
+
+;; Convenient alias, to avoid name clashes.
+(define make-manifest manifest)
+
+(define-record-type* <manifest-entry> manifest-entry
+  make-manifest-entry
+  manifest-entry?
+  (name         manifest-entry-name)              ; string
+  (version      manifest-entry-version)           ; string
+  (output       manifest-entry-output             ; string
+                (default "out"))
+  (path         manifest-entry-path)              ; store path
+  (dependencies manifest-entry-dependencies       ; list of store paths
+                (default '()))
+  (inputs       manifest-entry-inputs             ; list of inputs to build
+                (default '())))                   ; this entry
+
 (define (profile-manifest profile)
   "Return the PROFILE's manifest."
-  (let ((manifest (string-append profile "/manifest")))
-    (if (file-exists? manifest)
-        (call-with-input-file manifest read)
-        '(manifest (version 1) (packages ())))))
+  (let ((file (string-append profile "/manifest")))
+    (if (file-exists? file)
+        (call-with-input-file file read-manifest)
+        (manifest '()))))
+
+(define (manifest->sexp manifest)
+  "Return a representation of MANIFEST as an sexp."
+  (define (entry->sexp entry)
+    (match entry
+      (($ <manifest-entry> name version path output (deps ...))
+       (list name version path output deps))))
 
-(define (manifest-packages manifest)
-  "Return the packages listed in MANIFEST."
   (match manifest
+    (($ <manifest> (entries ...))
+     `(manifest (version 1)
+                (packages ,(map entry->sexp entries))))))
+
+(define (sexp->manifest sexp)
+  "Parse SEXP as a manifest."
+  (match sexp
     (('manifest ('version 0)
                 ('packages ((name version output path) ...)))
-     (zip name version output path
-          (make-list (length name) '())))
+     (manifest
+      (map (lambda (name version output path)
+             (manifest-entry
+              (name name)
+              (version version)
+              (output output)
+              (path path)))
+           name version output path)))
 
     ;; Version 1 adds a list of propagated inputs to the
     ;; name/version/output/path tuples.
     (('manifest ('version 1)
-                ('packages (packages ...)))
-     packages)
+                ('packages ((name version output path deps) ...)))
+     (manifest
+      (map (lambda (name version output path deps)
+             (manifest-entry
+              (name name)
+              (version version)
+              (output output)
+              (path path)
+              (dependencies deps)))
+           name version output path deps)))
 
     (_
      (error "unsupported manifest format" manifest))))
 
+(define (read-manifest port)
+  "Return the packages listed in MANIFEST."
+  (sexp->manifest (read port)))
+
+(define (write-manifest manifest port)
+  "Write MANIFEST to PORT."
+  (write (manifest->sexp manifest) port))
+
+(define (remove-manifest-entry name lst)
+  "Remove the manifest entry named NAME from LST."
+  (remove (match-lambda
+           (($ <manifest-entry> entry-name)
+            (string=? name entry-name)))
+          lst))
+
+(define (manifest-remove manifest names)
+  "Remove entries for each of NAMES from MANIFEST."
+  (make-manifest (fold remove-manifest-entry
+                       (manifest-entries manifest)
+                       names)))
+
+(define (manifest-installed? manifest name)
+  "Return #t if MANIFEST has an entry for NAME, #f otherwise."
+  (define (->bool x)
+    (not (not x)))
+
+  (->bool (find (match-lambda
+                 (($ <manifest-entry> entry-name)
+                  (string=? entry-name name)))
+                (manifest-entries manifest))))
+
+(define (manifest=? m1 m2)
+  "Return #t if manifests M1 and M2 are equal.  This differs from 'equal?' in
+that the 'inputs' field is ignored for the comparison, since it is know to
+have no effect on the manifest contents."
+  (equal? (manifest->sexp m1)
+          (manifest->sexp m2)))
+
+
+;;;
+;;; Profiles.
+;;;
+
 (define (profile-regexp profile)
   "Return a regular expression that matches PROFILE's name and number."
   (make-regexp (string-append "^" (regexp-quote (basename profile))
@@ -157,17 +254,9 @@ case when generations have been deleted (there are \"holes\")."
         0
         (generation-numbers profile)))
 
-(define (profile-derivation store packages)
-  "Return a derivation that builds a profile (a user environment) with
-all of PACKAGES, a list of name/version/output/path/deps tuples."
-  (define packages*
-    ;; Turn any package object in PACKAGES into its output path.
-    (map (match-lambda
-          ((name version output path (deps ...))
-           `(,name ,version ,output ,path
-                   ,(map input->name+path deps))))
-         packages))
-
+(define (profile-derivation store manifest)
+  "Return a derivation that builds a profile (aka. 'user environment') with
+the given MANIFEST."
   (define builder
     `(begin
        (use-modules (ice-9 pretty-print)
@@ -178,33 +267,29 @@ all of PACKAGES, a list of name/version/output/path/deps tuples."
 
        (let ((output (assoc-ref %outputs "out"))
              (inputs (map cdr %build-inputs)))
-         (format #t "building user environment `~a' with ~a packages...~%"
+         (format #t "building profile '~a' with ~a packages...~%"
                  output (length inputs))
-         (union-build output inputs)
+         (union-build output inputs
+                      #:log-port (%make-void-port "w"))
          (call-with-output-file (string-append output "/manifest")
            (lambda (p)
-             (pretty-print '(manifest (version 1)
-                                      (packages ,packages*))
-                           p))))))
-
-  (define ensure-valid-input
-    ;; If a package object appears in the given input, turn it into a
-    ;; derivation path.
-    (match-lambda
-     ((name (? package? p) sub-drv ...)
-      `(,name ,(package-derivation (%store) p) ,@sub-drv))
-     (input
-      input)))
-
-  (build-expression->derivation store "user-environment"
+             (pretty-print ',(manifest->sexp manifest) p))))))
+
+  (build-expression->derivation store "profile"
                                 (%current-system)
                                 builder
                                 (append-map (match-lambda
-                                             ((name version output path deps)
-                                              `((,name ,path)
-                                                ,@(map ensure-valid-input
-                                                       deps))))
-                                            packages)
+                                             (($ <manifest-entry> name version
+                                                 output path deps (inputs ..1))
+                                              (map (cute lower-input
+                                                         (%store) <>)
+                                                   inputs))
+                                             (($ <manifest-entry> name version
+                                                 output path deps)
+                                              ;; Assume PATH and DEPS are
+                                              ;; already valid.
+                                              `((,name ,path) ,@deps)))
+                                            (manifest-entries manifest))
                                 #:modules '((guix build union))))
 
 (define (generation-number profile)
@@ -214,9 +299,13 @@ all of PACKAGES, a list of name/version/output/path/deps tuples."
              (compose string->number (cut match:substring <> 1)))
       0))
 
+(define (generation-file-name profile generation)
+  "Return the file name for PROFILE's GENERATION."
+  (format #f "~a-~a-link" profile generation))
+
 (define (link-to-empty-profile generation)
   "Link GENERATION, a string, to the empty profile."
-  (let* ((drv  (profile-derivation (%store) '()))
+  (let* ((drv  (profile-derivation (%store) (manifest '())))
          (prof (derivation->output-path drv "out")))
     (when (not (build-derivations (%store) (list drv)))
           (leave (_ "failed to build the empty profile~%")))
@@ -227,8 +316,7 @@ all of PACKAGES, a list of name/version/output/path/deps tuples."
   "Atomically switch PROFILE to the previous generation."
   (let* ((number              (generation-number profile))
          (previous-number     (previous-generation-number profile number))
-         (previous-generation (format #f "~a-~a-link"
-                                      profile previous-number)))
+         (previous-generation (generation-file-name profile previous-number)))
     (format #t (_ "switching from generation ~a to ~a~%")
             number previous-number)
     (switch-symlinks profile previous-generation)))
@@ -237,8 +325,7 @@ all of PACKAGES, a list of name/version/output/path/deps tuples."
   "Roll back to the previous generation of PROFILE."
   (let* ((number              (generation-number profile))
          (previous-number     (previous-generation-number profile number))
-         (previous-generation (format #f "~a-~a-link"
-                                      profile previous-number))
+         (previous-generation (generation-file-name profile previous-number))
          (manifest            (string-append previous-generation "/manifest")))
     (cond ((not (file-exists? profile))                 ; invalid profile
            (leave (_ "profile '~a' does not exist~%")
@@ -256,7 +343,7 @@ all of PACKAGES, a list of name/version/output/path/deps tuples."
 (define (generation-time profile number)
   "Return the creation time of a generation in the UTC format."
   (make-time time-utc 0
-             (stat:ctime (stat (format #f "~a-~a-link" profile number)))))
+             (stat:ctime (stat (generation-file-name profile number)))))
 
 (define* (matching-generations str #:optional (profile %current-profile)
                                #:key (duration-relation <=))
@@ -325,8 +412,8 @@ DURATION-RELATION with the current time."
         (else #f)))
 
 (define (find-packages-by-description rx)
-  "Search in SYNOPSIS and DESCRIPTION using RX.  Return a list of
-matching packages."
+  "Return the list of packages whose name, synopsis, or description matches
+RX."
   (define (same-location? p1 p2)
     ;; Compare locations of two packages.
     (equal? (package-location p1) (package-location p2)))
@@ -337,7 +424,8 @@ matching packages."
                      (define matches?
                        (cut regexp-exec rx <>))
 
-                     (if (or (and=> (package-synopsis package)
+                     (if (or (matches? (gettext (package-name package)))
+                             (and=> (package-synopsis package)
                                     (compose matches? gettext))
                              (and=> (package-description package)
                                     (compose matches? gettext)))
@@ -349,6 +437,16 @@ matching packages."
                 (package-name p2))))
    same-location?))
 
+(define* (lower-input store input #:optional (system (%current-system)))
+  "Lower INPUT so that it contains derivations instead of packages."
+  (match input
+    ((name (? package? package))
+     `(,name ,(package-derivation store package system)))
+    ((name (? package? package) output)
+     `(,name ,(package-derivation store package system)
+             ,output))
+    (_ input)))
+
 (define (input->name+path input)
   "Convert the name/package/sub-drv tuple INPUT to a name/store-path tuple."
   (let loop ((input input))
@@ -402,6 +500,76 @@ return its return value."
        (format (current-error-port) "  interrupted by signal ~a~%" SIGINT)
        #f))))
 
+
+;;;
+;;; Package specifications.
+;;;
+
+(define newest-available-packages
+  (memoize find-newest-available-packages))
+
+(define (find-best-packages-by-name name version)
+  "If version is #f, return the list of packages named NAME with the highest
+version numbers; otherwise, return the list of packages named NAME and at
+VERSION."
+  (if version
+      (find-packages-by-name name version)
+      (match (vhash-assoc name (newest-available-packages))
+        ((_ version pkgs ...) pkgs)
+        (#f '()))))
+
+(define* (specification->package+output spec #:optional (output "out"))
+  "Find the package and output specified by SPEC, or #f and #f; SPEC may
+optionally contain a version number and an output name, as in these examples:
+
+  guile
+  guile-2.0.9
+  guile:debug
+  guile-2.0.9:debug
+
+If SPEC does not specify a version number, return the preferred newest
+version; if SPEC does not specify an output, return OUTPUT."
+  (define (ensure-output p sub-drv)
+    (if (member sub-drv (package-outputs p))
+        sub-drv
+        (leave (_ "package `~a' lacks output `~a'~%")
+               (package-full-name p)
+               sub-drv)))
+
+  (let*-values (((name sub-drv)
+                 (match (string-rindex spec #\:)
+                   (#f    (values spec output))
+                   (colon (values (substring spec 0 colon)
+                                  (substring spec (+ 1 colon))))))
+                ((name version)
+                 (package-name->name+version name)))
+    (match (find-best-packages-by-name name version)
+      ((p)
+       (values p (ensure-output p sub-drv)))
+      ((p p* ...)
+       (warning (_ "ambiguous package specification `~a'~%")
+                spec)
+       (warning (_ "choosing ~a from ~a~%")
+                (package-full-name p)
+                (location->string (package-location p)))
+       (values p (ensure-output p sub-drv)))
+      (()
+       (leave (_ "~a: package not found~%") spec)))))
+
+(define (upgradeable? name current-version current-path)
+  "Return #t if there's a version of package NAME newer than CURRENT-VERSION,
+or if the newest available version is equal to CURRENT-VERSION but would have
+an output path different than CURRENT-PATH."
+  (match (vhash-assoc name (newest-available-packages))
+    ((_ candidate-version pkg . rest)
+     (case (version-compare candidate-version current-version)
+       ((>) #t)
+       ((<) #f)
+       ((=) (let ((candidate-path (derivation->output-path
+                                   (package-derivation (%store) pkg))))
+              (not (string=? current-path candidate-path))))))
+    (#f #f)))
+
 (define ftp-open*
   ;; Memoizing version of `ftp-open'.  The goal is to avoid initiating a new
   ;; FTP connection for each package, esp. since most of them are to the same
@@ -437,26 +605,31 @@ but ~a is available upstream~%")
         ((getaddrinfo-error ftp-error) #f)
         (else (apply throw key args))))))
 
-(define* (search-path-environment-variables packages profile
+
+;;;
+;;; Search paths.
+;;;
+
+(define* (search-path-environment-variables entries profile
                                             #:optional (getenv getenv))
   "Return environment variable definitions that may be needed for the use of
-PACKAGES in PROFILE.  Use GETENV to determine the current settings and report
-only settings not already effective."
+ENTRIES, a list of manifest entries, in PROFILE.  Use GETENV to determine the
+current settings and report only settings not already effective."
 
   ;; Prefer ~/.guix-profile to the real profile directory name.
-  (let ((profile (if (and %user-environment-directory
+  (let ((profile (if (and %user-profile-directory
                           (false-if-exception
-                           (string=? (readlink %user-environment-directory)
+                           (string=? (readlink %user-profile-directory)
                                      profile)))
-                     %user-environment-directory
+                     %user-profile-directory
                      profile)))
 
     ;; The search path info is not stored in the manifest.  Thus, we infer the
     ;; search paths from same-named packages found in the distro.
 
-    (define package-in-manifest->package
+    (define manifest-entry->package
       (match-lambda
-       ((name version _ ...)
+       (($ <manifest-entry> name version)
         (match (append (find-packages-by-name name version)
                        (find-packages-by-name name))
           ((p _ ...) p)
@@ -478,16 +651,16 @@ only settings not already effective."
                       variable
                       (string-join directories separator)))))))
 
-    (let* ((packages     (filter-map package-in-manifest->package packages))
+    (let* ((packages     (filter-map manifest-entry->package entries))
            (search-paths (delete-duplicates
                           (append-map package-native-search-paths
                                       packages))))
       (filter-map search-path-definition search-paths))))
 
-(define (display-search-paths packages profile)
+(define (display-search-paths entries profile)
   "Display the search path environment variables that may need to be set for
-PACKAGES, in the context of PROFILE."
-  (let ((settings (search-path-environment-variables packages profile)))
+ENTRIES, a list of manifest entries, in the context of PROFILE."
+  (let ((settings (search-path-environment-variables entries profile)))
     (unless (null? settings)
       (format #t (_ "The following environment variable definitions may be needed:~%"))
       (format #t "~{   ~a~%~}" settings))))
@@ -633,6 +806,110 @@ Install, remove, or upgrade PACKAGES in a single transaction.\n"))
                   (cons `(query list-available ,(or arg ""))
                         result)))))
 
+(define (options->installable opts manifest)
+  "Given MANIFEST, the current manifest, and OPTS, the result of 'args-fold',
+return the new list of manifest entries."
+  (define (deduplicate deps)
+    ;; Remove duplicate entries from DEPS, a list of propagated inputs, where
+    ;; each input is a name/path tuple.
+    (define (same? d1 d2)
+      (match d1
+        ((_ p1)
+         (match d2
+           ((_ p2) (eq? p1 p2))
+           (_      #f)))
+        ((_ p1 out1)
+         (match d2
+           ((_ p2 out2)
+            (and (string=? out1 out2)
+                 (eq? p1 p2)))
+           (_ #f)))))
+
+    (delete-duplicates deps same?))
+
+  (define (package->manifest-entry p output)
+    ;; Return a manifest entry for the OUTPUT of package P.
+    (check-package-freshness p)
+    ;; When given a package via `-e', install the first of its
+    ;; outputs (XXX).
+    (let* ((output (or output (car (package-outputs p))))
+           (path   (package-output (%store) p output))
+           (deps   (deduplicate (package-transitive-propagated-inputs p))))
+      (manifest-entry
+       (name (package-name p))
+       (version (package-version p))
+       (output output)
+       (path path)
+       (dependencies (map input->name+path deps))
+       (inputs (cons (list (package-name p) p output)
+                     deps)))))
+
+  (define upgrade-regexps
+    (filter-map (match-lambda
+                 (('upgrade . regexp)
+                  (make-regexp (or regexp "")))
+                 (_ #f))
+                opts))
+
+  (define packages-to-upgrade
+    (match upgrade-regexps
+      (()
+       '())
+      ((_ ...)
+       (let ((newest (find-newest-available-packages)))
+         (filter-map (match-lambda
+                      (($ <manifest-entry> name version output path _)
+                       (and (any (cut regexp-exec <> name)
+                                 upgrade-regexps)
+                            (upgradeable? name version path)
+                            (let ((output (or output "out")))
+                              (call-with-values
+                                  (lambda ()
+                                    (specification->package+output name output))
+                                list))))
+                      (_ #f))
+                     (manifest-entries manifest))))))
+
+  (define to-upgrade
+    (map (match-lambda
+          ((package output)
+           (package->manifest-entry package output)))
+         packages-to-upgrade))
+
+  (define packages-to-install
+    (filter-map (match-lambda
+                 (('install . (? package? p))
+                  (list p "out"))
+                 (('install . (? string? spec))
+                  (and (not (store-path? spec))
+                       (let-values (((package output)
+                                     (specification->package+output spec)))
+                         (and package (list package output)))))
+                 (_ #f))
+                opts))
+
+  (define to-install
+    (append (map (match-lambda
+                  ((package output)
+                   (package->manifest-entry package output)))
+                 packages-to-install)
+            (filter-map (match-lambda
+                         (('install . (? package?))
+                          #f)
+                         (('install . (? store-path? path))
+                          (let-values (((name version)
+                                        (package-name->name+version
+                                         (store-path-package-name path))))
+                            (manifest-entry
+                             (name name)
+                             (version version)
+                             (output #f)
+                             (path path))))
+                         (_ #f))
+                        opts)))
+
+  (append to-upgrade to-install))
+
 
 ;;;
 ;;; Entry point.
@@ -653,67 +930,6 @@ Install, remove, or upgrade PACKAGES in a single transaction.\n"))
     (let ((out (derivation->output-path (%guile-for-build))))
       (not (valid-path? (%store) out))))
 
-  (define newest-available-packages
-    (memoize find-newest-available-packages))
-
-  (define (find-best-packages-by-name name version)
-    (if version
-        (find-packages-by-name name version)
-        (match (vhash-assoc name (newest-available-packages))
-          ((_ version pkgs ...) pkgs)
-          (#f '()))))
-
-  (define* (find-package name #:optional (output "out"))
-    ;; Find the package NAME; NAME may contain a version number and a
-    ;; sub-derivation name.  If the version number is not present,
-    ;; return the preferred newest version.  If the sub-derivation name is not
-    ;; present, use OUTPUT.
-    (define request name)
-
-    (define (ensure-output p sub-drv)
-      (if (member sub-drv (package-outputs p))
-          p
-          (leave (_ "package `~a' lacks output `~a'~%")
-                 (package-full-name p)
-                 sub-drv)))
-
-    (let*-values (((name sub-drv)
-                   (match (string-rindex name #\:)
-                     (#f    (values name output))
-                     (colon (values (substring name 0 colon)
-                                    (substring name (+ 1 colon))))))
-                  ((name version)
-                   (package-name->name+version name)))
-      (match (find-best-packages-by-name name version)
-        ((p)
-         (list name (package-version p) sub-drv (ensure-output p sub-drv)
-               (package-transitive-propagated-inputs p)))
-        ((p p* ...)
-         (warning (_ "ambiguous package specification `~a'~%")
-                  request)
-         (warning (_ "choosing ~a from ~a~%")
-                  (package-full-name p)
-                  (location->string (package-location p)))
-         (list name (package-version p) sub-drv (ensure-output p sub-drv)
-               (package-transitive-propagated-inputs p)))
-        (()
-         (leave (_ "~a: package not found~%") request)))))
-
-  (define (upgradeable? name current-version current-path)
-    ;; Return #t if there's a version of package NAME newer than
-    ;; CURRENT-VERSION, or if the newest available version is equal to
-    ;; CURRENT-VERSION but would have an output path different than
-    ;; CURRENT-PATH.
-    (match (vhash-assoc name (newest-available-packages))
-      ((_ candidate-version pkg . rest)
-       (case (version-compare candidate-version current-version)
-         ((>) #t)
-         ((<) #f)
-         ((=) (let ((candidate-path (derivation->output-path
-                                     (package-derivation (%store) pkg))))
-                (not (string=? current-path candidate-path))))))
-      (#f #f)))
-
   (define (ensure-default-profile)
     ;; Ensure the default profile symlink and directory exist and are
     ;; writable.
@@ -725,11 +941,11 @@ more information.~%"))
       (exit 1))
 
     ;; Create ~/.guix-profile if it doesn't exist yet.
-    (when (and %user-environment-directory
+    (when (and %user-profile-directory
                %current-profile
                (not (false-if-exception
-                     (lstat %user-environment-directory))))
-      (symlink %current-profile %user-environment-directory))
+                     (lstat %user-profile-directory))))
+      (symlink %current-profile %user-profile-directory))
 
     (let ((s (stat %profile-directory #f)))
       ;; Attempt to create /…/profiles/per-user/$USER if needed.
@@ -767,48 +983,17 @@ more information.~%"))
     (define verbose? (assoc-ref opts 'verbose?))
     (define profile  (assoc-ref opts 'profile))
 
-    (define (canonicalize-deps deps)
-      ;; Remove duplicate entries from DEPS, a list of propagated inputs,
-      ;; where each input is a name/path tuple.
-      (define (same? d1 d2)
-        (match d1
-          ((_ p1)
-           (match d2
-             ((_ p2) (eq? p1 p2))
-             (_      #f)))
-          ((_ p1 out1)
-           (match d2
-             ((_ p2 out2)
-              (and (string=? out1 out2)
-                   (eq? p1 p2)))
-             (_ #f)))))
-
-      (delete-duplicates deps same?))
-
-    (define (same-package? tuple name out)
-      (match tuple
-        ((tuple-name _ tuple-output _ ...)
-         (and (equal? name tuple-name)
-              (equal? out tuple-output)))))
-
-    (define (package->tuple p)
-      ;; Convert package P to a tuple.
-      ;; When given a package via `-e', install the first of its
-      ;; outputs (XXX).
-      (let* ((out  (car (package-outputs p)))
-             (path (package-output (%store) p out))
-             (deps (package-transitive-propagated-inputs p)))
-        `(,(package-name p)
-          ,(package-version p)
-          ,out
-          ,p
-          ,(canonicalize-deps deps))))
+    (define (same-package? entry name output)
+      (match entry
+        (($ <manifest-entry> entry-name _ entry-output _ ...)
+         (and (equal? name entry-name)
+              (equal? output entry-output)))))
 
     (define (show-what-to-remove/install remove install dry-run?)
       ;; Tell the user what's going to happen in high-level terms.
       ;; TODO: Report upgrades more clearly.
       (match remove
-        (((name version _ path _) ..1)
+        ((($ <manifest-entry> name version _ path _) ..1)
          (let ((len    (length name))
                (remove (map (cut format #f "  ~a-~a\t~a" <> <> <>)
                             name version path)))
@@ -825,7 +1010,7 @@ more information.~%"))
                        remove))))
         (_ #f))
       (match install
-        (((name version output path _) ..1)
+        ((($ <manifest-entry> name version output path _) ..1)
          (let ((len     (length name))
                (install (map (cut format #f "   ~a-~a\t~a\t~a" <> <> <> <>)
                              name version output path)))
@@ -846,15 +1031,15 @@ more information.~%"))
       (generation-number profile))
 
     (define (display-and-delete number)
-      (let ((generation (format #f "~a-~a-link" profile number)))
+      (let ((generation (generation-file-name profile number)))
         (unless (zero? number)
           (format #t (_ "deleting ~a~%") generation)
           (delete-file generation))))
 
     (define (delete-generation number)
       (let* ((previous-number (previous-generation-number profile number))
-             (previous-generation (format #f "~a-~a-link"
-                                          profile previous-number)))
+             (previous-generation
+              (generation-file-name profile previous-number)))
         (cond ((zero? number))  ; do not delete generation 0
               ((and (= number current-generation-number)
                     (not (file-exists? previous-generation)))
@@ -909,126 +1094,59 @@ more information.~%"))
              (_ #f))
             opts))
           (else
-           (let* ((installed (manifest-packages (profile-manifest profile)))
-                  (upgrade-regexps (filter-map (match-lambda
-                                                (('upgrade . regexp)
-                                                 (make-regexp (or regexp "")))
-                                                (_ #f))
-                                               opts))
-                  (upgrade (if (null? upgrade-regexps)
-                               '()
-                               (let ((newest (find-newest-available-packages)))
-                                 (filter-map
-                                  (match-lambda
-                                   ((name version output path _)
-                                    (and (any (cut regexp-exec <> name)
-                                              upgrade-regexps)
-                                         (upgradeable? name version path)
-                                         (find-package name
-                                                       (or output "out"))))
-                                   (_ #f))
-                                  installed))))
-                  (install (append
-                            upgrade
-                            (filter-map (match-lambda
-                                         (('install . (? package? p))
-                                          (package->tuple p))
-                                         (('install . (? store-path?))
-                                          #f)
-                                         (('install . package)
-                                          (find-package package))
+           (let* ((manifest (profile-manifest profile))
+                  (install* (options->installable opts manifest))
+                  (remove   (filter-map (match-lambda
+                                         (('remove . package)
+                                          package)
                                          (_ #f))
-                                        opts)))
-                  (drv (filter-map (match-lambda
-                                    ((name version sub-drv
-                                           (? package? package)
-                                           (deps ...))
-                                     (check-package-freshness package)
-                                     (package-derivation (%store) package))
-                                    (_ #f))
-                                   install))
-                  (install*
-                   (append
-                    (filter-map (match-lambda
-                                 (('install . (? package? p))
-                                  #f)
-                                 (('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 _ (deps ...))
-                                   (let ((output-path
-                                          (derivation->output-path
-                                           drv sub-drv)))
-                                     `(,name ,version ,sub-drv ,output-path
-                                             ,(canonicalize-deps deps))))))
-                         install drv)))
-                  (remove (filter-map (match-lambda
-                                       (('remove . package)
-                                        package)
-                                        (_ #f))
-                                      opts))
-                  (remove* (filter-map (cut assoc <> installed) remove))
-                  (packages
+                                        opts))
+                  (remove*  (filter (cut manifest-installed? manifest <>)
+                                    remove))
+                  (entries
                    (append install*
                            (fold (lambda (package result)
                                    (match package
-                                          ((name _ out _ ...)
-                                           (filter (negate
-                                                    (cut same-package? <>
-                                                         name out))
-                                                   result))))
-                                 (fold alist-delete installed remove)
-                                 install*))))
-
-          (when (equal? profile %current-profile)
-            (ensure-default-profile))
-
-          (show-what-to-remove/install remove* install* dry-run?)
-          (show-what-to-build (%store) drv
-                              #:use-substitutes? (assoc-ref opts 'substitutes?)
-                              #:dry-run? dry-run?)
-
-          (or dry-run?
-              (and (build-derivations (%store) drv)
-                   (let* ((prof-drv (profile-derivation (%store) packages))
-                          (prof     (derivation->output-path prof-drv))
-                          (old-drv  (profile-derivation
-                                     (%store) (manifest-packages
-                                               (profile-manifest profile))))
-                          (old-prof (derivation->output-path old-drv))
-                          (number   (generation-number profile))
-
-                          ;; Always use NUMBER + 1 for the new profile,
-                          ;; possibly overwriting a "previous future
-                          ;; generation".
-                          (name     (format #f "~a-~a-link"
-                                            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)))
-                              (let ((count (length packages)))
+                                     (($ <manifest-entry> name _ out _ ...)
+                                      (filter (negate
+                                               (cut same-package? <>
+                                                    name out))
+                                              result))))
+                                 (manifest-entries
+                                  (manifest-remove manifest remove))
+                                 install*)))
+                  (new      (make-manifest entries)))
+
+             (when (equal? profile %current-profile)
+               (ensure-default-profile))
+
+             (if (manifest=? new manifest)
+                 (format (current-error-port) (_ "nothing to be done~%"))
+                 (let ((prof-drv (profile-derivation (%store) new)))
+                   (show-what-to-remove/install remove* install* dry-run?)
+                   (show-what-to-build (%store) (list prof-drv)
+                                       #:use-substitutes?
+                                       (assoc-ref opts 'substitutes?)
+                                       #:dry-run? dry-run?)
+
+                   (or dry-run?
+                       (let* ((prof   (derivation->output-path prof-drv))
+                              (number (generation-number profile))
+
+                              ;; Always use NUMBER + 1 for the new profile,
+                              ;; possibly overwriting a "previous future
+                              ;; generation".
+                              (name   (generation-file-name profile
+                                                            (+ 1 number))))
+                         (and (build-derivations (%store) (list prof-drv))
+                              (let ((count (length entries)))
                                 (switch-symlinks name prof)
                                 (switch-symlinks profile name)
                                 (format #t (N_ "~a package in profile~%"
                                                "~a packages in profile~%"
                                                count)
                                         count)
-                                (display-search-paths packages
+                                (display-search-paths entries
                                                       profile)))))))))))
 
   (define (process-query opts)
@@ -1049,15 +1167,15 @@ more information.~%"))
                    (format #t (_ "~a\t(current)~%") header)
                    (format #t "~a~%" header)))
              (for-each (match-lambda
-                        ((name version output location _)
+                        (($ <manifest-entry> name version output location _)
                          (format #t "  ~a\t~a\t~a\t~a~%"
                                  name version output location)))
 
                        ;; Show most recently installed packages last.
                        (reverse
-                        (manifest-packages
+                        (manifest-entries
                          (profile-manifest
-                          (format #f "~a-~a-link" profile number)))))
+                          (generation-file-name profile number)))))
              (newline)))
 
          (cond ((not (file-exists? profile)) ; XXX: race condition
@@ -1082,9 +1200,9 @@ more information.~%"))
         (('list-installed regexp)
          (let* ((regexp    (and regexp (make-regexp regexp)))
                 (manifest  (profile-manifest profile))
-                (installed (manifest-packages manifest)))
+                (installed (manifest-entries manifest)))
            (for-each (match-lambda
-                      ((name version output path _)
+                      (($ <manifest-entry> name version output path _)
                        (when (or (not regexp)
                                  (regexp-exec regexp name))
                          (format #t "~a\t~a\t~a\t~a~%"
@@ -1125,9 +1243,9 @@ more information.~%"))
 
         (('search-paths)
          (let* ((manifest (profile-manifest profile))
-                (packages (manifest-packages manifest))
-                (settings (search-path-environment-variables packages
-                                                             profile
+                (entries  (manifest-entries manifest))
+                (packages (map manifest-entry-name entries))
+                (settings (search-path-environment-variables entries profile
                                                              (const #f))))
            (format #t "~{~a~%~}" settings)
            #t))
@@ -1139,6 +1257,7 @@ more information.~%"))
         (with-error-handling
           (parameterize ((%store (open-connection)))
             (set-build-options (%store)
+                               #:print-build-trace #f
                                #:fallback? (assoc-ref opts 'fallback?)
                                #:use-substitutes?
                                (assoc-ref opts 'substitutes?)