Unverified Commit ce426df9 authored by Dmitry Kalinkin's avatar Dmitry Kalinkin Committed by GitHub
Browse files

Merge pull request #294826 from xworld21/texlive-install-tl

texlive.withPackages: replace postBuild with install-tl like script
parents ce31cd2a 87216540
Loading
Loading
Loading
Loading
+8 −12
Original line number Diff line number Diff line
@@ -81,6 +81,14 @@ rec {
    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}]
@@ -333,18 +341,6 @@ rec {
    done
  '';

  # test that fmtutil.cnf is fully regenerated on scheme-full
  fmtutilCnf = runCommand "texlive-test-fmtutil.cnf" {
    kpathsea = texlive.pkgs.kpathsea.tex;
    schemeFull = texliveFull;
  } ''
    mkdir -p "$out"

    diff --ignore-matching-lines='^# Generated by ' -u \
      {"$kpathsea","$schemeFull"/share/texmf-var}/web2c/fmtutil.cnf \
      | tee "$out/fmtutil.cnf.patch"
  '';

  # verify that the restricted mode gets enabled when
  # needed (detected by checking if it disallows --gscmd)
  repstopdf = runCommand "texlive-test-repstopdf" {
+90 −229
Original line number Diff line number Diff line
@@ -38,6 +38,12 @@ lib.fix (self: {
}@args:

let
  ### buildEnv with custom attributes
  buildEnv' = args: (buildEnv
    ({ inherit (args) name paths; })
      // lib.optionalAttrs (args ? extraOutputsToInstall) { inherit (args) extraOutputsToInstall; })
    .overrideAttrs (removeAttrs args [ "extraOutputsToInstall" "name" "paths" "pkgs" ]);

  ### 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;
@@ -78,7 +84,7 @@ let
    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"
        ([ "out" ] ++ lib.optional (otherOutputs ? man) "man"
          ++ lib.concatLists (builtins.catAttrs "outputsToInstall" (builtins.catAttrs "meta" specified.wrong)));
      operator = _: [ ];
    });
@@ -103,6 +109,14 @@ let

    # outputs that do not become part of the environment
    nonEnvOutputs = lib.subtractLists [ "out" "tex" "texdoc" "texsource" "tlpkg" ] otherOutputNames;

    # packages that contribute to config files and formats
    fontMaps = lib.filter (p: p ? fontMaps && (p.tlOutputName or p.outputName == "tex")) nonbin;
    sortedFontMaps = builtins.sort (a: b: a.pname < b.pname) fontMaps;
    hyphenPatterns = lib.filter (p: p ? hyphenPatterns && (p.tlOutputName or p.outputName == "tex")) nonbin;
    sortedHyphenPatterns = builtins.sort (a: b: a.pname < b.pname) hyphenPatterns;
    formatPkgs = lib.filter (p: p ? formats && (p.outputSpecified or false -> p.tlOutputName or p.outputName == "tex") && builtins.any (f: f.enabled or true) p.formats) all;
    sortedFormatPkgs = builtins.sort (a: b: a.pname < b.pname) formatPkgs;
  };

  # list generated by inspecting `grep -IR '\([^a-zA-Z]\|^\)gs\( \|$\|"\)' "$TEXMFDIST"/scripts`
