doc: Generate cross-references in code snippets to the Guile manual.
[jackhill/guix/guix.git] / doc / build.scm
CommitLineData
ccadafdc 1;;; GNU Guix --- Functional package management for GNU
f9e0488c 2;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
7c65fc37 3;;; Copyright © 2020 Björn Höfling <bjoern.hoefling@bjoernhoefling.de>
ccadafdc
LC
4;;;
5;;; This file is part of GNU Guix.
6;;;
7;;; GNU Guix is free software; you can redistribute it and/or modify it
8;;; under the terms of the GNU General Public License as published by
9;;; the Free Software Foundation; either version 3 of the License, or (at
10;;; your option) any later version.
11;;;
12;;; GNU Guix is distributed in the hope that it will be useful, but
13;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;;; GNU General Public License for more details.
16;;;
17;;; You should have received a copy of the GNU General Public License
18;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20
21;; This file contains machinery to build HTML and PDF copies of the manual
22;; that can be readily published on the web site. To do that, run:
23;;
24;; guix build -f build.scm
25;;
26;; The result is a directory hierarchy that can be used as the manual/
27;; sub-directory of the web site.
28
29(use-modules (guix)
30 (guix gexp)
31 (guix git)
32 (guix git-download)
7854bbeb 33 (guix utils)
ccadafdc
LC
34 (git)
35 (gnu packages base)
97ce30cc 36 (gnu packages compression)
ccadafdc
LC
37 (gnu packages gawk)
38 (gnu packages gettext)
39 (gnu packages guile)
f8c143a7 40 (gnu packages guile-xyz)
e591541d 41 (gnu packages iso-codes)
ccadafdc
LC
42 (gnu packages texinfo)
43 (gnu packages tex)
0f7d0743 44 (ice-9 match)
97ce30cc 45 (srfi srfi-1)
ccadafdc 46 (srfi srfi-19)
db1d4453 47 (srfi srfi-26)
ccadafdc
LC
48 (srfi srfi-71))
49
50(define file-append*
51 (@@ (guix self) file-append*))
52
53(define translated-texi-manuals
54 (@@ (guix self) translate-texi-manuals))
55
56(define info-manual
57 (@@ (guix self) info-manual))
58
cacb5576
LC
59(define %manual
60 ;; The manual to build--i.e., the base name of a .texi file, such as "guix"
61 ;; or "guix-cookbook".
62 (or (getenv "GUIX_MANUAL")
63 "guix"))
64
ccadafdc 65(define %languages
7c65fc37
BH
66 ;; The cookbook is currently only translated into German.
67 (if (string=? %manual "guix-cookbook")
68 '("de" "en")
69 '("de" "en" "es" "fr" "ru" "zh_CN")))
ccadafdc
LC
70
71(define (texinfo-manual-images source)
72 "Return a directory containing all the images used by the user manual, taken
73from SOURCE, the root of the source tree."
74 (define graphviz
75 (module-ref (resolve-interface '(gnu packages graphviz))
76 'graphviz))
77
78 (define images
79 (file-append* source "doc/images"))
80
81 (define build
82 (with-imported-modules '((guix build utils))
83 #~(begin
84 (use-modules (guix build utils)
85 (srfi srfi-26))
86
87 (define (dot->image dot-file format)
88 (invoke #+(file-append graphviz "/bin/dot")
89 "-T" format "-Gratio=.9" "-Gnodesep=.005"
90 "-Granksep=.00005" "-Nfontsize=9"
91 "-Nheight=.1" "-Nwidth=.1"
92 "-o" (string-append #$output "/"
93 (basename dot-file ".dot")
94 "." format)
95 dot-file))
96
97 ;; Build graphs.
98 (mkdir-p #$output)
99 (for-each (lambda (dot-file)
100 (for-each (cut dot->image dot-file <>)
101 '("png" "pdf")))
102 (find-files #$images "\\.dot$"))
103
104 ;; Copy other PNGs.
105 (for-each (lambda (png-file)
106 (install-file png-file #$output))
107 (find-files #$images "\\.png$")))))
108
109 (computed-file "texinfo-manual-images" build))
110
111(define* (texinfo-manual-source source #:key
112 (version "0.0")
113 (languages %languages)
114 (date 1))
115 "Gather all the source files of the Texinfo manuals from SOURCE--.texi file
116as well as images, OS examples, and translations."
117 (define documentation
118 (file-append* source "doc"))
119
120 (define examples
121 (file-append* source "gnu/system/examples"))
122
123 (define build
124 (with-imported-modules '((guix build utils))
125 #~(begin
126 (use-modules (guix build utils)
127 (srfi srfi-19))
128
129 (define (make-version-texi language)
130 ;; Create the 'version.texi' file for LANGUAGE.
131 (let ((file (if (string=? language "en")
132 "version.texi"
133 (string-append "version-" language ".texi"))))
134 (call-with-output-file (string-append #$output "/" file)
135 (lambda (port)
136 (let* ((version #$version)
137 (time (make-time time-utc 0 #$date))
138 (date (time-utc->date time)))
139 (format port "
140@set UPDATED ~a
141@set UPDATED-MONTH ~a
142@set EDITION ~a
143@set VERSION ~a\n"
144 (date->string date "~e ~B ~Y")
145 (date->string date "~B ~Y")
146 version version))))))
147
8c23d7a1 148 (install-file #$(file-append documentation "/htmlxref.cnf")
ccadafdc
LC
149 #$output)
150
151 (for-each (lambda (texi)
152 (install-file texi #$output))
cb26edc8 153 (append (find-files #$documentation "\\.(texi|scm|json)$")
ccadafdc
LC
154 (find-files #$(translated-texi-manuals source)
155 "\\.texi$")))
156
157 ;; Create 'version.texi'.
158 (for-each make-version-texi '#$languages)
159
160 ;; Copy configuration templates that the manual includes.
161 (for-each (lambda (template)
162 (copy-file template
163 (string-append
164 #$output "/os-config-"
165 (basename template ".tmpl")
166 ".texi")))
167 (find-files #$examples "\\.tmpl$"))
168
169 (symlink #$(texinfo-manual-images source)
170 (string-append #$output "/images")))))
171
172 (computed-file "texinfo-manual-source" build))
173
174(define %web-site-url
175 ;; URL of the web site home page.
176 (or (getenv "GUIX_WEB_SITE_URL")
177 "/software/guix/"))
178
179(define %makeinfo-html-options
180 ;; Options passed to 'makeinfo --html'.
2f000f2e
JL
181 '("--css-ref=https://www.gnu.org/software/gnulib/manual.css"
182 "-c" "EXTRA_HEAD=<meta name=\"viewport\" \
183content=\"width=device-width, initial-scale=1\" />"))
ccadafdc 184
7854bbeb
LC
185(define guile-lib/htmlprag-fixed
186 ;; Guile-Lib with a hotfix for (htmlprag).
187 (package
188 (inherit guile-lib)
7854bbeb
LC
189 (arguments
190 (substitute-keyword-arguments (package-arguments guile-lib)
191 ((#:phases phases '%standard-phases)
192 `(modify-phases ,phases
e5b495c1
LC
193 (add-before 'build 'fix-htmlprag
194 (lambda _
195 ;; When parsing
196 ;; "<body><blockquote><p>foo</p>\n</blockquote></body>",
197 ;; 'html->shtml' would mistakenly close 'blockquote' right
198 ;; before <p>. This patch removes 'p' from the
199 ;; 'parent-constraints' alist to fix that.
200 (substitute* "src/htmlprag.scm"
201 (("^[[:blank:]]*\\(p[[:blank:]]+\\. \\(body td th\\)\\).*")
202 ""))
203 #t))
204 (add-before 'check 'skip-known-failure
205 (lambda _
206 ;; XXX: The above change causes one test failure among
207 ;; the htmlprag tests.
208 (setenv "XFAIL_TESTS" "htmlprag.scm")
209 #t))))))))
7854bbeb 210
0f7d0743
LC
211(define (normalize-language-code language) ;XXX: deduplicate
212 ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn".
213 (string-map (match-lambda
214 (#\_ #\-)
215 (chr chr))
216 (string-downcase language)))
217
218(define* (html-manual-identifier-index manual base-url
219 #:key
220 (name "html-manual-identifier-index"))
221 "Return an index of all the identifiers that appear in MANUAL, a
222makeinfo-generated manual. The index is a file that contains an alist; each
223key is an identifier and the associated value is the URL reference pointing to
224that identifier. The URL is constructed by concatenating BASE-URL to the
225actual file name."
226 (define build
227 (with-extensions (list guile-lib/htmlprag-fixed)
228 (with-imported-modules '((guix build utils))
229 #~(begin
230 (use-modules (guix build utils)
231 (htmlprag)
232 (srfi srfi-1)
233 (srfi srfi-26)
234 (ice-9 ftw)
235 (ice-9 match)
236 (ice-9 threads)
237 (ice-9 pretty-print))
238
239 (define file-url
240 (let ((prefix (string-append #$manual "/")))
241 (lambda (file)
242 ;; Return the URL for FILE.
243 (let ((file (string-drop file (string-length prefix)))
244 (base #$base-url))
245 (if (string-null? base)
246 file
247 (string-append base "/" file))))))
248
249 (define (underscore-decode str)
250 ;; Decode STR, an "underscore-encoded" string as produced by
251 ;; makeinfo for indexes, such as "_0025base_002dservices" for
252 ;; "%base-services".
253 (let loop ((str str)
254 (result '()))
255 (match (string-index str #\_)
256 (#f
257 (string-concatenate-reverse (cons str result)))
258 (index
259 (let ((char (string->number
260 (substring str (+ index 1) (+ index 5))
261 16)))
262 (loop (string-drop str (+ index 5))
263 (append (list (string (integer->char char))
264 (string-take str index))
265 result)))))))
266
267 (define (anchor-id->key id)
268 ;; Convert ID, an anchor ID such as
269 ;; "index-pam_002dlimits_002dservice" to the corresponding key,
270 ;; "pam-limits-service" in this example. Drop the suffix of
271 ;; duplicate anchor IDs like "operating_002dsystem-1".
272 (let ((id (if (any (cut string-suffix? <> id)
273 '("-1" "-2" "-3" "-4" "-5"))
274 (string-drop-right id 2)
275 id)))
276 (underscore-decode
277 (string-drop id (string-length "index-")))))
278
279 (define* (collect-anchors file #:optional (anchors '()))
280 ;; Collect the anchors that appear in FILE, a makeinfo-generated
281 ;; file. Grab those from <dt> tags, which corresponds to
282 ;; Texinfo @deftp, @defvr, etc. Return ANCHORS augmented with
283 ;; more name/reference pairs.
284 (define string-or-entity?
285 (match-lambda
286 ((? string?) #t)
287 (('*ENTITY* _ ...) #t)
288 (_ #f)))
289
290 (define (worthy-entry? lst)
291 ;; Attempt to match:
292 ;; Scheme Variable: <strong>x</strong>
293 ;; but not:
294 ;; <code>cups-configuration</code> parameter: …
295 (let loop ((lst lst))
296 (match lst
297 (((? string-or-entity?) rest ...)
298 (loop rest))
299 ((('strong _ ...) _ ...)
300 #t)
301 (_ #f))))
302
303 (let ((shtml (call-with-input-file file html->shtml)))
304 (let loop ((shtml shtml)
305 (anchors anchors))
306 (match shtml
307 (('dt ('@ ('id id)) rest ...)
308 (if (and (string-prefix? "index-" id)
309 (worthy-entry? rest))
310 (alist-cons (anchor-id->key id)
311 (string-append (file-url file)
312 "#" id)
313 anchors)
314 anchors))
315 ((tag ('@ _ ...) body ...)
316 (fold loop anchors body))
317 ((tag body ...)
318 (fold loop anchors body))
319 (_ anchors)))))
320
321 (define (html-files directory)
322 ;; Return the list of HTML files under DIRECTORY.
323 (map (cut string-append directory "/" <>)
324 (scandir #$manual (lambda (file)
325 (string-suffix? ".html" file)))))
326
327 (define anchors
328 (sort (concatenate
329 (n-par-map (parallel-job-count)
330 (cut collect-anchors <>)
331 (html-files #$manual)))
332 (match-lambda*
333 (((key1 . url1) (key2 . url2))
334 (if (string=? key1 key2)
335 (string<? url1 url2)
336 (string<? key1 key2))))))
337
338 (call-with-output-file #$output
339 (lambda (port)
340 (display ";; Identifier index for the manual.\n\n"
341 port)
342 (pretty-print anchors port)))))))
343
344 (computed-file name build))
345
346(define* (html-identifier-indexes manual directory-suffix
347 #:key (languages %languages)
348 (manual-name %manual)
349 (base-url (const "")))
350 (map (lambda (language)
351 (let ((language (normalize-language-code language)))
352 (list language
353 (html-manual-identifier-index
354 (file-append manual "/" language directory-suffix)
355 (base-url language)
356 #:name (string-append manual-name "-html-index-"
357 language)))))
358 languages))
359
f8c143a7
LC
360(define* (syntax-highlighted-html input
361 #:key
362 (name "highlighted-syntax")
0f7d0743
LC
363 (languages %languages)
364 (mono-node-indexes
365 (html-identifier-indexes input ""
366 #:languages
367 languages))
368 (split-node-indexes
369 (html-identifier-indexes input
370 "/html_node"
371 #:languages
372 languages))
f8c143a7
LC
373 (syntax-css-url
374 "/static/base/css/code.css"))
375 "Return a derivation called NAME that processes all the HTML files in INPUT
376to (1) add them a link to SYNTAX-CSS-URL, and (2) highlight the syntax of all
377its <pre class=\"lisp\"> blocks (as produced by 'makeinfo --html')."
378 (define build
7854bbeb 379 (with-extensions (list guile-lib/htmlprag-fixed guile-syntax-highlight)
f8c143a7
LC
380 (with-imported-modules '((guix build utils))
381 #~(begin
382 (use-modules (htmlprag)
383 (syntax-highlight)
384 (syntax-highlight scheme)
385 (syntax-highlight lexers)
386 (guix build utils)
da9deba1 387 (srfi srfi-1)
deac7bf6 388 (srfi srfi-26)
f8c143a7 389 (ice-9 match)
da9deba1
LC
390 (ice-9 threads)
391 (ice-9 vlist))
f8c143a7 392
012c93e9
LC
393 (define (pair-open/close lst)
394 ;; Pair 'open' and 'close' tags produced by 'highlights' and
395 ;; produce nested 'paren' tags instead.
396 (let loop ((lst lst)
397 (level 0)
398 (result '()))
399 (match lst
400 ((('open open) rest ...)
401 (call-with-values
402 (lambda ()
403 (loop rest (+ 1 level) '()))
404 (lambda (inner close rest)
405 (loop rest level
406 (cons `(paren ,level ,open ,inner ,close)
407 result)))))
408 ((('close str) rest ...)
409 (if (> level 0)
410 (values (reverse result) str rest)
411 (begin
412 (format (current-error-port)
413 "warning: extra closing paren; context:~% ~y~%"
414 (reverse result))
415 (loop rest 0 (cons `(close ,str) result)))))
416 ((item rest ...)
417 (loop rest level (cons item result)))
418 (()
419 (when (> level 0)
420 (format (current-error-port)
421 "warning: missing ~a closing parens; context:~% ~y%"
422 level (reverse result)))
423 (values (reverse result) "" '())))))
424
da9deba1 425 (define (highlights->sxml* highlights anchors)
012c93e9
LC
426 ;; Like 'highlights->sxml', but handle nested 'paren tags. This
427 ;; allows for paren matching highlights via appropriate CSS
da9deba1
LC
428 ;; "hover" properties. When a symbol is encountered, look it up
429 ;; in ANCHORS, a vhash, and emit the corresponding href, if any.
012c93e9
LC
430 (define (tag->class tag)
431 (string-append "syntax-" (symbol->string tag)))
432
433 (map (match-lambda
434 ((? string? str) str)
435 (('paren level open (body ...) close)
436 `(span (@ (class ,(string-append "syntax-paren"
437 (number->string level))))
438 ,open
439 (span (@ (class "syntax-symbol"))
da9deba1 440 ,@(highlights->sxml* body anchors))
012c93e9 441 ,close))
da9deba1
LC
442 (('symbol text)
443 ;; Check whether we can emit a hyperlink for TEXT.
444 (match (vhash-assoc text anchors)
445 (#f
446 `(span (@ (class ,(tag->class 'symbol))) ,text))
447 ((_ . target)
448 `(a (@ (class ,(tag->class 'symbol)) (href ,target))
449 ,text))))
012c93e9
LC
450 ((tag text)
451 `(span (@ (class ,(tag->class tag))) ,text)))
452 highlights))
453
f8c143a7
LC
454 (define entity->string
455 (match-lambda
456 ("rArr" "⇒")
295c6a7e 457 ("rarr" "→")
f8c143a7
LC
458 ("hellip" "…")
459 ("rsquo" "’")
460 (e (pk 'unknown-entity e) (primitive-exit 2))))
461
462 (define (concatenate-snippets pieces)
463 ;; Concatenate PIECES, which contains strings and entities,
464 ;; replacing entities with their corresponding string.
465 (let loop ((pieces pieces)
466 (strings '()))
467 (match pieces
468 (()
469 (string-concatenate-reverse strings))
470 (((? string? str) . rest)
471 (loop rest (cons str strings)))
472 ((('*ENTITY* "additional" entity) . rest)
473 (loop rest (cons (entity->string entity) strings)))
474 ((('span _ lst ...) . rest) ;for <span class="roman">
475 (loop (append lst rest) strings))
fe409700
LC
476 ((('var name) . rest) ;for @var{name} within @lisp
477 (loop rest (cons name strings))) ;XXX: losing formatting
f8c143a7
LC
478 (something
479 (pk 'unsupported-code-snippet something)
480 (primitive-exit 1)))))
481
da9deba1 482 (define (syntax-highlight sxml anchors)
f8c143a7 483 ;; Recurse over SXML and syntax-highlight code snippets.
da9deba1
LC
484 (let loop ((sxml sxml))
485 (match sxml
486 (('*TOP* decl body ...)
487 `(*TOP* ,decl ,@(map loop body)))
488 (('head things ...)
489 `(head ,@things
490 (link (@ (rel "stylesheet")
491 (type "text/css")
492 (href #$syntax-css-url)))))
493 (('pre ('@ ('class "lisp")) code-snippet ...)
494 `(pre (@ (class "lisp"))
495 ,@(highlights->sxml*
496 (pair-open/close
497 (highlight lex-scheme
498 (concatenate-snippets code-snippet)))
499 anchors)))
500 ((tag ('@ attributes ...) body ...)
501 `(,tag (@ ,@attributes) ,@(map loop body)))
502 ((tag body ...)
503 `(,tag ,@(map loop body)))
504 ((? string? str)
505 str))))
506
da9deba1 507 (define (process-html file anchors)
f8c143a7
LC
508 ;; Parse FILE and perform syntax highlighting for its Scheme
509 ;; snippets. Install the result to #$output.
510 (format (current-error-port) "processing ~a...~%" file)
511 (let* ((shtml (call-with-input-file file html->shtml))
da9deba1 512 (highlighted (syntax-highlight shtml anchors))
f8c143a7
LC
513 (base (string-drop file (string-length #$input)))
514 (target (string-append #$output base)))
515 (mkdir-p (dirname target))
516 (call-with-output-file target
517 (lambda (port)
518 (write-shtml-as-html highlighted port)))))
519
520 (define (copy-as-is file)
521 ;; Copy FILE as is to #$output.
522 (let* ((base (string-drop file (string-length #$input)))
523 (target (string-append #$output base)))
524 (mkdir-p (dirname target))
525 (catch 'system-error
526 (lambda ()
527 (if (eq? 'symlink (stat:type (lstat file)))
528 (symlink (readlink file) target)
529 (link file target)))
530 (lambda args
531 (let ((errno (system-error-errno args)))
532 (pk 'error-link file target (strerror errno))
533 (primitive-exit 3))))))
534
da9deba1
LC
535 (define (html? file stat)
536 (string-suffix? ".html" file))
537
0f7d0743
LC
538 (define language+node-anchors
539 (match-lambda
540 ((language files ...)
541 (cons language
542 (fold (lambda (file vhash)
543 (let ((alist (call-with-input-file file read)))
544 ;; Use 'fold-right' so that the first entry
545 ;; wins (e.g., "car" from "Pairs" rather than
546 ;; from "rnrs base" in the Guile manual).
547 (fold-right (match-lambda*
548 (((key . value) vhash)
549 (vhash-cons key value vhash)))
550 vhash
551 alist)))
552 vlist-null
553 files)))))
554
555 (define mono-node-anchors
556 ;; List of language/vhash pairs, where each vhash maps an
557 ;; identifier to the corresponding URL in a single-page manual.
558 (map language+node-anchors '#$mono-node-indexes))
559
560 (define multi-node-anchors
561 ;; Likewise for split-node manuals.
562 (map language+node-anchors '#$split-node-indexes))
563
f8c143a7
LC
564 ;; Install a UTF-8 locale so we can process UTF-8 files.
565 (setenv "GUIX_LOCPATH"
566 #+(file-append glibc-utf8-locales "/lib/locale"))
567 (setlocale LC_ALL "en_US.utf8")
568
da9deba1 569 ;; First process the mono-node 'guix.html' files.
0f7d0743
LC
570 (for-each (match-lambda
571 ((language . anchors)
572 (let ((files (find-files
573 (string-append #$input "/" language)
574 "^guix(-cookbook|)(\\.[a-zA-Z_-]+)?\\.html$")))
575 (n-par-for-each (parallel-job-count)
576 (cut process-html <> anchors)
577 files))))
578 mono-node-anchors)
579
580 ;; Process the multi-node HTML files.
581 (for-each (match-lambda
582 ((language . anchors)
583 (let ((files (find-files
584 (string-append #$input "/" language
585 "/html_node")
586 "\\.html$")))
587 (n-par-for-each (parallel-job-count)
588 (cut process-html <> anchors)
589 files))))
590 multi-node-anchors)
da9deba1
LC
591
592 ;; Last, copy non-HTML files as is.
593 (for-each copy-as-is
594 (find-files #$input (negate html?)))))))
f8c143a7
LC
595
596 (computed-file name build))
597
ccadafdc
LC
598(define* (html-manual source #:key (languages %languages)
599 (version "0.0")
cacb5576 600 (manual %manual)
db1d4453
LC
601 (mono-node-indexes (map list languages))
602 (split-node-indexes (map list languages))
ccadafdc
LC
603 (date 1)
604 (options %makeinfo-html-options))
605 "Return the HTML manuals built from SOURCE for all LANGUAGES, with the given
606makeinfo OPTIONS."
607 (define manual-source
608 (texinfo-manual-source source
609 #:version version
610 #:languages languages
611 #:date date))
612
e3e9c191
LC
613 (define images
614 (texinfo-manual-images source))
615
ccadafdc
LC
616 (define build
617 (with-imported-modules '((guix build utils))
618 #~(begin
619 (use-modules (guix build utils)
620 (ice-9 match))
621
622 (define (normalize language)
e591541d 623 ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn".
ccadafdc
LC
624 (string-map (match-lambda
625 (#\_ #\-)
626 (chr chr))
627 (string-downcase language)))
628
cacb5576
LC
629 (define (language->texi-file-name language)
630 (if (string=? language "en")
631 (string-append #$manual-source "/"
632 #$manual ".texi")
633 (string-append #$manual-source "/"
634 #$manual "." language ".texi")))
635
ccadafdc
LC
636 ;; Install a UTF-8 locale so that 'makeinfo' is at ease.
637 (setenv "GUIX_LOCPATH"
638 #+(file-append glibc-utf8-locales "/lib/locale"))
639 (setenv "LC_ALL" "en_US.utf8")
640
641 (setvbuf (current-output-port) 'line)
642 (setvbuf (current-error-port) 'line)
643
f9e0488c
LC
644 ;; 'makeinfo' looks for "htmlxref.cnf" in the current directory, so
645 ;; copy it right here.
646 (copy-file (string-append #$manual-source "/htmlxref.cnf")
647 "htmlxref.cnf")
648
ccadafdc 649 (for-each (lambda (language)
cacb5576
LC
650 (let* ((texi (language->texi-file-name language))
651 (opts `("--html"
652 "-c" ,(string-append "TOP_NODE_UP_URL=/manual/"
ccadafdc 653 language)
cacb5576
LC
654 #$@options
655 ,texi)))
ccadafdc
LC
656 (format #t "building HTML manual for language '~a'...~%"
657 language)
658 (mkdir-p (string-append #$output "/"
659 (normalize language)))
660 (setenv "LANGUAGE" language)
661 (apply invoke #$(file-append texinfo "/bin/makeinfo")
662 "-o" (string-append #$output "/"
663 (normalize language)
664 "/html_node")
665 opts)
666 (apply invoke #$(file-append texinfo "/bin/makeinfo")
667 "--no-split"
668 "-o"
669 (string-append #$output "/"
670 (normalize language)
671 "/" #$manual
672 (if (string=? language "en")
673 ""
674 (string-append "." language))
675 ".html")
e3e9c191
LC
676 opts)
677
678 ;; Make sure images are available.
679 (symlink #$images
680 (string-append #$output "/" (normalize language)
681 "/images"))
682 (symlink #$images
683 (string-append #$output "/" (normalize language)
684 "/html_node/images"))))
cacb5576
LC
685 (filter (compose file-exists? language->texi-file-name)
686 '#$languages)))))
ccadafdc 687
f8c143a7
LC
688 (let* ((name (string-append manual "-html-manual"))
689 (manual (computed-file name build)))
690 (syntax-highlighted-html manual
db1d4453
LC
691 #:mono-node-indexes mono-node-indexes
692 #:split-node-indexes split-node-indexes
f8c143a7 693 #:name (string-append name "-highlighted"))))
ccadafdc
LC
694
695(define* (pdf-manual source #:key (languages %languages)
696 (version "0.0")
cacb5576 697 (manual %manual)
ccadafdc
LC
698 (date 1)
699 (options '()))
700 "Return the HTML manuals built from SOURCE for all LANGUAGES, with the given
701makeinfo OPTIONS."
702 (define manual-source
703 (texinfo-manual-source source
704 #:version version
705 #:languages languages
706 #:date date))
707
708 ;; FIXME: This union works, except for the table of contents of non-English
709 ;; manuals, which contains escape sequences like "^^ca^^fe" instead of
710 ;; accented letters.
711 ;;
712 ;; (define texlive
713 ;; (texlive-union (list texlive-tex-texinfo
714 ;; texlive-generic-epsf
715 ;; texlive-fonts-ec)))
716
717 (define build
718 (with-imported-modules '((guix build utils))
719 #~(begin
720 (use-modules (guix build utils)
721 (srfi srfi-34)
722 (ice-9 match))
723
724 (define (normalize language) ;XXX: deduplicate
725 ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn".
726 (string-map (match-lambda
727 (#\_ #\-)
728 (chr chr))
729 (string-downcase language)))
730
731 ;; Install a UTF-8 locale so that 'makeinfo' is at ease.
732 (setenv "GUIX_LOCPATH"
733 #+(file-append glibc-utf8-locales "/lib/locale"))
734 (setenv "LC_ALL" "en_US.utf8")
735 (setenv "PATH"
736 (string-append #+(file-append texlive "/bin") ":"
737 #+(file-append texinfo "/bin") ":"
738
739 ;; Below are command-line tools needed by
740 ;; 'texi2dvi' and friends.
741 #+(file-append sed "/bin") ":"
742 #+(file-append grep "/bin") ":"
743 #+(file-append coreutils "/bin") ":"
744 #+(file-append gawk "/bin") ":"
745 #+(file-append tar "/bin") ":"
746 #+(file-append diffutils "/bin")))
747
748 (setvbuf (current-output-port) 'line)
749 (setvbuf (current-error-port) 'line)
750
751 (setenv "HOME" (getcwd)) ;for kpathsea/mktextfm
752
753 ;; 'SOURCE_DATE_EPOCH' is honored by pdftex.
754 (setenv "SOURCE_DATE_EPOCH" "1")
755
756 (for-each (lambda (language)
757 (let ((opts `("--pdf"
758 "-I" "."
759 #$@options
760 ,(if (string=? language "en")
761 (string-append #$manual-source "/"
762 #$manual ".texi")
763 (string-append #$manual-source "/"
764 #$manual "." language ".texi")))))
765 (format #t "building PDF manual for language '~a'...~%"
766 language)
767 (mkdir-p (string-append #$output "/"
768 (normalize language)))
769 (setenv "LANGUAGE" language)
770
771
772 ;; FIXME: Unfortunately building PDFs for non-Latin
773 ;; alphabets doesn't work:
774 ;; <https://lists.gnu.org/archive/html/help-texinfo/2012-01/msg00014.html>.
775 (guard (c ((invoke-error? c)
776 (format (current-error-port)
777 "~%~%Failed to produce \
778PDF for language '~a'!~%~%"
779 language)))
780 (apply invoke #$(file-append texinfo "/bin/makeinfo")
781 "--pdf" "-o"
782 (string-append #$output "/"
783 (normalize language)
784 "/" #$manual
785 (if (string=? language "en")
786 ""
787 (string-append "."
788 language))
789 ".pdf")
790 opts))))
791 '#$languages))))
792
793 (computed-file (string-append manual "-pdf-manual") build))
794
795(define (guix-manual-text-domain source languages)
796 "Return the PO files for LANGUAGES of the 'guix-manual' text domain taken
797from SOURCE."
798 (define po-directory
799 (file-append* source "/po/doc"))
800
801 (define build
802 (with-imported-modules '((guix build utils))
803 #~(begin
804 (use-modules (guix build utils))
805
806 (mkdir-p #$output)
807 (for-each (lambda (language)
808 (define directory
809 (string-append #$output "/" language
810 "/LC_MESSAGES"))
811
812 (mkdir-p directory)
813 (invoke #+(file-append gnu-gettext "/bin/msgfmt")
814 "-c" "-o"
815 (string-append directory "/guix-manual.mo")
816 (string-append #$po-directory "/guix-manual."
817 language ".po")))
818 '#$(delete "en" languages)))))
819
820 (computed-file "guix-manual-po" build))
821
822(define* (html-manual-indexes source
823 #:key (languages %languages)
824 (version "0.0")
cacb5576 825 (manual %manual)
208cc522
LC
826 (title (if (string=? "guix" manual)
827 "GNU Guix Reference Manual"
828 "GNU Guix Cookbook"))
ccadafdc
LC
829 (date 1))
830 (define build
e591541d
LC
831 (with-extensions (list guile-json-3)
832 (with-imported-modules '((guix build utils))
833 #~(begin
834 (use-modules (guix build utils)
835 (json)
836 (ice-9 match)
837 (ice-9 popen)
838 (sxml simple)
839 (srfi srfi-1)
840 (srfi srfi-19))
841
842 (define (normalize language) ;XXX: deduplicate
843 ;; Normalize LANGUAGE. For instance, "zh_CN" becomes "zh-cn".
844 (string-map (match-lambda
845 (#\_ #\-)
846 (chr chr))
847 (string-downcase language)))
848
849 (define-syntax-rule (with-language language exp ...)
850 (let ((lang (getenv "LANGUAGE")))
851 (dynamic-wind
852 (lambda ()
853 (setenv "LANGUAGE" language)
854 (setlocale LC_MESSAGES))
855 (lambda () exp ...)
856 (lambda ()
857 (if lang
858 (setenv "LANGUAGE" lang)
859 (unsetenv "LANGUAGE"))
860 (setlocale LC_MESSAGES)))))
861
862 ;; (put 'with-language 'scheme-indent-function 1)
863 (define* (translate str language
864 #:key (domain "guix-manual"))
865 (define exp
866 `(begin
867 (bindtextdomain "guix-manual"
868 #+(guix-manual-text-domain
869 source
870 languages))
871 (bindtextdomain "iso_639-3" ;language names
872 #+(file-append iso-codes
873 "/share/locale"))
874 (write (gettext ,str ,domain))))
875
876 (with-language language
877 ;; Since the 'gettext' function caches msgid translations,
878 ;; regardless of $LANGUAGE, we have to spawn a new process each
879 ;; time we want to translate to a different language. Bah!
880 (let* ((pipe (open-pipe* OPEN_READ
881 #+(file-append guile-2.2
882 "/bin/guile")
883 "-c" (object->string exp)))
884 (str (read pipe)))
885 (close-pipe pipe)
886 str)))
887
888 (define (seconds->string seconds language)
889 (let* ((time (make-time time-utc 0 seconds))
890 (date (time-utc->date time)))
891 (with-language language (date->string date "~e ~B ~Y"))))
892
893 (define (guix-url path)
894 (string-append #$%web-site-url path))
895
896 (define (sxml-index language title body)
897 ;; FIXME: Avoid duplicating styling info from guix-artwork.git.
898 `(html (@ (lang ,language))
899 (head
900 (title ,(string-append title " — GNU Guix"))
901 (meta (@ (charset "UTF-8")))
902 (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
903 ;; Menu prefetch.
904 (link (@ (rel "prefetch") (href ,(guix-url "menu/index.html"))))
905 ;; Base CSS.
906 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/elements.css"))))
907 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/common.css"))))
908 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/messages.css"))))
909 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/navbar.css"))))
910 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/breadcrumbs.css"))))
911 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/buttons.css"))))
912 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/footer.css"))))
913
914 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/page.css"))))
915 (link (@ (rel "stylesheet") (href ,(guix-url "static/base/css/post.css")))))
916 (body
917 (header (@ (class "navbar"))
918 (h1 (a (@ (class "branding")
919 (href #$%web-site-url)))
920 (span (@ (class "a11y-offset"))
921 "Guix"))
922 (nav (@ (class "menu"))))
923 (nav (@ (class "breadcrumbs"))
924 (a (@ (class "crumb")
925 (href #$%web-site-url))
926 "Home"))
927 ,body
928 (footer))))
929
930 (define (language-index language)
931 (define title
208cc522 932 (translate #$title language))
e591541d
LC
933
934 (sxml-index
935 language title
936 `(main
937 (article
938 (@ (class "page centered-block limit-width"))
939 (h2 ,title)
940 (p (@ (class "post-metadata centered-text"))
941 #$version " — "
942 ,(seconds->string #$date language))
943
944 (div
945 (ul
946 (li (a (@ (href "html_node"))
947 "HTML, with one page per node"))
948 (li (a (@ (href
949 ,(string-append
950 #$manual
951 (if (string=? language
952 "en")
953 ""
954 (string-append "."
955 language))
956 ".html")))
957 "HTML, entirely on one page"))
958 ,@(if (member language '("ru" "zh_CN"))
959 '()
960 `((li (a (@ (href ,(string-append
961 #$manual
962 (if (string=? language "en")
963 ""
964 (string-append "."
965 language))
966 ".pdf"))))
967 "PDF")))))))))
968
969 (define %iso639-languages
970 (vector->list
971 (assoc-ref (call-with-input-file
972 #+(file-append iso-codes
973 "/share/iso-codes/json/iso_639-3.json")
974 json->scm)
975 "639-3")))
976
977 (define (language-code->name code)
978 "Return the full name of a language from its ISO-639-3 code."
979 (let ((code (match (string-index code #\_)
980 (#f code)
981 (index (string-take code index)))))
982 (any (lambda (language)
983 (and (string=? (or (assoc-ref language "alpha_2")
984 (assoc-ref language "alpha_3"))
985 code)
986 (assoc-ref language "name")))
987 %iso639-languages)))
988
989 (define (top-level-index languages)
208cc522 990 (define title #$title)
e591541d
LC
991 (sxml-index
992 "en" title
993 `(main
994 (article
995 (@ (class "page centered-block limit-width"))
996 (h2 ,title)
997 (div
208cc522 998 "This document is available in the following
e591541d
LC
999languages:\n"
1000 (ul
1001 ,@(map (lambda (language)
1002 `(li (a (@ (href ,(normalize language)))
1003 ,(translate
1004 (language-code->name language)
1005 language
1006 #:domain "iso_639-3"))))
1007 languages)))))))
1008
1009 (define (write-html file sxml)
1010 (call-with-output-file file
1011 (lambda (port)
1012 (display "<!DOCTYPE html>\n" port)
1013 (sxml->xml sxml port))))
1014
1015 (setenv "GUIX_LOCPATH"
1016 #+(file-append glibc-utf8-locales "/lib/locale"))
1017 (setenv "LC_ALL" "en_US.utf8")
1018 (setlocale LC_ALL "en_US.utf8")
1019
1020 (for-each (lambda (language)
1021 (define directory
1022 (string-append #$output "/"
1023 (normalize language)))
1024
1025 (mkdir-p directory)
1026 (write-html (string-append directory "/index.html")
1027 (language-index language)))
1028 '#$languages)
1029
1030 (write-html (string-append #$output "/index.html")
1031 (top-level-index '#$languages))))))
ccadafdc
LC
1032
1033 (computed-file "html-indexes" build))
1034
1035(define* (pdf+html-manual source
1036 #:key (languages %languages)
1037 (version "0.0")
1038 (date (time-second (current-time time-utc)))
db1d4453
LC
1039 (mono-node-indexes (map list %languages))
1040 (split-node-indexes (map list %languages))
cacb5576 1041 (manual %manual))
ccadafdc
LC
1042 "Return the union of the HTML and PDF manuals, as well as the indexes."
1043 (directory-union (string-append manual "-manual")
1044 (map (lambda (proc)
1045 (proc source
1046 #:date date
1047 #:languages languages
1048 #:version version
1049 #:manual manual))
1050 (list html-manual-indexes
db1d4453
LC
1051 (lambda (source . args)
1052 (apply html-manual source
1053 #:mono-node-indexes mono-node-indexes
1054 #:split-node-indexes split-node-indexes
1055 args))
1056 pdf-manual))
ccadafdc
LC
1057 #:copy? #t))
1058
1059(define (latest-commit+date directory)
1060 "Return two values: the last commit ID (a hex string) for DIRECTORY, and its
1061commit date (an integer)."
1062 (let* ((repository (repository-open directory))
1063 (head (repository-head repository))
1064 (oid (reference-target head))
1065 (commit (commit-lookup repository oid)))
1066 ;; TODO: Use (git describe) when it's widely available.
1067 (values (oid->string oid) (commit-time commit))))
1068
1069\f
97ce30cc
LC
1070;;;
1071;;; Guile manual.
1072;;;
1073
1074(define guile-manual
1075 ;; The Guile manual as HTML, including both the mono-node "guile.html" and
1076 ;; the split-node "html_node" directory.
1077 (let ((guile guile-3.0-latest))
1078 (computed-file (string-append "guile-manual-" (package-version guile))
1079 (with-imported-modules '((guix build utils))
1080 #~(begin
1081 (use-modules (guix build utils)
1082 (ice-9 match))
1083
1084 (setenv "PATH"
1085 (string-append #+tar "/bin:"
1086 #+xz "/bin:"
1087 #+texinfo "/bin"))
1088 (invoke "tar" "xf" #$(package-source guile))
1089 (mkdir-p (string-append #$output "/en/html_node"))
1090
1091 (let* ((texi (find-files "." "^guile\\.texi$"))
1092 (documentation (match texi
1093 ((file) (dirname file)))))
1094 (with-directory-excursion documentation
1095 (invoke "makeinfo" "--html" "--no-split"
1096 "-o" (string-append #$output
1097 "/en/guile.html")
1098 "guile.texi")
1099 (invoke "makeinfo" "--html" "-o" "split"
1100 "guile.texi")
1101 (copy-recursively
1102 "split"
1103 (string-append #$output "/en/html_node")))))))))
1104
1105(define %guile-manual-base-url
1106 "https://www.gnu.org/software/guile/manual")
1107
1108(define (for-all-languages index)
1109 (map (lambda (language)
1110 (list language index))
1111 %languages))
1112
1113(define guile-mono-node-indexes
1114 ;; The Guile manual is only available in English so use the same index in
1115 ;; all languages.
1116 (for-all-languages
1117 (html-manual-identifier-index (file-append guile-manual "/en")
1118 %guile-manual-base-url
1119 #:name "guile-html-index-en")))
1120
1121(define guile-split-node-indexes
1122 (for-all-languages
1123 (html-manual-identifier-index (file-append guile-manual "/en/html_node")
1124 (string-append %guile-manual-base-url
1125 "/html_node")
1126 #:name "guile-html-index-en")))
1127
1128(define (merge-index-alists alist1 alist2)
1129 "Merge ALIST1 and ALIST2, both of which are list of tuples like:
1130
1131 (LANGUAGE INDEX1 INDEX2 ...)
1132
1133where LANGUAGE is a string like \"en\" and INDEX1 etc. are indexes as returned
1134by 'html-identifier-indexes'."
1135 (let ((languages (delete-duplicates
1136 (append (match alist1
1137 (((languages . _) ...)
1138 languages))
1139 (match alist2
1140 (((languages . _) ...)
1141 languages))))))
1142 (map (lambda (language)
1143 (cons language
1144 (append (or (assoc-ref alist1 language) '())
1145 (or (assoc-ref alist2 language) '()))))
1146 languages)))
1147
1148\f
ccadafdc
LC
1149(let* ((root (canonicalize-path
1150 (string-append (current-source-directory) "/..")))
3cd1a7ac 1151 (commit date (latest-commit+date root))
db1d4453
LC
1152 (version (or (getenv "GUIX_MANUAL_VERSION")
1153 (string-take commit 7)))
3cd1a7ac
LC
1154 (select? (let ((vcs? (git-predicate root)))
1155 (lambda (file stat)
1156 (and (vcs? file stat)
1157 ;; Filter out this file.
db1d4453
LC
1158 (not (string=? (basename file) "build.scm"))))))
1159 (source (local-file root "guix" #:recursive? #t
1160 #:select? select?)))
1161
1162 (define guix-manual
1163 (html-manual source
1164 #:manual "guix"
1165 #:version version
1166 #:date date))
1167
97ce30cc 1168 (define guix-mono-node-indexes
db1d4453
LC
1169 ;; Alist of indexes for GUIX-MANUAL, where each key is a language code and
1170 ;; each value is a file-like object containing the identifier index.
1171 (html-identifier-indexes guix-manual ""
97ce30cc 1172 #:manual-name "guix"
db1d4453
LC
1173 #:base-url (if (string=? %manual "guix")
1174 (const "")
1175 (cut string-append "/manual/" <>))
1176 #:languages %languages))
1177
97ce30cc 1178 (define guix-split-node-indexes
db1d4453
LC
1179 ;; Likewise for the split-node variant of GUIX-MANUAL.
1180 (html-identifier-indexes guix-manual "/html_node"
97ce30cc 1181 #:manual-name "guix"
db1d4453
LC
1182 #:base-url (if (string=? %manual "guix")
1183 (const "")
1184 (cut string-append "/manual/" <>
1185 "/html_node"))
1186 #:languages %languages))
1187
97ce30cc
LC
1188 (define mono-node-indexes
1189 (merge-index-alists guix-mono-node-indexes guile-mono-node-indexes))
1190
1191 (define split-node-indexes
1192 (merge-index-alists guix-split-node-indexes guile-split-node-indexes))
1193
ccadafdc
LC
1194 (format (current-error-port)
1195 "building manual from work tree around commit ~a, ~a~%"
1196 commit
1197 (let* ((time (make-time time-utc 0 date))
1198 (date (time-utc->date time)))
1199 (date->string date "~e ~B ~Y")))
db1d4453
LC
1200
1201 (pdf+html-manual source
97ce30cc
LC
1202 ;; Always use the identifier indexes of GUIX-MANUAL and
1203 ;; GUILE-MANUAL. Both "guix" and "guix-cookbook" can
1204 ;; contain links to definitions that appear in either of
1205 ;; these two manuals.
db1d4453
LC
1206 #:mono-node-indexes mono-node-indexes
1207 #:split-node-indexes split-node-indexes
1208 #:version version
ccadafdc 1209 #:date date))