;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2017 Ricardo Wurmus ;;; Copyright © 2021 Maxim Cournoyer ;;; Copyright © 2021 Thiago Jung Bauermann ;;; Copyright © 2023 Nicolas Goaziou ;;; ;;; 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 . (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