;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2014 Alex Kost ;;; ;;; 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 . ;;; 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)))))))))