about
aboutsummaryrefslogtreecommitdiff
path: root/build-aux/update-guix-package.scm
blob: 9fe6c201ccb5fd014895e842f1a45d32a0f3ef17 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017, 2018 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@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:
;;;
;;; This scripts updates the definition of the 'guix' package in Guix for the
;;; current commit.  It requires Git to be installed.
;;;
;;; Code:

(use-modules (guix)
             (guix ui)
             (guix git-download)
             (guix upstream)
             (guix utils)
             (guix base32)
             (guix build utils)
             (guix scripts hash)
             (gnu packages package-management)
             (ice-9 match)
             (ice-9 popen)
             (ice-9 regex)
             (ice-9 textual-ports)
             (srfi srfi-1)
             (srfi srfi-2)
             (srfi srfi-26))

(define %top-srcdir
  (string-append (current-source-directory) "/.."))

(define (package-definition-location)
  "Return the source properties of the definition of the 'guix' package."
  (call-with-input-file (location-file (package-location guix))
    (lambda (port)
      (let loop ()
        (match (read port)
          ((? eof-object?)
           (error "definition of 'guix' package could not be found"
                  (port-filename port)))
          (('define-public 'guix value)
           (source-properties value))
          (_
           (loop)))))))

(define* (update-definition commit hash
                            #:key version old-hash)
  "Return a one-argument procedure that takes a string, the definition of the
'guix' package, and returns a string, the update definition for VERSION,
COMMIT."
  (define (linear-offset str line column)
    ;; Return the offset in characters to reach LINE and COLUMN (both
    ;; zero-indexed) in STR.
    (call-with-input-string str
      (lambda (port)
        (let loop ((offset 0))
          (cond ((and (= (port-column port) column)
                      (= (port-line port) line))
                 offset)
                ((eof-object? (read-char port))
                 (error "line and column not reached!"
                        str))
                (else
                 (loop (+ 1 offset))))))))

  (define (update-hash str)
    ;; Replace OLD-HASH with HASH in STR.
    (string-replace-substring str
                              (bytevector->nix-base32-string old-hash)
                              (bytevector->nix-base32-string hash)))

  (lambda (str)
    (match (call-with-input-string str read)
      (('let (('version old-version)
              ('commit old-commit)
              ('revision old-revision))
         defn)
       (let* ((location (source-properties defn))
              (line     (assq-ref location 'line))
              (column   0)
              (offset   (linear-offset str line column)))
         (string-append (format #f "(let ((version \"~a\")
        (commit \"~a\")
        (revision ~a))\n"
                                (or version old-version)
                                commit
                                (if (and version
                                         (not (string=? version old-version)))
                                    0
                                    (+ 1 old-revision)))
                        (string-drop (update-hash str) offset))))
      (exp
       (error "'guix' package definition is not as expected" exp)))))

(define (git-add-worktree directory commit)
  "Create a new git worktree at DIRECTORY, detached on commit COMMIT."
  (invoke "git" "worktree" "add" "--detach" directory commit))

(define (call-with-temporary-git-worktree commit proc)
  "Execute PROC in the context of a temporary git worktree created from
COMMIT.  PROC receives the temporary directory file name as an argument."
  (call-with-temporary-directory
   (lambda (tmp-directory)
     (dynamic-wind
       (lambda ()
         #t)
       (lambda ()
         (git-add-worktree tmp-directory commit)
         (proc tmp-directory))
       (lambda ()
         (invoke "git" "worktree" "remove" "--force" tmp-directory))))))

(define %savannah-guix-git-repo-push-url-regexp
  "git.(savannah|sv).gnu.org:?/srv/git/guix.git \\(push\\)")

(define-syntax-rule (with-input-pipe-to-string prog arg ...)
  (let* ((input-pipe (open-pipe* OPEN_READ prog arg ...))
	 (output (get-string-all input-pipe))
	 (exit-val (status:exit-val (close-pipe input-pipe))))
    (unless (zero? exit-val)
      (error (format #f "Command ~s exited with non-zero exit status: ~s"
                     (string-join (list prog arg ...)) exit-val)))
    (string-trim-both output)))

(define (find-origin-remote)
  "Find the name of the git remote with the Savannah Guix git repo URL."
  (and-let* ((remotes (string-split (with-input-pipe-to-string
                                     "git" "remote" "-v")
                                    #\newline))
             (origin-entry (find (cut string-match
                                      %savannah-guix-git-repo-push-url-regexp
                                      <>)
                                 remotes)))
    (first (string-split origin-entry #\tab))))

(define (commit-already-pushed? remote commit)
  "True if COMMIT is found in the REMOTE repository."
  (not (string-null? (with-input-pipe-to-string
                      "git" "branch" "-r" "--contains" commit
                      (string-append remote "/master")))))

(define (keep-source-in-store store source)
  "Add SOURCE to the store under the name that the 'guix' package expects."

  ;; Add SOURCE to the store, but this time under the real name used in the
  ;; 'origin'.  This allows us to build the package without having to make a
  ;; real checkout; thus, it also works when working on a private branch.
  (reload-module
   (resolve-module '(gnu packages package-management)))

  (let* ((source (add-to-store store
                               (origin-file-name (package-source guix))
                               #t "sha256" source
                               #:select? (git-predicate source)))
         (root   (store-path-package-name source)))

    ;; Add an indirect GC root for SOURCE in the current directory.
    (false-if-exception (delete-file root))
    (symlink source root)
    (add-indirect-root store
                       (string-append (getcwd) "/" root))

    (info (G_ "source code kept in ~a (GC root: ~a)~%")
          source root)))


(define (main . args)
  (match args
    ((commit version)
     (with-directory-excursion %top-srcdir
       (or (getenv "GUIX_ALLOW_ME_TO_USE_PRIVATE_COMMIT")
           (let ((remote (find-origin-remote)))
             (unless remote
               (leave (G_ "Failed to find the origin git remote.~%")))
             (commit-already-pushed? remote commit))
           (leave (G_ "Commit ~a is not pushed upstream.  Aborting.~%") commit))
       (call-with-temporary-git-worktree commit
           (lambda (tmp-directory)
             (let* ((hash (nix-base32-string->bytevector
                           (string-trim-both
                            (with-output-to-string
		              (lambda ()
		                (guix-hash "-rx" tmp-directory))))))
                    (location (package-definition-location))
                    (old-hash (content-hash-value
                               (origin-hash (package-source guix)))))
               (edit-expression location
                                (update-definition commit hash
                                                   #:old-hash old-hash
                                                   #:version version))
               ;; When GUIX_ALLOW_ME_TO_USE_PRIVATE_COMMIT is set, the sources are
               ;; added to the store.  This is used as part of 'make release'.
               (when (getenv "GUIX_ALLOW_ME_TO_USE_PRIVATE_COMMIT")
                 (with-store store
                   (keep-source-in-store store tmp-directory))))))))
    ((commit)
     ;; Automatically deduce the version and revision numbers.
     (main commit #f))))

(apply main (cdr (command-line)))
en/close lst) ;; Pair 'open' and 'close' tags produced by 'highlights' and ;; produce nested 'paren' tags instead. (let loop ((lst lst) (level 0) (result '())) (match lst ((('open open) rest ...) (call-with-values (lambda () (loop rest (+ 1 level) '())) (lambda (inner close rest) (loop rest level (cons `(paren ,level ,open ,inner ,close) result))))) ((('close str) rest ...) (if (> level 0) (values (reverse result) str rest) (begin (format (current-error-port) "warning: extra closing paren; context:~% ~y~%" (reverse result)) (loop rest 0 (cons `(close ,str) result))))) ((item rest ...) (loop rest level (cons item result))) (() (when (> level 0) (format (current-error-port) "warning: missing ~a closing parens; context:~% ~y%" level (reverse result))) (values (reverse result) "" '()))))) (define (highlights->sxml* highlights anchors) ;; Like 'highlights->sxml', but handle nested 'paren tags. This ;; allows for paren matching highlights via appropriate CSS ;; "hover" properties. When a symbol is encountered, look it up ;; in ANCHORS, a vhash, and emit the corresponding href, if any. (define (tag->class tag) (string-append "syntax-" (symbol->string tag))) (map (match-lambda ((? string? str) str) (('paren level open (body ...) close) `(span (@ (class ,(string-append "syntax-paren" (number->string level)))) ,open (span (@ (class "syntax-symbol")) ,@(highlights->sxml* body anchors)) ,close)) (('symbol text) ;; Check whether we can emit a hyperlink for TEXT. (match (vhash-assoc text anchors) (#f `(span (@ (class ,(tag->class 'symbol))) ,text)) ((_ . target) `(a (@ (class ,(tag->class 'symbol)) (href ,target)) ,text)))) ((tag text) `(span (@ (class ,(tag->class tag))) ,text))) highlights)) (define entity->string (match-lambda ("rArr" "⇒") ("rarr" "→") ("hellip" "…") ("rsquo" "’") ("nbsp" " ") (e (pk 'unknown-entity e) (primitive-exit 2)))) (define (concatenate-snippets pieces) ;; Concatenate PIECES, which contains strings and entities, ;; replacing entities with their corresponding string. (let loop ((pieces pieces) (strings '())) (match pieces (() (string-concatenate-reverse strings)) (((? string? str) . rest) (loop rest (cons str strings))) ((('*ENTITY* "additional" entity) . rest) (loop rest (cons (entity->string entity) strings))) ((('span _ lst ...) . rest) ;for <span class="roman"> (loop (append lst rest) strings)) ((('var name) . rest) ;for @var{name} within @lisp (loop rest (cons name strings))) ;XXX: losing formatting (something (pk 'unsupported-code-snippet something) (primitive-exit 1))))) (define (highlight-definition id category symbol args) ;; Produce stylable HTML for the given definition (an @deftp, ;; @deffn, or similar). `(dt (@ (id ,id) (class "symbol-definition")) (span (@ (class "symbol-definition-category")) ,@category) (span (@ (class "symbol-definition-prototype")) ,symbol " " ,@args))) (define (space? obj) (and (string? obj) (string-every char-set:whitespace obj))) (define (syntax-highlight sxml anchors) ;; Recurse over SXML and syntax-highlight code snippets. (let loop ((sxml sxml)) (match sxml (('*TOP* decl body ...) `(*TOP* ,decl ,@(map loop body))) (('head things ...) `(head ,@things (link (@ (rel "stylesheet") (type "text/css") (href #$syntax-css-url))))) (('pre ('@ ('class "lisp")) code-snippet ...) `(pre (@ (class "lisp")) ,@(highlights->sxml* (pair-open/close (highlight lex-scheme (concatenate-snippets code-snippet))) anchors))) ;; Replace the ugly <strong> used for @deffn etc., which ;; translate to <dt>, with more stylable markup. (('dt ('@ ('id id)) ;raw Texinfo 6.8 ('span ('@ ('class "category")) category ...) ('span ('strong thing) anchor)) (highlight-definition id category thing '())) (('dt (@ ('id id)) ('span ('@ ('class "category")) category ...) ('span ('strong thing) (? space?) ('em args ...) anchor)) (highlight-definition id category thing args)) ((tag ('@ attributes ...) body ...) `(,tag (@ ,@attributes) ,@(map loop body))) ((tag body ...) `(,tag ,@(map loop body))) ((? string? str) str)))) (define (process-html file anchors) ;; Parse FILE and perform syntax highlighting for its Scheme ;; snippets. Install the result to #$output. (format (current-error-port) "processing ~a...~%" file) (let* ((shtml (call-with-input-file file html->shtml)) (highlighted (syntax-highlight shtml anchors)) (base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (call-with-output-file target (lambda (port) (write-shtml-as-html highlighted port))))) (define (copy-as-is file) ;; Copy FILE as is to #$output. (let* ((base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (catch 'system-error (lambda () (if (eq? 'symlink (stat:type (lstat file))) (symlink (readlink file) target) (link file target))) (lambda args (let ((errno (system-error-errno args))) (pk 'error-link file target (strerror errno)) (primitive-exit 3)))))) (define (html? file stat) (string-suffix? ".html" file)) (define language+node-anchors (match-lambda ((language files ...) (cons language (fold (lambda (file vhash) (let ((alist (call-with-input-file file read))) ;; Use 'fold-right' so that the first entry ;; wins (e.g., "car" from "Pairs" rather than ;; from "rnrs base" in the Guile manual). (fold-right (match-lambda* (((key . value) vhash) (vhash-cons key value vhash))) vhash alist))) vlist-null files))))) (define mono-node-anchors ;; List of language/vhash pairs, where each vhash maps an ;; identifier to the corresponding URL in a single-page manual. (map language+node-anchors '#$mono-node-indexes)) (define multi-node-anchors ;; Likewise for split-node manuals. (map language+node-anchors '#$split-node-indexes)) ;; Install a UTF-8 locale so we can process UTF-8 files. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setlocale LC_ALL "en_US.utf8") ;; First process the mono-node 'guix.html' files. (for-each (match-lambda ((language . anchors) (let ((files (find-files (string-append #$input "/" language) "^guix(-cookbook|)(\\.[a-zA-Z_-]+)?\\.html$"))) (n-par-for-each (parallel-job-count) (cut process-html <> anchors) files)))) mono-node-anchors) ;; Process the multi-node HTML files. (for-each (match-lambda ((language . anchors) (let ((files (find-files (string-append #$input "/" language "/html_node") "\\.html$"))) (n-par-for-each (parallel-job-count) (cut process-html <> anchors) files)))) multi-node-anchors) ;; Last, copy non-HTML files as is. (for-each copy-as-is (find-files #$input (negate html?))))))) (computed-file name build)) (define* (stylized-html source input #:key (languages %languages) (manual %manual) (manual-css-url "/themes/initial/css/manual.css")) "Process all the HTML files in INPUT; add them MANUAL-CSS-URL as a <style> link, and add a menu to choose among LANGUAGES. Use the Guix PO files found in SOURCE." (define build (with-extensions (list guile-lib) (with-imported-modules `((guix build utils) ((localization) => ,(localization-helper-module source languages))) #~(begin (use-modules (htmlprag) (localization) (guix build utils) (srfi srfi-1) (ice-9 match) (ice-9 threads)) (define* (menu-dropdown #:key (label "Item") (url "#") (items '())) ;; Return an SHTML <li> element representing a dropdown for the ;; navbar. LABEL is the text of the dropdown menu, and ITEMS is ;; the list of items in this menu. (define id "visible-dropdown") `(li (@ (class "navbar-menu-item dropdown dropdown-btn")) (input (@ (class "navbar-menu-hidden-input") (type "radio") (name "dropdown") (id ,id))) (label (@ (for ,id)) ,label) (label (@ (for "all-dropdowns-hidden")) ,label) (div (@ (class "navbar-submenu") (id "navbar-submenu")) (div (@ (class "navbar-submenu-triangle")) " ") (ul ,@items)))) (define (menu-item label url) ;; Return an SHTML <li> element for a menu item with the given ;; LABEL and URL. `(li (a (@ (class "navbar-menu-item") (href ,url)) ,label))) (define* (navigation-bar menus #:key split-node?) ;; Return the navigation bar showing all of MENUS. `(header (@ (class "navbar")) (h1 (a (@ (class "branding") (href ,(if split-node? ".." "#"))))) (nav (@ (class "navbar-menu")) (input (@ (class "navbar-menu-hidden-input") (type "radio") (name "dropdown") (id "all-dropdowns-hidden"))) (ul ,@menus)) ;; This is the button that shows up on small screen in ;; lieu of the drop-down button. (a (@ (class "navbar-menu-btn") (href ,(if split-node? "../.." "..")))))) (define* (base-language-url code manual #:key split-node?) ;; Return the base URL of MANUAL for language CODE. (if split-node? (string-append "../../" (normalize code) "/html_node") (string-append "../" (normalize code) "/" manual (if (string=? code "en") "" (string-append "." code)) ".html"))) (define (language-menu-items file) ;; Return the language menu items to be inserted in FILE. (define split-node? (string-contains file "/html_node/")) (append (map (lambda (code) (menu-item (language-code->native-name code) (base-language-url code #$manual #:split-node? split-node?))) '#$%languages) (list (menu-item "⊕" (if (string=? #$manual "guix-cookbook") "https://translate.fedoraproject.org/projects/guix/documentation-cookbook/" "https://translate.fedoraproject.org/projects/guix/documentation-manual/"))))) (define (stylized-html sxml file) ;; Return SXML, which was read from FILE, with additional ;; styling. (define split-node? (string-contains file "/html_node/")) (let loop ((sxml sxml)) (match sxml (('*TOP* decl body ...) `(*TOP* ,decl ,@(map loop body))) (('head elements ...) ;; Add reference to our own manual CSS, which provides ;; support for the language menu. `(head ,@elements (link (@ (rel "stylesheet") (type "text/css") (href #$manual-css-url))))) (('body ('@ attributes ...) elements ...) `(body (@ ,@attributes) ,(navigation-bar ;; TODO: Add "Contribute" menu, to report ;; errors, etc. (list (menu-dropdown #:label `(img (@ (alt "Language") (src "/themes/initial/img/language-picker.svg"))) #:items (language-menu-items file))) #:split-node? split-node?) ,@elements)) ((tag ('@ attributes ...) body ...) `(,tag (@ ,@attributes) ,@(map loop body))) ((tag body ...) `(,tag ,@(map loop body))) ((? string? str) str)))) (define (process-html file) ;; Parse FILE and add links to translations. Install the result ;; to #$output. (format (current-error-port) "processing ~a...~%" file) (let* ((shtml (parameterize ((%strict-tokenizer? #t)) (call-with-input-file file html->shtml))) (processed (stylized-html shtml file)) (base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (call-with-output-file target (lambda (port) (write-shtml-as-html processed port))))) ;; Install a UTF-8 locale so we can process UTF-8 files. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setlocale LC_ALL "en_US.utf8") (setenv "LC_ALL" "en_US.utf8") (setvbuf (current-error-port) 'line) (n-par-for-each (parallel-job-count) (lambda (file) (if (string-suffix? ".html" file) (process-html file) ;; Copy FILE as is to #$output. (let* ((base (string-drop file (string-length #$input))) (target (string-append #$output base))) (mkdir-p (dirname target)) (if (eq? 'symlink (stat:type (lstat file))) (symlink (readlink file) target) (copy-file file target))))) (find-files #$input)))))) (computed-file "stylized-html-manual" build)) (define* (html-manual source #:key (languages %languages) (version "0.0") (manual %manual) (mono-node-indexes (map list languages)) (split-node-indexes (map list languages)) (date 1) (options %makeinfo-html-options)) "Return the HTML manuals built from SOURCE for all LANGUAGES, with the given makeinfo OPTIONS." (define manual-source (texinfo-manual-source source #:version version #:languages languages #:date date)) (define images (texinfo-manual-images source)) (define build (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (define (normalize language) ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn". (string-map (match-lambda (#\_ #\-) (chr chr)) (string-downcase language))) (define (language->texi-file-name language) (if (string=? language "en") (string-append #$manual-source "/" #$manual ".texi") (string-append #$manual-source "/" #$manual "." language ".texi"))) ;; Install a UTF-8 locale so that 'makeinfo' is at ease. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setenv "LC_ALL" "en_US.utf8") (setvbuf (current-output-port) 'line) (setvbuf (current-error-port) 'line) ;; 'makeinfo' looks for "htmlxref.cnf" in the current directory, so ;; copy it right here. (copy-file (string-append #$manual-source "/htmlxref.cnf") "htmlxref.cnf") (for-each (lambda (language) (let* ((texi (language->texi-file-name language)) (opts `("--html" "-c" ,(string-append "TOP_NODE_UP_URL=/manual/" language) #$@options ,texi))) (format #t "building HTML manual for language '~a'...~%" language) (mkdir-p (string-append #$output "/" (normalize language))) (setenv "LANGUAGE" language) (apply invoke #$(file-append texinfo "/bin/makeinfo") "-o" (string-append #$output "/" (normalize language) "/html_node") opts) (apply invoke #$(file-append texinfo "/bin/makeinfo") "--no-split" "-o" (string-append #$output "/" (normalize language) "/" #$manual (if (string=? language "en") "" (string-append "." language)) ".html") opts) ;; Make sure images are available. (symlink #$images (string-append #$output "/" (normalize language) "/images")) (symlink #$images (string-append #$output "/" (normalize language) "/html_node/images")))) (filter (compose file-exists? language->texi-file-name) '#$languages))))) (let* ((name (string-append manual "-html-manual")) (manual* (computed-file name build #:local-build? #f))) (syntax-highlighted-html (stylized-html source manual* #:languages languages #:manual manual) #:mono-node-indexes mono-node-indexes #:split-node-indexes split-node-indexes #:name (string-append name "-highlighted")))) (define* (pdf-manual source #:key (languages %languages) (version "0.0") (manual %manual) (date 1) (options '())) "Return the HTML manuals built from SOURCE for all LANGUAGES, with the given makeinfo OPTIONS." (define manual-source (texinfo-manual-source source #:version version #:languages languages #:date date)) (define texinfo-profile (profile (content (packages->manifest ;; texi2dvi requires various command line tools. (list coreutils diffutils gawk grep sed tar texinfo (texlive-updmap.cfg (list texlive-epsf texlive-texinfo))))))) (define build (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (srfi srfi-34) (ice-9 match)) (define (normalize language) ;XXX: deduplicate ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn". (string-map (match-lambda (#\_ #\-) (chr chr)) (string-downcase language))) ;; Install a UTF-8 locale so that 'makeinfo' is at ease. (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setenv "LC_ALL" "en_US.utf8") (setenv "PATH" #+(file-append texinfo-profile "/bin")) (setenv "GUIX_TEXMF" #+(file-append texinfo-profile "/share/texmf-dist")) (setvbuf (current-output-port) 'line) (setvbuf (current-error-port) 'line) (setenv "HOME" (getcwd)) ;for kpathsea/mktextfm ;; 'SOURCE_DATE_EPOCH' is honored by pdftex. (setenv "SOURCE_DATE_EPOCH" "1") (for-each (lambda (language) (let ((opts `("--pdf" "-I" "." #$@options ,(if (string=? language "en") (string-append #$manual-source "/" #$manual ".texi") (string-append #$manual-source "/" #$manual "." language ".texi"))))) (format #t "building PDF manual for language '~a'...~%" language) (mkdir-p (string-append #$output "/" (normalize language))) (setenv "LANGUAGE" language) ;; FIXME: Unfortunately building PDFs for non-Latin ;; alphabets doesn't work: ;; <https://lists.gnu.org/archive/html/help-texinfo/2012-01/msg00014.html>. (guard (c ((invoke-error? c) (format (current-error-port) "~%~%Failed to produce \ PDF for language '~a'!~%~%" language))) (apply invoke #$(file-append texinfo "/bin/makeinfo") "--pdf" "-o" (string-append #$output "/" (normalize language) "/" #$manual (if (string=? language "en") "" (string-append "." language)) ".pdf") opts)))) '#$languages)))) (computed-file (string-append manual "-pdf-manual") build #:local-build? #f)) (define* (guix-manual-text-domain source #:optional (languages %manual-languages)) "Return the PO files for LANGUAGES of the 'guix-manual' text domain taken from SOURCE." (define po-directory (file-append* source "/po/doc")) (define build (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (mkdir-p #$output) (for-each (lambda (language) (define directory (string-append #$output "/" language "/LC_MESSAGES")) (mkdir-p directory) (invoke #+(file-append gnu-gettext "/bin/msgfmt") "-c" "-o" (string-append directory "/guix-manual.mo") (string-append #$po-directory "/guix-manual." language ".po"))) '#$(delete "en" languages))))) (computed-file "guix-manual-po" build)) (define* (localization-helper-module source #:optional (languages %languages)) "Return a file-like object for use as the (localization) module. SOURCE must be the Guix top-level source directory, from which PO files are taken." (define content (with-extensions (list guile-json-3) #~(begin (define-module (localization) #:use-module (json) #:use-module (srfi srfi-1) #:use-module (srfi srfi-19) #:use-module (ice-9 match) #:use-module (ice-9 popen) #:export (normalize with-language translate language-code->name language-code->native-name seconds->string)) (define (normalize language) ;XXX: deduplicate ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn". (string-map (match-lambda (#\_ #\-) (chr chr)) (string-downcase language))) (define-syntax-rule (with-language language exp ...) (let ((lang (getenv "LANGUAGE"))) (dynamic-wind (lambda () (setenv "LANGUAGE" language) (setlocale LC_MESSAGES)) (lambda () exp ...) (lambda () (if lang (setenv "LANGUAGE" lang) (unsetenv "LANGUAGE")) (setlocale LC_MESSAGES))))) ;; (put 'with-language 'scheme-indent-function 1) (define* (translate str language #:key (domain "guix-manual")) (define exp `(begin (bindtextdomain "guix-manual" #+(guix-manual-text-domain source)) (bindtextdomain "iso_639-3" ;language names #+(file-append iso-codes "/share/locale")) (setenv "LANGUAGE" ,language) (write (gettext ,str ,domain)))) ;; Since the 'gettext' function caches msgid translations, ;; regardless of $LANGUAGE, we have to spawn a new process each ;; time we want to translate to a different language. Bah! (let* ((pipe (open-pipe* OPEN_READ #+(file-append guile-3.0 "/bin/guile") "-c" (object->string exp))) (str (read pipe))) (close-pipe pipe) str)) (define %iso639-languages (vector->list (assoc-ref (call-with-input-file #+(file-append iso-codes "/share/iso-codes/json/iso_639-3.json") json->scm) "639-3"))) (define (language-code->name code) "Return the full name of a language from its ISO-639-3 code." (let ((code (match (string-index code #\_) (#f code) (index (string-take code index))))) (any (lambda (language) (and (string=? (or (assoc-ref language "alpha_2") (assoc-ref language "alpha_3")) code) (assoc-ref language "name"))) %iso639-languages))) (define (language-code->native-name code) "Return the name of language CODE in that language." (translate (language-code->name code) code #:domain "iso_639-3")) (define (seconds->string seconds language) (let* ((time (make-time time-utc 0 seconds)) (date (time-utc->date time))) (with-language language (date->string date "~e ~B ~Y"))))))) (scheme-file "localization.scm" content)) (define* (html-manual-indexes source #:key (languages %languages) (version "0.0") (manual %manual) (title (if (string=? "guix" manual) "GNU Guix Reference Manual" "GNU Guix Cookbook")) (date 1)) (define build (with-imported-modules `((guix build utils) ((localization) => ,(localization-helper-module source languages))) #~(begin (use-modules (guix build utils) (localization) (sxml simple) (srfi srfi-1)) (define (guix-url path) (string-append #$%web-site-url path)) (define (sxml-index language title body) ;; FIXME: Avoid duplicating styling info from guix-artwork.git. `(html (@ (lang ,language)) (head (title ,(string-append title " — GNU Guix")) (meta (@ (charset "UTF-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) ;; Menu prefetch. (link (@ (rel "prefetch") (href ,(guix-url "menu/index.html")))) ;; Base CSS. (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/elements.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/common.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/messages.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/navbar.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/breadcrumbs.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/buttons.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/footer.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/page.css")))) (link (@ (rel "stylesheet") (href ,(guix-url "themes/initial/css/post.css"))))) (body (header (@ (class "navbar")) (h1 (a (@ (class "branding") (href #$%web-site-url))) (span (@ (class "a11y-offset")) "Guix")) (nav (@ (class "menu")))) (nav (@ (class "breadcrumbs")) (a (@ (class "crumb") (href #$%web-site-url)) "Home")) ,body (footer)))) (define (language-index language) (define title (translate #$title language)) (sxml-index language title `(main (article (@ (class "page centered-block limit-width")) (h2 ,title) (p (@ (class "post-metadata centered-text")) #$version " — " ,(seconds->string #$date language)) (div (ul (li (a (@ (href "html_node")) "HTML, with a separate page per node")) (li (a (@ (href ,(string-append #$manual (if (string=? language "en") "" (string-append "." language)) ".html"))) "HTML, entirely on one page")) ,@(if (member language '("ru" "zh_CN")) '() `((li (a (@ (href ,(string-append #$manual (if (string=? language "en") "" (string-append "." language)) ".pdf")))) "PDF"))))))))) (define (top-level-index languages) (define title #$title) (sxml-index "en" title `(main (article (@ (class "page centered-block limit-width")) (h2 ,title) (div "This document is available in the following languages:\n" (ul ,@(map (lambda (language) `(li (a (@ (href ,(normalize language))) ,(language-code->native-name language)))) languages))))))) (define (write-html file sxml) (call-with-output-file file (lambda (port) (display "<!DOCTYPE html>\n" port) (sxml->xml sxml port)))) (setenv "GUIX_LOCPATH" #+(file-append glibc-utf8-locales "/lib/locale")) (setenv "LC_ALL" "en_US.utf8") (setlocale LC_ALL "en_US.utf8") (for-each (lambda (language) (define directory (string-append #$output "/" (normalize language))) (mkdir-p directory) (write-html (string-append directory "/index.html") (language-index language))) '#$languages) (write-html (string-append #$output "/index.html") (top-level-index '#$languages))))) (computed-file "html-indexes" build)) (define* (pdf+html-manual source #:key (languages %languages) (version "0.0") (date (time-second (current-time time-utc))) (mono-node-indexes (map list %languages)) (split-node-indexes (map list %languages)) (manual %manual)) "Return the union of the HTML and PDF manuals, as well as the indexes." (directory-union (string-append manual "-manual") (map (lambda (proc) (proc source #:date date #:languages languages #:version version #:manual manual)) (list html-manual-indexes (lambda (source . args) (apply html-manual source #:mono-node-indexes mono-node-indexes #:split-node-indexes split-node-indexes args)) pdf-manual)) #:copy? #t)) (define (latest-commit+date directory) "Return two values: the last commit ID (a hex string) for DIRECTORY, and its commit date (an integer)." (let* ((repository (repository-open directory)) (head (repository-head repository)) (oid (reference-target head)) (commit (commit-lookup repository oid))) ;; TODO: Use (git describe) when it's widely available. (values (oid->string oid) (commit-time commit)))) ;;; ;;; Guile manual. ;;; (define guile-manual ;; The Guile manual as HTML, including both the mono-node "guile.html" and ;; the split-node "html_node" directory. (let ((guile guile-3.0-latest)) (computed-file (string-append "guile-manual-" (package-version guile)) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (setenv "PATH" (string-append #+tar "/bin:" #+xz "/bin:" #+zstd "/bin:" #+texinfo "/bin")) (invoke "tar" "xf" #$(package-source guile)) (mkdir-p (string-append #$output "/en/html_node")) (let* ((texi (find-files "." "^guile\\.texi$")) (documentation (match texi ((file) (dirname file))))) (with-directory-excursion documentation (invoke "makeinfo" "--html" "--no-split" "-o" (string-append #$output "/en/guile.html") "guile.texi") (invoke "makeinfo" "--html" "-o" "split" "guile.texi") (copy-recursively "split" (string-append #$output "/en/html_node"))))))))) (define %guile-manual-base-url "https://www.gnu.org/software/guile/manual") (define (for-all-languages index) (map (lambda (language) (list language index)) %languages)) (define guile-mono-node-indexes ;; The Guile manual is only available in English so use the same index in ;; all languages. (for-all-languages (html-manual-identifier-index (file-append guile-manual "/en") %guile-manual-base-url #:name "guile-html-index-en"))) (define guile-split-node-indexes (for-all-languages (html-manual-identifier-index (file-append guile-manual "/en/html_node") (string-append %guile-manual-base-url "/html_node") #:name "guile-html-index-en"))) (define (merge-index-alists alist1 alist2) "Merge ALIST1 and ALIST2, both of which are list of tuples like: (LANGUAGE INDEX1 INDEX2 ...) where LANGUAGE is a string like \"en\" and INDEX1 etc. are indexes as returned by 'html-identifier-indexes'." (let ((languages (delete-duplicates (append (match alist1 (((languages . _) ...) languages)) (match alist2 (((languages . _) ...) languages)))))) (map (lambda (language) (cons language (append (or (assoc-ref alist1 language) '()) (or (assoc-ref alist2 language) '())))) languages))) (let* ((root (canonicalize-path (string-append (current-source-directory) "/.."))) (commit date (latest-commit+date root)) (version (or (getenv "GUIX_MANUAL_VERSION") (string-take commit 7))) (select? (let ((vcs? (git-predicate root))) (lambda (file stat) (and (vcs? file stat) ;; Filter out this file. (not (string=? (basename file) "build.scm")))))) (source (local-file root "guix" #:recursive? #t #:select? select?))) (define guix-manual (html-manual source #:manual "guix" #:version version #:date date)) (define guix-mono-node-indexes ;; Alist of indexes for GUIX-MANUAL, where each key is a language code and ;; each value is a file-like object containing the identifier index. (html-identifier-indexes guix-manual "" #:manual-name "guix" #:base-url (if (string=? %manual "guix") (const "") (cut string-append "/manual/devel/" <>)) #:languages %languages)) (define guix-split-node-indexes ;; Likewise for the split-node variant of GUIX-MANUAL. (html-identifier-indexes guix-manual "/html_node" #:manual-name "guix" #:base-url (if (string=? %manual "guix") (const "") (cut string-append "/manual/devel/" <> "/html_node")) #:languages %languages)) (define mono-node-indexes (merge-index-alists guix-mono-node-indexes guile-mono-node-indexes)) (define split-node-indexes (merge-index-alists guix-split-node-indexes guile-split-node-indexes)) (format (current-error-port) "building manual from work tree around commit ~a, ~a~%" commit (let* ((time (make-time time-utc 0 date)) (date (time-utc->date time))) (date->string date "~e ~B ~Y"))) (pdf+html-manual source ;; Always use the identifier indexes of GUIX-MANUAL and ;; GUILE-MANUAL. Both "guix" and "guix-cookbook" can ;; contain links to definitions that appear in either of ;; these two manuals. #:mono-node-indexes mono-node-indexes #:split-node-indexes split-node-indexes #:version version #:date date))