summary refs log tree commit diff
path: root/pkgs/tools/typesetting/tex/texlive/default.nix
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/tools/typesetting/tex/texlive/default.nix')
-rw-r--r--pkgs/tools/typesetting/tex/texlive/default.nix431
1 files changed, 400 insertions, 31 deletions
diff --git a/pkgs/tools/typesetting/tex/texlive/default.nix b/pkgs/tools/typesetting/tex/texlive/default.nix
index f5696eba4f9..8dff1469a90 100644
--- a/pkgs/tools/typesetting/tex/texlive/default.nix
+++ b/pkgs/tools/typesetting/tex/texlive/default.nix
@@ -4,8 +4,10 @@
 */
 { stdenv, lib, fetchurl, runCommand, writeText, buildEnv
 , callPackage, ghostscript_headless, harfbuzz
-, makeWrapper, python3, ruby, perl, gnused, gnugrep, coreutils
-, libfaketime, makeFontsConf
+, makeWrapper
+, python3, ruby, perl, tk, jdk, bash, snobol4
+, coreutils, findutils, gawk, getopt, gnugrep, gnumake, gnused, gzip, ncurses, zip
+, libfaketime, asymptote, makeFontsConf
 , useFixedHashes ? true
 , recurseIntoAttrs
 }:
@@ -22,7 +24,7 @@ let
   # function for creating a working environment from a set of TL packages
   combine = import ./combine.nix {
     inherit bin combinePkgs buildEnv lib makeWrapper writeText runCommand
-      stdenv python3 ruby perl gnused gnugrep coreutils libfaketime makeFontsConf;
+      stdenv perl libfaketime makeFontsConf bash tl coreutils gawk gnugrep gnused;
     ghostscript = ghostscript_headless;
   };
 
@@ -32,24 +34,340 @@ let
 
   # the set of TeX Live packages, collections, and schemes; using upstream naming
   tl = let
