From 457f60fa068c7becf60841daa2b6fc5121aedead Mon Sep 17 00:00:00 2001 From: Alex Kost Date: Wed, 27 Aug 2014 16:44:17 +0400 Subject: Add Emacs user interface. * configure.ac (emacsuidir): New variable. (AC_CONFIG_FILES): Add 'emacs/guix-init.el', 'emacs/guix-helper.scm'. * Makefile.am: Include 'emacs.am'. * emacs.am: New file. * doc/emacs.texi: New file. * doc/guix.texi: Include 'emacs.texi'. * emacs/guix-backend.el: New file. * emacs/guix-base.el: New file. * emacs/guix-helper.scm.in: New file. * emacs/guix-history.el: New file. * emacs/guix-info.el: New file. * emacs/guix-init.el.in: New file. * emacs/guix-list.el: New file. * emacs/guix-main.scm: New file. * emacs/guix-utils.el: New file. * emacs/guix.el: New file. --- emacs/guix-main.scm | 603 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 603 insertions(+) create mode 100644 emacs/guix-main.scm (limited to 'emacs/guix-main.scm') diff --git a/emacs/guix-main.scm b/emacs/guix-main.scm new file mode 100644 index 0000000000..1383d08830 --- /dev/null +++ b/emacs/guix-main.scm @@ -0,0 +1,603 @@ +;;; 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))))))))) + -- cgit v1.2.3