├── .github ├── CONTRIBUTING.md ├── dependabot.yml ├── tl_packages ├── typos.toml └── workflows │ ├── deploy.yaml │ └── main.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.lua ├── config-context.lua ├── config-pdf.lua ├── config-plain.lua ├── examples ├── Bundle-Flat │ ├── Module-One │ │ ├── build.lua │ │ ├── module-one-code.tex │ │ ├── module-one.dtx │ │ ├── module-one.ins │ │ ├── module-one.tex │ │ └── testfiles │ │ │ ├── module-one-001.lvt │ │ │ └── module-one-001.tlg │ ├── Module-Two │ │ ├── build.lua │ │ ├── module-two-code.tex │ │ ├── module-two.dtx │ │ ├── module-two.ins │ │ ├── module-two.tex │ │ └── testfiles │ │ │ ├── module-two-001.lvt │ │ │ └── module-two-001.tlg │ ├── README.md │ └── build.lua ├── Bundle-Tree │ ├── Module-One │ │ ├── build.lua │ │ ├── code │ │ │ ├── module-one.dtx │ │ │ └── module-one.ins │ │ ├── doc │ │ │ └── module-one-doc.tex │ │ └── testfiles │ │ │ ├── module-one-001.lvt │ │ │ └── module-one-001.tlg │ ├── Module-Two │ │ ├── build.lua │ │ ├── code │ │ │ ├── module-two.dtx │ │ │ └── module-two.ins │ │ ├── doc │ │ │ └── module-two.tex │ │ └── testfiles │ │ │ ├── module-two-001.lvt │ │ │ └── module-two-001.tlg │ ├── README.md │ └── build.lua ├── README.md ├── Simple-Flat │ ├── README.md │ ├── build.lua │ ├── simple-flat-code.tex │ ├── simple-flat.dtx │ ├── simple-flat.ins │ ├── simple-flat.tex │ └── testfiles │ │ ├── simple-flat-001.lvt │ │ └── simple-flat-001.tlg └── Simple-Tree │ ├── README.md │ ├── build.lua │ ├── code │ ├── simple-tree.dtx │ └── simple-tree.ins │ ├── doc │ └── simple-tree-doc.tex │ └── testfiles │ ├── simple-tree-001.lvt │ └── simple-tree-001.tlg ├── l3build-arguments.lua ├── l3build-aux.lua ├── l3build-check.lua ├── l3build-clean.lua ├── l3build-ctan.lua ├── l3build-file-functions.lua ├── l3build-help.lua ├── l3build-install.lua ├── l3build-manifest-setup.lua ├── l3build-manifest.lua ├── l3build-stdmain.lua ├── l3build-tagging.lua ├── l3build-typesetting.lua ├── l3build-unpack.lua ├── l3build-upload.lua ├── l3build-variables.lua ├── l3build-zip.lua ├── l3build.dtx ├── l3build.ins ├── l3build.lua ├── testfiles-context ├── context.lvt └── context.tlg ├── testfiles-pdf ├── 00-test-2.pvt ├── 00-test-2.tpf └── 00-test-2.xetex.tpf ├── testfiles-plain ├── plain-pdftex.lvt ├── plain-pdftex.ptex.tlg ├── plain-pdftex.tlg ├── plain-pdftex.uptex.tlg ├── plain-pdftex.xetex.tlg └── support │ └── regression-test.cfg └── testfiles ├── 00-test-1.luatex.tlg ├── 00-test-1.lvt ├── 00-test-1.ptex.tlg ├── 00-test-1.tlg ├── 00-test-1.uptex.tlg ├── 00-test-1.xetex.tlg ├── 01-expect.dtx ├── 01-expect.ins └── support └── regression-test.cfg /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for considering contributing to `l3build`: feedback, fixes and ideas are 2 | all useful. Here, we ([The LaTeX Project](https://www.latex-project.org)) have 3 | collected together a few pointers to help things along. 4 | 5 | ## Bugs 6 | 7 | Please log bugs using the [issues](https://github.com/latex3/l3build/issues) 8 | system on GitHub. Handy information that you might 9 | include, depending on the nature of the issue, includes 10 | 11 | - Your version of `l3build` (`l3build version`) 12 | - Your TeX system details (for example 'TeX Live 2017') 13 | - Your operating system 14 | - The contents of your `build.lua` file 15 | - An 'ASCII art' explanation of your directory layout 16 | 17 | ## Feature requests 18 | 19 | Feature requests are welcome: log them in the same way as bugs. 20 | We welcome feature requests for the test set up, 21 | the build process, _etc._ 22 | 23 | ## Code contributions 24 | 25 | If you want to discuss a possible contribution before (or instead of) 26 | making a pull request, drop a line to 27 | [the team](mailto:latex-team@latex-project.org). 28 | 29 | There are a few things that might look non-standard to most Lua programmers, 30 | which come about as `l3build`'s focus is testing and building LaTeX packages: 31 | 32 | - Our target Lua set up is `texlua` (part of LuaTeX), not standalone `lua` 33 | - The `l3build` is self-contained as this helps with bootstrapping LaTeX: 34 | we are aiming to maintain `l3build`, currently as a set of `l3build*.lua` 35 | files with no external `.lua` dependencies 36 | - The primary documentation is aimed at the TeX world, so is in PDF format 37 | and generated from `l3build.dtx`; documentation in the `.lua` file is 38 | also welcome, but anything for general use does need to be in the `.dtx` 39 | - As far as possible, everything is done within `l3build` itself or tools 40 | directly available in a TeX system or as standard in the supported 41 | systems (Windows, MacOS, Linux) 42 | - The `l3build` interfaces should be platform-agnostic (though it may be 43 | necessary of course to branch inside particular functions) 44 | 45 | If you are submitting a pull request, notice that 46 | 47 | - We use GitHub Actions for (light) testing so you can test changes on your 48 | fork first 49 | - We favor a single linear history so will rebase agreed pull requests on to 50 | the `main` branch 51 | - Where a commit fixes or closes an issue, please include this information 52 | in the first line of the commit message 53 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/tl_packages: -------------------------------------------------------------------------------- 1 | # The test framework itself 2 | # (A dependency below but explicitly listed) 3 | luatex 4 | # 5 | # Required to build formats 6 | # 7 | context 8 | latex-bin 9 | tex 10 | uplatex 11 | xetex 12 | # Requirements for the tests 13 | etex-pkg 14 | # 15 | # Support for typesetting the docs 16 | # 17 | alphalph 18 | amsmath 19 | booktabs 20 | ec 21 | colortbl 22 | csquotes 23 | enumitem 24 | fancyvrb 25 | hologo 26 | hypdoc 27 | hyperref 28 | infwarerr 29 | iftex 30 | kvoptions 31 | listings 32 | makeindex 33 | needspace 34 | pdftexcmds 35 | psnfss 36 | tools 37 | underscore 38 | -------------------------------------------------------------------------------- /.github/typos.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/crate-ci/typos/blob/master/docs/reference.md 2 | 3 | [files] 4 | extend-exclude =[ 5 | "*.tlg", 6 | "*.tpf", 7 | "LICENSE", 8 | ] 9 | 10 | [default] 11 | extend-ignore-re = [ 12 | "\\.ist\\b", 13 | "\\\\cs\\{openin\\}", 14 | "\\bGhost\\[sS\\]cript\\b", 15 | ] 16 | locale = "en-us" 17 | 18 | [default.extend-words] 19 | nd = "nd" 20 | ND = "ND" 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | # We create releases for all new tags 4 | on: 5 | push: 6 | tags: 7 | - "*" 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | name: Build release 13 | environment: Release 14 | steps: 15 | # Boilerplate 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | # We need Ghostscript for XeTeX tests. 19 | - run: sudo apt-get update && sudo apt-get install ghostscript 20 | - name: Install TeX Live 21 | uses: zauguin/install-texlive@v4 22 | with: 23 | # List the required TeX Live packages in a separate file to allow reuse in 24 | # different workflows. 25 | package_file: .github/tl_packages 26 | # Work around a TL issue 27 | - run: mtxrun --generate && context --luatex --generate 28 | - name: Run l3build 29 | run: texlua l3build.lua ctan -H --show-log-on-error 30 | # Now create the release (this only runs if the previous steps were successful) 31 | - name: Create GitHub release 32 | uses: ncipollo/release-action@v1 33 | with: 34 | artifacts: "build/distrib/ctan/*.zip" 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Automated testing 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Boilerplate 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | # We need Ghostscript for XeTeX tests. 17 | - run: sudo apt-get update && sudo apt-get install ghostscript 18 | - name: Install TeX Live 19 | uses: zauguin/install-texlive@v4 20 | with: 21 | # List the required TeX Live packages in a separate file to allow reuse in 22 | # different workflows. 23 | package_file: .github/tl_packages 24 | # Work around a TL issue 25 | - run: mtxrun --generate && context --luatex --generate 26 | - name: Run l3build 27 | run: texlua l3build.lua check -q -H --show-log-on-error 28 | docs: 29 | runs-on: ubuntu-latest 30 | steps: 31 | # Boilerplate 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | # We need Ghostscript for XeTeX tests. 35 | - run: sudo apt-get update && sudo apt-get install ghostscript 36 | - name: Install TeX Live 37 | uses: zauguin/install-texlive@v4 38 | with: 39 | # List the required TeX Live packages in a separate file to allow reuse in 40 | # different workflows. 41 | package_file: .github/tl_packages 42 | - name: Run l3build 43 | run: texlua l3build.lua doc -q -H 44 | typos: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v4 49 | - name: Check spelling 50 | # run `typos -c .github/typos.toml` to check spelling locally 51 | uses: crate-ci/typos@v1 52 | with: 53 | config: .github/typos.toml 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | 3 | *.zip 4 | 5 | *.pdf 6 | !*/testfiles/*.pdf 7 | 8 | *.*~ 9 | 10 | *.1 11 | *.aux 12 | *.bbl 13 | *.glo 14 | *.gz 15 | *.hd 16 | *.idx 17 | *.ilg 18 | *.ind 19 | *.log 20 | *.out 21 | *.toc 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The LaTeX Project Public License 2 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 3 | 4 | LPPL Version 1.3c 2008-05-04 5 | 6 | Copyright 1999 2002-2008 LaTeX3 Project 7 | Everyone is allowed to distribute verbatim copies of this 8 | license document, but modification of it is not allowed. 9 | 10 | 11 | PREAMBLE 12 | ======== 13 | 14 | The LaTeX Project Public License (LPPL) is the primary license under 15 | which the LaTeX kernel and the base LaTeX packages are distributed. 16 | 17 | You may use this license for any work of which you hold the copyright 18 | and which you wish to distribute. This license may be particularly 19 | suitable if your work is TeX-related (such as a LaTeX package), but 20 | it is written in such a way that you can use it even if your work is 21 | unrelated to TeX. 22 | 23 | The section `WHETHER AND HOW TO DISTRIBUTE WORKS UNDER THIS LICENSE', 24 | below, gives instructions, examples, and recommendations for authors 25 | who are considering distributing their works under this license. 26 | 27 | This license gives conditions under which a work may be distributed 28 | and modified, as well as conditions under which modified versions of 29 | that work may be distributed. 30 | 31 | We, the LaTeX3 Project, believe that the conditions below give you 32 | the freedom to make and distribute modified versions of your work 33 | that conform with whatever technical specifications you wish while 34 | maintaining the availability, integrity, and reliability of 35 | that work. If you do not see how to achieve your goal while 36 | meeting these conditions, then read the document `cfgguide.tex' 37 | and `modguide.tex' in the base LaTeX distribution for suggestions. 38 | 39 | 40 | DEFINITIONS 41 | =========== 42 | 43 | In this license document the following terms are used: 44 | 45 | `Work' 46 | Any work being distributed under this License. 47 | 48 | `Derived Work' 49 | Any work that under any applicable law is derived from the Work. 50 | 51 | `Modification' 52 | Any procedure that produces a Derived Work under any applicable 53 | law -- for example, the production of a file containing an 54 | original file associated with the Work or a significant portion of 55 | such a file, either verbatim or with modifications and/or 56 | translated into another language. 57 | 58 | `Modify' 59 | To apply any procedure that produces a Derived Work under any 60 | applicable law. 61 | 62 | `Distribution' 63 | Making copies of the Work available from one person to another, in 64 | whole or in part. Distribution includes (but is not limited to) 65 | making any electronic components of the Work accessible by 66 | file transfer protocols such as FTP or HTTP or by shared file 67 | systems such as Sun's Network File System (NFS). 68 | 69 | `Compiled Work' 70 | A version of the Work that has been processed into a form where it 71 | is directly usable on a computer system. This processing may 72 | include using installation facilities provided by the Work, 73 | transformations of the Work, copying of components of the Work, or 74 | other activities. Note that modification of any installation 75 | facilities provided by the Work constitutes modification of the Work. 76 | 77 | `Current Maintainer' 78 | A person or persons nominated as such within the Work. If there is 79 | no such explicit nomination then it is the `Copyright Holder' under 80 | any applicable law. 81 | 82 | `Base Interpreter' 83 | A program or process that is normally needed for running or 84 | interpreting a part or the whole of the Work. 85 | 86 | A Base Interpreter may depend on external components but these 87 | are not considered part of the Base Interpreter provided that each 88 | external component clearly identifies itself whenever it is used 89 | interactively. Unless explicitly specified when applying the 90 | license to the Work, the only applicable Base Interpreter is a 91 | `LaTeX-Format' or in the case of files belonging to the 92 | `LaTeX-format' a program implementing the `TeX language'. 93 | 94 | 95 | 96 | CONDITIONS ON DISTRIBUTION AND MODIFICATION 97 | =========================================== 98 | 99 | 1. Activities other than distribution and/or modification of the Work 100 | are not covered by this license; they are outside its scope. In 101 | particular, the act of running the Work is not restricted and no 102 | requirements are made concerning any offers of support for the Work. 103 | 104 | 2. You may distribute a complete, unmodified copy of the Work as you 105 | received it. Distribution of only part of the Work is considered 106 | modification of the Work, and no right to distribute such a Derived 107 | Work may be assumed under the terms of this clause. 108 | 109 | 3. You may distribute a Compiled Work that has been generated from a 110 | complete, unmodified copy of the Work as distributed under Clause 2 111 | above, as long as that Compiled Work is distributed in such a way that 112 | the recipients may install the Compiled Work on their system exactly 113 | as it would have been installed if they generated a Compiled Work 114 | directly from the Work. 115 | 116 | 4. If you are the Current Maintainer of the Work, you may, without 117 | restriction, modify the Work, thus creating a Derived Work. You may 118 | also distribute the Derived Work without restriction, including 119 | Compiled Works generated from the Derived Work. Derived Works 120 | distributed in this manner by the Current Maintainer are considered to 121 | be updated versions of the Work. 122 | 123 | 5. If you are not the Current Maintainer of the Work, you may modify 124 | your copy of the Work, thus creating a Derived Work based on the Work, 125 | and compile this Derived Work, thus creating a Compiled Work based on 126 | the Derived Work. 127 | 128 | 6. If you are not the Current Maintainer of the Work, you may 129 | distribute a Derived Work provided the following conditions are met 130 | for every component of the Work unless that component clearly states 131 | in the copyright notice that it is exempt from that condition. Only 132 | the Current Maintainer is allowed to add such statements of exemption 133 | to a component of the Work. 134 | 135 | a. If a component of this Derived Work can be a direct replacement 136 | for a component of the Work when that component is used with the 137 | Base Interpreter, then, wherever this component of the Work 138 | identifies itself to the user when used interactively with that 139 | Base Interpreter, the replacement component of this Derived Work 140 | clearly and unambiguously identifies itself as a modified version 141 | of this component to the user when used interactively with that 142 | Base Interpreter. 143 | 144 | b. Every component of the Derived Work contains prominent notices 145 | detailing the nature of the changes to that component, or a 146 | prominent reference to another file that is distributed as part 147 | of the Derived Work and that contains a complete and accurate log 148 | of the changes. 149 | 150 | c. No information in the Derived Work implies that any persons, 151 | including (but not limited to) the authors of the original version 152 | of the Work, provide any support, including (but not limited to) 153 | the reporting and handling of errors, to recipients of the 154 | Derived Work unless those persons have stated explicitly that 155 | they do provide such support for the Derived Work. 156 | 157 | d. You distribute at least one of the following with the Derived Work: 158 | 159 | 1. A complete, unmodified copy of the Work; 160 | if your distribution of a modified component is made by 161 | offering access to copy the modified component from a 162 | designated place, then offering equivalent access to copy 163 | the Work from the same or some similar place meets this 164 | condition, even though third parties are not compelled to 165 | copy the Work along with the modified component; 166 | 167 | 2. Information that is sufficient to obtain a complete, 168 | unmodified copy of the Work. 169 | 170 | 7. If you are not the Current Maintainer of the Work, you may 171 | distribute a Compiled Work generated from a Derived Work, as long as 172 | the Derived Work is distributed to all recipients of the Compiled 173 | Work, and as long as the conditions of Clause 6, above, are met with 174 | regard to the Derived Work. 175 | 176 | 8. The conditions above are not intended to prohibit, and hence do not 177 | apply to, the modification, by any method, of any component so that it 178 | becomes identical to an updated version of that component of the Work as 179 | it is distributed by the Current Maintainer under Clause 4, above. 180 | 181 | 9. Distribution of the Work or any Derived Work in an alternative 182 | format, where the Work or that Derived Work (in whole or in part) is 183 | then produced by applying some process to that format, does not relax or 184 | nullify any sections of this license as they pertain to the results of 185 | applying that process. 186 | 187 | 10. a. A Derived Work may be distributed under a different license 188 | provided that license itself honors the conditions listed in 189 | Clause 6 above, in regard to the Work, though it does not have 190 | to honor the rest of the conditions in this license. 191 | 192 | b. If a Derived Work is distributed under a different license, that 193 | Derived Work must provide sufficient documentation as part of 194 | itself to allow each recipient of that Derived Work to honor the 195 | restrictions in Clause 6 above, concerning changes from the Work. 196 | 197 | 11. This license places no restrictions on works that are unrelated to 198 | the Work, nor does this license place any restrictions on aggregating 199 | such works with the Work by any means. 200 | 201 | 12. Nothing in this license is intended to, or may be used to, prevent 202 | complete compliance by all parties with all applicable laws. 203 | 204 | 205 | NO WARRANTY 206 | =========== 207 | 208 | There is no warranty for the Work. Except when otherwise stated in 209 | writing, the Copyright Holder provides the Work `as is', without 210 | warranty of any kind, either expressed or implied, including, but not 211 | limited to, the implied warranties of merchantability and fitness for a 212 | particular purpose. The entire risk as to the quality and performance 213 | of the Work is with you. Should the Work prove defective, you assume 214 | the cost of all necessary servicing, repair, or correction. 215 | 216 | In no event unless required by applicable law or agreed to in writing 217 | will The Copyright Holder, or any author named in the components of the 218 | Work, or any other party who may distribute and/or modify the Work as 219 | permitted above, be liable to you for damages, including any general, 220 | special, incidental or consequential damages arising out of any use of 221 | the Work or out of inability to use the Work (including, but not limited 222 | to, loss of data, data being rendered inaccurate, or losses sustained by 223 | anyone as a result of any failure of the Work to operate with any other 224 | programs), even if the Copyright Holder or said author or said other 225 | party has been advised of the possibility of such damages. 226 | 227 | 228 | MAINTENANCE OF THE WORK 229 | ======================= 230 | 231 | The Work has the status `author-maintained' if the Copyright Holder 232 | explicitly and prominently states near the primary copyright notice in 233 | the Work that the Work can only be maintained by the Copyright Holder 234 | or simply that it is `author-maintained'. 235 | 236 | The Work has the status `maintained' if there is a Current Maintainer 237 | who has indicated in the Work that they are willing to receive error 238 | reports for the Work (for example, by supplying a valid e-mail 239 | address). It is not required for the Current Maintainer to acknowledge 240 | or act upon these error reports. 241 | 242 | The Work changes from status `maintained' to `unmaintained' if there 243 | is no Current Maintainer, or the person stated to be Current 244 | Maintainer of the work cannot be reached through the indicated means 245 | of communication for a period of six months, and there are no other 246 | significant signs of active maintenance. 247 | 248 | You can become the Current Maintainer of the Work by agreement with 249 | any existing Current Maintainer to take over this role. 250 | 251 | If the Work is unmaintained, you can become the Current Maintainer of 252 | the Work through the following steps: 253 | 254 | 1. Make a reasonable attempt to trace the Current Maintainer (and 255 | the Copyright Holder, if the two differ) through the means of 256 | an Internet or similar search. 257 | 258 | 2. If this search is successful, then enquire whether the Work 259 | is still maintained. 260 | 261 | a. If it is being maintained, then ask the Current Maintainer 262 | to update their communication data within one month. 263 | 264 | b. If the search is unsuccessful or no action to resume active 265 | maintenance is taken by the Current Maintainer, then announce 266 | within the pertinent community your intention to take over 267 | maintenance. (If the Work is a LaTeX work, this could be 268 | done, for example, by posting to comp.text.tex.) 269 | 270 | 3a. If the Current Maintainer is reachable and agrees to pass 271 | maintenance of the Work to you, then this takes effect 272 | immediately upon announcement. 273 | 274 | b. If the Current Maintainer is not reachable and the Copyright 275 | Holder agrees that maintenance of the Work be passed to you, 276 | then this takes effect immediately upon announcement. 277 | 278 | 4. If you make an `intention announcement' as described in 2b. above 279 | and after three months your intention is challenged neither by 280 | the Current Maintainer nor by the Copyright Holder nor by other 281 | people, then you may arrange for the Work to be changed so as 282 | to name you as the (new) Current Maintainer. 283 | 284 | 5. If the previously unreachable Current Maintainer becomes 285 | reachable once more within three months of a change completed 286 | under the terms of 3b) or 4), then that Current Maintainer must 287 | become or remain the Current Maintainer upon request provided 288 | they then update their communication data within one month. 289 | 290 | A change in the Current Maintainer does not, of itself, alter the fact 291 | that the Work is distributed under the LPPL license. 292 | 293 | If you become the Current Maintainer of the Work, you should 294 | immediately provide, within the Work, a prominent and unambiguous 295 | statement of your status as Current Maintainer. You should also 296 | announce your new status to the same pertinent community as 297 | in 2b) above. 298 | 299 | 300 | WHETHER AND HOW TO DISTRIBUTE WORKS UNDER THIS LICENSE 301 | ====================================================== 302 | 303 | This section contains important instructions, examples, and 304 | recommendations for authors who are considering distributing their 305 | works under this license. These authors are addressed as `you' in 306 | this section. 307 | 308 | Choosing This License or Another License 309 | ---------------------------------------- 310 | 311 | If for any part of your work you want or need to use *distribution* 312 | conditions that differ significantly from those in this license, then 313 | do not refer to this license anywhere in your work but, instead, 314 | distribute your work under a different license. You may use the text 315 | of this license as a model for your own license, but your license 316 | should not refer to the LPPL or otherwise give the impression that 317 | your work is distributed under the LPPL. 318 | 319 | The document `modguide.tex' in the base LaTeX distribution explains 320 | the motivation behind the conditions of this license. It explains, 321 | for example, why distributing LaTeX under the GNU General Public 322 | License (GPL) was considered inappropriate. Even if your work is 323 | unrelated to LaTeX, the discussion in `modguide.tex' may still be 324 | relevant, and authors intending to distribute their works under any 325 | license are encouraged to read it. 326 | 327 | A Recommendation on Modification Without Distribution 328 | ----------------------------------------------------- 329 | 330 | It is wise never to modify a component of the Work, even for your own 331 | personal use, without also meeting the above conditions for 332 | distributing the modified component. While you might intend that such 333 | modifications will never be distributed, often this will happen by 334 | accident -- you may forget that you have modified that component; or 335 | it may not occur to you when allowing others to access the modified 336 | version that you are thus distributing it and violating the conditions 337 | of this license in ways that could have legal implications and, worse, 338 | cause problems for the community. It is therefore usually in your 339 | best interest to keep your copy of the Work identical with the public 340 | one. Many works provide ways to control the behavior of that work 341 | without altering any of its licensed components. 342 | 343 | How to Use This License 344 | ----------------------- 345 | 346 | To use this license, place in each of the components of your work both 347 | an explicit copyright notice including your name and the year the work 348 | was authored and/or last substantially modified. Include also a 349 | statement that the distribution and/or modification of that 350 | component is constrained by the conditions in this license. 351 | 352 | Here is an example of such a notice and statement: 353 | 354 | %% pig.dtx 355 | %% Copyright 2005 M. Y. Name 356 | % 357 | % This work may be distributed and/or modified under the 358 | % conditions of the LaTeX Project Public License, either version 1.3 359 | % of this license or (at your option) any later version. 360 | % The latest version of this license is in 361 | % https://www.latex-project.org/lppl.txt 362 | % and version 1.3 or later is part of all distributions of LaTeX 363 | % version 2005/12/01 or later. 364 | % 365 | % This work has the LPPL maintenance status `maintained'. 366 | % 367 | % The Current Maintainer of this work is M. Y. Name. 368 | % 369 | % This work consists of the files pig.dtx and pig.ins 370 | % and the derived file pig.sty. 371 | 372 | Given such a notice and statement in a file, the conditions 373 | given in this license document would apply, with the `Work' referring 374 | to the three files `pig.dtx', `pig.ins', and `pig.sty' (the last being 375 | generated from `pig.dtx' using `pig.ins'), the `Base Interpreter' 376 | referring to any `LaTeX-Format', and both `Copyright Holder' and 377 | `Current Maintainer' referring to the person `M. Y. Name'. 378 | 379 | If you do not want the Maintenance section of LPPL to apply to your 380 | Work, change `maintained' above into `author-maintained'. 381 | However, we recommend that you use `maintained', as the Maintenance 382 | section was added in order to ensure that your Work remains useful to 383 | the community even when you can no longer maintain and support it 384 | yourself. 385 | 386 | Derived Works That Are Not Replacements 387 | --------------------------------------- 388 | 389 | Several clauses of the LPPL specify means to provide reliability and 390 | stability for the user community. They therefore concern themselves 391 | with the case that a Derived Work is intended to be used as a 392 | (compatible or incompatible) replacement of the original Work. If 393 | this is not the case (e.g., if a few lines of code are reused for a 394 | completely different task), then clauses 6b and 6d shall not apply. 395 | 396 | 397 | Important Recommendations 398 | ------------------------- 399 | 400 | Defining What Constitutes the Work 401 | 402 | The LPPL requires that distributions of the Work contain all the 403 | files of the Work. It is therefore important that you provide a 404 | way for the licensee to determine which files constitute the Work. 405 | This could, for example, be achieved by explicitly listing all the 406 | files of the Work near the copyright notice of each file or by 407 | using a line such as: 408 | 409 | % This work consists of all files listed in manifest.txt. 410 | 411 | in that place. In the absence of an unequivocal list it might be 412 | impossible for the licensee to determine what is considered by you 413 | to comprise the Work and, in such a case, the licensee would be 414 | entitled to make reasonable conjectures as to which files comprise 415 | the Work. 416 | 417 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | l3build: a testing and building system for LaTeX 2 | ================================================= 3 | 4 | Release 2025-05-08 5 | 6 | Overview 7 | -------- 8 | 9 | The `l3build` module is designed to support the development of 10 | high-quality LaTeX code by providing: 11 | * A unit testing system 12 | * Automated typesetting of code sources 13 | * A reliable packaging system for CTAN releases 14 | 15 | The bundle consists of a Lua script to run the tasks and a 16 | `.tex` file which provides the testing environment. These were 17 | originally developed for supporting LaTeX development but 18 | are designed such that they can be readily used by others. Full 19 | documentation is provided. 20 | 21 | Issues 22 | ------ 23 | 24 | The issue tracker for LaTeX is currently located 25 | [on GitHub](https://github.com/latex3/l3build/issues). 26 | 27 | Development team 28 | ---------------- 29 | 30 | The LaTeX kernel is developed by [The LaTeX Project](https://latex-project.org). 31 | 32 | ----- 33 | 34 |

Copyright (C) 2014-2025 The LaTeX Project
35 | https://latex-project.org/
36 | All rights reserved.

37 | -------------------------------------------------------------------------------- /build.lua: -------------------------------------------------------------------------------- 1 | -- Build script for LaTeX "l3build" files 2 | 3 | -- Identify the bundle and module 4 | module = "l3build" 5 | bundle = "" 6 | 7 | -- Non-standard settings 8 | checkconfigs = {"build", "config-pdf", "config-plain","config-context"} 9 | checkdeps = { } 10 | checkengines = {"pdftex", "xetex", "luatex", "ptex", "uptex"} 11 | cleanfiles = {"*.pdf", "*.tex", "*.zip"} 12 | exefiles = {"l3build.lua"} 13 | installfiles = {"regression-test.tex"} 14 | packtdszip = true 15 | scriptfiles = {"l3build*.lua"} 16 | scriptmanfiles = {"l3build.1"} 17 | sourcefiles = {"*.dtx", "l3build*.lua", "*.ins"} 18 | typesetruns = 4 19 | typesetcmds = "\\AtBeginDocument{\\DisableImplementation}" 20 | unpackdeps = { } 21 | tagfiles = { 22 | "l3build.1", 23 | "l3build.dtx", 24 | "l3build.ins", 25 | "**/*.md", -- to include README.md in ./examples 26 | "l3build*.lua", 27 | "**/regression-test.cfg" 28 | } 29 | 30 | uploadconfig = { 31 | author = "The LaTeX Team", 32 | license = "lppl1.3c", 33 | summary = "A testing and building system for (La)TeX", 34 | topic = {"macro-supp", "package-devel"}, 35 | ctanPath = "/macros/latex/contrib/l3build", 36 | repository = "https://github.com/latex3/l3build/", 37 | bugtracker = "https://github.com/latex3/l3build/issues", 38 | update = true, 39 | description = [[ 40 | The build system supports testing and building (La)TeX code, on 41 | Linux, macOS, and Windows systems. The package offers: 42 | * A unit testing system for (La)TeX code; 43 | * A system for typesetting package documentation; and 44 | * An automated process for creating CTAN releases. 45 | ]] 46 | } 47 | 48 | -- Detail how to set the version automatically 49 | function update_tag(file,content,tagname,tagdate) 50 | local iso = "%d%d%d%d%-%d%d%-%d%d" 51 | local url = "https://github.com/latex3/l3build/compare/" 52 | -- update copyright 53 | local year = os.date("%Y") 54 | local oldyear = math.tointeger(year - 1) 55 | if string.match(content,"%(C%)%s*" .. oldyear .. " The LaTeX Project") then 56 | content = string.gsub(content, 57 | "%(C%)%s*" .. oldyear .. " The LaTeX Project", 58 | "(C) " .. year .. " The LaTeX Project") 59 | elseif string.match(content,"%(C%)%s*%d%d%d%d%-" .. oldyear .. " The LaTeX Project") then 60 | content = string.gsub(content, 61 | "%(C%)%s*(%d%d%d%d%-)" .. oldyear .. " The LaTeX Project", 62 | "(C) %1" .. year .. " The LaTeX Project") 63 | end 64 | -- update release date 65 | if string.match(file, "%.1$") then 66 | return string.gsub(content, 67 | '%.TH l3build 1 "' .. iso .. '"\n', 68 | '.TH l3build 1 "' .. tagname .. '"\n') 69 | elseif string.match(file, "%.dtx$") then 70 | return string.gsub(content, 71 | "\n%% \\date{Released " .. iso .. "}\n", 72 | "\n%% \\date{Released " .. tagname .. "}\n") 73 | elseif string.match(file, "%.md$") then 74 | if string.match(file,"CHANGELOG.md") then 75 | local previous = string.match(content,"compare/(" .. iso .. ")%.%.%.HEAD") 76 | if tagname == previous then return content end 77 | content = string.gsub(content, 78 | "## %[Unreleased%]", 79 | "## [Unreleased]\n\n## [" .. tagname .."]") 80 | return string.gsub(content, 81 | iso .. "%.%.%.HEAD", 82 | tagname .. "...HEAD\n[" .. tagname .. "]: " .. url .. previous 83 | .. "..." .. tagname) 84 | end 85 | return string.gsub(content, 86 | "\nRelease " .. iso .. "\n", 87 | "\nRelease " .. tagname .. "\n") 88 | elseif string.match(file, "%.lua$") then 89 | return string.gsub(content, 90 | '\nrelease_date = "' .. iso .. '"\n', 91 | '\nrelease_date = "' .. tagname .. '"\n') 92 | end 93 | return content 94 | end 95 | 96 | function tag_hook(tagname) 97 | os.execute('git commit -a -m "Step release tag"') 98 | end 99 | 100 | -- Auto-generate a .1 file from the help 101 | function docinit_hook() 102 | local find = string.find 103 | local insert = table.insert 104 | local open = io.open 105 | 106 | ---@type file*? 107 | local f = assert(open("README.md","rb")) 108 | ---@cast f file* 109 | local readme = f:read("a") 110 | f:close() 111 | f = nil 112 | 113 | local date_start,date_end = find(readme,"%d%d%d%d%p%d%d%p%d%d") 114 | 115 | local man_t = {} 116 | insert(man_t,'.TH ' .. string.upper(module) .. ' 1 "' 117 | .. readme:sub(date_start,date_end) .. '" "LaTeX"\n') 118 | insert(man_t,(".SH NAME\n" .. module .. "\n")) 119 | insert(man_t,(".SH SYNOPSIS\n Usage " .. module .. " [] []\n")) 120 | insert(man_t,".SH DESCRIPTION") 121 | 122 | local _,desc_start = find(readme,"Overview\n--------") 123 | local desc_end,_ = find(readme,"Issues") 124 | 125 | local overview = readme:sub(desc_start + 8,desc_end - 2):gsub("[_]",""):gsub("`",'"'):gsub("[*] ","\n * ") 126 | insert(man_t,overview) 127 | 128 | local cmd = "texlua ./" .. module .. ".lua --help" 129 | f = assert(io.popen(cmd,"r")) 130 | local help_text = assert(f:read("a")) 131 | f:close() 132 | f = nil 133 | 134 | insert(man_t,(help_text:gsub("\nUsage.*names>]\n\n","") 135 | :gsub("Valid targets",".SH COMMANDS\nValid targets") 136 | :gsub("Valid options",".SH OPTIONS\nValid options") 137 | :gsub("Full manual",'.SH "SEE ALSO"\nFull manual') 138 | :gsub("Bug tracker","\nBug tracker") 139 | :gsub("Copyright",".SH AUTHORS\nCopyright"))) 140 | 141 | f = assert(open(module .. ".1","wb")) 142 | f:write((table.concat(man_t,"\n"):gsub("\n$",""))) 143 | f:close() 144 | return 0 145 | end 146 | 147 | if not release_date then 148 | dofile("./l3build.lua") 149 | end 150 | -------------------------------------------------------------------------------- /config-context.lua: -------------------------------------------------------------------------------- 1 | stdengine = "luametatex" 2 | checkengines = {"luametatex","luatex"} 3 | checkformat = "context" 4 | testfiledir = "testfiles-context" 5 | -------------------------------------------------------------------------------- /config-pdf.lua: -------------------------------------------------------------------------------- 1 | checkengines = {"pdftex", "xetex"} 2 | testfiledir = "testfiles-pdf" -------------------------------------------------------------------------------- /config-plain.lua: -------------------------------------------------------------------------------- 1 | checkformat = "tex" 2 | testfiledir = "testfiles-plain" -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-One/build.lua: -------------------------------------------------------------------------------- 1 | bundle = "bundle-flat" 2 | module = "module-one" 3 | maindir = ".." 4 | 5 | typesetfiles = {"*.tex"} 6 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-One/module-one-code.tex: -------------------------------------------------------------------------------- 1 | 2 | \AtBeginDocument{\AlsoImplementation} 3 | \input{module-one.dtx} 4 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-One/module-one.dtx: -------------------------------------------------------------------------------- 1 | % 2 | % \iffalse 3 | %<*driver> 4 | \ProvidesFile{module-one.dtx} 5 | % 6 | %\ProvidesPackage{module-one} 7 | %<*pkg> 8 | [2017/12/11 v0.1 Module One example] 9 | % 10 | %<*driver> 11 | \documentclass{ltxdoc} 12 | \EnableCrossrefs 13 | \CodelineIndex 14 | \begin{document} 15 | \DocInput{module-one.dtx} 16 | \end{document} 17 | % 18 | % \fi 19 | % 20 | % \GetFileInfo{module-one.dtx} 21 | % \title{The Module One example} 22 | % \date{\fileversion \qquad \filedate} 23 | % \maketitle 24 | % 25 | % \begin{abstract} 26 | % This is the documentation of the Module One example. 27 | % \end{abstract} 28 | % 29 | % \section{Introduction} 30 | % 31 | % This is where you would explain the package to a user. 32 | % 33 | % \StopEventually{} 34 | % 35 | % \section{Implementation} 36 | % 37 | % \begin{macrocode} 38 | %<*pkg> 39 | % \end{macrocode} 40 | % 41 | % \begin{macrocode} 42 | \typeout{Actually this isn't a real package!} 43 | % \end{macrocode} 44 | % 45 | % \begin{macrocode} 46 | % 47 | % \end{macrocode} 48 | % 49 | % \Finale 50 | % 51 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-One/module-one.ins: -------------------------------------------------------------------------------- 1 | 2 | \input docstrip.tex 3 | \keepsilent 4 | \askforoverwritefalse 5 | \generate{\file{\jobname.sty}{\from{\jobname.dtx}{pkg}}} 6 | \endbatchfile 7 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-One/module-one.tex: -------------------------------------------------------------------------------- 1 | 2 | \AtBeginDocument{\OnlyDescription} 3 | \input{module-one.dtx} 4 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-One/testfiles/module-one-001.lvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \usepackage{module-one} 6 | 7 | \begin{document} 8 | 9 | \START 10 | % Tests go here 11 | 12 | \end{document} 13 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-One/testfiles/module-one-001.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | (module-one-001.aux) 4 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-Two/build.lua: -------------------------------------------------------------------------------- 1 | bundle = "bundle-flat" 2 | module = "module-two" 3 | maindir = ".." 4 | 5 | typesetfiles = {"*.tex"} 6 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-Two/module-two-code.tex: -------------------------------------------------------------------------------- 1 | 2 | \AtBeginDocument{\AlsoImplementation} 3 | \input{module-two.dtx} 4 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-Two/module-two.dtx: -------------------------------------------------------------------------------- 1 | % 2 | % \iffalse 3 | %<*driver> 4 | \ProvidesFile{module-two.dtx} 5 | % 6 | %\ProvidesPackage{module-two} 7 | %<*pkg> 8 | [2017/12/11 v0.1 Module Two example] 9 | % 10 | %<*driver> 11 | \documentclass{ltxdoc} 12 | \EnableCrossrefs 13 | \CodelineIndex 14 | \begin{document} 15 | \DocInput{module-two.dtx} 16 | \end{document} 17 | % 18 | % \fi 19 | % 20 | % \GetFileInfo{module-two.dtx} 21 | % \title{The Module Two example} 22 | % \date{\fileversion \qquad \filedate} 23 | % \maketitle 24 | % 25 | % \begin{abstract} 26 | % This is the documentation of the Module Two example. 27 | % \end{abstract} 28 | % 29 | % \section{Introduction} 30 | % 31 | % This is where you would explain the package to a user. 32 | % 33 | % \StopEventually{} 34 | % 35 | % \section{Implementation} 36 | % 37 | % \begin{macrocode} 38 | %<*pkg> 39 | % \end{macrocode} 40 | % 41 | % \begin{macrocode} 42 | \typeout{Actually this isn't a real package!} 43 | % \end{macrocode} 44 | % 45 | % \begin{macrocode} 46 | % 47 | % \end{macrocode} 48 | % 49 | % \Finale 50 | % 51 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-Two/module-two.ins: -------------------------------------------------------------------------------- 1 | 2 | \input docstrip.tex 3 | \keepsilent 4 | \askforoverwritefalse 5 | \generate{\file{\jobname.sty}{\from{\jobname.dtx}{pkg}}} 6 | \endbatchfile 7 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-Two/module-two.tex: -------------------------------------------------------------------------------- 1 | 2 | \AtBeginDocument{\OnlyDescription} 3 | \input{module-two.dtx} 4 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-Two/testfiles/module-two-001.lvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \usepackage{module-two} 6 | 7 | \begin{document} 8 | 9 | \START 10 | % Tests go here 11 | 12 | \end{document} 13 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/Module-Two/testfiles/module-two-001.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | (module-two-001.aux) 4 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/README.md: -------------------------------------------------------------------------------- 1 | L3BUILD `bundle-flat` example 2 | ================================================= 3 | 4 | This example demonstrates a bundle of two modules, with each module arranged in a ‘flat’ 5 | structure. There is nothing noteworthy about the modules themselves; they are based on the 6 | `simple-flat` example. 7 | 8 | Note the importance of the nested `build.lua` scripts, especially the setting of `maindir` 9 | in the build script for each module. 10 | 11 | ----- 12 | 13 | Copyright (C) 2014-2025 The LaTeX Project
14 |
15 | All rights reserved. 16 | -------------------------------------------------------------------------------- /examples/Bundle-Flat/build.lua: -------------------------------------------------------------------------------- 1 | bundle = "bundle-flat" 2 | 3 | packtdszip = true 4 | 5 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-One/build.lua: -------------------------------------------------------------------------------- 1 | bundle = "bundle-tree" 2 | module = "module-one" 3 | maindir = ".." 4 | 5 | sourcefiledir = "code" 6 | docfiledir = "doc" 7 | typesetfiles = {"*.dtx","*.tex"} 8 | packtdszip = true -- recommended for "tree" layouts 9 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-One/code/module-one.dtx: -------------------------------------------------------------------------------- 1 | % \iffalse 2 | % 3 | %<*driver> 4 | \ProvidesFile{module-one.dtx} 5 | % 6 | %\ProvidesPackage{module-one} 7 | %<*pkg> 8 | [2017/12/10 v0.1 Module One example] 9 | % 10 | % 11 | %<*driver> 12 | \documentclass{ltxdoc} 13 | \EnableCrossrefs 14 | \CodelineIndex 15 | \begin{document} 16 | \DocInput{\jobname.dtx} 17 | \end{document} 18 | % 19 | % \fi 20 | % 21 | % \GetFileInfo{module-one.dtx} 22 | % \title{The \textsf{module-one} example} 23 | % \date{\filedate\qquad\fileversion} 24 | % \maketitle 25 | % \begin{abstract} 26 | % This is the implementation of the module-one example. 27 | % \end{abstract} 28 | % 29 | % \tableofcontents 30 | % 31 | % \section{Introduction} 32 | % 33 | % In the module-one example, code is located in code/ and documentation is located in doc/. 34 | % 35 | % \section{Implementation} 36 | % 37 | % \begin{macrocode} 38 | %<*pkg> 39 | % \end{macrocode} 40 | % 41 | % \begin{macrocode} 42 | \typeout{Actually this isn't a real package!} 43 | % \end{macrocode} 44 | % 45 | % \begin{macrocode} 46 | % 47 | % \end{macrocode} 48 | % 49 | % \Finale 50 | % 51 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-One/code/module-one.ins: -------------------------------------------------------------------------------- 1 | 2 | \input docstrip.tex 3 | \keepsilent 4 | \askforoverwritefalse 5 | \generate{\file{\jobname.sty}{\from{\jobname.dtx}{pkg}}} 6 | \endbatchfile 7 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-One/doc/module-one-doc.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \title{Documentation for bundle tree / module one} 6 | \maketitle 7 | 8 | \section{Introduction} 9 | 10 | There's not much more to say right here. 11 | This is where the user documentation for the example goes. 12 | 13 | \end{document} 14 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-One/testfiles/module-one-001.lvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \usepackage{module-one} 6 | 7 | \begin{document} 8 | 9 | \START 10 | % Tests go here 11 | 12 | \end{document} 13 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-One/testfiles/module-one-001.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | (module-one-001.aux) 4 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-Two/build.lua: -------------------------------------------------------------------------------- 1 | bundle = "bundle-tree" 2 | module = "module-two" 3 | maindir = ".." 4 | 5 | sourcefiledir = "code" 6 | docfiledir = "doc" 7 | typesetfiles = {"*.dtx","*.tex"} 8 | packtdszip = true -- recommended for "tree" layouts 9 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-Two/code/module-two.dtx: -------------------------------------------------------------------------------- 1 | % \iffalse 2 | % 3 | %<*driver> 4 | \ProvidesFile{module-two.dtx} 5 | % 6 | %\ProvidesPackage{module-two} 7 | %<*pkg> 8 | [2017/12/10 v0.1 Module two example] 9 | % 10 | % 11 | %<*driver> 12 | \documentclass{ltxdoc} 13 | \EnableCrossrefs 14 | \CodelineIndex 15 | \begin{document} 16 | \DocInput{\jobname.dtx} 17 | \end{document} 18 | % 19 | % \fi 20 | % 21 | % \GetFileInfo{module-two.dtx} 22 | % \title{The \textsf{module-two} example} 23 | % \date{\filedate\qquad\fileversion} 24 | % \maketitle 25 | % \begin{abstract} 26 | % This is the implementation of the module-two example. 27 | % \end{abstract} 28 | % 29 | % \tableofcontents 30 | % 31 | % \section{Introduction} 32 | % 33 | % In the module-two example, code is located in code/ and documentation is located in doc/. 34 | % 35 | % \section{Implementation} 36 | % 37 | % \begin{macrocode} 38 | %<*pkg> 39 | % \end{macrocode} 40 | % 41 | % \begin{macrocode} 42 | \typeout{Actually this isn't a real package!} 43 | % \end{macrocode} 44 | % 45 | % \begin{macrocode} 46 | % 47 | % \end{macrocode} 48 | % 49 | % \Finale 50 | % 51 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-Two/code/module-two.ins: -------------------------------------------------------------------------------- 1 | 2 | \input docstrip.tex 3 | \keepsilent 4 | \askforoverwritefalse 5 | \generate{\file{\jobname.sty}{\from{\jobname.dtx}{pkg}}} 6 | \endbatchfile 7 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-Two/doc/module-two.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \title{Documentation for bundle tree / module two} 6 | \maketitle 7 | 8 | \section{Introduction} 9 | 10 | There's not much more to say right here. 11 | This is where the user documentation for the example goes. 12 | 13 | \end{document} 14 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-Two/testfiles/module-two-001.lvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \usepackage{module-two} 6 | 7 | \begin{document} 8 | 9 | \START 10 | % Tests go here 11 | 12 | \end{document} 13 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/Module-Two/testfiles/module-two-001.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | (module-two-001.aux) 4 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/README.md: -------------------------------------------------------------------------------- 1 | L3BUILD `bundle-tree` example 2 | ================================================= 3 | 4 | This example demonstrates a bundle of two modules, with each module arranged in a ‘tree’ 5 | structure. There is nothing noteworthy about the modules themselves; they are based on the 6 | `simple-tree` example. 7 | 8 | Note the importance of the nested `build.lua` scripts, especially the setting of `maindir` 9 | in the build script for each module. 10 | 11 | ----- 12 | 13 | Copyright (C) 2014-2025 The LaTeX Project
14 |
15 | All rights reserved. 16 | -------------------------------------------------------------------------------- /examples/Bundle-Tree/build.lua: -------------------------------------------------------------------------------- 1 | bundle = "bundle-tree" 2 | 3 | packtdszip = true 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | l3build: a testing and building system for LaTeX 2 | ================================================= 3 | 4 | Examples 5 | -------- 6 | 7 | Each sub-directory here is a self-contained example of a package set up to use `l3build`. 8 | These are intended for both testing and documentation purposes. 9 | 10 | The examples are: 11 | 12 | | Example | Description | 13 | | --- | --- | 14 | | `Simple-Flat` | A simple package in a flat layout | 15 | | `Simple-Tree` | A simple package in a tree layout | 16 | | `Bundle-Flat` | A bundle with two modules in a flat layout | 17 | | `Bundle-Tree` | A bundle with two modules in a tree layout | 18 | 19 | 20 | ----- 21 | 22 | Copyright (C) 2014-2025 The LaTeX Project
23 |
24 | All rights reserved. 25 | -------------------------------------------------------------------------------- /examples/Simple-Flat/README.md: -------------------------------------------------------------------------------- 1 | L3BUILD `simple-flat` example 2 | ================================================= 3 | 4 | This is a good example demonstrating a generic use case for a simple package using `l3build`. 5 | 6 | This package is set up to produce two PDF files: one including the user documentation for the package, and the second, with ‘`-code`’ suffix, which includes both the user documentation and the typeset package code. 7 | Note that these are produced by the two `.tex` files, which simply set typesetting options and read in the `.dtx` docstrip file so only one source file needs to be maintained. 8 | 9 | A variety of alternative docstrip arrangements can be set up to similar effect; the arrangement here is chosen for simplicity. 10 | As the `.dtx` package file grows larger, it may be sensible to split it up into multiple files, including separating the user documentation from the code itself. 11 | 12 | ----- 13 | 14 | Copyright (C) 2014-2025 The LaTeX Project
15 |
16 | All rights reserved. 17 | -------------------------------------------------------------------------------- /examples/Simple-Flat/build.lua: -------------------------------------------------------------------------------- 1 | module = "simple-flat" 2 | 3 | typesetfiles = {"*.tex"} 4 | -------------------------------------------------------------------------------- /examples/Simple-Flat/simple-flat-code.tex: -------------------------------------------------------------------------------- 1 | 2 | \AtBeginDocument{\AlsoImplementation} 3 | \input{simple-flat.dtx} 4 | -------------------------------------------------------------------------------- /examples/Simple-Flat/simple-flat.dtx: -------------------------------------------------------------------------------- 1 | % 2 | % \iffalse 3 | %<*driver> 4 | \ProvidesFile{simple-flat.dtx} 5 | % 6 | %\ProvidesPackage{simple-flat} 7 | %<*pkg> 8 | [2017/12/11 v0.1 Simple flat example] 9 | % 10 | %<*driver> 11 | \documentclass{ltxdoc} 12 | \EnableCrossrefs 13 | \CodelineIndex 14 | \begin{document} 15 | \DocInput{simple-flat.dtx} 16 | \end{document} 17 | % 18 | % \fi 19 | % 20 | % \GetFileInfo{simple-flat.dtx} 21 | % \title{The Simple Flat example} 22 | % \date{\fileversion \qquad \filedate} 23 | % \maketitle 24 | % 25 | % \begin{abstract} 26 | % This is the documentation of the Simple Flat example. 27 | % \end{abstract} 28 | % 29 | % \section{Introduction} 30 | % 31 | % This is where you would explain the package to a user. 32 | % 33 | % \StopEventually{} 34 | % 35 | % \section{Implementation} 36 | % 37 | % \begin{macrocode} 38 | %<*pkg> 39 | % \end{macrocode} 40 | % 41 | % \begin{macrocode} 42 | \typeout{Actually this isn't a real package!} 43 | % \end{macrocode} 44 | % 45 | % \begin{macrocode} 46 | % 47 | % \end{macrocode} 48 | % 49 | % \Finale 50 | % 51 | -------------------------------------------------------------------------------- /examples/Simple-Flat/simple-flat.ins: -------------------------------------------------------------------------------- 1 | 2 | \input docstrip.tex 3 | \keepsilent 4 | \askforoverwritefalse 5 | \generate{\file{\jobname.sty}{\from{\jobname.dtx}{pkg}}} 6 | \endbatchfile 7 | -------------------------------------------------------------------------------- /examples/Simple-Flat/simple-flat.tex: -------------------------------------------------------------------------------- 1 | 2 | \AtBeginDocument{\OnlyDescription} 3 | \input{simple-flat.dtx} 4 | -------------------------------------------------------------------------------- /examples/Simple-Flat/testfiles/simple-flat-001.lvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \usepackage{simple-flat} 6 | 7 | \begin{document} 8 | 9 | \START 10 | % Tests go here 11 | 12 | \end{document} 13 | -------------------------------------------------------------------------------- /examples/Simple-Flat/testfiles/simple-flat-001.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | (simple-flat-001.aux) 4 | -------------------------------------------------------------------------------- /examples/Simple-Tree/README.md: -------------------------------------------------------------------------------- 1 | L3BUILD `simple-tree` example 2 | ================================================= 3 | 4 | This is a simple example demonstrating a more complex package using `l3build`. 5 | Here, the code files are located in a `code/` subdirectory, and documentation files in `doc/`. 6 | 7 | In this simple case, the code and documentation consist of only one file each, which hardly seems necessary to subdivide. 8 | But as a package becomes larger it makes more sense to break the `.dtx` file into independent parts of the code, and the documentation can be broken itself into parts and chapters. 9 | This is left as an exercise to the energetic package writer studying these examples. 10 | 11 | ----- 12 | 13 | Copyright (C) 2014-2025 The LaTeX Project
14 |
15 | All rights reserved. 16 | -------------------------------------------------------------------------------- /examples/Simple-Tree/build.lua: -------------------------------------------------------------------------------- 1 | module = "simple-tree" 2 | 3 | sourcefiledir = "code" 4 | docfiledir = "doc" 5 | typesetfiles = {"*.dtx","*.tex"} 6 | packtdszip = true -- recommended for "tree" layouts 7 | -------------------------------------------------------------------------------- /examples/Simple-Tree/code/simple-tree.dtx: -------------------------------------------------------------------------------- 1 | % \iffalse 2 | % 3 | %<*driver> 4 | \ProvidesFile{simple-tree.dtx} 5 | % 6 | %\ProvidesPackage{simple-tree} 7 | %<*pkg> 8 | [2017/12/10 v0.1 Simple tree example] 9 | % 10 | % 11 | %<*driver> 12 | \documentclass{ltxdoc} 13 | \EnableCrossrefs 14 | \CodelineIndex 15 | \begin{document} 16 | \DocInput{\jobname.dtx} 17 | \end{document} 18 | % 19 | % \fi 20 | % 21 | % \GetFileInfo{simple-tree.dtx} 22 | % \title{The \textsf{simple-tree} example} 23 | % \date{\filedate\qquad\fileversion} 24 | % \maketitle 25 | % \begin{abstract} 26 | % This is the implementation of the simple-tree example. 27 | % \end{abstract} 28 | % 29 | % \tableofcontents 30 | % 31 | % \section{Introduction} 32 | % 33 | % In the simple-tree example, code is located in code/ and documentation is located in doc/. 34 | % 35 | % \section{Implementation} 36 | % 37 | % \begin{macrocode} 38 | %<*pkg> 39 | % \end{macrocode} 40 | % 41 | % \begin{macrocode} 42 | \typeout{Actually this isn't a real package!} 43 | % \end{macrocode} 44 | % 45 | % \begin{macrocode} 46 | % 47 | % \end{macrocode} 48 | % 49 | % \Finale 50 | % 51 | -------------------------------------------------------------------------------- /examples/Simple-Tree/code/simple-tree.ins: -------------------------------------------------------------------------------- 1 | 2 | \input docstrip.tex 3 | \keepsilent 4 | \askforoverwritefalse 5 | \generate{\file{\jobname.sty}{\from{\jobname.dtx}{pkg}}} 6 | \endbatchfile 7 | -------------------------------------------------------------------------------- /examples/Simple-Tree/doc/simple-tree-doc.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \title{Documentation for simple tree example} 6 | \maketitle 7 | 8 | \section{Introduction} 9 | 10 | There's not much more to say right here. 11 | This is where the user documentation for the simple tree example goes. 12 | 13 | \end{document} 14 | -------------------------------------------------------------------------------- /examples/Simple-Tree/testfiles/simple-tree-001.lvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \usepackage{simple-tree} 6 | 7 | \begin{document} 8 | 9 | \START 10 | % Tests go here 11 | 12 | \end{document} 13 | -------------------------------------------------------------------------------- /examples/Simple-Tree/testfiles/simple-tree-001.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | (simple-tree-001.aux) 4 | -------------------------------------------------------------------------------- /l3build-arguments.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-arguments.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local exit = os.exit 26 | local stderr = io.stderr 27 | 28 | local find = string.find 29 | local gmatch = string.gmatch 30 | local match = string.match 31 | local sub = string.sub 32 | 33 | local insert = table.insert 34 | 35 | -- Parse command line options 36 | 37 | option_list = 38 | { 39 | config = 40 | { 41 | desc = "Sets the config(s) used for running tests", 42 | short = "c", 43 | type = "table" 44 | }, 45 | date = 46 | { 47 | desc = "Sets the date to insert into sources", 48 | short = "d", 49 | type = "string" 50 | }, 51 | debug = 52 | { 53 | desc = "Runs target in debug mode", 54 | type = "boolean" 55 | }, 56 | dev = 57 | { 58 | desc = "Use the development LaTeX format", 59 | type = "boolean" 60 | }, 61 | dirty = 62 | { 63 | desc = "Skips cleaning up the test area", 64 | type = "boolean" 65 | }, 66 | ["dry-run"] = 67 | { 68 | desc = "Dry run for install or upload", 69 | type = "boolean" 70 | }, 71 | email = 72 | { 73 | desc = "Email address of CTAN uploader", 74 | type = "string" 75 | }, 76 | engine = 77 | { 78 | desc = "Sets the engine(s) to use for running test", 79 | short = "e", 80 | type = "table" 81 | }, 82 | epoch = 83 | { 84 | desc = "Sets the epoch for tests and typesetting", 85 | type = "string" 86 | }, 87 | file = 88 | { 89 | desc = "Takes the upload announcement from the given file", 90 | short = "F", 91 | type = "string" 92 | }, 93 | first = 94 | { 95 | desc = "Name of first test to run", 96 | type = "string" 97 | }, 98 | full = 99 | { 100 | desc = "Installs all files", 101 | type = "boolean" 102 | }, 103 | ["halt-on-error"] = 104 | { 105 | desc = "Stops running tests after the first failure", 106 | short = "H", 107 | type = "boolean" 108 | }, 109 | help = 110 | { 111 | desc = "Prints this message and exits", 112 | short = "h", 113 | type = "boolean" 114 | }, 115 | last = 116 | { 117 | desc = "Name of last test to run", 118 | type = "string" 119 | }, 120 | message = 121 | { 122 | desc = "Text for upload announcement message", 123 | short = "m", 124 | type = "string" 125 | }, 126 | quiet = 127 | { 128 | desc = "Suppresses TeX output when unpacking", 129 | short = "q", 130 | type = "boolean" 131 | }, 132 | rerun = 133 | { 134 | desc = "Skips setup: simply reruns tests", 135 | type = "boolean" 136 | }, 137 | ["show-log-on-error"] = 138 | { 139 | desc = "Shows the full log of the failure with 'halt-on-error'", 140 | type = "boolean" 141 | }, 142 | ["show-saves"] = 143 | { 144 | desc = "Shows the invocation to update failing .tlg files", 145 | short = "S", 146 | type = "boolean" 147 | }, 148 | shuffle = 149 | { 150 | desc = "Shuffles order of tests", 151 | type = "boolean" 152 | }, 153 | stdengine = 154 | { 155 | desc = "Run tests with the std engine (config dependent)", 156 | short = "s", 157 | type = "boolean" 158 | }, 159 | texmfhome = 160 | { 161 | desc = "Location of user texmf tree", 162 | type = "string" 163 | }, 164 | version = 165 | { 166 | desc = "Prints version information and exits", 167 | type = "boolean" 168 | } 169 | } 170 | 171 | -- This is done as a function (rather than do ... end) as it allows early 172 | -- termination (break) 173 | local function argparse() 174 | local result = { } 175 | local names = { } 176 | local long_options = { } 177 | local short_options = { } 178 | -- Turn long/short options into two lookup tables 179 | for k,v in pairs(option_list) do 180 | if v["short"] then 181 | short_options[v["short"]] = k 182 | end 183 | long_options[k] = k 184 | end 185 | local arg = arg 186 | -- arg[1] is a special case: must be a command or "-h"/"--help" 187 | -- Deal with this by assuming help and storing only apparently-valid 188 | -- input 189 | local a = arg[1] 190 | result["target"] = "help" 191 | if a then 192 | -- No options are allowed in position 1, so filter those out 193 | if a == "--version" then 194 | result["target"] = "version" 195 | elseif not match(a, "^%-") then 196 | result["target"] = a 197 | end 198 | end 199 | -- Stop here if help or version is required 200 | if result["target"] == "help" or result["target"] == "version" then 201 | return result 202 | end 203 | -- An auxiliary to grab all file names into a table 204 | local function remainder(num) 205 | local names = { } 206 | for i = num, #arg do 207 | insert(names, arg[i]) 208 | end 209 | return names 210 | end 211 | -- Examine all other arguments 212 | -- Use a while loop rather than for as this makes it easier 213 | -- to grab arg for optionals where appropriate 214 | local i = 2 215 | while i <= #arg do 216 | local a = arg[i] 217 | -- Terminate search for options 218 | if a == "--" then 219 | names = remainder(i + 1) 220 | break 221 | end 222 | -- Look for optionals 223 | local opt 224 | local optarg 225 | local opts 226 | -- Look for and option and get it into a variable 227 | if match(a, "^%-") then 228 | if match(a, "^%-%-") then 229 | opts = long_options 230 | local pos = find(a, "=", 1, true) 231 | if pos then 232 | opt = sub(a, 3, pos - 1) 233 | optarg = sub(a, pos + 1) 234 | else 235 | opt = sub(a, 3) 236 | end 237 | else 238 | opts = short_options 239 | opt = sub(a, 2, 2) 240 | -- Only set optarg if it is there 241 | if #a > 2 then 242 | optarg = sub(a, 3) 243 | end 244 | end 245 | -- Now check that the option is valid and sort out the argument 246 | -- if required 247 | local optname = opts[opt] 248 | if optname then 249 | -- Tidy up arguments 250 | if option_list[optname]["type"] == "boolean" then 251 | if optarg then 252 | local opt = "-" .. (match(a, "^%-%-") and "-" or "") .. opt 253 | stderr:write("Value not allowed for option " .. opt .."\n") 254 | return { target = "help" } 255 | end 256 | else 257 | if not optarg then 258 | optarg = arg[i + 1] 259 | if not optarg then 260 | stderr:write("Missing value for option " .. a .."\n") 261 | return { target = "help" } 262 | end 263 | i = i + 1 264 | end 265 | end 266 | else 267 | stderr:write("Unknown option " .. a .."\n") 268 | return { target = "help" } 269 | end 270 | -- Store the result 271 | if optarg then 272 | if option_list[optname]["type"] == "string" then 273 | result[optname] = optarg 274 | else 275 | local opts = result[optname] or { } 276 | for hit in gmatch(optarg, "([^,]+)") do 277 | insert(opts, hit) 278 | end 279 | result[optname] = opts 280 | end 281 | else 282 | result[optname] = true 283 | end 284 | i = i + 1 285 | end 286 | if not opt then 287 | names = remainder(i) 288 | break 289 | end 290 | end 291 | if next(names) then 292 | result["names"] = names 293 | end 294 | return result 295 | end 296 | 297 | options = argparse() 298 | 299 | -- Sanity check 300 | function check_engines(config) 301 | if options["engine"] then 302 | -- Make a lookup table 303 | local t = { } 304 | for _, engine in pairs(checkengines) do 305 | t[engine] = true 306 | end 307 | checkengines = {} 308 | for _,engine in ipairs(options["engine"]) do 309 | if t[engine] then 310 | insert(checkengines,engine) 311 | else 312 | print("Skipping unknown engine " .. engine) 313 | end 314 | end 315 | end 316 | if not next(checkengines) then 317 | print("No applicable engine requested, config ignored") 318 | exit(0) 319 | end 320 | end 321 | -------------------------------------------------------------------------------- /l3build-aux.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-aux.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | -- local safety guards and shortcuts 26 | 27 | local match = string.match 28 | local gsub = string.gsub 29 | 30 | local pairs = pairs 31 | local print = print 32 | 33 | local lookup = kpse.lookup 34 | 35 | local os_time = os.time 36 | local os_type = os.type 37 | 38 | -- 39 | -- Auxiliary functions which are used by more than one main function 40 | -- 41 | 42 | ---Convert the given `epoch` to a number. 43 | ---@param epoch string 44 | ---@return number 45 | ---@see l3build.lua 46 | ---@usage private? 47 | function normalize_epoch(epoch) 48 | assert(epoch, 'normalize_epoch argument must not be nil') 49 | -- If given as an ISO date, turn into an epoch number 50 | local y, m, d = match(epoch, "^(%d%d%d%d)-(%d%d)-(%d%d)$") 51 | if y then 52 | return os_time({ 53 | year = y, month = m, day = d, 54 | hour = 0, sec = 0, isdst = nil 55 | }) - os_time({ 56 | year = 1970, month = 1, day = 1, 57 | hour = 0, sec = 0, isdst = nil 58 | }) 59 | elseif match(epoch, "^%d+$") then 60 | return tonumber(epoch) 61 | else 62 | return 0 63 | end 64 | end 65 | 66 | ---Returns the CLI command (ending with `os_concat`) to set the epoch 67 | ---when forcecheckepoch is true, a void string otherwise. 68 | ---Will be run while checking or typesetting 69 | ---@param epoch string 70 | ---@param force boolean 71 | ---@return string 72 | ---@see check, typesetting 73 | ---@usage private? 74 | function set_epoch_cmd(epoch, force) 75 | return force and ( 76 | os_setenv .. " SOURCE_DATE_EPOCH=" .. epoch 77 | .. os_concat .. 78 | os_setenv .. " SOURCE_DATE_EPOCH_TEX_PRIMITIVES=1" 79 | .. os_concat .. 80 | os_setenv .. " FORCE_SOURCE_DATE=1" 81 | .. os_concat 82 | ) or "" 83 | end 84 | 85 | ---Returns the script name depending on the calling sequence. 86 | ---`l3build ...` -> full path of `l3build.lua` in the TDS 87 | ---When called via `texlua l3build.lua ...`, `l3build.lua` is resolved to either 88 | ---`./l3build.lua` or the full path of `l3build.lua` in the TDS. 89 | ---`texlua l3build.lua` -> `/Library/TeX/texbin/l3build.lua` or `./l3build.lua` 90 | ---@return string 91 | local function get_script_name() 92 | if match(arg[0], "l3build$") or match(arg[0], "l3build%.lua$") then 93 | return lookup("l3build.lua") 94 | else 95 | return arg[0] -- Why no lookup here? 96 | end 97 | end 98 | 99 | -- Performs the task named target given modules in a bundle. 100 | ---A module is the path of a directory relative to the main one. 101 | ---Uses `run` to launch a command. 102 | ---@param modules table List of modules. 103 | ---@param target string 104 | ---@param opts table 105 | ---@return number 0 on a successful completion, a non 0 error code otherwise. 106 | ---@see many places, including latex2e/build.lua 107 | ---@usage Public 108 | function call(modules, target, opts) 109 | -- Turn the option table into a CLI option string 110 | opts = opts or options 111 | local cli_opts = "" 112 | for k,v in pairs(opts) do 113 | if k ~= "names" and k ~= "target" then -- Special cases, TODO enhance the design to remove the need for this comment 114 | local t = option_list[k] or {} 115 | local value = "" 116 | if t["type"] == "string" then 117 | value = value .. "=" .. v 118 | elseif t["type"] == "table" then 119 | for _,a in pairs(v) do 120 | if value == "" then 121 | value = "=" .. a -- Add the initial "=" here 122 | else 123 | value = value .. "," .. a 124 | end 125 | end 126 | end 127 | cli_opts = cli_opts .. " --" .. k .. value 128 | end 129 | end 130 | if opts.names then 131 | for _, name in pairs(opts.names) do 132 | cli_opts = cli_opts .. " " .. name 133 | end 134 | end 135 | local script_name = get_script_name() 136 | for _, module in ipairs(modules) do 137 | local text 138 | if module == "." and opts["config"] and #opts["config"]>0 then 139 | text = " and configuration \"" .. opts["config"][1] .. "\"" 140 | else 141 | text = " for module \"" .. module .. "\"" 142 | end 143 | print("Running l3build with target \"" .. target .. "\"" .. text ) 144 | local error_level = run( 145 | module, 146 | "texlua " .. script_name .. " " .. target .. cli_opts 147 | ) 148 | if error_level ~= 0 then 149 | return error_level 150 | end 151 | end 152 | return 0 153 | end 154 | 155 | ---Unpack the given dependencies. 156 | ---A dependency is the path of a directory relative to the main one. 157 | ---@param deps table regular array of dependencies. 158 | ---@return number 0 on a successful completion, a non 0 error code otherwise. 159 | ---@see stdmain, check, unpack, typesetting 160 | ---@usage Private? 161 | function dep_install(deps) 162 | local error_level 163 | for _, dep in ipairs(deps) do 164 | print("Installing dependency: " .. dep) 165 | error_level = run(dep, "texlua " .. get_script_name() .. " unpack -q") 166 | if error_level ~= 0 then 167 | return error_level 168 | end 169 | end 170 | return 0 171 | end 172 | 173 | -- Construct a localtexmf including any tdsdirs 174 | -- Needed for checking and typesetting, hence global 175 | function localtexmf() 176 | local paths = "" 177 | for src,_ in pairs(tdsdirs) do 178 | paths = paths .. os_pathsep .. abspath(src) .. "//" 179 | end 180 | if texmfdir and texmfdir ~= "" and direxists(texmfdir) then 181 | paths = paths .. os_pathsep .. abspath(texmfdir) .. "//" 182 | end 183 | return paths 184 | end 185 | 186 | -- Run a command after setting up the environmental variables 187 | function runcmd(cmd,dir,vars) 188 | dir = dir or "." 189 | dir = abspath(dir) 190 | vars = vars or {} 191 | -- Allow for local texmf files 192 | local env 193 | if checkformat ~= "context" then 194 | env = os_setenv .. " TEXMFCNF=." .. os_pathsep 195 | end 196 | local envpaths = "." .. localtexmf() .. os_pathsep 197 | .. abspath(localdir) .. os_pathsep 198 | .. dir .. (typesetsearch and os_pathsep or "") 199 | -- Deal with spaces in paths 200 | if os_type == "windows" and match(envpaths," ") then 201 | envpaths = gsub(envpaths,'"','') 202 | end 203 | for _,var in pairs(vars) do 204 | env = (env and (env .. os_concat) or "") 205 | .. os_setenv .. " " .. var .. "=" .. envpaths 206 | end 207 | return run(dir,set_epoch_cmd(epoch, forcedocepoch) 208 | .. (env and (env .. os_concat) or "") .. cmd) 209 | end 210 | -------------------------------------------------------------------------------- /l3build-clean.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-clean.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local pairs = pairs 26 | local ipairs = ipairs 27 | local insert = table.insert 28 | 29 | -- Remove all generated files 30 | function clean() 31 | -- To make sure that distribdir never contains any stray subdirs, 32 | -- it is entirely removed then recreated rather than simply deleting 33 | -- all of the files 34 | local errorlevel = rmdir(distribdir) 35 | + mkdir(distribdir) 36 | + cleandir(localdir) 37 | + cleandir(testdir) 38 | + cleandir(typesetdir) 39 | + cleandir(unpackdir) 40 | 41 | if errorlevel ~= 0 then return errorlevel end 42 | 43 | for _,dir in pairs(remove_duplicates({maindir,sourcefiledir,docfiledir})) do 44 | local clean_list = {} 45 | local flags = {} 46 | for _,glob in pairs(cleanfiles) do 47 | for _,p in ipairs(tree(dir,glob)) do 48 | insert(clean_list, p.src) 49 | flags[p.src] = true 50 | end 51 | end 52 | for _,glob in pairs(sourcefiles) do 53 | for _,p in ipairs(tree(dir,glob)) do 54 | flags[p.src] = nil 55 | end 56 | end 57 | for i = #clean_list, 1, -1 do 58 | local p_src = clean_list[i] 59 | if flags[p_src] then 60 | errorlevel = rm(dir,p_src) 61 | if errorlevel ~= 0 then 62 | return errorlevel 63 | end 64 | end 65 | end 66 | end 67 | 68 | return 0 69 | end 70 | 71 | function bundleclean() 72 | local errorlevel = call(modules, "clean") 73 | for _,i in ipairs(cleanfiles) do 74 | errorlevel = rm(currentdir, i) + errorlevel 75 | end 76 | return errorlevel 77 | + rmdir(ctandir) 78 | + rmdir(tdsdir) 79 | end 80 | -------------------------------------------------------------------------------- /l3build-ctan.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-ctan.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local lfs = require("lfs") 26 | 27 | local pairs = pairs 28 | local print = print 29 | 30 | local attributes = lfs.attributes 31 | local lower = string.lower 32 | local match = string.match 33 | 34 | local newzip = require"l3build-zip" 35 | 36 | -- Copy files to the main CTAN release directory 37 | function copyctan() 38 | local pkgdir = ctandir .. "/" .. ctanpkg 39 | mkdir(pkgdir) 40 | 41 | -- Handle pre-formed sources: do two passes to avoid any cleandir() issues 42 | for _,dest in pairs(tdsdirs) do 43 | mkdir(pkgdir .. "/" .. dest) 44 | end 45 | for src,dest in pairs(tdsdirs) do 46 | cp("*",src,pkgdir .. "/" .. dest) 47 | end 48 | 49 | -- Now deal with the one-at-a-time files 50 | local function copyfiles(files,source) 51 | if source == currentdir or flatten then 52 | for _,filetype in pairs(files) do 53 | cp(filetype,source,pkgdir) 54 | end 55 | else 56 | for _,filetype in pairs(files) do 57 | for _,p in ipairs(tree(source,filetype)) do 58 | local path = dirname(p.src) 59 | local ctantarget = pkgdir .. "/" 60 | .. source .. "/" .. path 61 | mkdir(ctantarget) 62 | cp(p.src,source,ctantarget) 63 | end 64 | end 65 | end 66 | end 67 | for _,tab in pairs( 68 | {bibfiles,demofiles,docfiles,pdffiles,scriptmanfiles,typesetlist}) do 69 | copyfiles(tab,docfiledir) 70 | end 71 | copyfiles(sourcefiles,sourcefiledir) 72 | for _,file in pairs(textfiles) do 73 | cp(file, textfiledir, pkgdir) 74 | end 75 | 76 | end 77 | 78 | function bundlectan() 79 | local errorlevel = install_files(tdsdir,true) 80 | if errorlevel ~=0 then return errorlevel end 81 | copyctan() 82 | return 0 83 | end 84 | 85 | function ctan() 86 | -- Always run tests for all engines 87 | options["engine"] = nil 88 | local function dirzip(dir, zipname) 89 | zipname = zipname .. ".zip" 90 | local zip = assert(newzip(dir .. '/' .. zipname)) 91 | local function tab_to_check(table) 92 | local patterns = {} 93 | for n,i in ipairs(table) do 94 | patterns[n] = glob_to_pattern(i) 95 | end 96 | return function(name) 97 | for n, patt in ipairs(patterns) do 98 | if name:match"([^/]*)$":match(patt) then return true end 99 | end 100 | return false 101 | end 102 | end 103 | -- Convert the tables of files to quoted strings 104 | local binfile = tab_to_check(binaryfiles) 105 | local exclude = tab_to_check(excludefiles) 106 | local exefile = tab_to_check(exefiles) 107 | -- First, zip up all of the text files 108 | for _, p in ipairs(tree(dir, "**")) do 109 | local src = p.src:sub(3) -- Strip ./ 110 | if not (attributes(p.cwd, "mode") == "directory" or exclude(src) or src == zipname) then 111 | zip:add(p.cwd, src, binfile(src), exefile(src)) 112 | end 113 | end 114 | return zip:close() 115 | end 116 | local errorlevel 117 | local standalone = false 118 | if bundle == "" then 119 | standalone = true 120 | end 121 | if standalone then 122 | errorlevel = call({"."},"check") 123 | bundle = module 124 | else 125 | errorlevel = call(modules, "bundlecheck") 126 | end 127 | if errorlevel == 0 then 128 | rmdir(ctandir) 129 | mkdir(ctandir .. "/" .. ctanpkg) 130 | rmdir(tdsdir) 131 | mkdir(tdsdir) 132 | if standalone then 133 | errorlevel = install_files(tdsdir,true) 134 | if errorlevel ~=0 then return errorlevel end 135 | copyctan() 136 | else 137 | errorlevel = call(modules, "bundlectan") 138 | end 139 | else 140 | print("\n====================") 141 | print("Tests failed, zip stage skipped!") 142 | print("====================\n") 143 | return errorlevel 144 | end 145 | if errorlevel == 0 then 146 | for _,i in ipairs(textfiles) do 147 | for _,j in pairs({unpackdir, textfiledir}) do 148 | cp(i, j, ctandir .. "/" .. ctanpkg) 149 | cp(i, j, tdsdir .. "/doc/" .. tdsroot .. "/" .. bundle) 150 | end 151 | end 152 | -- Rename README if necessary 153 | if ctanreadme ~= "" and not match(lower(ctanreadme),"^readme$") and 154 | not match(lower(ctanreadme),"^readme%.%w+") then 155 | local newfile = "README." .. match(ctanreadme,"%.(%w+)$") 156 | for _,dir in pairs({ctandir .. "/" .. ctanpkg, 157 | tdsdir .. "/doc/" .. tdsroot .. "/" .. bundle}) do 158 | if fileexists(dir .. "/" .. ctanreadme) then 159 | rm(dir,newfile) 160 | ren(dir,ctanreadme,newfile) 161 | end 162 | end 163 | end 164 | dirzip(tdsdir, ctanpkg .. ".tds") 165 | if packtdszip then 166 | cp(ctanpkg .. ".tds.zip", tdsdir, ctandir) 167 | cp(ctanpkg .. ".tds.zip", tdsdir, currentdir) 168 | end 169 | dirzip(ctandir, ctanzip) 170 | cp(ctanzip .. ".zip", ctandir, currentdir) 171 | else 172 | print("\n====================") 173 | print("Typesetting failed, zip stage skipped!") 174 | print("====================\n") 175 | end 176 | return errorlevel 177 | end 178 | -------------------------------------------------------------------------------- /l3build-file-functions.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-file-functions.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local lfs = require("lfs") 26 | 27 | local print = print 28 | 29 | local open = io.open 30 | 31 | local attributes = lfs.attributes 32 | local currentdir = lfs.currentdir 33 | local chdir = lfs.chdir 34 | local lfs_dir = lfs.dir 35 | 36 | local execute = os.execute 37 | local exit = os.exit 38 | local getenv = os.getenv 39 | local remove = os.remove 40 | local os_type = os.type 41 | 42 | local luatex_revision = status.luatex_revision 43 | local luatex_version = status.luatex_version 44 | 45 | local match = string.match 46 | local sub = string.sub 47 | local gsub = string.gsub 48 | 49 | local insert = table.insert 50 | 51 | -- Convert a file glob into a pattern for use by e.g. string.gub 52 | -- Based on https://github.com/davidm/lua-glob-pattern 53 | -- Simplified substantially: "[...]" syntax not supported as is not 54 | -- required by the file patterns used by the team. Also note style 55 | -- changes to match coding approach in rest of this file. 56 | -- 57 | -- License for original globtopattern 58 | --[[ 59 | 60 | (c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT). 61 | 62 | Permission is hereby granted, free of charge, to any person obtaining a copy 63 | of this software and associated documentation files (the "Software"), to deal 64 | in the Software without restriction, including without limitation the rights 65 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 66 | copies of the Software, and to permit persons to whom the Software is 67 | furnished to do so, subject to the following conditions: 68 | 69 | The above copyright notice and this permission notice shall be included in 70 | all copies or substantial portions of the Software. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 73 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 74 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 75 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 76 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 77 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 78 | THE SOFTWARE. 79 | (end license) 80 | 81 | --]] 82 | function glob_to_pattern(glob) 83 | 84 | local pattern = "^" -- pattern being built 85 | local i = 0 -- index in glob 86 | local char -- char at index i in glob 87 | 88 | -- escape pattern char 89 | local function escape(char) 90 | return match(char, "^%w$") and char or "%" .. char 91 | end 92 | 93 | -- Convert tokens. 94 | while true do 95 | i = i + 1 96 | char = sub(glob, i, i) 97 | if char == "" then 98 | pattern = pattern .. "$" 99 | break 100 | elseif char == "?" then 101 | pattern = pattern .. "." 102 | elseif char == "*" then 103 | pattern = pattern .. ".*" 104 | elseif char == "[" then 105 | -- Ignored 106 | print("[...] syntax not supported in globs!") 107 | elseif char == "\\" then 108 | i = i + 1 109 | char = sub(glob, i, i) 110 | if char == "" then 111 | pattern = pattern .. "\\$" 112 | break 113 | end 114 | pattern = pattern .. escape(char) 115 | else 116 | pattern = pattern .. escape(char) 117 | end 118 | end 119 | return pattern 120 | end 121 | 122 | -- Detect the operating system in use 123 | -- Support items are defined here for cases where a single string can cover 124 | -- both Windows and Unix cases: more complex situations are handled inside 125 | -- the support functions 126 | os_concat = ";" 127 | os_null = "/dev/null" 128 | os_pathsep = ":" 129 | os_setenv = "export" 130 | os_yes = "printf 'y\\n%.0s' {1..300}" 131 | 132 | os_ascii = "echo \"\"" 133 | os_diffext = getenv("diffext") or ".diff" 134 | os_diffexe = getenv("diffexe") or "diff -c --strip-trailing-cr" 135 | os_grepexe = "grep" 136 | os_newline = "\n" 137 | 138 | if os_type == "windows" then 139 | os_ascii = "@echo." 140 | os_concat = "&" 141 | os_diffext = getenv("diffext") or ".fc" 142 | os_diffexe = getenv("diffexe") or "fc /n" 143 | os_grepexe = "findstr /r" 144 | os_newline = "\n" 145 | if tonumber(luatex_version) < 100 or 146 | (tonumber(luatex_version) == 100 147 | and tonumber(luatex_revision) < 4) then 148 | os_newline = "\r\n" 149 | end 150 | os_null = "nul" 151 | os_pathsep = ";" 152 | os_setenv = "set" 153 | os_yes = "for /l %I in (1,1,300) do @echo y" 154 | end 155 | 156 | -- Deal with codepage hell on Windows 157 | local function fixname(f) return f end 158 | if chgstrcp then 159 | fixname = chgstrcp.utf8tosyscp 160 | end 161 | 162 | -- Deal with the fact that Windows and Unix use different path separators 163 | local function unix_to_win(path) 164 | return fixname(gsub(path, "/", "\\")) 165 | end 166 | 167 | function normalize_path(path) 168 | if os_type == "windows" then 169 | return unix_to_win(path) 170 | end 171 | return path 172 | end 173 | 174 | -- Return an absolute path from a relative one 175 | -- Due to chdir, path must exist and be accessible. 176 | function abspath(path) 177 | local oldpwd = currentdir() 178 | local ok, msg = chdir(path) 179 | if ok then 180 | local result = currentdir() 181 | chdir(oldpwd) 182 | return escapepath(gsub(gsub(result,"^\\\\%?\\",""), "\\", "/")) 183 | end 184 | error(msg) 185 | end 186 | 187 | -- TODO: Fix the cross platform problem 188 | function escapepath(path) 189 | if os_type == "windows" then 190 | local path,count = gsub(path,'"','') 191 | if count % 2 ~= 0 then 192 | print("Unbalanced quotes in path") 193 | exit(0) 194 | else 195 | if match(path," ") then 196 | return '"' .. path .. '"' 197 | end 198 | return path 199 | end 200 | else 201 | path = gsub(path,"\\ ","[PATH-SPACE]") 202 | path = gsub(path," ","\\ ") 203 | return gsub(path,"%[PATH%-SPACE%]","\\ ") 204 | end 205 | end 206 | 207 | -- For cleaning out a directory, which also ensures that it exists 208 | function cleandir(dir) 209 | local errorlevel = mkdir(dir) 210 | if errorlevel ~= 0 then 211 | return errorlevel 212 | end 213 | return rm(dir, "**") 214 | end 215 | 216 | function direxists(dir) 217 | return attributes(dir, "mode") == "directory" 218 | end 219 | 220 | function fileexists(file) 221 | local f = open(file, "r") 222 | if f ~= nil then 223 | f:close() 224 | return true 225 | else 226 | return false -- also file exits and is not readable 227 | end 228 | end 229 | 230 | -- Copy files 'quietly' 231 | function cp(glob, source, dest) 232 | local errorlevel 233 | for _,p in ipairs(tree(source, glob)) do 234 | -- p_src is a path relative to `source` whereas 235 | -- p_cwd is the counterpart relative to the current working directory 236 | if os_type == "windows" then 237 | if direxists(p.cwd) then 238 | errorlevel = execute( 239 | 'xcopy /y /e /i "' .. unix_to_win(p.cwd) .. '" ' 240 | .. unix_to_win(dest .. '/' .. escapepath(p.src)) .. ' > nul' 241 | ) and 0 or 1 242 | else 243 | errorlevel = execute( 244 | 'xcopy /y "' .. unix_to_win(p.cwd) .. '" ' 245 | .. unix_to_win(dest .. '/') .. ' > nul' 246 | ) and 0 or 1 247 | end 248 | else 249 | -- Ensure we get similar behavior on all platforms 250 | if not direxists(dirname(dest)) then 251 | errorlevel = mkdir(dirname(dest)) 252 | if errorlevel ~=0 then return errorlevel end 253 | end 254 | errorlevel = execute( 255 | "cp -RLf '" .. p.cwd .. "' " .. dest 256 | ) and 0 or 1 257 | end 258 | if errorlevel ~=0 then 259 | return errorlevel 260 | end 261 | end 262 | return 0 263 | end 264 | 265 | -- Generate a table containing all file names of the given glob or all files 266 | -- if absent 267 | function filelist(path, glob) 268 | local files = { } 269 | local pattern 270 | if glob then 271 | pattern = glob_to_pattern(glob) 272 | end 273 | if direxists(path) then 274 | for entry in lfs_dir(path) do 275 | if pattern then 276 | if match(entry, pattern) then 277 | insert(files, entry) 278 | end 279 | else 280 | if entry ~= "." and entry ~= ".." then 281 | insert(files, entry) 282 | end 283 | end 284 | end 285 | end 286 | return files 287 | end 288 | function ordered_filelist(...) 289 | local files = filelist(...) 290 | table.sort(files) 291 | return files 292 | end 293 | 294 | ---@class tree_entry_t 295 | ---@field src string path relative to the source directory 296 | ---@field cwd string path counterpart relative to the current working directory 297 | 298 | ---Does what filelist does, but can also glob subdirectories. 299 | ---In the returned table, the keys are paths relative to the given source path, 300 | ---the values are their counterparts relative to the current working directory. 301 | ---@param src_path string 302 | ---@param glob string 303 | ---@return table 304 | function tree(src_path, glob) 305 | local function cropdots(path) 306 | return path:gsub( "^%./", ""):gsub("/%./", "/") 307 | end 308 | src_path = cropdots(src_path) 309 | glob = cropdots(glob) 310 | local function always_true() 311 | return true 312 | end 313 | ---@type table 314 | local result = { { 315 | src = ".", 316 | cwd = src_path, 317 | } } 318 | for glob_part, sep in glob:gmatch("([^/]+)(/?)/*") do 319 | local accept = sep == "/" and direxists or always_true 320 | ---Feeds the given table according to `glob_part` 321 | ---@param p tree_entry_t path counterpart relative to the current working directory 322 | ---@param table table 323 | local function fill(p, table) 324 | for _,file in ipairs(filelist(p.cwd, glob_part)) do 325 | if file ~= "." and file ~= ".." then 326 | local pp = { 327 | src = p.src .. "/" .. file, 328 | cwd = p.cwd .. "/" .. file, 329 | } 330 | if pp.cwd ~= builddir -- TODO: ensure that `builddir` is properly formatted 331 | and accept(pp.cwd) 332 | then 333 | insert(table, pp) 334 | end 335 | end 336 | end 337 | end 338 | local new_result = {} 339 | if glob_part == "**" then 340 | local i = 1 341 | while true do 342 | local p = result[i] 343 | i = i + 1 344 | if not p then 345 | break 346 | end 347 | insert(new_result, p) -- shorter path 348 | fill(p, result) -- after longer 349 | end 350 | else 351 | for _,p in ipairs(result) do 352 | fill(p, new_result) 353 | end 354 | end 355 | result = new_result 356 | end 357 | return result 358 | end 359 | 360 | function remove_duplicates(a) 361 | -- Return array with duplicate entries removed from input array `a`. 362 | 363 | local uniq = {} 364 | local hash = {} 365 | 366 | for _,v in ipairs(a) do 367 | if (not hash[v]) then 368 | hash[v] = true 369 | uniq[#uniq+1] = v 370 | end 371 | end 372 | 373 | return uniq 374 | end 375 | 376 | function mkdir(dir) 377 | dir = escapepath(dir) 378 | if os_type == "windows" then 379 | -- Windows (with the extensions) will automatically make directory trees 380 | -- but issues a warning if the dir already exists: avoid by including a test 381 | dir = unix_to_win(dir) 382 | return execute( 383 | "if not exist " .. dir .. "\\nul " .. "mkdir " .. dir 384 | ) 385 | else 386 | return execute("mkdir -p " .. dir) 387 | end 388 | end 389 | 390 | -- Rename 391 | function ren(dir, source, dest) 392 | dir = dir .. "/" 393 | if os_type == "windows" then 394 | source = gsub(source, "^%.+/", "") 395 | dest = gsub(dest, "^%.+/", "") 396 | return execute("ren " .. unix_to_win(dir) .. source .. " " .. dest) 397 | else 398 | return execute("mv " .. dir .. source .. " " .. dir .. dest) 399 | end 400 | end 401 | 402 | -- Remove file(s) based on a glob 403 | function rm(source, glob) 404 | for _,p in ipairs(tree(source, glob)) do 405 | rmfile(source,p.src) 406 | end 407 | -- os.remove doesn't give a sensible errorlevel 408 | return 0 409 | end 410 | 411 | -- Remove file 412 | function rmfile(source, file) 413 | remove(source .. "/" .. file) 414 | -- os.remove doesn't give a sensible errorlevel 415 | return 0 416 | end 417 | 418 | -- Remove a directory tree 419 | function rmdir(dir) 420 | -- First, make sure it exists to avoid any errors 421 | mkdir(dir) 422 | if os_type == "windows" then 423 | return execute("rmdir /s /q " .. unix_to_win(dir)) 424 | else 425 | return execute("rm -r " .. dir) 426 | end 427 | end 428 | 429 | -- Run a command in a given directory 430 | function run(dir, cmd) 431 | return execute("cd " .. dir .. os_concat .. cmd) 432 | end 433 | 434 | -- Split a path into file and directory component 435 | function splitpath(file) 436 | local path, name = match(file, "^(.*)/([^/]*)$") 437 | if path then 438 | return path, name 439 | else 440 | return ".", file 441 | end 442 | end 443 | 444 | -- Arguably clearer names 445 | function basename(file) 446 | return(select(2, splitpath(file))) 447 | end 448 | 449 | function dirname(file) 450 | return(select(1, splitpath(file))) 451 | end 452 | 453 | -- Strip the extension from a file name (if present) 454 | function jobname(file) 455 | local name = match(basename(file), "^(.*)%.") 456 | return name or file 457 | end 458 | 459 | -- Look for files, directory by directory, and return the first existing 460 | function locate(dirs, names) 461 | for _,i in ipairs(dirs) do 462 | for _,j in ipairs(names) do 463 | local path = i .. "/" .. j 464 | if fileexists(path) then 465 | return path 466 | end 467 | end 468 | end 469 | end 470 | -------------------------------------------------------------------------------- /l3build-help.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-help.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local insert = table.insert 26 | local match = string.match 27 | local rep = string.rep 28 | local sort = table.sort 29 | 30 | local copyright = "Copyright (C) 2014-2025 The LaTeX Project\n" 31 | 32 | function version() 33 | print( 34 | "\n" .. 35 | "l3build: A testing and building system for LaTeX\n\n" .. 36 | "Release " .. release_date .. "\n" .. 37 | copyright 38 | ) 39 | end 40 | 41 | function help() 42 | local function setup_list(list) 43 | local longest = 0 44 | for k,_ in pairs(list) do 45 | if k:len() > longest then 46 | longest = k:len() 47 | end 48 | end 49 | -- Sort the options 50 | local t = { } 51 | for k,_ in pairs(list) do 52 | insert(t, k) 53 | end 54 | sort(t) 55 | return longest,t 56 | end 57 | 58 | local scriptname = "l3build" 59 | if not (match(arg[0], "l3build%.lua$") or match(arg[0],"l3build$")) then 60 | scriptname = arg[0] 61 | end 62 | print("\nUsage: " .. scriptname .. " [] []") 63 | print("") 64 | print("Valid targets are:") 65 | local longest,t = setup_list(target_list) 66 | for _,k in ipairs(t) do 67 | local target = target_list[k] 68 | local filler = rep(" ", longest - k:len() + 1) 69 | if target["desc"] then 70 | print(" " .. k .. filler .. target["desc"]) 71 | end 72 | end 73 | print("") 74 | print("Valid options are:") 75 | longest,t = setup_list(option_list) 76 | for _,k in ipairs(t) do 77 | local opt = option_list[k] 78 | local filler = rep(" ", longest - k:len() + 1) 79 | if opt["desc"] then 80 | if opt["short"] then 81 | print(" --" .. k .. "|-" .. opt["short"] .. filler .. opt["desc"]) 82 | else 83 | print(" --" .. k .. " " .. filler .. opt["desc"]) 84 | end 85 | end 86 | end 87 | print("") 88 | print("Full manual available via 'texdoc l3build'.") 89 | print("") 90 | print("Repository : https://github.com/latex3/l3build") 91 | print("Bug tracker : https://github.com/latex3/l3build/issues") 92 | print(copyright) 93 | end 94 | -------------------------------------------------------------------------------- /l3build-install.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-install.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local ipairs = ipairs 26 | local pairs = pairs 27 | local print = print 28 | 29 | local set_program = kpse.set_program_name 30 | local var_value = kpse.var_value 31 | 32 | local gsub = string.gsub 33 | local lower = string.lower 34 | local match = string.match 35 | 36 | local insert = table.insert 37 | 38 | local function gethome() 39 | set_program("latex") 40 | local result = options["texmfhome"] or var_value("TEXMFHOME") 41 | if not result or result == "" or match(result, os_pathsep) then 42 | print("Ambiguous TEXMFHOME setting: please use the --texmfhome option") 43 | os.exit(1) 44 | end 45 | mkdir(result) 46 | return abspath(result) 47 | end 48 | 49 | function uninstall() 50 | local function zapdir(dir) 51 | local installdir = gethome() .. "/" .. dir 52 | if options["dry-run"] then 53 | local files = filelist(installdir) 54 | if files[1] then 55 | print("\n" .. "For removal from " .. installdir .. ":") 56 | for _,file in ipairs(ordered_filelist(installdir)) do 57 | print("- " .. file) 58 | end 59 | end 60 | return 0 61 | else 62 | if direxists(installdir) then 63 | return rmdir(installdir) 64 | end 65 | end 66 | return 0 67 | end 68 | local function uninstall_files(dir,subdir) 69 | subdir = subdir or moduledir 70 | dir = dir .. "/" .. subdir 71 | return zapdir(dir) 72 | end 73 | local errorlevel = 0 74 | -- Any script man files need special handling 75 | local manfiles = { } 76 | for _,glob in pairs(scriptmanfiles) do 77 | for _,p in ipairs(tree(docfiledir,glob)) do 78 | -- Man files should have a single-digit extension: the type 79 | local installdir = gethome() .. "/doc/man/man" .. match(p.src,".$") 80 | if fileexists(installdir .. "/" .. p.src) then 81 | if options["dry-run"] then 82 | insert(manfiles,"man" .. match(p.src,".$") .. "/" .. 83 | select(2,splitpath(p.src))) 84 | else 85 | errorlevel = errorlevel + rm(installdir,p.src) 86 | end 87 | end 88 | end 89 | end 90 | if next(manfiles) then 91 | print("\n" .. "For removal from " .. gethome() .. "/doc/man:") 92 | for _,v in ipairs(manfiles) do 93 | print("- " .. v) 94 | end 95 | end 96 | errorlevel = uninstall_files("doc") 97 | + uninstall_files("source") 98 | + uninstall_files("tex") 99 | + uninstall_files("bibtex/bst",module) 100 | + uninstall_files("makeindex",module) 101 | + uninstall_files("scripts",module) 102 | + errorlevel 103 | if errorlevel ~= 0 then return errorlevel end 104 | -- Finally, clean up special locations 105 | for _,location in ipairs(tdslocations) do 106 | local path = dirname(location) 107 | errorlevel = zapdir(path) 108 | if errorlevel ~= 0 then return errorlevel end 109 | end 110 | -- We remove all directories which contain at least one ordinary file in the source tree 111 | for src, dest in pairs(tdsdirs) do 112 | dest = dest .. '/' 113 | local skipdir 114 | for _, p in ipairs(tree(src, '**')) do 115 | local src = p.src:sub(2) -- Skip the first '.' 116 | if skipdir and src:sub(1, #skipdir) ~= skipdir then 117 | skipdir = nil 118 | end 119 | if (not skipdir) and (not direxists(p.cwd)) then 120 | skipdir = dirname(src) 121 | errorlevel = zapdir(dest .. skipdir) 122 | if errorlevel ~= 0 then return errorlevel end 123 | skipdir = skipdir .. '/' 124 | end 125 | end 126 | end 127 | return 0 128 | end 129 | 130 | function install_files(target,full,dry_run) 131 | 132 | -- Needed so paths are only cleaned out once 133 | local cleanpaths = { } 134 | -- Collect up all file data before copying: 135 | -- ensures no files are lost during clean-up 136 | local installmap = { } 137 | 138 | local function create_install_map(source,dir,files,subdir) 139 | subdir = subdir or moduledir 140 | -- For material associated with secondary tools (BibTeX, MakeIndex) 141 | -- the structure needed is slightly different from those items going 142 | -- into the tex/doc/source trees 143 | if (dir == "makeindex" or match(dir,"$bibtex")) and module == "base" then 144 | subdir = "latex" 145 | end 146 | dir = dir .. (subdir and ("/" .. subdir) or "") 147 | local filenames = { } 148 | local sourcepaths = { } 149 | local paths = { } 150 | -- Generate a file list and include the directory 151 | for _,glob_table in pairs(files) do 152 | for _,glob in pairs(glob_table) do 153 | for _,p in ipairs(tree(source,glob)) do 154 | -- Just want the name 155 | local path,filename = splitpath(p.src) 156 | local sourcepath = "/" 157 | if path == "." then 158 | sourcepaths[filename] = source 159 | else 160 | path = gsub(path,"^%.","") 161 | sourcepaths[filename] = source .. path 162 | if not flattentds then sourcepath = path .. "/" end 163 | end 164 | local matched = false 165 | for _,location in ipairs(tdslocations) do 166 | local l_dir,l_glob = splitpath(location) 167 | local pattern = glob_to_pattern(l_glob) 168 | if match(filename,pattern) then 169 | insert(paths,l_dir) 170 | insert(filenames,l_dir .. sourcepath .. filename) 171 | matched = true 172 | break 173 | end 174 | end 175 | if not matched then 176 | insert(paths,dir) 177 | insert(filenames,dir .. sourcepath .. filename) 178 | end 179 | end 180 | end 181 | end 182 | 183 | local errorlevel = 0 184 | -- The target is only created if there are actual files to install 185 | if next(filenames) then 186 | if not dry_run then 187 | for _,path in ipairs(paths) do 188 | local target_path = target .. "/" .. path 189 | if not cleanpaths[target_path] then 190 | errorlevel = cleandir(target_path) 191 | if errorlevel ~= 0 then return errorlevel end 192 | end 193 | cleanpaths[target_path] = true 194 | end 195 | end 196 | for _,name in ipairs(filenames) do 197 | if dry_run then 198 | print("- " .. name) 199 | else 200 | local path,file = splitpath(name) 201 | insert(installmap, 202 | {file = file, source = sourcepaths[file], dest = target .. "/" .. path}) 203 | end 204 | end 205 | end 206 | return 0 207 | end 208 | 209 | local errorlevel = unpack() 210 | if errorlevel ~= 0 then return errorlevel end 211 | 212 | -- Creates a 'controlled' list of files 213 | local function create_file_list(dir,include,exclude) 214 | dir = dir or currentdir 215 | include = include or { } 216 | exclude = exclude or { } 217 | insert(exclude,excludefiles) 218 | local excludelist = { } 219 | for _,glob_table in pairs(exclude) do 220 | for _,glob in pairs(glob_table) do 221 | for _,p in ipairs(tree(dir,glob)) do 222 | excludelist[p.src] = true 223 | end 224 | end 225 | end 226 | local result = { } 227 | for _,glob in pairs(include) do 228 | for _,p in ipairs(tree(dir,glob)) do 229 | if not excludelist[p.src] then 230 | insert(result, p.src) 231 | end 232 | end 233 | end 234 | return result 235 | end 236 | 237 | local installlist = create_file_list(unpackdir,installfiles,{scriptfiles}) 238 | 239 | if full then 240 | errorlevel = doc() 241 | if errorlevel ~= 0 then return errorlevel end 242 | -- For the purposes here, any typesetting demo files need to be 243 | -- part of the main typesetting list 244 | local typesetfiles = typesetfiles 245 | for _,glob in pairs(typesetdemofiles) do 246 | insert(typesetfiles,glob) 247 | end 248 | 249 | -- Find PDF files 250 | pdffiles = { } 251 | for _,glob in pairs(typesetfiles) do 252 | insert(pdffiles,(gsub(glob,"%.%w+$",".pdf"))) 253 | end 254 | 255 | -- Set up lists: global as they are also needed to do CTAN releases 256 | typesetlist = create_file_list(docfiledir,typesetfiles,{sourcefiles}) 257 | sourcelist = create_file_list(sourcefiledir,sourcefiles, 258 | {bstfiles,installfiles,makeindexfiles,scriptfiles}) 259 | 260 | if dry_run then 261 | print("\nFor installation inside " .. target .. ":") 262 | end 263 | 264 | errorlevel = create_install_map(sourcefiledir,"source",{sourcelist}) 265 | + create_install_map(docfiledir,"doc", 266 | {bibfiles,demofiles,docfiles,pdffiles,textfiles,typesetlist}) 267 | if errorlevel ~= 0 then return errorlevel end 268 | 269 | -- Rename README if necessary 270 | if not dry_run then 271 | if ctanreadme ~= "" and not match(lower(ctanreadme),"^readme%.%w+") then 272 | local installdir = target .. "/doc/" .. moduledir 273 | if fileexists(installdir .. "/" .. ctanreadme) then 274 | ren(installdir,ctanreadme,"README." .. match(ctanreadme,"%.(%w+)$")) 275 | end 276 | end 277 | end 278 | 279 | -- Any script man files need special handling 280 | local manfiles = { } 281 | for _,glob in pairs(scriptmanfiles) do 282 | for _,p in ipairs(tree(docfiledir,glob)) do 283 | if dry_run then 284 | insert(manfiles,"man" .. match(p.src,".$") .. "/" .. 285 | select(2,splitpath(p.src))) 286 | else 287 | -- Man files should have a single-digit extension: the type 288 | local installdir = target .. "/doc/man/man" .. match(p.src,".$") 289 | errorlevel = errorlevel + mkdir(installdir) 290 | errorlevel = errorlevel + cp(p.src,docfiledir,installdir) 291 | end 292 | end 293 | end 294 | if next(manfiles) then 295 | for _,v in ipairs(manfiles) do 296 | print("- doc/man/" .. v) 297 | end 298 | end 299 | end 300 | 301 | if errorlevel ~= 0 then return errorlevel end 302 | 303 | errorlevel = create_install_map(unpackdir,"tex",{installlist}) 304 | + create_install_map(unpackdir,"bibtex/bst",{bstfiles},module) 305 | + create_install_map(unpackdir,"makeindex",{makeindexfiles},module) 306 | + create_install_map(unpackdir,"scripts",{scriptfiles},module) 307 | 308 | for src, dest in pairs(tdsdirs) do 309 | dest = target .. '/' .. dest 310 | insert(installmap, 311 | {file = '*', source = src, dest = dest}) 312 | dest = dest .. '/' 313 | local skipdir 314 | for _, p in ipairs(tree(src, '**')) do 315 | local src = p.src:sub(2) -- Skip the first '.' 316 | if skipdir and src:sub(1, #skipdir) ~= skipdir then 317 | skipdir = nil 318 | end 319 | if (not skipdir) and (not direxists(p.cwd)) then 320 | skipdir = dirname(src) 321 | errorlevel = cleandir(dest .. skipdir) 322 | if errorlevel ~= 0 then return errorlevel end 323 | skipdir = skipdir .. '/' 324 | end 325 | end 326 | end 327 | 328 | if errorlevel ~= 0 then return errorlevel end 329 | 330 | -- Track created destination directories to avoid overhead from 331 | -- repeatedly creating them 332 | local destination_dirs = {} 333 | 334 | -- Files are all copied in one shot: this ensures that cleandir() 335 | -- can't be an issue even if there are complex set-ups 336 | for _,v in ipairs(installmap) do 337 | if not destination_dirs[v.dest] then 338 | mkdir(v.dest) 339 | destination_dirs[v.dest] = true 340 | end 341 | errorlevel = cp(v.file,v.source,v.dest) 342 | if errorlevel ~= 0 then return errorlevel end 343 | end 344 | 345 | return 0 346 | end 347 | 348 | function install() 349 | return install_files(gethome(),options["full"],options["dry-run"]) 350 | end 351 | -------------------------------------------------------------------------------- /l3build-manifest-setup.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-manifest-setup.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | 26 | --[[ 27 | L3BUILD MANIFEST SETUP 28 | ====================== 29 | This file contains all of the code that is easily replaceable by the user. 30 | Either create a copy of this file, rename, and include alongside your `build.lua` 31 | script and load it with `dofile()`, or simply copy/paste the definitions below 32 | into your `build.lua` script directly. 33 | --]] 34 | 35 | 36 | --[[ 37 | Setup of manifest "groups" 38 | -------------------------- 39 | 40 | The grouping of manifest files is broken into three subheadings: 41 | 42 | * The development repository 43 | * The TDS structure from `ctan` 44 | * The CTAN structure from `ctan` 45 | 46 | The latter two will only be produced if the `manifest` target is run *after* 47 | the `ctan` target. Contrarily, if you run `clean` before `manifest` then 48 | only the first grouping will be printed. 49 | 50 | If you want to omit the files in the development repository, essentially 51 | producing a minimalist manifest with only the files included for distribution, 52 | make a copy of the `manifest_setup` function and delete the groups under 53 | the ‘Repository manifest’ subheading below. 54 | --]] 55 | 56 | 57 | function manifest_setup() 58 | local groups = { 59 | { 60 | subheading = "Repository manifest", 61 | description = [[ 62 | The following groups list the files included in the development repository of the package. 63 | Files listed with a ‘†’ marker are included in the TDS but not CTAN files, and files listed 64 | with ‘‡’ are included in both. 65 | ]], 66 | }, 67 | { 68 | name = "Source files", 69 | description = [[ 70 | These are source files for a number of purposes, including the `unpack` process which 71 | generates the installation files of the package. Additional files included here will also 72 | be installed for processing such as testing. 73 | ]], 74 | files = {sourcefiles}, 75 | dir = sourcefiledir or maindir, -- TODO: remove "or maindir" after rebasing onto master 76 | }, 77 | { 78 | name = "Typeset documentation source files", 79 | description = [[ 80 | These files are typeset using LaTeX to produce the PDF documentation for the package. 81 | ]], 82 | files = {typesetfiles,typesetsourcefiles,typesetdemofiles}, 83 | }, 84 | { 85 | name = "Documentation files", 86 | description = [[ 87 | These files form part of the documentation but are not typeset. Generally they will be 88 | additional input files for the typeset documentation files listed above. 89 | ]], 90 | files = {docfiles}, 91 | dir = docfiledir or maindir, -- TODO: remove "or maindir" after rebasing onto master 92 | }, 93 | { 94 | name = "Text files", 95 | description = [[ 96 | Plain text files included as documentation or metadata. 97 | ]], 98 | files = {textfiles}, 99 | skipfiledescription = true, 100 | }, 101 | { 102 | name = "Demo files", 103 | description = [[ 104 | Files included to demonstrate package functionality. These files are *not* 105 | typeset or compiled in any way. 106 | ]], 107 | files = {demofiles}, 108 | }, 109 | { 110 | name = "Bibliography and index files", 111 | description = [[ 112 | Supplementary files used for compiling package documentation. 113 | ]], 114 | files = {bibfiles,bstfiles,makeindexfiles}, 115 | }, 116 | { 117 | name = "Derived files", 118 | description = [[ 119 | The files created by ‘unpacking’ the package sources. This typically includes 120 | `.sty` and `.cls` files created from DocStrip `.dtx` files. 121 | ]], 122 | files = {installfiles}, 123 | exclude = {excludefiles,sourcefiles}, 124 | dir = unpackdir, 125 | skipfiledescription = true, 126 | }, 127 | { 128 | name = "Typeset documents", 129 | description = [[ 130 | The output files (PDF, essentially) from typesetting the various source, demo, 131 | etc., package files. 132 | ]], 133 | files = {typesetfiles,typesetsourcefiles,typesetdemofiles}, 134 | rename = {"%.%w+$", ".pdf"}, 135 | skipfiledescription = true, 136 | }, 137 | { 138 | name = "Support files", 139 | description = [[ 140 | These files are used for unpacking, typesetting, or checking purposes. 141 | ]], 142 | files = {unpacksuppfiles,typesetsuppfiles,checksuppfiles}, 143 | dir = supportdir, 144 | }, 145 | { 146 | name = "Checking-specific support files", 147 | description = [[ 148 | Support files for checking the test suite. 149 | ]], 150 | files = {"*.*"}, 151 | exclude = {{".",".."},excludefiles}, 152 | dir = testsuppdir, 153 | }, 154 | { 155 | name = "Test files", 156 | description = [[ 157 | These files form the test suite for the package. `.lvt` or `.lte` files are the individual 158 | unit tests, and `.tlg` are the stored output for ensuring changes to the package produce 159 | the same output. These output files are sometimes shared and sometime specific for 160 | different engines (pdfTeX, XeTeX, LuaTeX, etc.). 161 | ]], 162 | files = {"*"..lvtext,"*"..lveext,"*"..tlgext}, 163 | dir = testfiledir, 164 | skipfiledescription = true, 165 | }, 166 | { 167 | subheading = "TDS manifest", 168 | description = [[ 169 | The following groups list the files included in the TeX Directory Structure used to install 170 | the package into a TeX distribution. 171 | ]], 172 | }, 173 | { 174 | name = "Source files (TDS)", 175 | description = "All files included in the `"..module.."/source` directory.\n", 176 | dir = tdsdir.."/source/"..moduledir, 177 | files = {"*.*"}, 178 | exclude = {".",".."}, 179 | flag = false, 180 | skipfiledescription = true, 181 | }, 182 | { 183 | name = "TeX files (TDS)", 184 | description = "All files included in the `"..module.."/tex` directory.\n", 185 | dir = tdsdir.."/tex/"..moduledir, 186 | files = {"*.*"}, 187 | exclude = {".",".."}, 188 | flag = false, 189 | skipfiledescription = true, 190 | }, 191 | { 192 | name = "Doc files (TDS)", 193 | description = "All files included in the `"..module.."/doc` directory.\n", 194 | dir = tdsdir.."/doc/"..moduledir, 195 | files = {"*.*"}, 196 | exclude = {".",".."}, 197 | flag = false, 198 | skipfiledescription = true, 199 | }, 200 | { 201 | subheading = "CTAN manifest", 202 | description = [[ 203 | The following group lists the files included in the CTAN package. 204 | ]], 205 | }, 206 | { 207 | name = "CTAN files", 208 | dir = ctandir.."/"..module, 209 | files = {"*.*"}, 210 | exclude = {".",".."}, 211 | flag = false, 212 | skipfiledescription = true, 213 | }, 214 | } 215 | return groups 216 | end 217 | 218 | --[[ 219 | Sorting within groups 220 | --------------------- 221 | --]] 222 | 223 | manifest_sort_within_match = manifest_sort_within_match or function(files) 224 | table.sort(files) 225 | return files 226 | end 227 | 228 | manifest_sort_within_group = manifest_sort_within_group or function(files) 229 | --[[ 230 | -- no-op by default; make your own definition to customize. E.g.: 231 | table.sort(files) 232 | --]] 233 | return files 234 | end 235 | 236 | --[[ 237 | Writing to file 238 | --------------- 239 | --]] 240 | 241 | function manifest_write_opening(filehandle) 242 | 243 | filehandle:write("# Manifest for " .. module .. "\n\n") 244 | filehandle:write([[ 245 | This file is a listing of all files considered to be part of this package. 246 | It is automatically generated with `l3build manifest`. 247 | ]]) 248 | 249 | end 250 | 251 | function manifest_write_subheading(filehandle,heading,description) 252 | 253 | filehandle:write("\n\n## " .. heading .. "\n\n") 254 | 255 | if description then 256 | filehandle:write(description) 257 | end 258 | 259 | end 260 | 261 | function manifest_write_group_heading(filehandle,heading,description) 262 | 263 | filehandle:write("\n### " .. heading .. "\n\n") 264 | 265 | if description then 266 | filehandle:write(description .. "\n") 267 | end 268 | 269 | end 270 | 271 | function manifest_write_group_file(filehandle,filename,param) 272 | --[[ 273 | filehandle : write file object 274 | filename : the count of the filename to be written 275 | 276 | param.dir : the directory of the file 277 | param.count : the name of the file to write 278 | param.filemaxchar : the maximum number of chars of all filenames in this group 279 | param.flag : false OR string for indicating CTAN/TDS location 280 | param.ctanfile : (boolean) if file is in CTAN dir 281 | param.tdsfile : (boolean) if file is in TDS dir 282 | --]] 283 | 284 | -- no file description: plain bullet list item: 285 | 286 | flagstr = param.flag or "" 287 | filehandle:write("* " .. filename .. " " .. flagstr .. "\n") 288 | 289 | --[[ 290 | -- or if you prefer an enumerated list: 291 | filehandle:write(param.count..". " .. filename .. "\n") 292 | --]] 293 | 294 | 295 | end 296 | 297 | function manifest_write_group_file_descr(filehandle,filename,descr,param) 298 | --[[ 299 | filehandle : write file object 300 | filename : the name of the file to write 301 | descr : description of the file to write 302 | 303 | param.dir : the directory of the file 304 | param.count : the count of the filename to be written 305 | param.filemaxchar : the maximum number of chars of all filenames in this group 306 | param.descmaxchar : the maximum number of chars of all descriptions in this group 307 | param.flag : false OR string for indicating CTAN/TDS location 308 | param.ctanfile : (boolean) if file is in CTAN dir 309 | param.tdsfile : (boolean) if file is in TDS dir 310 | --]] 311 | 312 | -- filename+description: Github-flavored Markdown table 313 | 314 | filestr = string.format(" | %-"..param.filemaxchar.."s",filename) 315 | flagstr = param.flag and string.format(" | %s",param.flag) or "" 316 | descstr = string.format(" | %-"..param.descmaxchar.."s",descr) 317 | 318 | filehandle:write(filestr..flagstr..descstr.." |\n") 319 | 320 | end 321 | 322 | --[[ 323 | Extracting ‘descriptions’ from source files 324 | ------------------------------------------- 325 | --]] 326 | 327 | function manifest_extract_filedesc(filehandle) 328 | 329 | -- no-op by default; two examples below 330 | 331 | end 332 | 333 | --[[ 334 | 335 | -- From the first match of a pattern in a file: 336 | manifest_extract_filedesc = function(filehandle) 337 | 338 | local all_file = filehandle:read("a") 339 | local matchstr = "\\section{(.-)}" 340 | 341 | filedesc = string.match(all_file,matchstr) 342 | 343 | return filedesc 344 | end 345 | 346 | -- From the match of the 2nd line (say) of a file: 347 | manifest_extract_filedesc = function(filehandle) 348 | 349 | local end_read_loop = 2 350 | local matchstr = "%%%S%s+(.*)" 351 | local this_line = "" 352 | 353 | for ii = 1, end_read_loop do 354 | this_line = filehandle:read("*line") 355 | end 356 | 357 | filedesc = string.match(this_line,matchstr) 358 | 359 | return filedesc 360 | end 361 | 362 | ]]-- 363 | -------------------------------------------------------------------------------- /l3build-manifest.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-manifest.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | 26 | --[[ 27 | L3BUILD MANIFEST 28 | ================ 29 | If desired this entire function can be replaced; if not, it uses a number of 30 | auxiliary functions which are included in this file. 31 | 32 | Additional setup can be performed by replacing the functions lists in the file 33 | `l3build-manifest-setup.lua`. 34 | --]] 35 | 36 | local ctanfiles 37 | local tdsfiles 38 | 39 | function manifest() 40 | 41 | -- build list of ctan files 42 | ctanfiles = {} 43 | for _,f in ipairs(filelist(ctandir.."/"..ctanpkg,"*.*")) do 44 | ctanfiles[f] = true 45 | end 46 | tdsfiles = {} 47 | for _,subdir in ipairs({"/doc/","/source/","/tex/"}) do 48 | for _,f in ipairs(filelist(tdsdir..subdir..moduledir,"*.*")) do 49 | tdsfiles[f] = true 50 | end 51 | end 52 | 53 | local manifest_entries = manifest_setup() 54 | 55 | for ii,_ in ipairs(manifest_entries) do 56 | manifest_entries[ii] = manifest_build_list(manifest_entries[ii]) 57 | end 58 | 59 | manifest_write(manifest_entries) 60 | 61 | local printline = "Manifest written to " .. manifestfile 62 | print((printline:gsub(".","*"))) print(printline) print((printline:gsub(".","*"))) 63 | 64 | return 0 65 | 66 | end 67 | 68 | --[[ 69 | Internal Manifest functions: build_list 70 | --------------------------------------- 71 | --]] 72 | 73 | manifest_build_list = function(entry) 74 | 75 | if not(entry.subheading) then 76 | 77 | entry = manifest_build_init(entry) 78 | 79 | -- build list of excluded files 80 | for _,glob_list in ipairs(entry.exclude) do 81 | for _,this_glob in ipairs(glob_list) do 82 | for _,this_file in ipairs(filelist(maindir,this_glob)) do 83 | entry.excludes[this_file] = true 84 | end 85 | end 86 | end 87 | 88 | -- build list of matched files 89 | for _,glob_list in ipairs(entry.files) do 90 | for _,this_glob in ipairs(glob_list) do 91 | 92 | local these_files = filelist(entry.dir,this_glob) 93 | these_files = manifest_sort_within_match(these_files) 94 | 95 | for _,this_file in ipairs(these_files) do 96 | entry = manifest_build_file(entry,this_file) 97 | end 98 | 99 | entry.files_ordered = manifest_sort_within_group(entry.files_ordered) 100 | 101 | end 102 | end 103 | 104 | end 105 | 106 | return entry 107 | 108 | end 109 | 110 | 111 | manifest_build_init = function(entry) 112 | 113 | -- currently these aren't customizable; I guess they could be? 114 | local manifest_group_defaults = { 115 | skipfiledescription = false , 116 | rename = false , 117 | dir = maindir , 118 | exclude = {excludefiles} , 119 | flag = true , 120 | } 121 | 122 | -- internal data added to each group in the table that needs to be initialized 123 | local manifest_group_init = { 124 | N = 0 , -- # matched files 125 | ND = 0 , -- # descriptions 126 | matches = {} , 127 | excludes = {} , 128 | files_ordered = {} , 129 | descr = {} , 130 | Nchar_file = 4 , -- TODO: generalize 131 | Nchar_descr = 11 , -- TODO: generalize 132 | } 133 | 134 | -- copy default options to each group if necessary 135 | for kk,ll in pairs(manifest_group_defaults) do 136 | if entry[kk] == nil then 137 | entry[kk] = ll 138 | end 139 | -- can't use "entry[kk] = entry[kk] or ll" because false/nil are indistinguishable! 140 | end 141 | 142 | -- initialization for internal data 143 | for kk,ll in pairs(manifest_group_init) do 144 | entry[kk] = ll 145 | end 146 | 147 | -- allow nested tables by requiring two levels of nesting 148 | if type(entry.files[1])=="string" then 149 | entry.files = {entry.files} 150 | end 151 | if type(entry.exclude[1])=="string" then 152 | entry.exclude = {entry.exclude} 153 | end 154 | 155 | return entry 156 | 157 | end 158 | 159 | local open = io.open 160 | 161 | manifest_build_file = function(entry,this_file) 162 | 163 | if entry.rename then 164 | this_file = this_file:gsub(entry.rename[1],entry.rename[2]) 165 | end 166 | 167 | if not entry.excludes[this_file] then 168 | 169 | entry.N = entry.N+1 170 | if not(entry.matches[this_file]) then 171 | 172 | entry.matches[this_file] = true -- store the file name 173 | entry.files_ordered[entry.N] = this_file -- store the file order 174 | entry.Nchar_file = math.max(entry.Nchar_file,this_file:len()) 175 | 176 | end 177 | 178 | if not(entry.skipfiledescription) then 179 | 180 | local ff = assert(open(entry.dir .. "/" .. this_file, "r")) 181 | this_descr = manifest_extract_filedesc(ff,this_file) 182 | ff:close() 183 | 184 | if this_descr and this_descr ~= "" then 185 | entry.descr[this_file] = this_descr 186 | entry.ND = entry.ND+1 187 | entry.Nchar_descr = math.max(entry.Nchar_descr,this_descr:len()) 188 | end 189 | 190 | end 191 | end 192 | 193 | return entry 194 | 195 | end 196 | 197 | --[[ 198 | Internal Manifest functions: write 199 | ---------------------------------- 200 | --]] 201 | 202 | manifest_write = function(manifest_entries) 203 | 204 | local f = assert(open(manifestfile, "w")) 205 | manifest_write_opening(f) 206 | 207 | for ii,vv in ipairs(manifest_entries) do 208 | if manifest_entries[ii].subheading then 209 | manifest_write_subheading(f,manifest_entries[ii].subheading,manifest_entries[ii].description) 210 | elseif manifest_entries[ii].N > 0 then 211 | manifest_write_group(f,manifest_entries[ii]) 212 | end 213 | end 214 | 215 | f:close() 216 | 217 | end 218 | 219 | 220 | manifest_write_group = function(f,entry) 221 | 222 | manifest_write_group_heading(f,entry.name,entry.description) 223 | 224 | if entry.ND > 0 then 225 | 226 | for ii,file in ipairs(entry.files_ordered) do 227 | local descr = entry.descr[file] or "" 228 | local param = { 229 | dir = entry.dir , 230 | count = ii , 231 | filemaxchar = entry.Nchar_file , 232 | descmaxchar = entry.Nchar_descr , 233 | ctanfile = ctanfiles[file] , 234 | tdsfile = tdsfiles[file] , 235 | flag = false , 236 | } 237 | 238 | if entry.flag then 239 | param.flag = " " 240 | if tdsfiles[file] and not(ctanfiles[file]) then 241 | param.flag = "† " 242 | elseif ctanfiles[file] then 243 | param.flag = "‡ " 244 | end 245 | end 246 | 247 | if ii == 1 then 248 | -- header of table 249 | -- TODO: generalize 250 | local p = {} 251 | for k,v in pairs(param) do p[k] = v end 252 | p.count = -1 253 | p.flag = p.flag and "Flag" 254 | manifest_write_group_file_descr(f,"File","Description",p) 255 | p.flag = p.flag and "--- " 256 | manifest_write_group_file_descr(f,"---","---",p) 257 | end 258 | 259 | manifest_write_group_file_descr(f,file,descr,param) 260 | end 261 | 262 | else 263 | 264 | for ii,file in ipairs(entry.files_ordered) do 265 | local param = { 266 | dir = entry.dir , 267 | count = ii , 268 | filemaxchar = entry.Nchar_file , 269 | ctanfile = ctanfiles[file] , 270 | tdsfile = tdsfiles[file] , 271 | } 272 | if entry.flag then 273 | param.flag = "" 274 | if tdsfiles[file] and not(ctanfiles[file]) then 275 | param.flag = "†" 276 | elseif ctanfiles[file] then 277 | param.flag = "‡" 278 | end 279 | end 280 | manifest_write_group_file(f,file,param) 281 | end 282 | 283 | end 284 | 285 | end 286 | -------------------------------------------------------------------------------- /l3build-stdmain.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-stdmain.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local lfs = require("lfs") 26 | 27 | local exit = os.exit 28 | local insert = table.insert 29 | 30 | -- List all modules 31 | function listmodules() 32 | local modules = { } 33 | local exclmodules = exclmodules or { } 34 | for entry in lfs.dir(".") do 35 | if entry ~= "." and entry ~= ".." then 36 | local attr = lfs.attributes(entry) 37 | assert(type(attr) == "table") 38 | if attr.mode == "directory" and fileexists(entry .."/" .."build.lua") then 39 | if not exclmodules[entry] then 40 | insert(modules, entry) 41 | end 42 | end 43 | end 44 | end 45 | return modules 46 | end 47 | 48 | target_list = 49 | { 50 | -- Some hidden targets 51 | bundlecheck = 52 | { 53 | func = check, 54 | pre = function(names) 55 | if names then 56 | print("Bundle checks should not list test names") 57 | help() 58 | exit(1) 59 | end 60 | return 0 61 | end 62 | }, 63 | bundlectan = 64 | { 65 | func = bundlectan 66 | }, 67 | bundleunpack = 68 | { 69 | func = bundleunpack, 70 | pre = function() return(dep_install(unpackdeps)) end 71 | }, 72 | -- Public targets 73 | check = 74 | { 75 | bundle_target = true, 76 | desc = "Runs all automated tests", 77 | func = check, 78 | }, 79 | clean = 80 | { 81 | bundle_func = bundleclean, 82 | desc = "Cleans out directory tree", 83 | func = clean 84 | }, 85 | ctan = 86 | { 87 | bundle_func = ctan, 88 | desc = "Creates CTAN-ready archive", 89 | func = ctan 90 | }, 91 | doc = 92 | { 93 | desc = "Typesets all documentation files", 94 | func = doc 95 | }, 96 | install = 97 | { 98 | desc = "Installs files into the local texmf tree", 99 | func = install 100 | }, 101 | manifest = 102 | { 103 | desc = "Creates a manifest file", 104 | func = manifest 105 | }, 106 | save = 107 | { 108 | desc = "Saves test validation log", 109 | func = save 110 | }, 111 | tag = 112 | { 113 | bundle_func = function(names) 114 | local modules = modules or listmodules() 115 | local errorlevel = call(modules,"tag") 116 | -- Deal with any files in the bundle dir itself 117 | if errorlevel == 0 then 118 | errorlevel = tag(names) 119 | end 120 | return errorlevel 121 | end, 122 | desc = "Updates release tags in files", 123 | func = tag, 124 | pre = function(names) 125 | if names and #names > 1 then 126 | print("Too many tags specified; exactly one required") 127 | exit(1) 128 | end 129 | return 0 130 | end 131 | }, 132 | uninstall = 133 | { 134 | desc = "Uninstalls files from the local texmf tree", 135 | func = uninstall 136 | }, 137 | unpack= 138 | { 139 | bundle_target = true, 140 | desc = "Unpacks the source files into the build tree", 141 | func = unpack 142 | }, 143 | upload = 144 | { 145 | desc = "Sends archive to CTAN for public release", 146 | func = upload 147 | }, 148 | } 149 | 150 | -- 151 | -- The overall main function 152 | -- 153 | 154 | function main(target,names) 155 | -- Deal with unknown targets up-front 156 | if not target_list[target] then 157 | help() 158 | exit(1) 159 | end 160 | local errorlevel = 0 161 | if module == "" then 162 | modules = modules or listmodules() 163 | if target_list[target].bundle_func then 164 | errorlevel = target_list[target].bundle_func(names) 165 | else 166 | -- Detect all of the modules 167 | if target_list[target].bundle_target then 168 | target = "bundle" .. target 169 | end 170 | errorlevel = call(modules,target) 171 | end 172 | else 173 | if target_list[target].pre then 174 | errorlevel = target_list[target].pre(names) 175 | if errorlevel ~= 0 then 176 | exit(1) 177 | end 178 | end 179 | errorlevel = target_list[target].func(names) 180 | end 181 | -- All done, finish up 182 | if errorlevel ~= 0 then 183 | exit(1) 184 | else 185 | exit(0) 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /l3build-tagging.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-tagging.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local pairs = pairs 26 | local open = io.open 27 | local os_date = os.date 28 | local match = string.match 29 | local gsub = string.gsub 30 | 31 | function update_tag(filename,content,tagname,tagdate) 32 | return content 33 | end 34 | 35 | function tag_hook(tagname,tagdate) 36 | return 0 37 | end 38 | 39 | local function update_file_tag(file,tagname,tagdate) 40 | local filename = basename(file) 41 | print("Tagging ".. filename) 42 | ---@type file*? 43 | local f = assert(open(file,"rb")) 44 | ---@cast f file* 45 | local content = f:read("a") 46 | f:close() 47 | f = nil 48 | -- Deal with Unix/Windows line endings 49 | content = gsub(content .. (match(content,"\n$") and "" or "\n"), 50 | "\r\n", "\n") 51 | local updated_content = update_tag(filename,content,tagname,tagdate) 52 | if content == updated_content then 53 | return 0 54 | else 55 | local path = dirname(file) 56 | ren(path,filename,filename .. ".bak") 57 | f = assert(open(file,"w")) 58 | -- Convert line ends back if required during write 59 | -- Watch for the second return value! 60 | f:write((gsub(updated_content,"\n",os_newline))) 61 | f:close() 62 | rm(path,filename .. ".bak") 63 | end 64 | return 0 65 | end 66 | 67 | function tag(tagnames) 68 | local tagdate = options["date"] or os_date("%Y-%m-%d") 69 | local tagname = nil 70 | if tagnames then 71 | tagname = tagnames[1] 72 | end 73 | local dirs = remove_duplicates({currentdir, sourcefiledir, docfiledir}) 74 | local errorlevel = 0 75 | for _,dir in pairs(dirs) do 76 | for _,filetype in pairs(tagfiles) do 77 | for _,p in ipairs(tree(dir,filetype)) do 78 | errorlevel = update_file_tag(dir .. "/" .. p.src,tagname,tagdate) 79 | if errorlevel ~= 0 then 80 | return errorlevel 81 | end 82 | end 83 | end 84 | end 85 | return tag_hook(tagname,tagdate) 86 | end 87 | -------------------------------------------------------------------------------- /l3build-typesetting.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-typesetting.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | -- 26 | -- Auxiliary functions for typesetting: need to be generally available 27 | -- 28 | 29 | local ipairs = ipairs 30 | local pairs = pairs 31 | local print = print 32 | 33 | local gsub = string.gsub 34 | 35 | local os_type = os.type 36 | 37 | function dvitopdf(name, dir, engine, hide) 38 | runcmd( 39 | set_epoch_cmd(epoch, forcecheckepoch) .. 40 | "dvips " .. name .. dviext 41 | .. (hide and (" > " .. os_null) or "") 42 | .. os_concat .. 43 | "ps2pdf " .. ps2pdfopts .. " " .. name .. psext 44 | .. (hide and (" > " .. os_null) or ""), 45 | dir 46 | ) 47 | end 48 | 49 | function biber(name,dir) 50 | if fileexists(dir .. "/" .. name .. ".bcf") then 51 | return 52 | runcmd(biberexe .. " " .. biberopts .. " " .. name,dir,{"BIBINPUTS"}) 53 | end 54 | return 0 55 | end 56 | 57 | function bibtex(name,dir) 58 | dir = dir or "." 59 | if fileexists(dir .. "/" .. name .. ".aux") then 60 | -- LaTeX always generates an .aux file, so there is a need to 61 | -- look inside it for a \citation line 62 | local grep 63 | if os_type == "windows" then 64 | grep = "\\\\" 65 | else 66 | grep = "\\\\\\\\" 67 | end 68 | if run(dir, 69 | os_grepexe .. " \"^" .. grep .. "citation{\" " .. name .. ".aux > " 70 | .. os_null 71 | ) + run(dir, 72 | os_grepexe .. " \"^" .. grep .. "bibdata{\" " .. name .. ".aux > " 73 | .. os_null 74 | ) == 0 then 75 | local errorlevel = runcmd(bibtexexe .. " " .. bibtexopts .. " " .. name, 76 | dir,{"BIBINPUTS","BSTINPUTS"}) 77 | -- BibTeX(8) signals warnings with errorlevel 1 78 | if errorlevel > 1 then return errorlevel else return 0 end 79 | end 80 | end 81 | return 0 82 | end 83 | 84 | function makeindex(name,dir,inext,outext,logext,style) 85 | dir = dir or "." 86 | if fileexists(dir .. "/" .. name .. inext) then 87 | if style == "" then style = nil end 88 | return runcmd(makeindexexe .. " " .. makeindexopts 89 | .. " -o " .. name .. outext 90 | .. (style and (" -s " .. style) or "") 91 | .. " -t " .. name .. logext .. " " .. name .. inext, 92 | dir, 93 | {"INDEXSTYLE"}) 94 | end 95 | return 0 96 | end 97 | 98 | function tex(file,dir,cmd) 99 | dir = dir or "." 100 | cmd = cmd or typesetexe .. " " .. typesetopts 101 | return runcmd(cmd .. " \"" .. typesetcmds 102 | .. "\\input " .. file .. "\"", 103 | dir,{"TEXINPUTS","LUAINPUTS"}) 104 | end 105 | 106 | local function typesetpdf(file,dir) 107 | dir = dir or "." 108 | local name = jobname(file) 109 | print("Typesetting " .. name) 110 | local fn = typeset 111 | local cmd = typesetexe .. " " .. typesetopts 112 | if specialtypesetting and specialtypesetting[file] then 113 | fn = specialtypesetting[file].func or fn 114 | cmd = specialtypesetting[file].cmd or cmd 115 | end 116 | local errorlevel = fn(file,dir,cmd) 117 | if errorlevel ~= 0 then 118 | print(" ! Compilation failed") 119 | return errorlevel 120 | end 121 | return 0 122 | end 123 | 124 | function typeset(file,dir,exe) 125 | dir = dir or "." 126 | local errorlevel = tex(file,dir,exe) 127 | if errorlevel ~= 0 then 128 | return errorlevel 129 | end 130 | local name = jobname(file) 131 | errorlevel = biber(name,dir) + bibtex(name,dir) 132 | if errorlevel ~= 0 then 133 | return errorlevel 134 | end 135 | for i = 2,typesetruns do 136 | errorlevel = 137 | makeindex(name,dir,".glo",".gls",".glg",glossarystyle) + 138 | makeindex(name,dir,".idx",".ind",".ilg",indexstyle) + 139 | tex(file,dir,exe) 140 | if errorlevel ~= 0 then break end 141 | end 142 | return errorlevel 143 | end 144 | 145 | -- A hook to allow additional typesetting of demos 146 | function typeset_demo_tasks() 147 | return 0 148 | end 149 | 150 | local function docinit() 151 | -- Set up 152 | dep_install(typesetdeps) 153 | unpack({sourcefiles, typesetsourcefiles}, {sourcefiledir, docfiledir}) 154 | cleandir(typesetdir) 155 | for _,file in pairs(typesetfiles) do 156 | cp(file, unpackdir, typesetdir) 157 | end 158 | for _,filetype in pairs( 159 | {bibfiles, docfiles, typesetfiles, typesetdemofiles} 160 | ) do 161 | for _,file in pairs(filetype) do 162 | cp(file, docfiledir, typesetdir) 163 | end 164 | end 165 | for _,file in pairs(sourcefiles) do 166 | cp(file, sourcefiledir, typesetdir) 167 | end 168 | for _,file in pairs(typesetsuppfiles) do 169 | cp(file, supportdir, typesetdir) 170 | end 171 | -- Main loop for doc creation 172 | local errorlevel = typeset_demo_tasks() 173 | if errorlevel ~= 0 then 174 | return errorlevel 175 | end 176 | return docinit_hook() 177 | end 178 | 179 | function docinit_hook() return 0 end 180 | 181 | -- Typeset all required documents 182 | -- Uses a set of dedicated auxiliaries that need to be available to others 183 | function doc(files) 184 | local errorlevel = 0 185 | if not options["rerun"] then 186 | errorlevel = docinit() 187 | end 188 | if errorlevel ~= 0 then return errorlevel end 189 | local done = {} 190 | local files_unknown = {} 191 | if files and next(files) then 192 | for _, file in pairs(files) do 193 | files_unknown[file] = true 194 | end 195 | end 196 | for _,typesetfiles in ipairs({typesetdemofiles,typesetfiles}) do 197 | for _,glob in pairs(typesetfiles) do 198 | local destpath,globstub = splitpath(glob) 199 | destpath = docfiledir .. gsub(gsub(destpath,"^./",""),"^.","") 200 | for _,p in ipairs(tree(typesetdir,globstub)) do 201 | local path,srcname = splitpath(p.cwd) 202 | local name = jobname(srcname) 203 | if not done[name] then 204 | local typeset = true 205 | -- Allow for command line selection of files 206 | if files and next(files) then 207 | typeset = false 208 | for _,file in pairs(files) do 209 | if name == file then 210 | files_unknown[file] = nil 211 | typeset = true 212 | break 213 | end 214 | end 215 | end 216 | -- Now know if we should typeset this source 217 | if typeset then 218 | errorlevel = typesetpdf(srcname,path) 219 | if errorlevel ~= 0 then 220 | return errorlevel 221 | else 222 | done[name] = true 223 | local pdfname = jobname(srcname) .. pdfext 224 | rm(pdfname,destpath) 225 | cp(pdfname,path,destpath) 226 | end 227 | end 228 | end 229 | end 230 | end 231 | end 232 | if next(files_unknown) then 233 | for file, _ in pairs(files_unknown) do 234 | print("Unknown doc name \"" .. file .. "\"") 235 | end 236 | return 1 237 | end 238 | return 0 239 | end 240 | -------------------------------------------------------------------------------- /l3build-unpack.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-unpack.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | -- Unpack the package files using an 'isolated' system: this requires 26 | -- a copy of the 'basic' DocStrip program, which is used then removed 27 | function unpack(sources, sourcedirs) 28 | local errorlevel = dep_install(unpackdeps) 29 | if errorlevel ~= 0 then 30 | return errorlevel 31 | end 32 | errorlevel = bundleunpack(sourcedirs, sources) 33 | if errorlevel ~= 0 then 34 | return errorlevel 35 | end 36 | for _,i in ipairs(installfiles) do 37 | errorlevel = cp(i, unpackdir, localdir) 38 | if errorlevel ~= 0 then 39 | return errorlevel 40 | end 41 | end 42 | return 0 43 | end 44 | 45 | -- Split off from the main unpack so it can be used on a bundle and not 46 | -- leave only one modules files 47 | function bundleunpack(sourcedirs, sources) 48 | local errorlevel = mkdir(localdir) 49 | if errorlevel ~=0 then 50 | return errorlevel 51 | end 52 | errorlevel = cleandir(unpackdir) 53 | if errorlevel ~=0 then 54 | return errorlevel 55 | end 56 | for _,i in ipairs(sourcedirs or {sourcefiledir}) do 57 | for _,j in ipairs(sources or {sourcefiles}) do 58 | for _,k in ipairs(j) do 59 | errorlevel = cp(k, i, unpackdir) 60 | if errorlevel ~=0 then 61 | return errorlevel 62 | end 63 | end 64 | end 65 | end 66 | for _,i in ipairs(unpacksuppfiles) do 67 | errorlevel = cp(i, supportdir, localdir) 68 | if errorlevel ~=0 then 69 | return errorlevel 70 | end 71 | end 72 | local popen = io.popen 73 | for _,i in ipairs(unpackfiles) do 74 | for _,p in ipairs(tree(unpackdir, i)) do 75 | local path, name = splitpath(p.src) 76 | local localdir = abspath(localdir) 77 | local success = assert(popen( 78 | "cd " .. unpackdir .. "/" .. path .. os_concat .. 79 | os_setenv .. " TEXINPUTS=." .. os_pathsep 80 | .. localdir .. (unpacksearch and os_pathsep or "") .. 81 | os_concat .. 82 | os_setenv .. " LUAINPUTS=." .. os_pathsep 83 | .. localdir .. (unpacksearch and os_pathsep or "") .. 84 | os_concat .. 85 | unpackexe .. " " .. unpackopts .. " " .. name 86 | .. (options["quiet"] and (" > " .. os_null) or ""), 87 | "w" 88 | ):write(string.rep("y\n", 300))):close() 89 | if not success then 90 | return 1 91 | end 92 | end 93 | end 94 | return 0 95 | end 96 | -------------------------------------------------------------------------------- /l3build-upload.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-upload.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local lfs = require("lfs") 26 | 27 | local pairs = pairs 28 | local print = print 29 | local tostring = tostring 30 | 31 | local close = io.close 32 | local flush = io.flush 33 | local open = io.open 34 | local output = io.output 35 | local popen = io.popen 36 | local read = io.read 37 | local write = io.write 38 | 39 | local len = string.len 40 | local lower = string.lower 41 | local match = string.match 42 | 43 | -- UPLOAD() 44 | -- 45 | -- takes a package configuration table and an optional boolean 46 | -- 47 | -- if the upload parameter is not supplied or is not true, only package validation 48 | -- is used, if upload is true then package upload will be attempted if validation 49 | -- succeeds. 50 | 51 | -- fields are given as a string, or optionally for fields allowing multiple 52 | -- values, as a table of strings. 53 | 54 | -- Mandatory fields are checked in Lua 55 | -- Maximum string lengths are checked. 56 | 57 | -- Currently string values are not checked, eg license names, or URL syntax. 58 | 59 | -- The input form could be used to construct a post body but 60 | -- luasec is not included in texlua. Instead an external program is used to post. 61 | -- As Windows (since April 2018) includes curl now use curl. 62 | -- A version using ctan-o-mat is available in the ctan-post github repo. 63 | 64 | -- the main interface is 65 | -- upload() 66 | -- with a configuration table `uploadconfig` 67 | 68 | 69 | local curl_debug = curl_debug or false -- to disable posting 70 | -- For now, this is undocumented. 71 | 72 | 73 | if options["dry-run"] then 74 | ctanupload = false 75 | end 76 | -- if ctanupload is nil or false, only validation is attempted 77 | -- if ctanupload is true the ctan upload URL will be used after validation 78 | -- if upload is anything else, the user will be prompted whether to upload. 79 | -- For now, this is undocumented. I think I would prefer to keep it always set to ask for the time being. 80 | 81 | local ctan_post -- this is private to the module 82 | 83 | -- TODO: next is a public global method, 84 | -- but following functions are semantically local 85 | -- despite they are declared globally. 86 | 87 | function upload(tagnames) 88 | 89 | local uploadfile = ctanzip..".zip" 90 | 91 | -- Keep data local 92 | local uploadconfig = uploadconfig 93 | 94 | -- try a sensible default for the package name: 95 | uploadconfig.pkg = uploadconfig.pkg or ctanpkg or nil 96 | 97 | -- Get data from command line if appropriate 98 | if options["file"] then 99 | local f = assert(open(options["file"],"r")) 100 | uploadconfig.announcement = assert(f:read('a')) 101 | f:close() 102 | end 103 | uploadconfig.announcement = options["message"] or uploadconfig.announcement or file_contents(uploadconfig.announcement_file) 104 | uploadconfig.email = options["email"] or uploadconfig.email 105 | 106 | 107 | uploadconfig.note = uploadconfig.note or file_contents(uploadconfig.note_file) 108 | 109 | tagnames = tagnames or { } 110 | uploadconfig.version = tagnames[1] or uploadconfig.version 111 | 112 | local override_update_check = false 113 | if uploadconfig.update == nil then 114 | uploadconfig.update = true 115 | override_update_check = true 116 | end 117 | 118 | -- avoid lower level error from post command if zip file missing 119 | local ziptime = lfs.attributes(trim_space(tostring(uploadfile)), 'modification') 120 | if not ziptime then 121 | error("Missing zip file '" .. tostring(uploadfile) .. "'. \z 122 | Maybe you forgot to run 'l3build ctan' first?") 123 | end 124 | local age = os.time() - ziptime 125 | if age >= 86400 then 126 | print(string.format("------------------------------------------\n\z 127 | | The local archive is older than %3i days. |\n\z 128 | | Are you sure that you executed 'l3build ctan' first? |\n\z 129 | --------------------------------------------------------", 130 | age // 86400)) 131 | print("Are you sure you want to continue? [y/n]" ) 132 | io.stdout:write("> "):flush() 133 | if lower(read(),1,1) ~= "y" then 134 | print'Aborting' 135 | return 1 136 | end 137 | end 138 | 139 | ctan_post = construct_ctan_post(uploadfile,options["debug"]) 140 | 141 | 142 | -- curl file version 143 | local curloptfile = uploadconfig.curlopt_file or (ctanzip .. ".curlopt") 144 | ---@type file*? 145 | local curlopt=assert(open(curloptfile,"w")) 146 | ---@cast curlopt file* 147 | output(curlopt) 148 | write(ctan_post) 149 | curlopt:close() 150 | curlopt = nil 151 | 152 | ctan_post=curlexe .. " --config " .. curloptfile 153 | 154 | 155 | if options["debug"] then 156 | ctan_post = ctan_post .. ' https://httpbin.org/post' 157 | fp_return = shell(ctan_post) 158 | print('\n\nCURL COMMAND:') 159 | print(ctan_post) 160 | print("\n\nHTTP RESPONSE:") 161 | print(fp_return) 162 | return 1 163 | else 164 | ctan_post = ctan_post .. ' https://ctan.org/submit/' 165 | end 166 | 167 | -- call post command to validate the upload at CTAN's validate URL 168 | local exit_status=0 169 | local fp_return="" 170 | 171 | -- use popen not execute so get the return body local exit_status=os.execute(ctan_post .. "validate") 172 | if (curl_debug==false) then 173 | print("Contacting CTAN for validation:") 174 | fp_return = shell(ctan_post .. "validate") 175 | else 176 | fp_return="WARNING: curl_debug==true: posting disabled" 177 | print(ctan_post) 178 | return 1 179 | end 180 | if override_update_check then 181 | if match(fp_return,"non%-existent%spackage") then 182 | print("Package not found on CTAN; re-validating as new package:") 183 | uploadconfig.update = false 184 | ctan_post = construct_ctan_post(uploadfile) 185 | fp_return = shell(ctan_post .. "validate") 186 | end 187 | end 188 | if (match(fp_return,"ERROR")) then 189 | exit_status=1 190 | end 191 | 192 | -- if upload requested and validation succeeded repost to the upload URL 193 | if (exit_status==0 or exit_status==nil) then 194 | if (ctanupload ~=nil and ctanupload ~=false and ctanupload ~= true) then 195 | if (match(fp_return,"WARNING")) then 196 | print("Warnings from CTAN package validation:" .. fp_return:gsub("%[","\n["):gsub("%]%]","]\n]")) 197 | else 198 | print("Validation successful." ) 199 | end 200 | print("" ) 201 | if age < 86400 and age >= 60 then 202 | if age >= 3600 then 203 | print("----------------------------------------------------" ) 204 | print(string.format("| The local archive is older than %2i hours. |", age//3600 )) 205 | print("| Have you executed l3build ctan first? If so ... |" ) 206 | print("----------------------------------------------------" ) 207 | else 208 | print(string.format("The local archive is %i minutes old.", age//60 )) 209 | end 210 | end 211 | print("Do you want to upload to CTAN? [y/n]" ) 212 | local answer="" 213 | io.stdout:write("> ") 214 | io.stdout:flush() 215 | answer=read() 216 | if(lower(answer,1,1)=="y") then 217 | ctanupload=true 218 | end 219 | end 220 | if (ctanupload==true) then 221 | fp_return = shell(ctan_post .. "upload") 222 | -- this is just html, could save to a file 223 | -- or echo a cleaned up version 224 | print('Response from CTAN:') 225 | print(fp_return) 226 | if match(fp_return,"WARNING") or match(fp_return,"ERROR") then 227 | exit_status=1 228 | end 229 | else 230 | if (match(fp_return,"WARNING")) then 231 | print("Warnings from CTAN package validation:" .. fp_return:gsub("%[","\n["):gsub("%]%]","]\n]")) 232 | else 233 | print("CTAN validation successful") 234 | end 235 | end 236 | else 237 | error("Warnings from CTAN package validation:\n" .. fp_return) 238 | end 239 | return exit_status 240 | end 241 | 242 | 243 | function trim_space(s) 244 | return (s:gsub("^%s*(.-)%s*$", "%1")) 245 | end 246 | 247 | 248 | function shell(s) 249 | local h = assert(popen(s, 'r')) 250 | local t = assert(h:read('*a')) 251 | local success = h:close() 252 | if (success) then 253 | return t 254 | else 255 | error("\nError from shell command:\n" .. s .. "\n" .. t .. "\n") 256 | end 257 | end 258 | 259 | function construct_ctan_post(uploadfile,debug) 260 | 261 | -- start building the curl command: 262 | -- commandline ctan_post = curlexe .. " " 263 | ctan_post="" 264 | 265 | -- build up the curl command field-by-field: 266 | 267 | -- field max desc mandatory multi 268 | -- ---------------------------------------------------------------------------------------------------- 269 | ctan_field("announcement", uploadconfig.announcement, 8192, "Announcement", true, false ) 270 | ctan_field("author", uploadconfig.author, 128, "Author name", true, false ) 271 | ctan_field("bugtracker", uploadconfig.bugtracker, 255, "URL(s) of bug tracker", false, true ) 272 | ctan_field("ctanPath", uploadconfig.ctanPath, 255, "CTAN path", true, false ) 273 | ctan_field("description", uploadconfig.description, 4096, "Short description of package", false, false ) 274 | ctan_field("development", uploadconfig.development, 255, "URL(s) of development channels", false, true ) 275 | ctan_field("email", uploadconfig.email, 255, "Email of uploader", true, false ) 276 | ctan_field("home", uploadconfig.home, 255, "URL(s) of home page", false, true ) 277 | ctan_field("license", uploadconfig.license, 2048, "Package license(s)", true, true ) 278 | ctan_field("note", uploadconfig.note, 4096, "Internal note to ctan", false, false ) 279 | ctan_field("pkg", uploadconfig.pkg, 32, "Package name", true, false ) 280 | ctan_field("repository", uploadconfig.repository, 255, "URL(s) of source repositories", false, true ) 281 | ctan_field("summary", uploadconfig.summary, 128, "One-line summary of package", true, false ) 282 | ctan_field("support", uploadconfig.support, 255, "URL(s) of support channels", false, true ) 283 | ctan_field("topic", uploadconfig.topic, 1024, "Topic(s)", false, true ) 284 | ctan_field("update", uploadconfig.update, 8, "Boolean: true=update, false=new pkg", false, false ) 285 | ctan_field("uploader", uploadconfig.uploader, 255, "Name of uploader", true, false ) 286 | ctan_field("version", uploadconfig.version, 32, "Package version", true, false ) 287 | 288 | ctan_post = ctan_post .. '\nform="file=@' .. tostring(uploadfile) .. ';filename=' .. tostring(uploadfile) .. '"' 289 | 290 | return ctan_post 291 | 292 | end 293 | 294 | function ctan_field(fname,fvalue,max,desc,mandatory,multi) 295 | if (type(fvalue)=="table" and multi==true) then 296 | for i, v in pairs(fvalue) do 297 | ctan_single_field(fname,v,max,desc,mandatory and i==1) 298 | end 299 | else 300 | ctan_single_field(fname,fvalue,max,desc,mandatory) 301 | end 302 | end 303 | 304 | 305 | function ctan_single_field(fname,fvalue,max,desc,mandatory) 306 | local fvalueprint = fvalue 307 | if fvalue == nil then fvalueprint = '??' end 308 | print('ctan-upload | ' .. fname .. ': ' ..tostring(fvalueprint)) 309 | if ((fvalue==nil and mandatory) or (fvalue == 'ask')) then 310 | if (max < 256) then 311 | fvalue=input_single_line_field(fname) 312 | else 313 | fvalue=input_multi_line_field(fname) 314 | end 315 | end 316 | if (fvalue==nil or type(fvalue)~="table") then 317 | local vs=trim_space(tostring(fvalue)) 318 | if (mandatory==true and (fvalue == nil or vs=="")) then 319 | if (fname=="announcement") then 320 | print("Empty announcement: No ctan announcement will be made") 321 | else 322 | error("The field " .. fname .. " must contain " .. desc) 323 | end 324 | end 325 | if (fvalue ~=nil and len(vs) > 0) then 326 | if (max > 0 and len(vs) > max) then 327 | error("The field " .. fname .. " is longer than " .. max) 328 | end 329 | vs = vs:gsub('\\','\\\\') 330 | vs = vs:gsub('"','\\"') 331 | vs = vs:gsub('`','\\`') 332 | vs = vs:gsub('\n','\\n') 333 | -- for strings on commandline version ctan_post=ctan_post .. ' --form "' .. fname .. "=" .. vs .. '"' 334 | ctan_post=ctan_post .. '\nform-string="' .. fname .. '=' .. vs .. '"' 335 | end 336 | else 337 | error("The value of the field '" .. fname .."' must be a scalar not a table") 338 | end 339 | end 340 | 341 | 342 | -- function for interactive multiline fields 343 | function input_multi_line_field (name) 344 | print("Enter " .. name .. " three or ctrl-D to stop") 345 | 346 | local field="" 347 | 348 | local answer_line 349 | local return_count=0 350 | repeat 351 | write("> ") 352 | flush() 353 | answer_line=read() 354 | if answer_line=="" then 355 | return_count=return_count+1 356 | else 357 | for i=1,return_count,1 do 358 | field = field .. "\n" 359 | end 360 | return_count=0 361 | if answer_line~=nil then 362 | field = field .. "\n" .. answer_line 363 | end 364 | end 365 | until (return_count==3 or answer_line==nil or answer_line=='\004') 366 | return field 367 | end 368 | 369 | function input_single_line_field(name) 370 | print("Enter " .. name ) 371 | 372 | local field="" 373 | 374 | write("> ") 375 | flush() 376 | field=read() 377 | return field 378 | end 379 | 380 | 381 | -- if filename is non nil and file readable return contents otherwise nil 382 | function file_contents (filename) 383 | if filename ~= nil then 384 | local f= assert(open(filename,"r")) 385 | if f==nil then 386 | return nil 387 | else 388 | local s = f:read("a") 389 | f:close() 390 | return s 391 | end 392 | else 393 | return nil 394 | end 395 | end 396 | -------------------------------------------------------------------------------- /l3build-variables.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-variables.lua Copyright (C) 2018-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | -- "module" is a deprecated function in Lua 5.2: as we want the name 26 | -- for other purposes, and it should eventually be 'free', simply 27 | -- remove the built-in 28 | if type(module) == "function" then 29 | module = nil 30 | end 31 | 32 | -- Ensure the module and bundle exist 33 | module = module or "" 34 | bundle = bundle or "" 35 | 36 | -- Directory structure for the build system 37 | -- Use Unix-style path separators 38 | currentdir = "." 39 | maindir = maindir or currentdir 40 | 41 | -- Substructure for file locations 42 | docfiledir = docfiledir or currentdir 43 | sourcefiledir = sourcefiledir or currentdir 44 | textfiledir = textfiledir or currentdir 45 | supportdir = supportdir or maindir .. "/support" 46 | testfiledir = testfiledir or currentdir .. "/testfiles" 47 | testsuppdir = testsuppdir or testfiledir .. "/support" 48 | texmfdir = texmfdir or maindir .. "/texmf" 49 | 50 | -- Structure within a development area 51 | builddir = builddir or maindir .. "/build" 52 | distribdir = distribdir or builddir .. "/distrib" 53 | localdir = localdir or builddir .. "/local" 54 | resultdir = resultdir or builddir .. "/result" 55 | testdir = testdir or builddir .. "/test" 56 | typesetdir = typesetdir or builddir .. "/doc" 57 | unpackdir = unpackdir or builddir .. "/unpacked" 58 | 59 | -- Substructure for CTAN release material 60 | ctandir = ctandir or distribdir .. "/ctan" 61 | tdsdir = tdsdir or distribdir .. "/tds" 62 | tdsroot = tdsroot or "latex" 63 | 64 | -- Location for installation on CTAN or in TEXMFHOME 65 | if bundle == "" then 66 | moduledir = tdsroot .. "/" .. module 67 | ctanpkg = ctanpkg or module 68 | else 69 | moduledir = tdsroot .. "/" .. bundle .. "/" .. module 70 | ctanpkg = ctanpkg or bundle 71 | end 72 | 73 | -- File types for various operations 74 | -- Use Unix-style globs 75 | -- All of these may be set earlier, so an initialized conditionally 76 | auxfiles = auxfiles or {"*.aux", "*.lof", "*.lot", "*.toc"} 77 | bibfiles = bibfiles or {"*.bib"} 78 | binaryfiles = binaryfiles or {"*.pdf", "*.zip"} 79 | bstfiles = bstfiles or {"*.bst"} 80 | checkfiles = checkfiles or { } 81 | checksuppfiles = checksuppfiles or { } 82 | cleanfiles = cleanfiles or {"*.log", "*.pdf", "*.zip"} 83 | demofiles = demofiles or { } 84 | docfiles = docfiles or { } 85 | dynamicfiles = dynamicfiles or { } 86 | excludefiles = excludefiles or {"*~","build.lua","config-*.lua"} 87 | exefiles = exefiles or { } 88 | installfiles = installfiles or {"*.sty","*.cls"} 89 | makeindexfiles = makeindexfiles or {"*.ist"} 90 | scriptfiles = scriptfiles or { } 91 | scriptmanfiles = scriptmanfiles or { } 92 | sourcefiles = sourcefiles or {"*.dtx", "*.ins", "*-????-??-??.sty"} 93 | tagfiles = tagfiles or {"*.dtx"} 94 | textfiles = textfiles or {"*.md", "*.txt"} 95 | typesetdemofiles = typesetdemofiles or { } 96 | typesetfiles = typesetfiles or {"*.dtx"} 97 | typesetsuppfiles = typesetsuppfiles or { } 98 | typesetsourcefiles = typesetsourcefiles or { } 99 | unpackfiles = unpackfiles or {"*.ins"} 100 | unpacksuppfiles = unpacksuppfiles or { } 101 | 102 | -- Roots which should be unpacked to support unpacking/testing/typesetting 103 | checkdeps = checkdeps or { } 104 | typesetdeps = typesetdeps or { } 105 | unpackdeps = unpackdeps or { } 106 | 107 | -- Executable names plus following options 108 | typesetexe = typesetexe or "pdflatex" 109 | unpackexe = unpackexe or "pdftex" 110 | 111 | checkopts = checkopts or "-interaction=nonstopmode" 112 | typesetopts = typesetopts or "-interaction=nonstopmode" 113 | unpackopts = unpackopts or "" 114 | 115 | -- Engines for testing 116 | checkengines = checkengines or {"pdftex", "xetex", "luatex"} 117 | checkformat = checkformat or "latex" 118 | specialformats = specialformats or { } 119 | specialformats.context = specialformats.context or { 120 | luametatex = {binary = "context", format = ""}, 121 | luatex = {binary = "context", format = "", options = "--luatex"}, 122 | pdftex = {binary = "texexec", format = ""}, 123 | xetex = {binary = "texexec", format = "", options = "--xetex"} 124 | } 125 | specialformats.latex = specialformats.latex or { } 126 | specialformats.latex.etex = specialformats.latex.etex or 127 | {format = "latex"} 128 | specialformats.latex.ptex = specialformats.latex.ptex or 129 | {binary = "euptex", options = "-kanji-internal=euc"} 130 | specialformats.latex.uptex = specialformats.latex.uptex or 131 | {binary = "euptex"} 132 | if not string.find(status.banner,"2019") then 133 | specialformats.latex.luatex = specialformats.latex.luatex or 134 | {binary = "luahbtex",format = "lualatex"} 135 | specialformats["latex-dev"] = specialformats["latex-dev"] or 136 | {luatex = {binary="luahbtex",format = "lualatex-dev"}} 137 | end 138 | specialformats.latex["make4ht"] = specialformats.latex["make4ht"] or 139 | {binary = "make4ht"} 140 | 141 | stdengine = stdengine or checkengines[1] or "pdftex" 142 | 143 | -- The tests themselves 144 | includetests = includetests or {"*"} 145 | excludetests = excludetests or { } 146 | 147 | -- Configs for testing 148 | checkconfigs = checkconfigs or {"build"} 149 | 150 | -- Enable access to trees outside of the repo 151 | -- As these may be set false, a more elaborate test than normal is needed 152 | if checksearch == nil then 153 | checksearch = true 154 | end 155 | if typesetsearch == nil then 156 | typesetsearch = true 157 | end 158 | if unpacksearch == nil then 159 | unpacksearch = true 160 | end 161 | 162 | -- Additional settings to fine-tune typesetting 163 | glossarystyle = glossarystyle or "gglo.ist" 164 | indexstyle = indexstyle or "gind.ist" 165 | specialtypesetting = specialtypesetting or { } 166 | 167 | -- Supporting binaries and options 168 | biberexe = biberexe or "biber" 169 | biberopts = biberopts or "" 170 | bibtexexe = bibtexexe or "bibtex8" 171 | bibtexopts = bibtexopts or "-W" 172 | makeindexexe = makeindexexe or "makeindex" 173 | makeindexopts = makeindexopts or "" 174 | 175 | -- Forcing epoch 176 | if forcecheckepoch == nil then 177 | forcecheckepoch = true 178 | end 179 | if forcedocepoch == nil then 180 | forcedocepoch = false 181 | end 182 | 183 | -- Other required settings 184 | asciiengines = asciiengines or {"pdftex"} 185 | checkruns = checkruns or 1 186 | if forcecheckruns == nil then 187 | forcecheckruns = false 188 | end 189 | ctanreadme = ctanreadme or "README.md" 190 | ctanzip = ctanzip or ctanpkg .. "-ctan" 191 | epoch = epoch or 1463734800 192 | if flatten == nil then 193 | flatten = true 194 | end 195 | if flattentds == nil then 196 | flattentds = true 197 | end 198 | maxprintline = maxprintline or 9999 199 | errorline = errorline or 79 200 | halferrorline = halferrorline or 50 201 | if packtdszip == nil then 202 | packtdszip = false 203 | end 204 | -- support "ps2pdfopt" for backward compatibility, gh issue #275 205 | ps2pdfopts = ps2pdfopts or ps2pdfopt or "" 206 | typesetcmds = typesetcmds or "" 207 | typesetruns = typesetruns or 3 208 | if recordstatus == nil then 209 | recordstatus = false 210 | end 211 | 212 | -- Extensions for various file types: used to abstract out stuff a bit 213 | bakext = bakext or ".bak" 214 | dviext = dviext or ".dvi" 215 | logext = logext or ".log" 216 | lveext = lveext or ".lve" 217 | lvtext = lvtext or ".lvt" 218 | pdfext = pdfext or ".pdf" 219 | psext = psext or ".ps" 220 | pvtext = pvtext or ".pvt" 221 | tlgext = tlgext or ".tlg" 222 | tpfext = tpfext or ".tpf" 223 | 224 | test_types = test_types or { } 225 | test_types.log = test_types.log or { 226 | test = lvtext, 227 | generated = logext, 228 | reference = tlgext, 229 | expectation = lveext, 230 | compare = compare_tlg, 231 | rewrite = rewrite_log, 232 | } 233 | test_types.pdf = test_types.pdf or { 234 | test = pvtext, 235 | generated = pdfext, 236 | reference = tpfext, 237 | rewrite = rewrite_pdf, 238 | } 239 | 240 | test_order = test_order or {"log", "pdf"} 241 | 242 | -- Manifest options 243 | manifestfile = manifestfile or "MANIFEST.md" 244 | 245 | -- Non-standard installation locations 246 | tdslocations = tdslocations or { } 247 | tdsdirs = tdsdirs or {} 248 | 249 | -- Upload settings 250 | curlexe = curlexe or "curl" 251 | uploadconfig = uploadconfig or {} 252 | ctanupload = ctanupload or "ask" 253 | -------------------------------------------------------------------------------- /l3build-zip.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | File l3build-zip.lua Copyright (C) 2021-2025 The LaTeX Project 4 | 5 | It may be distributed and/or modified under the conditions of the 6 | LaTeX Project Public License (LPPL), either version 1.3c of this 7 | license or (at your option) any later version. The latest version 8 | of this license is in the file 9 | 10 | https://www.latex-project.org/lppl.txt 11 | 12 | This file is part of the "l3build bundle" (The Work in LPPL) 13 | and all files in that bundle must be distributed together. 14 | 15 | ----------------------------------------------------------------------- 16 | 17 | The development version of the bundle can be found at 18 | 19 | https://github.com/latex3/l3build 20 | 21 | for those people who are interested. 22 | 23 | --]] 24 | 25 | local concat = table.concat 26 | local open = io.open 27 | local osdate = os.date 28 | local pack = string.pack 29 | local setmetatable = setmetatable 30 | local iotype = io.type 31 | 32 | local compress = zlib.compress 33 | local crc32 = zlib.crc32 34 | 35 | local function encode_time(unix) 36 | local t = osdate('*t', unix) 37 | local date = t.day | (t.month << 5) | ((t.year-1980) << 9) 38 | local time = (t.sec//2) | (t.min << 5) | (t.hour << 11) 39 | return date, time 40 | end 41 | 42 | local function extra_timestamp(mod, access, creation) 43 | local flags = 0 44 | local local_extra, central_extra = '', '' 45 | if mod then 46 | flags = flags | 0x1 47 | local_extra = pack('= #content then 76 | compressed = nil 77 | end 78 | local timestamp = os.time() 79 | local date, time = encode_time(timestamp) 80 | local central_extra, local_extra = extra_timestamp(timestamp, nil, nil) 81 | z.f:write(pack(" 1 then 156 | if options["target"] == "check" or options["target"] == "bundlecheck" then 157 | local errorlevel = 0 158 | local failed = { } 159 | for i = 1, #checkconfigs do 160 | options["config"] = {checkconfigs[i]} 161 | errorlevel = call({"."}, "check", options) 162 | if errorlevel ~= 0 then 163 | if options["halt-on-error"] then 164 | exit(1) 165 | else 166 | insert(failed,checkconfigs[i]) 167 | end 168 | end 169 | end 170 | if next(failed) then 171 | for _,config in ipairs(failed) do 172 | checkdiff(config) 173 | end 174 | if options["show-saves"] then 175 | local savecmds, recheckcmds = "", "" 176 | for _,config in ipairs(failed) do 177 | local testdir = testdir 178 | if config ~= "build" then 179 | testdir = testdir .. "-" .. config 180 | end 181 | local f = open(testdir .. "/.savecommands") 182 | if not f then 183 | print("Error: Cannot find save commands for configuration \"" .. 184 | config .. "\"") 185 | exit(2) 186 | end 187 | for line in f:lines() do 188 | if line == "" then break end 189 | savecmds = savecmds .. " " .. line .. "\n" 190 | end 191 | for line in f:lines() do 192 | recheckcmds = recheckcmds .. " " .. line .. "\n" 193 | end 194 | f:close() 195 | end 196 | print"To regenerate the test files, run\n" 197 | print(savecmds) 198 | if recheckcmds ~= "" and #checkengines ~= 1 then 199 | print"To detect engine-specific differences, run after that\n" 200 | print(recheckcmds) 201 | end 202 | end 203 | exit(1) 204 | else 205 | -- Avoid running the 'main' set of tests twice 206 | exit(0) 207 | end 208 | elseif options["target"] == "clean" then 209 | local failure 210 | for i = 1, #checkconfigs do 211 | options["config"] = {checkconfigs[i]} 212 | failure = 0 ~= call({"."}, "clean", options) or failure 213 | end 214 | exit(failure and 1 or 0) 215 | end 216 | end 217 | if #checkconfigs == 1 and 218 | (options["target"] == "check" or options["target"] == "save" or options["target"] == "clean") then 219 | if checkconfigs[1] == "build" then 220 | -- Sanity check for default config 221 | check_engines("build.lua") 222 | else 223 | local configname = gsub(checkconfigs[1], "%.lua$", "") 224 | local config = "./" .. configname .. ".lua" 225 | if fileexists(config) then 226 | local savedtestfiledir = testfiledir 227 | dofile(config) 228 | -- Sanity check for non-default config 229 | check_engines(configname .. ".lua") 230 | testdir = testdir .. "-" .. configname 231 | -- Reset testsuppdir if required 232 | if savedtestfiledir ~= testfiledir and 233 | testsuppdir == savedtestfiledir .. "/support" then 234 | testsuppdir = testfiledir .. "/support" 235 | end 236 | else 237 | print("Error: Cannot find configuration \"" .. configname .. ".lua\"") 238 | exit(1) 239 | end 240 | end 241 | end 242 | 243 | -- Call the main function 244 | main(options["target"], options["names"]) 245 | -------------------------------------------------------------------------------- /testfiles-context/context.lvt: -------------------------------------------------------------------------------- 1 | \input{regression-test} 2 | \starttext 3 | \START 4 | Hello! 5 | \TYPE{Hello} 6 | \OMIT 7 | \stoptext 8 | -------------------------------------------------------------------------------- /testfiles-context/context.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | Hello 4 | -------------------------------------------------------------------------------- /testfiles-pdf/00-test-2.pvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \begin{document} 6 | 7 | \begin{verbatim} 8 | #$%& 9 | \end{verbatim} 10 | 11 | \begin{verbatim} 12 | #$%& 13 | \end{verbatim} 14 | 15 | \end{document} 16 | -------------------------------------------------------------------------------- /testfiles-pdf/00-test-2.tpf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/latex3/l3build/bdd3b0001fc46fffc99d39a56d267597ea1695c0/testfiles-pdf/00-test-2.tpf -------------------------------------------------------------------------------- /testfiles-pdf/00-test-2.xetex.tpf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/latex3/l3build/bdd3b0001fc46fffc99d39a56d267597ea1695c0/testfiles-pdf/00-test-2.xetex.tpf -------------------------------------------------------------------------------- /testfiles-plain/plain-pdftex.lvt: -------------------------------------------------------------------------------- 1 | %&pdftex 2 | 3 | \input regression-test.tex\relax 4 | 5 | \newtoks\foo % \outer 6 | 7 | \START 8 | 9 | \ISCFGLOADED 10 | 11 | \TEST{\afterassignment}{ 12 | \def\x{\afterassignment{\edef\y{world}}\foo} 13 | \x={hello} 14 | \showthe\foo 15 | \show\y 16 | } 17 | 18 | \END 19 | -------------------------------------------------------------------------------- /testfiles-plain/plain-pdftex.ptex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | CFG FILE IS LOADED 5 | ============================================================ 6 | ============================================================ 7 | TEST 1: \afterassignment 8 | ============================================================ 9 | > hello. 10 | ...d}}\foo } \x ={hello} \showthe \foo 11 | \show \y 12 | l. ...} 13 | > \y=macro: 14 | ->world. 15 | ... \x ={hello} \showthe \foo \show \y 16 | l. ...} 17 | ============================================================ 18 | -------------------------------------------------------------------------------- /testfiles-plain/plain-pdftex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | CFG FILE IS LOADED 5 | ============================================================ 6 | ============================================================ 7 | TEST 1: \afterassignment 8 | ============================================================ 9 | > hello. 10 | ...d}}\foo } \x ={hello} \showthe \foo 11 | \show \y 12 | l. ...} 13 | > \y=macro: 14 | ->world. 15 | ... \x ={hello} \showthe \foo \show \y 16 | l. ...} 17 | ============================================================ 18 | -------------------------------------------------------------------------------- /testfiles-plain/plain-pdftex.uptex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | CFG FILE IS LOADED 5 | ============================================================ 6 | ============================================================ 7 | TEST 1: \afterassignment 8 | ============================================================ 9 | > hello. 10 | ...d}}\foo } \x ={hello} \showthe \foo 11 | \show \y 12 | l. ...} 13 | > \y=macro: 14 | ->world. 15 | ... \x ={hello} \showthe \foo \show \y 16 | l. ...} 17 | ============================================================ 18 | -------------------------------------------------------------------------------- /testfiles-plain/plain-pdftex.xetex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | CFG FILE IS LOADED 5 | ============================================================ 6 | ============================================================ 7 | TEST 1: \afterassignment 8 | ============================================================ 9 | > hello. 10 | ...d}}\foo } \x ={hello} \showthe \foo 11 | \show \y 12 | l. ...} 13 | > \y=macro: 14 | ->world. 15 | ... \x ={hello} \showthe \foo \show \y 16 | l. ...} 17 | ============================================================ 18 | -------------------------------------------------------------------------------- /testfiles-plain/support/regression-test.cfg: -------------------------------------------------------------------------------- 1 | 2 | % just a test 3 | 4 | \def\ISCFGLOADED{\SEPARATOR\TYPE{CFG FILE IS LOADED}\SEPARATOR} 5 | -------------------------------------------------------------------------------- /testfiles/00-test-1.luatex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | TEST 1: \afterassignment 5 | ============================================================ 6 | > hello. 7 | ...d}}\foo } \x ={hello} \showthe \foo 8 | \show \y 9 | l. ...} 10 | > \y=macro: 11 | ->world. 12 | ... \x ={hello} \showthe \foo \show \y 13 | l. ...} 14 | ============================================================ 15 | ============================================================ 16 | TEST 2: Environment test 17 | ============================================================ 18 | ============================================================ 19 | ============================================================ 20 | TEST 3: Assertions 21 | ============================================================ 22 | PASSED 23 | FAILED 24 | PASSED 25 | ============================================================ 26 | Completed box being shipped out [1] 27 | \vbox(578.15999+0.0)x469.75499, direction TLT 28 | .\glue 0.0 29 | .\vbox(578.15999+0.0)x469.75499, direction TLT 30 | ..\vbox(0.0+0.0)x469.75499, direction TLT 31 | ...\glue 0.0 plus 1.0fil 32 | ...\hbox(0.0+0.0)x469.75499, direction TLT 33 | ....\hbox(0.0+0.0)x469.75499, direction TLT 34 | ..\glue 0.0 35 | ..\glue(\lineskip) 0.0 36 | ..\vbox(578.15999+0.0)x469.75499, glue set 532.15997fil, direction TLT 37 | ...\glue(\topskip) 3.06 38 | ...\hbox(6.94+0.83)x469.75499, glue set 427.755fil, direction TLT 39 | ....\localpar 40 | .....\localinterlinepenalty=0 41 | .....\localbrokenpenalty=0 42 | .....\localleftbox=null 43 | .....\localrightbox=null 44 | ....\hbox(0.0+0.0)x0.0, direction TLT 45 | .....\glue 0.0 46 | .....\glue 0.0 47 | .....\glue 0.0 48 | .....\hbox(0.0+0.0)x0.0, direction TLT 49 | .....\glue 0.0 50 | ....\penalty 10000 51 | ....\glue(\spaceskip) 5.25 52 | ....\penalty 10000 53 | ....\glue(\spaceskip) 5.25 54 | ....\penalty 10000 55 | ....\glue(\spaceskip) 5.25 56 | ....\penalty 10000 57 | ....\glue(\spaceskip) 5.25 58 | ....\TU/lmtt/m/n/10 # 59 | ....\TU/lmtt/m/n/10 $ 60 | ....\TU/lmtt/m/n/10 % 61 | ....\TU/lmtt/m/n/10 & 62 | ....\hbox(0.0+0.0)x0.0, direction TLT 63 | ....\penalty 10000 64 | ....\glue(\parfillskip) 0.0 plus 1.0fil 65 | ....\glue(\rightskip) 0.0 66 | ...\penalty 0 67 | ...\glue(\parskip) 0.0 68 | ...\glue(\parskip) 0.0 69 | ...\glue(\baselineskip) 11.17 70 | ...\hbox(0.0+0.0)x469.75499, glue set 464.505fil, direction TLT 71 | ....\localpar 72 | .....\localinterlinepenalty=0 73 | .....\localbrokenpenalty=0 74 | .....\localleftbox=null 75 | .....\localrightbox=null 76 | ....\hbox(0.0+0.0)x0.0, direction TLT 77 | ....\penalty 10000 78 | ....\glue(\spaceskip) 5.25 79 | ....\penalty 10000 80 | ....\hbox(0.0+0.0)x0.0, direction TLT 81 | ....\penalty 10000 82 | ....\glue(\parfillskip) 0.0 plus 1.0fil 83 | ....\glue(\rightskip) 0.0 84 | ...\penalty 0 85 | ...\penalty 0 86 | ...\glue 0.0 87 | ...\penalty 0 88 | ...\glue 0.0 plus 1.0 89 | ...\glue 0.0 plus -1.0 90 | ...\glue 0.0 plus 1.0 91 | ...\glue(\parskip) 0.0 92 | ...\glue(\parskip) 0.0 93 | ...\glue(\baselineskip) 5.06 94 | ...\hbox(6.94+0.83)x469.75499, glue set 427.755fil, direction TLT 95 | ....\localpar 96 | .....\localinterlinepenalty=0 97 | .....\localbrokenpenalty=0 98 | .....\localleftbox=null 99 | .....\localrightbox=null 100 | ....\hbox(0.0+0.0)x0.0, direction TLT 101 | .....\glue 0.0 102 | .....\glue 0.0 103 | .....\glue 0.0 104 | .....\hbox(0.0+0.0)x0.0, direction TLT 105 | .....\glue 0.0 106 | ....\penalty 10000 107 | ....\glue(\spaceskip) 5.25 108 | ....\penalty 10000 109 | ....\glue(\spaceskip) 5.25 110 | ....\penalty 10000 111 | ....\glue(\spaceskip) 5.25 112 | ....\penalty 10000 113 | ....\glue(\spaceskip) 5.25 114 | ....\TU/lmtt/m/n/10 # 115 | ....\TU/lmtt/m/n/10 $ 116 | ....\TU/lmtt/m/n/10 % 117 | ....\TU/lmtt/m/n/10 & 118 | ....\hbox(0.0+0.0)x0.0, direction TLT 119 | ....\penalty 10000 120 | ....\glue(\parfillskip) 0.0 plus 1.0fil 121 | ....\glue(\rightskip) 0.0 122 | ...\penalty 0 123 | ...\glue(\parskip) 0.0 124 | ...\glue(\parskip) 0.0 125 | ...\glue(\baselineskip) 11.17 126 | ...\hbox(0.0+0.0)x469.75499, glue set 464.505fil, direction TLT 127 | ....\localpar 128 | .....\localinterlinepenalty=0 129 | .....\localbrokenpenalty=0 130 | .....\localleftbox=null 131 | .....\localrightbox=null 132 | ....\hbox(0.0+0.0)x0.0, direction TLT 133 | ....\penalty 10000 134 | ....\glue(\spaceskip) 5.25 135 | ....\penalty 10000 136 | ....\hbox(0.0+0.0)x0.0, direction TLT 137 | ....\penalty 10000 138 | ....\glue(\parfillskip) 0.0 plus 1.0fil 139 | ....\glue(\rightskip) 0.0 140 | ...\penalty 0 141 | ...\penalty 0 142 | ...\glue 0.0 143 | ...\glue 0.0 plus 1.0fil 144 | ...\glue 0.0 145 | ..\glue(\baselineskip) 0.0 146 | ..\hbox(0.0+0.0)x469.75499, direction TLT 147 | ...\hbox(0.0+0.0)x469.75499, direction TLT 148 | (00-test-1.aux) 149 | -------------------------------------------------------------------------------- /testfiles/00-test-1.lvt: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | 3 | \documentclass{minimal} 4 | 5 | \begin{document} 6 | 7 | \newtoks\foo % \outer 8 | 9 | \START 10 | 11 | \TEST{\afterassignment}{ 12 | \def\x{\afterassignment{\edef\y{world}}\foo} 13 | \x={hello} 14 | \showthe\foo 15 | \show\y 16 | } 17 | 18 | \newpage 19 | 20 | \showoutput 21 | 22 | \OMIT 23 | % Force font loading 24 | \begin{verbatim} 25 | #$%& 26 | \end{verbatim} 27 | \TIMO 28 | 29 | \BEGINTEST{Environment test} 30 | \begin{verbatim} 31 | #$%& 32 | \end{verbatim} 33 | \ENDTEST 34 | 35 | \begingroup 36 | \catcode`Q=4 % 37 | \gdef\ODD{Q}% 38 | \endgroup 39 | 40 | \TEST{Assertions}{% 41 | \ASSERT{A}{A}% 42 | \ASSERT{Q}{\ODD}% 43 | \ASSERTSTR{Q}{\ODD}% 44 | } 45 | 46 | \end{document} 47 | -------------------------------------------------------------------------------- /testfiles/00-test-1.ptex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | TEST 1: \afterassignment 5 | ============================================================ 6 | > hello. 7 | ...d}}\foo } \x ={hello} \showthe \foo 8 | \show \y 9 | l. ...} 10 | > \y=macro: 11 | ->world. 12 | ... \x ={hello} \showthe \foo \show \y 13 | l. ...} 14 | ============================================================ 15 | ============================================================ 16 | TEST 2: Environment test 17 | ============================================================ 18 | ============================================================ 19 | ============================================================ 20 | TEST 3: Assertions 21 | ============================================================ 22 | PASSED 23 | FAILED 24 | PASSED 25 | ============================================================ 26 | Completed box being shipped out [1] 27 | \vbox(578.15999+0.0)x469.75499 28 | .\hbox(0.0+0.0)x0.0 29 | .\glue 0.0 30 | .\vbox(578.15999+0.0)x469.75499 31 | ..\vbox(0.0+0.0)x469.75499 32 | ...\glue 0.0 plus 1.0fil 33 | ...\hbox(0.0+0.0)x469.75499 34 | ....\hbox(0.0+0.0)x469.75499 35 | ..\glue 0.0 36 | ..\glue(\lineskip) 0.0 37 | ..\vbox(578.15999+0.0)x469.75499, glue set 532.15999fil 38 | ...\glue(\topskip) 3.05556 39 | ...\hbox(6.94444+0.83333)x469.75499, glue set 427.75536fil 40 | ....\hbox(0.0+0.0)x0.0 41 | .....\glue 0.0 42 | .....\glue 0.0 43 | .....\glue 0.0 44 | .....\hbox(0.0+0.0)x0.0 45 | .....\glue 0.0 46 | ....\penalty 10000 47 | ....\glue 5.24995 48 | ....\penalty 10000 49 | ....\glue 5.24995 50 | ....\penalty 10000 51 | ....\glue 5.24995 52 | ....\penalty 10000 53 | ....\glue 5.24995 54 | ....\OT1/cmtt/m/n/10 # 55 | ....\OT1/cmtt/m/n/10 $ 56 | ....\OT1/cmtt/m/n/10 % 57 | ....\OT1/cmtt/m/n/10 & 58 | ....\hbox(0.0+0.0)x0.0 59 | ....\penalty 10000 60 | ....\glue(\parfillskip) 0.0 plus 1.0fil 61 | ....\glue(\rightskip) 0.0 62 | ...\penalty 0 63 | ...\glue(\parskip) 0.0 64 | ...\glue(\parskip) 0.0 65 | ...\glue(\baselineskip) 11.16667 66 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50504fil 67 | ....\hbox(0.0+0.0)x0.0 68 | ....\penalty 10000 69 | ....\glue 5.24995 70 | ....\penalty 10000 71 | ....\hbox(0.0+0.0)x0.0 72 | ....\penalty 10000 73 | ....\glue(\parfillskip) 0.0 plus 1.0fil 74 | ....\glue(\rightskip) 0.0 75 | ...\penalty 0 76 | ...\penalty 0 77 | ...\glue 0.0 78 | ...\penalty 0 79 | ...\glue 0.0 plus 1.0 80 | ...\glue 0.0 plus -1.0 81 | ...\glue 0.0 plus 1.0 82 | ...\glue(\parskip) 0.0 83 | ...\glue(\parskip) 0.0 84 | ...\glue(\baselineskip) 5.05556 85 | ...\hbox(6.94444+0.83333)x469.75499, glue set 427.75536fil 86 | ....\hbox(0.0+0.0)x0.0 87 | .....\glue 0.0 88 | .....\glue 0.0 89 | .....\glue 0.0 90 | .....\hbox(0.0+0.0)x0.0 91 | .....\glue 0.0 92 | ....\penalty 10000 93 | ....\glue 5.24995 94 | ....\penalty 10000 95 | ....\glue 5.24995 96 | ....\penalty 10000 97 | ....\glue 5.24995 98 | ....\penalty 10000 99 | ....\glue 5.24995 100 | ....\OT1/cmtt/m/n/10 # 101 | ....\OT1/cmtt/m/n/10 $ 102 | ....\OT1/cmtt/m/n/10 % 103 | ....\OT1/cmtt/m/n/10 & 104 | ....\hbox(0.0+0.0)x0.0 105 | ....\penalty 10000 106 | ....\glue(\parfillskip) 0.0 plus 1.0fil 107 | ....\glue(\rightskip) 0.0 108 | ...\penalty 0 109 | ...\glue(\parskip) 0.0 110 | ...\glue(\parskip) 0.0 111 | ...\glue(\baselineskip) 11.16667 112 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50504fil 113 | ....\hbox(0.0+0.0)x0.0 114 | ....\penalty 10000 115 | ....\glue 5.24995 116 | ....\penalty 10000 117 | ....\hbox(0.0+0.0)x0.0 118 | ....\penalty 10000 119 | ....\glue(\parfillskip) 0.0 plus 1.0fil 120 | ....\glue(\rightskip) 0.0 121 | ...\penalty 0 122 | ...\penalty 0 123 | ...\glue 0.0 124 | ...\glue 0.0 plus 1.0fil 125 | ...\glue 0.0 126 | ..\glue(\baselineskip) 0.0 127 | ..\hbox(0.0+0.0)x469.75499 128 | ...\hbox(0.0+0.0)x469.75499 129 | .\kern 0.0 130 | (00-test-1.aux) 131 | -------------------------------------------------------------------------------- /testfiles/00-test-1.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | TEST 1: \afterassignment 5 | ============================================================ 6 | > hello. 7 | ...d}}\foo } \x ={hello} \showthe \foo 8 | \show \y 9 | l. ...} 10 | > \y=macro: 11 | ->world. 12 | ... \x ={hello} \showthe \foo \show \y 13 | l. ...} 14 | ============================================================ 15 | ============================================================ 16 | TEST 2: Environment test 17 | ============================================================ 18 | ============================================================ 19 | ============================================================ 20 | TEST 3: Assertions 21 | ============================================================ 22 | PASSED 23 | FAILED 24 | PASSED 25 | ============================================================ 26 | Completed box being shipped out [1] 27 | \vbox(578.15999+0.0)x469.75499 28 | .\glue 0.0 29 | .\vbox(578.15999+0.0)x469.75499 30 | ..\vbox(0.0+0.0)x469.75499 31 | ...\glue 0.0 plus 1.0fil 32 | ...\hbox(0.0+0.0)x469.75499 33 | ....\hbox(0.0+0.0)x469.75499 34 | ..\glue 0.0 35 | ..\glue(\lineskip) 0.0 36 | ..\vbox(578.15999+0.0)x469.75499, glue set 532.15999fil 37 | ...\glue(\topskip) 3.05556 38 | ...\hbox(6.94444+0.83333)x469.75499, glue set 427.75536fil 39 | ....\hbox(0.0+0.0)x0.0 40 | .....\glue 0.0 41 | .....\glue 0.0 42 | .....\glue 0.0 43 | .....\hbox(0.0+0.0)x0.0 44 | .....\glue 0.0 45 | ....\penalty 10000 46 | ....\glue 5.24995 47 | ....\penalty 10000 48 | ....\glue 5.24995 49 | ....\penalty 10000 50 | ....\glue 5.24995 51 | ....\penalty 10000 52 | ....\glue 5.24995 53 | ....\OT1/cmtt/m/n/10 # 54 | ....\OT1/cmtt/m/n/10 $ 55 | ....\OT1/cmtt/m/n/10 % 56 | ....\OT1/cmtt/m/n/10 & 57 | ....\hbox(0.0+0.0)x0.0 58 | ....\penalty 10000 59 | ....\glue(\parfillskip) 0.0 plus 1.0fil 60 | ....\glue(\rightskip) 0.0 61 | ...\penalty 0 62 | ...\glue(\parskip) 0.0 63 | ...\glue(\parskip) 0.0 64 | ...\glue(\baselineskip) 11.16667 65 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50504fil 66 | ....\hbox(0.0+0.0)x0.0 67 | ....\penalty 10000 68 | ....\glue 5.24995 69 | ....\penalty 10000 70 | ....\hbox(0.0+0.0)x0.0 71 | ....\penalty 10000 72 | ....\glue(\parfillskip) 0.0 plus 1.0fil 73 | ....\glue(\rightskip) 0.0 74 | ...\penalty 0 75 | ...\penalty 0 76 | ...\glue 0.0 77 | ...\penalty 0 78 | ...\glue 0.0 plus 1.0 79 | ...\glue 0.0 plus -1.0 80 | ...\glue 0.0 plus 1.0 81 | ...\glue(\parskip) 0.0 82 | ...\glue(\parskip) 0.0 83 | ...\glue(\baselineskip) 5.05556 84 | ...\hbox(6.94444+0.83333)x469.75499, glue set 427.75536fil 85 | ....\hbox(0.0+0.0)x0.0 86 | .....\glue 0.0 87 | .....\glue 0.0 88 | .....\glue 0.0 89 | .....\hbox(0.0+0.0)x0.0 90 | .....\glue 0.0 91 | ....\penalty 10000 92 | ....\glue 5.24995 93 | ....\penalty 10000 94 | ....\glue 5.24995 95 | ....\penalty 10000 96 | ....\glue 5.24995 97 | ....\penalty 10000 98 | ....\glue 5.24995 99 | ....\OT1/cmtt/m/n/10 # 100 | ....\OT1/cmtt/m/n/10 $ 101 | ....\OT1/cmtt/m/n/10 % 102 | ....\OT1/cmtt/m/n/10 & 103 | ....\hbox(0.0+0.0)x0.0 104 | ....\penalty 10000 105 | ....\glue(\parfillskip) 0.0 plus 1.0fil 106 | ....\glue(\rightskip) 0.0 107 | ...\penalty 0 108 | ...\glue(\parskip) 0.0 109 | ...\glue(\parskip) 0.0 110 | ...\glue(\baselineskip) 11.16667 111 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50504fil 112 | ....\hbox(0.0+0.0)x0.0 113 | ....\penalty 10000 114 | ....\glue 5.24995 115 | ....\penalty 10000 116 | ....\hbox(0.0+0.0)x0.0 117 | ....\penalty 10000 118 | ....\glue(\parfillskip) 0.0 plus 1.0fil 119 | ....\glue(\rightskip) 0.0 120 | ...\penalty 0 121 | ...\penalty 0 122 | ...\glue 0.0 123 | ...\glue 0.0 plus 1.0fil 124 | ...\glue 0.0 125 | ..\glue(\baselineskip) 0.0 126 | ..\hbox(0.0+0.0)x469.75499 127 | ...\hbox(0.0+0.0)x469.75499 128 | (00-test-1.aux) 129 | -------------------------------------------------------------------------------- /testfiles/00-test-1.uptex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | TEST 1: \afterassignment 5 | ============================================================ 6 | > hello. 7 | ...d}}\foo } \x ={hello} \showthe \foo 8 | \show \y 9 | l. ...} 10 | > \y=macro: 11 | ->world. 12 | ... \x ={hello} \showthe \foo \show \y 13 | l. ...} 14 | ============================================================ 15 | ============================================================ 16 | TEST 2: Environment test 17 | ============================================================ 18 | ============================================================ 19 | ============================================================ 20 | TEST 3: Assertions 21 | ============================================================ 22 | PASSED 23 | FAILED 24 | PASSED 25 | ============================================================ 26 | Completed box being shipped out [1] 27 | \vbox(578.15999+0.0)x469.75499 28 | .\hbox(0.0+0.0)x0.0 29 | .\glue 0.0 30 | .\vbox(578.15999+0.0)x469.75499 31 | ..\vbox(0.0+0.0)x469.75499 32 | ...\glue 0.0 plus 1.0fil 33 | ...\hbox(0.0+0.0)x469.75499 34 | ....\hbox(0.0+0.0)x469.75499 35 | ..\glue 0.0 36 | ..\glue(\lineskip) 0.0 37 | ..\vbox(578.15999+0.0)x469.75499, glue set 532.15999fil 38 | ...\glue(\topskip) 3.05556 39 | ...\hbox(6.94444+0.83333)x469.75499, glue set 427.75536fil 40 | ....\hbox(0.0+0.0)x0.0 41 | .....\glue 0.0 42 | .....\glue 0.0 43 | .....\glue 0.0 44 | .....\hbox(0.0+0.0)x0.0 45 | .....\glue 0.0 46 | ....\penalty 10000 47 | ....\glue 5.24995 48 | ....\penalty 10000 49 | ....\glue 5.24995 50 | ....\penalty 10000 51 | ....\glue 5.24995 52 | ....\penalty 10000 53 | ....\glue 5.24995 54 | ....\OT1/cmtt/m/n/10 # 55 | ....\OT1/cmtt/m/n/10 $ 56 | ....\OT1/cmtt/m/n/10 % 57 | ....\OT1/cmtt/m/n/10 & 58 | ....\hbox(0.0+0.0)x0.0 59 | ....\penalty 10000 60 | ....\glue(\parfillskip) 0.0 plus 1.0fil 61 | ....\glue(\rightskip) 0.0 62 | ...\penalty 0 63 | ...\glue(\parskip) 0.0 64 | ...\glue(\parskip) 0.0 65 | ...\glue(\baselineskip) 11.16667 66 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50504fil 67 | ....\hbox(0.0+0.0)x0.0 68 | ....\penalty 10000 69 | ....\glue 5.24995 70 | ....\penalty 10000 71 | ....\hbox(0.0+0.0)x0.0 72 | ....\penalty 10000 73 | ....\glue(\parfillskip) 0.0 plus 1.0fil 74 | ....\glue(\rightskip) 0.0 75 | ...\penalty 0 76 | ...\penalty 0 77 | ...\glue 0.0 78 | ...\penalty 0 79 | ...\glue 0.0 plus 1.0 80 | ...\glue 0.0 plus -1.0 81 | ...\glue 0.0 plus 1.0 82 | ...\glue(\parskip) 0.0 83 | ...\glue(\parskip) 0.0 84 | ...\glue(\baselineskip) 5.05556 85 | ...\hbox(6.94444+0.83333)x469.75499, glue set 427.75536fil 86 | ....\hbox(0.0+0.0)x0.0 87 | .....\glue 0.0 88 | .....\glue 0.0 89 | .....\glue 0.0 90 | .....\hbox(0.0+0.0)x0.0 91 | .....\glue 0.0 92 | ....\penalty 10000 93 | ....\glue 5.24995 94 | ....\penalty 10000 95 | ....\glue 5.24995 96 | ....\penalty 10000 97 | ....\glue 5.24995 98 | ....\penalty 10000 99 | ....\glue 5.24995 100 | ....\OT1/cmtt/m/n/10 # 101 | ....\OT1/cmtt/m/n/10 $ 102 | ....\OT1/cmtt/m/n/10 % 103 | ....\OT1/cmtt/m/n/10 & 104 | ....\hbox(0.0+0.0)x0.0 105 | ....\penalty 10000 106 | ....\glue(\parfillskip) 0.0 plus 1.0fil 107 | ....\glue(\rightskip) 0.0 108 | ...\penalty 0 109 | ...\glue(\parskip) 0.0 110 | ...\glue(\parskip) 0.0 111 | ...\glue(\baselineskip) 11.16667 112 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50504fil 113 | ....\hbox(0.0+0.0)x0.0 114 | ....\penalty 10000 115 | ....\glue 5.24995 116 | ....\penalty 10000 117 | ....\hbox(0.0+0.0)x0.0 118 | ....\penalty 10000 119 | ....\glue(\parfillskip) 0.0 plus 1.0fil 120 | ....\glue(\rightskip) 0.0 121 | ...\penalty 0 122 | ...\penalty 0 123 | ...\glue 0.0 124 | ...\glue 0.0 plus 1.0fil 125 | ...\glue 0.0 126 | ..\glue(\baselineskip) 0.0 127 | ..\hbox(0.0+0.0)x469.75499 128 | ...\hbox(0.0+0.0)x469.75499 129 | .\kern 0.0 130 | (00-test-1.aux) 131 | -------------------------------------------------------------------------------- /testfiles/00-test-1.xetex.tlg: -------------------------------------------------------------------------------- 1 | This is a generated file for the l3build validation system. 2 | Don't change this file in any respect. 3 | ============================================================ 4 | TEST 1: \afterassignment 5 | ============================================================ 6 | > hello. 7 | ...d}}\foo } \x ={hello} \showthe \foo 8 | \show \y 9 | l. ...} 10 | > \y=macro: 11 | ->world. 12 | ... \x ={hello} \showthe \foo \show \y 13 | l. ...} 14 | ============================================================ 15 | ============================================================ 16 | TEST 2: Environment test 17 | ============================================================ 18 | ============================================================ 19 | ============================================================ 20 | TEST 3: Assertions 21 | ============================================================ 22 | PASSED 23 | FAILED 24 | PASSED 25 | ============================================================ 26 | Completed box being shipped out [1] 27 | \vbox(578.15999+0.0)x469.75499 28 | .\glue 0.0 29 | .\vbox(578.15999+0.0)x469.75499 30 | ..\vbox(0.0+0.0)x469.75499 31 | ...\glue 0.0 plus 1.0fil 32 | ...\hbox(0.0+0.0)x469.75499 33 | ....\hbox(0.0+0.0)x469.75499 34 | ..\glue 0.0 35 | ..\glue(\lineskip) 0.0 36 | ..\vbox(578.15999+0.0)x469.75499, glue set 532.15999fil 37 | ...\glue(\topskip) 3.06 38 | ...\hbox(6.94+0.82999)x469.75499, glue set 427.75499fil 39 | ....\hbox(0.0+0.0)x0.0 40 | .....\glue 0.0 41 | .....\glue 0.0 42 | .....\glue 0.0 43 | .....\hbox(0.0+0.0)x0.0 44 | .....\glue 0.0 45 | ....\penalty 10000 46 | ....\glue 5.25 47 | ....\penalty 10000 48 | ....\glue 5.25 49 | ....\penalty 10000 50 | ....\glue 5.25 51 | ....\penalty 10000 52 | ....\glue 5.25 53 | ....\TU/lmtt/m/n/10 #$%& 54 | ....\hbox(0.0+0.0)x0.0 55 | ....\penalty 10000 56 | ....\glue(\parfillskip) 0.0 plus 1.0fil 57 | ....\glue(\rightskip) 0.0 58 | ...\penalty 0 59 | ...\glue(\parskip) 0.0 60 | ...\glue(\parskip) 0.0 61 | ...\glue(\baselineskip) 11.17001 62 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50499fil 63 | ....\hbox(0.0+0.0)x0.0 64 | ....\penalty 10000 65 | ....\glue 5.25 66 | ....\penalty 10000 67 | ....\hbox(0.0+0.0)x0.0 68 | ....\penalty 10000 69 | ....\glue(\parfillskip) 0.0 plus 1.0fil 70 | ....\glue(\rightskip) 0.0 71 | ...\penalty 0 72 | ...\penalty 0 73 | ...\glue 0.0 74 | ...\penalty 0 75 | ...\glue 0.0 plus 1.0 76 | ...\glue 0.0 plus -1.0 77 | ...\glue 0.0 plus 1.0 78 | ...\glue(\parskip) 0.0 79 | ...\glue(\parskip) 0.0 80 | ...\glue(\baselineskip) 5.06 81 | ...\hbox(6.94+0.82999)x469.75499, glue set 427.75499fil 82 | ....\hbox(0.0+0.0)x0.0 83 | .....\glue 0.0 84 | .....\glue 0.0 85 | .....\glue 0.0 86 | .....\hbox(0.0+0.0)x0.0 87 | .....\glue 0.0 88 | ....\penalty 10000 89 | ....\glue 5.25 90 | ....\penalty 10000 91 | ....\glue 5.25 92 | ....\penalty 10000 93 | ....\glue 5.25 94 | ....\penalty 10000 95 | ....\glue 5.25 96 | ....\TU/lmtt/m/n/10 #$%& 97 | ....\hbox(0.0+0.0)x0.0 98 | ....\penalty 10000 99 | ....\glue(\parfillskip) 0.0 plus 1.0fil 100 | ....\glue(\rightskip) 0.0 101 | ...\penalty 0 102 | ...\glue(\parskip) 0.0 103 | ...\glue(\parskip) 0.0 104 | ...\glue(\baselineskip) 11.17001 105 | ...\hbox(0.0+0.0)x469.75499, glue set 464.50499fil 106 | ....\hbox(0.0+0.0)x0.0 107 | ....\penalty 10000 108 | ....\glue 5.25 109 | ....\penalty 10000 110 | ....\hbox(0.0+0.0)x0.0 111 | ....\penalty 10000 112 | ....\glue(\parfillskip) 0.0 plus 1.0fil 113 | ....\glue(\rightskip) 0.0 114 | ...\penalty 0 115 | ...\penalty 0 116 | ...\glue 0.0 117 | ...\glue 0.0 plus 1.0fil 118 | ...\glue 0.0 119 | ..\glue(\baselineskip) 0.0 120 | ..\hbox(0.0+0.0)x469.75499 121 | ...\hbox(0.0+0.0)x469.75499 122 | (00-test-1.aux) 123 | -------------------------------------------------------------------------------- /testfiles/01-expect.dtx: -------------------------------------------------------------------------------- 1 | \input regression-test.tex\relax 2 | \START 3 | \TEST{counter-math}{ 4 | %<*test> 5 | \OMIT 6 | \newcounter{numbers} 7 | \setcounter{numbers}{2} 8 | \addtocounter{numbers}{2} 9 | \stepcounter{numbers} 10 | \TIMO 11 | \typeout{\arabic{numbers}} 12 | % 13 | % \typeout{5} 14 | } 15 | \END 16 | -------------------------------------------------------------------------------- /testfiles/01-expect.ins: -------------------------------------------------------------------------------- 1 | \input docstrip.tex 2 | \generate{ 3 | \file{\jobname-1.lvt}{\from{\jobname.dtx}{test}} 4 | \file{\jobname-1.lve}{\from{\jobname.dtx}{expect}} 5 | } 6 | \endbatchfile 7 | -------------------------------------------------------------------------------- /testfiles/support/regression-test.cfg: -------------------------------------------------------------------------------- 1 | %% File regression-test.cfg (C) Copyright 2014-2023 The LaTeX Project 2 | 3 | \ifx\RequirePackage\@undefined\else 4 | \OMIT 5 | \RequirePackage{etex} 6 | \TIMO 7 | \fi 8 | \newcount\regression@test@loop@int 9 | \long\def\regression@test@alloc#1#2{% 10 | \regression@test@loop@int=\numexpr#1\relax 11 | \regression@test@loop#2% 12 | } 13 | \long\def\regression@test@loop#1{% 14 | \ifnum 0<\regression@test@loop@int 15 | #1\regression@test@dummy 16 | \advance\regression@test@loop@int by -1\relax 17 | \expandafter\regression@test@loop 18 | \expandafter#1% 19 | \fi 20 | } 21 | \ifx\RequirePackage\@undefined 22 | \expandafter\def\expandafter\newcount\expandafter{\newcount} 23 | \expandafter\def\expandafter\newbox\expandafter{\newbox} 24 | \expandafter\def\expandafter\newdimen\expandafter{\newdimen} 25 | \expandafter\def\expandafter\newmuskip\expandafter{\newmuskip} 26 | \expandafter\def\expandafter\newskip\expandafter{\newskip} 27 | \fi 28 | \regression@test@alloc {30} \newcount 29 | \regression@test@alloc {30} \newbox 30 | \regression@test@alloc {30} \newdimen 31 | \regression@test@alloc {30} \newmuskip 32 | \regression@test@alloc {30} \newskip 33 | 34 | \def\ISCFGLOADED{\SEPARATOR\TYPE{CFG FILE IS LOADED}\SEPARATOR} 35 | --------------------------------------------------------------------------------