-    orig = removeAttrs tlpdb [ "00texlive.config" ];
+    # most format -> engine links are generated by texlinks according to fmtutil.cnf at combine time
+    # so we remove them from binfiles, and add back the ones texlinks purposefully ignore (e.g. mptopdf)
+    removeFormatLinks = lib.mapAttrs (_: attrs:
+      if (attrs ? formats && attrs ? binfiles)
+      then let formatLinks = lib.catAttrs "name" (lib.filter (f: f.name != f.engine) attrs.formats);
+               binNotFormats = lib.subtractLists formatLinks attrs.binfiles;
+           in if binNotFormats != [] then attrs // { binfiles = binNotFormats; } else removeAttrs attrs [ "binfiles" ]
+      else attrs);
+
+    orig = removeFormatLinks (removeAttrs tlpdb [ "00texlive.config" ]);
+
+    overridden = lib.recursiveUpdate orig rec {
+      #### overrides of texlive.tlpdb
+
+      #### nonstandard script folders
+      context.scriptsFolder = "context/stubs/unix";
+      cyrillic-bin.scriptsFolder = "texlive-extra";
+      fontinst.scriptsFolder = "texlive-extra";
+      mptopdf.scriptsFolder = "context/perl";
+      pdftex.scriptsFolder = "simpdftex";
+      "texlive.infra".scriptsFolder = "texlive";
+      texlive-scripts.scriptsFolder = "texlive";
+      texlive-scripts-extra.scriptsFolder = "texlive-extra";
+      xetex.scriptsFolder = "texlive-extra";
+
+      #### interpreters not detected by looking at the script extensions
+      ctanbib.extraBuildInputs = [ bin.luatex ];
+      de-macro.extraBuildInputs = [ python3 ];
+      match_parens.extraBuildInputs = [ ruby ];
+      optexcount.extraBuildInputs = [ python3 ];
+      pdfbook2.extraBuildInputs = [ python3 ];
+      texlogsieve.extraBuildInputs = [ bin.luatex ];
+
+      #### perl packages
+      crossrefware.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ LWP URI ])) ];
+      ctan-o-mat.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ LWP LWPProtocolHttps ])) ];
+      ctanify.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ FileCopyRecursive ])) ];
+      ctanupload.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ HTMLFormatter WWWMechanize ])) ];
+      exceltex.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ SpreadsheetParseExcel ])) ];
+      latex-git-log.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ IPCSystemSimple ])) ];
+      latexindent.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ FileHomeDir LogDispatch LogLog4perl UnicodeLineBreak YAMLTiny ])) ];
+      pax.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ FileWhich ])) ];
+      ptex-fontmaps.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ Tk ])) ];
+      purifyeps.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ FileWhich ])) ];
+      svn-multi.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ TimeDate ])) ];
+      texdoctk.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ Tk ])) ];
+      ulqda.extraBuildInputs = [ (perl.withPackages (ps: with ps; [ DigestSHA1 ])) ];
+
+      #### python packages
+      pythontex.extraBuildInputs = [ (python3.withPackages (ps: with ps; [ pygments ])) ];
+
+      #### other runtime PATH dependencies
+      a2ping.extraBuildInputs = [ ghostscript_headless ];
+      bibexport.extraBuildInputs = [ gnugrep ];
+      checklistings.extraBuildInputs = [ coreutils ];
+      cjk-gs-integrate.extraBuildInputs = [ ghostscript_headless ];
+      context.extraBuildInputs = [ coreutils ruby ];
+      cyrillic-bin.extraBuildInputs = [ coreutils gnused ];
+      dtxgen.extraBuildInputs = [ coreutils getopt gnumake zip ];
+      dviljk.extraBuildInputs = [ coreutils ];
+      epspdf.extraBuildInputs = [ ghostscript_headless ];
+      epstopdf.extraBuildInputs = [ ghostscript_headless ];
+      fragmaster.extraBuildInputs = [ ghostscript_headless ];
+      installfont.extraBuildInputs = [ coreutils getopt gnused ];
+      latexfileversion.extraBuildInputs = [ coreutils gnugrep gnused ];
+      listings-ext.extraBuildInputs = [ coreutils getopt ];
+      ltxfileinfo.extraBuildInputs = [ coreutils getopt gnused ];
+      ltximg.extraBuildInputs = [ ghostscript_headless ];
+      luaotfload.extraBuildInputs = [ ncurses ];
+      makeindex.extraBuildInputs = [ coreutils gnused ];
+      pagelayout.extraBuildInputs = [ gnused ncurses ];
+      pdfcrop.extraBuildInputs = [ ghostscript_headless ];
+      pdftex.extraBuildInputs = [ coreutils ghostscript_headless gnused ];
+      pdftex-quiet.extraBuildInputs = [ coreutils ];
+      pdfxup.extraBuildInputs = [ coreutils ghostscript_headless ];
+      pkfix-helper.extraBuildInputs = [ ghostscript_headless ];
+      ps2eps.extraBuildInputs = [ ghostscript_headless ];
+      pst2pdf.extraBuildInputs = [ ghostscript_headless ];
+      tex4ht.extraBuildInputs = [ ruby ];
+      texlive-scripts.extraBuildInputs = [ gnused ];
+      texlive-scripts-extra.extraBuildInputs = [ coreutils findutils ghostscript_headless gnused ];
+      thumbpdf.extraBuildInputs = [ ghostscript_headless ];
+      tpic2pdftex.extraBuildInputs = [ gawk ];
+      wordcount.extraBuildInputs = [ coreutils gnugrep ];
+      xdvi.extraBuildInputs = [ coreutils gnugrep ];
+      xindy.extraBuildInputs = [ gzip ];
+
+      #### adjustments to binaries
+      # TODO patch the scripts from bin.* directly in bin.* instead of here
+
+      # TODO we do not build binaries for the following packages (yet!)
+      biber-ms.binfiles = [];
+      xpdfopen.binfiles = [];
+
+      # mptopdf is a format link, but not generated by texlinks
+      # so we add it back to binfiles to generate it from mkPkgBin
+      mptopdf.binfiles = (orig.mptopdf.binfiles or []) ++ [ "mptopdf" ];
+
+      # mktexlsr distributed by texlive.infra has implicit dependencies (e.g. kpsestat)
+      # the perl one hidden in texlive-scripts is better behaved
+      "texlive.infra".binfiles = lib.remove "mktexlsr" orig."texlive.infra".binfiles;
+
+      # remove man, add mktexlsr
+      texlive-scripts.binfiles = (lib.remove "man" orig.texlive-scripts.binfiles) ++ [ "mktexlsr" ];
+
+      # upmendex is "TODO" in bin.nix
+      uptex.binfiles = lib.remove "upmendex" orig.uptex.binfiles;
+
+      # teckit_compile seems to be missing from bin.core{,-big}
+      # TODO find it!
+      xetex.binfiles = lib.remove "teckit_compile" orig.xetex.binfiles;
+
+      # xindy is broken on some platforms unfortunately
+      xindy.binfiles = if bin ? xindy
+        then lib.subtractLists [ "xindy.mem" "xindy.run" ] orig.xindy.binfiles
+        else [];
+
+      #### additional symlinks
+      cluttex.binlinks = {
+        cllualatex = "cluttex";
+        clxelatex = "cluttex";
+      };
 
