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