aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017, 2018, 2019 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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:
;;;
;;; This script updates the list of new and updated packages in 'NEWS'.
;;;
;;; Code:

(use-modules (gnu) (guix)
             (guix build utils)
             ((guix ui) #:select (fill-paragraph))
             (srfi srfi-1)
             (srfi srfi-11)
             (ice-9 match)
             (ice-9 rdelim)
             (ice-9 regex)
             (ice-9 vlist)
             (ice-9 pretty-print))

(define %header-rx
  (make-regexp "^\\* Changes in (version )?([0-9.]+) \\(since ([0-9.]+)\\)"))

(define (NEWS->versions port)
  "Return two values: the previous version and the current version as read
from PORT, which is an input port on the 'NEWS' file."
  (let loop ()
    (let ((line (read-line port)))
      (cond ((eof-object? line)
             (error "failed to determine previous and current version"
                    port))
            ((regexp-exec %header-rx line)
             =>
             (lambda (match)
               (values (match:substring match 3)
                       (match:substring match 2))))
            (else
             (loop))))))

(define (skip-to-org-heading port)
  "Read from PORT until an Org heading is found."
  (let loop ()
    (let ((next (peek-char port)))
      (cond ((eqv? next #\*)
             #t)
            ((eof-object? next)
             (error "next heading could not be found"))
            (else
             (read-line port)
             (loop))))))

(define (rewrite-org-section input output heading-rx proc)
  "Write to OUTPUT the text read from INPUT, but with the first Org section
matching HEADING-RX replaced by NEW-HEADING and CONTENTS."
  (let loop ()
    (let ((line (read-line input)))
      (cond ((eof-object? line)
             (error "failed to match heading regexp" heading-rx))
            ((regexp-exec heading-rx line)
             =>
             (lambda (match)
               (proc match output)
               (skip-to-org-heading input)
               (dump-port input output)
               #t))
            (else
             (display line output)
             (newline output)
             (loop))))))

(define (enumeration->paragraph lst)
  "Turn LST, a list of strings, into a single string that is a ready-to-print
paragraph."
  (fill-paragraph (string-join (sort lst string<?) ", ")
                  75))

(define (write-packages-added news-file old new)
  "Write to NEWS-FILE the list of packages added between OLD and NEW."
  (let ((added (lset-difference string=? (map car new) (map car old))))
    (with-atomic-file-replacement news-file
      (lambda (input output)
        (rewrite-org-section input output
                             (make-regexp "^(\\*+).*new packages")
                             (lambda (match port)
                               (let ((stars (match:substring match 1)))
                                 (format port
                                         "~a ~a new packages~%~%"
                                         stars (length added)))))))))

(define (write-packages-updates news-file old new)
  "Write to NEWS-FILE the list of packages upgraded between OLD and NEW."
  (define important
    '("gcc-toolchain" "glibc" "binutils" "gdb"         ;toolchain
      "shepherd" "linux-libre" "xorg-server" "cups"    ;OS
      "gnome" "xfce" "enlightenment" "lxde" "mate"     ;desktop env.
      "guile" "bash" "python" "python2" "perl"         ;languages
      "ghc" "rust" "go" "julia" "r" "ocaml"
      "icedtea" "openjdk" "clojure" "sbcl" "racket"
      "emacs" "gimp" "inkscape" "libreoffice"          ;applications
      "octave" "icecat" "gnupg"))

  (let* ((table    (fold (lambda (package table)
                           (match package
                             ((name . version)
                              (vhash-cons name version table))))
                         vlist-null
                         new))
         (latest   (lambda (name)
                     (let ((versions (vhash-fold* cons '() name table)))
                       (match (sort versions version>?)
                         ((latest . _) latest)))))
         (upgraded (filter-map (match-lambda
                                 ((package . new-version)
                                  (match (assoc package old)
                                    ((_ . old-version)
                                     (and (string=? new-version
                                                    (latest package))
                                          (version>? new-version old-version)
                                          (cons package new-version)))
                                    (_ #f))))
                               new))
         (noteworthy (filter (match-lambda
                               ((package . version)
                                (member package important)))
                             upgraded)))
    (with-atomic-file-replacement news-file
      (lambda (input output)
        (rewrite-org-section input output
                             (make-regexp "^(\\*+).*package updates")
                             (lambda (match port)
                               (let ((stars (match:substring match 1))
                                     (lst   (map (match-lambda
                                                   ((package . version)
                                                    (string-append package " "
                                                                   version)))
                                                 noteworthy)))
                                 (format port
                                         "~a ~a package updates~%~%Noteworthy updates:~%~a~%~%"
                                         stars (length upgraded)
                                         (enumeration->paragraph lst)))))))))


(define (main . args)
  (match args
    ((news-file data-directory)
     ;; Don't browse things listed in the user's $GUIX_PACKAGE_PATH and
     ;; in external channels.
     (parameterize ((%package-module-path
                     %default-package-module-path))
       (define (package-file version)
         (string-append data-directory "/packages-"
                        version ".txt"))

       (define (package<? p1 p2)
         (string<? (package-full-name p1) (package-full-name p2)))

       (let-values (((previous-version new-version)
                     (call-with-input-file news-file NEWS->versions)))
         (format (current-error-port) "Updating NEWS for ~a to ~a...~%"
                 previous-version new-version)
         (let* ((old (call-with-input-file (package-file previous-version)
                       read))
                (all-packages/sorted (sort (fold-packages (lambda (p r)
                                                            (cons p r))
                                                          '())
                                           package<?))
                (new (map (lambda (p)
                            (cons (package-name p) (package-version p)))
                          all-packages/sorted)))
           (call-with-output-file (package-file new-version)
             (lambda (port)
               (pretty-print new port)))

           (write-packages-added news-file old new)
           (write-packages-updates news-file old new)))))
    (x
     (format (current-error-port) "Usage: update-NEWS NEWS-FILE DATA-DIRECTORY

Update the list of new and updated packages in NEWS-FILE using the
previous-version package list from DATA-DIRECTORY.\n")
     (exit 1))))

(apply main (cdr (command-line)))
#:use-module (guix i18n) #:use-module (guix records) #:use-module (newt) #:use-module (srfi srfi-1) #:use-module (srfi srfi-26) #:use-module (srfi srfi-34) #:use-module (srfi srfi-35) #:use-module (ice-9 i18n) #:use-module (ice-9 match) #:export (run-keymap-page keyboard-layout->configuration)) (define (run-layout-page layouts layout->text context) (let ((title (G_ "Layout"))) (run-listbox-selection-page #:title title #:info-text (case context ((param) (G_ "Please choose your keyboard layout. \ It will only be used during the installation process. \ Non-Latin layouts can be toggled with Alt+Shift.")) (else (G_ "Please choose your keyboard layout. \ It will be used during the install process, and for the installed system. \ Non-Latin layouts can be toggled with Alt+Shift. You can switch to a \ different layout at any time from the parameters menu."))) #:listbox-items layouts #:listbox-item->text layout->text #:sort-listbox-items? #f #:button-text (case context ((param) (G_ "Continue")) (else (G_ "Exit"))) #:button-callback-procedure (case context ((param) (const #t)) (else (lambda _ (raise (condition (&installer-step-abort))))))))) (define (run-variant-page variants variant->text) (let ((title (G_ "Variant"))) (run-listbox-selection-page #:title title #:info-text (G_ "Please choose a variant for your keyboard layout.") #:listbox-items variants #:listbox-item->text variant->text #:sort-listbox-items? #f #:button-text (G_ "Back") #:button-callback-procedure (lambda _ (raise (condition (&installer-step-abort))))))) (define (sort-layouts layouts) "Sort LAYOUTS list by putting the US layout ahead and return it." (define (layout<? layout1 layout2) (let ((text1 (x11-keymap-layout-description layout1)) (text2 (x11-keymap-layout-description layout2))) ;; XXX: We're calling 'gettext' more than once per item. (string-locale<? (gettext text1 "xkeyboard-config") (gettext text2 "xkeyboard-config")))) (define preferred ;; Two-letter language tag for the preferred keyboard layout. (or (getenv "LANGUAGE") "us")) (call-with-values (lambda () (partition (lambda (layout) ;; The 'synopsis' field is usually a language code (e.g., "en") ;; while the 'name' field is a country code (e.g., "us"). (or (string=? (x11-keymap-layout-name layout) preferred) (string=? (x11-keymap-layout-synopsis layout) preferred))) layouts)) (lambda (main others) (append (sort main layout<?) (sort others layout<?))))) (define (add-empty-variant variants) "Prepend #f to VARIANTS so the user has the option to select no variant. The resulting layout may be different from all other variants (e.g. for Azerbaijani)." (cons #f variants)) (define (sort-variants variants) "Sort VARIANTS list by putting the international variant ahead and return it." (call-with-values (lambda () (partition (lambda (variant) (and variant (let ((name (x11-keymap-variant-name variant))) (string=? name "altgr-intl")))) variants)) (cut append <> <>))) (define %non-latin-layouts ;; List of keyboard layouts marked as $nonlatin in xkeyboard-config. ;; See comments in xkeyboard-config file /share/X11/xkb/rules/base. ;; We ignore layouts that support Latin input: "kr" '("am" "ara" "ben" "bd" "bg" "bt" "by" "cs" "deva" "ge" "gh" "gr" "guj" "guru" "il" "in" "ir" "iku" "jp" "kan" "kh" "la" "lao" "lk" "mk" "mm" "mn" "mv" "mal" "olck" "ori" "pk" "ru" "scc" "sy" "syr" "tel" "th" "tj" "tam" "ua" "uz" ;; The list from xkeyboard-config is incomplete. Add more layouts when ;; noticed: "et" "kz")) (define %non-latin-variants '("cyrillic")) (define %latin-layout+variants ;; These layout+variant combinations are Latin after all. '(("ir" "ku"))) (define (toggleable-latin-layout layout variant) "If LAYOUT is a non-Latin layout, return a new combined layout, a variant, and options that allow the user to switch between the non-Latin and the Latin layout. Otherwise, return LAYOUT, VARIANT, and #f." (if (and (not (equal? variant "latin")) (not (member (list layout variant) %latin-layout+variants)) (or (member layout %non-latin-layouts) (member variant %non-latin-variants))) (let ((latin-layout (if (equal? variant "azerty") "fr" "us"))) (list (string-append layout "," latin-layout) ;; Comma to use variant only for non-Latin: (and variant (string-append variant ",")) "grp:alt_shift_toggle")) (list layout variant #f))) (define* (run-keymap-page layouts #:key (context #f)) "Run a page asking the user to select a keyboard layout and variant. LAYOUTS is a list of supported X11-KEYMAP-LAYOUT. For non-Latin keyboard layouts, a second layout and toggle options will be added automatically. Return a list of three elements, the names of the selected keyboard layout, variant and options." (define keymap-steps (list (installer-step (id 'layout) (compute (lambda _ (run-layout-page (sort-layouts layouts) (lambda (layout) (gettext (x11-keymap-layout-description layout) "xkeyboard-config")) context)))) ;; Propose the user to select a variant among those supported by the ;; previously selected layout. (installer-step (id 'variant) (compute (lambda (result _) (let* ((layout (result-step result 'layout)) (variants (x11-keymap-layout-variants layout))) ;; Return #f if the layout does not have any variant. (and (not (null? variants)) (run-variant-page (sort-variants (add-empty-variant variants)) (lambda (variant) (if variant (gettext (x11-keymap-variant-description variant) "xkeyboard-config") ;; Text to opt for no variant at all: (gettext (x11-keymap-layout-description layout) "xkeyboard-config"))))))))))) (define (format-result result) (let ((layout (x11-keymap-layout-name (result-step result 'layout))) (variant (and=> (result-step result 'variant) (lambda (variant) (gettext (x11-keymap-variant-name variant) "xkeyboard-config"))))) (toggleable-latin-layout layout variant))) (format-result (run-installer-steps #:steps keymap-steps))) (define (keyboard-layout->configuration keymap) "Return the operating system configuration snippet to install KEYMAP." (match keymap ((name #f "grp:alt_shift_toggle") `((keyboard-layout (keyboard-layout ,name #:options '("grp:alt_shift_toggle"))))) ((name #f _) `((keyboard-layout (keyboard-layout ,name)))) ((name variant "grp:alt_shift_toggle") `((keyboard-layout (keyboard-layout ,name ,variant #:options '("grp:alt_shift_toggle"))))) ((name variant _) `((keyboard-layout (keyboard-layout ,name ,variant))))))