3 # WARNING! Do not edit! https://waf.io/book/index.html#_obtaining_the_waf_file
7 # Thomas Nagy, 2006-2018 (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`
59 for match in re_bibunit.finditer(code):
60 path = match.group('file')
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)
72 Logs.debug('tex: could not find %s', path)
74 Logs.debug('tex: found the following bibunit files: %s', nodes)
77 exts_deps_tex = ['', '.ltx', '.tex', '.bib', '.pdf', '.png', '.eps', '.ps', '.sty']
78 """List of typical file extensions included in latex files"""
80 exts_tex = ['.ltx', '.tex']
81 """List of typical file extensions that contain latex"""
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"""
86 g_bibtex_re = re.compile('bibdata', re.M)
87 """Regexp for bibtex files"""
89 g_glossaries_re = re.compile('\\@newglossary', re.M)
90 """Regexp for expressions that create glossaries"""
94 Compiles a tex/latex file.
96 .. inheritance-diagram:: waflib.Tools.tex.latex waflib.Tools.tex.xelatex waflib.Tools.tex.pdflatex
99 bibtex_fun, _ = Task.compile_fun('${BIBTEX} ${BIBTEXFLAGS} ${SRCFILE}', shell=False)
100 bibtex_fun.__doc__ = """
101 Execute the program **bibtex**
104 makeindex_fun, _ = Task.compile_fun('${MAKEINDEX} ${MAKEINDEXFLAGS} ${SRCFILE}', shell=False)
105 makeindex_fun.__doc__ = """
106 Execute the program **makeindex**
109 makeglossaries_fun, _ = Task.compile_fun('${MAKEGLOSSARIES} ${SRCFILE}', shell=False)
110 makeglossaries_fun.__doc__ = """
111 Execute the program **makeglossaries**
114 def exec_command(self, cmd, **kw):
116 Executes TeX commands without buffering (latex may prompt for inputs)
118 :return: the return code
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)
126 def scan_aux(self, node):
128 Recursive regex-based scanner that finds included auxiliary files.
131 re_aux = re.compile(r'\\@input{(?P<file>[^{}]*)}', re.M)
133 def parse_node(node):
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)
147 Recursive regex-based scanner that finds latex dependencies. It uses :py:attr:`waflib.Tools.tex.re_tex`
149 Depending on your needs you might want:
153 from waflib.Tools import tex
156 * or to change the method scan from the latex tasks::
158 from waflib.Task import classes
159 classes['latex'].scan = myscanfunction
161 node = self.inputs[0]
167 return (nodes, names)
169 def parse_node(node):
174 for match in re_tex.finditer(code):
176 multibib = match.group('type')
177 if multibib and multibib.startswith('bibliography'):
178 multibib = multibib[len('bibliography'):]
179 if multibib.startswith('style'):
184 for path in match.group('file').split(','):
188 for k in exts_deps_tex:
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)
198 for tsk in self.generator.tasks:
199 if not found or found in tsk.outputs:
205 if found.name.endswith(ext):
210 if found and multibib and found.name.endswith('.bib'):
212 self.multibibs.append(found)
213 except AttributeError:
214 self.multibibs = [found]
216 # no break, people are crazy
222 x.parent.get_bld().mkdir()
224 Logs.debug("tex: found the following : %s and names %s", nodes, names)
225 return (nodes, names)
227 def check_status(self, msg, retcode):
229 Checks an exit status and raise an error with a particular message
231 :param msg: message to display if the code is non-zero
233 :param retcode: condition
234 :type retcode: boolean
237 raise Errors.WafError('%r command exit status %r' % (msg, retcode))
239 def info(self, *k, **kw):
241 info = self.generator.bld.conf.logger.info
242 except AttributeError:
248 Parses *.aux* files to find bibfiles to process.
249 If present, execute :py:meth:`waflib.Tools.tex.tex.bibtex_fun`
251 for aux_node in self.aux_nodes:
254 except EnvironmentError:
255 Logs.error('Error reading %s: %r', aux_node.abspath())
258 if g_bibtex_re.findall(ct):
259 self.info('calling bibtex')
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())
267 for node in getattr(self, 'multibibs', []):
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())
276 Parses *.aux* file to find bibunit files. If there are bibunit files,
277 runs :py:meth:`waflib.Tools.tex.tex.bibtex_fun`.
280 bibunits = bibunitscan(self)
282 Logs.error('error bibunitscan')
285 fn = ['bu' + str(i) for i in range(1, len(bibunits) + 1)]
287 self.info('calling bibtex on bibunits')
290 self.env.env = {'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()}
292 self.check_status('error when calling bibtex', self.bibtex_fun())
296 Searches the filesystem for *.idx* files to process. If present,
297 runs :py:meth:`waflib.Tools.tex.tex.makeindex_fun`
299 self.idx_node = self.inputs[0].change_ext('.idx')
301 idx_path = self.idx_node.abspath()
304 self.info('index file %s absent, not calling makeindex', idx_path)
306 self.info('calling makeindex')
308 self.env.SRCFILE = self.idx_node.name
310 self.check_status('error when calling makeindex %s' % idx_path, self.makeindex_fun())
314 Lists additional .aux files from the bibtopic package
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')
320 def makeglossaries(self):
322 Lists additional glossaries from .aux files. If present, runs the makeglossaries program.
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:
330 except EnvironmentError:
331 Logs.error('Error reading %s: %r', aux_node.abspath())
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())
344 Returns the list of texinput nodes as a string suitable for the TEXINPUTS environment variables
348 return os.pathsep.join([k.abspath() for k in self.texinputs_nodes]) + os.pathsep
352 Runs the whole TeX build process
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.
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')
365 # important, set the cwd for everybody
366 self.cwd = self.inputs[0].parent.get_bld()
368 self.info('first pass on %s', self.__class__.__name__)
370 # Hash .aux files before even calling the LaTeX compiler
371 cur_hash = self.hash_aux_nodes()
375 # Find the .aux files again since bibtex processing can require it
376 self.hash_aux_nodes()
382 self.makeglossaries()
385 # There is no need to call latex again if the .aux hash value has not changed
387 cur_hash = self.hash_aux_nodes()
389 Logs.error('No aux.h to process')
390 if cur_hash and cur_hash == prev_hash:
394 self.info('calling %s', self.__class__.__name__)
397 def hash_aux_nodes(self):
399 Returns a hash of the .aux file contents
401 :rtype: string or bytes
405 except AttributeError:
407 self.aux_nodes = self.scan_aux(self.inputs[0].change_ext('.aux'))
410 return Utils.h_list([Utils.h_file(x.abspath()) for x in self.aux_nodes])
412 def call_latex(self):
414 Runs the TeX compiler once
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())
423 "Compiles LaTeX files"
424 texfun, vars = Task.compile_fun('${LATEX} ${LATEXFLAGS} ${SRCFILE}', shell=False)
427 "Compiles PdfLaTeX files"
428 texfun, vars = Task.compile_fun('${PDFLATEX} ${PDFLATEXFLAGS} ${SRCFILE}', shell=False)
432 texfun, vars = Task.compile_fun('${XELATEX} ${XELATEXFLAGS} ${SRCFILE}', shell=False)
434 class dvips(Task.Task):
435 "Converts dvi files to postscript"
436 run_str = '${DVIPS} ${DVIPSFLAGS} ${SRC} -o ${TGT}'
438 after = ['latex', 'pdflatex', 'xelatex']
440 class dvipdf(Task.Task):
441 "Converts dvi files to pdf"
442 run_str = '${DVIPDF} ${DVIPDFFLAGS} ${SRC} ${TGT}'
444 after = ['latex', 'pdflatex', 'xelatex']
446 class pdf2ps(Task.Task):
447 "Converts pdf files to postscript"
448 run_str = '${PDF2PS} ${PDF2PSFLAGS} ${SRC} ${TGT}'
450 after = ['latex', 'pdflatex', 'xelatex']
453 @before_method('process_source')
456 Creates :py:class:`waflib.Tools.tex.tex` objects, and
457 dvips/dvipdf/pdf2ps tasks if necessary (outs='ps', etc).
459 if not getattr(self, 'type', None) in ('latex', 'pdflatex', 'xelatex'):
460 self.type = 'pdflatex'
462 outs = Utils.to_list(getattr(self, 'outs', []))
464 # prompt for incomplete files (else the batchmode is used)
466 self.generator.bld.conf
467 except AttributeError:
468 default_prompt = False
470 default_prompt = True
471 self.env.PROMPT_LATEX = getattr(self, 'prompt', default_prompt)
475 if getattr(self, 'deps', None):
476 deps = self.to_list(self.deps)
478 if isinstance(dep, str):
479 n = self.path.find_resource(dep)
481 self.bld.fatal('Could not find %r for %r' % (dep, self))
482 if not n in deps_lst:
484 elif isinstance(dep, Node.Node):
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'))
497 # add the manual dependencies
500 if not n in task.dep_nodes:
501 task.dep_nodes.append(n)
503 # texinputs is a nasty beast
504 if hasattr(self, 'texinputs_nodes'):
505 task.texinputs_nodes = self.texinputs_nodes
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
512 lst = lst.split(os.pathsep)
516 p = self.bld.root.find_node(x)
518 task.texinputs_nodes.append(p)
520 Logs.error('Invalid TEXINPUTS folder %s', x)
522 Logs.error('Cannot resolve relative paths in TEXINPUTS %s', x)
524 if self.type == 'latex':
526 tsk = self.create_task('dvips', task.outputs, node.change_ext('.ps'))
527 tsk.env.env = dict(os.environ)
529 tsk = self.create_task('dvipdf', task.outputs, node.change_ext('.pdf'))
530 tsk.env.env = dict(os.environ)
531 elif self.type == 'pdflatex':
533 self.create_task('pdf2ps', task.outputs, node.change_ext('.ps'))
538 Find the programs tex, latex and others without raising errors.
541 for p in 'tex latex pdflatex xelatex bibtex dvips dvipdf ps2pdf makeindex pdf2ps makeglossaries'.split():
543 self.find_program(p, var=p.upper())
544 except self.errors.ConfigurationError:
546 v.DVIPSFLAGS = '-Ppdf'