@@ -113,7 +127,7 @@ let
  name = if __combine then "texlive-${__extraName}-${bin.texliveYear}${__extraVersion}" # texlive.combine: old name name
    else "texlive-${bin.texliveYear}-env";

  texmfdist = (buildEnv {
  texmfdist = buildEnv' {
    name = "${name}-texmfdist";

    # remove fake derivations (without 'outPath') to avoid undesired build dependencies
@@ -126,14 +140,14 @@ let
    ''
      mktexlsr "$out"
    '';
  }).overrideAttrs (_: { allowSubstitutes = true; });
  };

  tlpkg = (buildEnv {
  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)
@@ -170,12 +184,10 @@ let
          (requiredTeXPackages tl);
  };

  # emulate split output derivation
  splitOutputs = {
    texmfdist = texmfdist // { outputSpecified = true; };
    texmfroot = texmfroot // { outputSpecified = true; };
  } // (lib.genAttrs pkgList.nonEnvOutputs (outName: (buildEnv {
  # other outputs
  nonEnvOutputs = lib.genAttrs pkgList.nonEnvOutputs (outName: buildEnv' {
    inherit name;
    outputs = [ outName ];
    paths = builtins.catAttrs "outPath"
      (pkgList.otherOutputs.${outName} or [ ] ++ pkgList.specifiedOutputs.${outName} or [ ]);
    # force the output to be ${outName} or nix-env will not work
@@ -183,11 +195,9 @@ let
      export out="''${${outName}-}"
    '') ];
    inherit meta passthru;
  }).overrideAttrs { outputs = [ outName ]; } // { outputSpecified = true; }));
  });

  passthru = {
    # these are not part of pkgList.nonEnvOutputs and must be exported in passthru
    inherit (splitOutputs) texmfdist texmfroot;
    # 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)
@@ -201,13 +211,57 @@ let
    withPackages = reqs: self (args // { requiredTeXPackages = ps: requiredTeXPackages ps ++ reqs ps; __fromCombineWrapper = false; });
  };

  # TeXLive::TLOBJ::fmtutil_cnf_lines
  fmtutilLine = { name, engine, enabled ? true, patterns ? [ "-" ], options ? "", ... }:
    lib.optionalString (! enabled) "#! " + "${name} ${engine} ${lib.concatStringsSep "," patterns} ${options}";
  fmtutilLines = { pname, formats, ...}:
    [ "#" "# from ${pname}:" ] ++ map fmtutilLine formats;

  # TeXLive::TLOBJ::language_dat_lines
  langDatLine = { name, file, synonyms ? [ ], ... }:
    [ "${name} ${file}" ] ++ map (s: "=" + s) synonyms;
  langDatLines = { pname, hyphenPatterns, ... }:
    [ "% from ${pname}:" ] ++ builtins.concatMap langDatLine hyphenPatterns;

  # TeXLive::TLOBJ::language_def_lines
  # see TeXLive::TLUtils::parse_AddHyphen_line for default values
  langDefLine = { name, file, lefthyphenmin ? "", righthyphenmin ? "", synonyms ? [ ], ... }:
    map (n: "\\addlanguage{${n}}{${file}}{}{${if lefthyphenmin == "" then "2" else lefthyphenmin}}{${if righthyphenmin ==  "" then "3" else righthyphenmin}}")
    ([ name ] ++ synonyms);
  langDefLines = { pname, hyphenPatterns, ... }:
    [ "% from ${pname}:" ] ++ builtins.concatMap langDefLine hyphenPatterns;

  # TeXLive::TLOBJ::language_lua_lines
  # see TeXLive::TLUtils::parse_AddHyphen_line for default values
  langLuaLine = { name, file, lefthyphenmin ? "", righthyphenmin ? "", synonyms ? [ ], ... }@args: ''
      ''\t['${name}'] = {
      ''\t''\tloader = '${file}',
      ''\t''\tlefthyphenmin = ${if lefthyphenmin == "" then "2" else lefthyphenmin},
      ''\t''\trighthyphenmin = ${if righthyphenmin ==  "" then "3" else righthyphenmin},
      ''\t''\tsynonyms = { ${lib.concatStringsSep ", " (map (s: "'${s}'") synonyms)} },
    ''
    + lib.optionalString (args ? file_patterns) "\t\tpatterns = '${args.file_patterns}',\n"
    + lib.optionalString (args ? file_exceptions) "\t\thyphenation = '${args.file_exceptions}',\n"
    + lib.optionalString (args ? luaspecial) "\t\tspecial = '${args.luaspecial}',\n"
    + "\t},";
  langLuaLines = { pname, hyphenPatterns, ... }:
    [ "-- from ${pname}:" ] ++ map langLuaLine hyphenPatterns;

  assembleConfigLines = f: packages:
    builtins.concatStringsSep "\n" (builtins.concatMap f packages);

  updmapLines = { pname, fontMaps, ...}:
    [ "# from ${pname}:" ] ++ fontMaps;

  out =
# no indent for git diff purposes
(buildEnv {
buildEnv' {

  inherit name;

  ignoreCollisions = false;
  # use attrNames, attrValues to ensure the two lists are sorted in the same way
  outputs = [ "out" ] ++ lib.optionals (! __combine) (builtins.attrNames nonEnvOutputs);
  otherOutputs = lib.optionals (! __combine) (builtins.attrValues nonEnvOutputs);

  # remove fake derivations (without 'outPath') to avoid undesired build dependencies
  paths = builtins.catAttrs "outPath" pkgList.bin
@@ -230,219 +284,26 @@ let
    perl
  ];

  buildInputs = [ coreutils gawk gnugrep gnused ] ++ lib.optional needsGhostscript ghostscript;

  inherit meta passthru;

  postBuild =
    # create outputs
  lib.optionalString (! __combine) ''
    for otherOutputName in $outputs ; do
      if [[ "$otherOutputName" == 'out' ]] ; then continue ; fi
      otherOutput="otherOutput_$otherOutputName"
      ln -s "''${!otherOutput}" "''${!otherOutputName}"
    done
  '' +
    # 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

      # skip non-executable files (such as context.lua)
      if [[ ! -x "$target" ]] ; then continue ; 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"
  '' +
    # temporarily patch mtxrun.lua to generate uuid's deterministically from SOURCE_DATE_EPOCH
  ''
    if [[ -e "$out/bin/mtxrun" ]]; then
      mv "$out"/bin/mtxrun.lua{,.orig}
      substitute "$TEXMFDIST"/scripts/context/lua/mtxrun.lua "$out"/bin/mtxrun.lua \
        --replace-fail 'randomseed(math.initialseed)' "randomseed($SOURCE_DATE_EPOCH)"
    fi
  '' +
  # texlive postactions (see TeXLive::TLUtils::_do_postaction_script)
  # this step includes generating the ConTeXt file databases since TL 2023
  (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"
    FORCE_SOURCE_DATE=1 TZ= $postInterp "$TEXMFROOT"/$postaction install "$TEXMFROOT"
  '') (lib.filter (pkg: pkg ? postactionScript) pkgList.tlpkg)) +
  # restore the original mtxrun.lua
  ''
    if [[ -e "$out/bin/mtxrun" ]]; then
      mv "$out"/bin/mtxrun.lua{.orig,}
    fi
  '' +
    # generate formats
    # TODO generate ConTeXt formats (based on fmtutil.cnf?)
  ''
    # 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-fail '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
  '' +
  # 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 (prev:
  { allowSubstitutes = true; }
  // lib.optionalAttrs (! __combine) ({
    outputs = [ "out" ] ++ pkgList.nonEnvOutputs;
    meta = prev.meta // { inherit (pkgList) outputsToInstall; };
  } // builtins.listToAttrs
    (map (out: { name = "otherOutput_" + out; value = splitOutputs.${out}; }) pkgList.nonEnvOutputs)
  )
);
in out)
  inherit texmfdist texmfroot;

  fontconfigFile = makeFontsConf { fontDirectories = [ "${texmfroot}/texmf-dist/fonts" ]; };

  fmtutilCnf = assembleConfigLines fmtutilLines pkgList.sortedFormatPkgs;
  updmapCfg = assembleConfigLines updmapLines pkgList.sortedFontMaps;

  languageDat = assembleConfigLines langDatLines pkgList.sortedHyphenPatterns;
  languageDef = assembleConfigLines langDefLines pkgList.sortedHyphenPatterns;
  languageLua = assembleConfigLines langLuaLines pkgList.sortedHyphenPatterns;

  postactionScripts = builtins.catAttrs "postactionScript" pkgList.tlpkg;

  postBuild = ''
    . "${./build-tex-env.sh}"
  '';
};
  # outputsToInstall must be set *after* overrideAttrs (used in buildEnv') or it fails the checkMeta tests
