summaryrefslogtreecommitdiff
path: root/macros/latex/contrib/l3build
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /macros/latex/contrib/l3build
Initial commit
Diffstat (limited to 'macros/latex/contrib/l3build')
-rw-r--r--macros/latex/contrib/l3build/CHANGELOG.md346
-rw-r--r--macros/latex/contrib/l3build/CONTRIBUTING.md52
-rw-r--r--macros/latex/contrib/l3build/README.md36
-rw-r--r--macros/latex/contrib/l3build/l3build-arguments.lua301
-rw-r--r--macros/latex/contrib/l3build/l3build-aux.lua126
-rw-r--r--macros/latex/contrib/l3build/l3build-check.lua990
-rw-r--r--macros/latex/contrib/l3build/l3build-clean.lua56
-rw-r--r--macros/latex/contrib/l3build/l3build-ctan.lua159
-rw-r--r--macros/latex/contrib/l3build/l3build-file-functions.lua444
-rw-r--r--macros/latex/contrib/l3build/l3build-help.lua86
-rw-r--r--macros/latex/contrib/l3build/l3build-install.lua268
-rw-r--r--macros/latex/contrib/l3build/l3build-manifest-setup.lua364
-rw-r--r--macros/latex/contrib/l3build/l3build-manifest.lua280
-rw-r--r--macros/latex/contrib/l3build/l3build-stdmain.lua185
-rw-r--r--macros/latex/contrib/l3build/l3build-tagging.lua84
-rw-r--r--macros/latex/contrib/l3build/l3build-typesetting.lua233
-rw-r--r--macros/latex/contrib/l3build/l3build-unpack.lua102
-rw-r--r--macros/latex/contrib/l3build/l3build-upload.lua350
-rw-r--r--macros/latex/contrib/l3build/l3build-variables.lua205
-rw-r--r--macros/latex/contrib/l3build/l3build.167
-rw-r--r--macros/latex/contrib/l3build/l3build.dtx2369
-rw-r--r--macros/latex/contrib/l3build/l3build.ins56
-rw-r--r--macros/latex/contrib/l3build/l3build.lua189
-rw-r--r--macros/latex/contrib/l3build/l3build.pdfbin0 -> 703605 bytes
24 files changed, 7348 insertions, 0 deletions
diff --git a/macros/latex/contrib/l3build/CHANGELOG.md b/macros/latex/contrib/l3build/CHANGELOG.md
new file mode 100644
index 0000000000..b7d20890a6
--- /dev/null
+++ b/macros/latex/contrib/l3build/CHANGELOG.md
@@ -0,0 +1,346 @@
+# Changelog
+All notable changes to the `l3build` bundle since the start of 2018
+will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+this project uses date-based 'snapshot' version identifiers.
+
+## [Unreleased]
+
+## [2019-08-24]
+
+### Changed
+
+- Include `LUAINPUTS` when setting `TEXINPUTS` for `checksearch = false`, etc.
+
+### Fixed
+
+- Remove `.log` file before each check run: prevent inter-engine confusion
+
+## [2019-07-31]
+
+### Fixed
+
+- Interaction between secondary files in some tests
+
+## [2019-07-30]
+
+### Added
+
+- Support for non-standard file layouts via `tdslocations` table
+
+### Changed
+
+- Only write (x)dvipdfmx specials for XeTeX and (u)pTeX (see #94)
+
+## [2019-06-27]
+
+### Changed
+
+- Back out change for normalisation of LuaTeX v1.10 stack information
+ (timing issue: will reintroduce later)
+
+### Fixed
+
+- Correct test for LuaTeX (see #93)
+
+## [2019-06-26]
+
+### Added
+
+- Support for HarfTeX (see #92)
+
+### Changed
+
+- Support for normalisation of LuaTeX v1.10 stack information
+ (may require new `.tlg` files)
+
+### Fixed
+
+- Support for spaces in paths when typesetting (see #91)
+
+## [2019-06-18]
+
+### Added
+
+- Switch `--show-log-on-error` for use with `--halt-on-error`. Results in the `.log` file
+ being show in full on the console to aid in non-interactive debugging.
+
+### Changed
+
+- Moved LuaTeX-specific font cache normalisation
+
+## [2019-02-10]
+
+### Fixed
+
+- Handling of ASCII engines
+- Execution of all tests by `ctan` target (see #85)
+
+## [2019-02-06]
+
+### Added
+
+- Table-based control of binary/format combinations: `specialformats`
+ (see #84)
+- Switch `--debug` for chasing problems with the `upload` target
+
+### Changed
+
+- The `uploadconfig.update` field by default now automatically detects whether it
+ needs to be `true` or `false`
+- The `tag` target now allows no tag name to allow for setting this programmatically
+ within a `build.lua` script
+- Better support for multiple LuaTeX-like engines
+
+### Fixed
+
+- Uploading via Windows should now work
+
+## [2018-12-23]
+
+### Added
+
+- Switch `--email` for providing upload email address
+- Switch `-F|--file` for providing upload announcement from file
+- Switch `-m|--message` for providing upload announcement from command line
+
+### Changed
+
+- Enable `--dry-run` option for `upload` target
+- Enable tag/version to be passed as optional argument to `upload` target
+
+### Fixed
+
+- Packaging of some team-specific files
+- Handling of upload data containing newlines (Windows only)
+
+## [2018-12-18]
+
+### Changed
+
+- Add new `upload` target which uses `curl` with the CTAN API to send a package
+ for release (see #1)
+
+## [2018-11-08]
+
+### Changed
+
+- Strip leading spaces from file loading/page number lines (see #78)
+
+### Fixed
+
+- Print test failures correctly per-config (see #77)
+
+## [2018-10-30]
+
+### Fixed
+
+- Substitution of spaces in Unix paths (see #76)
+
+## [2018-10-25]
+
+### Added
+
+- `\SHOWFILE` command
+
+### Changed
+
+- Better support for multiple LuaTeX engines (see #75)
+
+## Fixed
+
+- Support for spaces in directory names (fixes #76)
+- Support relative directories as argument to `--texmfhome`
+
+## [2018-09-23]
+
+### Changed
+
+- Entries in `scriptfiles` are excluded from `installfiles`
+- Use a per-config directory for running tests
+- Enable use of local `texmf.cnf` file in tests and documentation
+- New `ctanreadme` variable
+
+## [2018-09-21]
+
+### Changed
+
+- Normalise date lines to contain "...-..-.." rather than removing
+ (including normalising version data in such lines): note that
+ `.tlg` file updates may be required after this change
+- Explicitly exclude `.fd` file lines
+
+## [2018-08-07]
+
+### Changed
+- Suppress file name info in PDF output for included images, etc.
+
+### Fixed
+- Issue with creation of CTAN releases for bundles
+
+## [2018-08-04]
+
+### Added
+- `ctanzip` variable
+- `--full` option
+
+### Changed
+- Run PDF-based tests for all engines
+- Tweaks to PDF-based normalisation: new `.tpf` files will be required
+- The `--halt-on-error|-H` setting now applies to multiple configs
+
+### Fixed
+- Testing using `.lve` files
+- Tagging with new approach to top-level targets
+
+## [2018-08-02]
+
+### Added
+- `CHANGELOG.md`
+- `--dirty` option
+- `includetests` and `excludetests` variables for controlling which tests
+ run
+- `target_list` table to allow control of targets without redefining
+ `main()`
+
+### Changed
+- PDF-based testing now uses 'digested' PDF file for comparison,
+ working from dedicated `.pvt` input files
+
+### Removed
+- `--pdf|-p` command line switch
+
+## [2018-05-10]
+
+### Changed
+- Revert appearance of date lines in `.tlg` files:
+ this is on balance problematic
+
+## [2018-05-06]
+
+### Added
+- Variable `dynamicfiles` to be cleaned between each test run
+
+### Changed
+- Normalise dates to placeholder "YYYY-MM-DD": may require `.tlg` updates
+
+### Fixed
+- Include dot files in `tree()` (fixes #30)
+
+## [2018-03-26]
+
+### Changed
+- Omit ISO date lines in `.tlg` files
+
+## [2018-03-24]
+
+### Changed
+- Allow 'short cut' of check runs
+- Support for upcoming LaTeX kernel release functions
+
+## [2018-03-10]
+
+### Changed
+- Add `#!` line for POSIX users
+- Set POSIX u+x on `l3build.lua`
+
+### Fixed
+- Handling of script name with or without extension
+
+## [2018-03-09]
+
+### Fixed
+- Pass through script name correctly with new set up
+
+## [2018-03-08]
+
+### Added
+- Target `tag`, variable `tagfiles` and function `update_tag()`
+- Variables `scriptfiles` and `scriptmanfiles` to support installation
+ of scripts
+
+### Changed
+- `l3build` can now be run as a top-level script rather than using
+ `texlua build.lua ...`
+- Normalisation of LuaTeX-derived `tlg` files, in preparation for
+ TeX Live 2018
+
+### Deprecated
+- Use of wrapper `build.lua` script to call `l3build`: the new
+ top-level script approach is preferred
+
+### Removed
+- Target `setversion` and variable `versionfiles`
+
+## [2018-02-20]
+
+### Changed
+- Allow for `checkopts` adding code/files
+
+### Fixed
+- Creation of 'structured' CTAN releases
+- Quote test names correctly
+
+## [2018-01-27]
+
+### Added
+- Target `uninstall`
+- Options `--first` and `--last`
+
+## Changed
+- Normalisation for upcoming LuaTeX 1.07 release
+
+### Fixed
+- Behaviour of check on Windows when using standard `fc` tool
+
+## [2018-01-10]
+
+## Added
+- Target `manifest` for construction of file manifests automatically
+- Variable `auxfiles`
+- Option `--dry-run` for installation/cleaning
+- Option `--texmfhome` to allow customisation of installation
+- Option `--shuffle` to run tests in a random order
+
+### Changed
+- Sort list of tests to avoid system-dependent ordering
+- Split `l3build` into multiple files for improved maintenance
+
+### Fixed
+- Issue with `recordstatus`
+
+### Removed
+- Rationalise short option names: removed `-d`, `-E`, `-r`
+- Target `cmdcheck`: specific to LaTeX3 kernel work
+
+[Unreleased]: https://github.com/latex3/l3build/compare/2019-08-24...HEAD
+[2019-08-24]: https://github.com/latex3/l3build/compare/2019-07-31...2019-08-24
+[2019-07-31]: https://github.com/latex3/l3build/compare/2019-07-30...2019-07-31
+[2019-07-30]: https://github.com/latex3/l3build/compare/2019-06-27...2019-07-30
+[2019-06-27]: https://github.com/latex3/l3build/compare/2019-06-26...2019-06-27
+[2019-06-26]: https://github.com/latex3/l3build/compare/2019-06-18...2019-06-26
+[2019-06-18]: https://github.com/latex3/l3build/compare/2019-02-10...2019-06-18
+[2019-02-10]: https://github.com/latex3/l3build/compare/2019-02-06...2019-02-10
+[2019-02-06]: https://github.com/latex3/l3build/compare/2018-12-23...2019-02-06
+[2018-12-23]: https://github.com/latex3/l3build/compare/2018-12-18...2018-12-23
+[2018-12-18]: https://github.com/latex3/l3build/compare/2018-11-08...2018-12-18
+[2018-11-08]: https://github.com/latex3/l3build/compare/2018-10-30...2018-11-08
+[2018-10-30]: https://github.com/latex3/l3build/compare/2018-10-25...2018-10-30
+[2018-10-25]: https://github.com/latex3/l3build/compare/2018-09-26...2018-10-25
+[2018-09-26]: https://github.com/latex3/l3build/compare/2018-09-23...2018-09-26
+[2018-09-23]: https://github.com/latex3/l3build/compare/2018-09-21...2018-09-23
+[2018-09-21]: https://github.com/latex3/l3build/compare/2018-08-07...2018-09-21
+[2018-08-07]: https://github.com/latex3/l3build/compare/2018-08-04...2018-08-07
+[2018-08-04]: https://github.com/latex3/l3build/compare/2018-08-02...2018-08-04
+[2018-08-02]: https://github.com/latex3/l3build/compare/2018-05-10...2018-08-02
+[2018-05-10]: https://github.com/latex3/l3build/compare/2018-05-06...2018-05-10
+[2018-05-06]: https://github.com/latex3/l3build/compare/2018-03-26...2018-05-06
+[2018-03-26]: https://github.com/latex3/l3build/compare/2018-03-24...2018-03-26
+[2018-03-24]: https://github.com/latex3/l3build/compare/2018-03-10...2018-03-24
+[2018-03-10]: https://github.com/latex3/l3build/compare/2018-03-09...2018-03-10
+[2018-03-09]: https://github.com/latex3/l3build/compare/2018-03-08...2018-03-09
+[2018-03-08]: https://github.com/latex3/l3build/compare/2018-02-20...2018-03-08
+[2018-02-20]: https://github.com/latex3/l3build/compare/2018-01-27...2018-02-20
+[2018-01-27]: https://github.com/latex3/l3build/compare/2018-01-10...2018-01-27
+[2018-01-10]: https://github.com/latex3/l3build/compare/2017-12-12...2018-01-10
diff --git a/macros/latex/contrib/l3build/CONTRIBUTING.md b/macros/latex/contrib/l3build/CONTRIBUTING.md
new file mode 100644
index 0000000000..a152299e83
--- /dev/null
+++ b/macros/latex/contrib/l3build/CONTRIBUTING.md
@@ -0,0 +1,52 @@
+Thanks for considering contributing to `l3build`: feedback, fixes and ideas are
+all useful. Here, we ([The LaTeX3 Project](https://www.latex-project.org)) have
+collected together a few pointers to help things along.
+
+## Bugs
+
+Please log bugs using the [issues](https://github.com/latex3/l3build/issues)
+system on GitHub, and choose the 'bug' label. Handy information that you might
+include, depending on the nature of the issue, includes
+
+- Your version of `l3build` (`texlua l3build.lua version`)
+- Your TeX system details (for example 'TeX Live 2017')
+- Your operating system
+- The contents of your `build.lua` file
+- An 'ASCII art' explanation of your directory layout
+
+## Feature requests
+
+Feature requests are welcome: log them in the same way as bugs and pick
+the 'Enhancement' label. We welcome feature requests for the test set up,
+the build process, _etc._
+
+## Code contributions
+
+If you want to discuss a possible contribution before (or instead of)
+making a pull request, drop a line to
+[the team](mailto:latex-team@latex-project.org).
+
+There are a few things that might look non-standard to most Lua programmers,
+which come about as `l3build`'s focus is testing and building LaTeX packages:
+
+- Our target Lua set up is `texlua` (part of LuaTeX), not standalone `lua`
+- The main `l3build.lua` file is self-contained as this helps with
+ bootstrapping LaTeX: we are aiming to maintain a single file with no
+ `.lua` dependencies
+- The primary documentation is aimed at the TeX world, so is in PDF format
+ and generated from `l3build.dtx`; documentation in the `.lua` file is
+ also welcome, but anything for general use does need to be in the `.dtx`
+- As far as possible, everything is done within `l3build` itself or tools
+ directly available in a TeX system or as standard in the supported
+ systems (Windows, MacOS, Linux)
+- The `l3build` interfaces should be platform-agnostic (though it may be
+ necessary of course to branch inside particular functions)
+
+If you are submitting a pull request, notice that
+
+- We use Travis-CI for (light) testing so add `[ci skip]` to documentation-only
+ commit messages
+- We favour a single linear history so will rebase agreed pull requests on to
+ the `master` branch
+- Where a commit fixes or closes an issue, please include this information
+ in the first line of the commit message
diff --git a/macros/latex/contrib/l3build/README.md b/macros/latex/contrib/l3build/README.md
new file mode 100644
index 0000000000..16abce04d9
--- /dev/null
+++ b/macros/latex/contrib/l3build/README.md
@@ -0,0 +1,36 @@
+l3build: a testing and building system for LaTeX3
+=================================================
+
+Release 2019-08-24
+
+Overview
+--------
+
+The `l3build` module is designed to support the development of
+high-quality LaTeX code by providing:
+* A unit testing system
+* Automated typesetting of code sources
+* A reliable packaging system for CTAN releases
+
+The bundle consists of a Lua script to run the tasks and a
+`.tex` file which provides the testing environment. These were
+originally developed for supporting LaTeX3 development but
+are designed such that they can be readily used by others. Full
+documentation is provided.
+
+Issues
+------
+
+The issue tracker for LaTeX3 is currently located
+[on GitHub](https://github.com/latex3/l3build/issues).
+
+Development team
+----------------
+
+The LaTeX kernel is developed by [The LaTeX3 Project](https://latex-project.org).
+
+-----
+
+<p>Copyright (C) 2014-2019 The LaTeX3 Project <br />
+<a href="http://latex-project.org/">http://latex-project.org/</a> <br />
+All rights reserved.</p>
diff --git a/macros/latex/contrib/l3build/l3build-arguments.lua b/macros/latex/contrib/l3build/l3build-arguments.lua
new file mode 100644
index 0000000000..cbf68c6254
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-arguments.lua
@@ -0,0 +1,301 @@
+--[[
+
+File l3build-arguments.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local exit = os.exit
+local stderr = io.stderr
+
+local find = string.find
+local gmatch = string.gmatch
+local match = string.match
+local sub = string.sub
+
+local insert = table.insert
+
+-- Parse command line options
+
+option_list =
+ {
+ config =
+ {
+ desc = "Sets the config(s) used for running tests",
+ short = "c",
+ type = "table"
+ },
+ date =
+ {
+ desc = "Sets the date to insert into sources",
+ type = "string"
+ },
+ debug =
+ {
+ desc = "Runs target in debug mode (not supported by all targets)",
+ type = "boolean"
+ },
+ dirty =
+ {
+ desc = "Skip cleaning up the test area",
+ type = "boolean"
+ },
+ ["dry-run"] =
+ {
+ desc = "Dry run for install",
+ type = "boolean"
+ },
+ email =
+ {
+ desc = "Email address of CTAN uploader",
+ type = "string"
+ },
+ engine =
+ {
+ desc = "Sets the engine(s) to use for running test",
+ short = "e",
+ type = "table"
+ },
+ epoch =
+ {
+ desc = "Sets the epoch for tests and typesetting",
+ type = "string"
+ },
+ file =
+ {
+ desc = "Take the upload announcement from the given file",
+ short = "F",
+ type = "string"
+ },
+ first =
+ {
+ desc = "Name of first test to run",
+ type = "string"
+ },
+ force =
+ {
+ desc = "Force tests to run if engine is not set up",
+ short = "f",
+ type = "boolean"
+ },
+ full =
+ {
+ desc = "Install all files",
+ type = "boolean"
+ },
+ ["halt-on-error"] =
+ {
+ desc = "Stops running tests after the first failure",
+ short = "H",
+ type = "boolean"
+ },
+ help =
+ {
+ short = "h",
+ type = "boolean"
+ },
+ last =
+ {
+ desc = "Name of last test to run",
+ type = "string"
+ },
+ message =
+ {
+ desc = "Text for upload announcement message",
+ short = "m",
+ type = "string"
+ },
+ quiet =
+ {
+ desc = "Suppresses TeX output when unpacking",
+ short = "q",
+ type = "boolean"
+ },
+ rerun =
+ {
+ desc = "Skip setup: simply rerun tests",
+ type = "boolean"
+ },
+ ["show-log-on-error"] =
+ {
+ desc = "If 'halt-on-error' stops, show the full log of the failure",
+ type = "boolean"
+ },
+ shuffle =
+ {
+ desc = "Shuffle order of tests",
+ type = "boolean"
+ },
+ texmfhome =
+ {
+ desc = "Location of user texmf tree",
+ type = "string"
+ }
+ }
+
+-- This is done as a function (rather than do ... end) as it allows early
+-- termination (break)
+local function argparse()
+ local result = { }
+ local names = { }
+ local long_options = { }
+ local short_options = { }
+ -- Turn long/short options into two lookup tables
+ for k,v in pairs(option_list) do
+ if v["short"] then
+ short_options[v["short"]] = k
+ end
+ long_options[k] = k
+ end
+ local args = args
+ -- arg[1] is a special case: must be a command or "-h"/"--help"
+ -- Deal with this by assuming help and storing only apparently-valid
+ -- input
+ local a = arg[1]
+ result["target"] = "help"
+ if a then
+ -- No options are allowed in position 1, so filter those out
+ if a == "--version" then
+ result["target"] = "version"
+ elseif not match(a, "^%-") then
+ result["target"] = a
+ end
+ end
+ -- Stop here if help or version is required
+ if result["target"] == "help" or result["target"] == "version" then
+ return result
+ end
+ -- An auxiliary to grab all file names into a table
+ local function remainder(num)
+ local names = { }
+ for i = num, #arg do
+ insert(names, arg[i])
+ end
+ return names
+ end
+ -- Examine all other arguments
+ -- Use a while loop rather than for as this makes it easier
+ -- to grab arg for optionals where appropriate
+ local i = 2
+ while i <= #arg do
+ local a = arg[i]
+ -- Terminate search for options
+ if a == "--" then
+ names = remainder(i + 1)
+ break
+ end
+ -- Look for optionals
+ local opt
+ local optarg
+ local opts
+ -- Look for and option and get it into a variable
+ if match(a, "^%-") then
+ if match(a, "^%-%-") then
+ opts = long_options
+ local pos = find(a, "=", 1, true)
+ if pos then
+ opt = sub(a, 3, pos - 1)
+ optarg = sub(a, pos + 1)
+ else
+ opt = sub(a, 3)
+ end
+ else
+ opts = short_options
+ opt = sub(a, 2, 2)
+ -- Only set optarg if it is there
+ if #a > 2 then
+ optarg = sub(a, 3)
+ end
+ end
+ -- Now check that the option is valid and sort out the argument
+ -- if required
+ local optname = opts[opt]
+ if optname then
+ -- Tidy up arguments
+ if option_list[optname]["type"] == "boolean" then
+ if optarg then
+ local opt = "-" .. (match(a, "^%-%-") and "-" or "") .. opt
+ stderr:write("Value not allowed for option " .. opt .."\n")
+ return {"help"}
+ end
+ else
+ if not optarg then
+ optarg = arg[i + 1]
+ if not optarg then
+ stderr:write("Missing value for option " .. a .."\n")
+ return {"help"}
+ end
+ i = i + 1
+ end
+ end
+ else
+ stderr:write("Unknown option " .. a .."\n")
+ return {"help"}
+ end
+ -- Store the result
+ if optarg then
+ if option_list[optname]["type"] == "string" then
+ result[optname] = optarg
+ else
+ local opts = result[optname] or { }
+ for hit in gmatch(optarg, "([^,%s]+)") do
+ insert(opts, hit)
+ end
+ result[optname] = opts
+ end
+ else
+ result[optname] = true
+ end
+ i = i + 1
+ end
+ if not opt then
+ names = remainder(i)
+ break
+ end
+ end
+ if next(names) then
+ result["names"] = names
+ end
+ return result
+end
+
+options = argparse()
+
+-- Sanity check
+function check_engines()
+ if options["engine"] and not options["force"] then
+ -- Make a lookup table
+ local t = { }
+ for _, engine in pairs(checkengines) do
+ t[engine] = true
+ end
+ for _, engine in pairs(options["engine"]) do
+ if not t[engine] then
+ print("\n! Error: Engine \"" .. engine .. "\" not set up for testing!")
+ print("\n Valid values are:")
+ for _, engine in ipairs(checkengines) do
+ print(" - " .. engine)
+ end
+ print("")
+ exit(1)
+ end
+ end
+ end
+end
diff --git a/macros/latex/contrib/l3build/l3build-aux.lua b/macros/latex/contrib/l3build/l3build-aux.lua
new file mode 100644
index 0000000000..2146d9a142
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-aux.lua
@@ -0,0 +1,126 @@
+--[[
+
+File l3build-aux.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local match = string.match
+
+local pairs = pairs
+local print = print
+
+local lookup = kpse.lookup
+
+--
+-- Auxiliary functions which are used by more than one main function
+--
+
+function normalise_epoch()
+ -- If given as an ISO date, turn into an epoch number
+ local y, m, d = match(epoch, "^(%d%d%d%d)-(%d%d)-(%d%d)$")
+ if y then
+ epoch =
+ os_time({year = y, month = m, day = d, hour = 0, sec = 0, isdst = nil}) -
+ os_time({year = 1970, month = 1, day = 1, hour = 0, sec = 0, isdst = nil})
+ elseif match(epoch, "^%d+$") then
+ epoch = tonumber(epoch)
+ else
+ epoch = 0
+ end
+end
+
+function setepoch()
+ return
+ os_setenv .. " SOURCE_DATE_EPOCH=" .. epoch
+ .. os_concat ..
+ os_setenv .. " SOURCE_DATE_EPOCH_TEX_PRIMITIVES=1"
+ .. os_concat ..
+ os_setenv .. " FORCE_SOURCE_DATE=1"
+ .. os_concat
+end
+
+local function getscriptname()
+ if match(arg[0], "l3build$") or match(arg[0], "l3build%.lua$") then
+ return lookup("l3build.lua")
+ else
+ return arg[0]
+ end
+end
+
+-- Do some subtarget for all modules in a bundle
+function call(dirs, target, opts)
+ -- Turn the option table into a string
+ local opts = opts or options
+ local s = ""
+ for k,v in pairs(opts) do
+ if k ~= "names" and k ~= "target" then -- Special cases
+ local t = option_list[k] or { }
+ local arg = ""
+ if t["type"] == "string" then
+ arg = arg .. "=" .. v
+ end
+ if t["type"] == "table" then
+ for _,a in pairs(v) do
+ if arg == "" then
+ arg = "=" .. a -- Add the initial "=" here
+ else
+ arg = arg .. "," .. a
+ end
+ end
+ end
+ s = s .. " --" .. k .. arg
+ end
+ end
+ if opts["names"] then
+ for _,v in pairs(opts["names"]) do
+ s = s .. " " .. v
+ end
+ end
+ local scriptname = getscriptname()
+ for _,i in ipairs(dirs) do
+ local text = " for module " .. i
+ if i == "." and opts["config"] then
+ text = " with configuration " .. opts["config"][1]
+ end
+ print("Running l3build with target \"" .. target .. "\"" .. text )
+ local errorlevel = run(
+ i,
+ "texlua " .. scriptname .. " " .. target .. s
+ )
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ end
+ return 0
+end
+
+-- Unpack files needed to support testing/typesetting/unpacking
+function depinstall(deps)
+ local errorlevel
+ for _,i in ipairs(deps) do
+ print("Installing dependency: " .. i)
+ errorlevel = run(i, "texlua " .. getscriptname() .. " unpack -q")
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ end
+ return 0
+end
diff --git a/macros/latex/contrib/l3build/l3build-check.lua b/macros/latex/contrib/l3build/l3build-check.lua
new file mode 100644
index 0000000000..c05defc792
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-check.lua
@@ -0,0 +1,990 @@
+--[[
+
+File l3build-check.lua Copyright (C) 2018,2019 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+-- Local access to functions
+local open = io.open
+local close = io.close
+local write = io.write
+local output = io.output
+
+local rnd = math.random
+
+local luatex_version = status.luatex_version
+
+local len = string.len
+local char = string.char
+local format = string.format
+local gmatch = string.gmatch
+local gsub = string.gsub
+local match = string.match
+
+local insert = table.insert
+local sort = table.sort
+
+local utf8_char = unicode.utf8.char
+
+local exit = os.exit
+local execute = os.execute
+local remove = os.remove
+
+--
+-- Auxiliary functions which are used by more than one main function
+--
+
+-- Set up the check system files: needed for checking one or more tests and
+-- for saving the test files
+function checkinit()
+ if not options["dirty"] then
+ cleandir(testdir)
+ end
+ depinstall(checkdeps)
+ -- Copy dependencies to the test directory itself: this makes the paths
+ -- a lot easier to manage, and is important for dealing with the log and
+ -- with file input/output tests
+ for _,i in ipairs(filelist(localdir)) do
+ cp(i, localdir, testdir)
+ end
+ bundleunpack({sourcefiledir, testfiledir})
+ for _,i in ipairs(installfiles) do
+ cp(i, unpackdir, testdir)
+ end
+ for _,i in ipairs(checkfiles) do
+ cp(i, unpackdir, testdir)
+ end
+ if direxists(testsuppdir) then
+ for _,i in ipairs(filelist(testsuppdir)) do
+ cp(i, testsuppdir, testdir)
+ end
+ end
+ for _,i in ipairs(checksuppfiles) do
+ cp(i, supportdir, testdir)
+ end
+ execute(os_ascii .. ">" .. testdir .. "/ascii.tcx")
+end
+
+local function rewrite(source,result,processor,...)
+ local file = assert(open(source,"rb"))
+ local content = gsub(file:read("*all") .. "\n","\r\n","\n")
+ close(file)
+ local new_content = processor(content,...)
+ local newfile = open(result,"w")
+ output(newfile)
+ write(new_content)
+ close(newfile)
+end
+
+-- Convert the raw log file into one for comparison/storage: keeps only
+-- the 'business' part from the tests and removes system-dependent stuff
+local function normalize_log(content,engine,errlevels)
+ local maxprintline = maxprintline
+ if match(engine,"^lua") or match(engine,"^harf") then
+ maxprintline = maxprintline + 1 -- Deal with an out-by-one error
+ end
+ local function killcheck(line)
+ -- Skip \openin/\openout lines in web2c 7.x
+ -- As Lua doesn't allow "(in|out)", a slightly complex approach:
+ -- do a substitution to check the line is exactly what is required!
+ if match(gsub(line, "^\\openin", "\\openout"), "^\\openout%d%d? = ") then
+ return true
+ end
+ return false
+ end
+ -- Substitutions to remove some non-useful changes
+ local function normalize(line,lastline,drop_fd)
+ if drop_fd then
+ if match(line," *%)") then
+ return "",""
+ else
+ return "","",true
+ end
+ end
+ -- Zap line numbers from \show, \showbox, \box_show and the like:
+ -- do this before wrapping lines
+ line = gsub(line, "^l%.%d+ ", "l. ...")
+ -- Also from lua stack traces.
+ line = gsub(line, "lua:%d+: in function", "lua:...: in function")
+ -- Allow for wrapped lines: preserve the content and wrap
+ -- Skip lines that have an explicit marker for truncation
+ if len(line) == maxprintline and
+ not match(line, "%.%.%.$") then
+ return "", (lastline or "") .. line
+ end
+ local line = (lastline or "") .. line
+ lastline = ""
+ -- Zap ./ at begin of filename
+ line = gsub(line, "%(%.%/", "(")
+ -- Zap paths if places other than 'here' are accessible
+ if checksearch then
+ -- The pattern excludes < and > as the image part can have
+ -- several entries on one line
+ local pattern = "%w?:?/[^ %<%>]*/([^/%(%)]*%.%w*)"
+ -- Files loaded from TeX: all start ( -- )
+ line = gsub(line, "%(" .. pattern, "(../%1")
+ -- Images
+ line = gsub(line, "<" .. pattern .. ">", "<../%1>")
+ -- luaotfload files start with keywords
+ line = gsub(line, "from " .. pattern .. "%(", "from. ./%1(")
+ line = gsub(line, ": " .. pattern .. "%)", ": ../%1)")
+ -- Deal with XeTeX specials
+ if match(line, "^%.+\\XeTeX.?.?.?file") then
+ line = gsub(line, pattern, "../%1")
+ end
+ end
+ -- Deal with dates
+ if match(line, "[^<]%d%d%d%d[/%-]%d%d[/%-]%d%d") then
+ line = gsub(line,"%d%d%d%d[/%-]%d%d[/%-]%d%d","....-..-..")
+ line = gsub(line,"v%d+%.?%d?%d?%w?","v...")
+ end
+ -- Deal with leading spaces for file and page number lines
+ line = gsub(line,"^ *%[(%d)","[%1")
+ line = gsub(line,"^ *%(","(")
+ -- Zap .fd lines: drop the first part, and skip to the end
+ if match(line, "^ *%([%.%/%w]+%.fd[^%)]*$") then
+ return "","",true
+ end
+ -- TeX90/XeTeX knows only the smaller set of dimension units
+ line = gsub(line,
+ "cm, mm, dd, cc, bp, or sp",
+ "cm, mm, dd, cc, nd, nc, bp, or sp")
+ -- On the other hand, (u)pTeX has some new units!
+ line = gsub(line,
+ "em, ex, zw, zh, in, pt, pc,",
+ "em, ex, in, pt, pc,")
+ line = gsub(line,
+ "cm, mm, dd, cc, bp, H, Q, or sp;",
+ "cm, mm, dd, cc, nd, nc, bp, or sp;")
+ -- Normalise a case where fixing a TeX bug changes the message text
+ line = gsub(line, "\\csname\\endcsname ", "\\csname\\endcsname")
+ -- Zap "on line <num>" and replace with "on line ..."
+ -- Two similar cases, Lua patterns mean we need to do them separately
+ line = gsub(line, "on line %d*", "on line ...")
+ line = gsub(line, "on input line %d*", "on input line ...")
+ -- Tidy up to ^^ notation
+ for i = 0, 31 do
+ line = gsub(line, char(i), "^^" .. char(64 + i))
+ end
+ -- Normalise register allocation to hard-coded numbers
+ -- No regex, so use a pattern plus lookup approach
+ local register_types = {
+ attribute = true,
+ box = true,
+ bytecode = true,
+ catcodetable = true,
+ count = true,
+ dimen = true,
+ insert = true,
+ language = true,
+ luabytecode = true,
+ luachunk = true,
+ luafunction = true,
+ marks = true,
+ muskip = true,
+ read = true,
+ skip = true,
+ toks = true,
+ whatsit = true,
+ write = true,
+ XeTeXcharclass = true
+ }
+ if register_types[match(line, "^\\[^%]]+=\\([a-z]+)%d+$")] then
+ line = gsub(line, "%d+$", "...")
+ end
+ -- Also deal with showing boxes
+ if match(line, "^> \\box%d+=$") or match(line, "^> \\box%d+=(void)$") then
+ line = gsub(line, "%d+=", "...=")
+ end
+ -- Remove 'normal' direction information on boxes with (u)pTeX
+ line = gsub(line, ",? yoko direction,?", "")
+ line = gsub(line, ",? yoko%(math%) direction,?", "")
+ -- Remove '\displace 0.0' lines in (u)pTeX
+ if match(line,"^%.*\\displace 0%.0$") then
+ return ""
+ end
+ -- Remove the \special line that in DVI mode keeps PDFs comparable
+ if match(line, "^%.*\\special%{pdf: docinfo << /Creator") then
+ return ""
+ end
+ if match(line, "^%.*\\special%{dvipdfmx:config") then
+ return ""
+ end
+ -- Remove the \special line possibly present in DVI mode for paper size
+ if match(line, "^%.*\\special%{papersize") then
+ return ""
+ end
+ -- Remove ConTeXt stuff
+ if match(line, "^backend >") or
+ match(line, "^close source >") or
+ match(line, "^mkiv lua stats >") or
+ match(line, "^pages >") or
+ match(line, "^system >") or
+ match(line, "^used file >") or
+ match(line, "^used option >") or
+ match(line, "^used structure >") then
+ return ""
+ end
+ -- The first time a new font is used by LuaTeX, it shows up
+ -- as being cached: make it appear loaded every time
+ line = gsub(line, "save cache:", "load cache:")
+ -- A tidy-up to keep LuaTeX and other engines in sync
+ line = gsub(line, utf8_char(127), "^^?")
+ -- Unicode engines display chars in the upper half of the 8-bit range:
+ -- tidy up to match pdfTeX if an ASCII engine is in use
+ if next(asciiengines) then
+ for i = 128, 255 do
+ line = gsub(line, utf8_char(i), "^^" .. format("%02x", i))
+ end
+ end
+ return line, lastline
+ end
+ local lastline = ""
+ local drop_fd = false
+ local new_content = ""
+ local prestart = true
+ local skipping = false
+ for line in gmatch(content, "([^\n]*)\n") do
+ if line == "START-TEST-LOG" then
+ prestart = false
+ elseif line == "END-TEST-LOG" or
+ match(line, "^Here is how much of .?.?.?TeX\'s memory you used:") then
+ break
+ elseif line == "OMIT" then
+ skipping = true
+ elseif match(line, "^%)?TIMO$") then
+ skipping = false
+ elseif not prestart and not skipping then
+ line, lastline, drop_fd = normalize(line, lastline,drop_fd)
+ if not match(line, "^ *$") and not killcheck(line) then
+ new_content = new_content .. line .. os_newline
+ end
+ end
+ end
+ if recordstatus then
+ new_content = new_content .. '***************' .. os_newline
+ for i = 1, checkruns do
+ new_content = new_content ..
+ 'Compilation ' .. i .. ' of test file completed with exit status ' ..
+ errlevels[i] .. os_newline
+ end
+ end
+ return new_content
+end
+
+-- Additional normalization for LuaTeX
+local function normalize_lua_log(content,luatex)
+ local function normalize(line, lastline, dropping)
+ -- Find \discretionary or \whatsit lines:
+ -- These may come back later
+ if match(line, "^%.+\\discretionary$") or
+ match(line, "^%.+\\discretionary %(penalty 50%)$") or
+ match(line, "^%.+\\discretionary50%|$") or
+ match(line, "^%.+\\discretionary50%| replacing $") or
+ match(line, "^%.+\\whatsit$") then
+ return "", line
+ end
+ -- For \mathon, we always need this line but the next
+ -- may be affected
+ if match(line, "^%.+\\mathon$") then
+ return line, line
+ end
+ -- LuaTeX has a flexible output box
+ line = gsub(line,"\\box\\outputbox", "\\box255")
+ -- LuaTeX identifies spaceskip glue
+ line = gsub(line,"%(\\spaceskip%) ", " ")
+ -- Remove 'display' at end of display math boxes:
+ -- LuaTeX omits this as it includes direction in all cases
+ line = gsub(line, "(\\hbox%(.*), display$", "%1")
+ -- Remove 'normal' direction information on boxes:
+ -- any bidi/vertical stuff will still show
+ line = gsub(line, ", direction TLT", "")
+ -- Find glue setting and round out the last place
+ local function round_digits(l, m)
+ return gsub(
+ l,
+ m .. " (%-?)%d+%.%d+",
+ m .. " %1"
+ .. format(
+ "%.3f",
+ match(line, m .. " %-?(%d+%.%d+)") or 0
+ )
+ )
+ end
+ if match(line, "glue set %-?%d+%.%d+") then
+ line = round_digits(line, "glue set")
+ end
+ if match(
+ line, "glue %-?%d+%.%d+ plus %-?%d+%.%d+ minus %-?%d+%.%d+$"
+ )
+ then
+ line = round_digits(line, "glue")
+ line = round_digits(line, "plus")
+ line = round_digits(line, "minus")
+ end
+ -- LuaTeX writes ^^M as a new line, which we lose
+ line = gsub(line, "%^%^M", "")
+ -- Remove U+ notation in the "Missing character" message
+ line = gsub(
+ line,
+ "Missing character: There is no (%^%^..) %(U%+(....)%)",
+ "Missing character: There is no %1"
+ )
+ -- LuaTeX from v1.07 logs kerns differently ...
+ -- This block only applies to the output of LuaTeX itself,
+ -- hence needing a flag to skip the case of the reference log
+ if luatex and
+ tonumber(luatex_version) >= 107 and
+ match(line, "^%.*\\kern") then
+ -- Re-insert the space in explicit kerns
+ if match(line, "kern%-?%d+%.%d+ *$") then
+ line = gsub(line, "kern", "kern ")
+ elseif match(line, "%(accent%)$") then
+ line = gsub(line, "kern", "kern ")
+ line = gsub(line, "%(accent%)$", "(for accent)")
+ elseif match(line, "%(italic%)$") then
+ line = gsub(line, "kern", "kern ")
+ line = gsub(line, " %(italic%)$", "")
+ else
+ line = gsub(line, " %(font%)$", "")
+ end
+ end
+ -- Changes in PDF specials
+ line = gsub(line, "\\pdfliteral origin", "\\pdfliteral")
+ -- A function to handle the box prefix part
+ local function boxprefix(s)
+ return gsub(match(s, "^(%.+)"), "%.", "%%.")
+ end
+ -- 'Recover' some discretionary data
+ if match(lastline, "^%.+\\discretionary %(penalty 50%)$") and
+ match(line, boxprefix(lastline) .. "%.= ") then
+ line = gsub(line," %(font%)$","")
+ return gsub(line, "%.= ", ""),""
+ end
+ -- Where the last line was a discretionary, looks for the
+ -- info one level in about what it represents
+ if match(lastline, "^%.+\\discretionary$") or
+ match(lastline, "^%.+\\discretionary %(penalty 50%)$") or
+ match(lastline, "^%.+\\discretionary50%|$") or
+ match(lastline, "^%.+\\discretionary50%| replacing $") then
+ local prefix = boxprefix(lastline)
+ if match(line, prefix .. "%.") or
+ match(line, prefix .. "%|") then
+ if match(lastline, " replacing $") and
+ not dropping then
+ -- Modify the return line
+ return gsub(line, "^%.", ""), lastline, true
+ else
+ return "", lastline, true
+ end
+ else
+ if dropping then
+ -- End of a \discretionary block
+ return line, ""
+ else
+ -- Not quite a normal discretionary
+ if match(lastline, "^%.+\\discretionary50%|$") then
+ lastline = gsub(lastline, "50%|$", "")
+ end
+ -- Remove some info that TeX90 lacks
+ lastline = gsub(lastline, " %(penalty 50%)$", "")
+ -- A normal (TeX90) discretionary:
+ -- add with the line break reintroduced
+ return lastline .. os_newline .. line, ""
+ end
+ end
+ end
+ -- Look for another form of \discretionary, replacing a "-"
+ pattern = "^%.+\\discretionary replacing *$"
+ if match(line, pattern) then
+ return "", line
+ else
+ if match(lastline, pattern) then
+ local prefix = boxprefix(lastline)
+ if match(line, prefix .. "%.\\kern") then
+ return gsub(line, "^%.", ""), lastline, true
+ elseif dropping then
+ return "", ""
+ else
+ return lastline .. os_newline .. line, ""
+ end
+ end
+ end
+ -- For \mathon, if the current line is an empty \hbox then
+ -- drop it
+ if match(lastline, "^%.+\\mathon$") then
+ local prefix = boxprefix(lastline)
+ if match(line, prefix .. "\\hbox%(0%.0%+0%.0%)x0%.0$") then
+ return "", ""
+ end
+ end
+ -- Various \local... things that other engines do not do:
+ -- Only remove the no-op versions
+ if match(line, "^%.+\\localpar$") or
+ match(line, "^%.+\\localinterlinepenalty=0$") or
+ match(line, "^%.+\\localbrokenpenalty=0$") or
+ match(line, "^%.+\\localleftbox=null$") or
+ match(line, "^%.+\\localrightbox=null$") then
+ return "", ""
+ end
+ -- Older LuaTeX versions set the above up as a whatsit
+ -- (at some stage this can therefore go)
+ if match(lastline, "^%.+\\whatsit$") then
+ local prefix = boxprefix(lastline)
+ if match(line, prefix .. "%.") then
+ return "", lastline, true
+ else
+ -- End of a \whatsit block
+ return line, ""
+ end
+ end
+ -- Wrap some cases that can be picked out
+ -- In some places LuaTeX does use max_print_line, then we
+ -- get into issues with different wrapping approaches
+ if len(line) == maxprintline then
+ return "", lastline .. line
+ elseif len(lastline) == maxprintline then
+ if match(line, "\\ETC%.%}$") then
+ -- If the line wrapped at \ETC we might have lost a space
+ return lastline
+ .. ((match(line, "^\\ETC%.%}$") and " ") or "")
+ .. line, ""
+ elseif match(line, "^%}%}%}$") then
+ return lastline .. line, ""
+ else
+ return lastline .. os_newline .. line, ""
+ end
+ -- Return all of the text for a wrapped (multi)line
+ elseif len(lastline) > maxprintline then
+ return lastline .. line, ""
+ end
+ -- Remove spaces at the start of lines: deals with the fact that LuaTeX
+ -- uses a different number to the other engines
+ return gsub(line, "^%s+", ""), ""
+ end
+ local new_content = ""
+ local lastline = ""
+ local dropping = false
+ for line in gmatch(content, "([^\n]*)\n") do
+ line, lastline, dropping = normalize(line, lastline, dropping)
+ if not match(line, "^ *$") then
+ new_content = new_content .. line .. os_newline
+ end
+ end
+ return new_content
+end
+
+local function normalize_pdf(content)
+ local new_content = ""
+ local stream_content = ""
+ local binary = false
+ local stream = false
+ for line in gmatch(content,"([^\n]*)\n") do
+ if stream then
+ if match(line,"endstream") then
+ stream = false
+ if binary then
+ new_content = new_content .. "[BINARY STREAM]" .. os_newline
+ else
+ new_content = new_content .. stream_content .. line .. os_newline
+ end
+ binary = false
+ else
+ for i = 0, 31 do
+ if match(line,char(i)) then
+ binary = true
+ break
+ end
+ end
+ if not binary and not match(line, "^ *$") then
+ stream_content = stream_content .. line .. os_newline
+ end
+ end
+ elseif match(line,"^stream$") then
+ binary = false
+ stream = true
+ stream_content = "stream" .. os_newline
+ elseif not match(line, "^ *$") then
+ new_content = new_content .. line .. os_newline
+ end
+ end
+ return new_content
+end
+
+-- Run one test which may have multiple engine-dependent comparisons
+-- Should create a difference file for each failed test
+function runcheck(name, hide)
+ if not testexists(name) then
+ print("Failed to find input for test " .. name)
+ return 1
+ end
+ local checkengines = checkengines
+ if options["engine"] then
+ checkengines = options["engine"]
+ end
+ -- Used for both .lvt and .pvt tests
+ local function check_and_diff(ext,engine,comp,pdftest)
+ runtest(name,engine,hide,ext,pdftest,true)
+ local errorlevel = comp(name,engine)
+ if errorlevel == 0 then
+ return errorlevel
+ end
+ if options["show-log-on-error"] then
+ showfailedlog(name)
+ end
+ if options["halt-on-error"] then
+ showfaileddiff()
+ end
+ return errorlevel
+ end
+ local errorlevel = 0
+ for _,engine in pairs(checkengines) do
+ setup_check(name,engine)
+ local errlevel = 0
+ if fileexists(testfiledir .. "/" .. name .. pvtext) then
+ errlevel = check_and_diff(pvtext,engine,compare_pdf,true)
+ else
+ errlevel = check_and_diff(lvtext,engine,compare_tlg)
+ end
+ if errlevel ~= 0 and options["halt-on-error"] then
+ return 1
+ end
+ if errlevel > errorlevel then
+ errorlevel = errlevel
+ end
+ end
+ -- Return everything
+ return errorlevel
+end
+
+function setup_check(name, engine)
+ local testname = name .. "." .. engine
+ local tlgfile = locate(
+ {testfiledir, unpackdir},
+ {testname .. tlgext, name .. tlgext}
+ )
+ local tpffile = locate(
+ {testfiledir, unpackdir},
+ {testname .. tpfext, name .. tpfext}
+ )
+ -- Attempt to generate missing reference file from expectation
+ if not (tlgfile or tpffile) then
+ if not locate({unpackdir, testfiledir}, {name .. lveext}) then
+ print(
+ "Error: failed to find " .. tlgext .. ", " .. tpfext .. " or "
+ .. lveext .. " file for " .. name .. "!"
+ )
+ exit(1)
+ end
+ runtest(name, engine, true, lveext)
+ ren(testdir, testname .. logext, testname .. tlgext)
+ else
+ -- Install comparison files found
+ for _,v in pairs({tlgfile, tpffile}) do
+ if v then
+ cp(
+ match(v, ".*/(.*)"),
+ match(v, "(.*)/.*"),
+ testdir
+ )
+ end
+ end
+ end
+end
+
+function compare_pdf(name,engine,cleanup)
+ local testname = name .. "." .. engine
+ local difffile = testdir .. "/" .. testname .. pdfext .. os_diffext
+ local pdffile = testdir .. "/" .. testname .. pdfext
+ local tpffile = locate({testdir}, {testname .. tpfext, name .. tpfext})
+ if not tpffile then
+ return 1
+ end
+ local errorlevel = execute(os_diffexe .. " "
+ .. normalize_path(tpffile .. " " .. pdffile .. " > " .. difffile))
+ if errorlevel == 0 or cleanup then
+ remove(difffile)
+ end
+ return errorlevel
+end
+
+function compare_tlg(name,engine,cleanup)
+ local errorlevel
+ local testname = name .. "." .. engine
+ local difffile = testdir .. "/" .. testname .. os_diffext
+ local logfile = testdir .. "/" .. testname .. logext
+ local tlgfile = locate({testdir}, {testname .. tlgext, name .. tlgext})
+ if not tlgfile then
+ return 1
+ end
+ -- Do additional log formatting if the engine is LuaTeX, there is no
+ -- LuaTeX-specific .tlg file and the default engine is not LuaTeX
+ if (match(engine,"^lua") or match(engine,"^harf"))
+ and not match(tlgfile, "%.luatex" .. "%" .. tlgext)
+ and not match(stdengine,"^lua")
+ then
+ local lualogfile = logfile
+ if cleanup then
+ lualogfile = testdir .. "/" .. testname .. ".tmp" .. logext
+ end
+ local luatlgfile = testdir .. "/" .. testname .. tlgext
+ rewrite(tlgfile,luatlgfile,normalize_lua_log)
+ rewrite(logfile,lualogfile,normalize_lua_log,true)
+ errorlevel = execute(os_diffexe .. " "
+ .. normalize_path(luatlgfile .. " " .. lualogfile .. " > " .. difffile))
+ if cleanup then
+ remove(lualogfile)
+ remove(luatlgfile)
+ end
+ else
+ errorlevel = execute(os_diffexe .. " "
+ .. normalize_path(tlgfile .. " " .. logfile .. " > " .. difffile))
+ end
+ if errorlevel == 0 or cleanup then
+ remove(difffile)
+ end
+ return errorlevel
+end
+
+-- Run one of the test files: doesn't check the result so suitable for
+-- both creating and verifying
+function runtest(name, engine, hide, ext, pdfmode, breakout)
+ local lvtfile = name .. (ext or lvtext)
+ cp(lvtfile, fileexists(testfiledir .. "/" .. lvtfile)
+ and testfiledir or unpackdir, testdir)
+ local checkopts = checkopts
+ local engine = engine or stdengine
+ local binary = engine
+ local format = gsub(engine,"tex$",checkformat)
+ -- Special binary/format combos
+ if specialformats[checkformat] and next(specialformats[checkformat]) then
+ local t = specialformats[checkformat]
+ if t[engine] and next(t[engine]) then
+ local t = t[engine]
+ binary = t.binary or binary
+ checkopts = t.options or checkopts
+ format = t.format or format
+ end
+ end
+ -- Finalise format string
+ if format ~= "" then
+ format = " --fmt=" .. format
+ end
+ -- Special casing for XeTeX engine
+ if match(engine, "xetex") and not pdfmode then
+ checkopts = checkopts .. " -no-pdf"
+ end
+ -- Special casing for ConTeXt
+ local function setup(file)
+ return " -jobname=" .. name .. " " .. ' "\\input ' .. file .. '" '
+ end
+ if match(checkformat,"^context$") then
+ function setup(file) return ' "' .. file .. '" ' end
+ end
+ local basename = testdir .. "/" .. name
+ local logfile = basename .. logext
+ local newfile = basename .. "." .. engine .. logext
+ local pdffile = basename .. pdfext
+ local npffile = basename .. "." .. engine .. pdfext
+ local asciiopt = ""
+ for _,i in ipairs(asciiengines) do
+ if binary == i then
+ asciiopt = "-translate-file ./ascii.tcx "
+ break
+ end
+ end
+ -- Clean out any dynamic files
+ for _,filetype in pairs(dynamicfiles) do
+ rm(testdir,filetype)
+ end
+ -- Ensure there is no stray .log file
+ rm(testdir,name .. logext)
+ local errlevels = {}
+ for i = 1, checkruns do
+ errlevels[i] = run(
+ testdir,
+ -- No use of localdir here as the files get copied to testdir:
+ -- avoids any paths in the logs
+ os_setenv .. " TEXINPUTS=." .. (checksearch and os_pathsep or "")
+ .. os_concat ..
+ os_setenv .. " LUAINPUTS=." .. (checksearch and os_pathsep or "")
+ .. os_concat ..
+ -- Avoid spurious output from (u)pTeX
+ os_setenv .. " GUESS_INPUT_KANJI_ENCODING=0"
+ .. os_concat ..
+ -- Allow for local texmf files
+ os_setenv .. " TEXMFCNF=." .. os_pathsep
+ .. os_concat ..
+ (forcecheckepoch and setepoch() or "") ..
+ -- Ensure lines are of a known length
+ os_setenv .. " max_print_line=" .. maxprintline
+ .. os_concat ..
+ binary .. format
+ .. " " .. asciiopt .. " " .. checkopts
+ .. setup(lvtfile)
+ .. (hide and (" > " .. os_null) or "")
+ .. os_concat ..
+ runtest_tasks(jobname(lvtfile))
+ )
+ -- Break the loop if the result is stable
+ if breakout and i < checkruns then
+ if pdfmode then
+ rewrite(pdffile,npffile,normalize_pdf)
+ if compare_pdf(name,engine,true) == 0 then
+ break
+ end
+ else
+ rewrite(logfile,newfile,normalize_log,engine,errlevels)
+ if compare_tlg(name,engine,true) == 0 then
+ break
+ end
+ end
+ end
+ end
+ if pdfmode and fileexists(testdir .. "/" .. name .. dviext) then
+ dvitopdf(name, testdir, engine, hide)
+ end
+ if pdfmode then
+ rewrite(pdffile,npffile,normalize_pdf)
+ else
+ rewrite(logfile,newfile,normalize_log,engine,errlevels)
+ end
+ -- Store secondary files for this engine
+ for _,filetype in pairs(auxfiles) do
+ for _,file in pairs(filelist(testdir, filetype)) do
+ if match(file,"^" .. name .. ".[^.]+$") then
+ local ext = match(file, "%.[^.]+$")
+ if ext ~= lvtext and
+ ext ~= tlgext and
+ ext ~= lveext and
+ ext ~= logext then
+ local newname = gsub(file,"(%.[^.]+)$","." .. engine .. "%1")
+ if fileexists(testdir,newname) then
+ rm(testdir,newname)
+ end
+ ren(testdir,file,newname)
+ end
+ end
+ end
+ end
+ return 0
+end
+
+-- A hook to allow additional tasks to run for the tests
+runtest_tasks = runtest_tasks or function(name)
+ return ""
+end
+
+-- Look for a test: could be in the testfiledir or the unpackdir
+function testexists(test)
+ return(locate({testfiledir, unpackdir},
+ {test .. lvtext, test .. pvtext}))
+end
+
+function check(names)
+ local errorlevel = 0
+ if testfiledir ~= "" and direxists(testfiledir) then
+ if not options["rerun"] then
+ checkinit()
+ end
+ local hide = true
+ if names and next(names) then
+ hide = false
+ end
+ names = names or { }
+ -- No names passed: find all test files
+ if not next(names) then
+ local excludenames = { }
+ for _,glob in pairs(excludetests) do
+ for _,name in pairs(filelist(testfiledir, glob .. lvtext)) do
+ excludenames[jobname(name)] = true
+ end
+ for _,name in pairs(filelist(unpackdir, glob .. lvtext)) do
+ excludenames[jobname(name)] = true
+ end
+ for _,name in pairs(filelist(testfiledir, glob .. pvtext)) do
+ excludenames[jobname(name)] = true
+ end
+ end
+ local function addname(name)
+ if not excludenames[jobname(name)] then
+ insert(names,jobname(name))
+ end
+ end
+ for _,glob in pairs(includetests) do
+ for _,name in pairs(filelist(testfiledir, glob .. lvtext)) do
+ addname(name)
+ end
+ for _,name in pairs(filelist(testfiledir, glob .. pvtext)) do
+ addname(name)
+ end
+ for _,name in pairs(filelist(unpackdir, glob .. lvtext)) do
+ if fileexists(testfiledir .. "/" .. name) then
+ print("Duplicate test file: " .. i)
+ return 1
+ end
+ addname(name)
+ end
+ end
+ sort(names)
+ -- Deal limiting range of names
+ if options["first"] then
+ local allnames = names
+ local active = false
+ local firstname = options["first"]
+ names = { }
+ for _,name in ipairs(allnames) do
+ if name == firstname then
+ active = true
+ end
+ if active then
+ insert(names,name)
+ end
+ end
+ end
+ if options["last"] then
+ local allnames = names
+ local lastname = options["last"]
+ names = { }
+ for _,name in ipairs(allnames) do
+ insert(names,name)
+ if name == lastname then
+ break
+ end
+ end
+ end
+ end
+ -- https://stackoverflow.com/a/32167188
+ local function shuffle(tbl)
+ local len, random = #tbl, rnd
+ for i = len, 2, -1 do
+ local j = random(1, i)
+ tbl[i], tbl[j] = tbl[j], tbl[i]
+ end
+ return tbl
+ end
+ if options["shuffle"] then
+ names = shuffle(names)
+ end
+ -- Actually run the tests
+ print("Running checks on")
+ local i = 0
+ for _,name in ipairs(names) do
+ i = i + 1
+ print(" " .. name .. " (" .. i.. "/" .. #names ..")")
+ local errlevel = runcheck(name, hide)
+ -- Return value must be 1 not errlevel
+ if errlevel ~= 0 then
+ if options["halt-on-error"] then
+ return 1
+ else
+ errorlevel = 1
+ -- visually show that something has failed
+ print(" --> failed\n")
+ end
+ end
+ end
+ if errorlevel ~= 0 then
+ checkdiff()
+ else
+ print("\n All checks passed\n")
+ end
+ end
+ return errorlevel
+end
+
+-- A short auxiliary to print the list of differences for check
+function checkdiff()
+ print("\n Check failed with difference files")
+ for _,i in ipairs(filelist(testdir, "*" .. os_diffext)) do
+ print(" - " .. testdir .. "/" .. i)
+ end
+ print("")
+end
+
+function showfailedlog(name)
+ print("\nCheck failed with log file")
+ for _,i in ipairs(filelist(testdir, name..".log")) do
+ print(" - " .. testdir .. "/" .. i)
+ print("")
+ local f = open(testdir .. "/" .. i,"r")
+ local content = f:read("*all")
+ close(f)
+ print("-----------------------------------------------------------------------------------")
+ print(content)
+ print("-----------------------------------------------------------------------------------")
+ end
+end
+
+function showfaileddiff()
+ print("\nCheck failed with difference file")
+ for _,i in ipairs(filelist(testdir, "*" .. os_diffext)) do
+ print(" - " .. testdir .. "/" .. i)
+ print("")
+ local f = open(testdir .. "/" .. i,"r")
+ local content = f:read("*all")
+ close(f)
+ print("-----------------------------------------------------------------------------------")
+ print(content)
+ print("-----------------------------------------------------------------------------------")
+ end
+end
+
+function save(names)
+ checkinit()
+ local engines = options["engine"] or {stdengine}
+ for _,name in pairs(names) do
+ if testexists(name) then
+ for _,engine in pairs(engines) do
+ local testengine = ((engine == stdengine and "") or "." .. engine)
+ local function save_test(test_ext,gen_ext,out_ext,pdfmode)
+ local out_file = name .. testengine .. out_ext
+ local gen_file = name .. "." .. engine .. gen_ext
+ print("Creating and copying " .. out_file)
+ runtest(name,engine,false,test_ext,pdfmode)
+ ren(testdir,gen_file,out_file)
+ cp(out_file,testdir,testfiledir)
+ if fileexists(unpackdir .. "/" .. out_file) then
+ print("Saved " .. out_ext
+ .. " file overrides unpacked version of the same name")
+ return 1
+ end
+ return 0
+ end
+ local errorlevel
+ if fileexists(testfiledir .. "/" .. name .. lvtext) then
+ errorlevel = save_test(lvtext,logext,tlgext)
+ else
+ errorlevel = save_test(pvtext,pdfext,tpfext,true)
+ end
+ if errorlevel ~=0 then return errorlevel end
+ end
+ elseif locate({unpackdir, testfiledir}, {name .. lveext}) then
+ print("Saved " .. tlgext .. " file overrides a "
+ .. lveext .. " file of the same name")
+ return 1
+ else
+ print('Test "'.. name .. '"not found')
+ return 1
+ end
+ end
+ return 0
+end
diff --git a/macros/latex/contrib/l3build/l3build-clean.lua b/macros/latex/contrib/l3build/l3build-clean.lua
new file mode 100644
index 0000000000..01927a71d0
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-clean.lua
@@ -0,0 +1,56 @@
+--[[
+
+File l3build-clean.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+-- Remove all generated files
+function clean()
+ -- To make sure that distribdir never contains any stray subdirs,
+ -- it is entirely removed then recreated rather than simply deleting
+ -- all of the files
+ local errorlevel =
+ rmdir(distribdir) +
+ mkdir(distribdir) +
+ cleandir(localdir) +
+ cleandir(testdir) +
+ cleandir(typesetdir) +
+ cleandir(unpackdir)
+ for _,i in ipairs(cleanfiles) do
+ for _,dir in pairs(remove_duplicates({maindir, sourcefiledir, docfiledir})) do
+ errorlevel = rm(dir, i) + errorlevel
+ end
+ end
+ return errorlevel
+end
+
+function bundleclean()
+ local errorlevel = call(modules, "clean")
+ for _,i in ipairs(cleanfiles) do
+ errorlevel = rm(currentdir, i) + errorlevel
+ end
+ return (
+ errorlevel +
+ rmdir(ctandir) +
+ rmdir(tdsdir)
+ )
+end
+
diff --git a/macros/latex/contrib/l3build/l3build-ctan.lua b/macros/latex/contrib/l3build/l3build-ctan.lua
new file mode 100644
index 0000000000..09b10e0eca
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-ctan.lua
@@ -0,0 +1,159 @@
+--[[
+
+File l3build-ctan.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local pairs = pairs
+local print = print
+
+local lower = string.lower
+local match = string.match
+
+-- Copy files to the main CTAN release directory
+function copyctan()
+ mkdir(ctandir .. "/" .. ctanpkg)
+ local function copyfiles(files,source)
+ if source == currentdir or flatten then
+ for _,filetype in pairs(files) do
+ cp(filetype,source,ctandir .. "/" .. ctanpkg)
+ end
+ else
+ for _,filetype in pairs(files) do
+ for file,_ in pairs(tree(source,filetype)) do
+ local path = splitpath(file)
+ local ctantarget = ctandir .. "/" .. ctanpkg .. "/"
+ .. source .. "/" .. path
+ mkdir(ctantarget)
+ cp(file,source,ctantarget)
+ end
+ end
+ end
+ end
+ for _,tab in pairs(
+ {bibfiles,demofiles,docfiles,pdffiles,scriptmanfiles,typesetlist}) do
+ copyfiles(tab,docfiledir)
+ end
+ copyfiles(sourcefiles,sourcefiledir)
+ for _,file in pairs(textfiles) do
+ cp(file, currentdir, ctandir .. "/" .. ctanpkg)
+ end
+end
+
+function bundlectan()
+ local errorlevel = install_files(tdsdir,true)
+ if errorlevel ~=0 then return errorlevel end
+ copyctan()
+ return 0
+end
+
+function ctan()
+ -- Always run tests for all engines
+ options["engine"] = nil
+ local function dirzip(dir, name)
+ local zipname = name .. ".zip"
+ local function tab_to_str(table)
+ local string = ""
+ for _,i in ipairs(table) do
+ string = string .. " " .. "\"" .. i .. "\""
+ end
+ return string
+ end
+ -- Convert the tables of files to quoted strings
+ local binfiles = tab_to_str(binaryfiles)
+ local exclude = tab_to_str(excludefiles)
+ -- First, zip up all of the text files
+ run(
+ dir,
+ zipexe .. " " .. zipopts .. " -ll ".. zipname .. " " .. "."
+ .. (
+ (binfiles or exclude) and (" -x" .. binfiles .. " " .. exclude)
+ or ""
+ )
+ )
+ -- Then add the binary ones
+ run(
+ dir,
+ zipexe .. " " .. zipopts .. " -g ".. zipname .. " " .. ". -i" ..
+ binfiles .. (exclude and (" -x" .. exclude) or "")
+ )
+ end
+ local errorlevel
+ local standalone = false
+ if bundle == "" then
+ standalone = true
+ end
+ if standalone then
+ errorlevel = call({"."},"check")
+ bundle = module
+ else
+ errorlevel = call(modules, "bundlecheck")
+ end
+ if errorlevel == 0 then
+ rmdir(ctandir)
+ mkdir(ctandir .. "/" .. ctanpkg)
+ rmdir(tdsdir)
+ mkdir(tdsdir)
+ if standalone then
+ errorlevel = install_files(tdsdir,true)
+ if errorlevel ~=0 then return errorlevel end
+ copyctan()
+ else
+ errorlevel = call(modules, "bundlectan")
+ end
+ else
+ print("\n====================")
+ print("Tests failed, zip stage skipped!")
+ print("====================\n")
+ return errorlevel
+ end
+ if errorlevel == 0 then
+ for _,i in ipairs(textfiles) do
+ for _,j in pairs({unpackdir, currentdir}) do
+ cp(i, j, ctandir .. "/" .. ctanpkg)
+ cp(i, j, tdsdir .. "/doc/" .. tdsroot .. "/" .. bundle)
+ end
+ end
+ -- Rename README if necessary
+ if ctanreadme ~= "" and not match(lower(ctanreadme),"^readme%.%w+") then
+ local newfile = "README." .. match(ctanreadme,"%.(%w+)$")
+ for _,dir in pairs({ctandir .. "/" .. ctanpkg,
+ tdsdir .. "/doc/" .. tdsroot .. "/" .. bundle}) do
+ if fileexists(dir .. "/" .. ctanreadme) then
+ rm(dir,newfile)
+ ren(dir,ctanreadme,newfile)
+ end
+ end
+ end
+ dirzip(tdsdir, ctanpkg .. ".tds")
+ if packtdszip then
+ cp(ctanpkg .. ".tds.zip", tdsdir, ctandir)
+ end
+ dirzip(ctandir, ctanzip)
+ cp(ctanzip .. ".zip", ctandir, currentdir)
+ else
+ print("\n====================")
+ print("Typesetting failed, zip stage skipped!")
+ print("====================\n")
+ end
+ return errorlevel
+end
+
diff --git a/macros/latex/contrib/l3build/l3build-file-functions.lua b/macros/latex/contrib/l3build/l3build-file-functions.lua
new file mode 100644
index 0000000000..9c7ac1fb6d
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-file-functions.lua
@@ -0,0 +1,444 @@
+--[[
+
+File l3build-file-functions.lua Copyright (C) 2018,2019 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local pairs = pairs
+local print = print
+
+local open = io.open
+
+local attributes = lfs.attributes
+local currentdir = lfs.currentdir
+local chdir = lfs.chdir
+local lfs_dir = lfs.dir
+
+local execute = os.execute
+local exit = os.exit
+local getenv = os.getenv
+local remove = os.remove
+local os_time = os.time
+local os_type = os.type
+
+local luatex_revision = status.luatex_revision
+local luatex_version = status.luatex_version
+
+local match = string.match
+local sub = string.sub
+local gmatch = string.gmatch
+local gsub = string.gsub
+
+local insert = table.insert
+
+-- Convert a file glob into a pattern for use by e.g. string.gub
+-- Based on https://github.com/davidm/lua-glob-pattern
+-- Simplified substantially: "[...]" syntax not supported as is not
+-- required by the file patterns used by the team. Also note style
+-- changes to match coding approach in rest of this file.
+--
+-- License for original globtopattern
+--[[
+
+ (c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT).
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ (end license)
+
+--]]
+function glob_to_pattern(glob)
+
+ local pattern = "^" -- pattern being built
+ local i = 0 -- index in glob
+ local char -- char at index i in glob
+
+ -- escape pattern char
+ local function escape(char)
+ return match(char, "^%w$") and char or "%" .. char
+ end
+
+ -- Convert tokens.
+ while true do
+ i = i + 1
+ char = sub(glob, i, i)
+ if char == "" then
+ pattern = pattern .. "$"
+ break
+ elseif char == "?" then
+ pattern = pattern .. "."
+ elseif char == "*" then
+ pattern = pattern .. ".*"
+ elseif char == "[" then
+ -- Ignored
+ print("[...] syntax not supported in globs!")
+ elseif char == "\\" then
+ i = i + 1
+ char = sub(glob, i, i)
+ if char == "" then
+ pattern = pattern .. "\\$"
+ break
+ end
+ pattern = pattern .. escape(char)
+ else
+ pattern = pattern .. escape(char)
+ end
+ end
+ return pattern
+end
+
+-- Detect the operating system in use
+-- Support items are defined here for cases where a single string can cover
+-- both Windows and Unix cases: more complex situations are handled inside
+-- the support functions
+os_concat = ";"
+os_null = "/dev/null"
+os_pathsep = ":"
+os_setenv = "export"
+os_yes = "printf 'y\\n%.0s' {1..200}"
+
+os_ascii = "echo \"\""
+os_cmpexe = getenv("cmpexe") or "cmp"
+os_cmpext = getenv("cmpext") or ".cmp"
+os_diffext = getenv("diffext") or ".diff"
+os_diffexe = getenv("diffexe") or "diff -c --strip-trailing-cr"
+os_grepexe = "grep"
+os_newline = "\n"
+
+if os_type == "windows" then
+ os_ascii = "@echo."
+ os_cmpexe = getenv("cmpexe") or "fc /b"
+ os_cmpext = getenv("cmpext") or ".cmp"
+ os_concat = "&"
+ os_diffext = getenv("diffext") or ".fc"
+ os_diffexe = getenv("diffexe") or "fc /n"
+ os_grepexe = "findstr /r"
+ os_newline = "\n"
+ if tonumber(luatex_version) < 100 or
+ (tonumber(luatex_version) == 100
+ and tonumber(luatex_revision) < 4) then
+ os_newline = "\r\n"
+ end
+ os_null = "nul"
+ os_pathsep = ";"
+ os_setenv = "set"
+ os_yes = "for /l %I in (1,1,200) do @echo y"
+end
+
+-- Deal with the fact that Windows and Unix use different path separators
+local function unix_to_win(path)
+ return gsub(path, "/", "\\")
+end
+
+function normalize_path(path)
+ if os_type == "windows" then
+ return unix_to_win(path)
+ end
+ return path
+end
+
+-- Return an absolute path from a relative one
+function abspath(path)
+ local oldpwd = currentdir()
+ chdir(path)
+ local result = currentdir()
+ chdir(oldpwd)
+ return escapepath(gsub(result, "\\", "/"))
+end
+
+function escapepath(path)
+ if os_type == "windows" then
+ local path,count = gsub(path,'"','')
+ if count % 2 ~= 0 then
+ print("Unbalanced quotes in path")
+ exit(0)
+ else
+ if match(path," ") then
+ return '"' .. path .. '"'
+ end
+ return path
+ end
+ else
+ path = gsub(path,"\\ ","[PATH-SPACE]")
+ path = gsub(path," ","\\ ")
+ return gsub(path,"%[PATH-SPACE%]","\\ ")
+ end
+end
+
+-- For cleaning out a directory, which also ensures that it exists
+function cleandir(dir)
+ local errorlevel = mkdir(dir)
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ return rm(dir, "*")
+end
+
+-- Copy files 'quietly'
+function cp(glob, source, dest)
+ local errorlevel
+ for i,_ in pairs(tree(source, glob)) do
+ local source = source .. "/" .. i
+ if os_type == "windows" then
+ if attributes(source)["mode"] == "directory" then
+ errorlevel = execute(
+ 'xcopy /y /e /i "' .. unix_to_win(source) .. '" "'
+ .. unix_to_win(dest .. '/' .. i) .. '" > nul'
+ )
+ else
+ errorlevel = execute(
+ 'xcopy /y "' .. unix_to_win(source) .. '" "'
+ .. unix_to_win(dest .. '/') .. '" > nul'
+ )
+ end
+ else
+ errorlevel = execute("cp -RLf '" .. source .. "' '" .. dest .. "'")
+ end
+ if errorlevel ~=0 then
+ return errorlevel
+ end
+ end
+ return 0
+end
+
+-- OS-dependent test for a directory
+function direxists(dir)
+ local errorlevel
+ if os_type == "windows" then
+ errorlevel =
+ execute("if not exist \"" .. unix_to_win(dir) .. "\" exit 1")
+ else
+ errorlevel = execute("[ -d " .. dir .. " ]")
+ end
+ if errorlevel ~= 0 then
+ return false
+ end
+ return true
+end
+
+function fileexists(file)
+ local f = open(file, "r")
+ if f ~= nil then
+ f:close()
+ return true
+ else
+ return false
+ end
+end
+
+-- Generate a table containing all file names of the given glob or all files
+-- if absent
+function filelist(path, glob)
+ local files = { }
+ local pattern
+ if glob then
+ pattern = glob_to_pattern(glob)
+ end
+ if direxists(path) then
+ for entry in lfs_dir(path) do
+ if pattern then
+ if match(entry, pattern) then
+ insert(files, entry)
+ end
+ else
+ if entry ~= "." and entry ~= ".." then
+ insert(files, entry)
+ end
+ end
+ end
+ end
+ return files
+end
+
+-- Does what filelist does, but can also glob subdirectories. In the returned
+-- table, the keys are paths relative to the given starting path, the values
+-- are their counterparts relative to the current working directory.
+function tree(path, glob)
+ local function cropdots(path)
+ return gsub(gsub(path, "^%./", ""), "/%./", "/")
+ end
+ local function always_true()
+ return true
+ end
+ local function is_dir(file)
+ return attributes(file)["mode"] == "directory"
+ end
+ local dirs = {["."] = cropdots(path)}
+ for pattern, criterion in gmatch(cropdots(glob), "([^/]+)(/?)") do
+ local criterion = criterion == "/" and is_dir or always_true
+ function fill(path, dir, table)
+ for _, file in ipairs(filelist(dir, pattern)) do
+ local fullpath = path .. "/" .. file
+ if file ~= "." and file ~= ".." and
+ fullpath ~= builddir
+ then
+ local fulldir = dir .. "/" .. file
+ if criterion(fulldir) then
+ table[fullpath] = fulldir
+ end
+ end
+ end
+ end
+ local newdirs = {}
+ if pattern == "**" then
+ while true do
+ path, dir = next(dirs)
+ if not path then
+ break
+ end
+ dirs[path] = nil
+ newdirs[path] = dir
+ fill(path, dir, dirs)
+ end
+ else
+ for path, dir in pairs(dirs) do
+ fill(path, dir, newdirs)
+ end
+ end
+ dirs = newdirs
+ end
+ return dirs
+end
+
+function remove_duplicates(a)
+ -- Return array with duplicate entries removed from input array `a`.
+
+ local uniq = {}
+ local hash = {}
+
+ for _,v in ipairs(a) do
+ if (not hash[v]) then
+ hash[v] = true
+ uniq[#uniq+1] = v
+ end
+ end
+
+ return uniq
+end
+
+function mkdir(dir)
+ if os_type == "windows" then
+ -- Windows (with the extensions) will automatically make directory trees
+ -- but issues a warning if the dir already exists: avoid by including a test
+ local dir = unix_to_win(dir)
+ return execute(
+ "if not exist " .. dir .. "\\nul " .. "mkdir " .. dir
+ )
+ else
+ return execute("mkdir -p " .. dir)
+ end
+end
+
+-- Rename
+function ren(dir, source, dest)
+ local dir = dir .. "/"
+ if os_type == "windows" then
+ local source = gsub(source, "^%.+/", "")
+ local dest = gsub(dest, "^%.+/", "")
+ return execute("ren " .. unix_to_win(dir) .. source .. " " .. dest)
+ else
+ return execute("mv " .. dir .. source .. " " .. dir .. dest)
+ end
+end
+
+-- Remove file(s) based on a glob
+function rm(source, glob)
+ for _,i in ipairs(filelist(source, glob)) do
+ rmfile(source,i)
+ end
+ -- os.remove doesn't give a sensible errorlevel
+ return 0
+end
+
+-- Remove file
+function rmfile(source, file)
+ remove(source .. "/" .. file)
+ -- os.remove doesn't give a sensible errorlevel
+ return 0
+end
+
+-- Remove a directory tree
+function rmdir(dir)
+ -- First, make sure it exists to avoid any errors
+ mkdir(dir)
+ if os_type == "windows" then
+ return execute("rmdir /s /q " .. unix_to_win(dir))
+ else
+ return execute("rm -r " .. dir)
+ end
+end
+
+-- Run a command in a given directory
+function run(dir, cmd)
+ return execute("cd " .. dir .. os_concat .. cmd)
+end
+
+-- Split a path into file and directory component
+function splitpath(file)
+ local path, name = match(file, "^(.*)/([^/]*)$")
+ if path then
+ return path, name
+ else
+ return ".", file
+ end
+end
+
+-- Arguably clearer names
+function basename(file)
+ return(select(2, splitpath(file)))
+end
+
+function dirname(file)
+ return(select(1, splitpath(file)))
+end
+
+-- Strip the extension from a file name (if present)
+function jobname(file)
+ local name = match(basename(file), "^(.*)%.")
+ return name or file
+end
+
+-- Look for files, directory by directory, and return the first existing
+function locate(dirs, names)
+ for _,i in ipairs(dirs) do
+ for _,j in ipairs(names) do
+ local path = i .. "/" .. j
+ if fileexists(path) then
+ return path
+ end
+ end
+ end
+end
diff --git a/macros/latex/contrib/l3build/l3build-help.lua b/macros/latex/contrib/l3build/l3build-help.lua
new file mode 100644
index 0000000000..8c3877595f
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-help.lua
@@ -0,0 +1,86 @@
+--[[
+
+File l3build-help.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local insert = table.insert
+local match = string.match
+local rep = string.rep
+local sort = table.sort
+
+function version()
+ print(
+ "\n" ..
+ "l3build: A testing and building system for LaTeX\n\n" ..
+ "Release " .. release_date
+ )
+end
+
+function help()
+ local function setup_list(list)
+ local longest = 0
+ for k,v in pairs(list) do
+ if k:len() > longest then
+ longest = k:len()
+ end
+ end
+ -- Sort the options
+ local t = { }
+ for k,_ in pairs(list) do
+ insert(t, k)
+ end
+ sort(t)
+ return longest,t
+ end
+
+ local scriptname = "l3build"
+ if not match(arg[0], "l3build(%.lua)$") then
+ scriptname = arg[0]
+ end
+ print("usage: " .. scriptname .. " <command> [<options>] [<names>]")
+ print("")
+ print("Valid commands are:")
+ local longest,t = setup_list(target_list)
+ for _,k in ipairs(t) do
+ local target = target_list[k]
+ local filler = rep(" ", longest - k:len() + 1)
+ if target["desc"] then
+ print(" " .. k .. filler .. target["desc"])
+ end
+ end
+ print("")
+ print("Valid options are:")
+ local longest,t = setup_list(option_list)
+ for _,k in ipairs(t) do
+ local opt = option_list[k]
+ local filler = rep(" ", longest - k:len() + 1)
+ if opt["desc"] then
+ if opt["short"] then
+ print(" --" .. k .. "|-" .. opt["short"] .. filler .. opt["desc"])
+ else
+ print(" --" .. k .. " " .. filler .. opt["desc"])
+ end
+ end
+ end
+ print("")
+ print("See l3build.pdf for further details.")
+end
diff --git a/macros/latex/contrib/l3build/l3build-install.lua b/macros/latex/contrib/l3build/l3build-install.lua
new file mode 100644
index 0000000000..d2222c8561
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-install.lua
@@ -0,0 +1,268 @@
+--[[
+
+File l3build-install.lua Copyright (C) 2018,2019 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local ipairs = ipairs
+local pairs = pairs
+local print = print
+
+local set_program = kpse.set_program_name
+local var_value = kpse.var_value
+
+local gsub = string.gsub
+local lower = string.lower
+local match = string.match
+
+local insert = table.insert
+
+local function gethome()
+ set_program("latex")
+ return abspath(options["texmfhome"] or var_value("TEXMFHOME"))
+end
+
+function uninstall()
+ local function zapdir(dir)
+ local installdir = gethome() .. "/" .. dir
+ if options["dry-run"] then
+ local files = filelist(installdir)
+ if next(files) then
+ print("\n" .. "For removal from " .. installdir .. ":")
+ for _,file in pairs(filelist(installdir)) do
+ print("- " .. file)
+ end
+ end
+ return 0
+ else
+ if direxists(installdir) then
+ return rmdir(installdir)
+ end
+ end
+ return 0
+ end
+ local function uninstall_files(dir,subdir)
+ subdir = subdir or moduledir
+ dir = dir .. "/" .. subdir
+ return zapdir(dir)
+ end
+ local errorlevel = 0
+ -- Any script man files need special handling
+ local manfiles = { }
+ for _,glob in pairs(scriptmanfiles) do
+ for file,_ in pairs(tree(docfiledir,glob)) do
+ -- Man files should have a single-digit extension: the type
+ local installdir = gethome() .. "/doc/man/man" .. match(file,".$")
+ if fileexists(installdir .. "/" .. file) then
+ if options["dry-run"] then
+ insert(manfiles,"man" .. match(file,".$") .. "/" ..
+ select(2,splitpath(file)))
+ else
+ errorlevel = errorlevel + rm(installdir,file)
+ end
+ end
+ end
+ end
+ if next(manfiles) then
+ print("\n" .. "For removal from " .. gethome() .. "/doc/man:")
+ for _,v in ipairs(manfiles) do
+ print("- " .. v)
+ end
+ end
+ errorlevel = uninstall_files("doc")
+ + uninstall_files("source")
+ + uninstall_files("tex")
+ + uninstall_files("bibtex/bst",module)
+ + uninstall_files("makeindex",module)
+ + uninstall_files("scripts",module)
+ + errorlevel
+ if errorlevel ~= 0 then return errorlevel end
+ -- Finally, clean up special locations
+ for _,location in ipairs(tdslocations) do
+ local path,glob = splitpath(location)
+ errorlevel = zapdir(path)
+ if errorlevel ~= 0 then return errorlevel end
+ end
+ return 0
+end
+
+function install_files(target,full,dry_run)
+ local function install_files(source,dir,files,subdir)
+ subdir = subdir or moduledir
+ -- For material associated with secondary tools (BibTeX, MakeIndex)
+ -- the structure needed is slightly different from those items going
+ -- into the tex/doc/source trees
+ if (dir == "makeindex" or match(dir,"$bibtex")) and module == "base" then
+ subdir = "latex"
+ end
+ dir = dir .. (subdir and ("/" .. subdir) or "")
+ local filenames = { }
+ local paths = { }
+ -- Generate a file list and include the directory
+ for _,glob_table in pairs(files) do
+ for _,glob in pairs(glob_table) do
+ for file,_ in pairs(tree(source,glob)) do
+ -- Just want the name
+ local file = gsub(file,"^%./","")
+ local matched = false
+ for _,location in ipairs(tdslocations) do
+ local path,glob = splitpath(location)
+ local pattern = glob_to_pattern(glob)
+ if match(file,pattern) then
+ insert(paths,path)
+ insert(filenames,path .. "/" .. file)
+ matched = true
+ break
+ end
+ end
+ if not matched then
+ insert(paths,dir)
+ insert(filenames,dir .. "/" .. file)
+ end
+ end
+ end
+ end
+
+ local errorlevel = 0
+ -- The target is only created if there are actual files to install
+ if next(filenames) then
+ if not dry_run then
+ for _,path in pairs(paths) do
+ errorlevel = cleandir(target .. "/" .. path)
+ if errorlevel ~= 0 then return errorlevel end
+ end
+ end
+ for _,file in ipairs(filenames) do
+ if dry_run then
+ print("- " .. file)
+ else
+ local path,file = splitpath(file)
+ errorlevel = cp(file,source,target .. "/" .. path)
+ if errorlevel ~= 0 then return errorlevel end
+ end
+ end
+ end
+ return 0
+ end
+ local errorlevel = unpack()
+ if errorlevel ~= 0 then return errorlevel end
+
+ -- Creates a 'controlled' list of files
+ local function excludelist(dir,include,exclude)
+ include = include or { }
+ exclude = exclude or { }
+ dir = dir or currentdir
+ local includelist = { }
+ local excludelist = { }
+ for _,glob_table in pairs(exclude) do
+ for _,glob in pairs(glob_table) do
+ for file,_ in pairs(tree(dir,glob)) do
+ excludelist[file] = true
+ end
+ end
+ end
+ for _,glob in pairs(include) do
+ for file,_ in pairs(tree(dir,glob)) do
+ if not excludelist[file] then
+ insert(includelist, file)
+ end
+ end
+ end
+ return includelist
+ end
+
+ local installlist = excludelist(unpackdir,installfiles,{scriptfiles})
+
+ if full then
+ errorlevel = doc()
+ if errorlevel ~= 0 then return errorlevel end
+ -- For the purposes here, any typesetting demo files need to be
+ -- part of the main typesetting list
+ local typesetfiles = typesetfiles
+ for _,glob in pairs(typesetdemofiles) do
+ insert(typesetfiles,glob)
+ end
+
+ -- Find PDF files
+ pdffiles = { }
+ for _,glob in pairs(typesetfiles) do
+ insert(pdffiles,(gsub(glob,"%.%w+$",".pdf")))
+ end
+
+ -- Set up lists: global as they are also needed to do CTAN releases
+ typesetlist = excludelist(docfiledir,typesetfiles,{sourcefiles})
+ sourcelist = excludelist(sourcefiledir,sourcefiles,
+ {bstfiles,installfiles,makeindexfiles,scriptfiles})
+
+ if dry_run then
+ print("\nFor installation inside " .. target .. ":")
+ end
+
+ errorlevel = install_files(sourcefiledir,"source",{sourcelist})
+ + install_files(docfiledir,"doc",
+ {bibfiles,demofiles,docfiles,pdffiles,textfiles,typesetlist})
+ if errorlevel ~= 0 then return errorlevel end
+
+ -- Rename README if necessary
+ if not dry_run then
+ if ctanreadme ~= "" and not match(lower(ctanreadme),"^readme%.%w+") then
+ local installdir = target .. "/doc/" .. moduledir
+ if fileexists(installdir .. "/" .. ctanreadme) then
+ ren(installdir,ctanreadme,"README." .. match(ctanreadme,"%.(%w+)$"))
+ end
+ end
+ end
+
+ -- Any script man files need special handling
+ local manfiles = { }
+ for _,glob in pairs(scriptmanfiles) do
+ for file,_ in pairs(tree(docfiledir,glob)) do
+ if dry_run then
+ insert(manfiles,"man" .. match(file,".$") .. "/" ..
+ select(2,splitpath(file)))
+ else
+ -- Man files should have a single-digit extension: the type
+ local installdir = target .. "/doc/man/man" .. match(file,".$")
+ errorlevel = errorlevel + mkdir(installdir)
+ errorlevel = errorlevel + cp(file,docfiledir,installdir)
+ end
+ end
+ end
+ if next(manfiles) then
+ for _,v in ipairs(manfiles) do
+ print("- doc/man/" .. v)
+ end
+ end
+ end
+
+ if errorlevel ~= 0 then return errorlevel end
+
+ errorlevel = install_files(unpackdir,"tex",{installlist})
+ + install_files(unpackdir,"bibtex/bst",{bstfiles},module)
+ + install_files(unpackdir,"makeindex",{makeindexfiles},module)
+ + install_files(unpackdir,"scripts",{scriptfiles},module)
+
+ return errorlevel
+end
+
+function install()
+ return install_files(gethome(),options["full"],options["dry-run"])
+end
diff --git a/macros/latex/contrib/l3build/l3build-manifest-setup.lua b/macros/latex/contrib/l3build/l3build-manifest-setup.lua
new file mode 100644
index 0000000000..fb46eb1d36
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-manifest-setup.lua
@@ -0,0 +1,364 @@
+--[[
+
+File l3build-manifest-setup.lua Copyright (C) 2017-2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+
+--[[
+ L3BUILD MANIFEST SETUP
+ ======================
+ This file contains all of the code that is easily replaceable by the user.
+ Either create a copy of this file, rename, and include alongside your `build.lua`
+ script and load it with `dofile()`, or simply copy/paste the definitions below
+ into your `build.lua` script directly.
+--]]
+
+
+--[[
+ Setup of manifest "groups"
+ --------------------------
+
+ The grouping of manifest files is broken into three subheadings:
+
+ * The development repository
+ * The TDS structure from `ctan`
+ * The CTAN structure from `ctan`
+
+ The latter two will only be produced if the `manifest` target is run *after*
+ the `ctan` target. Contrarily, if you run `clean` before `manifest` then
+ only the first grouping will be printed.
+
+ If you want to omit the files in the development repository, essentially
+ producing a minimalist manifest with only the files included for distribution,
+ make a copy of the `manifest_setup` function and delete the groups under
+ the ‘Repository manifest’ subheading below.
+--]]
+
+
+manifest_setup = manifest_setup or function()
+ local groups = {
+ {
+ subheading = "Repository manifest",
+ description = [[
+The following groups list the files included in the development repository of the package.
+Files listed with a ‘†’ marker are included in the TDS but not CTAN files, and files listed
+with ‘‡’ are included in both.
+]],
+ },
+ {
+ name = "Source files",
+ description = [[
+These are source files for a number of purposes, including the `unpack` process which
+generates the installation files of the package. Additional files included here will also
+be installed for processing such as testing.
+]],
+ files = {sourcefiles},
+ dir = sourcefiledir or maindir, -- TODO: remove "or maindir" after rebasing onto master
+ },
+ {
+ name = "Typeset documentation source files",
+ description = [[
+These files are typeset using LaTeX to produce the PDF documentation for the package.
+]],
+ files = {typesetfiles,typesetsourcefiles,typesetdemofiles},
+ },
+ {
+ name = "Documentation files",
+ description = [[
+These files form part of the documentation but are not typeset. Generally they will be
+additional input files for the typeset documentation files listed above.
+]],
+ files = {docfiles},
+ dir = docfiledir or maindir, -- TODO: remove "or maindir" after rebasing onto master
+ },
+ {
+ name = "Text files",
+ description = [[
+Plain text files included as documentation or metadata.
+]],
+ files = {textfiles},
+ skipfiledescription = true,
+ },
+ {
+ name = "Demo files",
+ description = [[
+Files included to demonstrate package functionality. These files are *not*
+typeset or compiled in any way.
+]],
+ files = {demofiles},
+ },
+ {
+ name = "Bibliography and index files",
+ description = [[
+Supplementary files used for compiling package documentation.
+]],
+ files = {bibfiles,bstfiles,makeindexfiles},
+ },
+ {
+ name = "Derived files",
+ description = [[
+The files created by ‘unpacking’ the package sources. This typically includes
+`.sty` and `.cls` files created from DocStrip `.dtx` files.
+]],
+ files = {installfiles},
+ exclude = {excludefiles,sourcefiles},
+ dir = unpackdir,
+ skipfiledescription = true,
+ },
+ {
+ name = "Typeset documents",
+ description = [[
+The output files (PDF, essentially) from typesetting the various source, demo,
+etc., package files.
+]],
+ files = {typesetfiles,typesetsourcefiles,typesetdemofiles},
+ rename = {"%.%w+$", ".pdf"},
+ skipfiledescription = true,
+ },
+ {
+ name = "Support files",
+ description = [[
+These files are used for unpacking, typesetting, or checking purposes.
+]],
+ files = {unpacksuppfiles,typesetsuppfiles,checksuppfiles},
+ dir = supportdir,
+ },
+ {
+ name = "Checking-specific support files",
+ description = [[
+Support files for checking the test suite.
+]],
+ files = {"*.*"},
+ exclude = {{".",".."},excludefiles},
+ dir = testsuppdir,
+ },
+ {
+ name = "Test files",
+ description = [[
+These files form the test suite for the package. `.lvt` or `.lte` files are the individual
+unit tests, and `.tlg` are the stored output for ensuring changes to the package produce
+the same output. These output files are sometimes shared and sometime specific for
+different engines (pdfTeX, XeTeX, LuaTeX, etc.).
+]],
+ files = {"*"..lvtext,"*"..lveext,"*"..tlgext},
+ dir = testfiledir,
+ skipfiledescription = true,
+ },
+ {
+ subheading = "TDS manifest",
+ description = [[
+The following groups list the files included in the TeX Directory Structure used to install
+the package into a TeX distribution.
+]],
+ },
+ {
+ name = "Source files (TDS)",
+ description = "All files included in the `"..module.."/source` directory.\n",
+ dir = tdsdir.."/source/"..moduledir,
+ files = {"*.*"},
+ exclude = {".",".."},
+ flag = false,
+ skipfiledescription = true,
+ },
+ {
+ name = "TeX files (TDS)",
+ description = "All files included in the `"..module.."/tex` directory.\n",
+ dir = tdsdir.."/tex/"..moduledir,
+ files = {"*.*"},
+ exclude = {".",".."},
+ flag = false,
+ skipfiledescription = true,
+ },
+ {
+ name = "Doc files (TDS)",
+ description = "All files included in the `"..module.."/doc` directory.\n",
+ dir = tdsdir.."/doc/"..moduledir,
+ files = {"*.*"},
+ exclude = {".",".."},
+ flag = false,
+ skipfiledescription = true,
+ },
+ {
+ subheading = "CTAN manifest",
+ description = [[
+The following group lists the files included in the CTAN package.
+]],
+ },
+ {
+ name = "CTAN files",
+ dir = ctandir.."/"..module,
+ files = {"*.*"},
+ exclude = {".",".."},
+ flag = false,
+ skipfiledescription = true,
+ },
+ }
+ return groups
+end
+
+--[[
+ Sorting within groups
+ ---------------------
+--]]
+
+manifest_sort_within_match = manifest_sort_within_match or function(files)
+ local f = files
+ table.sort(f)
+ return f
+end
+
+manifest_sort_within_group = manifest_sort_within_group or function(files)
+ local f = files
+ --[[
+ -- no-op by default; make your own definition to customise. E.g.:
+ table.sort(f)
+ --]]
+ return f
+end
+
+--[[
+ Writing to file
+ ---------------
+--]]
+
+manifest_write_opening = manifest_write_opening or function(filehandle)
+
+ filehandle:write("# Manifest for " .. module .. "\n\n")
+ filehandle:write([[
+This file is a listing of all files considered to be part of this package.
+It is automatically generated with `texlua build.lua manifest`.
+]])
+
+end
+
+manifest_write_subheading = manifest_write_subheading or function(filehandle,heading,description)
+
+ filehandle:write("\n\n## " .. heading .. "\n\n")
+
+ if description then
+ filehandle:write(description)
+ end
+
+end
+
+manifest_write_group_heading = manifest_write_group_heading or function (filehandle,heading,description)
+
+ filehandle:write("\n### " .. heading .. "\n\n")
+
+ if description then
+ filehandle:write(description .. "\n")
+ end
+
+end
+
+manifest_write_group_file = manifest_write_group_file or function(filehandle,filename,param)
+ --[[
+ filehandle : write file object
+ filename : the count of the filename to be written
+
+ param.dir : the directory of the file
+ param.count : the name of the file to write
+ param.filemaxchar : the maximum number of chars of all filenames in this group
+ param.flag : false OR string for indicating CTAN/TDS location
+ param.ctanfile : (boolean) if file is in CTAN dir
+ param.tdsfile : (boolean) if file is in TDS dir
+ --]]
+
+ -- no file description: plain bullet list item:
+
+ flagstr = param.flag or ""
+ filehandle:write("* " .. filename .. " " .. flagstr .. "\n")
+
+ --[[
+ -- or if you prefer an enumerated list:
+ filehandle:write(param.count..". " .. filename .. "\n")
+ --]]
+
+
+end
+
+manifest_write_group_file_descr = manifest_write_group_file_descr or function(filehandle,filename,descr,param)
+ --[[
+ filehandle : write file object
+ filename : the name of the file to write
+ descr : description of the file to write
+
+ param.dir : the directory of the file
+ param.count : the count of the filename to be written
+ param.filemaxchar : the maximum number of chars of all filenames in this group
+ param.descmaxchar : the maximum number of chars of all descriptions in this group
+ param.flag : false OR string for indicating CTAN/TDS location
+ param.ctanfile : (boolean) if file is in CTAN dir
+ param.tdsfile : (boolean) if file is in TDS dir
+ --]]
+
+ -- filename+description: Github-flavoured Markdown table
+
+ filestr = string.format(" | %-"..param.filemaxchar.."s",filename)
+ flagstr = param.flag and string.format(" | %s",param.flag) or ""
+ descstr = string.format(" | %-"..param.descmaxchar.."s",descr)
+
+ filehandle:write(filestr..flagstr..descstr.." |\n")
+
+end
+
+--[[
+ Extracting ‘descriptions’ from source files
+ -------------------------------------------
+--]]
+
+manifest_extract_filedesc = manifest_extract_filedesc or function(filehandle)
+
+ -- no-op by default; two examples below
+
+end
+
+--[[
+
+-- From the first match of a pattern in a file:
+manifest_extract_filedesc = function(filehandle)
+
+ local all_file = filehandle:read("*all")
+ local matchstr = "\\section{(.-)}"
+
+ filedesc = string.match(all_file,matchstr)
+
+ return filedesc
+end
+
+-- From the match of the 2nd line (say) of a file:
+manifest_extract_filedesc = function(filehandle)
+
+ local end_read_loop = 2
+ local matchstr = "%%%S%s+(.*)"
+ local this_line = ""
+
+ for ii = 1, end_read_loop do
+ this_line = filehandle:read("*line")
+ end
+
+ filedesc = string.match(this_line,matchstr)
+
+ return filedesc
+end
+
+]]--
diff --git a/macros/latex/contrib/l3build/l3build-manifest.lua b/macros/latex/contrib/l3build/l3build-manifest.lua
new file mode 100644
index 0000000000..3b899d55fd
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-manifest.lua
@@ -0,0 +1,280 @@
+--[[
+
+File l3build-manifest.lua Copyright (C) 2017-2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+
+--[[
+ L3BUILD MANIFEST
+ ================
+ If desired this entire function can be replaced; if not, it uses a number of
+ auxiliary functions which are included in this file.
+
+ Additional setup can be performed by replacing the functions lists in the file
+ `l3build-manifest-setup.lua`.
+--]]
+
+manifest = manifest or function()
+
+ -- build list of ctan files
+ ctanfiles = {}
+ for _,f in ipairs(filelist(ctandir.."/"..ctanpkg,"*.*")) do
+ ctanfiles[f] = true
+ end
+ tdsfiles = {}
+ for _,subdir in ipairs({"/doc/","/source/","/tex/"}) do
+ for _,f in ipairs(filelist(tdsdir..subdir..moduledir,"*.*")) do
+ tdsfiles[f] = true
+ end
+ end
+
+ local manifest_entries = manifest_setup()
+
+ for ii,_ in ipairs(manifest_entries) do
+ manifest_entries[ii] = manifest_build_list(manifest_entries[ii])
+ end
+
+ manifest_write(manifest_entries)
+
+ printline = "Manifest written to " .. manifestfile
+ print((printline:gsub(".","*"))) print(printline) print((printline:gsub(".","*")))
+
+end
+
+--[[
+ Internal Manifest functions: build_list
+ ---------------------------------------
+--]]
+
+manifest_build_list = function(entry)
+
+ if not(entry.subheading) then
+
+ entry = manifest_build_init(entry)
+
+ -- build list of excluded files
+ for _,glob_list in ipairs(entry.exclude) do
+ for _,this_glob in ipairs(glob_list) do
+ for _,this_file in ipairs(filelist(maindir,this_glob)) do
+ entry.excludes[this_file] = true
+ end
+ end
+ end
+
+ -- build list of matched files
+ for _,glob_list in ipairs(entry.files) do
+ for _,this_glob in ipairs(glob_list) do
+
+ local these_files = filelist(entry.dir,this_glob)
+ these_files = manifest_sort_within_match(these_files)
+
+ for _,this_file in ipairs(these_files) do
+ entry = manifest_build_file(entry,this_file)
+ end
+
+ entry.files_ordered = manifest_sort_within_group(entry.files_ordered)
+
+ end
+ end
+
+ end
+
+ return entry
+
+end
+
+
+manifest_build_init = function(entry)
+
+ -- currently these aren't customisable; I guess they could be?
+ local manifest_group_defaults = {
+ skipfiledescription = false ,
+ rename = false ,
+ dir = maindir ,
+ exclude = {excludefiles} ,
+ flag = true ,
+ }
+
+ -- internal data added to each group in the table that needs to be initialised
+ local manifest_group_init = {
+ N = 0 , -- # matched files
+ ND = 0 , -- # descriptions
+ matches = {} ,
+ excludes = {} ,
+ files_ordered = {} ,
+ descr = {} ,
+ Nchar_file = 4 , -- TODO: generalise
+ Nchar_descr = 11 , -- TODO: generalise
+ }
+
+ -- copy default options to each group if necessary
+ for kk,ll in pairs(manifest_group_defaults) do
+ if entry[kk] == nil then
+ entry[kk] = ll
+ end
+ -- can't use "entry[kk] = entry[kk] or ll" because false/nil are indistinguishable!
+ end
+
+ -- initialisation for internal data
+ for kk,ll in pairs(manifest_group_init) do
+ entry[kk] = ll
+ end
+
+ -- allow nested tables by requiring two levels of nesting
+ if type(entry.files[1])=="string" then
+ entry.files = {entry.files}
+ end
+ if type(entry.exclude[1])=="string" then
+ entry.exclude = {entry.exclude}
+ end
+
+ return entry
+
+end
+
+
+manifest_build_file = function(entry,this_file)
+
+ if entry.rename then
+ this_file = this_file:gsub(entry.rename[1],entry.rename[2])
+ end
+
+ if not entry.excludes[this_file] then
+
+ entry.N = entry.N+1
+ if not(entry.matches[this_file]) then
+
+ entry.matches[this_file] = true -- store the file name
+ entry.files_ordered[entry.N] = this_file -- store the file order
+ entry.Nchar_file = math.max(entry.Nchar_file,this_file:len())
+
+ end
+
+ if not(entry.skipfiledescription) then
+
+ local ff = assert(io.open(entry.dir .. "/" .. this_file, "r"))
+ this_descr = manifest_extract_filedesc(ff,this_file)
+ ff:close()
+
+ if this_descr and this_descr ~= "" then
+ entry.descr[this_file] = this_descr
+ entry.ND = entry.ND+1
+ entry.Nchar_descr = math.max(entry.Nchar_descr,this_descr:len())
+ end
+
+ end
+ end
+
+ return entry
+
+end
+
+--[[
+ Internal Manifest functions: write
+ ----------------------------------
+--]]
+
+manifest_write = function(manifest_entries)
+
+ local f = assert(io.open(manifestfile, "w"))
+ manifest_write_opening(f)
+
+ for ii,vv in ipairs(manifest_entries) do
+ if manifest_entries[ii].subheading then
+ manifest_write_subheading(f,manifest_entries[ii].subheading,manifest_entries[ii].description)
+ elseif manifest_entries[ii].N > 0 then
+ manifest_write_group(f,manifest_entries[ii])
+ end
+ end
+
+ f:close()
+
+end
+
+
+manifest_write_group = function(f,entry)
+
+ manifest_write_group_heading(f,entry.name,entry.description)
+
+ if entry.ND > 0 then
+
+ for ii,file in ipairs(entry.files_ordered) do
+ local descr = entry.descr[file] or ""
+ local param = {
+ dir = entry.dir ,
+ count = ii ,
+ filemaxchar = entry.Nchar_file ,
+ descmaxchar = entry.Nchar_descr ,
+ ctanfile = ctanfiles[file] ,
+ tdsfile = tdsfiles[file] ,
+ flag = false ,
+ }
+
+ if entry.flag then
+ param.flag = " "
+ if tdsfiles[file] and not(ctanfiles[file]) then
+ param.flag = "† "
+ elseif ctanfiles[file] then
+ param.flag = "‡ "
+ end
+ end
+
+ if ii == 1 then
+ -- header of table
+ -- TODO: generalise
+ local p = {}
+ for k,v in pairs(param) do p[k] = v end
+ p.count = -1
+ p.flag = p.flag and "Flag"
+ manifest_write_group_file_descr(f,"File","Description",p)
+ p.flag = p.flag and "--- "
+ manifest_write_group_file_descr(f,"---","---",p)
+ end
+
+ manifest_write_group_file_descr(f,file,descr,param)
+ end
+
+ else
+
+ for ii,file in ipairs(entry.files_ordered) do
+ local param = {
+ dir = entry.dir ,
+ count = ii ,
+ filemaxchar = entry.Nchar_file ,
+ ctanfile = ctanfiles[file] ,
+ tdsfile = tdsfiles[file] ,
+ }
+ if entry.flag then
+ param.flag = ""
+ if tdsfiles[file] and not(ctanfiles[file]) then
+ param.flag = "†"
+ elseif ctanfiles[file] then
+ param.flag = "‡"
+ end
+ end
+ manifest_write_group_file(f,file,param)
+ end
+
+ end
+
+end
+
diff --git a/macros/latex/contrib/l3build/l3build-stdmain.lua b/macros/latex/contrib/l3build/l3build-stdmain.lua
new file mode 100644
index 0000000000..668b2d02d0
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-stdmain.lua
@@ -0,0 +1,185 @@
+--[[
+
+File l3build-stdmain.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local exit = os.exit
+local insert = table.insert
+
+-- List all modules
+function listmodules()
+ local modules = { }
+ local exclmodules = exclmodules or { }
+ for entry in lfs.dir(".") do
+ if entry ~= "." and entry ~= ".." then
+ local attr = lfs.attributes(entry)
+ assert(type(attr) == "table")
+ if attr.mode == "directory" then
+ if not exclmodules[entry] then
+ insert(modules, entry)
+ end
+ end
+ end
+ end
+ return modules
+end
+
+target_list =
+ {
+ -- Some hidden targets
+ bundlecheck =
+ {
+ func = check,
+ pre = function()
+ if names then
+ print("Bundle checks should not list test names")
+ help()
+ exit(1)
+ end
+ return 0
+ end
+ },
+ bundlectan =
+ {
+ func = bundlectan
+ },
+ bundleunpack =
+ {
+ func = bundleunpack,
+ pre = function() return(depinstall(unpackdeps)) end
+ },
+ -- Public targets
+ check =
+ {
+ bundle_target = true,
+ desc = "Run all automated tests",
+ func = check,
+ },
+ clean =
+ {
+ bundle_func = bundleclean,
+ desc = "Clean out directory tree",
+ func = clean
+ },
+ ctan =
+ {
+ bundle_func = ctan,
+ desc = "Create CTAN-ready archive",
+ func = ctan
+ },
+ doc =
+ {
+ desc = "Typesets all documentation files",
+ func = doc
+ },
+ install =
+ {
+ desc = "Installs files into the local textmf tree",
+ func = install
+ },
+ manifest =
+ {
+ desc = "Creates a manifest file",
+ func = manifest
+ },
+ save =
+ {
+ desc = "Saves test validation log",
+ func = save
+ },
+ tag =
+ {
+ bundle_func = function(names)
+ local modules = modules or listmodules()
+ local errorlevel = call(modules,"tag")
+ -- Deal with any files in the bundle dir itself
+ if errorlevel == 0 then
+ errorlevel = tag(names)
+ end
+ return errorlevel
+ end,
+ desc = "Updates release tags in files",
+ func = tag,
+ pre = function(names)
+ if names and #names > 1 then
+ print("Too many tags specified; exactly one required")
+ exit(1)
+ end
+ return 0
+ end
+ },
+ uninstall =
+ {
+ desc = "Uninstalls files from the local textmf tree",
+ func = uninstall
+ },
+ unpack=
+ {
+ bundle_target = true,
+ desc = "Unpacks the source files into the build tree",
+ func = unpack
+ },
+ upload =
+ {
+ desc = "Send archive to CTAN for public release",
+ func = upload
+ },
+ }
+
+--
+-- The overall main function
+--
+
+function stdmain(target,names)
+ -- Deal with unknown targets up-front
+ if not target_list[target] then
+ help()
+ exit(1)
+ end
+ local errorlevel = 0
+ if module == "" then
+ modules = modules or listmodules()
+ if target_list[target].bundle_func then
+ errorlevel = target_list[target].bundle_func(names)
+ else
+ -- Detect all of the modules
+ if target_list[target].bundle_target then
+ target = "bundle" .. target
+ end
+ errorlevel = call(modules,target)
+ end
+ else
+ if target_list[target].pre then
+ errorlevel = target_list[target].pre(names)
+ if errorlevel ~= 0 then
+ exit(1)
+ end
+ end
+ errorlevel = target_list[target].func(names)
+ end
+ -- All done, finish up
+ if errorlevel ~= 0 then
+ exit(1)
+ else
+ exit(0)
+ end
+end
diff --git a/macros/latex/contrib/l3build/l3build-tagging.lua b/macros/latex/contrib/l3build/l3build-tagging.lua
new file mode 100644
index 0000000000..2c0c1641a4
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-tagging.lua
@@ -0,0 +1,84 @@
+--[[
+
+File l3build-tagging.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local pairs = pairs
+local open = io.open
+local os_date = os.date
+local match = string.match
+local gsub = string.gsub
+
+update_tag = update_tag or function(filename,content,tagname,tagdate)
+ return content
+end
+
+function tag_hook(tagname,tagdate)
+ return 0
+end
+
+local function update_file_tag(file,tagname,tagdate)
+ local filename = basename(file)
+ print("Tagging ".. filename)
+ local f = assert(open(file,"rb"))
+ local content = f:read("*all")
+ f:close()
+ -- Deal with Unix/Windows line endings
+ content = gsub(content .. (match(content,"\n$") and "" or "\n"),
+ "\r\n", "\n")
+ local updated_content = update_tag(filename,content,tagname,tagdate)
+ if content == updated_content then
+ return 0
+ else
+ local path = dirname(file)
+ ren(path,filename,filename .. ".bak")
+ local f = assert(open(file,"w"))
+ -- Convert line ends back if required during write
+ -- Watch for the second return value!
+ f:write((gsub(updated_content,"\n",os_newline)))
+ f:close()
+ rm(path,filename .. ".bak")
+ end
+ return 0
+end
+
+function tag(tagnames)
+ local tagdate = options["date"] or os_date("%Y-%m-%d")
+ local tagname = nil
+ if tagnames then
+ tagname = tagnames[1]
+ end
+ local dirs = remove_duplicates({currentdir, sourcefiledir, docfiledir})
+ local errorlevel = 0
+ for _,dir in pairs(dirs) do
+ for _,filetype in pairs(tagfiles) do
+ for file,_ in pairs(tree(dir,filetype)) do
+ errorlevel = update_file_tag(dir .. "/" .. file,tagname,tagdate)
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ end
+ end
+ end
+ return tag_hook(tagname,tagdate)
+end
+
diff --git a/macros/latex/contrib/l3build/l3build-typesetting.lua b/macros/latex/contrib/l3build/l3build-typesetting.lua
new file mode 100644
index 0000000000..eb2974cdf2
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-typesetting.lua
@@ -0,0 +1,233 @@
+--[[
+
+File l3build-typesetting.lua Copyright (C) 2018,2019 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+--
+-- Auxiliary functions for typesetting: need to be generally available
+--
+
+local ipairs = ipairs
+local pairs = pairs
+local print = print
+
+local gsub = string.gsub
+local match = string.match
+
+local os_type = os.type
+
+function dvitopdf(name, dir, engine, hide)
+ if match(engine, "^u?ptex$") then
+ run(
+ dir,
+ (forcecheckepoch and setepoch() or "") ..
+ "dvipdfmx " .. name .. dviext
+ .. (hide and (" > " .. os_null) or "")
+ )
+ else
+ run(
+ dir,
+ (forcecheckepoch and setepoch() or "") ..
+ "dvips " .. name .. dviext
+ .. (hide and (" > " .. os_null) or "")
+ .. os_concat ..
+ "ps2pdf " .. name .. psext
+ .. (hide and (" > " .. os_null) or "")
+ )
+ end
+end
+
+-- An auxiliary used to set up the environmental variables
+function runcmd(cmd,dir,vars)
+ local dir = dir or "."
+ local dir = abspath(dir)
+ local vars = vars or {}
+ -- Allow for local texmf files
+ local env = os_setenv .. " TEXMFCNF=." .. os_pathsep
+ local envpaths = "." .. os_pathsep
+ .. abspath(localdir) .. os_pathsep
+ .. dir .. (typesetsearch and os_pathsep or "")
+ -- Deal with spaces in paths
+ if os_type == "windows" and match(envpaths," ") then
+ envpaths = gsub(envpaths,'"','')
+ end
+ for _,var in pairs(vars) do
+ env = env .. os_concat .. os_setenv .. " " .. var .. "=" .. envpaths
+ end
+ return run(dir,(forcedocepoch and setepoch() or "") .. env .. os_concat .. cmd)
+end
+
+function biber(name,dir)
+ if fileexists(dir .. "/" .. name .. ".bcf") then
+ return
+ runcmd(biberexe .. " " .. biberopts .. " " .. name,dir,{"BIBINPUTS"})
+ end
+ return 0
+end
+
+function bibtex(name,dir)
+ local dir = dir or "."
+ if fileexists(dir .. "/" .. name .. ".aux") then
+ -- LaTeX always generates an .aux file, so there is a need to
+ -- look inside it for a \citation line
+ local grep
+ if os_type == "windows" then
+ grep = "\\\\"
+ else
+ grep = "\\\\\\\\"
+ end
+ if run(dir,
+ os_grepexe .. " \"^" .. grep .. "citation{\" " .. name .. ".aux > "
+ .. os_null
+ ) + run(dir,
+ os_grepexe .. " \"^" .. grep .. "bibdata{\" " .. name .. ".aux > "
+ .. os_null
+ ) == 0 then
+ return runcmd(bibtexexe .. " " .. bibtexopts .. " " .. name,dir,
+ {"BIBINPUTS","BSTINPUTS"})
+ end
+ end
+ return 0
+end
+
+function makeindex(name,dir,inext,outext,logext,style)
+ local dir = dir or "."
+ if fileexists(dir .. "/" .. name .. inext) then
+ if style == "" then style = nil end
+ return runcmd(makeindexexe .. " " .. makeindexopts
+ .. " -o " .. name .. outext
+ .. (style and (" -s " .. style) or "")
+ .. " -t " .. name .. logext .. " " .. name .. inext,
+ dir,
+ {"INDEXSTYLE"})
+ end
+ return 0
+end
+
+function tex(file,dir)
+ local dir = dir or "."
+ return runcmd(typesetexe .. " " .. typesetopts .. " \"" .. typesetcmds
+ .. "\\input " .. file .. "\"",
+ dir,{"TEXINPUTS","LUAINPUTS"})
+end
+
+local function typesetpdf(file,dir)
+ local dir = dir or "."
+ local name = jobname(file)
+ print("Typesetting " .. name)
+ local errorlevel = typeset(file,dir)
+ if errorlevel ~= 0 then
+ print(" ! Compilation failed")
+ return errorlevel
+ end
+ pdfname = name .. pdfext
+ rm(docfiledir,pdfname)
+ return cp(pdfname,dir,docfiledir)
+end
+
+typeset = typeset or function(file,dir)
+ dir = dir or "."
+ local errorlevel = tex(file,dir)
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ local name = jobname(file)
+ errorlevel = biber(name,dir) + bibtex(name,dir)
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ for i = 2,typesetruns do
+ errorlevel =
+ makeindex(name,dir,".glo",".gls",".glg",glossarystyle) +
+ makeindex(name,dir,".idx",".ind",".ilg",indexstyle) +
+ tex(file,dir)
+ if errorlevel ~= 0 then break end
+ end
+ return errorlevel
+end
+
+-- A hook to allow additional typesetting of demos
+typeset_demo_tasks = typeset_demo_tasks or function()
+ return 0
+end
+
+-- Typeset all required documents
+-- Uses a set of dedicated auxiliaries that need to be available to others
+function doc(files)
+ -- Set up
+ cleandir(typesetdir)
+ for _,filetype in pairs(
+ {bibfiles, docfiles, typesetfiles, typesetdemofiles}
+ ) do
+ for _,file in pairs(filetype) do
+ cp(file, docfiledir, typesetdir)
+ end
+ end
+ for _,file in pairs(sourcefiles) do
+ cp(file, sourcefiledir, typesetdir)
+ end
+ for _,file in pairs(typesetsuppfiles) do
+ cp(file, supportdir, typesetdir)
+ end
+ depinstall(typesetdeps)
+ unpack({sourcefiles, typesetsourcefiles}, {sourcefiledir, docfiledir})
+ -- Main loop for doc creation
+ local errorlevel = typeset_demo_tasks()
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ local done = {}
+ for _,typesetfiles in ipairs({typesetdemofiles,typesetfiles}) do
+ for _,glob in pairs(typesetfiles) do
+ for _,dir in ipairs({typesetdir,unpackdir}) do
+ for _,file in pairs(tree(dir,glob)) do
+ local path,srcname = splitpath(file)
+ local name = jobname(srcname)
+ if not done[name] then
+ local typeset = true
+ -- Allow for command line selection of files
+ if files and next(files) then
+ typeset = false
+ for _,file in pairs(files) do
+ if name == file then
+ typeset = true
+ break
+ end
+ end
+ end
+ -- Now know if we should typeset this source
+ if typeset then
+ local errorlevel = typesetpdf(srcname,path)
+ if errorlevel ~= 0 then
+ return errorlevel
+ else
+ done[name] = true
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ return 0
+end
+
diff --git a/macros/latex/contrib/l3build/l3build-unpack.lua b/macros/latex/contrib/l3build/l3build-unpack.lua
new file mode 100644
index 0000000000..89bbfaa17f
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-unpack.lua
@@ -0,0 +1,102 @@
+--[[
+
+File l3build-unpack.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local execute = os.execute
+
+-- Unpack the package files using an 'isolated' system: this requires
+-- a copy of the 'basic' DocStrip program, which is used then removed
+function unpack(sources, sourcedirs)
+ local errorlevel = depinstall(unpackdeps)
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ errorlevel = bundleunpack(sourcedirs, sources)
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ for _,i in ipairs(installfiles) do
+ errorlevel = cp(i, unpackdir, localdir)
+ if errorlevel ~= 0 then
+ return errorlevel
+ end
+ end
+ return 0
+end
+
+-- Split off from the main unpack so it can be used on a bundle and not
+-- leave only one modules files
+bundleunpack = bundleunpack or function(sourcedirs, sources)
+ local errorlevel = mkdir(localdir)
+ if errorlevel ~=0 then
+ return errorlevel
+ end
+ errorlevel = cleandir(unpackdir)
+ if errorlevel ~=0 then
+ return errorlevel
+ end
+ for _,i in ipairs(sourcedirs or {sourcefiledir}) do
+ for _,j in ipairs(sources or {sourcefiles}) do
+ for _,k in ipairs(j) do
+ errorlevel = cp(k, i, unpackdir)
+ if errorlevel ~=0 then
+ return errorlevel
+ end
+ end
+ end
+ end
+ for _,i in ipairs(unpacksuppfiles) do
+ errorlevel = cp(i, supportdir, localdir)
+ if errorlevel ~=0 then
+ return errorlevel
+ end
+ end
+ for _,i in ipairs(unpackfiles) do
+ for j,_ in pairs(tree(unpackdir, i)) do
+ -- This 'yes' business is needed to pass a series of "y\n" to
+ -- TeX if \askforoverwrite is true
+ -- That is all done using a file as it's the only way on Windows and
+ -- on Unix the "yes" command can't be used inside execute (it never
+ -- stops, which confuses Lua)
+ execute(os_yes .. ">>" .. localdir .. "/yes")
+ local path, name = splitpath(j)
+ local localdir = abspath(localdir)
+ errorlevel = run(
+ unpackdir .. "/" .. path,
+ os_setenv .. " TEXINPUTS=." .. os_pathsep
+ .. localdir .. (unpacksearch and os_pathsep or "") ..
+ os_concat ..
+ os_setenv .. " LUAINPUTS=." .. os_pathsep
+ .. localdir .. (unpacksearch and os_pathsep or "") ..
+ os_concat ..
+ unpackexe .. " " .. unpackopts .. " " .. name .. " < "
+ .. localdir .. "/yes"
+ .. (options["quiet"] and (" > " .. os_null) or "")
+ )
+ if errorlevel ~=0 then
+ return errorlevel
+ end
+ end
+ end
+ return 0
+end
diff --git a/macros/latex/contrib/l3build/l3build-upload.lua b/macros/latex/contrib/l3build/l3build-upload.lua
new file mode 100644
index 0000000000..de1899ad2c
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-upload.lua
@@ -0,0 +1,350 @@
+--[[
+
+File l3build-upload.lua Copyright (C) 2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local pairs = pairs
+local print = print
+local tostring = tostring
+
+local close = io.close
+local flush = io.flush
+local open = io.open
+local output = io.output
+local popen = io.popen
+local read = io.read
+local write = io.write
+
+local os_type = os.type
+
+local len = string.len
+local lower = string.lower
+local match = string.match
+
+-- UPLOAD()
+--
+-- takes a package configuration table and an optional boolean
+--
+-- if the upload parameter is not supplied or is not true, only package validation
+-- is used, if upload is true then package upload will be attempted if validation
+-- succeeds.
+
+-- fields are given as a string, or optionally for fields allowing multiple
+-- values, as a table of strings.
+
+-- Mandatory fields are checked in Lua
+-- Maximum string lengths are checked.
+
+-- Currently string values are not checked, eg licence names, or URL syntax.
+
+-- The input form could be used to construct a post body but
+-- luasec is not included in texlua. Instead an external program is used to post.
+-- As Windows (since April 2018) includes curl now use curl.
+-- A version using ctan-o-mat is available in the ctan-post github repo.
+
+-- the main interface is
+-- upload()
+-- with a configuration table `uploadconfig`
+
+
+local curl_debug = curl_debug or false -- to disable posting
+-- For now, this is undocumented.
+
+local ctanupload = ctanupload or "ask"
+if options["dry-run"] then
+ ctanupload = false
+end
+-- if ctanupload is nil or false, only validation is attempted
+-- if ctanupload is true the ctan upload URL will be used after validation
+-- if upload is anything else, the user will be prompted whether to upload.
+-- For now, this is undocumented. I think I would prefer to keep it always set to ask for the time being.
+
+function upload(tagnames)
+
+ local uploadfile = ctanzip..".zip"
+
+ -- Keep data local
+ local uploadconfig = uploadconfig
+
+ -- try a sensible default for the package name:
+ uploadconfig.pkg = uploadconfig.pkg or ctanpkg or nil
+
+ -- Get data from command line if appropriate
+ if options["file"] then
+ local f = open(options["file"],"r")
+ uploadconfig.announcement = assert(f:read('*a'))
+ close(f)
+ end
+ uploadconfig.announcement = options["message"] or uploadconfig.announcement or file_contents(uploadconfig.announcement_file)
+ uploadconfig.email = options["email"] or uploadconfig.email
+
+
+ uploadconfig.note = uploadconfig.note or file_contents(uploadconfig.note_file)
+
+ local tagnames = tagnames or { }
+ uploadconfig.version = tagnames[1] or uploadconfig.version
+
+ local override_update_check = false
+ if uploadconfig.update == nil then
+ uploadconfig.update = true
+ override_update_check = true
+ end
+
+ -- avoid lower level error from post command if zip file missing
+ local zip=open(trim_space(tostring(uploadfile)),"r")
+ if zip~=nil then
+ close(zip)
+ else
+ error("Missing zip file '" .. tostring(uploadfile) .. "'")
+ end
+
+ ctan_post = construct_ctan_post(uploadfile,options["debug"])
+
+
+-- curl file version
+ local curloptfile = uploadconfig.curlopt_file or (ctanzip .. ".curlopt")
+ local curlopt=open(curloptfile,"w")
+ output(curlopt)
+ write(ctan_post)
+ close(curlopt)
+
+ ctan_post=curlexe .. " --config " .. curloptfile
+
+
+if options["debug"] then
+ ctan_post = ctan_post .. ' https://httpbin.org/post'
+ fp_return = shell(ctan_post)
+ print('\n\nCURL COMMAND:')
+ print(ctan_post)
+ print("\n\nHTTP RESPONSE:")
+ print(fp_return)
+ return 1
+else
+ ctan_post = ctan_post .. ' https://ctan.org/submit/'
+end
+
+ -- call post command to validate the upload at CTAN's validate URL
+ local exit_status=0
+ local fp_return=""
+
+ -- use popen not execute so get the return body local exit_status=os.execute(ctan_post .. "validate")
+ if (curl_debug==false) then
+ print("Contacting CTAN for validation:")
+ fp_return = shell(ctan_post .. "validate")
+ else
+ fp_return="WARNING: curl_debug==true: posting disabled"
+ print(ctan_post)
+ return 1
+ end
+ if override_update_check then
+ if match(fp_return,"non%-existent%spackage") then
+ print("Package not found on CTAN; re-validating as new package:")
+ uploadconfig.update = false
+ ctan_post = construct_ctan_post(uploadfile)
+ fp_return = shell(ctan_post .. "validate")
+ end
+ end
+ if match(fp_return,"WARNING") or match(fp_return,"ERROR") then
+ exit_status=1
+ end
+
+ -- if upload requested and validation succeeded repost to the upload URL
+ if (exit_status==0 or exit_status==nil) then
+ if (ctanupload ~=nil and ctanupload ~=false and ctanupload ~= true) then
+ print("Validation successful, do you want to upload to CTAN? [y/n]" )
+ local answer=""
+ io.stdout:write("> ")
+ io.stdout:flush()
+ answer=read()
+ if(lower(answer,1,1)=="y") then
+ ctanupload=true
+ end
+ end
+ if (ctanupload==true) then
+ fp_return = shell(ctan_post .. "upload")
+-- this is just html, could save to a file
+-- or echo a cleaned up version
+ print('Response from CTAN:')
+ print(fp_return)
+ if match(fp_return,"WARNING") or match(fp_return,"ERROR") then
+ exit_status=1
+ end
+ else
+ print("CTAN validation successful")
+ end
+ else
+ error("Warnings from CTAN package validation:\n" .. fp_return)
+ end
+ return exit_status
+end
+
+
+function trim_space(s)
+ return (s:gsub("^%s*(.-)%s*$", "%1"))
+end
+
+
+function shell(s)
+ local h = assert(popen(s, 'r'))
+ t = assert(h:read('*a'))
+ h:close()
+ return t
+end
+
+function construct_ctan_post(uploadfile,debug)
+
+ -- start building the curl command:
+-- commandline ctan_post = curlexe .. " "
+ ctan_post=""
+
+ -- build up the curl command field-by-field:
+
+ -- field max desc mandatory multi
+ -- ----------------------------------------------------------------------------------------------------
+ ctan_field("announcement", uploadconfig.announcement, 8192, "Announcement", true, false )
+ ctan_field("author", uploadconfig.author, 128, "Author name", true, false )
+ ctan_field("bugtracker", uploadconfig.bugtracker, 255, "URL(s) of bug tracker", false, true )
+ ctan_field("ctanPath", uploadconfig.ctanPath, 255, "CTAN path", true, false )
+ ctan_field("description", uploadconfig.description, 4096, "Short description of package", false, false )
+ ctan_field("development", uploadconfig.development, 255, "URL(s) of development channels", false, true )
+ ctan_field("email", uploadconfig.email, 255, "Email of uploader", true, false )
+ ctan_field("home", uploadconfig.home, 255, "URL(s) of home page", false, true )
+ ctan_field("license", uploadconfig.license, 2048, "Package license(s)", true, true )
+ ctan_field("note", uploadconfig.note, 4096, "Internal note to ctan", false, false )
+ ctan_field("pkg", uploadconfig.pkg, 32, "Package name", true, false )
+ ctan_field("repository", uploadconfig.repository, 255, "URL(s) of source repositories", false, true )
+ ctan_field("summary", uploadconfig.summary, 128, "One-line summary of package", true, false )
+ ctan_field("support", uploadconfig.support, 255, "URL(s) of support channels", false, true )
+ ctan_field("topic", uploadconfig.topic, 1024, "Topic(s)", false, true )
+ ctan_field("update", uploadconfig.update, 8, "Boolean: true=update, false=new pkg", false, false )
+ ctan_field("uploader", uploadconfig.uploader, 255, "Name of uploader", true, false )
+ ctan_field("version", uploadconfig.version, 32, "Package version", true, false )
+
+ -- finish constructing the curl command:
+ local qq = '"'
+ if os_type == "windows" then
+ qq = '\"'
+ end
+-- commandline ctan_post = ctan_post .. ' --form ' .. qq .. 'file=@' .. tostring(uploadfile) .. ';filename=' .. tostring(uploadfile) .. qq
+ ctan_post = ctan_post .. '\nform="file=@' .. tostring(uploadfile) .. ';filename=' .. tostring(uploadfile) .. '"'
+
+ return ctan_post
+
+end
+
+function ctan_field(fname,fvalue,max,desc,mandatory,multi)
+ if (type(fvalue)=="table" and multi==true) then
+ for i, v in pairs(fvalue) do
+ ctan_single_field(fname,v,max,desc,mandatory and i==1)
+ end
+ else
+ ctan_single_field(fname,fvalue,max,desc,mandatory)
+ end
+end
+
+
+function ctan_single_field(fname,fvalue,max,desc,mandatory)
+ local fvalueprint = fvalue
+ if fvalue == nil then fvalueprint = '??' end
+ print('ctan-upload | ' .. fname .. ': ' ..tostring(fvalueprint))
+ if ((fvalue==nil and mandatory) or (fvalue == 'ask')) then
+ if (max < 256) then
+ fvalue=input_single_line_field(fname)
+ else
+ fvalue=input_multi_line_field(fname)
+ end
+ end
+ if (fvalue==nil or type(fvalue)~="table") then
+ local vs=trim_space(tostring(fvalue))
+ if (mandatory==true and (fvalue == nil or vs=="")) then
+ error("The field " .. fname .. " must contain " .. desc)
+ end
+ if (fvalue ~=nil and len(vs) > 0) then
+ if (max > 0 and len(vs) > max) then
+ error("The field " .. fname .. " is longer than " .. max)
+ end
+ vs = vs:gsub('"','\\"')
+ vs = vs:gsub('`','\\`')
+ vs = vs:gsub('\n','\\n')
+-- for strings on commandline version ctan_post=ctan_post .. ' --form "' .. fname .. "=" .. vs .. '"'
+ ctan_post=ctan_post .. '\nform="' .. fname .. '=' .. vs .. '"'
+ end
+ else
+ error("The value of the field '" .. fname .."' must be a scalar not a table")
+ end
+end
+
+
+-- function for interactive multiline fields
+function input_multi_line_field (name)
+ print("Enter " .. name .. " three <return> or ctrl-D to stop")
+
+ local field=""
+
+ local answer_line
+ local return_count=0
+ repeat
+ write("> ")
+ flush()
+ answer_line=read()
+ if answer_line=="" then
+ return_count=return_count+1
+ else
+ for i=1,return_count,1 do
+ field = field .. "\n"
+ end
+ return_count=0
+ if answer_line~=nil then
+ field = field .. "\n" .. answer_line
+ end
+ end
+ until (return_count==3 or answer_line==nil or answer_line=='\004')
+ return field
+end
+
+function input_single_line_field(name)
+ print("Enter " .. name )
+
+ local field=""
+
+ write("> ")
+ flush()
+ field=read()
+ return field
+end
+
+
+-- if filename is non nil and file readable return contents otherwise nil
+function file_contents (filename)
+ if filename ~= nil then
+ local f= open(filename,"r")
+ if f==nil then
+ return nil
+ else
+ local s = f:read("*all")
+ close(f)
+ return s
+ end
+ else
+ return nil
+ end
+end
diff --git a/macros/latex/contrib/l3build/l3build-variables.lua b/macros/latex/contrib/l3build/l3build-variables.lua
new file mode 100644
index 0000000000..0b3c9a2e43
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build-variables.lua
@@ -0,0 +1,205 @@
+--[[
+
+File l3build-variables.lua Copyright (C) 2018,2019 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+local exit = os.exit
+
+-- "module" is a deprecated function in Lua 5.2: as we want the name
+-- for other purposes, and it should eventually be 'free', simply
+-- remove the built-in
+if type(module) == "function" then
+ module = nil
+end
+
+-- Ensure the module and bundle exist
+module = module or ""
+bundle = bundle or ""
+
+-- Directory structure for the build system
+-- Use Unix-style path separators
+currentdir = "."
+maindir = maindir or currentdir
+
+-- Substructure for file locations
+docfiledir = docfiledir or currentdir
+sourcefiledir = sourcefiledir or currentdir
+supportdir = supportdir or maindir .. "/support"
+testfiledir = testfiledir or currentdir .. "/testfiles"
+testsuppdir = testsuppdir or testfiledir .. "/support"
+
+-- Structure within a development area
+builddir = builddir or maindir .. "/build"
+distribdir = distribdir or builddir .. "/distrib"
+localdir = localdir or builddir .. "/local"
+testdir = testdir or builddir .. "/test"
+typesetdir = typesetdir or builddir .. "/doc"
+unpackdir = unpackdir or builddir .. "/unpacked"
+
+-- Substructure for CTAN release material
+ctandir = ctandir or distribdir .. "/ctan"
+tdsdir = tdsdir or distribdir .. "/tds"
+tdsroot = tdsroot or "latex"
+
+-- Location for installation on CTAN or in TEXMFHOME
+if bundle == "" then
+ moduledir = tdsroot .. "/" .. module
+ ctanpkg = ctanpkg or module
+else
+ moduledir = tdsroot .. "/" .. bundle .. "/" .. module
+ ctanpkg = ctanpkg or bundle
+end
+
+-- File types for various operations
+-- Use Unix-style globs
+-- All of these may be set earlier, so a initialised conditionally
+auxfiles = auxfiles or {"*.aux", "*.lof", "*.lot", "*.toc"}
+bibfiles = bibfiles or {"*.bib"}
+binaryfiles = binaryfiles or {"*.pdf", "*.zip"}
+bstfiles = bstfiles or {"*.bst"}
+checkfiles = checkfiles or { }
+checksuppfiles = checksuppfiles or { }
+cleanfiles = cleanfiles or {"*.log", "*.pdf", "*.zip"}
+demofiles = demofiles or { }
+docfiles = docfiles or { }
+dynamicfiles = dynamicfiles or { }
+excludefiles = excludefiles or {"*~"}
+installfiles = installfiles or {"*.sty","*.cls"}
+makeindexfiles = makeindexfiles or {"*.ist"}
+scriptfiles = scriptfiles or { }
+scriptmanfiles = scriptmanfiles or { }
+sourcefiles = sourcefiles or {"*.dtx", "*.ins", "*-????-??-??.sty"}
+tagfiles = tagfiles or {"*.dtx"}
+textfiles = textfiles or {"*.md", "*.txt"}
+typesetdemofiles = typesetdemofiles or { }
+typesetfiles = typesetfiles or {"*.dtx"}
+typesetsuppfiles = typesetsuppfiles or { }
+typesetsourcefiles = typesetsourcefiles or { }
+unpackfiles = unpackfiles or {"*.ins"}
+unpacksuppfiles = unpacksuppfiles or { }
+
+-- Roots which should be unpacked to support unpacking/testing/typesetting
+checkdeps = checkdeps or { }
+typesetdeps = typesetdeps or { }
+unpackdeps = unpackdeps or { }
+
+-- Executable names plus following options
+typesetexe = typesetexe or "pdflatex"
+unpackexe = unpackexe or "tex"
+zipexe = zipexe or "zip"
+
+checkopts = checkopts or "-interaction=nonstopmode"
+typesetopts = typesetopts or "-interaction=nonstopmode"
+unpackopts = unpackopts or ""
+zipopts = zipopts or "-v -r -X"
+
+-- Engines for testing
+checkengines = checkengines or {"pdftex", "xetex", "luatex"}
+checkformat = checkformat or "latex"
+specialformats = specialformats or { }
+specialformats.context = specialformats.context or {
+ luatex = {binary = "context", format = ""},
+ pdftex = {binary = "texexec", format = ""},
+ xetex = {binary = "texexec", format = "", options = "--xetex"}
+ }
+specialformats.latex = specialformats.latex or {
+ etex = {format = "latex"},
+ ptex = {binary = "eptex"},
+ uptex = {binary = "euptex"}
+ }
+stdengine = stdengine or "pdftex"
+
+-- The tests themselves
+includetests = includetests or {"*"}
+excludetests = excludetests or { }
+
+-- Configs for testing
+checkconfigs = checkconfigs or {"build"}
+
+-- Enable access to trees outside of the repo
+-- As these may be set false, a more elaborate test than normal is needed
+if checksearch == nil then
+ checksearch = true
+end
+if typesetsearch == nil then
+ typesetsearch = true
+end
+if unpacksearch == nil then
+ unpacksearch = true
+end
+
+-- Additional settings to fine-tune typesetting
+glossarystyle = glossarystyle or "gglo.ist"
+indexstyle = indexstyle or "gind.ist"
+
+-- Supporting binaries and options
+biberexe = biberexe or "biber"
+biberopts = biberopts or ""
+bibtexexe = bibtexexe or "bibtex8"
+bibtexopts = bibtexopts or "-W"
+makeindexexe = makeindexexe or "makeindex"
+makeindexopts = makeindexopts or ""
+
+-- Forcing epoch
+if forcecheckepoch == nil then
+ forcecheckepoch = true
+end
+if forcedocepoch == nil then
+ forcedocepoch = false
+end
+
+-- Other required settings
+asciiengines = asciiengines or {"pdftex"}
+checkruns = checkruns or 1
+ctanreadme = ctanreadme or "README.md"
+ctanzip = ctanzip or ctanpkg .. "-ctan"
+epoch = epoch or 1463734800
+if flatten == nil then
+ flatten = true
+end
+maxprintline = maxprintline or 79
+packtdszip = packtdszip or false
+typesetcmds = typesetcmds or ""
+typesetruns = typesetruns or 2
+recordstatus = recordstatus or false
+
+-- Extensions for various file types: used to abstract out stuff a bit
+bakext = bakext or ".bak"
+dviext = dviext or ".dvi"
+logext = logext or ".log"
+lveext = lveext or ".lve"
+lvtext = lvtext or ".lvt"
+pdfext = pdfext or ".pdf"
+psext = psext or ".ps"
+pvtext = pvtext or ".pvt"
+tlgext = tlgext or ".tlg"
+tpfext = tpfext or ".tpf"
+
+-- Manifest options
+manifestfile = manifestfile or "MANIFEST.md"
+
+-- Non-standard installation locations
+tdslocations = tdslocations or { }
+
+-- Upload settings
+curlexe = curlexe or "curl"
+uploadconfig = uploadconfig or {}
diff --git a/macros/latex/contrib/l3build/l3build.1 b/macros/latex/contrib/l3build/l3build.1
new file mode 100644
index 0000000000..4addb91317
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build.1
@@ -0,0 +1,67 @@
+.TH l3build 1 "2019-08-24"
+.SH NAME
+l3build \- Checking and building packages
+.SH SYNOPSIS
+l3build <command> [<options>] [<names>]
+.SH DESCRIPTION
+The l3build system is a Lua script for building TeX packages, with particular
+emphasis on regression testing. It is written in cross-platform Lua code, so
+can be used by any modern TeX distribution with the texlua interpreter. A
+package for building with l3build can be written in any TeX dialect; its
+defaults are set up for LaTeX packages written in the DocStrip style.
+.PP
+The most commonly used l3build commands are:
+.IP check
+Run all automated tests
+.IP clean
+Clean out directory tree
+.IP doc
+Typesets all documentation files
+.IP install
+Installs files into the local texmf tree
+.IP save
+Saves test validation log
+.IP tag
+Update release tags in files
+.IP uninstall
+Uninstalls files from the local texmf tree
+.IP unpack
+Unpacks the source files into the build tree
+.SH OPTIONS
+Various options apply
+.IP --config|-c
+Sets the config(s) used for running tests
+.IP --date
+Sets the date to insert into sources
+.IP --dry-run
+Dry run for install
+.IP --engine|-e
+Sets the engine(s) to use for running test
+.IP --epoch
+Sets the epoch for tests and typesetting
+.IP --first
+Name of first test to run
+.IP --force|-f
+Force tests to run if engine is not set up
+.IP --halt-on-error|-H
+Stops running tests after the first failure
+.IP --last
+Name of last test to run
+.IP --pdf|-p
+Check/save PDF files
+.IP --quiet|-q
+Suppresses TeX output when unpacking
+.IP --rerun
+Skip setup: simply rerun tests
+.IP --shuffle
+Shuffle order of tests
+.IP --texmfhome
+Location of user texmf tree
+.SH BUGS
+.SH AUTHOR
+The LaTeX3 Project (latex-team@latex-project.org)
+.PP
+Please log issues on the GitHub homepage:
+https://github.com/latex3/l3build/issues.
+.SH SEE ALSO
+See l3build.pdf for more details.
diff --git a/macros/latex/contrib/l3build/l3build.dtx b/macros/latex/contrib/l3build/l3build.dtx
new file mode 100644
index 0000000000..e4dbf51fb6
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build.dtx
@@ -0,0 +1,2369 @@
+% \iffalse
+%
+% File l3build.dtx (C) Copyright 2014-2019 The LaTeX3 Project
+%
+% It may be distributed and/or modified under the conditions of the
+% LaTeX Project Public License (LPPL), either version 1.3c of this
+% license or (at your option) any later version. The latest version
+% of this license is in the file
+%
+% http://www.latex-project.org/lppl.txt
+%
+% This file is part of the "l3build bundle" (The Work in LPPL)
+% and all files in that bundle must be distributed together.
+%
+% -----------------------------------------------------------------------
+%
+% The development version of the bundle can be found at
+%
+% https://github.com/latex3/l3build
+%
+% for those people who are interested.
+%
+%<*driver>
+\RequirePackage{expl3}
+\documentclass[full]{l3doc}
+\renewcommand\partname{Part}
+\usepackage{multicol,needspace}
+\makeatletter
+\addtolength\columnsep{1em}
+\renewcommand\columnseprule{0.4pt}
+\def\@starttoc#1{%
+ \begin{multicols}{2}
+ \small
+ \makeatletter
+ \@input{\jobname.#1}%
+ \expandafter\newwrite\csname tf@#1\endcsname
+ \immediate\openout \csname tf@#1\endcsname \jobname.#1\relax
+ \@nobreakfalse
+ \end{multicols}}
+\makeatother
+\newenvironment{buildcmd}[1]{%
+ \bigskip\par\noindent\hspace{-\parindent}%
+ \fbox{\ttfamily \textcolor[gray]{0.5}{\$} l3build #1}%
+ \nopagebreak\smallskip\nopagebreak\par\nopagebreak\noindent\ignorespaces
+ }
+ {}
+\makeatletter
+\newcommand\luavar[1]{\@ifundefined{lua_#1}{\ERROR}{\texttt{\@nameuse{lua_#1}}}}
+\newcommand\luavarset[3]{\@namedef{lua_#1}{#2}}
+\newcommand\luavarseparator{}
+\makeatother
+\def\allluavars{
+\luavarset{module}{""}{The name of the module}
+\luavarset{bundle}{""}{The name of the bundle in which the module belongs (where relevant)}
+\luavarset{ctanpkg}{module/bundle}{Name of the CTAN package matching this module}
+\luavarseparator
+\luavarset{modules}{\{\}}{The list of all modules in a bundle (when not auto-detecting)}
+\luavarset{exclmodules}{\{\}}{Directories to be excluded from automatic module detection}
+\luavarseparator
+\luavarset{maindir} {"."} {Top level directory for the module/bundle}
+\luavarset{docfiledir} {"."} {Directory containing documentation files}
+\luavarset{sourcefiledir}{"."} {Directory containing source files}
+\luavarset{supportdir} {maindir .. "/support"} {Directory containing general support files}
+\luavarset{testfiledir}{"./testfiles"} {Directory containing test files}
+\luavarset{testsuppdir}{testfiledir .. "/support"}{Directory containing test-specific support files}
+\luavarseparator
+\luavarset{builddir} {maindir .. "/build"} {Directory for building and testing}
+\luavarset{distribdir}{builddir .. "/distrib"}{Directory for generating distribution structure}
+\luavarset{localdir} {builddir .. "/local"} {Directory for extracted files in \enquote{sandboxed} \TeX{} runs}
+\luavarset{testdir} {builddir .. "/test"} {Directory for running tests}
+\luavarset{typesetdir}{builddir .. "/doc"} {Directory for building documentation}
+\luavarset{unpackdir} {builddir .. "/unpack"} {Directory for unpacking sources}
+\luavarseparator
+\luavarset{ctandir}{distribdir .. "/ctan"}{Directory for organising files for CTAN}
+\luavarset{tdsdir} {distribdir .. "/tds"} {Directory for organised files into TDS structure}
+\luavarset{tdsroot}{"latex"}{Root directory of the TDS structure for the bundle/module to be installed into}
+\luavarseparator
+\luavarset{auxfiles} {\{"*.aux", "*.lof", "*.lot", "*.toc"\}}{Secondary files to be saved as part of running tests}
+\luavarset{bibfiles} {\{"*.bib"\}}{\BibTeX{} database files}
+\luavarset{binaryfiles} {\{"*.pdf", "*.zip"\}}{Files to be added in binary mode to zip files}
+\luavarset{bstfiles} {\{"*.bst"\}}{\BibTeX{} style files}
+\luavarset{checkfiles} {\{~\}}{Extra files unpacked purely for tests}
+\luavarset{checksuppfiles} { }{Files needed for performing regression tests}
+\luavarset{cleanfiles} {\{"*.log", "*.pdf", "*.zip"\}}{Files to delete when cleaning}
+\luavarset{demofiles} {\{\}}{Files which show how to use a module}
+\luavarset{docfiles} {\{\}}{Files which are part of the documentation but should not be typeset}
+\luavarset{dynamicfiles} {\{ \}}{Secondary files to cleared before each test is run}
+\luavarset{excludefiles} {\{"*\string~"\}}{Files to ignore entirely (default for Emacs backup files)}
+\luavarset{installfiles} {\{"*.sty","*.cls"\}}{Files to install to the \texttt{tex} area of the \texttt{texmf} tree}
+\luavarset{makeindexfiles} {\{"*.ist"\}}{MakeIndex files to be included in a TDS-style zip}
+\luavarset{scriptfiles} {\{ \}}{Files to install to the \texttt{scripts} area of the \texttt{texmf} tree}
+\luavarset{scriptmanfiles} {\{ \}}{Files to install to the \texttt{doc/man} area of the \texttt{texmf} tree}
+\luavarset{sourcefiles} {\{"*.dtx", "*.ins", "*-????-??-??.sty"\}}{Files to copy for unpacking}
+\luavarset{tagfiles} {\{"*.dtx"\}}{Files for automatic tagging}
+\luavarset{textfiles} {\{"*.md", "*.txt"\}}{Plain text files to send to CTAN as-is}
+\luavarset{typesetdemofiles} {\{\}}{Files to typeset before the documentation for inclusion in main documentation files}
+\luavarset{typesetfiles} {\{"*.dtx"\}}{Files to typeset for documentation}
+\luavarset{typesetsuppfiles} {\{\}}{Files needed to support typesetting when \enquote{sandboxed}}
+\luavarset{typesetsourcefiles}{\{\}}{Files to copy to unpacking when typesetting}
+\luavarset{unpackfiles} {\{"*.ins"\}}{Files to run to perform unpacking}
+\luavarset{unpacksuppfiles} {\{\}}{Files needed to support unpacking when \enquote{sandboxed}}
+\luavarseparator
+\luavarset{includetests}{\{"*"\}}{Test names to include when checking}
+\luavarset{excludetests}{\{\}} {Test names to exclude when checking}
+\luavarseparator
+\luavarset{checkdeps} {\{\}}{List of dependencies for running checks}
+\luavarset{typesetdeps}{\{\}}{List of dependencies for typesetting docs}
+\luavarset{unpackdeps} {\{\}}{List of dependencies for unpacking}
+\luavarseparator
+\luavarset{checkengines}{\{"pdftex", "xetex", "luatex"\}}{Engines to check with \texttt{check} by default}
+\luavarset{stdengine} {"pdftex"}{Engine to generate \texttt{.tlg} file from}
+\luavarset{checkformat} {"latex"} {Format to use for tests}
+\luavarset{specialformats}{\meta{table}} {Non-standard engine/format combinations}
+\luavarseparator
+\luavarset{checkconfigs}{\{\}}{Configurations to use for tests}
+\luavarseparator
+\luavarset{typesetexe} {"pdflatex"} {Executable for compiling \texttt{doc(s)}}
+\luavarset{unpackexe} {"tex"} {Executable for running \texttt{unpack}}
+\luavarset{zipexe} {"zip"} {Executable for creating archive with \texttt{ctan}}
+\luavarset{biberexe} {"biber"} {Biber executable}
+\luavarset{bibtexexe} {"bibtex8"} {\BibTeX{} executable}
+\luavarset{makeindexexe} {"makeindex"}{MakeIndex executable}
+\luavarset{curlexe} {"curl"} {Curl executable for \texttt{upload}}
+\luavarseparator
+\luavarset{checkopts} {"-interaction=nonstopmode"}{Options passed to engine when running checks}
+\luavarset{typesetopts}{"-interaction=nonstopmode"}{Options passed to engine when typesetting}
+\luavarset{unpackopts} {""} {Options passed to engine when unpacking}
+\luavarset{zipopts} {"-v -r -X"} {Options passed to zip program}
+\luavarset{biberopts} {""} {Biber options}
+\luavarset{bibtexopts} {"-W"} {\BibTeX{} options}
+\luavarset{makeindexopts}{""} {MakeIndex options}
+\luavarseparator
+\luavarset{checksearch} {true}{Switch to search the system \texttt{texmf} for during checking}
+\luavarset{typesetsearch}{true}{Switch to search the system \texttt{texmf} for during typesetting}
+\luavarset{unpacksearch} {true}{Switch to search the system \texttt{texmf} for during unpacking}
+\luavarseparator
+\luavarset{glossarystyle}{"gglo.ist"}{MakeIndex style file for glossary/changes creation}
+\luavarset{indexstyle} {"gind.ist"}{MakeIndex style for index creation}
+\luavarseparator
+\luavarset{forcecheckepoch}{"true"} {Force epoch when running tests}
+\luavarset{forcedocepoch} {"false"} {Force epoch when typesetting}
+\luavarseparator
+\luavarset{asciiengines}{\{"pdftex"\}}{Engines which should log as pure ASCII}
+\luavarset{checkruns} {1} {Number of runs to complete for a test before comparing the log}
+\luavarset{ctanreadme}{"README.md"} {Name of the file to send to CTAN as \texttt{README.\meta{ext}}}
+\luavarset{ctanzip}{ctanpkg ... "-ctan"}{Name of the zip file (without extension) created for upload to CTAN}
+\luavarset{epoch} {1463734800} {Epoch (Unix date) to set for test runs}
+\luavarset{flatten} {true} {Switch to flatten any source structure when sending to CTAN}
+\luavarset{maxprintline}{79} {Length of line to use in log files}
+\luavarset{packtdszip} {false} {Switch to build a TDS-style zip file for CTAN}
+\luavarset{typesetcmds} {""} {Instructions to be passed to \TeX{} when doing typesetting}
+\luavarset{typesetruns}{3} {Number of cycles of typesetting to carry out}
+\luavarset{recordstatus}{false} {Switch to include error level from test runs in \texttt{.tlg} files}
+\luavarset{manifestfile} {"MANIFEST.md"} {Filename to use for the manifest file}
+\luavarseparator
+\luavarset{tdslocations}{\{ \}}{Map for non-standard file installations}
+\luavarseparator
+\luavarset{uploadconfig} {\meta{table}} {Metadata to describe the package for CTAN (see Table~\ref{tab:upload-setup})}
+\luavarset{uploadconfig.pkg}{ctanpkg}{Name of the CTAN package}
+\luavarseparator
+\luavarset{bakext}{".bak"}{Extension of backup files}
+\luavarset{dviext}{".dvi"}{Extension of DVI files}
+\luavarset{lvtext}{".lvt"}{Extension of log-based test files}
+\luavarset{tlgext}{".tlg"}{Extension of test file output}
+\luavarset{tpfext}{".tpf"}{Extension of PDF-based test output}
+\luavarset{lveext}{".lve"}{Extension of auto-generating test file output}
+\luavarset{logext}{".log"}{Extension of checking output, before processing it into a \texttt{.tlg}}
+\luavarset{pvtext}{".pvt"}{Extension of PDF-based test files}
+\luavarset{pdfext}{".pdf"}{Extension of PDF file for checking and saving}
+\luavarset{psext} {".ps"} {Extension of PostScript files}
+}
+\allluavars
+
+
+\newcommand\luavartypeset{%
+ \begingroup
+ \frenchspacing
+ \renewcommand\luavarset[3]{
+ \texttt{##1} & \texttt{##2} & ##3 \\
+ }
+ \renewcommand\luavarseparator{\midrule}
+ \setlength\LTleft{-0.21\linewidth}
+ \begin{longtable}{@{}%
+ >{\small}
+ p{0.18\linewidth}
+ @{\hspace{0.03\linewidth}}
+ >{\footnotesize\hangindent=1em}
+ p{0.34\linewidth}
+ @{\hspace{0.03\linewidth}}
+ >{\small\raggedright\arraybackslash}
+ p{0.63\linewidth}
+ @{}}
+ \toprule
+ Variable & Default & Description \\
+ \midrule\endhead
+ \allluavars
+ \bottomrule
+ \end{longtable}
+ \endgroup
+}
+\newcommand\code{\texttt}
+\newcommand\var{\texttt}
+\usepackage[procnames]{listings}
+\lstset{
+ basicstyle=\ttfamily\small,
+ numbers=left,
+ numberstyle={\tiny\color[gray]{0.4}},
+}
+\usepackage{shortvrb}
+\usepackage{enumitem}
+\usepackage{longtable}
+\MakeShortVerb\|
+\begin{document}
+ \DocInput{\jobname.dtx}
+\end{document}
+%</driver>
+% \fi
+%
+% \title{^^A
+% The \pkg{l3build} package\\ Checking and building packages^^A
+% }
+%
+% \author{^^A
+% The \LaTeX3 Project\thanks
+% {^^A
+% E-mail:
+% \href{mailto:latex-team@latex-project.org}
+% {latex-team@latex-project.org}^^A
+% }^^A
+% }
+%
+% \date{Released 2019-08-24}
+%
+% \maketitle
+% \tableofcontents
+%
+% \begin{documentation}
+%
+% \section{The \pkg{l3build} system}
+%
+% \subsection{Introduction}
+%
+% The \pkg{l3build} system is a Lua script for building \TeX{} packages, with particular emphasis on regression testing.
+% It is written in cross-platform Lua code, so can be used by any modern \TeX{} distribution with the |texlua| interpreter.
+% Wrapper functions/binaries are distributed in the standard \TeX{} distributions so that the script can be called using
+% \texttt{l3build} on the command line; run without arguments it prints a brief synopsis of its usage.
+%
+% The \pkg{l3build} system is designed for packages written in any \TeX\ dialect; its defaults are set up for \LaTeX\ packages written in the DocStrip style. (Caveat: minimal testing has yet been performed for non-\LaTeX{} packages.)
+%
+% Test files are written as standalone \TeX{} documents using the |regression-test.tex| setup file; documentation on writing these tests is discussed in Section~\ref{sec:writing-tests}.
+%
+% Each package will define its own |build.lua| configuration file which both sets variables (such as the name of the package) and may also provide custom functions.
+%
+% \pagebreak[2]
+% A standard package layout might look something like the following:
+% \begin{Verbatim}[fontsize=\small]
+% abc/
+% abc.dtx
+% abc.ins
+% build.lua
+% README.md
+% support/
+% testfiles/
+% \end{Verbatim}
+% Most of this should look fairly self-explanatory.
+% The top level |support/| directory (optional) would contain any necessary files for compiling documentation, running regression tests, and so on.
+%
+% The \pkg{l3build} system is also capable of building and checking \emph{bundles} of packages.
+% To avoid confusion, we refer to either a standalone package or a package within a bundle as a \emph{module}.
+%
+% For example, within the \LaTeX3 project we have the \textsf{l3packages} bundle which contains the \textsf{xparse}, \textsf{xtemplate}, etc., modules.
+% These are all built and distributed as one bundle for installation, distribution \emph{via} CTAN and so forth.
+%
+% Each module in a bundle will have its own build script, and a bundle build script brings them all together.
+% A standard bundle layout would contain the following structure.
+% \begin{Verbatim}
+% mybundle/
+% build.lua
+% support/
+% yyy/ zoo/
+% build.lua build.lua
+% README.md README.md
+% testfiles/ testfiles/
+% yyy.dtx zoo.dtx
+% yyy.ins zoo.ins
+% \end{Verbatim}
+% All modules within a bundle must use the same build script name.
+%
+% In a small number of cases, the name used by CTAN for a module or bundle is
+% different from that used in the installation tree. For example, the \LaTeXe{}
+% kernel is called \pkg{latex-base} by CTAN but is located inside
+% \texttt{\meta{texmf}/tex/latex/base}. This can be handled by using
+% \var{ctanpkg} for the name required by CTAN to override the standard
+% value.
+%
+% The |testfiles/| folder is local to each module, and its layout consists of a series of regression tests with their outputs.
+% \begin{Verbatim}
+% testfiles/
+% test1.lvt
+% test1.tlg
+% ...
+% support/
+% my-test.cls
+% \end{Verbatim}
+% Again, the |support/| directory contains any files necessary to run some or all of these tests.
+%
+% When the build system runs, it creates a directory |build/| for various unpacking, compilation, and testing purposes.
+% For a module, this build folder can be in the main directory of the package itself, but for a bundle it should be common for the bundle itself and for all modules within that bundle.
+% A |build/| folder can be safety deleted; all material within is re-generated for each command of the \pkg{l3build} system.
+%
+% \subsection{Main build commands}
+%
+% In the working directory of a bundle or module, \pkg{l3build} is run by executing
+% \begin{center}
+% \texttt{l3build \meta{command} [\meta{option(s)}]}
+% \end{center}
+% where \texttt{\meta{command}} can be one of the following:
+% \begin{itemize}[noitemsep]\ttfamily
+% \item check
+% \item check \meta{name(s)}
+% \item clean
+% \item ctan
+% \item doc
+% \item doc \meta{name(s)}
+% \item install
+% \item manifest
+% \item save \meta{name(s)}
+% \item tag \meta{tag}
+% \item uninstall
+% \item unpack
+% \item upload
+% \item upload \meta{tag}
+% \end{itemize}
+% These commands are described below.
+%
+% As well as these commands, the system recognises the options
+% \begin{itemize}
+% \item |--config| (|-c|) Configuration(s) to use for testing
+% \item |--date| Date to use when tagging data
+% \item |--debug| Runs the target in debug mode (not supported by all targets)
+% \item |--dirty| Skip cleaning up of the test area
+% \item |--dry-run| Runs the \texttt{install} target but does not copy
+% any files: simply lists those that would be installed
+% \item |--email| Sets the email address for CTAN upload
+% \item |--engine| (|-e|) Sets the engine to use for testing
+% \item |--epoch| Sets the epoch for typesetting and testing
+% \item |--file| (|-F|) Take the upload announcement from the given file
+% \item |--first| Name of the first test to run
+% \item |--force| (|-f|) Force checks to run even if sanity
+% checks fail, \emph{e.g.}~when |--engine| is not given in
+% \luavar{checkengines}
+% \item |--full| Instructs the \texttt{install} target to include the
+% \texttt{doc} and \texttt{source} trees
+% \item |--halt-on-error| (|-H|) Specifies that checks
+% should stop as soon as possible, rather than running all requested
+% tests; the difference file is printed in the terminal directly in the case of failure
+% \item |--last| Name of the last test to run
+% \item |--message| (|-m|) Text for upload announcement
+% \item |--quiet| (|-q|) Suppresses output from unpacking
+% \item |--rerun| Run tests without unpacking/set up
+% \item |--show-log-on-error| To be used in addition to \texttt{--halt-on-error} and results
+% in the full \texttt{.log} file of a failed test to be shown on the console
+% \item |--shuffle| Shuffle the order in which tests run
+% \item |--texmfhome| Sets the location of the user tree for installing
+% \end{itemize}
+%
+% \begin{buildcmd}{check}
+% The |check| command runs the entire test suite.
+% This involves iterating through each \texttt{.lvt} file in the test directory (specified by the \var{testfiledir} variable), compiling each test in a \enquote{sandbox} (a directory specified by \var{testdir}), and comparing the output against each matching predefined \texttt{.tlg} file.
+%
+% If changes to the package or the typesetting environment have affected the results, the check for that file fails.
+% A |diff| of the expected to actual output should then be inspected to determine the cause of the error; it is located in the \var{testdir} directory (default \luavar{testdir}).
+%
+% On Windows, the |diff| program is not available and so |fc| is used instead
+% (generating an |.fc| file). Setting the environmental variables |diffexe|
+% and |diffext| can be used to adjust the choice of comparison made: the standard
+% values are
+% \begin{itemize}
+% \item[Windows] |diffext = fc|, |diffexe = fc /n|
+% \item[*nix] |diffext = diff|, |diffexe = diff -c --strip-trailing-cr|
+% \end{itemize}
+%
+% The following files are moved into the \enquote{sandbox} for the |check| process:
+% \begin{itemize}
+% \item all \var{installfiles} after unpacking;
+% \item all \var{checkfiles} after unpacking;
+% \item any files in the directory \var{testsuppdir};
+% \item any files that match \var{checksuppfiles} in the \var{supportdir}.
+% \end{itemize}
+% This range of possibilities allow sensible defaults but significant flexibility for defining your own test setups.
+%
+% Checking can be performed with any or all of the `engines' \texttt{pdftex}, \texttt{xetex}, and \texttt{luatex}.
+% By default, each test is executed with all three, being compared against the \texttt{.tlg} file produced from the \var{pdftex} engine (these defaults are controlled by the |checkengines| and |stdengine| variable respectively).
+% The format used for tests can be altered by setting \var{checkformat}: the default setting \texttt{latex} means that tests are run using \emph{e.g.}~\texttt{pdflatex}, whereas setting to \texttt{plain} will run tests using \emph{e.g.}~\texttt{pdftex}.
+% (Currently, this should be one of \texttt{latex} or \texttt{plain}.)
+% To perform the check, the engine typesets each test up to \var{checkruns} times.
+% More detail on this in the documentation on |save|.
+% Options passed to the binary are defined in the variable \var{checkopts}.
+%
+% By default, |texmf| trees are searched for input files when checking. This can be disabled by setting \var{checksearch} to |false|: isolation provides confidence that the tests cannot accidentally be running with incorrect files installed in the main distribution or |hometexmf|.
+% \end{buildcmd}
+%
+%
+% \begin{buildcmd}{check \meta{name(s)}}
+% Checks only the test \texttt{\meta{name(s)}.lvt}.
+% All engines specified by \var{checkengines} are tested unless the command
+% line option |--engine| (or |-e|) has been given to limit
+% testing to a single engine. Normally testing is preceded by unpacking
+% source files and copying the result plus any additional support to the
+% test directory: this may be skipped using the |-s| option.
+% \end{buildcmd}
+%
+% \begin{buildcmd}{clean}
+% This command removes all temporary files used for package bundling and regression testing.
+% In the standard layout, these are all files within the directories defined by \var{localdir}, \var{testdir}, \var{typesetdir} and \var{unpackdir}, as well as all files defined in the \var{cleanfiles} variable in the same directory as the script.
+% The defaults are |.pdf| files from typesetting (|doc|) and |.zip| files from bundling (|ctan|).
+% \end{buildcmd}
+%
+%
+% \begin{buildcmd}{ctan}
+% Creates an archive of the package and its documentation, suitable for uploading to CTAN.
+% The archive is compiled in \var{distribdir}, and if the results are successful the resultant |.zip| file is moved into the same directory as the build script.
+% If \var{packtdszip} is set true then the building process includes a |.tds.zip| file containing the `\TeX\ Directory Structure' layout of the package or bundle.
+% The archive therefore may contain two `views' of the package:
+% \begin{Verbatim}
+% abc.zip/
+% abc/
+% abc.dtx
+% abc.ins
+% abc.pdf
+% README.md
+% abc.tds.zip/
+% doc/latex/abc/
+% abc.pdf
+% README.md
+% source/latex/abc/
+% abc.dtx
+% abc.ins
+% tex/latex/abc/
+% abc.sty
+% \end{Verbatim}
+% The files copied into the archive are controlled by a number of variables.
+% The `root' of the TDS structure is defined by \var{tdsroot}, which is \luavar{tdsroot} by default. Plain users would redefine this to |"plain"| (or perhaps |"generic"|), for example.
+% The build process for a |.tds.zip| file currently assumes a `standard'
+% structure in which all extracted files should be placed inside the |tex|
+% tree in a single directory, as shown above. If the module includes any
+% \BibTeX{} or MakeIndex styles these will be placed in the appropriate
+% subtrees.
+%
+% \pagebreak[2]
+% The |doc| tree is constructed from:
+% \begin{itemize}
+% \item all files matched by \var{demofiles},
+% \item all files matched by \var{docfiles},
+% \item all files matched by \var{typesetfiles} with their extension replaced with |.pdf|,
+% \item all files matched by \var{textfiles},
+% \item all files matched by \var{bibfiles}.
+% \end{itemize}
+% The |source| tree is constructed from all files matched by \var{typesetfiles} and \var{sourcefiles}.
+% The |tex| tree from all files matched by \var{installfiles}.
+%
+% The special case \var{ctanreadme} is used to allow renaming of a local
+% |foo.xyz| file to |README.xyz|. The local |foo.xyz| should be listed in
+% \var{textfiles}, and will be renamed as part of constructing the CTAN
+% structure. The file extension will be unchanged by this process.
+%
+% Files that should always be excluded from the archive are matched against the \var{excludefiles} variable; by default this is \luavar{excludefiles}, which match Emacs' autosave files.
+%
+% Binary files should be specified with the \var{binaryfiles} variable (default \luavar{binaryfiles}); these are added to the zip archive without normalising line endings (text files are automatically converted to Unix-style line endings).
+%
+% To create the archive, by default the binary \var{zipexe} is used (\luavar{zipexe}) with options \var{zipopts} (|-v -r -X|). The intermediate build directories \var{ctandir} and \var{tdsdir} are used to construct the archive.
+% \end{buildcmd}
+%
+%
+% \begin{buildcmd}{doc}
+% Compiles documentation files in the \var{typesetdir} directory.
+% In the absence of one or more file names, all documentation is typeset;
+% a file list may be given at the command line for selective typesetting.
+% If the compilation is successful the |.pdf| is moved back into the main directory.
+%
+% The documentation compilation is performed with the \var{typesetexe} binary (default \texttt{pdflatex}), with options \var{typesetopts}.
+% Additional \TeX{} material defined in \var{typesetcmds} is passed to the document (e.g., for writing |\\PassOptionsToClass{l3doc}{letterpaper}|, and so on---note that backslashes need to be escaped in Lua strings).
+%
+% Files that match |typesetsuppfiles| in the |support| directory (|supportdir|) are copied into the |build/local| directory (|localdir|) for the typesetting compilation process.
+% Additional dependencies listed in the \var{typesetdeps} variable (empty by default) will also be installed.
+%
+% Source files specified in \var{sourcefiles} and \var{typesetsourcefiles}
+% are unpacked before the typesetting takes place. (In most cases
+% \var{typesetsourcefiles} will be empty, but may be used where there are
+% files to unpack \emph{only} for typesetting.)
+%
+% If \var{typesetsearch} is \code{true} (default), standard \texttt{texmf} search trees are used in the typesetting compilation. If set to false, \emph{all} necessary files for compilation must be included in the |build/local| sandbox.
+%
+% \end{buildcmd}
+%
+% \begin{buildcmd}{doc \meta{name(s)}}
+% Typesets only the files with the \meta{name(s)} given, which should be the
+% basename without any extension.
+% \end{buildcmd}
+%
+% \begin{buildcmd}{install}
+% Copies all package files (defined by \var{installfiles}) into the user's home \texttt{texmf} tree in the form of the \TeX\ Directory Structure.
+% The location of the user tree can be adjusted using the |--texmfhome| swtich:
+% the standard setting is the location set as |TEXMFHOME|.
+% \end{buildcmd}
+%
+% \begin{buildcmd}{save \meta{name(s)}}
+% This command runs through the same execution as |check| for a specific test(s) \texttt{\meta{name(s)}.lvt}.
+% This command saves the output of the test to a |.tlg| file.
+% This file is then used in all subsequent checks against the \texttt{\meta{name}.lvt} test.
+%
+% If the |--engine| (or |-e|) is specified (one of |pdftex|, |xetex|, or |luatex|), the saved output is stored in \texttt{\meta{name}.\meta{engine}.tlg}. This is necessary if running the test through a different engine produces a different output.
+% A normalisation process is performed when checking to avoid common differences such as register allocation; full details are listed in section~\ref{sec:norm}.
+%
+% If the \var{recordstatus} variable is set \var{true}, additional information
+% will be added to the \texttt{.tlg} to record the \enquote{exit status} of the
+% typesetting compilation of the \texttt{.lvt} file. If the typesetting compilation
+% completed without throwing an error (due to \TeX\ programming errors, for example),
+% the \enquote{exit status} is zero, else non-zero.
+% \end{buildcmd}
+%
+% \begin{buildcmd}{manifest}
+% Generates a `manifest' file which lists the files of the package as known to \pkg{l3build}.
+% The filename of this file (by default \luavar{manifestfile}) can be set with the variable \var{manifestfile}.
+%
+% The intended purpose of this manifest file is to include it within a package as metadata.
+% This would allow, say, for the copyright statement for the package to refer to the
+% manifest file rather than requiring the author to manually keep a file list up-to-date
+% in multiple locations. The manifest file can be structured and documented with a degree
+% of flexibility. Additional information is described in Section~\ref{sec:manifest}.
+%
+% In order for \texttt{manifest} to detect derived and typeset files, it should be run
+% \emph{after} running \texttt{unpack} and \texttt{doc}. If \texttt{manifest}
+% is run after also running \texttt{ctan} it will include the files included
+% in the CTAN and TDS directories as well.
+%
+% Presently, this means that if you wish to include an up-to-date manifest file
+% as part of a \texttt{ctan} release, you must run
+% \texttt{ctan} / \texttt{manifest} / \texttt{ctan}.
+% Improvements to this process are planned for the future.
+% \end{buildcmd}
+%
+% \begin{buildcmd}{tag}
+% Modifies the contents of files specified by |tagfiles| using a search pattern
+% to automatically update the `release tag' (or package version) and date.
+% The tag is given as the command line option and the date using
+% |--date| (|-d|). If not given, the date will default to the current date in
+% ISO format (YYYY-MM-DD). If no option is given (i.e., no tag specified) the tag
+% is passed through as |nil| to allow setting the tag programmatically.
+%
+% The standard setup has no search pattern defined for this target and so no action
+% will be taken \emph{unless} tag substitution is set up by defining the Lua function
+% |update_tag()|. See Section~\ref{sec:tagging} for full details on this feature.
+% \end{buildcmd}
+%
+% \begin{buildcmd}{unpack}
+% This is an internal target that is normally not needed on user level.
+% It unpacks all files into the directory defined by \var{unpackdir}. This occurs before other build commands such as |doc|, |check|, etc.
+%
+% The unpacking process is performed by executing the \var{unpackexe} (default \texttt{tex}) with options \var{unpackopts} on all files defined by the \var{unpackfiles} variable; by default, all files that match \luavar{unpackfiles}.
+%
+% If additional support files are required for the unpacking process, these can be enumerated in the \var{unpacksuppfiles} variable.
+% Dependencies for unpacking are defined with \var{unpackdeps}.
+%
+% By default this process allows files to be accessed in all standard |texmf| trees; this can be disabled by setting \var{unpacksearch} to |false|.
+% \end{buildcmd}
+%
+% \begin{buildcmd}{upload}
+% This target uses \texttt{curl} to upload the package zip file (created using \texttt{ctan}) to CTAN.
+% To control the metadata used to upload the package, the \texttt{uploadconfig} table should be populated with a number of fields.
+% These are documented in Table~\ref{tab:upload-setup}.
+% Missing required fields will result in an interactive prompt for manual entry.
+%
+% See Section~\ref{sec:upload} for full details on this feature.
+% \end{buildcmd}
+%
+% \subsection{The \texttt{build.lua} file}
+%
+% The \texttt{build.lua} file used to control \pkg{l3build} is a simple Lua file
+% which is read during execution. In the current release of \pkg{l3build},
+% \texttt{build.lua} is read automatically and can access all of the global
+% functions provided by the script. Thus it may contain a simple list of
+% variable settings \emph{or} additionally custom code to change the build
+% process. A number of example scripts are given in Section~\ref{sec:examples}.
+%
+% \subsection{Example build scripts}
+% \label{sec:examples}
+%
+% An example of a standalone build script for a package that uses self-contained |.dtx| files is shown in Figure~\ref{fig:breqn}.
+% Here, the |module| only is defined, and since it doesn't use |.ins| files so the variable \var{unpackfiles} is redefined to run |tex| on the |.dtx| files instead to generate the necessary |.sty| files.
+% There are some PDFs in the repository that shouldn't be part of a CTAN submission, so they're explicitly excluded, and here unpacking is done `quietly' to minimise console output when building the package.
+%
+% \begin{figure}[!b]
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% -- Build configuration for breqn
+%
+% module = "breqn"
+%
+% unpackfiles = {"*.dtx"}
+% excludefiles = {"*/breqn-abbr-test.pdf",
+% "*/eqbreaks.pdf"}
+% unpackopts = "-interaction=batchmode"
+% \end{lstlisting}
+% \caption{The build configuration for the \pkg{breqn} package.}
+% \label{fig:breqn}
+% \end{figure}
+%
+% An example of a bundle build script for \pkg{l3packages} is shown in Figure~\ref{fig:bundle}.
+% Note for \LaTeX3 we use a common file to set all build variables in one place, and the path to the |l3build.lua| script is hard-coded so we always use our own most recent version of the script.
+% An example of an accompanying module build script is shown in Figure~\ref{fig:module}.
+%
+% \begin{figure}[p]
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% -- Build script for LaTeX3 "l3packages" files
+%
+% -- Identify the bundle: there is no module as this is the "driver"
+% bundle = "l3packages"
+%
+% -- Location of main directory: use Unix-style path separators
+% maindir = ".."
+% \end{lstlisting}
+% \caption{The build script for the \pkg{l3packages} bundle.}
+% \label{fig:bundle}
+% \end{figure}
+%
+% \begin{figure}[p]
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% -- Build script for LaTeX3 "xparse" files
+%
+% -- Identify the bundle and module:
+% bundle = "l3packages"
+% module = "xparse"
+%
+% -- Location of main directory: use Unix-style path separators
+% -- Should match that defined by the bundle.
+% maindir = "../.."
+% \end{lstlisting}
+% \caption{The build script for the \pkg{xparse} module.}
+% \label{fig:module}
+% \end{figure}
+%
+% \subsection{Backwards compatibility}
+%
+% Earlier releases of \pkg{l3build} required that the last line of
+% \texttt{build.lua} ran the main script, \emph{i.e.}~ that \texttt{build.lua}
+% was what the user called rather than \texttt{l3build.lua}. To allow scripts
+% to support both forms \emph{for the transition}, a simple test may be
+% included as showing in Figure~\ref{fig:build-compat}.
+% \begin{figure}
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% if not release_date then
+% dofile(kpse.lookup("l3build.lua"))
+% end
+% \end{lstlisting}
+% \caption{Final lines for a \texttt{build.lua} script usable with both older
+% and newer releases of \pkg{l3build}.}
+% \label{fig:build-compat}
+% \end{figure}
+%
+% Note that in time support for loading \pkg{l3build} by calling the
+% \texttt{build.lua} script \emph{may} be removed: the recommended approach for
+% new scripts is to run \texttt{l3build}.
+%
+% \subsection{Variables}
+%
+% This section lists all variables defined in the |l3build.lua| script that are available for customisation.
+%
+% \luavartypeset
+%
+% \subsection{Interaction between tests}
+%
+% Tests are run in a single directory, so whilst they are may be isolated from
+% the system \TeX{} tree they do share files. This may be significant if
+% installation-type files are generated during a test, for example by a
+% |filecontents| environment in \LaTeX{}. Typically, you should set up your
+% tests such that they do not use the same names for such files: this may lead
+% to variable outcomes depending on the order in which tests are run.
+%
+% \subsection{Selective running of tests}
+%
+% The variables |includetests| and |excludetests| may be used to select which
+% tests are run: these variables take test \emph{names} not full file names.
+% The list of tests in |excludetests| overrides any matches in |includetests|,
+% meaning that tests can be disabled selectively. It also makes it possible
+% to disable test on for example a platform basis: the Lua core variable
+% |os.type| may be used to set |excludetests| only on some systems.
+%
+% \subsection{Multiple sets of tests}
+%
+% In most cases, a single set of tests will be appropriate for the module, with
+% a common set of configuration settings applying. However, there are
+% situations where you may need entirely independent sets of tests which have
+% different setting values, for example using different formats or where the
+% entire set will be engine-dependent. To support this, \pkg{l3build} offers
+% the possibility of using multiple configurations for tests. This is supported
+% using the \var{checkconfigs} table. This is used to list the names of each
+% configuration (|.lua| file) which will be used to run tests.
+%
+% For example, for the core \LaTeXe{} tests the main test files are contained
+% in a directory |testfiles|. To test font loading for \XeTeX{} and \LuaTeX{}
+% there are a second set of tests in |testfiles-TU| which use the short
+% |build-TU.lua| file shown in Figure~\ref{fig:configs}. To run both sets of
+% tests, the main |build.lua| file contains the setting
+% |checkconfigs = {"build", "config-TU"}|. This will cause \pkg{l3build} to run
+% first using no additional settings (\emph{i.e.}~reading the normal
+% |build.lua| file alone), then running \emph{also} loading the settings from
+% |config-TU.lua|.
+% \begin{figure}
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% -- Special config for these tests
+% checksearch = true
+% checkengines = {"xetex","luatex"}
+% testfiledir = "testfiles-TU"
+% \end{lstlisting}
+% \caption{Example of using additional (or overriding) settings for configuring tests in a different subdirectory.}
+% \label{fig:configs}
+% \end{figure}
+%
+% To allow selection of a one or more configurations, and to allow saving of
+% |.tlg| files in non-standard configurations, the |--config| (|-c|) option may
+% be used. This works in the same way as |--engine|: it takes a comma list of
+% configurations to apply, overriding \var{checkconfigs}.
+%
+% \subsection{Dependencies}
+%
+% If you have multiple packages that are developed separately but still interact in some way, it's often desirable to integrate them when performing regression tests.
+% For \LaTeX3, for example, when we make changes to \pkg{l3kernel} it's important to check that the tests for \pkg{l3packages} still run correctly, so it's necessary to include the \pkg{l3kernel} files in the build process for \pkg{l3packages}.
+%
+% In other words, \pkg{l3packages} is \emph{dependent} on \pkg{l3kernel}, and this is specified in \pkg{l3build} by setting appropriately the variables \texttt{checkdeps}, \texttt{typesetdeps}, and \texttt{unpackdeps}.
+% The relevant parts of the \LaTeX3 repository is structured as the following.
+% \Needspace{3\baselineskip}
+% \begin{Verbatim}
+% l3/
+% l3kernel/
+% build.lua
+% expl3.dtx
+% expl3.ins
+% ...
+% testfiles/
+% l3packages/
+% build.lua
+% xparse/
+% build.lua
+% testfiles/
+% xparse.dtx
+% xparse.ins
+% support/
+% \end{Verbatim}
+% For \LaTeX3 build files, |maindir| is defined as top level folder |l3|, so all support files are located here, and the build directories will be created there.
+% To set \pkg{l3kernel} as a dependency of \pkg{l3package}, within |l3packages/xparse/build.lua| the equivalent of the following is set:
+% \begin{Verbatim}
+% maindir = "../.."
+% checkdeps = {maindir .. "/l3kernel"}
+% \end{Verbatim}
+% This ensures that the \pkg{l3kernel} code is included in all processes involved in unpacking and checking and so on.
+% The name of the script file in the dependency is set with the |scriptname| variable; by default these are |"build.lua"|.
+%
+% \subsection{Non-standard source layouts}
+%
+% A variety of source layouts are supported. In general, a \enquote{flat}
+% layout with all source files \enquote{here} is most convenient. However,
+% \pkg{l3build} supports placement of both code and documentation source
+% files in other locations using the \var{sourcefiledir} and \var{docfiledir}
+% variables. For pre-built trees, the glob syntax \texttt{**/*.\meta{ext}} may
+% be useful in these cases: this enables recursive searching in the appropriate
+% tree locations. With the standard settings, this structure will be removed
+% when creating a CTAN release: the variable \var{flatten} may be
+% used to control this behavior.
+%
+% A series of example layouts and matching |build.lua| files are available from
+% \url{https://github.com/latex3/l3build/tree/master/examples}.
+%
+% \subsection{Non-standard formats/binaries}
+%
+% The standard approach used by \pkg{l3build} is to use a combination
+% of \var{engine} and \var{checkformat} to generate the \emph{binary} and
+% \emph{format} combination used for tests. For example, when |pdftex| is
+% the \var{engine} and |latex| is the \var{checkformat}, the system call
+% used is
+% \begin{verbatim}
+% pdftex --fmt=pdflatex
+% \end{verbatim}
+% \emph{i.e.}~the binary names is the same as the \var{engine}, and the format
+% is a simple substitution of the \var{checkformat} into \var{engine}, replacing
+% |tex|.
+%
+% For more complex set ups, \var{specialformats} should be used. This is a
+% table with one entry per \var{checkformat}. Each entry is itself a table,
+% and these contain a list of engines and settings for |binary|, |format|
+% and |options|. For example, for Con\TeX{}t and appropriate set up is
+% \begin{verbatim}
+% specialformats.context = {
+% luatex = {binary = "context", format = ""},
+% pdftex = {binary = "texexec", format = ""},
+% xetex = {binary = "texexec", format = "", options = "--xetex"}
+% }
+% \end{verbatim}
+%
+% \subsection{Output normalisation}
+% \label{sec:norm}
+%
+% To allow test files to be used between different systems (\emph{e.g.}~when
+% multiple developers are involved in a project), the log files are normalised
+% before comparison during checking. This removes some system-dependent
+% data but also some variations due to different engines. This normalisation
+% consists of two parts: removing (\enquote{ignoring}) some lines and modifying
+% others to give consistent test. Currently, the following types of line are
+% ignored:
+% \begin{itemize}
+% \item Lines before the \cs{START}, after the \cs{END} and within
+% \cs{OMIT}/\cs{TIMO} blocks
+% \item Entirely blank lines, including those consisting only of spaces.
+% \item Lines related to loading |.fd| files (from \texttt{(\meta{name}.fd}
+% to the matching \texttt{)}).
+% \item Lines starting \cs{openin} or \cs{openout}.
+% \end{itemize}
+% Modifications made in lines are:
+% \begin{itemize}
+% \item Removal spaces at the start of lines.
+% \item Removal of |./| at start of file names.
+% \item Standardisation of the list of units known to \TeX{} (\pdfTeX{}
+% and \LuaTeX{} add a small number of additional units which are not
+% known to \TeX90 or \XeTeX{}, (u)p\TeX{} adds some additional non-standard
+% ones)
+% \item Standardisation of \verb*|\csname\endcsname | to |\csname\endcsname|
+% (the former is formally correct, but the latter was produced for many
+% years due to a \TeX{} bug).
+% \item Conversion of \texttt{on line \meta{number}} to \texttt{on line ...}
+% to allow flexibility in changes to test files.
+% \item Conversion of file dates to \texttt{....-..-..}, and any version
+% numbers on the same lines to \texttt{v...}.
+% \item Conversion of register numbers in assignment lines
+% \texttt{\cs{\meta{register}}=\cs{\meta{type}}\meta{number}} to
+% \texttt{\cs{\meta{type}}\meta{...}}
+% \item Conversion of box numbers in |\show| lines
+% \texttt{>~\cs{box}\meta{number}=} to \texttt{>~\cs{box}...=}
+% \item Removal of some (u)p\TeX{} data where it is equivalent to
+% \pdfTeX{} (|yoko direction|, |\displace 0.0|)
+% \end{itemize}
+%
+% \LuaTeX{} makes several additional changes to the log file. As normalising
+% these may not be desirable in all cases, they are handled separately.
+% When creating \LuaTeX{}-specific test files (either with \LuaTeX{} as
+% the standard engine or saving a \LuaTeX{}-specific |.tlg| file) no further
+% normalisation is undertaken. On the other hand, for cross-engine comparison
+% the following normalisation is applied:
+% \begin{itemize}
+% \item Removal of additional (unused) |\discretionary| points.
+% \item Normalisation of some |\discretionary| data to a \TeX{}90 form.
+% \item Removal of |U+...| notation for missing characters.
+% \item Removal of |display| for display math boxes
+% (included by \TeX90/\pdfTeX{}/\XeTeX).
+% \item Removal of Omega-like |direction TLT| information.
+% \item Removal of additional whatsit containing local paragraph information
+% (|\localinterlinepenalty|, \emph{etc.}).
+% \item Rounding of glue set to four decimal places (glue set may be
+% slightly different in \LuaTeX{} compared to other engines).
+% \item Conversion of low chars ($0$ to $31$) to |^^| notation.
+% \end{itemize}
+%
+% When making comparisons between 8-bit and Unicode engines it is useful to
+% format the top half of the 8-bit range such that it appears in the log as
+% |^^|\texttt{\meta{char}} (the exact nature of the 8-bit output is otherwise
+% dependent on the active code page). This may be controlled using the
+% |asciiengines| option. Any engines named here will use a |.tcx| file to
+% produce only ASCII chars in the log output, whilst for other engines
+% normalisation is carried out from UTF-8 to ASCII. If the option is set to
+% an empty table the latter process is skipped: suitable for cases where only
+% Unicode engines are in use.
+%
+% \section{Writing test files}
+% \label{sec:writing-tests}
+%
+% Test files are written in a \TeX{} dialect using the support file |regression-test.tex|, which should be |\input| at the very beginning of each test.
+% Additional customisations to this driver can be included in a local |regression-test.cfg| file, which will be loaded automatically if found.
+%
+% The macros loaded by |regression-test.tex| set up the test system and provide a number of commands to aid the production of a structured test suite.
+% The basis of the test suite is to output material into the |.log| file, from which a normalised test output (|.tlg|) file is produced by the build command |save|.
+% A number of commands are provided for this; they are all written in uppercase to help avoid possible conflicts with other package commands.
+%
+% \subsection{Metadata and structural commands}
+%
+% Any commands that write content to the |.log| file that should be ignored can be surrounded by |\OMIT| \dots\ |\TIMO|.
+% At the appropriate location in the document where the |.log| comparisons should start (say, after |\begin{document}|), the test suite must contain the |\START| macro.
+% The test should then include \cs{AUTHOR}\marg{authors details} in case a test file fails in the future and needs to be re-analysed.
+%
+% Some additional diagnostic information can then be included as metadata for the conditions of the test.
+% The desired format can be indicated with \cs{FORMAT}\marg{format name}, and any packages or classes loaded can be indicated with
+%
+% \noindent\hspace*{2\parindent} \cs{CLASS}\oarg{options}\marg{class name, version}\par
+% \noindent\hspace*{2\parindent} \cs{PACKAGE}\oarg{options}\marg{package name, version}
+%
+% \noindent These do not provide information that is useful for automated checking; after all, packages change their version numbers frequently.
+% Rather, including this information in a test indicates the conditions under which the test was definitely known to pass at a certain time in the past.
+%
+% The |\END| command signals the end of the test (but read on).
+% Some additional diagnostic information is printed at this time to debug if the test did not complete `properly' in terms of mismatched brace groups or \cs{if}\dots\cs{fi} groups.
+%
+% In a \LaTeX{} document, |\end{document}| will implicitly call |\END| at the very end of the compilation process.
+% If |\END| is used directly (replacing |\end{document}| in the test), the compilation will halt almost immediately, and various tasks that |\end{document}| usually performs will not occur (such as potentially writing to the various |.toc| files, and so on). This can be an advantage if there is additional material printed to the log file in this stage that you wish to ignore, but it is a disadvantage if the test relies on various auxiliary data for a subsequent typesetting run.
+% (See the \var{checkruns} variable for how these tests would be test up.)
+%
+% \subsection{Commands to help write tests}
+%
+% A simple command \cs{CHECKCOMMAND}\cs{\meta{macro}} is provided to check whether a particular \cs{\meta{macro}} is defined, undefined, or equivalent to \cs{relax}.
+% This is useful to flag either that internal macros are remaining local to their definitions, or that defined commands definitely are defined, or even as a reminder that commands you intend to define in a future package need to be tested once they appear.
+%
+% \cs{TYPE} is used to write material to the \texttt{.log} file, like \LaTeX's \cs{typeout}, but it allows `long' input.
+% The following commands are defined to use \cs{TYPE} to output strings to the \texttt{.log} file.
+% \begin{itemize}
+% \item
+% \cs{SEPARATOR} inserts a long line of \texttt{=} symbols to break up the log output.
+% \item
+% \cs{NEWLINE} inserts a linebreak into the log file.
+% \item
+% \cs{TRUE}, \cs{FALSE}, \cs{YES}, \cs{NO} output those strings to the log file.
+% \item
+% \cs{ERROR} is \emph{not} defined but is commonly used to indicate a code path that should never be reached.
+% \item
+% The \cs{TEST}\marg{title}\marg{contents} command surrounds its \meta{contents} with some \cs{SEPARATOR}s and a \meta{title}.
+% \item
+% \cs{TESTEXP} surrounds its contents with \cs{TYPE} and formatting to match \cs{TEST}; this can be used as a shorthand to test expandable commands.
+% \item
+% TODO: would a \cs{TESTFEXP} command (based on \cs{romannumeral} expansion) be useful as well?
+% \item
+% \cs{BEGINTEST}\marg{title} \dots \cs{ENDTEST} is an environment form of
+% \cs{TEST}, allowing verbatim material, \emph{etc.} to appear.
+% \end{itemize}
+% An example of some of these commands is shown following.
+% \begin{Verbatim}
+% \TEST{bool_set,~lazy~evaluation}
+% {
+% \bool_set:Nn \l_tmpa_bool
+% {
+% \int_compare_p:nNn 1=1
+% && \bool_lazy_any_p:n
+% {
+% { \int_compare_p:nNn 2=3 }
+% { \int_compare_p:nNn 4=4 }
+% { \int_compare_p:nNn 1=\ERROR } % is skipped
+% }
+% && \int_compare_p:nNn 2=2
+% }
+% \bool_if:NTF \l_tmpa_bool \TRUE \FALSE
+% }
+% \end{Verbatim}
+% This test will produce the following in the output.
+% \begin{Verbatim}
+% ==========================================
+% TEST 8: bool_set, lazy evaluation
+% ==========================================
+% TRUE
+% ==========================================
+% \end{Verbatim}
+% (Only if it's the eighth test in the file of course, and assuming \pkg{expl3}
+% coding conventions are active.)
+%
+% \subsection{Showing box content}
+%
+% The commands introduced above are only useful for checking algorithmic or logical correctness.
+% Many packages should be tested based on their typeset output instead; \TeX{} provides a mechanism for this by printing the contents of a box to the log file.
+% The |regression-test.tex| driver file sets up the relevant \TeX{} parameters to produce as much output as possible when showing box output.
+%
+% A plain \TeX{} example of showing box content follows.
+% \begin{Verbatim}[frame=single,fontsize=\small]
+% \input regression-test.tex\relax
+% \START
+% \setbox0=\hbox{\rm hello \it world $a=b+c$}
+% \showbox0
+% \END
+% \end{Verbatim}
+% This produces the output shown in Figure~\ref{fig:box-log} (left side).
+% It is clear that if the definitions used to typeset the material in the box changes, the log output will differ and the test will no longer pass.
+%
+% The equivalent test in \LaTeXe{} using \pkg{expl3} is similar.
+% \begin{Verbatim}[frame=single,fontsize=\small]
+% \input{regression-test.tex}
+% \documentclass{article}
+% \usepackage{expl3}
+% \START
+% \ExplSyntaxOn
+% \box_new:N \l_tmp_box
+% \hbox_set:Nn \l_tmp_box {hello~ \emph{world}~ $a=b+c$}
+% \box_show:N \l_tmp_box
+% \ExplSyntaxOff
+% \END
+% \end{Verbatim}
+% The output from this test is shown in Figure~\ref{fig:box-log} (right side).
+% There is marginal difference (mostly related to font selection and different logging settings in \LaTeX) between the plain and \pkg{expl3} versions.
+%
+% When examples are not self-contained enough to be typeset into boxes, it is possible to ask \TeX{} to output the entire contents of a page.
+% Insert \cs{showoutput} for \LaTeX{} or set \cs{tracingoutput} positive for plain \TeX{}; ensure that the test ends with \cs{newpage} or equivalent because \TeX{} waits until the entire page is finished before outputting it.
+%
+% TODO: should we add something like \cs{TRACEPAGES} to be format-agnostic here? Should this perhaps even be active by default?
+%
+% \begin{figure}
+% \hspace*{-3cm}
+% \begin{BVerbatim}[fontsize=\small]
+% > \box0=
+% \hbox(6.94444+0.83333)x90.56589
+% .\tenrm h
+% .\tenrm e
+% .\tenrm l
+% .\tenrm l
+% .\tenrm o
+% .\glue 3.33333 plus 1.66666 minus 1.11111
+% .\tenit w
+% .\tenit o
+% .\tenit r
+% .\tenit l
+% .\tenit d
+%
+% .\glue 3.57774 plus 1.53333 minus 1.0222
+% .\mathon
+% .\teni a
+% .\glue(\thickmuskip) 2.77771 plus 2.77771
+% .\tenrm =
+% .\glue(\thickmuskip) 2.77771 plus 2.77771
+% .\teni b
+% .\glue(\medmuskip) 2.22217 plus 1.11108 minus 2.22217
+% .\tenrm +
+% .\glue(\medmuskip) 2.22217 plus 1.11108 minus 2.22217
+% .\teni c
+% .\mathoff
+%
+% ! OK.
+% l.9 \showbox0
+%
+%
+% \end{BVerbatim}
+% \qquad
+% \begin{BVerbatim}[fontsize=\small]
+% > \box71=
+% \hbox(6.94444+0.83333)x91.35481
+% .\OT1/cmr/m/n/10 h
+% .\OT1/cmr/m/n/10 e
+% .\OT1/cmr/m/n/10 l
+% .\OT1/cmr/m/n/10 l
+% .\OT1/cmr/m/n/10 o
+% .\glue 3.33333 plus 1.66666 minus 1.11111
+% .\OT1/cmr/m/it/10 w
+% .\OT1/cmr/m/it/10 o
+% .\OT1/cmr/m/it/10 r
+% .\OT1/cmr/m/it/10 l
+% .\OT1/cmr/m/it/10 d
+% .\kern 1.03334
+% .\glue 3.33333 plus 1.66666 minus 1.11111
+% .\mathon
+% .\OML/cmm/m/it/10 a
+% .\glue(\thickmuskip) 2.77771 plus 2.77771
+% .\OT1/cmr/m/n/10 =
+% .\glue(\thickmuskip) 2.77771 plus 2.77771
+% .\OML/cmm/m/it/10 b
+% .\glue(\medmuskip) 2.22217 plus 1.11108 minus 2.22217
+% .\OT1/cmr/m/n/10 +
+% .\glue(\medmuskip) 2.22217 plus 1.11108 minus 2.22217
+% .\OML/cmm/m/it/10 c
+% .\mathoff
+%
+% ! OK.
+% <argument> \l_tmp_box
+%
+% l.12 \box_show:N \l_tmp_box
+% \end{BVerbatim}
+% \caption{Output from displaying the contents of a simple box to the log file, using plain \TeX{} (left) and \pkg{expl3} (right). Some blank lines have been added to the plain \TeX{} version to help with the comparison.}
+% \label{fig:box-log}
+% \end{figure}
+%
+% \subsection{Testing entire pages}
+%
+% There may be occasions where creating entire test pages is necessary
+% to observe the test output required. That is best achieved by applying
+% \cs{showoutput} and forcing a complete page to be produced, for example
+% \begin{Verbatim}[frame=single,fontsize=\small]
+% \input{regression-test.tex}
+% \documentclass{article}
+% \usepackage{expl3}
+% \START
+% \showoutput
+% % Test content here
+% \vfil\break
+% \END
+% \end{Verbatim}
+%
+% \subsection{Additional test tasks}
+%
+% A standard test will run the file \texttt{\meta{name}.lvt} using one
+% or more engines, but will not carry out any additional processing. For
+% some tests, for example bibliography generation, it may be desirable to
+% call one or more tools in addition to the engine. This can be arranged
+% by defining |runtest_tasks|, a function taking one argument, the name
+% of the current test (this is equivalent to \TeX{}'s \cs{jobname},
+% \emph{i.e.}~it lacks an extension). The function |runtest_tasks|
+% is is into a call to the system to run the engine. As such, it
+% should take return a string with the appropriate command(s)
+% and option(s). If more than one task is required, these should be separated
+% by use of |os_concat|, a string variable defined by \pkg{l3build} as the
+% correct concatenation marker for the system. An example of |runtest_tasks|
+% suitable for calling Biber is shown in Listing~\ref{fig:test-tasks}.
+% \begin{figure}
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% function runtest_tasks(name)
+% return "biber " .. name
+% end
+% \end{lstlisting}
+% \caption{Example \texttt{runtest_tasks} function.}
+% \label{fig:test-tasks}
+% \end{figure}
+%
+% \subsection{Epoch setting}
+%
+% To produce predictable output when using dates, the test system offers the
+% ability to set the epoch to a known value. The \luavar{epoch} variable may
+% be given as a raw value (a simple integer) or as a date in ISO format.
+% The two flags \luavar{forcecheckepoch} and \luavar{forcedocepoch} then
+% determine whether this is applied in testing and typesetting, respectively.
+%
+% The epoch may also be given as a command line option, |-E|, which again
+% takes either a date or raw epoch. When given, this will automatically
+% activate forcing of the epoch in both testing and typesetting.
+%
+% \subsection{Settings in \texttt{texmf.cnf}}
+%
+% To allow application of non-standard \TeX{} trees or similar non-standard
+% settings, \pkg{l3build} enables searching for a \texttt{texmf.cnf} file
+% by setting the envirnmental variable \texttt{TEXMFCNF}. This might
+% for example be used with a file containing
+% \begin{verbatim}
+% TEXMFAUXTREES = ../../texmf,
+% \end{verbatim}
+% for adding a local tree within the development repository (assuming the
+% typical \pkg{l3build} layout).
+%
+% \section{Alternative test formats}
+%
+% \subsection{Generating test files with \pkg{DocStrip}}
+%
+% It is possible to pack tests inside source files. Tests generated during the
+% unpacking process will be available to the \texttt{check} and \texttt{save}
+% commands as if they were stored in the \texttt{testfiledir}. Any explicit
+% test files inside \texttt{testfiledir} take priority over generated ones
+% with the same names.
+%
+% \subsection{Specifying expectations}
+%
+% Regression tests check whether changes introduced in the code modify the test
+% output. Especially while developing a complex package there is not yet a
+% baseline to save a test goal with. It might then be easier to formulate the
+% expected effects and outputs of tests directly. To achieve this, you may
+% create an \texttt{.lve} instead of a \texttt{.tlg}
+% file.\footnote{Mnemonic: \texttt{lv\textbf{t}}: \textbf{t}est,
+% \texttt{lv\textbf{e}}: \textbf{e}xpectation} It is processed exactly like
+% the \texttt{.lvt} to generate the expected outcome. The test fails when both
+% differ.
+%
+% Combining both features enables contrasting the test with its expected
+% outcome in a compact format. Listing \ref{fig:expect-dtx} exemplary tests
+% \TeX{}s counters. Listing \ref{fig:expect-ins} shows the relevant part of an
+% \texttt{.ins} file to generate it.
+%
+% \begin{figure}
+% \begin{lstlisting}[frame=single,language={TeX},gobble = 6]
+% \input regression-test.tex\relax
+% \START
+% \TEST{counter-math}{
+% %<*test>
+% \OMIT
+% \newcounter{numbers}
+% \setcounter{numbers}{2}
+% \addtocounter{numbers}{2}
+% \stepcounter{numbers}
+% \TIMO
+% \typeout{\arabic{numbers}}
+% %</test>
+% %<expect> \typeout{5}
+% }
+% \END
+% \end{lstlisting}
+% \caption{Test and expectation can be specified side-by-side in a single
+% \texttt{.dtx} file.}
+% \label{fig:expect-dtx}
+% \end{figure}
+%
+%\begin{figure}
+% \begin{lstlisting}[frame=single,language={TeX},gobble = 6]
+% \generate{\file{\jobname.lvt}{\from{\jobname.dtx}{test}}
+% \file{\jobname.lve}{\from{\jobname.dtx}{expect}}}
+% \end{lstlisting}
+% \caption{Test and expectation are generated from a \texttt{.dtx} file of
+% the same name.}
+% \label{fig:expect-ins}
+% \end{figure}
+%
+% \subsection{PDF-based tests}
+%
+% In most cases, testing is best handled by using the text-based methods
+% outlined above. However, there are cases where the detail of output structure
+% is important. This can only be fully tested by comparing PDF structure.
+% To support this, \pkg{l3build} can be instructed to build and compare
+% PDF files by setting up tests in \texttt{.pvt} files. The following
+% normalization takes place:
+% \begin{itemize}
+% \item Replacement of binary streams by the marker |[BINARY STREAM]|
+% \item Removal of blank lines
+% \end{itemize}
+%
+% After this normalization takes place, the file can not usually be rendered
+% properly. To check if the build system has produced a correct PDF, the
+% pre-normalization PDF can be found in the \texttt{build} folder.
+%
+% To allow platform-independence, PDF-based tests must use only Type~1 or
+% OpenType fonts: Type3 fonts are system-dependent. PDF files are
+% engine-specific, thus one |.tpf| file should be stored per engine to be
+% tested.
+%
+%
+% \section{Release-focussed features}
+%
+% \subsection{Installation structure}
+%
+% With the standard settings, \pkg{l3build} will install files within
+% the \TeX{} directory structure (TDS) as follows
+% \begin{itemize}
+% \item \var{installfiles} within a \texttt{\meta{bundle}/\meta{module}}
+% (or \texttt{\meta{module}}) directory inside \texttt{tex/\meta{format}}
+% \item \var{sourcefiles} within a \texttt{\meta{bundle}/\meta{module}}
+% (or \texttt{\meta{module}}) directory inside \texttt{source/\meta{format}}
+% \item Typeset PDFs within a \texttt{\meta{bundle}/\meta{module}}
+% (or \texttt{\meta{module}}) directory inside \texttt{doc/\meta{format}}
+% \item \var{bstfiles} within a \texttt{\meta{bundle}/\meta{module}}
+% (or \texttt{\meta{module}}) directory inside \texttt{bibtex/bst}
+% \item \var{bibfiles} within a \texttt{\meta{bundle}/\meta{module}}
+% (or \texttt{\meta{module}}) directory inside \texttt{bibtex/bib}
+% \item \var{makeindexfiles} within a \texttt{\meta{bundle}/\meta{module}}
+% (or \texttt{\meta{module}}) directory inside \texttt{makeindex}
+% \end{itemize}
+%
+% For more complex set ups, this can be customised using the
+% \luavar{tdslocations} table. Each entry there should be a glob specifying the
+% TDS position of a file or files. Any files not specified in the table
+% will use the standard locations above. For example, to place some files
+% in the generic tree, some in the plain \TeX{} tree and some in the \LaTeX{}
+% tree, one might use the set up shown in Figure~\ref{fig:tds}.
+% \begin{figure}
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% tdslocations =
+% {
+% "tex/generic/mypkg/*.generic.tex" ,
+% "tex/plain/mypkg/*.plain.tex" ,
+% "tex/latex/mypkg/*.latex.tex"
+% }
+% \end{lstlisting}
+% \caption{Example \texttt{tdslocations} table.}
+% \label{fig:tds}
+% \end{figure}
+%
+% The table is read in order, and thus specific file names should come before
+% potential wild-card matches.
+%
+% \subsection{Automatic tagging}
+% \label{sec:tagging}
+%
+% The |tag| target can automatically edit
+% source files to modify date and release tag. As standard, no automatic
+% replacement takes place, but setting up a |update_tag()| function
+% will allow this to happen. This function takes four input arguments:
+% \begin{enumerate}[nosep]
+% \item file name
+% \item full content of the file
+% \item tag name
+% \item tag date
+% \end{enumerate}
+% The |update_tag()| function should return the (modified) contents
+% for writing to disk.
+% For example, the function used by |l3build| itself is
+% shown in Figure~\ref{fig:update-tag}.
+%
+% \begin{figure}
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% -- Detail how to set the version automatically
+% function update_tag(file,content,tagname,tagdate)
+% if string.match(file, "%.dtx$") then
+% return string.gsub(content,
+% "\n%% \\date{Released %d%d%d%d/%d%d/%d%d}\n",
+% "\n%% \\date{Released " .. tagname .. "}\n")
+% elseif string.match(file, "%.md$") then
+% return string.gsub(content,
+% "\nRelease %d%d%d%d/%d%d/%d%d\n",
+% "\nRelease " .. tagname .. "\n")
+% elseif string.match(file, "%.lua$") then
+% return string.gsub(content,
+% '\nrelease_date = "%d%d%d%d/%d%d/%d%d"\n',
+% '\nrelease_date = "' .. tagname .. '"\n')
+% end
+% return contents
+% end
+% \end{lstlisting}
+% \caption{Example \texttt{update_tag} function.}
+% \label{fig:update-tag}
+% \end{figure}
+%
+% To allow more complex tasks to take place, a hook |tag_hook()| is also
+% available. It will receive the tag name and date as arguments, and
+% may be used to carry out arbitrary tasks after the main tagging process.
+% For example, this can be used to set a version control tag for an entire repository.
+%
+%
+% \subsection{Typesetting documentation}
+%
+% As part of the overall build process, \pkg{l3build} will create PDF
+% documentation as described earlier. The standard build process for PDFs
+% will attempt to run Biber, \BibTeX{} and MakeIndex as appropriate
+% (the exact binaries used are defined by \luavar{biberexe}, \luavar{bibtexexe}
+% and \luavar{makeindexexe}). However, there is no attempt to create an entire
+% PDF creation system in the style of \texttt{latexmk} or similar.
+%
+% For package authors who have more complex requirements than those covered
+% by the standard set up, the Lua script offers the possibility for
+% customisation. The Lua function \texttt{typeset} may be defined before
+% reading \texttt{l3build.lua} and should take one argument, the name of
+% the file to be typeset. Within this function, the auxiliary Lua
+% functions \texttt{biber}, \texttt{bibtex}, \texttt{makeindex} and
+% \texttt{tex} can be used, along with custom code, to define a PDF
+% typesetting pathway. The functions \texttt{biber} and \texttt{bibtex}
+% take a single argument: the name of the file to work with \emph{minus}
+% any extension. The \texttt{tex} takes as an argument the full name
+% of the file. The most complex function \texttt{makeindex} requires the
+% name, input extension, output extension, log extension and style name.
+% For example, Figure~\ref{fig:PDF} shows a simple script which might
+% apply to a case where multiple \BibTeX{} runs are needed (perhaps where
+% citations can appear within other references).
+%
+% Where there are complex requirements for pre-compiled demonstration
+% files, the hook |typeset_demo_tasks()| is available: it runs after
+% copying files to the typesetting location but before the main typesetting
+% run. This may be used for example to script a very large number of
+% demonstrations using a single source (see the \pkg{beamer} package
+% for an example of this).
+%
+% \begin{figure}[!b]
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% #!/usr/bin/env texlua
+%
+% -- Build script with custom PDF route
+%
+% module = "mymodule"
+%
+% function typeset(file)
+% local name = jobname(file)
+% local errorlevel = tex (file)
+% if errorlevel == 0 then
+% -- Return a non-zero errorlevel if anything goes wrong
+% errorlevel =(
+% bibtex(name) +
+% tex(file) +
+% bibtex(name) +
+% tex(file) +
+% tex(file)
+% )
+% end
+% return errorlevel
+% end
+% \end{lstlisting}
+% \caption{A customised PDF creation script.}
+% \label{fig:PDF}
+% \end{figure}
+%
+%
+% \subsection{Automated upload to CTAN}
+% \label{sec:upload}
+%
+% The CTAN upload process is backed by an API, which \pkg{l3build} can use
+% to send zip files for release. Along with the file, a variety of metadata
+% must be specified about the package, including the version, license, and so on.
+% A description of this metadata is outlined in Table~\ref{tab:upload-setup},
+% and a simple example of an extract from a \texttt{build.lua} file using this is shown
+% in Figure~\ref{fig:uploadconfig}.
+%
+% Note that the \texttt{upload} target will \emph{not} execute the
+% \texttt{ctan} target first.
+%
+% This upload facility assumes availablity of |curl| on your
+% system. In the case of Windows, the system curl will not be
+% available if you are using a 32 bit \TeX\ implementation. Curl
+% executables are available for a variety of operating systems from
+% \url{https://curl.haxx.se/download.html}.
+%
+% \paragraph{Announcement text}
+% It can be convenient not to include the announcement text within the |build.lua| file
+% directly. The command line argument |--message| (|-m|) allows the announcement to be
+% included as part of the |l3build| arguments, and |--file| (|-F|) reads the announcement
+% from a specified file. The \texttt{build.lua} file may also specify that this text is to
+% be taken from the file specified by
+% |uploadconfig.announcement_file|, this allows the release-specific announcement to be
+% specified outside the main |build.lua| file. If
+% |uploadconfig.announcement_file| is |nil| or specifies a file that
+% can not be read, and no announcement is provided by the
+% |announcement| field or commandline arguments, |l3build| will
+% interactively prompt for text (which may be empty).
+%
+% Note that if the announcement text is empty a `silent update'
+% is performed; this should usually be performed for minor bug or documentation fixes only.
+%
+% \paragraph{Note text}
+% This optional field is for passing notes to the CTAN maintainers. As
+% for announcements, the text may be set in |uploadconfig.note| or
+% perhaps more usefully, if |uploadconfig.note_file| is the filename of a
+% readable file the file text is used as the note.
+%
+% \paragraph{Uploader details}
+% The CTAN team use the uploader email address as a form of low-security sanity
+% check that the upload is coming from a reputable source. Therefore, it is advisable not
+% to store this information within a public |build.lua| file. It can be set on the command
+% line with the |--email| option to \texttt{l3build}; alternatively, a private
+% configuration file could be used to add this information at upload time.
+%
+% \paragraph{The \texttt{update} field}
+% In most scenarios the |update| field does not need to be explicitly set. By default
+% \pkg{l3build} assumes that the package being uploaded already exists on CTAN
+% (|update=true|). If it does not, this is caught in the validation process before
+% uploading and automatically corrected. If you set |update| explicitly this will be passed
+% directly to CTAN in all circumstances, leading to errors if you attempt to update a
+% non-existing package or if you attempt to upload a new package with the same name as a
+% pre-existing one.
+%
+% \paragraph{The \texttt{curl} options file}
+%
+% The \pkg{l3build} upload options are passed to |curl| by writing the
+% fields to a text file with a default name being
+% \meta{package}|-ctan.curlopt|. This is then passed to curl using its
+% |--config| commandline option. (Using an intermediate file helps
+% keep \pkg{l3build} portable between systems using different
+% commandline quoting conventions.)
+%
+% By default the file is written into the current directory alongside
+% the zip file to be uploaded. You may wish to specify that this file
+% is ignored by any version control in that directory (using
+% |.gitignore| or similar). Or alternatively you can use the
+% |uploadconfig.curl_file| field in the |build.lua| file to specify an
+% alternative name or location for this file.
+%
+% \paragraph{Debugging}
+% If you have have difficulty with the upload process, add the option |--debug| to divert
+% the request from CTAN to a service that redirects the input back
+% again so it can be examined.
+% It can also be useful to check the contents of the |curlopts| file which has a
+% record of the options passed to curl.
+%
+% \begin{table}[p]
+% \def\YES{\textbullet}
+% \caption{Fields used in the \texttt{uploadconfig} setup table. The
+% first section of fields are \emph{required} and if they are
+% omitted the user will be interactively prompted for further
+% input. Most commands take string input, but those that are
+% indicated with `Multi' accept more than one entry using an array
+% of strings. Most of the fields correspond directly to the fields
+% in the CTAN upload API, the last group relate to file use by
+% \pkg{l3build}.}
+% \label{tab:upload-setup}
+% \medskip
+% \begin{minipage}{\linewidth}
+% \begin{tabular}{@{}lccp{8cm}@{}}
+% \toprule
+% Field & Req. & Multi & Description \\
+% \midrule
+% \texttt{announcement} & \YES & & Announcement text \\
+% \texttt{author } & \YES & & Author name (semicolon-separated for multiple) \\
+% \texttt{ctanPath } & \YES & & CTAN path \\
+% \texttt{email } & \YES & & Email address of uploader \\
+% \texttt{license } & \YES & \YES & Package license(s)\footnote{See \url{https://ctan.org/license}} \\
+% \texttt{pkg } & \YES & & Package name \\
+% \texttt{summary } & \YES & & One-line summary \\
+% \texttt{uploader } & \YES & & Name of uploader \\
+% \texttt{version } & \YES & & Package version \\
+% \midrule
+% \texttt{bugtracker } & & \YES & URL(s) of bug tracker \\
+% \texttt{description } & & & Short description/abstract \\
+% \texttt{development } & & \YES & URL(s) of development channels \\
+% \texttt{home } & & \YES & URL(s) of home page \\
+% \texttt{note } & & & Internal note to CTAN \\
+% \texttt{repository } & & \YES & URL(s) of source repositories \\
+% \texttt{support } & & \YES & URL(s) of support channels \\
+% \texttt{topic } & & \YES & Topic(s)\footnote{See \url{https://ctan.org/topics/highscore}} \\
+% \texttt{update } & & & Boolean \texttt{true} for an update, \texttt{false} for a new package \\
+% \midrule
+% \texttt{announcement\_file} & & & Announcement text file \\
+% \texttt{note\_file} & & & Note text file \\
+% \texttt{curlopt\_file} & & & The filename containing the options passed to curl \\
+% \bottomrule
+% \end{tabular}
+% \end{minipage}
+% \end{table}
+%
+%
+% \begin{figure}[p]
+% \begin{lstlisting}[frame=single,language={[5.2]Lua},gobble = 6]
+% uploadconfig = {
+% pkg = "vertbars",
+% version = "v1.0c",
+% author = "Peter R Wilson; Will Robertson",
+% license = "lppl1.3c",
+% summary = "Mark vertical rules in margin of text",
+% ctanPath = "/macros/latex/contrib/vertbars",
+% repository = "https://github.com/wspr/herries-press/",
+% update = true,
+% }
+% \end{lstlisting}
+% \caption{Example of \texttt{uploadconfig} from the \pkg{vertbars} package.}
+% \label{fig:uploadconfig}
+% \end{figure}
+%
+% \section{Lua interfaces}
+%
+% Whilst for the majority of users the simple variable-based control methods
+% outlined above will suffice, for more advanced applications there will be
+% a need to adjust behavior by using interfaces within the Lua code. This
+% section details the global variables and functions provided.
+%
+% \subsection{Global variables}
+%
+% \begin{variable}{options}
+% The |options| table holds the values passed to \pkg{l3build} at the
+% command line. The possible entries in the table are given in the table
+% below.
+% \begin{center}
+% \begin{tabular}{ll}
+% \toprule
+% Entry & Type \\
+% \midrule
+% \var{config} & Table \\
+% \var{date} & String \\
+% \var{dirty} & Boolean \\
+% \var{dry-run} & Boolean \\
+% \var{email} & String \\
+% \var{engine} & Table \\
+% \var{epoch} & String \\
+% \var{file} & string \\
+% \var{first} & Boolean \\
+% \var{force} & Boolean \\
+% \var{full} & Boolean \\
+% \var{halt-on-error} & Boolean \\
+% \var{help} & Boolean \\
+% \var{message} & string \\
+% \var{names} & Table \\
+% \var{quiet} & Boolean \\
+% \var{rerun} & Boolean \\
+% \var{shuffle} & Boolean \\
+% \var{texmfhome} & String \\
+% \bottomrule
+% \end{tabular}
+% \end{center}
+% \end{variable}
+%
+% \subsection{Utility functions}
+%
+% The utility functions are largely focussed on file operations, though a small
+% number of others are provided. File paths should be given in Unix style
+% (using |/| as a path separator). File operations take place relative to the
+% path from which \pkg{l3build} is called. File operation syntax is largely
+% modelled on Unix command line commands but reflect the need to work on
+% Windows in a flexible way.
+%
+% \begin{function}{abspath()}
+% \begin{syntax}
+% |abspath(|\meta{target}|)|
+% \end{syntax}
+% Returns a string which gives the absolute location of the
+% \meta{target} directory.
+% \end{function}
+%
+% \begin{function}{dirname()}
+% \begin{syntax}
+% |dirname(|\meta{file}|)|
+% \end{syntax}
+% Returns a string comprising the path to a \meta{file} with the name removed
+% (\emph{i.e.}~up to the last |/|). Where the \meta{file} has no path data,
+% |"."| is returned.
+% \end{function}
+%
+% \begin{function}{basename()}
+% \begin{syntax}
+% |basename(|\meta{file}|)|
+% \end{syntax}
+% Returns a string comprising the full name of the \meta{file} with the
+% path removed (\emph{i.e.}~from the last |/| onward).
+% \end{function}
+%
+% \begin{function}{cleandir()}
+% \begin{syntax}
+% |cleandir(|\meta{dir}|)|
+% \end{syntax}
+% Removes any content within the \meta{dir}; returns an error level.
+% \end{function}
+%
+% \begin{function}{cp()}
+% \begin{syntax}
+% |cp(|\meta{glob}, \meta{source}, \meta{destination}|)|
+% \end{syntax}
+% Copies files matching the \meta{glob} from the \meta{source} directory
+% to the \meta{destination}; returns an error level.
+% \end{function}
+%
+% \begin{function}{direxists()}
+% \begin{syntax}
+% |direxists(|\meta{dir}|)|
+% \end{syntax}
+% Tests if the \meta{dir} exists; returns a boolean value.
+% \end{function}
+%
+% \begin{function}{fileexists()}
+% \begin{syntax}
+% |fileexists(|\meta{file}|)|
+% \end{syntax}
+% Tests if the \meta{file} exists; returns a boolean value.
+% \end{function}
+%
+% \begin{function}{filelist()}
+% \begin{syntax}
+% |filelist(|\meta{path}, \oarg{glob}|)|
+% \end{syntax}
+% Returns a table containing all of the files with the \meta{path}
+% which match the \meta{glob}; if the latter is absent returns a list of
+% all files in the \meta{path}.
+% \end{function}
+%
+% \begin{function}{glob_to_pattern()}
+% \begin{syntax}
+% |glob_to_pattern(|\meta{glob}|)|
+% \end{syntax}
+% Returns the \meta{glob} converted to a Lua pattern.
+% \end{function}
+%
+% \begin{function}{jobname()}
+% \begin{syntax}
+% |jobname(|\meta{file}|)|
+% \end{syntax}
+% Returns a string comprising the jobname of the file with the
+% path and extension removed (\emph{i.e.}~from the last |/| up to the
+% last |.|).
+% \end{function}
+%
+% \begin{function}{mkdir()}
+% \begin{syntax}
+% |mkdir(|\meta{dir}|)|
+% \end{syntax}
+% Creates the \meta{dir}; returns an error level.
+% \end{function}
+%
+% \begin{function}{ren()}
+% \begin{syntax}
+% |ren(|\meta{dir}, \meta{source}, \meta{destination}|)|
+% \end{syntax}
+% Renames the \meta{source} file to the \meta{destination} name within
+% the \meta{dir}; returns an error level.
+% \end{function}
+%
+% \begin{function}{rm()}
+% \begin{syntax}
+% |rm(|\meta{dir}, \meta{glob}|)|
+% \end{syntax}
+% Removes files in the \meta{dir} matching the \meta{glob}; returns an
+% error level.
+% \end{function}
+%
+% \begin{function}{run()}
+% \begin{syntax}
+% |run(|\meta{dir}, \meta{cmd}|)|
+% \end{syntax}
+% Executes the \meta{cmd}, starting it in the \meta{dir}; returns an
+% error level.
+% \end{function}
+%
+% \begin{function}{splitpath()}
+% \begin{syntax}
+% |splitpath(|\meta{file}|)|
+% \end{syntax}
+% Returns two strings split at the last |/|: the \texttt{dirname()} and
+% the |basename()|.
+% \end{function}
+%
+% \begin{function}{normalize_path()}
+% \begin{syntax}
+% |normalize_path(|\meta{path}|)|
+% \end{syntax}
+% When called on Windows, returns a string comprising the \meta{path} with
+% |/| characters replaced by |\\|. In other cases returns the path unchanged.
+% \end{function}
+%
+% \subsection{System-dependent strings}
+%
+% To support creation of additional functionality, the following low-level
+% strings are exposed by \pkg{l3build}: these all have system-dependent
+% definitions and avoid the need to test |os.type| during the construction
+% of system calls.
+%
+% \begin{variable}{os_concat}
+% The concatenation operation for using multiple commands in one
+% system call, \emph{e.g.}
+% \begin{verbatim}
+% os.execute("tex " .. file .. os_concat .. "tex " .. file)
+% \end{verbatim}
+% \end{variable}
+%
+% \begin{variable}{os_null}
+% The location to redirect commands which should produce no output
+% at the terminal: almost always used preceded by |>|, \emph{e.g.}
+% \begin{verbatim}
+% os.execute("tex " .. file .. " > " .. os_null)
+% \end{verbatim}
+% \end{variable}
+%
+% \begin{variable}{os_pathsep}
+% The separator used when setting an environment variable to multiple
+% paths, \emph{e.g.}
+% \begin{verbatim}
+% os.execute(os_setenv .. " PATH=../a" .. os_pathsep .. "../b")
+% \end{verbatim}
+% \end{variable}
+%
+% \begin{variable}{os_setenv}
+% The command to set an environmental variable, \emph{e.g.}
+% \begin{verbatim}
+% os.execute(os_setenv .. " PATH=../a")
+% \end{verbatim}
+% \end{variable}
+%
+% \begin{variable}{os_yes}
+% A command to generate a series of $200$ lines each containing the
+% character |y|: this is useful as the Unix |yes| command cannot be
+% used inside |os.execute| (it does not terminate).
+% \end{variable}
+%
+% \subsection{Components of \texttt{l3build}}
+%
+% \begin{function}{call()}
+% \begin{syntax}
+% |call(|\meta{dirs}, \meta{target}, \oarg{options}|)|
+% \end{syntax}
+% Runs the \texttt{l3build} \meta{target} (a string) for each directory in the
+% \meta{dirs} (a table). This will pass command line options for the parent
+% script to the child processes. The \meta{options} table should take the
+% same form as the global \meta{options}, described above. If it is
+% absent then the global list is used. Note that any entry for the
+% |target| in this table is ignored.
+% \end{function}
+%
+% \begin{function}{install_files()}
+% \begin{syntax}
+% |install_files(|\meta{target},\meta{full},\meta{dry-run}|)|
+% \end{syntax}
+% Installs the files from the module into the TDS root \meta{target}.
+% If \meta{full} is \texttt{true}, all files are copied: if it is
+% \texttt{false}, the \texttt{doc} and \texttt{source} trees are skipped.
+% If \meta{dry-run} is \texttt{true}, no files are copied, but instead the
+% files which would be copied are reported.
+% \end{function}
+%
+% \subsection{Typesetting functions}
+%
+% All typetting functions return $0$ on a successful completion.
+%
+% \begin{function}{biber()}
+% \begin{syntax}
+% |biber(|\meta{name},\meta{dir}|)|
+% \end{syntax}
+% Runs Biber on the \meta{name} (\emph{i.e.}~a jobname lacking any
+% extension) inside the \meta{dir}. If there is no |.bcf| file then
+% no action is taken with a return value of $0$.
+% \end{function}
+%
+% \begin{function}{bibtex()}
+% \begin{syntax}
+% |bibtex(|\meta{name},\meta{dir}|)|
+% \end{syntax}
+% Runs Bib\TeX{} on the \meta{name} (\emph{i.e.}~a jobname lacking any
+% extension) inside the \meta{dir}. If there are no |\citation| lines in
+% the |.aux| file then no action is taken with a return value of $0$.
+% \end{function}
+%
+% \begin{function}{makeindex()}
+% \begin{syntax}
+% |makeindex(|\meta{name},\meta{dir},\meta{inext},\meta{outext},\meta{logext},\meta{style}|)|
+% \end{syntax}
+% Runs MakeIndex on the \meta{name} (\emph{i.e.}~a jobname lacking any
+% extension) inside the \meta{dir}. The various extensions and the \meta{style}
+% should normally be given as it standard for MakeIndex.
+% \end{function}
+%
+% \begin{function}{tex()}
+% \begin{syntax}
+% |tex(|\meta{file},\meta{dir}|)|
+% \end{syntax}
+% Runs \luavar{typesetexe} on the \meta{name} inside the \meta{dir}.
+% \end{function}
+%
+% \begin{function}{runcmd()}
+% \begin{syntax}
+% |runcmd(|\meta{cmd},\meta{dir},|{|\meta{envvars}|})|
+% \end{syntax}
+% A generic function which runs the \meta{cmd} in the \meta{dir}, first
+% setting up all of the environmental variables specified to
+% point to the |local| and |working| directories. This function is useful
+% when creating non-standard typesetting steps.
+% \end{function}
+%
+% \subsection{Customising the target list}
+%
+% The targets known to \pkg{l3build} are stored in the global table
+% |target_list|. Each entry should have at least a |func|, pointing to the
+% function used to implement the target. This function will receive the
+% list of names given at the command line as a table argument.
+% In most cases, targets will also have a |desc|, used to construct |help()|
+% automatically. In addition, the following may also be used:
+% \begin{itemize}
+% \item |bundle_func| A variant of |func| used when at the top level of
+% a bundle
+% \item |bundle_target| A boolean to specify that when passing the target
+% name in a bundle, it should have |bundle| prepended.
+% \item |pre| A function executed before the main function, and receiving
+% the |names| as an argument; this allows checking of the |name| data
+% without impact on the main |func|.
+% \end{itemize}
+%
+% \subsection{Customising the manifest file}
+% \label{sec:manifest}
+%
+% The default setup for the manifest file creating with the \texttt{manifest}
+% target attempt to reflect the defaults for \pkg{l3build} itself.
+% The groups (and hence the files) displayed can be completely
+% customised by defining a new setup function which creates a Lua table with
+% the appropriate settings (\S\ref{sec:manifest-groups}).
+%
+% The formatting within the manifest file can be customised by redefining a number
+% of Lua functions. This includes
+% how the files are sorted within each group (\S\ref{sec:manifest-sorting}),
+% the inclusion of one-line descriptions for each file (\S\ref{sec:manifest-desc}),
+% and the details of the formatting of each entry (\S\ref{sec:manifest-formatting}).
+%
+% To perform such customisations, either include the re-definitions directly within your
+% package's |build.lua| file, or make a copy of |l3build-manifest-setup.lua|, rename it,
+% and load it within your |build.lua| using |dofile()|.
+%
+%
+% \subsubsection{Custom manifest groups}
+% \label{sec:manifest-groups}
+%
+% The setup code for defining each group of files within the manifest looks something like
+% the following:
+% \begin{verbatim}
+% manifest_setup = function()
+% local groups = {
+% {
+% subheading = "Repository files",
+% description = [[
+% Files located in the package development repository.
+% ]],
+% },
+% {
+% name = "Source files",
+% description = [[
+% These are source files generating the package files.
+% ]],
+% files = {sourcefiles},
+% },
+% {
+% name = "Typeset documentation source files",
+% description = [[
+% These files are typeset using LaTeX to produce the PDF documentation for the package.
+% ]],
+% files = {typesetfiles,typesetsourcefiles,typesetdemofiles},
+% },
+% ...
+% }
+% return groups
+% end
+% \end{verbatim}
+%
+% The |groups| variable is an ordered array of tables which contain the metadata about each
+% `group' in the manifest listing.
+% The keys supported in these tables are outlined in Table~\ref{tab:manifest-setup} and Table~\ref{tab:manifest-subheadings}
+% See the complete setup code in |l3build-manifest-setup.lua| for examples of these in use.
+%
+% \begin{table}
+% \caption{Table entries used in the manifest setup table for a group.}
+% \label{tab:manifest-setup}
+% \centering
+% \begin{tabular}{lp{8cm}}
+% \toprule
+% Entry & Description \\
+% \midrule
+% \var{name} & The heading of the group \\
+% \var{description} & The description printed below the heading \\
+% \var{files} & Files to include in this group \\
+% \var{exclude} & Files to exclude (default |{excludefiles}|) \\
+% \var{dir} & The directory to search (default |maindir|) \\
+% \var{rename} & An array with a |gsub| redefinition for the filename \\
+% \var{skipfiledescription} & Whether to extract file descriptions from these files (default |false|) \\
+% \bottomrule
+% \end{tabular}
+% \end{table}
+%
+% \begin{table}
+% \caption{Table entries used in the manifest setup table for a subheading.}
+% \label{tab:manifest-subheadings}
+% \centering
+% \begin{tabular}{lp{8cm}}
+% \toprule
+% Entry & Description \\
+% \midrule
+% \var{subheading} & The subheading \\
+% \var{description} & The description printed below the subheading \\
+% \bottomrule
+% \end{tabular}
+% \end{table}
+%
+%
+% \subsubsection{Sorting within each manifest group}
+% \label{sec:manifest-sorting}
+%
+% Within a single group in the manifest listing, files can be matched against multiple variables.
+% For example, for |sourcefiles={*.dtx,*.ins}| the following (unsorted) file listing might result:
+% \begin{itemize}[nosep]
+% \item foo.dtx
+% \item bar.dtx
+% \item foo.ins
+% \item bar.ins
+% \end{itemize}
+% This listing can be sorted using two separate functions by the default manifest code.
+% The first, default, is to sort alphabetically within a single variable match.
+% This keeps all files of a single extension contiguous in the listing.
+% To edit how this sort is performed, redefine the |manifest_sort_within_match| function.
+%
+% The second approach to sorting is to apply a sorting function to the entire set of matched files.
+% (This happens \emph{after} any sorting is applied for each match.)
+% By default this is a no-op but can be edited by redefining the |manifest_sort_within_group|
+% function. For example:
+% \begin{verbatim}
+% manifest_sort_within_group = function(files)
+% local f = files
+% table.sort(f)
+% return f
+% end
+% \end{verbatim}
+% This will produce an alphabetical listing of files:
+% \begin{itemize}
+% \item bar.dtx
+% \item bar.ins
+% \item foo.dtx
+% \item foo.ins
+% \end{itemize}
+%
+%
+% \subsubsection{File descriptions}
+% \label{sec:manifest-desc}
+%
+% By default the manifest contains lists of files, and with a small addition these
+% lists can be augmented with a one-line summary of each file.
+% If the Lua function |manifest_extract_filedesc| is defined, it will be used to search
+% the contents of each file to extract a description for that file.
+% For example, perhaps you are using multiple |.dtx| files for a project and the argument
+% to the first |\section| in each can be used as a file description:
+% \begin{verbatim}
+% manifest_extract_filedesc = function(filehandle,filename)
+%
+% local all_file = filehandle:read("*all")
+% local matchstr = "\\section{(.-)}"
+%
+% filedesc = string.match(all_file,matchstr)
+%
+% return filedesc
+% end
+% \end{verbatim}
+% (Note the |matchstr| above is only an example and doesn't handle nested braces.)
+%
+%
+% \subsubsection{Custom formatting}
+% \label{sec:manifest-formatting}
+%
+% After the manifest code has built a complete listing of files to print, a series of
+% file writing operations are performed which create the manifest file.
+% The following functions can be re-defined to change the formatting of the manifest file:
+% \begin{itemize}
+% \item |manifest_write_opening|: Write the heading of the manifest file and its opening paragraph.
+% \item |manifest_write_subheading|: Write a subheading and description
+% \item |manifest_write_group_heading|: Write the section heading of the manifest group and the group description
+% \item |manifest_write_group_file|: Write the filename (when not writing file descriptions)
+% \item |manifest_write_group_file_descr|: Write the filename and the file description
+% \end{itemize}
+% Full descriptions of their usage and arguments can be found within the |l3build-manifest-setup.lua|
+% code itself.
+%
+%
+% \end{documentation}
+%
+% \begin{implementation}
+%
+% \clearpage
+% \section{\texttt{regression-test.tex}}
+%
+% This section describes the code for setting up the regression test system.
+% Each test file should start with |\input regression-test.tex\relax|.
+%
+% \begin{macrocode}
+%<*package>
+% \end{macrocode}
+%
+% \subsection{Preliminaries}
+%
+% We require \eTeX{}.
+% \begin{macrocode}
+\begingroup\expandafter\expandafter\expandafter\endgroup
+\expandafter\ifx\csname eTeXversion\endcsname\relax
+ \errmessage{e-TeX is required to use regression-test.tex}%
+ \expandafter\endinput
+\fi
+% \end{macrocode}
+%
+% \begin{macro}{\reset@catcodes}
+% Unlike in the \LaTeXe{} regression test suite, reset catcodes: each test
+% should set these as appropriate. There's also a quick test for Con\TeX{}t:
+% it defines |\unprotect| which should be a reasonable marker (and is
+% needed as some catcodes are otherwise wrong).
+% \begin{macrocode}
+\ifx\unprotect\undefined
+ \expandafter\edef\csname reset\string @catcodes\endcsname{%
+ \catcode`\noexpand\@=\the\catcode`\@\relax
+ }%
+ \catcode`\@=11 %
+\else
+ \unprotect
+ \def\reset@catcodes{\protect}%
+\fi
+% \end{macrocode}
+% \end{macro}
+%
+% Put \TeX{} into scroll mode, and stop it showing the
+% implementation details of macros in error messages.
+% \begin{macrocode}
+\ifnum\interactionmode>1 \scrollmode\fi
+\errorcontextlines=-1 %
+% \end{macrocode}
+%
+% Show all box details: this avoids getting variable results if boxes
+% have different numbers of lines (\LuaTeX{} adds extra information).
+% \begin{macrocode}
+\showboxbreadth=\maxdimen
+\showboxdepth=\maxdimen
+% \end{macrocode}
+%
+% \begin{macro}{\logginoutput}
+% The |\showoutput| command of \LaTeX{} calls |\loggingoutput| which in turn
+% calls |\errorstopmode| but we want to continue running without any stops.
+% \begin{macrocode}
+\def\loggingoutput{%
+ \tracingoutput=1 %
+ \showboxbreadth=\maxdimen
+ \showboxdepth=\maxdimen
+}
+% \end{macrocode}
+% \end{macro}
+%
+% Set the newline character: \LaTeXe{} does this but plain-based formats do
+% not.
+% \begin{macrocode}
+\newlinechar=`\^^J
+% \end{macrocode}
+%
+% \subsection{Commands in test files}
+%
+% \begin{macro}{\LONGTYPEOUT, \TYPE}
+% A long version of |\typeout|, because tests may contain |\par| tokens.
+% Besides, with that |\TYPE|, we can do
+% |\TYPE { ... \TRUE ... \NEWLINE ... }|.
+% \begin{macrocode}
+\long\def\LONGTYPEOUT#1{%
+ \begingroup
+ \long\def\TYPE##1{##1}%
+ \immediate\write128{#1}%
+ \endgroup
+}
+\let\TYPE\LONGTYPEOUT
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\STARTMESSAGE, \START}
+% Start the test, after the optional |\documentclass|
+% |\begin{document}| commands with |\START|. All lines in the |.log| file
+% before this will be ignored.
+% \begin{macrocode}
+\def\STARTMESSAGE{This is a generated file for the l3build validation system.}
+\def\START{%
+ \LONGTYPEOUT{^^JSTART-TEST-LOG^^J}%
+ \LONGTYPEOUT{^^J%
+ \STARTMESSAGE%
+ ^^J^^JDon't change this file in any respect.%
+ ^^J^^J%
+ }%
+}
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\@@@end, \END}
+% The test should end with |\END| or |\end{document}|
+% or |\bye| in plain \TeX{}.
+% \begin{macrocode}
+\ifx\@@end\@undefined
+ \let\@@@end\end
+\else
+ \let\@@@end\@@end
+\fi
+\def\END{%
+ \ifnum\currentgrouplevel>0 %
+ \LONGTYPEOUT{Bad grouping: \the\currentgrouplevel!}%
+ \fi
+ \ifnum\currentiflevel>2 %
+ \LONGTYPEOUT{Bad conditionals: \the\numexpr\currentiflevel-2!}%
+ \fi
+ \LONGTYPEOUT{^^JEND-TEST-LOG^^J}%
+ \@@@end
+}
+\ifx\@@end\@undefined
+ \let\end\END
+\else
+ \let\@@end\END
+\fi
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\OMIT, \TIMO}
+% Surround commands which produce irrelevant lines in the |.log| file by
+% |\OMIT|\dots|\TIMO|
+% \begin{macrocode}
+\def\OMIT{\LONGTYPEOUT{OMIT}}
+\def\TIMO{\LONGTYPEOUT{TIMO}}
+% \end{macrocode}
+% \end{macro}
+%
+%
+
+% \begin{macro}{\SHOWFILE}
+% Load a file (e.g.,| \jobname.toc|) into the .log file with the usual special
+% characters rendered harmless. Use as |\SHOWFILE{\jobname.aux}|.
+% \begin{macrocode}
+\begingroup % within the scope of this groups each line needs to end in % !
+\catcode`\^^M\active %
+\gdef\SHOWFILE#1{%
+ \typeout{-------- #1 (start) ---------}%
+\IfFileExists{#1}%
+ {\begingroup %
+ \catcode`\^^M\active %
+ \edef^^M{^\string^M^^J}%
+ \everyeof{\noexpand}%
+ \obeyspaces %
+ \@sanitize %
+ \message{\@@input #1 }%
+ \endgroup }%
+ {\message{Not found}}%
+ \typeout{-------- #1 (end) -----------}%
+}%
+\endgroup
+% \end{macrocode}
+% \end{macro}
+%
+% To allow testing of possible changes, we allow extra code to be read
+% in before the test starts. The necessary code should be placed in a
+% file |regression-test.cfg|.
+% \begin{macrocode}
+\ifx\InputIfFileExists\@undefined
+ \newread\@inputcheck
+ \long\def\InputIfFileExists#1#2#3{%
+ \openin\@inputcheck#1\relax
+ \ifeof\@inputcheck
+ \def\reserved@a{#3}%
+ \else
+ \def\reserved@a{#2\input #1\relax}%
+ \fi
+ \closein\@inputcheck
+ \reserved@a
+ }%
+\fi
+\InputIfFileExists{regression-test.cfg}
+ {\LONGTYPEOUT{^^J***^^Jregression-test.cfg in operation^^J***^^J}}{}
+% \end{macrocode}
+%
+% \subsection{Formatting the \texttt{.log} file}
+%
+% \begin{macro}{\gTESTint}
+% For tracking the total number of tests.
+% \begin{macrocode}
+\newcount\gTESTint
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}
+% {
+% \SEPARATOR,
+% \BEGINTEST,
+% \ENDTEST,
+% \TEST,
+% \TESTEXP,
+% \TRUE,
+% \FALSE,
+% \YES,
+% \NO,
+% \NEWLINE
+% }
+% We are not starved for space in the log file output, so let's make it as
+% verbose as is useful when reading the |.diff|'s.
+% \begin{macrocode}
+\def\SEPARATOR{%
+ \TYPE{%
+ ============================================================%
+ }%
+}
+\protected\long\def\BEGINTEST#1{%
+ \global\advance\gTESTint by 1 %
+ \SEPARATOR
+ \LONGTYPEOUT{TEST \the\gTESTint: \detokenize{#1}}%
+ \SEPARATOR
+ \begingroup
+ \let\TYPE\LONGTYPEOUT
+}
+\protected\long\def\ENDTEST{%
+ \endgroup
+ \SEPARATOR
+ \LONGTYPEOUT{}%
+}
+\long\def\TEST#1#2{%
+ \BEGINTEST{#1}%
+ #2%
+ \ENDTEST
+}
+\long\def\TESTEXP#1#2{%
+ \BEGINTEST{#1}%
+ \LONGTYPEOUT{#2}%
+ \ENDTEST
+}
+\def \TRUE {\TYPE{TRUE}}
+\def \FALSE {\TYPE{FALSE}}
+\def \YES {\TYPE{YES}}
+\def \NO {\TYPE{NO}}
+\def \NEWLINE {\TYPE{^^J}}
+% \end{macrocode}
+% \end{macro}
+%
+% \subsection{Suppressing variable data in output}
+%
+% Load the map file early so it does not appear in the log.
+% \begin{macrocode}
+\ifx\pdfoutput\@undefined
+ \ifx\outputmode\@undefined
+ \else
+ \ifnum\outputmode>0 %
+ \pdfextension mapfile{pdftex.map}%
+ \fi
+ \fi
+\else
+ \ifnum\pdfoutput>0 %
+ \pdfmapfile{pdftex.map}%
+ \fi
+\fi
+% \end{macrocode}
+%
+% To make any PDF file produced comparable we need to suppress various
+% pieces of data. This works in concert with setting the epoch from the
+% environment side (as not all output can be controlled here). We are
+% somewhat stuck if \texttt{dvips} is being used so just hope for the best!
+% There is a limit to what can be done with the underlying PDF structure so
+% there is no point entirely suppressing \texttt{Producer}: simply avoid
+% any version numbers.
+% \begin{macrocode}
+\ifnum 0%
+ \ifx\pdfoutput\@undefined\else\ifnum\pdfoutput>0 1\fi\fi
+ \ifx\outputmode\@undefined\else\ifnum\outputmode>0 1\fi\fi
+ >0 %
+ \ifx\pdfvariable\@undefined
+ \pdfinfo{/Producer (\ifx\directlua\@undefined pdf\else Lua\fi TeX)}
+ \ifx\pdfinfoomitdate\@undefined\else
+ \pdfinfoomitdate = 1 %
+ \pdfsuppressptexinfo = \numexpr
+ 0
+ + 1 % PTEX.Fullbanner
+ + 2 % PTEX.FileName
+ \relax
+ \pdftrailerid{}
+ \fi
+ \else
+ \pdfextension info{/Producer (LuaTeX)}
+ \pdfvariable suppressoptionalinfo \numexpr
+ 0
+ + 1 % PTEX.Fullbanner
+ + 2 % PTEX.FileName
+ + 32 % CreationDate
+ + 64 % ModDate
+ + 512 % ID
+ \relax
+ \fi
+\else
+ \ifnum 0%
+ \ifx\XeTeXversion\@undefined\else 1\fi
+ \ifx\kanjiskip\@undefined\else 1\fi
+ >0 %
+ \special{%
+ pdf: docinfo
+ <<
+ /Creator (TeX)
+ /CreationDate ()
+ /ModDate ()
+ /Producer (\ifx\XeTeXversion\@undefined\else x\fi dvipdfmx)
+ >>
+ }
+ \fi
+\fi
+% \end{macrocode}
+%
+% Disable compression in PDF output.
+% \begin{macrocode}
+\ifnum 0%
+ \ifx\pdfoutput\@undefined\else\ifnum\pdfoutput>0 1\fi\fi
+ \ifx\outputmode\@undefined\else\ifnum\outputmode>0 1\fi\fi
+ >0 %
+ \ifx\pdfvariable\@undefined
+ \pdfcompresslevel=0 %
+ \pdfobjcompresslevel=0 %
+ \else
+ \pdfvariable compresslevel=0 %
+ \pdfvariable objcompresslevel=0 %
+ \fi
+\else
+ \ifnum 0%
+ \ifx\XeTeXversion\@undefined\else 1\fi
+ \ifx\kanjiskip\@undefined\else 1\fi
+ >0 %
+ \special{dvipdfmx:config z 0}% Compress level
+ \special{dvipdfmx:config C 0x40}% Object compression
+ \fi
+\fi
+% \end{macrocode}
+%
+% Finish up.
+% \begin{macrocode}
+\reset@catcodes
+% \end{macrocode}
+%
+% \begin{macrocode}
+%</package>
+% \end{macrocode}
+%
+% \newpage
+% \section{\texttt{l3build.lua}}
+%
+% This section consists of the |l3build.lua| code.
+% This code is \emph{not} generated from |l3build.dtx| since it needs to already be extracted to build \pkg{l3build} itself!
+% As Frank says, we don't want to end up with a double M\"unchhausen.
+%
+% \lstinputlisting
+% [
+% basicstyle=\ttfamily\scriptsize,
+% numbers=left,
+% numberstyle={\tiny\color[gray]{0.4}},
+% language={[5.2]Lua},
+% procnamekeys=function,
+% procnamestyle=\color{red},
+% ]
+% {l3build.lua}
+%
+% \end{implementation}
+%
+% \PrintIndex
diff --git a/macros/latex/contrib/l3build/l3build.ins b/macros/latex/contrib/l3build/l3build.ins
new file mode 100644
index 0000000000..483bab9d4a
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build.ins
@@ -0,0 +1,56 @@
+\iffalse meta-comment
+
+File l3build.ins Copyright (C) 2014-2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/latex3
+
+for those people who are interested.
+
+-----------------------------------------------------------------------
+
+Any modification of this file should ensure that the copyright and
+license information is placed in the derived files.
+
+\fi
+
+\input docstrip.tex
+\askforoverwritefalse
+
+\preamble
+
+Copyright (C) 2014-2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of
+the LaTeX Project Public License (LPPL), either version 1.3c of
+this license or (at your option) any later version. The latest
+version of this license is in the file:
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+\endpreamble
+% stop docstrip adding \endinput
+\postamble
+\endpostamble
+
+\keepsilent
+
+\generate{\file{regression-test.tex}{\from{l3build.dtx}{package}}}
+
+\endbatchfile
diff --git a/macros/latex/contrib/l3build/l3build.lua b/macros/latex/contrib/l3build/l3build.lua
new file mode 100644
index 0000000000..627778ef22
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build.lua
@@ -0,0 +1,189 @@
+#!/usr/bin/env texlua
+
+--[[
+
+File l3build.lua Copyright (C) 2014-2018 The LaTeX3 Project
+
+It may be distributed and/or modified under the conditions of the
+LaTeX Project Public License (LPPL), either version 1.3c of this
+license or (at your option) any later version. The latest version
+of this license is in the file
+
+ http://www.latex-project.org/lppl.txt
+
+This file is part of the "l3build bundle" (The Work in LPPL)
+and all files in that bundle must be distributed together.
+
+-----------------------------------------------------------------------
+
+The development version of the bundle can be found at
+
+ https://github.com/latex3/l3build
+
+for those people who are interested.
+
+--]]
+
+-- Version information
+release_date = "2019-08-24"
+
+-- File operations are aided by the LuaFileSystem module
+local lfs = require("lfs")
+
+-- Local access to functions
+
+local assert = assert
+local ipairs = ipairs
+local insert = table.insert
+local lookup = kpse.lookup
+local match = string.match
+local gsub = string.gsub
+local next = next
+local print = print
+local select = select
+local tonumber = tonumber
+local exit = os.exit
+
+-- l3build setup and functions
+kpse.set_program_name("kpsewhich")
+build_kpse_path = match(lookup("l3build.lua"),"(.*[/])")
+local function build_require(s)
+ require(lookup("l3build-"..s..".lua", { path = build_kpse_path } ) )
+end
+
+-- Minimal code to do basic checks
+build_require("arguments")
+build_require("help")
+
+build_require("file-functions")
+build_require("typesetting")
+build_require("aux")
+build_require("clean")
+build_require("check")
+build_require("ctan")
+build_require("install")
+build_require("unpack")
+build_require("manifest")
+build_require("manifest-setup")
+build_require("tagging")
+build_require("upload")
+build_require("stdmain")
+
+-- This has to come after stdmain(),
+-- and that has to come after the functions are defined
+if options["target"] == "help" then
+ help()
+ exit(0)
+elseif options["target"] == "version" then
+ version()
+ exit(0)
+end
+
+-- Allow main function to be disabled 'higher up'
+main = main or stdmain
+
+-- Load configuration file if running as a script
+if match(arg[0], "l3build$") or match(arg[0], "l3build%.lua$") then
+ -- Look for some configuration details
+ if fileexists("build.lua") then
+ dofile("build.lua")
+ else
+ print("Error: Cannot find configuration build.lua")
+ exit(1)
+ end
+end
+
+-- Load standard settings for variables:
+-- comes after any user versions
+build_require("variables")
+
+-- Ensure that directories are 'space safe'
+maindir = escapepath(maindir)
+docfiledir = escapepath(docfiledir)
+sourcefiledir = escapepath(sourcefiledir)
+supportdir = escapepath(supportdir)
+testfiledir = escapepath(testfiledir)
+testsuppdir = escapepath(testsuppdir)
+builddir = escapepath(builddir)
+distribdir = escapepath(distribdir)
+localdir = escapepath(localdir)
+testdir = escapepath(testdir)
+typesetdir = escapepath(typesetdir)
+unpackdir = escapepath(unpackdir)
+
+-- Tidy up the epoch setting
+-- Force an epoch if set at the command line
+-- Must be done after loading variables, etc.
+if options["epoch"] then
+ epoch = options["epoch"]
+ forcecheckepoch = true
+ forcedocepoch = true
+end
+normalise_epoch()
+
+-- Sanity check
+check_engines()
+
+--
+-- Deal with multiple configs for tests
+--
+
+-- When we have specific files to deal with, only use explicit configs
+-- (or just the std one)
+if options["names"] then
+ checkconfigs = options["config"] or {stdconfig}
+else
+ checkconfigs = options["config"] or checkconfigs
+end
+
+if options["target"] == "check" then
+ if #checkconfigs > 1 then
+ local errorlevel = 0
+ local opts = options
+ local failed = { }
+ for i = 1, #checkconfigs do
+ opts["config"] = {checkconfigs[i]}
+ errorlevel = call({"."}, "check", opts)
+ if errorlevel ~= 0 then
+ if options["halt-on-error"] then
+ exit(1)
+ else
+ insert(failed,checkconfigs[i])
+ end
+ end
+ end
+ if next(failed) then
+ for _,config in ipairs(failed) do
+ print("Failed tests for configuration " .. config .. ":")
+ print("\n Check failed with difference files")
+ local testdir = testdir
+ if config ~= "build" then
+ testdir = testdir .. "-" .. config
+ end
+ for _,i in ipairs(filelist(testdir,"*" .. os_diffext)) do
+ print(" - " .. testdir .. "/" .. i)
+ end
+ print("")
+ end
+ exit(1)
+ else
+ -- Avoid running the 'main' set of tests twice
+ exit(0)
+ end
+ end
+end
+if #checkconfigs == 1 and
+ checkconfigs[1] ~= "build" and
+ (options["target"] == "check" or options["target"] == "save") then
+ local config = "./" .. gsub(checkconfigs[1],".lua$","") .. ".lua"
+ if fileexists(config) then
+ dofile(config)
+ testdir = testdir .. "-" .. checkconfigs[1]
+ else
+ print("Error: Cannot find configuration " .. checkconfigs[1])
+ exit(1)
+ end
+end
+
+-- Call the main function
+main(options["target"], options["names"])
diff --git a/macros/latex/contrib/l3build/l3build.pdf b/macros/latex/contrib/l3build/l3build.pdf
new file mode 100644
index 0000000000..41ff401a00
--- /dev/null
+++ b/macros/latex/contrib/l3build/l3build.pdf
Binary files differ