guix/guix/build/texlive-build-system.scm

306 lines
13 KiB
Scheme

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017 Ricardo Wurmus <rekado@elephly.net>
;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;; Copyright © 2021 Thiago Jung Bauermann <bauermann@kolabnow.com>
;;; Copyright © 2023 Nicolas Goaziou <mail@nicolasgoaziou.fr>
;;;
;;; 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/>.
(define-module (guix build texlive-build-system)
#:use-module ((guix build gnu-build-system) #:prefix gnu:)
#:use-module (guix build utils)
#:use-module (guix build union)
#:use-module (ice-9 format)
#:use-module (ice-9 ftw)
#:use-module (ice-9 match)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-2)
#:use-module (srfi srfi-26)
#:export (%standard-phases
texlive-build))
;; Commentary:
;;
;; Builder-side code of the standard build procedure for TeX Live packages.
;;
;; Code:
(define (runfiles-root-directories)
"Return list of root directories containing runfiles."
(scandir "."
(negate
(cut member <> '("." ".." "build" "doc" "source")))))
(define (texlive-input? input)
"Return #t if INPUT is a texlive input, #f otherwise."
(match input
(((or "source" (? (cut string-prefix? "texlive-" <>))) . _) #t)
(_ #f)))
(define (install-as-runfiles dir regexp)
"Install files under DIR matching REGEXP on top of existing runfiles in the
current tree. Sub-directories below DIR are preserved when looking for the
runfile to replace. If a file has no matching runfile, it is ignored."
(let ((runfiles (append-map (cut find-files <>)
(runfiles-root-directories))))
(for-each (lambda (file)
(match (filter
(cut string-suffix?
(string-drop file (string-length dir))
<>)
runfiles)
;; Current file is not a runfile. Ignore it.
(() #f)
;; One candidate only. Replace it with the one from DIR.
((destination)
(let ((target (dirname destination)))
(install-file file target)
(format #t "re-generated file ~s in ~s~%"
(basename file)
target)))
;; Multiple candidates! Not much can be done. Hopefully,
;; this should never happen.
(_
(format (current-error-port)
"warning: ambiguous location for file ~s; ignoring it~%"
(basename file)))))
(find-files dir regexp))))
(define* (patch-shell-scripts #:rest _)
"Expand filenames for usual tools in shell scripts."
(when (file-exists? "scripts")
(let* ((commands '("awk" "basename" "cat" "grep" "mkdir" "rm" "sed" "sort"
"uname"))
(command-regexp (format #f
"\\b(~a)\\b"
(string-join commands "|"))))
(substitute* (find-files "scripts" "\\.sh$")
((command-regexp _ command)
(which command))))))
(define* (delete-drv-files #:rest _)
"Delete pre-generated \".drv\" files in order to prevent build failures."
(when (file-exists? "source")
(for-each delete-file (find-files "source" "\\.drv$"))))
(define* (generate-font-metrics #:key native-inputs inputs #:allow-other-keys)
;; Decide what Metafont files to build by comparing them to the expected
;; font metrics base names. Keep only files for which the two base names
;; do match.
(define (font-metrics root)
(and (file-exists? root)
(map (cut basename <> ".tfm") (find-files root "\\.tfm$"))))
(define (font-files directory metrics)
(if (file-exists? directory)
(delete-duplicates
(filter (lambda (f)
(or (not metrics)
(member (basename f ".mf") metrics)))
(find-files directory "\\.mf$")))
'()))
;; Metafont files could be scattered across multiple directories. Treat
;; each sub-directory as a separate font source.
(define (font-sources root metrics)
(delete-duplicates (map dirname (font-files root metrics))))
(and-let* ((local-metrics (font-metrics "fonts/tfm"))
(local-sources (font-sources "fonts/source" local-metrics))
((not (null? local-sources))) ;nothing to generate: bail out
(root (getcwd))
(metafont
(cond ((assoc-ref (or native-inputs inputs) "texlive-metafont") =>
(cut string-append <> "/share/texmf-dist"))
(else
(error "Missing 'texlive-metafont' native input"))))
;; Collect all font source files from texlive (native-)inputs so
;; "mf" can know where to look for them.
(font-inputs
(delete-duplicates
(append-map (match-lambda
((? (negate texlive-input?)) '())
(("texlive-bin" . _) '())
(("texlive-metafont" . _)
(list (string-append metafont "/metafont/base")))
((_ . input)
(font-sources input #f)))
(or native-inputs inputs)))))
;; Tell mf where to find "mf.base".
(setenv "MFBASES" (string-append metafont "/web2c/"))
(mkdir-p "build")
(for-each
(lambda (source)
;; Tell "mf" where are the font source files. In case current package
;; provides multiple sources, treat them separately.
(setenv "MFINPUTS"
(string-join (cons (string-append root "/" source)
font-inputs)
":"))
;; Build font metrics (tfm).
(with-directory-excursion source
(for-each (lambda (font)
(format #t "building font ~a~%" font)
(invoke "mf" "-progname=mf"
(string-append "-output-directory="
root "/build")
(string-append "\\"
"mode:=ljfour; "
"mag:=1; "
"batchmode; "
"input "
(basename font ".mf"))))
(font-files "." local-metrics)))
;; Refresh font metrics at the appropriate location.
(install-as-runfiles "build" "\\.tfm$"))
local-sources)))
(define* (create-formats #:key create-formats inputs #:allow-other-keys)
(define (collect-locations inputs pred)
(delete-duplicates
(append-map (match-lambda
((? (negate texlive-input?)) '())
((_ . dir)
(if pred
(map dirname (find-files dir pred))
(list dir))))
inputs)))
(when create-formats
(setenv "TFMFONTS"
(string-join (collect-locations inputs "\\.tfm$") ":"))
(setenv "TEXINPUTS"
(string-join (collect-locations inputs #f) "//:" 'suffix))
(setenv "LUAINPUTS"
(string-join (collect-locations inputs "\\.lua$") ":"))
(mkdir-p "web2c")
(for-each (cut invoke "fmtutil-sys" "--byfmt" <> "--fmtdir=web2c")
create-formats)
;; Remove cruft.
(for-each delete-file (find-files "web2c" "\\.log$"))))
(define (compile-with-latex engine format output file)
(invoke engine
"-interaction=nonstopmode"
(string-append "-output-directory=" output)
(if format (string-append "&" format) "-ini")
file))
(define* (build #:key inputs build-targets tex-engine tex-format
#:allow-other-keys)
(let ((targets
(cond
(build-targets
;; Collect the relative file names of all the specified targets.
(append-map (lambda (target)
(find-files "source"
(lambda (f _)
(string-suffix? (string-append "/" target)
f))))
build-targets))
((directory-exists? "source")
;; Prioritize ".ins" files over ".dtx" files. There's no
;; scientific reasoning here; it just seems to work better.
(match (find-files "source" "\\.ins$")
(() (find-files "source" "\\.dtx$"))
(files files)))
(else '()))))
(unless (null? targets)
(let ((output (string-append (getcwd) "/build")))
(mkdir-p output)
(for-each (lambda (target)
(with-directory-excursion (dirname target)
(compile-with-latex tex-engine
tex-format
output
(basename target))))
targets))
;; Now move generated files from the "build" directory into the rest of
;; the source tree, effectively replacing downloaded files.
;;
;; Documentation may have been generated, but replace only runfiles,
;; i.e., files that belong neither to "doc" nor "source" trees.
;;
;; In TeX Live, all packages are fully pre-generated. As a consequence,
;; a generated file from the "build" top directory absent from the rest of
;; the tree is deemed unnecessary and can safely be ignored.
(install-as-runfiles "build" "."))))
(define* (install #:key outputs #:allow-other-keys)
(let ((out (assoc-ref outputs "out"))
(doc (assoc-ref outputs "doc")))
;; Take care of documentation.
(when (directory-exists? "doc")
(unless doc
(format (current-error-port)
"warning: missing 'doc' output for package documentation~%"))
(let ((doc-dir (string-append (or doc out) "/share/texmf-dist/doc")))
(mkdir-p doc-dir)
(copy-recursively "doc" doc-dir)))
;; Install runfiles. The package may not contain any, though. Create
;; #$output anyway to handle this situation gracefully.
(mkdir-p out)
(let ((texmf (string-append out "/share/texmf-dist")))
(for-each (lambda (root)
(let ((destination (string-append texmf "/" root)))
(mkdir-p destination)
(copy-recursively root destination)))
(runfiles-root-directories)))))
(define* (link-scripts #:key link-scripts outputs #:allow-other-keys)
(when (pair? link-scripts)
(unless (file-exists? "scripts")
(error "missing \"scripts\" directory: no script to link"))
(let ((bin (string-append (assoc-ref outputs "out") "/bin"))
(filenames
(filter (lambda (f) (any (cut string-suffix? <> f) link-scripts))
(find-files "scripts"))))
;; Sanity check: make sure no provided script is ignored.
(let ((unknown (lset-difference string=?
(map basename link-scripts)
(map basename filenames))))
(when (pair? unknown)
(error (format #f "cannot find script(s): ~a~%"
(string-join unknown)))))
;; All lights are green. Create "bin/" and the symlinks.
(mkdir-p bin)
(for-each
(lambda (script)
;; Remove extension, if any.
(let ((name (match (string-split (basename script) #\.)
((name) name)
(tokens (string-join (drop-right tokens 1)))))
(origin (string-append "../share/texmf-dist/" script)))
(format #t "linking bin/~s to ~s~%" name origin)
(symlink origin (string-append bin "/" name))))
filenames))))
(define %standard-phases
(modify-phases gnu:%standard-phases
(delete 'bootstrap)
(delete 'configure)
(add-after 'unpack 'patch-shell-scripts patch-shell-scripts)
(add-before 'build 'delete-drv-files delete-drv-files)
(add-after 'delete-drv-files 'generate-font-metrics generate-font-metrics)
(replace 'build build)
(add-after 'build 'create-formats create-formats)
(delete 'check)
(replace 'install install)
(add-after 'install 'link-scripts link-scripts)))
(define* (texlive-build #:key inputs (phases %standard-phases)
#:allow-other-keys #:rest args)
"Build the given TeX Live package, applying all of PHASES in order."
(apply gnu:gnu-build #:inputs inputs #:phases phases args))
;;; texlive-build-system.scm ends here