-    overridden = lib.recursiveUpdate orig {
-      # overrides of texlive.tlpdb
+      epstopdf.binlinks.repstopdf = "epstopdf";
+      pdfcrop.binlinks.rpdfcrop = "pdfcrop";
 
-      # only *.po for tlmgr
-      texlive-msg-translations.hasTlpkg = false;
+      ptex.binlinks = {
+        pdvitomp = bin.metapost + "/bin/pdvitomp";
+        pmpost = bin.metapost + "/bin/pmpost";
+        r-pmpost = bin.metapost + "/bin/r-pmpost";
+      };
 
-      # it seems to need it to transform fonts
-      xdvi.deps = (orig.xdvi.deps or []) ++  [ "metafont" ];
+      texdef.binlinks = {
+        latexdef = "texdef";
+      };
 
-      # TODO: remove when updating to texlive-2023, metadata has been corrected in the TeX catalogue
-      # tlpdb lists license as "unknown", but the README says lppl13: http://mirrors.ctan.org/language/arabic/arabi-add/README
-      arabi-add.license = [  "lppl13c" ];
+      texlive-scripts.binlinks = {
+        mktexfmt = "fmtutil";
+        texhash = "mktexlsr";
+      };
 
-      # TODO: remove this when updating to texlive-2023, npp-for-context is no longer in texlive
-      # tlpdb lists license as "noinfo", but it's gpl3: https://github.com/luigiScarso/context-npp
-      npp-for-context.license = [  "gpl3Only" ];
+      texlive-scripts-extra.binlinks = {
+        allec = "allcm";
+        kpsepath = "kpsetool";
+        kpsexpand = "kpsetool";
+      };
+
+      # metapost binaries are in bin.metapost instead of bin.core
+      uptex.binlinks = {
+        r-upmpost = bin.metapost + "/bin/r-upmpost";
+        updvitomp = bin.metapost + "/bin/updvitomp";
+        upmpost = bin.metapost + "/bin/upmpost";
+      };
+
+      #### add PATH dependencies without wrappers
+      # TODO deduplicate this code
+      a2ping.postFixup = ''
+        sed -i '6i$ENV{PATH}='"'"'${lib.makeBinPath a2ping.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/a2ping
+      '';
+
+      bibexport.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath bibexport.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/bibexport
+      '';
+
+      checklistings.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath checklistings.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/checklistings
+      '';
+
+      cjk-gs-integrate.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath cjk-gs-integrate.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/cjk-gs-integrate
+      '';
+
+      context.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath [ coreutils ]}''${PATH:+:$PATH}"' "$out"/bin/{contextjit,mtxrunjit}
+        sed -i '2iPATH="${lib.makeBinPath [ ruby ]}''${PATH:+:$PATH}"' "$out"/bin/texexec
+      '';
+
+      cyrillic-bin.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath cyrillic-bin.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/rumakeindex
+      '';
+
+      dtxgen.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath dtxgen.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/dtxgen
+      '';
+
+      dviljk.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath dviljk.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/dvihp
+      '';
+
+      epstopdf.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath epstopdf.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/epstopdf
+      '';
+
+      fragmaster.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath fragmaster.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/fragmaster
+      '';
+
+      installfont.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath installfont.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/installfont-tl
+      '';
+
+      latexfileversion.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath latexfileversion.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/latexfileversion
+      '';
+
+      listings-ext.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath listings-ext.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/listings-ext.sh
+      '';
+
+      ltxfileinfo.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath ltxfileinfo.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/ltxfileinfo
+      '';
+
+      ltximg.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath ltximg.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/ltximg
+      '';
+
+      luaotfload.postFixup = ''
+        sed -i '2ios.setenv("PATH","${lib.makeBinPath luaotfload.extraBuildInputs}" .. (os.getenv("PATH") and ":" .. os.getenv("PATH") or ""))' "$out"/bin/luaotfload-tool
+      '';
+
+      makeindex.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath makeindex.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/mkindex
+      '';
+
+      pagelayout.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath [ gnused ]}''${PATH:+:$PATH}"' "$out"/bin/pagelayoutapi
+        sed -i '2iPATH="${lib.makeBinPath [ ncurses ]}''${PATH:+:$PATH}"' "$out"/bin/textestvis
+      '';
+
+      pdfcrop.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath pdfcrop.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/pdfcrop
+      '';
+
+      pdftex.postFixup = ''
+        sed -i -e '2iPATH="${lib.makeBinPath [ coreutils gnused ]}''${PATH:+:$PATH}"' \
+          -e 's!^distillerpath="/usr/local/bin"$!distillerpath="${lib.makeBinPath [ ghostscript_headless ]}"!' \
+          "$out"/bin/simpdftex
+      '';
+
+      pdftex-quiet.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath pdftex-quiet.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/pdftex-quiet
+      '';
+
+      pdfxup.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath pdfxup.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/pdfxup
+      '';
+
+      pkfix-helper.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath pkfix-helper.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/pkfix-helper
+      '';
+
+      ps2eps.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath ps2eps.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/ps2eps
+      '';
+
+      pst2pdf.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath pst2pdf.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/pst2pdf
+      '';
+
+      tex4ht.postFixup = ''
+        sed -i -e '2iPATH="${lib.makeBinPath tex4ht.extraBuildInputs}''${PATH:+:$PATH}"' -e 's/\\rubyCall//g;' "$out"/bin/htcontext
+      '';
+
+      texlive-scripts.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath texlive-scripts.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/{fmtutil-user,mktexmf,mktexpk,mktextfm,updmap-user}
+      '';
+
+      thumbpdf.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath thumbpdf.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/thumbpdf
+      '';
+
+      tpic2pdftex.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath tpic2pdftex.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/tpic2pdftex
+      '';
+
+      wordcount.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath wordcount.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/wordcount
+      '';
+
+      # TODO patch in bin.xdvi
+      xdvi.postFixup = ''
+        sed -i '2iPATH="${lib.makeBinPath xdvi.extraBuildInputs}''${PATH:+:$PATH}"' "$out"/bin/xdvi
+      '';
+
+      xindy.postFixup = ''
+        sed -i '2i$ENV{PATH}='"'"'${lib.makeBinPath xindy.extraBuildInputs}'"'"' . ($ENV{PATH} ? ":$ENV{PATH}" : '"'''"');' "$out"/bin/{texindy,xindy}
+      '';
+
+      #### other script fixes
+      # misc tab and python3 fixes
+      ebong.postFixup = ''
+        sed -Ei 's/import sre/import re/; s/file\(/open(/g; s/\t/        /g; s/print +(.*)$/print(\1)/g' "$out"/bin/ebong
+      '';
+
+      # find files in script directory, not binary directory
+      # add runtime dependencies to PATH
+      epspdf.postFixup = ''
+        sed -i '2ios.setenv("PATH","${lib.makeBinPath epspdf.extraBuildInputs}" .. (os.getenv("PATH") and ":" .. os.getenv("PATH") or ""))' "$out"/bin/epspdf
+        substituteInPlace "$out"/bin/epspdftk --replace '[info script]' "\"$scriptsFolder/epspdftk.tcl\""
+      '';
+
+      # find files in script directory, not in binary directory
+      latexindent.postFixup = ''
+        substituteInPlace "$out"/bin/latexindent --replace 'use FindBin;' "BEGIN { \$0 = '$scriptsFolder' . '/latexindent.pl'; }; use FindBin;"
+      '';
+
+      # make tlmgr believe it can use kpsewhich to evaluate TEXMFROOT
+      "texlive.infra".postFixup = ''
+        substituteInPlace "$out"/bin/tlmgr \
+          --replace 'if (-r "$bindir/$kpsewhichname")' 'if (1)'
+      '';
+
+      # Patch texlinks.sh back to 2015 version;
+      # otherwise some bin/ links break, e.g. xe(la)tex.
+      # add runtime dependencies to PATH
+      texlive-scripts-extra.postFixup = ''
+        patch -R "$out"/bin/texlinks < '${./texlinks.diff}'
+        sed -i '2iPATH="${lib.makeBinPath [ coreutils ]}''${PATH:+:$PATH}"' "$out"/bin/{allcm,dvired,mkocp,ps2frag}
+        sed -i '2iPATH="${lib.makeBinPath [ coreutils findutils ]}''${PATH:+:$PATH}"' "$out"/bin/allneeded
+        sed -i '2iPATH="${lib.makeBinPath [ coreutils ghostscript_headless ]}''${PATH:+:$PATH}"' "$out"/bin/dvi2fax
+        sed -i '2iPATH="${lib.makeBinPath [ gnused ]}''${PATH:+:$PATH}"' "$out"/bin/{kpsetool,texconfig,texconfig-sys}
+        sed -i '2iPATH="${lib.makeBinPath [ coreutils gnused ]}''${PATH:+:$PATH}"' "$out"/bin/texconfig-dialog
+      '';
+
+      # patch interpreter
+      texosquery.postFixup = ''
+        substituteInPlace "$out"/bin/* --replace java "$interpJava"
+      '';
+
+      #### dependency changes
+
+      # it seems to need it to transform fonts
+      xdvi.deps = (orig.xdvi.deps or []) ++  [ "metafont" ];
 
       # remove dependency-heavy packages from the basic collections
       collection-basic.deps = lib.subtractLists [ "metafont" "xdvi" ] orig.collection-basic.deps;
@@ -58,6 +376,15 @@ let
       collection-metapost.deps = orig.collection-metapost.deps ++ [ "metafont" ];
       collection-plaingeneric.deps = orig.collection-plaingeneric.deps ++ [ "xdvi" ];
 
+      #### misc
+
+      # tlpdb lists license as "unknown", but the README says lppl13: http://mirrors.ctan.org/language/arabic/arabi-add/README
+      arabi-add.license = [  "lppl13c" ];
+
+      # TODO: remove this when updating to texlive-2023, npp-for-context is no longer in texlive
+      # tlpdb lists license as "noinfo", but it's gpl3: https://github.com/luigiScarso/context-npp
+      npp-for-context.license = [  "gpl3Only" ];
+
       texdoc = {
         extraRevision = ".tlpdb${toString tlpdbVersion.revision}";
         extraVersion = "-tlpdb-${toString tlpdbVersion.revision}";
@@ -91,27 +418,27 @@ let
         pkg = attrs // {
           sha512 = attrs.sha512.${if tlType == "tlpkg" then "run" else tlType};
           inherit pname tlType version;
+        } // lib.optionalAttrs (attrs.hasManpages or false) {
+          hasManpages = true;
         };
         in mkPkg pkg;
-    in {
-      # TL pkg contains lists of packages: runtime files, docs, sources, tlpkg, binaries
-      pkgs =
-        # tarball of a collection/scheme itself only contains a tlobj file
-        [( if (attrs.hasRunfiles or false) then mkPkgV "run"
+      run = if (attrs.hasRunfiles or false) then mkPkgV "run"
             # the fake derivations are used for filtering of hyphenation patterns and formats
-          else {
+          else ({
             inherit pname version;
             tlType = "run";
-            hasFormats = attrs.hasFormats or false;
             hasHyphens = attrs.hasHyphens or false;
             tlDeps = map (n: tl.${n}) (attrs.deps or []);
-          }
-        )]
+          } // lib.optionalAttrs (attrs ? formats) { inherit (attrs) formats; });
+    in {
+      # TL pkg contains lists of packages: runtime files, docs, sources, tlpkg, binaries
+      pkgs =
+        # tarball of a collection/scheme itself only contains a tlobj file
+        [ run ]
         ++ lib.optional (attrs.sha512 ? doc) (mkPkgV "doc")
         ++ lib.optional (attrs.sha512 ? source) (mkPkgV "source")
         ++ lib.optional (attrs.hasTlpkg or false) (mkPkgV "tlpkg")
-        ++ lib.optional (bin ? ${pname})
-            ( bin.${pname} // { tlType = "bin"; } );
+        ++ lib.optional (attrs ? binfiles && attrs.binfiles != []) (mkPkgBin pname version run attrs);
     };
 
   version = {
@@ -167,8 +494,46 @@ let
   # name + version for the derivation
   mkTLName = { tlType, version, extraVersion ? "", ... }@attrs: mkURLName attrs + (lib.optionalString (tlType == "tlpkg") ".tlpkg") + "-${version}${extraVersion}";
 
+  # build tlType == "bin" containers based on `binfiles` in TLPDB
+  # see UPGRADING.md for how to keep the list of shebangs up to date
+  mkPkgBin = let extToInput = {
+      jar = jdk;
+      lua = bin.luatex;
+      py = python3;
+      rb = ruby;
+      sno = snobol4;
+      tcl = tk;
+      texlua = bin.luatex;
+      tlu = bin.luatex;
+    }; in pname: version: run:
+    { binfiles, scriptsFolder ? pname, postFixup ? "", scriptExts ? [], extraBuildInputs ? [], binlinks ? {}, ... }@args:
+    runCommand "texlive-${pname}.bin-${version}"
+      {
+        # metadata for texlive.combine
+        passthru = {
+          inherit pname version;
+          tlType = "bin";
+        };
+        # shebang interpreters
+        buildInputs = extraBuildInputs ++ [ bash perl ] ++ (lib.attrVals scriptExts extToInput);
+        # absolute scripts folder
+        scriptsFolder = lib.optionalString (run ? outPath) (run.outPath + "/scripts/" + scriptsFolder);
+        # binaries info
+        inherit binfiles;
+        binlinks = builtins.attrNames binlinks;
+        bintargets = builtins.attrValues binlinks;
+        binfolders = [ (lib.getBin bin.core) ] ++ lib.optional (bin ? ${pname}) (lib.getBin bin.${pname});
+        # build scripts
+        patchScripts = ./patch-scripts.sed;
+        makeBinContainers = ./make-bin-containers.sh;
+      }
+      ''
+        . "$makeBinContainers"
+        ${postFixup}
+      '';
+
   # create a derivation that contains an unpacked upstream TL package
-  mkPkg = { pname, tlType, revision, version, sha512, extraRevision ? "", postUnpack ? "", stripPrefix ? 1, ... }@args:
+  mkPkg = { pname, tlType, revision, version, sha512, extraRevision ? "", postUnpack ? "", stripPrefix ? 1, hasManpages ? false, ... }@args:
     let
       # the basename used by upstream (without ".tar.xz" suffix)
       urlName = mkURLName args;
@@ -191,11 +556,12 @@ let
           } // lib.optionalAttrs (tlType == "run" && args ? deps) {
             tlDeps = map (n: tl.${n}) args.deps;
           } // lib.optionalAttrs (tlType == "run") {
-            hasFormats = args.hasFormats or false;
             hasHyphens = args.hasHyphens or false;
           } // lib.optionalAttrs (tlType == "tlpkg" && args ? postactionScript) {
             postactionScript = args.postactionScript;
-          };
+          }
+          // lib.optionalAttrs (tlType == "run" && args ? formats) { inherit (args) formats; }
+          // lib.optionalAttrs (tlType == "doc" && hasManpages) { hasManpages = true; };
         } // lib.optionalAttrs (fixedHash != null) {
           outputHash = fixedHash;
           outputHashAlgo = "sha256";
@@ -250,7 +616,10 @@ in
       xz = tlpdbxz;
     };
 
-    bin = assert assertions; bin;
+    bin = assert assertions; bin // {
+      # for backward compatibility
+      latexindent = lib.findFirst (p: p.tlType == "bin") tl.latexindent.pkgs;
+    };
     combine = assert assertions; combine;
 
     # Pre-defined combined packages for TeX Live schemes,