in if __combine then out else lib.addMetaAttrs { inherit (pkgList) outputsToInstall; } out)
+328 −0

File added.

Preview size limit exceeded, changes collapsed.

+2 −1
Original line number Diff line number Diff line
@@ -82,8 +82,9 @@ let
    outputSpecified = true;
    inherit tex;
  } // lib.optionalAttrs (args ? deps) { tlDeps = args.deps; }
  // lib.optionalAttrs (args ? fontMaps) { inherit (args) fontMaps; }
  // lib.optionalAttrs (args ? formats) { inherit (args) formats; }
  // lib.optionalAttrs hasHyphens { inherit hasHyphens; }
  // lib.optionalAttrs (args ? hyphenPatterns) { inherit (args) hyphenPatterns; }
  // lib.optionalAttrs (args ? postactionScript) { inherit (args) postactionScript; }
  // lib.optionalAttrs hasDocfiles { texdoc = texdoc; }
  // lib.optionalAttrs hasSource { texsource = texsource; }
+46 −3
Original line number Diff line number Diff line
@@ -80,6 +80,22 @@ $a}
    D # restart cycle from the current line
  }

  # extract font maps
  /^execute add.*Map /{
    # open a list
    i\  fontMaps = [

    # loop through following map lines
    :next-map
      s/^\n?execute add(.*Map .*)$/    "\1"/p # print map
      s/^.*$//                              # clear pattern space
      N; /^\nexecute add.*Map /b next-map

    # close the string
    i\  ];
    D # restart cycle from the current line
  }

  # detect presence of notable files
  /^docfiles /{
    s/^.*$//  # ignore the first line
@@ -129,9 +145,36 @@ $a}
  # extract postaction scripts (right now, at most one per package, so a string suffices)
  s/^postaction script file=(.*)$/  postactionScript = "\1";/p

  # extract hyphenation patterns and formats
  # (this may create duplicate lines, use uniq to remove them)
  /^execute\sAddHyphen/i\  hasHyphens = true;
  # extract hyphenation patterns
  /^execute\sAddHyphen\s/{
    # open a list
    i\  hyphenPatterns = [

    # create one attribute set per hyphenation pattern

    # plain keys: name, lefthyphenmin, righthyphenmin, file, file_patterns, file_exceptions, comment
    # optionally double quoted key: luaspecial, comment
    # comma-separated lists: databases, synonyms
    :next-hyphen
      s/(^|\n)execute\sAddHyphen/    {/
      s/\s+luaspecial="([^"]+)"/\n      luaspecial = "\1";/
      s/\s+(name|lefthyphenmin|righthyphenmin|file|file_patterns|file_exceptions|luaspecial|comment)=([^ \t\n]*)/\n      \1 = "\2";/g
      s/\s+(databases|synonyms)=([^ \t\n]+)/\n      \1 = [ "\2" ];/g
      s/$/\n    }/

      :split-hyphens
        s/"([^,]+),([^"]+)" ]/"\1" "\2" ]/;
        t split-hyphens   # repeat until there are no commas

      p
      s/^.*$// # clear pattern space
      N
      /^\nexecute\sAddHyphen\s/b next-hyphen

    # close the list
    i\  ];
    D # restart cycle from the current line
  }

  # extract format details
  /^execute\sAddFormat\s/{
Loading