3 # WARNING! Do not edit! https://waf.io/book/index.html#_obtaining_the_waf_file
7 # Thomas Nagy, 2006-2016 (ita)
10 TeX/LaTeX/PDFLaTeX/XeLaTeX support
16 if not conf.env.LATEX:
17 conf.fatal('The program LaTex is required')
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
31 - To configure with a special program, use::
33 $ PDFLATEX=luatex waf configure
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)
41 from waflib import Utils, Task, Errors, Logs, Node
42 from waflib.TaskGen import feature, before_method
44 re_bibunit = re.compile(r'\\(?P<type>putbib)\[(?P<file>[^\[\]]*)\]',re.M)
45 def bibunitscan(self):
47 Parses TeX inputs and try to find the *bibunit* file dependencies
49 :return: list of bibunit files
50 :rtype: list of :py:class:`waflib.Node.Node`
55 if not node: return nodes
58 for match in re_bibunit.finditer(code):
59 path = match.group('file')
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)
67 # no break, people are crazy
69 Logs.debug('tex: could not find %s', path)
71 Logs.debug('tex: found the following bibunit files: %s', nodes)
74 exts_deps_tex = ['', '.ltx', '.tex', '.bib', '.pdf', '.png', '.eps', '.ps', '.sty']
75 """List of typical file extensions included in latex files"""
77 exts_tex = ['.ltx', '.tex']
78 """List of typical file extensions that contain latex"""
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"""
83 g_bibtex_re = re.compile('bibdata', re.M)
84 """Regexp for bibtex files"""
86 g_glossaries_re = re.compile('\\@newglossary', re.M)
87 """Regexp for expressions that create glossaries"""
91 Compiles a tex/latex file.
93 .. inheritance-diagram:: waflib.Tools.tex.latex waflib.Tools.tex.xelatex waflib.Tools.tex.pdflatex
96 bibtex_fun, _ = Task.compile_fun('${BIBTEX} ${BIBTEXFLAGS} ${SRCFILE}', shell=False)
97 bibtex_fun.__doc__ = """
98 Execute the program **bibtex**
101 makeindex_fun, _ = Task.compile_fun('${MAKEINDEX} ${MAKEINDEXFLAGS} ${SRCFILE}', shell=False)
102 makeindex_fun.__doc__ = """
103 Execute the program **makeindex**
106 makeglossaries_fun, _ = Task.compile_fun('${MAKEGLOSSARIES} ${SRCFILE}', shell=False)
107 makeglossaries_fun.__doc__ = """
108 Execute the program **makeglossaries**
111 def exec_command(self, cmd, **kw):
113 Executes TeX commands without buffering (latex may prompt for inputs)
115 :return: the return code
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)
123 def scan_aux(self, node):
125 Recursive regex-based scanner that finds included auxiliary files.
128 re_aux = re.compile(r'\\@input{(?P<file>[^{}]*)}', re.M)
130 def parse_node(node):
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)
144 Recursive regex-based scanner that finds latex dependencies. It uses :py:attr:`waflib.Tools.tex.re_tex`
146 Depending on your needs you might want:
150 from waflib.Tools import tex
153 * or to change the method scan from the latex tasks::
155 from waflib.Task import classes
156 classes['latex'].scan = myscanfunction
158 node = self.inputs[0]
163 if not node: return (nodes, names)
165 def parse_node(node):
171 for match in re_tex.finditer(code):
173 multibib = match.group('type')
174 if multibib and multibib.startswith('bibliography'):
175 multibib = multibib[len('bibliography'):]
176 if multibib.startswith('style'):
181 for path in match.group('file').split(','):
185 for k in exts_deps_tex:
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)
195 for tsk in self.generator.tasks:
196 if not found or found in tsk.outputs:
202 if found.name.endswith(ext):
207 if found and multibib and found.name.endswith('.bib'):
209 self.multibibs.append(found)
210 except AttributeError:
211 self.multibibs = [found]
213 # no break, people are crazy
219 x.parent.get_bld().mkdir()
221 Logs.debug("tex: found the following : %s and names %s", nodes, names)
222 return (nodes, names)
224 def check_status(self, msg, retcode):
226 Checks an exit status and raise an error with a particular message
228 :param msg: message to display if the code is non-zero
230 :param retcode: condition
231 :type retcode: boolean
234 raise Errors.WafError('%r command exit status %r' % (msg, retcode))
236 def info(self, *k, **kw):
238 info = self.generator.bld.conf.logger.info
239 except AttributeError:
245 Parses *.aux* files to find bibfiles to process.
246 If present, execute :py:meth:`waflib.Tools.tex.tex.bibtex_fun`
248 for aux_node in self.aux_nodes:
251 except EnvironmentError:
252 Logs.error('Error reading %s: %r', aux_node.abspath())
255 if g_bibtex_re.findall(ct):
256 self.info('calling bibtex')
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())
264 for node in getattr(self, 'multibibs', []):
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())
273 Parses *.aux* file to find bibunit files. If there are bibunit files,
274 runs :py:meth:`waflib.Tools.tex.tex.bibtex_fun`.
277 bibunits = bibunitscan(self)
279 Logs.error('error bibunitscan')
282 fn = ['bu' + str(i) for i in range(1, len(bibunits) + 1)]
284 self.info('calling bibtex on bibunits')
287 self.env.env = {'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()}
289 self.check_status('error when calling bibtex', self.bibtex_fun())
293 Searches the filesystem for *.idx* files to process. If present,
294 runs :py:meth:`waflib.Tools.tex.tex.makeindex_fun`
296 self.idx_node = self.inputs[0].change_ext('.idx')
298 idx_path = self.idx_node.abspath()
301 self.info('index file %s absent, not calling makeindex', idx_path)
303 self.info('calling makeindex')
305 self.env.SRCFILE = self.idx_node.name
307 self.check_status('error when calling makeindex %s' % idx_path, self.makeindex_fun())
311 Lists additional .aux files from the bibtopic package
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')
317 def makeglossaries(self):
319 Lists additional glossaries from .aux files. If present, runs the makeglossaries program.
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:
327 except EnvironmentError:
328 Logs.error('Error reading %s: %r', aux_node.abspath())
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())
341 Returns the list of texinput nodes as a string suitable for the TEXINPUTS environment variables
345 return os.pathsep.join([k.abspath() for k in self.texinputs_nodes]) + os.pathsep
349 Runs the whole TeX build process
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.
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')
362 # important, set the cwd for everybody
363 self.cwd = self.inputs[0].parent.get_bld()
365 self.info('first pass on %s', self.__class__.__name__)
367 # Hash .aux files before even calling the LaTeX compiler
368 cur_hash = self.hash_aux_nodes()
372 # Find the .aux files again since bibtex processing can require it
373 self.hash_aux_nodes()
379 self.makeglossaries()
382 # There is no need to call latex again if the .aux hash value has not changed
384 cur_hash = self.hash_aux_nodes()
386 Logs.error('No aux.h to process')
387 if cur_hash and cur_hash == prev_hash:
391 self.info('calling %s', self.__class__.__name__)
394 def hash_aux_nodes(self):
396 Returns a hash of the .aux file contents
398 :rtype: string or bytes
402 except AttributeError:
404 self.aux_nodes = self.scan_aux(self.inputs[0].change_ext('.aux'))
407 return Utils.h_list([Utils.h_file(x.abspath()) for x in self.aux_nodes])
409 def call_latex(self):
411 Runs the TeX compiler once
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())
420 "Compiles LaTeX files"
421 texfun, vars = Task.compile_fun('${LATEX} ${LATEXFLAGS} ${SRCFILE}', shell=False)
424 "Compiles PdfLaTeX files"
425 texfun, vars = Task.compile_fun('${PDFLATEX} ${PDFLATEXFLAGS} ${SRCFILE}', shell=False)
429 texfun, vars = Task.compile_fun('${XELATEX} ${XELATEXFLAGS} ${SRCFILE}', shell=False)
431 class dvips(Task.Task):
432 "Converts dvi files to postscript"
433 run_str = '${DVIPS} ${DVIPSFLAGS} ${SRC} -o ${TGT}'
435 after = ['latex', 'pdflatex', 'xelatex']
437 class dvipdf(Task.Task):
438 "Converts dvi files to pdf"
439 run_str = '${DVIPDF} ${DVIPDFFLAGS} ${SRC} ${TGT}'
441 after = ['latex', 'pdflatex', 'xelatex']
443 class pdf2ps(Task.Task):
444 "Converts pdf files to postscript"
445 run_str = '${PDF2PS} ${PDF2PSFLAGS} ${SRC} ${TGT}'
447 after = ['latex', 'pdflatex', 'xelatex']
450 @before_method('process_source')
453 Creates :py:class:`waflib.Tools.tex.tex` objects, and
454 dvips/dvipdf/pdf2ps tasks if necessary (outs='ps', etc).
456 if not getattr(self, 'type', None) in ('latex', 'pdflatex', 'xelatex'):
457 self.type = 'pdflatex'
459 outs = Utils.to_list(getattr(self, 'outs', []))
461 # prompt for incomplete files (else the batchmode is used)
463 self.generator.bld.conf
464 except AttributeError:
465 default_prompt = False
467 default_prompt = True
468 self.env.PROMPT_LATEX = getattr(self, 'prompt', default_prompt)
472 if getattr(self, 'deps', None):
473 deps = self.to_list(self.deps)
475 if isinstance(dep, str):
476 n = self.path.find_resource(dep)
478 self.bld.fatal('Could not find %r for %r' % (dep, self))
479 if not n in deps_lst:
481 elif isinstance(dep, Node.Node):
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'))
494 # add the manual dependencies
497 if not n in task.dep_nodes:
498 task.dep_nodes.append(n)
500 # texinputs is a nasty beast
501 if hasattr(self, 'texinputs_nodes'):
502 task.texinputs_nodes = self.texinputs_nodes
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
509 lst = lst.split(os.pathsep)
513 p = self.bld.root.find_node(x)
515 task.texinputs_nodes.append(p)
517 Logs.error('Invalid TEXINPUTS folder %s', x)
519 Logs.error('Cannot resolve relative paths in TEXINPUTS %s', x)
521 if self.type == 'latex':
523 tsk = self.create_task('dvips', task.outputs, node.change_ext('.ps'))
524 tsk.env.env = dict(os.environ)
526 tsk = self.create_task('dvipdf', task.outputs, node.change_ext('.pdf'))
527 tsk.env.env = dict(os.environ)
528 elif self.type == 'pdflatex':
530 self.create_task('pdf2ps', task.outputs, node.change_ext('.ps'))
535 Find the programs tex, latex and others without raising errors.
538 for p in 'tex latex pdflatex xelatex bibtex dvips dvipdf ps2pdf makeindex pdf2ps makeglossaries'.split():
540 self.find_program(p, var=p.upper())
541 except self.errors.ConfigurationError:
543 v.DVIPSFLAGS = '-Ppdf'