summary refs log tree commit diff
path: root/pkgs/tools/typesetting/tex/texlive/build-tex-env.nix
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/tools/typesetting/tex/texlive/build-tex-env.nix')
-rw-r--r--pkgs/tools/typesetting/tex/texlive/build-tex-env.nix435
1 files changed, 435 insertions, 0 deletions
diff --git a/pkgs/tools/typesetting/tex/texlive/build-tex-env.nix b/pkgs/tools/typesetting/tex/texlive/build-tex-env.nix
new file mode 100644
index 00000000000..133723cc391
--- /dev/null
+++ b/pkgs/tools/typesetting/tex/texlive/build-tex-env.nix
@@ -0,0 +1,435 @@
+{
+  # texlive package set
+  tl
+, bin
+
+, lib
+, buildEnv
+, libfaketime
+, makeFontsConf
+, makeWrapper
+, runCommand
+, writeShellScript
+, writeText
+, toTLPkgSets
+, bash
+, perl
+
+  # common runtime dependencies
+, coreutils
+, gawk
+, gnugrep
+, gnused
+, ghostscript
+}:
+
+lib.fix (self: {
+  withDocs ? false
+, withSources ? false
+, requiredTeXPackages ? ps: [ ps.scheme-infraonly ]
+
+### texlive.combine backward compatibility
+, __extraName ? "combined"
+, __extraVersion ? ""
+# emulate the old texlive.combine (e.g. add man pages to main output)
+, __combine ? false
+# adjust behavior further if called from the texlive.combine wrapper
+, __fromCombineWrapper ? false
+}@args:
+
+let
+  ### texlive.combine backward compatibility
+  # if necessary, convert old style { pkgs = [ ... ]; } packages to attribute sets
+  isOldPkgList = p: ! p.outputSpecified or false && p ? pkgs && builtins.all (p: p ? tlType) p.pkgs;
+  ensurePkgSets = ps: if ! __fromCombineWrapper && builtins.any isOldPkgList ps
+    then let oldPkgLists = builtins.partition isOldPkgList ps;
+      in oldPkgLists.wrong ++ lib.concatMap toTLPkgSets oldPkgLists.right
+    else ps;
+
+  pkgList = rec {
+    # resolve dependencies of the packages that affect the runtime
+    all =
+      let
+        # order of packages is irrelevant
+        packages = builtins.sort (a: b: a.pname < b.pname) (ensurePkgSets (requiredTeXPackages tl));
+        runtime = builtins.partition
+          (p: p.outputSpecified or false -> builtins.elem (p.tlOutputName or p.outputName) [ "out" "tex" "tlpkg" ])
+          packages;
+        keySet = p: {
+          key = ((p.name or "${p.pname}-${p.version}") + "-" + p.tlOutputName or p.outputName or "");
+          inherit p;
+          tlDeps = p.tlDeps or (p.requiredTeXPackages or (_: [ ]) [ ]);
+        };
+      in
+      # texlive.combine: the wrapper already resolves all dependencies
+      if __fromCombineWrapper then requiredTeXPackages null else
+        builtins.catAttrs "p" (builtins.genericClosure {
+          startSet = map keySet runtime.right;
+          operator = p: map keySet p.tlDeps;
+        }) ++ runtime.wrong;
+
+    # group the specified outputs
+    specified = builtins.partition (p: p.outputSpecified or false) all;
+    specifiedOutputs = lib.groupBy (p: p.tlOutputName or p.outputName) specified.right;
+    otherOutputNames = builtins.catAttrs "key" (builtins.genericClosure {
+      startSet = map (key: { inherit key; }) (lib.concatLists (builtins.catAttrs "outputs" specified.wrong));
+      operator = _: [ ];
+    });
+    otherOutputs = lib.genAttrs otherOutputNames (n: builtins.catAttrs n specified.wrong);
+    outputsToInstall = builtins.catAttrs "key" (builtins.genericClosure {
+      startSet = map (key: { inherit key; })
+        ([ "out" ] ++ lib.optional (splitOutputs ? man) "man"
+          ++ lib.concatLists (builtins.catAttrs "outputsToInstall" (builtins.catAttrs "meta" specified.wrong)));
+      operator = _: [ ];
+    });
+
+    # split binary and tlpkg from tex, texdoc, texsource
+    bin = if __fromCombineWrapper
+      then builtins.filter (p: p.tlType == "bin") all # texlive.combine: legacy filter
+      else otherOutputs.out or [ ] ++ specifiedOutputs.out or [ ];
+    tlpkg = if __fromCombineWrapper
+      then builtins.filter (p: p.tlType == "tlpkg") all # texlive.combine: legacy filter
+      else otherOutputs.tlpkg or [ ] ++ specifiedOutputs.tlpkg or [ ];
+
+    nonbin = if __fromCombineWrapper then builtins.filter (p: p.tlType != "bin" && p.tlType != "tlpkg") all # texlive.combine: legacy filter
+      else (if __combine then # texlive.combine: emulate old input ordering to avoid rebuilds
+        lib.concatMap (p: lib.optional (p ? tex) p.tex
+          ++ lib.optional ((withDocs || p ? man) && p ? texdoc) p.texdoc
+          ++ lib.optional (withSources && p ? texsource) p.texsource) specified.wrong
+        else otherOutputs.tex or [ ]
+          ++ lib.optionals withDocs (otherOutputs.texdoc or [ ])
+          ++ lib.optionals withSources (otherOutputs.texsource or [ ]))
+        ++ specifiedOutputs.tex or [ ] ++ specifiedOutputs.texdoc or [ ] ++ specifiedOutputs.texsource or [ ];
+
+    # outputs that do not become part of the environment
+    nonEnvOutputs = lib.subtractLists [ "out" "tex" "texdoc" "texsource" "tlpkg" ] otherOutputNames;
+  };
+
+  # list generated by inspecting `grep -IR '\([^a-zA-Z]\|^\)gs\( \|$\|"\)' "$TEXMFDIST"/scripts`
+  # and `grep -IR rungs "$TEXMFDIST"`
+  # and ignoring luatex, perl, and shell scripts (those must be patched using postFixup)
+  needsGhostscript = lib.any (p: lib.elem p.pname [ "context" "dvipdfmx" "latex-papersize" "lyluatex" ]) pkgList.bin;
+
+  name = if __combine then "texlive-${__extraName}-${bin.texliveYear}${__extraVersion}" # texlive.combine: old name name
+    else "texlive-${bin.texliveYear}-env";
+
+  texmfdist = (buildEnv {
+    name = "${name}-texmfdist";
+
+    # remove fake derivations (without 'outPath') to avoid undesired build dependencies
+    paths = builtins.catAttrs "outPath" pkgList.nonbin;
+
+    # mktexlsr
+    nativeBuildInputs = [ tl."texlive.infra" ];
+
+    postBuild = # generate ls-R database
+    ''
+      mktexlsr "$out"
+    '';
+  }).overrideAttrs (_: { allowSubstitutes = true; });
+
+  tlpkg = (buildEnv {
+    name = "${name}-tlpkg";
+
+    # remove fake derivations (without 'outPath') to avoid undesired build dependencies
+    paths = builtins.catAttrs "outPath" pkgList.tlpkg;
+  }).overrideAttrs (_: { allowSubstitutes = true; });
+
+  # the 'non-relocated' packages must live in $TEXMFROOT/texmf-dist
+  # and sometimes look into $TEXMFROOT/tlpkg (notably fmtutil, updmap look for perl modules in both)
+  texmfroot = runCommand "${name}-texmfroot" {
+    inherit texmfdist tlpkg;
+  } ''
+    mkdir -p "$out"
+    ln -s "$texmfdist" "$out"/texmf-dist
+    ln -s "$tlpkg" "$out"/tlpkg
+  '';
+
+  # texlive.combine: expose info and man pages in usual /share/{info,man} location
+  doc = buildEnv {
+    name = "${name}-doc";
+
+    paths = [ (texmfdist.outPath + "/doc") ];
+    extraPrefix = "/share";
+
+    pathsToLink = [
+      "/info"
+      "/man"
+    ];
+  };
+
+  meta = {
+    description = "TeX Live environment"
+      + lib.optionalString withDocs " with documentation"
+      + lib.optionalString (withDocs && withSources) " and"
+      + lib.optionalString withSources " with sources";
+    platforms = lib.platforms.all;
+    longDescription = "Contains the following packages and their transitive dependencies:\n - "
+      + lib.concatMapStringsSep "\n - "
+          (p: p.pname + (lib.optionalString (p.outputSpecified or false) " (${p.tlOutputName or p.outputName})"))
+          (requiredTeXPackages tl);
+  };
+
+  # emulate split output derivation
+  splitOutputs = {
+    out = out // { outputSpecified = true; };
+    texmfdist = texmfdist // { outputSpecified = true; };
+    texmfroot = texmfroot // { outputSpecified = true; };
+  } // (lib.genAttrs pkgList.nonEnvOutputs (outName: (buildEnv {
+    inherit name;
+    paths = builtins.catAttrs "outPath"
+      (pkgList.otherOutputs.${outName} or [ ] ++ pkgList.specifiedOutputs.${outName} or [ ]);
+    # force the output to be ${outName} or nix-env will not work
+    nativeBuildInputs = [ (writeShellScript "force-output.sh" ''
+      export out="''${${outName}-}"
+    '') ];
+    inherit meta passthru;
+  }).overrideAttrs { outputs = [ outName ]; } // { outputSpecified = true; }));
+
+  passthru = lib.optionalAttrs (! __combine) (splitOutputs // {
+    all = builtins.attrValues splitOutputs;
+    outputs = [ "out" ] ++ pkgList.nonEnvOutputs;
+  }) // {
+    # This is set primarily to help find-tarballs.nix to do its job
+    requiredTeXPackages = builtins.filter lib.isDerivation (pkgList.bin ++ pkgList.nonbin
+      ++ lib.optionals (! __fromCombineWrapper)
+        (lib.concatMap (n: (pkgList.otherOutputs.${n} or [ ] ++ pkgList.specifiedOutputs.${n} or [ ]))) pkgList.nonEnvOutputs);
+    # useful for inclusion in the `fonts.packages` nixos option or for use in devshells
+    fonts = "${texmfroot}/texmf-dist/fonts";
+    # support variants attrs, (prev: attrs)
+    __overrideTeXConfig = newArgs:
+      let appliedArgs = if builtins.isFunction newArgs then newArgs args else newArgs; in
+        self (args // { __fromCombineWrapper = false; } // appliedArgs);
+    withPackages = reqs: self (args // { requiredTeXPackages = ps: requiredTeXPackages ps ++ reqs ps; __fromCombineWrapper = false; });
+  };
+
+  out = (if (! __combine)
+    # meta.outputsToInstall = [ "out" "man" ] is invalid within buildEnv:
+    # checkMeta will notice that there is no actual "man" output, and fail
+    # so we set outputsToInstall from the outside, where it is safe
+    then lib.addMetaAttrs { inherit (pkgList) outputsToInstall; }
+    else x: x) # texlive.combine: man pages used to be part of out
+# no indent for git diff purposes
+((buildEnv {
+
+  inherit name;
+
+  ignoreCollisions = false;
+
+  # remove fake derivations (without 'outPath') to avoid undesired build dependencies
+  paths = builtins.catAttrs "outPath" pkgList.bin
+    ++ lib.optional __combine doc;
+  pathsToLink = [
+    "/"
+    "/share/texmf-var/scripts"
+    "/share/texmf-var/tex/generic/config"
+    "/share/texmf-var/web2c"
+    "/share/texmf-config"
+    "/bin" # ensure these are writeable directories
+  ];
+
+  nativeBuildInputs = [
+    makeWrapper
+    libfaketime
+    tl."texlive.infra" # mktexlsr
+    tl.texlive-scripts # fmtutil, updmap
+    tl.texlive-scripts-extra # texlinks
+    perl
+  ];
+
+  inherit meta passthru;
+
+  postBuild =
+    # environment variables (note: only export the ones that are used in the wrappers)
+  ''
+    TEXMFROOT="${texmfroot}"
+    TEXMFDIST="${texmfdist}"
+    export PATH="$out/bin:$PATH"
+    TEXMFSYSCONFIG="$out/share/texmf-config"
+    TEXMFSYSVAR="$out/share/texmf-var"
+    export TEXMFCNF="$TEXMFSYSVAR/web2c"
+  '' +
+    # wrap executables with required env vars as early as possible
+    # 1. we use the wrapped binaries in the scripts below, to catch bugs
+    # 2. we do not want to wrap links generated by texlinks
+  ''
+    enable -f '${bash}/lib/bash/realpath' realpath
+    declare -i wrapCount=0
+    for link in "$out"/bin/*; do
+      target="$(realpath "$link")"
+      if [[ "''${target##*/}" != "''${link##*/}" ]] ; then
+        # detected alias with different basename, use immediate target of $link to preserve $0
+        # relevant for mktexfmt, repstopdf, ...
+        target="$(readlink "$link")"
+      fi
+
+      rm "$link"
+      makeWrapper "$target" "$link" \
+        --inherit-argv0 \
+        --prefix PATH : "${
+          # very common dependencies that are not detected by tests.texlive.binaries
+          lib.makeBinPath ([ coreutils gawk gnugrep gnused ] ++ lib.optional needsGhostscript ghostscript)}:$out/bin" \
+        --set-default TEXMFCNF "$TEXMFCNF" \
+        --set-default FONTCONFIG_FILE "${
+          # necessary for XeTeX to find the fonts distributed with texlive
+          makeFontsConf { fontDirectories = [ "${texmfroot}/texmf-dist/fonts" ]; }
+        }"
+      wrapCount=$((wrapCount + 1))
+    done
+    echo "wrapped $wrapCount binaries and scripts"
+  '' +
+    # patch texmf-dist  -> $TEXMFDIST
+    # patch texmf-local -> $out/share/texmf-local
+    # patch texmf.cnf   -> $TEXMFSYSVAR/web2c/texmf.cnf
+    # TODO: perhaps do lua actions?
+    # tried inspiration from install-tl, sub do_texmf_cnf
+  ''
+    mkdir -p "$TEXMFCNF"
+    if [ -e "$TEXMFDIST/web2c/texmfcnf.lua" ]; then
+      sed \
+        -e "s,\(TEXMFOS[ ]*=[ ]*\)[^\,]*,\1\"$TEXMFROOT\",g" \
+        -e "s,\(TEXMFDIST[ ]*=[ ]*\)[^\,]*,\1\"$TEXMFDIST\",g" \
+        -e "s,\(TEXMFSYSVAR[ ]*=[ ]*\)[^\,]*,\1\"$TEXMFSYSVAR\",g" \
+        -e "s,\(TEXMFSYSCONFIG[ ]*=[ ]*\)[^\,]*,\1\"$TEXMFSYSCONFIG\",g" \
+        -e "s,\(TEXMFLOCAL[ ]*=[ ]*\)[^\,]*,\1\"$out/share/texmf-local\",g" \
+        -e "s,\$SELFAUTOLOC,$out,g" \
+        -e "s,selfautodir:/,$out/share/,g" \
+        -e "s,selfautodir:,$out/share/,g" \
+        -e "s,selfautoparent:/,$out/share/,g" \
+        -e "s,selfautoparent:,$out/share/,g" \
+        "$TEXMFDIST/web2c/texmfcnf.lua" > "$TEXMFCNF/texmfcnf.lua"
+    fi
+
+    sed \
+      -e "s,\(TEXMFROOT[ ]*=[ ]*\)[^\,]*,\1$TEXMFROOT,g" \
+      -e "s,\(TEXMFDIST[ ]*=[ ]*\)[^\,]*,\1$TEXMFDIST,g" \
+      -e "s,\(TEXMFSYSVAR[ ]*=[ ]*\)[^\,]*,\1$TEXMFSYSVAR,g" \
+      -e "s,\(TEXMFSYSCONFIG[ ]*=[ ]*\)[^\,]*,\1$TEXMFSYSCONFIG,g" \
+      -e "s,\$SELFAUTOLOC,$out,g" \
+      -e "s,\$SELFAUTODIR,$out/share,g" \
+      -e "s,\$SELFAUTOPARENT,$out/share,g" \
+      -e "s,\$SELFAUTOGRANDPARENT,$out/share,g" \
+      -e "/^mpost,/d" `# CVE-2016-10243` \
+      "$TEXMFDIST/web2c/texmf.cnf" > "$TEXMFCNF/texmf.cnf"
+  '' +
+    # now filter hyphenation patterns and formats
+  (let
+    hyphens = lib.filter (p: p.hasHyphens or false && p.tlOutputName or p.outputName == "tex") pkgList.nonbin;
+    hyphenPNames = map (p: p.pname) hyphens;
+    formats = lib.filter (p: p ? formats && p.tlOutputName or p.outputName == "tex") pkgList.nonbin;
+    formatPNames = map (p: p.pname) formats;
+    # sed expression that prints the lines in /start/,/end/ except for /end/
+    section = start: end: "/${start}/,/${end}/{ /${start}/p; /${end}/!p; };\n";
+    script =
+      writeText "hyphens.sed" (
+        # document how the file was generated (for language.dat)
+        "1{ s/^(% Generated by .*)$/\\1, modified by ${if __combine then "texlive.combine" else "Nixpkgs"}/; p; }\n"
+        # pick up the header
+        + "2,/^% from/{ /^% from/!p; };\n"
+        # pick up all sections matching packages that we combine
+        + lib.concatMapStrings (pname: section "^% from ${pname}:$" "^% from|^%%% No changes may be made beyond this point.$") hyphenPNames
+        # pick up the footer (for language.def)
+        + "/^%%% No changes may be made beyond this point.$/,$p;\n"
+      );
+    scriptLua =
+      writeText "hyphens.lua.sed" (
+        "1{ s/^(-- Generated by .*)$/\\1, modified by ${if __combine then "texlive.combine" else "Nixpkgs"}/; p; }\n"
+        + "2,/^-- END of language.us.lua/p;\n"
+        + lib.concatMapStrings (pname: section "^-- from ${pname}:$" "^}$|^-- from") hyphenPNames
+        + "$p;\n"
+      );
+    # formats not being installed must be disabled by prepending #! (see man fmtutil)
+    # sed expression that enables the formats in /start/,/end/
+    enableFormats = pname: "/^# from ${pname}:$/,/^# from/{ s/^#! //; };\n";
+    fmtutilSed =
+      writeText "fmtutil.sed" (
+        # document how file was generated
+        "1{ s/^(# Generated by .*)$/\\1, modified by ${if __combine then "texlive.combine" else "Nixpkgs"}/; }\n"
+        # disable all formats, even those already disabled
+        + "s/^([^#]|#! )/#! \\1/;\n"
+        # enable the formats from the packages being installed
+        + lib.concatMapStrings enableFormats formatPNames
+        # clean up formats that have been disabled twice
+        + "s/^#! #! /#! /;\n"
+      );
+  in ''
+    mkdir -p "$TEXMFSYSVAR/tex/generic/config"
+    for fname in tex/generic/config/language.{dat,def}; do
+      [[ -e "$TEXMFDIST/$fname" ]] && sed -E -n -f '${script}' "$TEXMFDIST/$fname" > "$TEXMFSYSVAR/$fname"
+    done
+    [[ -e "$TEXMFDIST"/tex/generic/config/language.dat.lua ]] && sed -E -n -f '${scriptLua}' \
+      "$TEXMFDIST"/tex/generic/config/language.dat.lua > "$TEXMFSYSVAR"/tex/generic/config/language.dat.lua
+    [[ -e "$TEXMFDIST"/web2c/fmtutil.cnf ]] && sed -E -f '${fmtutilSed}' "$TEXMFDIST"/web2c/fmtutil.cnf > "$TEXMFCNF"/fmtutil.cnf
+
+    # create $TEXMFSYSCONFIG database, make new $TEXMFSYSVAR files visible to kpathsea
+    mktexlsr "$TEXMFSYSCONFIG" "$TEXMFSYSVAR"
+  '') +
+    # generate format links (reads fmtutil.cnf to know which ones) *after* the wrappers have been generated
+  ''
+    texlinks --quiet "$out/bin"
+  '' +
+  # texlive postactions (see TeXLive::TLUtils::_do_postaction_script)
+  (lib.concatMapStrings (pkg: ''
+    postaction='${pkg.postactionScript}'
+    case "$postaction" in
+      *.pl) postInterp=perl ;;
+      *.texlua) postInterp=texlua ;;
+      *) postInterp= ;;
+    esac
+    echo "postaction install script for ${pkg.pname}: ''${postInterp:+$postInterp }$postaction install $TEXMFROOT"
+    $postInterp "$TEXMFROOT/$postaction" install "$TEXMFROOT"
+  '') (lib.filter (pkg: pkg ? postactionScript) pkgList.tlpkg)) +
+    # generate formats
+  ''
+    # many formats still ignore SOURCE_DATE_EPOCH even when FORCE_SOURCE_DATE=1
+    # libfaketime fixes non-determinism related to timestamps ignoring FORCE_SOURCE_DATE
+    # we cannot fix further randomness caused by luatex; for further details, see
+    # https://salsa.debian.org/live-team/live-build/-/blob/master/examples/hooks/reproducible/2006-reproducible-texlive-binaries-fmt-files.hook.chroot#L52
+    # note that calling faketime and fmtutil is fragile (faketime uses LD_PRELOAD, fmtutil calls /bin/sh, causing potential glibc issues on non-NixOS)
+    # so we patch fmtutil to use faketime, rather than calling faketime fmtutil
+    substitute "$TEXMFDIST"/scripts/texlive/fmtutil.pl fmtutil \
+      --replace 'my $cmdline = "$eng -ini ' 'my $cmdline = "faketime -f '"'"'\@1980-01-01 00:00:00 x0.001'"'"' $eng -ini '
+    FORCE_SOURCE_DATE=1 TZ= perl fmtutil --sys --all | grep '^fmtutil' # too verbose
+
+    # Disable unavailable map files
+    echo y | updmap --sys --syncwithtrees --force 2>&1 | grep '^\(updmap\|  /\)'
+    # Regenerate the map files (this is optional)
+    updmap --sys --force 2>&1 | grep '^\(updmap\|  /\)'
+
+    # sort entries to improve reproducibility
+    [[ -f "$TEXMFSYSCONFIG"/web2c/updmap.cfg ]] && sort -o "$TEXMFSYSCONFIG"/web2c/updmap.cfg "$TEXMFSYSCONFIG"/web2c/updmap.cfg
+
+    mktexlsr "$TEXMFSYSCONFIG" "$TEXMFSYSVAR" # to make sure (of what?)
+  '' +
+    # remove *-sys scripts since /nix/store is readonly
+  ''
+    rm "$out"/bin/*-sys
+  '' +
+  # TODO: a context trigger https://www.preining.info/blog/2015/06/debian-tex-live-2015-the-new-layout/
+    # http://wiki.contextgarden.net/ConTeXt_Standalone#Unix-like_platforms_.28Linux.2FMacOS_X.2FFreeBSD.2FSolaris.29
+
+  # MkIV uses its own lookup mechanism and we need to initialize
+  # caches for it.
+  # We use faketime to fix the embedded timestamps and patch the uuids
+  # with some random but constant values.
+  ''
+    if [[ -e "$out/bin/mtxrun" ]]; then
+      substitute "$TEXMFDIST"/scripts/context/lua/mtxrun.lua mtxrun.lua \
+        --replace 'cache_uuid=osuuid()' 'cache_uuid="e2402e51-133d-4c73-a278-006ea4ed734f"' \
+        --replace 'uuid=osuuid(),' 'uuid="242be807-d17e-4792-8e39-aa93326fc871",'
+      FORCE_SOURCE_DATE=1 TZ= faketime -f '@1980-01-01 00:00:00 x0.001' luatex --luaonly mtxrun.lua --generate
+    fi
+  '' +
+  # Get rid of all log files. They are not needed, but take up space
+  # and render the build unreproducible by their embedded timestamps
+  # and other non-deterministic diagnostics.
+  ''
+    find "$TEXMFSYSVAR"/web2c -name '*.log' -delete
+  '' +
+  # link TEXMFDIST in $out/share for backward compatibility
+  ''
+    ln -s "$TEXMFDIST" "$out"/share/texmf
+  ''
+  ;
+}).overrideAttrs (_: { allowSubstitutes = true; }));
+in out)