third_party:waf: update to upstream 2.0.4 release
[samba.git] / third_party / waf / waflib / Tools / tex.py
1 #! /usr/bin/env python
2 # encoding: utf-8
3 # WARNING! Do not edit! https://waf.io/book/index.html#_obtaining_the_waf_file
4
5 #!/usr/bin/env python
6 # encoding: utf-8
7 # Thomas Nagy, 2006-2018 (ita)
8
9 """
10 TeX/LaTeX/PDFLaTeX/XeLaTeX support
11
12 Example::
13
14         def configure(conf):
15                 conf.load('tex')
16                 if not conf.env.LATEX:
17                         conf.fatal('The program LaTex is required')
18
19         def build(bld):
20                 bld(
21                         features = 'tex',
22                         type     = 'latex', # pdflatex or xelatex
23                         source   = 'document.ltx', # mandatory, the source
24                         outs     = 'ps', # 'pdf' or 'ps pdf'
25                         deps     = 'crossreferencing.lst', # to give dependencies directly
26                         prompt   = 1, # 0 for the batch mode
27                 )
28
29 Notes:
30
31 - To configure with a special program, use::
32
33      $ PDFLATEX=luatex waf configure
34
35 - This tool does not use the target attribute of the task generator
36   (``bld(target=...)``); the target file name is built from the source
37   base name and the output type(s)
38 """
39
40 import os, re
41 from waflib import Utils, Task, Errors, Logs, Node
42 from waflib.TaskGen import feature, before_method
43
44 re_bibunit = re.compile(r'\\(?P<type>putbib)\[(?P<file>[^\[\]]*)\]',re.M)
45 def bibunitscan(self):
46         """
47         Parses TeX inputs and try to find the *bibunit* file dependencies
48
49         :return: list of bibunit files
50         :rtype: list of :py:class:`waflib.Node.Node`
51         """
52         node = self.inputs[0]
53
54         nodes = []
55         if not node:
56                 return nodes
57
58         code = node.read()
59         for match in re_bibunit.finditer(code):
60                 path = match.group('file')
61                 if path:
62                         found = None
63                         for k in ('', '.bib'):
64                                 # add another loop for the tex include paths?
65                                 Logs.debug('tex: trying %s%s', path, k)
66                                 fi = node.parent.find_resource(path + k)
67                                 if fi:
68                                         found = True
69                                         nodes.append(fi)
70                                         # no break
71                         if not found:
72                                 Logs.debug('tex: could not find %s', path)
73
74         Logs.debug('tex: found the following bibunit files: %s', nodes)
75         return nodes
76
77 exts_deps_tex = ['', '.ltx', '.tex', '.bib', '.pdf', '.png', '.eps', '.ps', '.sty']
78 """List of typical file extensions included in latex files"""
79
80 exts_tex = ['.ltx', '.tex']
81 """List of typical file extensions that contain latex"""
82
83 re_tex = re.compile(r'\\(?P<type>usepackage|RequirePackage|include|bibliography([^\[\]{}]*)|putbib|includegraphics|input|import|bringin|lstinputlisting)(\[[^\[\]]*\])?{(?P<file>[^{}]*)}',re.M)
84 """Regexp for expressions that may include latex files"""
85
86 g_bibtex_re = re.compile('bibdata', re.M)
87 """Regexp for bibtex files"""
88
89 g_glossaries_re = re.compile('\\@newglossary', re.M)
90 """Regexp for expressions that create glossaries"""
91
92 class tex(Task.Task):
93         """
94         Compiles a tex/latex file.
95
96         .. inheritance-diagram:: waflib.Tools.tex.latex waflib.Tools.tex.xelatex waflib.Tools.tex.pdflatex
97         """
98
99         bibtex_fun, _ = Task.compile_fun('${BIBTEX} ${BIBTEXFLAGS} ${SRCFILE}', shell=False)
100         bibtex_fun.__doc__ = """
101         Execute the program **bibtex**
102         """
103
104         makeindex_fun, _ = Task.compile_fun('${MAKEINDEX} ${MAKEINDEXFLAGS} ${SRCFILE}', shell=False)
105         makeindex_fun.__doc__ = """
106         Execute the program **makeindex**
107         """
108
109         makeglossaries_fun, _ = Task.compile_fun('${MAKEGLOSSARIES} ${SRCFILE}', shell=False)
110         makeglossaries_fun.__doc__ = """
111         Execute the program **makeglossaries**
112         """
113
114         def exec_command(self, cmd, **kw):
115                 """
116                 Executes TeX commands without buffering (latex may prompt for inputs)
117
118                 :return: the return code
119                 :rtype: int
120                 """
121                 if self.env.PROMPT_LATEX:
122                         # capture the outputs in configuration tests
123                         kw['stdout'] = kw['stderr'] = None
124                 return super(tex, self).exec_command(cmd, **kw)
125
126         def scan_aux(self, node):
127                 """
128                 Recursive regex-based scanner that finds included auxiliary files.
129                 """
130                 nodes = [node]
131                 re_aux = re.compile(r'\\@input{(?P<file>[^{}]*)}', re.M)
132
133                 def parse_node(node):
134                         code = node.read()
135                         for match in re_aux.finditer(code):
136                                 path = match.group('file')
137                                 found = node.parent.find_or_declare(path)
138                                 if found and found not in nodes:
139                                         Logs.debug('tex: found aux node %r', found)
140                                         nodes.append(found)
141                                         parse_node(found)
142                 parse_node(node)
143                 return nodes
144
145         def scan(self):
146                 """
147                 Recursive regex-based scanner that finds latex dependencies. It uses :py:attr:`waflib.Tools.tex.re_tex`
148
149                 Depending on your needs you might want:
150
151                 * to change re_tex::
152
153                         from waflib.Tools import tex
154                         tex.re_tex = myregex
155
156                 * or to change the method scan from the latex tasks::
157
158                         from waflib.Task import classes
159                         classes['latex'].scan = myscanfunction
160                 """
161                 node = self.inputs[0]
162
163                 nodes = []
164                 names = []
165                 seen = []
166                 if not node:
167                         return (nodes, names)
168
169                 def parse_node(node):
170                         if node in seen:
171                                 return
172                         seen.append(node)
173                         code = node.read()
174                         for match in re_tex.finditer(code):
175
176                                 multibib = match.group('type')
177                                 if multibib and multibib.startswith('bibliography'):
178                                         multibib = multibib[len('bibliography'):]
179                                         if multibib.startswith('style'):
180                                                 continue
181                                 else:
182                                         multibib = None
183
184                                 for path in match.group('file').split(','):
185                                         if path:
186                                                 add_name = True
187                                                 found = None
188                                                 for k in exts_deps_tex:
189
190                                                         # issue 1067, scan in all texinputs folders
191                                                         for up in self.texinputs_nodes:
192                                                                 Logs.debug('tex: trying %s%s', path, k)
193                                                                 found = up.find_resource(path + k)
194                                                                 if found:
195                                                                         break
196
197
198                                                         for tsk in self.generator.tasks:
199                                                                 if not found or found in tsk.outputs:
200                                                                         break
201                                                         else:
202                                                                 nodes.append(found)
203                                                                 add_name = False
204                                                                 for ext in exts_tex:
205                                                                         if found.name.endswith(ext):
206                                                                                 parse_node(found)
207                                                                                 break
208
209                                                         # multibib stuff
210                                                         if found and multibib and found.name.endswith('.bib'):
211                                                                 try:
212                                                                         self.multibibs.append(found)
213                                                                 except AttributeError:
214                                                                         self.multibibs = [found]
215
216                                                         # no break, people are crazy
217                                                 if add_name:
218                                                         names.append(path)
219                 parse_node(node)
220
221                 for x in nodes:
222                         x.parent.get_bld().mkdir()
223
224                 Logs.debug("tex: found the following : %s and names %s", nodes, names)
225                 return (nodes, names)
226
227         def check_status(self, msg, retcode):
228                 """
229                 Checks an exit status and raise an error with a particular message
230
231                 :param msg: message to display if the code is non-zero
232                 :type msg: string
233                 :param retcode: condition
234                 :type retcode: boolean
235                 """
236                 if retcode != 0:
237                         raise Errors.WafError('%r command exit status %r' % (msg, retcode))
238
239         def info(self, *k, **kw):
240                 try:
241                         info = self.generator.bld.conf.logger.info
242                 except AttributeError:
243                         info = Logs.info
244                 info(*k, **kw)
245
246         def bibfile(self):
247                 """
248                 Parses *.aux* files to find bibfiles to process.
249                 If present, execute :py:meth:`waflib.Tools.tex.tex.bibtex_fun`
250                 """
251                 for aux_node in self.aux_nodes:
252                         try:
253                                 ct = aux_node.read()
254                         except EnvironmentError:
255                                 Logs.error('Error reading %s: %r', aux_node.abspath())
256                                 continue
257
258                         if g_bibtex_re.findall(ct):
259                                 self.info('calling bibtex')
260
261                                 self.env.env = {}
262                                 self.env.env.update(os.environ)
263                                 self.env.env.update({'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()})
264                                 self.env.SRCFILE = aux_node.name[:-4]
265                                 self.check_status('error when calling bibtex', self.bibtex_fun())
266
267                 for node in getattr(self, 'multibibs', []):
268                         self.env.env = {}
269                         self.env.env.update(os.environ)
270                         self.env.env.update({'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()})
271                         self.env.SRCFILE = node.name[:-4]
272                         self.check_status('error when calling bibtex', self.bibtex_fun())
273
274         def bibunits(self):
275                 """
276                 Parses *.aux* file to find bibunit files. If there are bibunit files,
277                 runs :py:meth:`waflib.Tools.tex.tex.bibtex_fun`.
278                 """
279                 try:
280                         bibunits = bibunitscan(self)
281                 except OSError:
282                         Logs.error('error bibunitscan')
283                 else:
284                         if bibunits:
285                                 fn  = ['bu' + str(i) for i in range(1, len(bibunits) + 1)]
286                                 if fn:
287                                         self.info('calling bibtex on bibunits')
288
289                                 for f in fn:
290                                         self.env.env = {'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()}
291                                         self.env.SRCFILE = f
292                                         self.check_status('error when calling bibtex', self.bibtex_fun())
293
294         def makeindex(self):
295                 """
296                 Searches the filesystem for *.idx* files to process. If present,
297                 runs :py:meth:`waflib.Tools.tex.tex.makeindex_fun`
298                 """
299                 self.idx_node = self.inputs[0].change_ext('.idx')
300                 try:
301                         idx_path = self.idx_node.abspath()
302                         os.stat(idx_path)
303                 except OSError:
304                         self.info('index file %s absent, not calling makeindex', idx_path)
305                 else:
306                         self.info('calling makeindex')
307
308                         self.env.SRCFILE = self.idx_node.name
309                         self.env.env = {}
310                         self.check_status('error when calling makeindex %s' % idx_path, self.makeindex_fun())
311
312         def bibtopic(self):
313                 """
314                 Lists additional .aux files from the bibtopic package
315                 """
316                 p = self.inputs[0].parent.get_bld()
317                 if os.path.exists(os.path.join(p.abspath(), 'btaux.aux')):
318                         self.aux_nodes += p.ant_glob('*[0-9].aux')
319
320         def makeglossaries(self):
321                 """
322                 Lists additional glossaries from .aux files. If present, runs the makeglossaries program.
323                 """
324                 src_file = self.inputs[0].abspath()
325                 base_file = os.path.basename(src_file)
326                 base, _ = os.path.splitext(base_file)
327                 for aux_node in self.aux_nodes:
328                         try:
329                                 ct = aux_node.read()
330                         except EnvironmentError:
331                                 Logs.error('Error reading %s: %r', aux_node.abspath())
332                                 continue
333
334                         if g_glossaries_re.findall(ct):
335                                 if not self.env.MAKEGLOSSARIES:
336                                         raise Errors.WafError("The program 'makeglossaries' is missing!")
337                                 Logs.warn('calling makeglossaries')
338                                 self.env.SRCFILE = base
339                                 self.check_status('error when calling makeglossaries %s' % base, self.makeglossaries_fun())
340                                 return
341
342         def texinputs(self):
343                 """
344                 Returns the list of texinput nodes as a string suitable for the TEXINPUTS environment variables
345
346                 :rtype: string
347                 """
348                 return os.pathsep.join([k.abspath() for k in self.texinputs_nodes]) + os.pathsep
349
350         def run(self):
351                 """
352                 Runs the whole TeX build process
353
354                 Multiple passes are required depending on the usage of cross-references,
355                 bibliographies, glossaries, indexes and additional contents
356                 The appropriate TeX compiler is called until the *.aux* files stop changing.
357                 """
358                 env = self.env
359
360                 if not env.PROMPT_LATEX:
361                         env.append_value('LATEXFLAGS', '-interaction=batchmode')
362                         env.append_value('PDFLATEXFLAGS', '-interaction=batchmode')
363                         env.append_value('XELATEXFLAGS', '-interaction=batchmode')
364
365                 # important, set the cwd for everybody
366                 self.cwd = self.inputs[0].parent.get_bld()
367
368                 self.info('first pass on %s', self.__class__.__name__)
369
370                 # Hash .aux files before even calling the LaTeX compiler
371                 cur_hash = self.hash_aux_nodes()
372
373                 self.call_latex()
374
375                 # Find the .aux files again since bibtex processing can require it
376                 self.hash_aux_nodes()
377
378                 self.bibtopic()
379                 self.bibfile()
380                 self.bibunits()
381                 self.makeindex()
382                 self.makeglossaries()
383
384                 for i in range(10):
385                         # There is no need to call latex again if the .aux hash value has not changed
386                         prev_hash = cur_hash
387                         cur_hash = self.hash_aux_nodes()
388                         if not cur_hash:
389                                 Logs.error('No aux.h to process')
390                         if cur_hash and cur_hash == prev_hash:
391                                 break
392
393                         # run the command
394                         self.info('calling %s', self.__class__.__name__)
395                         self.call_latex()
396
397         def hash_aux_nodes(self):
398                 """
399                 Returns a hash of the .aux file contents
400
401                 :rtype: string or bytes
402                 """
403                 try:
404                         self.aux_nodes
405                 except AttributeError:
406                         try:
407                                 self.aux_nodes = self.scan_aux(self.inputs[0].change_ext('.aux'))
408                         except IOError:
409                                 return None
410                 return Utils.h_list([Utils.h_file(x.abspath()) for x in self.aux_nodes])
411
412         def call_latex(self):
413                 """
414                 Runs the TeX compiler once
415                 """
416                 self.env.env = {}
417                 self.env.env.update(os.environ)
418                 self.env.env.update({'TEXINPUTS': self.texinputs()})
419                 self.env.SRCFILE = self.inputs[0].abspath()
420                 self.check_status('error when calling latex', self.texfun())
421
422 class latex(tex):
423         "Compiles LaTeX files"
424         texfun, vars = Task.compile_fun('${LATEX} ${LATEXFLAGS} ${SRCFILE}', shell=False)
425
426 class pdflatex(tex):
427         "Compiles PdfLaTeX files"
428         texfun, vars =  Task.compile_fun('${PDFLATEX} ${PDFLATEXFLAGS} ${SRCFILE}', shell=False)
429
430 class xelatex(tex):
431         "XeLaTeX files"
432         texfun, vars = Task.compile_fun('${XELATEX} ${XELATEXFLAGS} ${SRCFILE}', shell=False)
433
434 class dvips(Task.Task):
435         "Converts dvi files to postscript"
436         run_str = '${DVIPS} ${DVIPSFLAGS} ${SRC} -o ${TGT}'
437         color   = 'BLUE'
438         after   = ['latex', 'pdflatex', 'xelatex']
439
440 class dvipdf(Task.Task):
441         "Converts dvi files to pdf"
442         run_str = '${DVIPDF} ${DVIPDFFLAGS} ${SRC} ${TGT}'
443         color   = 'BLUE'
444         after   = ['latex', 'pdflatex', 'xelatex']
445
446 class pdf2ps(Task.Task):
447         "Converts pdf files to postscript"
448         run_str = '${PDF2PS} ${PDF2PSFLAGS} ${SRC} ${TGT}'
449         color   = 'BLUE'
450         after   = ['latex', 'pdflatex', 'xelatex']
451
452 @feature('tex')
453 @before_method('process_source')
454 def apply_tex(self):
455         """
456         Creates :py:class:`waflib.Tools.tex.tex` objects, and
457         dvips/dvipdf/pdf2ps tasks if necessary (outs='ps', etc).
458         """
459         if not getattr(self, 'type', None) in ('latex', 'pdflatex', 'xelatex'):
460                 self.type = 'pdflatex'
461
462         outs = Utils.to_list(getattr(self, 'outs', []))
463
464         # prompt for incomplete files (else the batchmode is used)
465         try:
466                 self.generator.bld.conf
467         except AttributeError:
468                 default_prompt = False
469         else:
470                 default_prompt = True
471         self.env.PROMPT_LATEX = getattr(self, 'prompt', default_prompt)
472
473         deps_lst = []
474
475         if getattr(self, 'deps', None):
476                 deps = self.to_list(self.deps)
477                 for dep in deps:
478                         if isinstance(dep, str):
479                                 n = self.path.find_resource(dep)
480                                 if not n:
481                                         self.bld.fatal('Could not find %r for %r' % (dep, self))
482                                 if not n in deps_lst:
483                                         deps_lst.append(n)
484                         elif isinstance(dep, Node.Node):
485                                 deps_lst.append(dep)
486
487         for node in self.to_nodes(self.source):
488                 if self.type == 'latex':
489                         task = self.create_task('latex', node, node.change_ext('.dvi'))
490                 elif self.type == 'pdflatex':
491                         task = self.create_task('pdflatex', node, node.change_ext('.pdf'))
492                 elif self.type == 'xelatex':
493                         task = self.create_task('xelatex', node, node.change_ext('.pdf'))
494
495                 task.env = self.env
496
497                 # add the manual dependencies
498                 if deps_lst:
499                         for n in deps_lst:
500                                 if not n in task.dep_nodes:
501                                         task.dep_nodes.append(n)
502
503                 # texinputs is a nasty beast
504                 if hasattr(self, 'texinputs_nodes'):
505                         task.texinputs_nodes = self.texinputs_nodes
506                 else:
507                         task.texinputs_nodes = [node.parent, node.parent.get_bld(), self.path, self.path.get_bld()]
508                         lst = os.environ.get('TEXINPUTS', '')
509                         if self.env.TEXINPUTS:
510                                 lst += os.pathsep + self.env.TEXINPUTS
511                         if lst:
512                                 lst = lst.split(os.pathsep)
513                         for x in lst:
514                                 if x:
515                                         if os.path.isabs(x):
516                                                 p = self.bld.root.find_node(x)
517                                                 if p:
518                                                         task.texinputs_nodes.append(p)
519                                                 else:
520                                                         Logs.error('Invalid TEXINPUTS folder %s', x)
521                                         else:
522                                                 Logs.error('Cannot resolve relative paths in TEXINPUTS %s', x)
523
524                 if self.type == 'latex':
525                         if 'ps' in outs:
526                                 tsk = self.create_task('dvips', task.outputs, node.change_ext('.ps'))
527                                 tsk.env.env = dict(os.environ)
528                         if 'pdf' in outs:
529                                 tsk = self.create_task('dvipdf', task.outputs, node.change_ext('.pdf'))
530                                 tsk.env.env = dict(os.environ)
531                 elif self.type == 'pdflatex':
532                         if 'ps' in outs:
533                                 self.create_task('pdf2ps', task.outputs, node.change_ext('.ps'))
534         self.source = []
535
536 def configure(self):
537         """
538         Find the programs tex, latex and others without raising errors.
539         """
540         v = self.env
541         for p in 'tex latex pdflatex xelatex bibtex dvips dvipdf ps2pdf makeindex pdf2ps makeglossaries'.split():
542                 try:
543                         self.find_program(p, var=p.upper())
544                 except self.errors.ConfigurationError:
545                         pass
546         v.DVIPSFLAGS = '-Ppdf'
547