├── .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 |
--------------------------------------------------------------------------------