guix-devel/emacs/guix-main.scm

604 lines
22 KiB
Scheme
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2014 Alex Kost <alezost@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Information about packages and generations is passed to the elisp
;; side in the form of alists of parameters (such as name or
;; version) and their values. These alists are called "entries" in
;; this code. So to distinguish, just "package" in the name of a
;; function means a guile object ("package" record) while
;; "package entry" means alist of package parameters and values (see
;; package-param-alist).
;;
;; "Entry" is probably not the best name for such alists, because there
;; already exists "manifest-entry" which has nothing to do with the
;; "entry" described above. Do not be confused :)
;; get-entries function is the “entry point” for the elisp side to get
;; information about packages and generations.
;; Since name/version pair is not necessarily unique, we use
;; `object-address' to identify a package (for id parameter), if
;; possible. However for the obsolete packages (that can be found in
;; installed manifest but not in a package directory), id parameter is
;; still "name-version" string. So id package parameter in the code
;; below is either an object-address number or a full-name string.
;;
;; Important: as object addresses live only during guile session, elisp
;; part should take care about updating information after "Guix REPL" is
;; restarted (TODO!)
;;
;; installed parameter of a package entry contains information about
;; installed outputs. It is a list of "installed entries" (see
;; package-installed-param-alist).
;; To speed-up the process of getting information, the following
;; auxiliary variables are used:
;;
;; - `%packages' - VHash of "package address"/"package" pairs.
;;
;; - `%package-table' - Hash table of
;; "name+version key"/"list of packages" pairs.
;;
;; - `%current-manifest-entries-table' - Hash table of
;; "name+version key"/"list of manifest entries" pairs. This variable
;; is set by `set-current-manifest-maybe!' when it is needed.
;;; Code:
(use-modules
(ice-9 vlist)
(ice-9 match)
(srfi srfi-1)
(srfi srfi-11)
(srfi srfi-19)
(srfi srfi-26)
(guix)
(guix packages)
(guix profiles)
(guix licenses)
(guix utils)
(guix ui)
(guix scripts package)
(gnu packages))
(define-syntax-rule (first-or-false lst)
(and (not (null? lst))
(first lst)))
(define full-name->name+version package-name->name+version)
(define (name+version->full-name name version)
(string-append name "-" version))
(define* (make-package-specification name #:optional version output)
(let ((full-name (if version
(name+version->full-name name version)
name)))
(if output
(string-append full-name ":" output)
full-name)))
(define name+version->key cons)
(define key->name+version car+cdr)
(define %current-manifest #f)
(define %current-manifest-entries-table #f)
(define %packages
(fold-packages (lambda (pkg res)
(vhash-consq (object-address pkg) pkg res))
vlist-null))
(define %package-table
(let ((table (make-hash-table (vlist-length %packages))))
(vlist-for-each
(lambda (elem)
(match elem
((address . pkg)
(let* ((key (name+version->key (package-name pkg)
(package-version pkg)))
(ref (hash-ref table key)))
(hash-set! table key
(if ref (cons pkg ref) (list pkg)))))))
%packages)
table))
;; FIXME get rid of this function!
(define (set-current-manifest-maybe! profile)
(define (manifest-entries->hash-table entries)
(let ((entries-table (make-hash-table (length entries))))
(for-each (lambda (entry)
(let* ((key (name+version->key
(manifest-entry-name entry)
(manifest-entry-version entry)))
(ref (hash-ref entries-table key)))
(hash-set! entries-table key
(if ref (cons entry ref) (list entry)))))
entries)
entries-table))
(when profile
(let ((manifest (profile-manifest profile)))
(unless (and (manifest? %current-manifest)
(equal? manifest %current-manifest))
(set! %current-manifest manifest)
(set! %current-manifest-entries-table
(manifest-entries->hash-table
(manifest-entries manifest)))))))
(define (manifest-entries-by-name+version name version)
(or (hash-ref %current-manifest-entries-table
(name+version->key name version))
'()))
(define (packages-by-name+version name version)
(or (hash-ref %package-table
(name+version->key name version))
'()))
(define (packages-by-full-name full-name)
(call-with-values
(lambda () (full-name->name+version full-name))
packages-by-name+version))
(define (package-by-address address)
(and=> (vhash-assq address %packages)
cdr))
(define (packages-by-id id)
(if (integer? id)
(let ((pkg (package-by-address id)))
(if pkg (list pkg) '()))
(packages-by-full-name id)))
(define (package-by-id id)
(first-or-false (packages-by-id id)))
(define (newest-package-by-id id)
(and=> (id->name+version id)
(lambda (name)
(first-or-false (find-best-packages-by-name name #f)))))
(define (id->name+version id)
(if (integer? id)
(and=> (package-by-address id)
(lambda (pkg)
(values (package-name pkg)
(package-version pkg))))
(full-name->name+version id)))
(define (fold-manifest-entries proc init)
"Fold over `%current-manifest-entries-table'.
Call (PROC NAME VERSION ENTRIES RESULT) for each element of the hash
table, using INIT as the initial value of RESULT."
(hash-fold (lambda (key entries res)
(let-values (((name version) (key->name+version key)))
(proc name version entries res)))
init
%current-manifest-entries-table))
(define (fold-object proc init obj)
(fold proc init
(if (list? obj) obj (list obj))))
(define* (object-transformer param-alist #:optional (params '()))
"Return function for transforming an object into alist of parameters/values.
PARAM-ALIST is alist of available object parameters (symbols) and functions
returning values of these parameters. Each function is called with object as
a single argument.
PARAMS is list of parameters from PARAM-ALIST that should be returned by a
resulting function. If PARAMS is not specified or is an empty list, use all
available parameters.
Example:
(let ((alist `((plus1 . ,1+) (minus1 . ,1-) (mul2 . ,(cut * 2 <>))))
(number->alist (object-transformer alist '(plus1 mul2))))
(number->alist 8))
=>
((plus1 . 9) (mul2 . 16))
"
(let ((alist (let ((use-all-params (null? params)))
(filter-map (match-lambda
((param . fun)
(and (or use-all-params
(memq param params))
(cons param fun)))
(_ #f))
param-alist))))
(lambda (object)
(map (match-lambda
((param . fun)
(cons param (fun object))))
alist))))
(define package-installed-param-alist
(list
(cons 'output manifest-entry-output)
(cons 'path manifest-entry-item)
(cons 'dependencies manifest-entry-dependencies)))
(define manifest-entry->installed-entry
(object-transformer package-installed-param-alist))
(define (manifest-entries->installed-entries entries)
(map manifest-entry->installed-entry entries))
(define (installed-entries-by-name+version name version)
(manifest-entries->installed-entries
(manifest-entries-by-name+version name version)))
(define (installed-entries-by-package package)
(installed-entries-by-name+version (package-name package)
(package-version package)))
(define (package-inputs-names inputs)
"Return list of full names of the packages from package INPUTS."
(filter-map (match-lambda
((_ (? package? package))
(package-full-name package))
(_ #f))
inputs))
(define (package-license-names package)
"Return list of license names of the PACKAGE."
(fold-object (lambda (license res)
(if (license? license)
(cons (license-name license) res)
res))
'()
(package-license package)))
(define (package-unique? package)
"Return #t if PACKAGE is a single package with such name/version."
(null? (cdr (packages-by-name+version (package-name package)
(package-version package)))))
(define package-param-alist
(list
(cons 'id object-address)
(cons 'name package-name)
(cons 'version package-version)
(cons 'license package-license-names)
(cons 'synopsis package-synopsis)
(cons 'description package-description)
(cons 'home-url package-home-page)
(cons 'outputs package-outputs)
(cons 'non-unique (negate package-unique?))
(cons 'inputs (lambda (pkg) (package-inputs-names
(package-inputs pkg))))
(cons 'native-inputs (lambda (pkg) (package-inputs-names
(package-native-inputs pkg))))
(cons 'propagated-inputs (lambda (pkg) (package-inputs-names
(package-propagated-inputs pkg))))
(cons 'location (lambda (pkg) (location->string
(package-location pkg))))
(cons 'installed installed-entries-by-package)))
(define (package-param package param)
"Return the value of a PACKAGE PARAM."
(define (accessor param)
(and=> (assq param package-param-alist)
cdr))
(and=> (accessor param)
(cut <> package)))
(define (matching-package-entries ->entry predicate)
"Return list of package entries for the matching packages.
PREDICATE is called on each package."
(fold-packages (lambda (pkg res)
(if (predicate pkg)
(cons (->entry pkg) res)
res))
'()))
(define (make-obsolete-package-entry name version entries)
"Return package entry for an obsolete package with NAME and VERSION.
ENTRIES is a list of manifest entries used to get installed info."
`((id . ,(name+version->full-name name version))
(name . ,name)
(version . ,version)
(outputs . ,(map manifest-entry-output entries))
(obsolete . #t)
(installed . ,(manifest-entries->installed-entries entries))))
(define (package-entries-by-name+version ->entry name version)
"Return list of package entries for packages with NAME and VERSION."
(let ((packages (packages-by-name+version name version)))
(if (null? packages)
(let ((entries (manifest-entries-by-name+version name version)))
(if (null? entries)
'()
(list (make-obsolete-package-entry name version entries))))
(map ->entry packages))))
(define (package-entries-by-spec profile ->entry spec)
"Return list of package entries for packages with name specification SPEC."
(set-current-manifest-maybe! profile)
(let-values (((name version)
(full-name->name+version spec)))
(if version
(package-entries-by-name+version ->entry name version)
(matching-package-entries
->entry
(lambda (pkg) (string=? name (package-name pkg)))))))
(define (package-entries-by-regexp profile ->entry regexp match-params)
"Return list of package entries for packages matching REGEXP string.
MATCH-PARAMS is a list of parameters that REGEXP can match."
(define (package-match? package regexp)
(any (lambda (param)
(let ((val (package-param package param)))
(and (string? val) (regexp-exec regexp val))))
match-params))
(set-current-manifest-maybe! profile)
(let ((re (make-regexp regexp regexp/icase)))
(matching-package-entries ->entry (cut package-match? <> re))))
(define (package-entries-by-ids profile ->entry ids)
"Return list of package entries for packages matching KEYS.
IDS may be an object-address, a full-name or a list of such elements."
(set-current-manifest-maybe! profile)
(fold-object
(lambda (id res)
(if (integer? id)
(let ((pkg (package-by-address id)))
(if pkg
(cons (->entry pkg) res)
res))
(let ((entries (package-entries-by-spec #f ->entry id)))
(if (null? entries)
res
(append res entries)))))
'()
ids))
(define (newest-available-package-entries profile ->entry)
"Return list of package entries for the newest available packages."
(set-current-manifest-maybe! profile)
(vhash-fold (lambda (name elem res)
(match elem
((version newest pkgs ...)
(cons (->entry newest) res))))
'()
(find-newest-available-packages)))
(define (all-available-package-entries profile ->entry)
"Return list of package entries for all available packages."
(set-current-manifest-maybe! profile)
(matching-package-entries ->entry (const #t)))
(define (manifest-package-entries ->entry)
"Return list of package entries for the current manifest."
(fold-manifest-entries
(lambda (name version entries res)
;; We don't care about duplicates for the list of
;; installed packages, so just take any package (car)
;; matching name+version
(cons (car (package-entries-by-name+version ->entry name version))
res))
'()))
(define (installed-package-entries profile ->entry)
"Return list of package entries for all installed packages."
(set-current-manifest-maybe! profile)
(manifest-package-entries ->entry))
(define (generation-package-entries profile ->entry generation)
"Return list of package entries for packages from GENERATION."
(set-current-manifest-maybe!
(generation-file-name profile generation))
(manifest-package-entries ->entry))
(define (obsolete-package-entries profile _)
"Return list of package entries for obsolete packages."
(set-current-manifest-maybe! profile)
(fold-manifest-entries
(lambda (name version entries res)
(let ((packages (packages-by-name+version name version)))
(if (null? packages)
(cons (make-obsolete-package-entry name version entries) res)
res)))
'()))
;;; Generation entries
(define (profile-generations profile)
"Return list of generations for PROFILE."
(let ((generations (generation-numbers profile)))
(if (equal? generations '(0))
'()
generations)))
(define (generation-param-alist profile)
"Return alist of generation parameters and functions for PROFILE."
(list
(cons 'id identity)
(cons 'number identity)
(cons 'prev-number (cut previous-generation-number profile <>))
(cons 'path (cut generation-file-name profile <>))
(cons 'time (lambda (gen)
(time-second (generation-time profile gen))))))
(define (matching-generation-entries profile ->entry predicate)
"Return list of generation entries for the matching generations.
PREDICATE is called on each generation."
(filter-map (lambda (gen)
(and (predicate gen) (->entry gen)))
(profile-generations profile)))
(define (last-generation-entries profile ->entry number)
"Return list of last NUMBER generation entries.
If NUMBER is 0 or less, return all generation entries."
(let ((generations (profile-generations profile))
(number (if (<= number 0) +inf.0 number)))
(map ->entry
(if (> (length generations) number)
(list-head (reverse generations) number)
generations))))
(define (all-generation-entries profile ->entry)
"Return list of all generation entries."
(last-generation-entries profile ->entry +inf.0))
(define (generation-entries-by-ids profile ->entry ids)
"Return list of generation entries for generations matching IDS.
IDS is a list of generation numbers."
(matching-generation-entries profile ->entry (cut memq <> ids)))
;;; Getting package/generation entries
(define %package-entries-functions
(alist->vhash
`((id . ,package-entries-by-ids)
(name . ,package-entries-by-spec)
(regexp . ,package-entries-by-regexp)
(all-available . ,all-available-package-entries)
(newest-available . ,newest-available-package-entries)
(installed . ,installed-package-entries)
(obsolete . ,obsolete-package-entries)
(generation . ,generation-package-entries))
hashq))
(define %generation-entries-functions
(alist->vhash
`((id . ,generation-entries-by-ids)
(last . ,last-generation-entries)
(all . ,all-generation-entries))
hashq))
(define (get-entries profile params entry-type search-type search-vals)
"Return list of entries.
ENTRY-TYPE and SEARCH-TYPE define a search function that should be
applied to PARAMS and VALS."
(let-values (((vhash ->entry)
(case entry-type
((package)
(values %package-entries-functions
(object-transformer
package-param-alist params)))
((generation)
(values %generation-entries-functions
(object-transformer
(generation-param-alist profile) params)))
(else (format (current-error-port)
"Wrong entry type '~a'" entry-type)))))
(match (vhash-assq search-type vhash)
((key . fun)
(apply fun profile ->entry search-vals))
(_ '()))))
;;; Actions
(define* (package->manifest-entry* package #:optional output)
(and package
(begin
(check-package-freshness package)
(package->manifest-entry package output))))
(define* (make-install-manifest-entries id #:optional output)
(package->manifest-entry* (package-by-id id) output))
(define* (make-upgrade-manifest-entries id #:optional output)
(package->manifest-entry* (newest-package-by-id id) output))
(define* (make-manifest-pattern id #:optional output)
"Make manifest pattern from a package ID and OUTPUT."
(let-values (((name version)
(id->name+version id)))
(and name version
(manifest-pattern
(name name)
(version version)
(output output)))))
(define (convert-action-pattern pattern proc)
"Convert action PATTERN into a list of objects returned by PROC.
PROC is called: (PROC ID) or (PROC ID OUTPUT)."
(match pattern
((id . outputs)
(if (null? outputs)
(let ((obj (proc id)))
(if obj (list obj) '()))
(filter-map (cut proc id <>)
outputs)))
(_ '())))
(define (convert-action-patterns patterns proc)
(append-map (cut convert-action-pattern <> proc)
patterns))
(define* (process-package-actions
profile #:key (install '()) (upgrade '()) (remove '())
(use-substitutes? #t) dry-run?)
"Perform package actions.
INSTALL, UPGRADE, REMOVE are lists of 'package action patterns'.
Each pattern should have the following form:
(ID . OUTPUTS)
ID is an object address or a full-name of a package.
OUTPUTS is a list of package outputs (may be an empty list)."
(format #t "The process begins ...~%")
(let* ((install (append
(convert-action-patterns
install make-install-manifest-entries)
(convert-action-patterns
upgrade make-upgrade-manifest-entries)))
(remove (convert-action-patterns remove make-manifest-pattern))
(transaction (manifest-transaction (install install)
(remove remove)))
(manifest (profile-manifest profile))
(new-manifest (manifest-perform-transaction
manifest transaction)))
(unless (and (null? install) (null? remove))
(let* ((store (open-connection))
(derivation (run-with-store
store (profile-derivation new-manifest)))
(derivations (list derivation))
(new-profile (derivation->output-path derivation)))
(set-build-options store
#:use-substitutes? use-substitutes?)
(manifest-show-transaction store manifest transaction
#:dry-run? dry-run?)
(show-what-to-build store derivations
#:use-substitutes? use-substitutes?
#:dry-run? dry-run?)
(unless dry-run?
(let ((name (generation-file-name
profile
(+ 1 (generation-number profile)))))
(and (build-derivations store derivations)
(let* ((entries (manifest-entries new-manifest))
(count (length entries)))
(switch-symlinks name new-profile)
(switch-symlinks profile name)
(format #t (N_ "~a package in profile~%"
"~a packages in profile~%"
count)
count)))))))))