{ lib, stdenv, buildEnv, runCommand, fetchurl, file, texlive, writeShellScript, writeText, texliveInfraOnly, texliveConTeXt, texliveSmall, texliveMedium, texliveFull }: rec { mkTeXTest = lib.makeOverridable ( { name , format , text , texLive ? texliveSmall , options ? "-interaction=errorstopmode" , preTest ? "" , postTest ? "" , ... }@attrs: runCommand "texlive-test-tex-${name}" ({ nativeBuildInputs = [ texLive ] ++ attrs.nativeBuildInputs or [ ]; text = builtins.toFile "${name}.tex" text; } // builtins.removeAttrs attrs [ "nativeBuildInputs" "text" "texLive" ]) '' export HOME="$(mktemp -d)" mkdir "$out" cd "$out" cp "$text" "$name.tex" ${preTest} $format $options "$name.tex" ${postTest} '' ); tlpdbNix = runCommand "texlive-test-tlpdb-nix" { nixpkgsTlpdbNix = ../../tools/typesetting/tex/texlive/tlpdb.nix; tlpdbNix = texlive.tlpdb.nix; } '' mkdir -p "$out" diff -u "''${nixpkgsTlpdbNix}" "''${tlpdbNix}" | tee "$out/tlpdb.nix.patch" ''; # test two completely different font discovery mechanisms, both of which were once broken: # - lualatex uses its own luaotfload script (#220228) # - xelatex uses fontconfig (#228196) opentype-fonts = lib.recurseIntoAttrs rec { lualatex = mkTeXTest { name = "opentype-fonts-lualatex"; format = "lualatex"; texLive = texliveSmall.withPackages (ps: [ ps.libertinus-fonts ]); text = '' \documentclass{article} \usepackage{fontspec} \setmainfont{Libertinus Serif} \begin{document} \LaTeX{} is great \end{document} ''; }; xelatex = lualatex.override { name = "opentype-fonts-xelatex"; format = "xelatex"; }; }; chktex = runCommand "texlive-test-chktex" { nativeBuildInputs = [ (texlive.withPackages (ps: [ ps.chktex ])) ]; input = builtins.toFile "chktex-sample.tex" '' \documentclass{article} \begin{document} \LaTeX is great \end{document} ''; } '' # chktex is supposed to return 2 when it (successfully) finds warnings, but no errors, # see http://git.savannah.nongnu.org/cgit/chktex.git/commit/?id=ec0fb9b58f02a62ff0bfec98b997208e9d7a5998 (set +e; chktex -v -nall -w1 "$input" 2>&1; [ $? = 2 ] || exit 1; set -e) | tee "$out" # also check that the output does indeed contain "One warning printed" grep "One warning printed" "$out" ''; context = mkTeXTest { name = "texlive-test-context"; format = "context"; texLive = texliveConTeXt; # check that the PDF has been created: we have hit cases of context # failing with exit status 0 due to a misconfigured texlive postTest = '' if [[ ! -f "$name".pdf ]] ; then echo "ConTeXt test failed: file '$name.pdf' not found" exit 1 fi ''; text = '' \starttext \startsection[title={ConTeXt test document}] This is an {\em incredibly} simple ConTeXt document. \stopsection \stoptext ''; }; dvipng = lib.recurseIntoAttrs { # https://github.com/NixOS/nixpkgs/issues/75605 basic = runCommand "texlive-test-dvipng-basic" { nativeBuildInputs = [ file texliveMedium ]; input = fetchurl { name = "test_dvipng.tex"; url = "http://git.savannah.nongnu.org/cgit/dvipng.git/plain/test_dvipng.tex?id=b872753590a18605260078f56cbd6f28d39dc035"; sha256 = "1pjpf1jvwj2pv5crzdgcrzvbmn7kfmgxa39pcvskl4pa0c9hl88n"; }; } '' cp "$input" ./document.tex latex document.tex dvipng -T tight -strict -picky document.dvi for f in document*.png; do file "$f" | tee output grep PNG output done mkdir "$out" mv document*.png "$out"/ ''; # test dvipng's limited capability to render postscript specials via GS ghostscript = runCommand "texlive-test-ghostscript" { nativeBuildInputs = [ file (texliveSmall.withPackages (ps: [ ps.dvipng ])) ]; input = builtins.toFile "postscript-sample.tex" '' \documentclass{minimal} \begin{document} Ni \special{ps: newpath 0 0 moveto 7 7 rlineto 0 7 moveto 7 -7 rlineto stroke showpage } \end{document} ''; gs_trap = writeShellScript "gs_trap.sh" '' exit 1 ''; } '' cp "$gs_trap" ./gs export PATH=$PWD:$PATH # check that the trap works gs && exit 1 cp "$input" ./document.tex latex document.tex dvipng -T 1in,1in -strict -picky document.dvi for f in document*.png; do file "$f" | tee output grep PNG output done mkdir "$out" mv document*.png "$out"/ ''; }; # https://github.com/NixOS/nixpkgs/issues/75070 dvisvgm = runCommand "texlive-test-dvisvgm" { nativeBuildInputs = [ file texliveMedium ]; input = builtins.toFile "dvisvgm-sample.tex" '' \documentclass{article} \begin{document} mwe \end{document} ''; } '' cp "$input" ./document.tex latex document.tex dvisvgm document.dvi -n -o document_dvi.svg cat document_dvi.svg file document_dvi.svg | grep SVG pdflatex document.tex dvisvgm -P document.pdf -n -o document_pdf.svg cat document_pdf.svg file document_pdf.svg | grep SVG mkdir "$out" mv document*.svg "$out"/ ''; texdoc = runCommand "texlive-test-texdoc" { nativeBuildInputs = [ (texlive.withPackages (ps: [ ps.luatex ps.texdoc ps.texdoc.texdoc ])) ]; } '' texdoc --version texdoc --debug --list texdoc | tee "$out" grep texdoc.pdf "$out" ''; # check that the default language is US English defaultLanguage = lib.recurseIntoAttrs rec { # language.def etex = mkTeXTest { name = "default-language-etex"; format = "etex"; text = '' \catcode`\@=11 \ifnum\language=\lang@USenglish \message{[tests.texlive] Default language is US English.} \else\errmessage{[tests.texlive] Error: default language is NOT US English.}\fi \ifnum\language=0\message{[tests.texlive] Default language has id 0.} \else\errmessage{[tests.texlive] Error: default language does NOT have id 0.}\fi \bye ''; }; # language.dat latex = mkTeXTest { name = "default-language-latex"; format = "latex"; text = '' \makeatletter \ifnum\language=\l@USenglish \GenericWarning{}{[tests.texlive] Default language is US English} \else\GenericError{}{[tests.texlive] Error: default language is NOT US English}{}{}\fi \ifnum\language=0\GenericWarning{}{[tests.texlive] Default language has id 0} \else\GenericError{}{[tests.texlive] Error: default language does NOT have id 0}{}{}\fi \stop ''; }; # language.dat.lua luatex = etex.override { name = "default-language-luatex"; format = "luatex"; }; }; # check that all languages are available, including synonyms allLanguages = let hyphenBase = texlive.pkgs.hyphen-base; texLive = texliveFull; in lib.recurseIntoAttrs { # language.def etex = mkTeXTest { name = "all-languages-etex"; format = "etex"; inherit hyphenBase texLive; text = '' \catcode`\@=11 \input kvsetkeys.sty \def\CheckLang#1{ \ifcsname lang@#1\endcsname\message{[tests.texlive] Found language #1} \else\errmessage{[tests.texlive] Error: missing language #1}\fi } \comma@parse{@texLanguages@}\CheckLang \bye ''; preTest = '' texLanguages="$(sed -n -E 's/^\\addlanguage\s*\{([^}]+)\}.*$/\1/p' < "$hyphenBase"/tex/generic/config/language.def)" texLanguages="''${texLanguages//$'\n'/,}" substituteInPlace "$name.tex" --subst-var texLanguages ''; }; # language.dat latex = mkTeXTest { name = "all-languages-latex"; format = "latex"; inherit hyphenBase texLive; text = '' \makeatletter \@for\Lang:=italian,@texLanguages@\do{ \ifcsname l@\Lang\endcsname \GenericWarning{}{[tests.texlive] Found language \Lang} \else \GenericError{}{[tests.texlive] Error: missing language \Lang}{}{} \fi } \stop ''; preTest = '' texLanguages="$(sed -n -E 's/^([^%= \t]+).*$/\1/p' < "$hyphenBase"/tex/generic/config/language.dat)" texLanguages="''${texLanguages//$'\n'/,}" substituteInPlace "$name.tex" --subst-var texLanguages ''; }; # language.dat.lua luatex = mkTeXTest { name = "all-languages-luatex"; format = "luatex"; inherit hyphenBase texLive; text = '' \directlua{ require('luatex-hyphen.lua') langs = '@texLanguages@,' texio.write('\string\n') for l in langs:gmatch('([^,]+),') do if luatexhyphen.lookupname(l) \string~= nil then texio.write('[tests.texlive] Found language '..l..'.\string\n') else error('[tests.texlive] Error: missing language '..l..'.', 2) end end } \bye ''; preTest = '' texLanguages="$(sed -n -E 's/^.*\[("|'\''')(.*)("|'\''')].*$/\2/p' < "$hyphenBase"/tex/generic/config/language.dat.lua)" texLanguages="''${texLanguages//$'\n'/,}" substituteInPlace "$name.tex" --subst-var texLanguages ''; }; }; # test that language files are generated as expected hyphen-base = runCommand "texlive-test-hyphen-base" { hyphenBase = texlive.pkgs.hyphen-base; schemeFull = texliveFull; schemeInfraOnly = texliveInfraOnly; } '' mkdir -p "$out"/{scheme-infraonly,scheme-full} # create language files with no hyphenation patterns cat "$hyphenBase"/tex/generic/config/language.us >language.dat cat "$hyphenBase"/tex/generic/config/language.us.def >language.def cat "$hyphenBase"/tex/generic/config/language.us.lua >language.dat.lua cat >>language.dat.lua <>language.def <&1 || true) | grep forbidden >/dev/null (repstopdf --gscmd echo /dev/null 2>&1 || true) | grep forbidden >/dev/null mkdir "$out" ''; # verify that the restricted mode gets enabled when # needed (detected by checking if it disallows --gscmd) rpdfcrop = runCommand "texlive-test-rpdfcrop" { nativeBuildInputs = [ (texlive.withPackages (ps: [ ps.pdfcrop ])) ]; } '' ! (pdfcrop --gscmd echo $(command -v pdfcrop) 2>&1 || true) | grep 'restricted mode' >/dev/null (rpdfcrop --gscmd echo $(command -v pdfcrop) 2>&1 || true) | grep 'restricted mode' >/dev/null mkdir "$out" ''; # check that all binaries run successfully, in the following sense: # (1) run --version, -v, --help, -h successfully; or # (2) run --help, -h, or no argument with error code but show help text; or # (3) run successfully on a test.tex or similar file # we ignore the binaries that cannot be tested as above, and are either # compiled binaries or trivial shell wrappers binaries = let # TODO known broken binaries broken = [ # *.inc files in source container rather than run "texaccents" # 'Error initialising QuantumRenderer: no suitable pipeline found' "tlcockpit" ] ++ lib.optional stdenv.hostPlatform.isDarwin "epspdftk"; # wish shebang is a script, not a binary! # (1) binaries requiring -v shortVersion = [ "devnag" "diadia" "pmxchords" "ptex2pdf" "simpdftex" "ttf2afm" ]; # (1) binaries requiring --help or -h help = [ "arlatex" "bundledoc" "cachepic" "checklistings" "dvipos" "extractres" "fig4latex" "fragmaster" "kpsewhere" "latex-git-log" "ltxfileinfo" "mendex" "perltex" "pn2pdf" "psbook" "psnup" "psresize" "purifyeps" "simpdftex" "tex2xindy" "texluac" "texluajitc" "upmendex" "urlbst" "yplan" ]; shortHelp = [ "adhocfilelist" "authorindex" "bbl2bib" "bibdoiadd" "bibmradd" "biburl2doi" "bibzbladd" "ctanupload" "disdvi" "dvibook" "dviconcat" "getmapdl" "latex2man" "listings-ext.sh" "pygmentex" ]; # (2) binaries that return non-zero exit code even if correctly asked for help ignoreExitCode = [ "authorindex" "dvibook" "dviconcat" "dvipos" "extractres" "fig4latex" "fragmaster" "latex2man" "latex-git-log" "listings-ext.sh" "psbook" "psnup" "psresize" "purifyeps" "tex2xindy" "texluac" "texluajitc" ]; # (2) binaries that print help on no argument, returning non-zero exit code noArg = [ "a2ping" "bg5+latex" "bg5+pdflatex" "bg5latex" "bg5pdflatex" "cef5latex" "cef5pdflatex" "ceflatex" "cefpdflatex" "cefslatex" "cefspdflatex" "chkdvifont" "dvi2fax" "dvired" "dviselect" "dvitodvi" "epsffit" "findhyph" "gbklatex" "gbkpdflatex" "komkindex" "kpsepath" "listbib" "listings-ext" "mag" "mathspic" "mf2pt1" "mk4ht" "mkt1font" "mkgrkindex" "musixflx" "pdf2ps" "pdfclose" "pdftosrc" "pdfxup" "pedigree" "pfb2pfa" "pk2bm" "prepmx" "ps2pk" "psselect" "pstops" "rubibtex" "rubikrotation" "sjislatex" "sjispdflatex" "srcredact" "t4ht" "teckit_compile" "tex4ht" "texdiff" "texdirflatten" "texplate" "tie" "ttf2kotexfont" "ttfdump" "vlna" "vpl2ovp" "vpl2vpl" "yplan" ]; # (3) binaries requiring a .tex file contextTest = [ "htcontext" ]; latexTest = [ "de-macro" "e2pall" "htlatex" "htxelatex" "makeindex" "pslatex" "rumakeindex" "tpic2pdftex" "wordcount" "xhlatex" ]; texTest = [ "fontinst" "htmex" "httex" "httexi" "htxetex" ]; # tricky binaries or scripts that are obviously working but are hard to test # (e.g. because they expect user input no matter the arguments) # (printafm comes from ghostscript, not texlive) ignored = [ # compiled binaries "dt2dv" "dv2dt" "dvi2tty" "dvidvi" "dvispc" "otp2ocp" "outocp" "pmxab" # GUI scripts that accept no argument or crash without a graphics server; please test manualy "epspdftk" "texdoctk" "tlshell" "xasy" # requires Cinderella, not open source and not distributed via Nixpkgs "ketcindy" ]; # binaries that need a combined scheme and cannot work standalone needScheme = [ # pfarrei: require working kpse to find lua module "a5toa4" # bibexport: requires kpsewhich "bibexport" # crossrefware: require bibtexperllibs under TEXMFROOT "bbl2bib" "bibdoiadd" "bibmradd" "biburl2doi" "bibzbladd" "checkcites" "ltx2crossrefxml" # epstopdf: requires kpsewhich "epstopdf" "repstopdf" # requires kpsewhich "memoize-extract.pl" "memoize-extract.py" # require other texlive binaries in PATH "allcm" "allec" "chkweb" "fontinst" "ht*" "installfont-tl" "kanji-config-updmap-sys" "kanji-config-updmap-user" "kpse*" "latexfileversion" "mkocp" "mkofm" "mtxrunjit" "pdftex-quiet" "pslatex" "rumakeindex" "texconfig" "texconfig-sys" "texexec" "texlinks" "texmfstart" "typeoutfileinfo" "wordcount" "xdvi" "xhlatex" # misc luatex binaries searching for luatex in PATH "citeproc-lua" "context" "contextjit" "ctanbib" "digestif" "epspdf" "l3build" "luafindfont" "luaotfload-tool" "luatools" "make4ht" "pmxchords" "tex4ebook" "texblend" "texdoc" "texfindpkg" "texlogsieve" "xindex" # requires full TEXMFROOT (e.g. for config) "mktexfmt" "mktexmf" "mktexpk" "mktextfm" "psnup" "psresize" "pstops" "tlmgr" "updmap" "webquiz" # texlive-scripts: requires texlive.infra's TeXLive::TLUtils under TEXMFROOT "fmtutil" "fmtutil-sys" "fmtutil-user" # texlive-scripts: not used in nixpkgs, need updmap in PATH "updmap-sys" "updmap-user" ]; # simple test files contextTestTex = writeText "context-test.tex" '' \starttext A simple test file. \stoptext ''; latexTestTex = writeText "latex-test.tex" '' \documentclass{article} \begin{document} A simple test file. \end{document} ''; texTestTex = writeText "tex-test.tex" '' Hello. \bye ''; # link all binaries in single derivation binPackages = lib.catAttrs "out" (lib.attrValues texlive.pkgs); binaries = buildEnv { name = "texlive-binaries"; paths = binPackages; }; in runCommand "texlive-test-binaries" { inherit binaries contextTestTex latexTestTex texTestTex; texliveScheme = texliveFull; } '' loadables="$(command -v bash)" loadables="''${loadables%/bin/bash}/lib/bash" enable -f "$loadables/realpath" realpath mkdir -p "$out" export HOME="$(mktemp -d)" declare -i binCount=0 ignoredCount=0 brokenCount=0 failedCount=0 cp "$contextTestTex" context-test.tex cp "$latexTestTex" latex-test.tex cp "$texTestTex" tex-test.tex testBin () { path="$(realpath "$bin")" path="''${path##*/}" if [[ -z "$ignoreExitCode" ]] ; then PATH="$path" "$bin" $args >"$out/$base.log" 2>&1 ret=$? if [[ $ret == 0 ]] && grep -i 'command not found' "$out/$base.log" >/dev/null ; then echo "command not found when running '$base''${args:+ $args}'" return 1 fi return $ret else PATH="$path" "$bin" $args >"$out/$base.log" 2>&1 ret=$? if [[ $ret == 0 ]] && grep -i 'command not found' "$out/$base.log" >/dev/null ; then echo "command not found when running '$base''${args:+ $args}'" return 1 fi if ! grep -Ei '(Example:|Options:|Syntax:|Usage:|improper command|SYNOPSIS)' "$out/$base.log" >/dev/null ; then echo "did not find usage info when running '$base''${args:+ $args}'" return $ret fi fi } for bin in "$binaries"/bin/* ; do base="''${bin##*/}" args= ignoreExitCode= binCount=$((binCount + 1)) # ignore non-executable files (such as context.lua) if [[ ! -x "$bin" ]] ; then ignoredCount=$((ignoredCount + 1)) continue fi case "$base" in ${lib.concatStringsSep "|" ignored}) ignoredCount=$((ignoredCount + 1)) continue ;; ${lib.concatStringsSep "|" broken}) brokenCount=$((brokenCount + 1)) continue ;; ${lib.concatStringsSep "|" help}) args=--help ;; ${lib.concatStringsSep "|" shortHelp}) args=-h ;; ${lib.concatStringsSep "|" noArg}) ;; ${lib.concatStringsSep "|" contextTest}) args=context-test.tex ;; ${lib.concatStringsSep "|" latexTest}) args=latex-test.tex ;; ${lib.concatStringsSep "|" texTest}) args=tex-test.tex ;; ${lib.concatStringsSep "|" shortVersion}) args=-v ;; ebong) touch empty args=empty ;; ht) args='latex latex-test.tex' ;; pdf2dsc) args='--help --help --help' ;; typeoutfileinfo) args=/dev/null ;; *) args=--version ;; esac case "$base" in ${lib.concatStringsSep "|" (ignoreExitCode ++ noArg)}) ignoreExitCode=1 ;; esac case "$base" in ${lib.concatStringsSep "|" needScheme}) bin="$texliveScheme/bin/$base" if [[ ! -f "$bin" ]] ; then ignoredCount=$((ignoredCount + 1)) continue fi ;; esac if testBin ; then : ; else # preserve exit code echo "failed '$base''${args:+ $args}' (exit code: $?)" sed 's/^/ > /' < "$out/$base.log" failedCount=$((failedCount + 1)) fi done echo "tested $binCount binaries: $ignoredCount ignored, $brokenCount broken, $failedCount failed" [[ $failedCount = 0 ]] ''; # check that all scripts have a Nix shebang shebangs = let binPackages = lib.catAttrs "out" (lib.attrValues texlive.pkgs); in runCommand "texlive-test-shebangs" { } ('' echo "checking that all texlive scripts shebangs are in '$NIX_STORE'" declare -i scriptCount=0 invalidCount=0 '' + (lib.concatMapStrings (pkg: '' for bin in '${pkg.outPath}'/bin/* ; do grep -I -q . "$bin" || continue # ignore binary files [[ -x "$bin" ]] || continue # ignore non-executable files (such as context.lua) scriptCount=$((scriptCount + 1)) read -r cmdline < "$bin" read -r interp <<< "$cmdline" if [[ "$interp" != "#!$NIX_STORE"/* && "$interp" != "#! $NIX_STORE"/* ]] ; then echo "error: non-nix shebang '$interp' in script '$bin'" invalidCount=$((invalidCount + 1)) fi done '') binPackages) + '' echo "checked $scriptCount scripts, found $invalidCount non-nix shebangs" [[ $invalidCount -gt 0 ]] && exit 1 mkdir -p "$out" '' ); # verify that the precomputed licensing information in default.nix # does indeed match the metadata of the individual packages. # # This is part of the test suite (and not the normal evaluation) to save # time for "normal" evaluations. To be more in line with the other tests, this # also builds a derivation, even though it is essentially an eval-time assertion. licenses = let concatLicenses = builtins.foldl' (acc: el: if builtins.elem el acc then acc else acc ++ [ el ]); # converts a license to its attribute name in lib.licenses licenseToAttrName = license: builtins.head (builtins.attrNames (lib.filterAttrs (n: v: license == v) lib.licenses)); lt = (a: b: a < b); savedLicenses = scheme: scheme.meta.license; savedLicensesAttrNames = scheme: map licenseToAttrName (savedLicenses scheme); correctLicenses = scheme: builtins.foldl' (acc: pkg: concatLicenses acc (lib.toList (pkg.meta.license or []))) [] scheme.passthru.requiredTeXPackages; correctLicensesAttrNames = scheme: lib.sort lt (map licenseToAttrName (correctLicenses scheme)); hasLicenseMismatch = scheme: (lib.isDerivation scheme) && (savedLicensesAttrNames scheme) != (correctLicensesAttrNames scheme); incorrectSchemes = lib.filterAttrs (n: hasLicenseMismatch) (texlive.combined // texlive.schemes); prettyPrint = name: scheme: '' license info for ${name} is incorrect! Note that order is enforced. saved: [ ${lib.concatStringsSep " " (savedLicensesAttrNames scheme)} ] correct: [ ${lib.concatStringsSep " " (correctLicensesAttrNames scheme)} ] ''; errorText = lib.concatStringsSep "\n\n" (lib.mapAttrsToList prettyPrint incorrectSchemes); in runCommand "texlive-test-license" { inherit errorText; } (if (incorrectSchemes == {}) then "echo everything is fine! > $out" else '' echo "$errorText" false ''); # verify that all fixed hashes are present # this is effectively an eval-time assertion, converted into a derivation for # ease of testing fixedHashes = let fods = lib.concatMap (p: lib.optional (p ? tex && lib.isDerivation p.tex) p.tex ++ lib.optional (p ? texdoc) p.texdoc ++ lib.optional (p ? texsource) p.texsource ++ lib.optional (p ? tlpkg) p.tlpkg) (lib.attrValues texlive.pkgs); errorText = lib.concatMapStrings (p: lib.optionalString (! p ? outputHash) "${p.pname}-${p.tlOutputName} does not have a fixed output hash\n") fods; in runCommand "texlive-test-fixed-hashes" { inherit errorText; passAsFile = [ "errorText" ]; } '' if [[ -s "$errorTextPath" ]] ; then cat "$errorTextPath" echo Failed: some TeX Live packages do not have fixed output hashes. Please read UPGRADING.md for how to generate a new fixed-hashes.nix. exit 1 else touch "$out" fi ''; }