waf/waflib/Tools/tex.py

509 lines
14 KiB
Python
Raw Normal View History

2011-09-10 11:13:51 +02:00
#!/usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2006-2010 (ita)
"""
TeX/LaTeX/PDFLaTeX/XeLaTeX support
Example::
def configure(conf):
conf.load('tex')
if not conf.env.LATEX:
conf.fatal('The program LaTex is required')
def build(bld):
bld(
features = 'tex',
type = 'latex', # pdflatex or xelatex
source = 'document.ltx', # mandatory, the source
outs = 'ps', # 'pdf' or 'ps pdf'
deps = 'crossreferencing.lst', # to give dependencies directly
prompt = 1, # 0 for the batch mode
)
2013-09-14 06:34:13 +02:00
Notes:
- To configure with a special program, use::
$ PDFLATEX=luatex waf configure
- This tool doesn't use the target attribute of the task generator
(``bld(target=...)``); the target file name is built from the source
base name and the out type(s)
2011-09-10 11:13:51 +02:00
"""
import os, re
from waflib import Utils, Task, Errors, Logs, Node
2011-09-10 11:13:51 +02:00
from waflib.TaskGen import feature, before_method
re_bibunit = re.compile(r'\\(?P<type>putbib)\[(?P<file>[^\[\]]*)\]',re.M)
def bibunitscan(self):
"""
Parse the inputs and try to find the *bibunit* dependencies
:return: list of bibunit files
:rtype: list of :py:class:`waflib.Node.Node`
"""
node = self.inputs[0]
nodes = []
if not node: return nodes
2012-05-08 18:31:20 +02:00
code = node.read()
2011-09-10 11:13:51 +02:00
for match in re_bibunit.finditer(code):
path = match.group('file')
if path:
for k in ('', '.bib'):
2011-09-10 11:13:51 +02:00
# add another loop for the tex include paths?
2016-03-19 14:46:22 +01:00
Logs.debug('tex: trying %s%s', path, k)
2011-09-10 11:13:51 +02:00
fi = node.parent.find_resource(path + k)
if fi:
nodes.append(fi)
# no break, people are crazy
else:
2016-03-19 14:46:22 +01:00
Logs.debug('tex: could not find %s', path)
2011-09-10 11:13:51 +02:00
2016-03-19 14:46:22 +01:00
Logs.debug("tex: found the following bibunit files: %s", nodes)
2011-09-10 11:13:51 +02:00
return nodes
exts_deps_tex = ['', '.ltx', '.tex', '.bib', '.pdf', '.png', '.eps', '.ps', '.sty']
2011-09-10 11:13:51 +02:00
"""List of typical file extensions included in latex files"""
2011-11-06 12:58:26 +01:00
exts_tex = ['.ltx', '.tex']
"""List of typical file extensions that contain latex"""
re_tex = re.compile(r'\\(?P<type>usepackage|RequirePackage|include|bibliography([^\[\]{}]*)|putbib|includegraphics|input|import|bringin|lstinputlisting)(\[[^\[\]]*\])?{(?P<file>[^{}]*)}',re.M)
2011-09-10 11:13:51 +02:00
"""Regexp for expressions that may include latex files"""
g_bibtex_re = re.compile('bibdata', re.M)
"""Regexp for bibtex files"""
g_glossaries_re = re.compile('\\@newglossary', re.M)
"""Regexp for expressions that create glossaries"""
2011-09-10 11:13:51 +02:00
class tex(Task.Task):
"""
Compile a tex/latex file.
.. inheritance-diagram:: waflib.Tools.tex.latex waflib.Tools.tex.xelatex waflib.Tools.tex.pdflatex
"""
bibtex_fun, _ = Task.compile_fun('${BIBTEX} ${BIBTEXFLAGS} ${SRCFILE}', shell=False)
bibtex_fun.__doc__ = """
Execute the program **bibtex**
"""
makeindex_fun, _ = Task.compile_fun('${MAKEINDEX} ${MAKEINDEXFLAGS} ${SRCFILE}', shell=False)
makeindex_fun.__doc__ = """
Execute the program **makeindex**
"""
makeglossaries_fun, _ = Task.compile_fun('${MAKEGLOSSARIES} ${SRCFILE}', shell=False)
makeglossaries_fun.__doc__ = """
Execute the program **makeglossaries**
"""
def exec_command(self, cmd, **kw):
"""
Override :py:meth:`waflib.Task.Task.exec_command` to execute the command without buffering (latex may prompt for inputs)
:return: the return code
:rtype: int
"""
2016-01-11 05:25:46 +01:00
kw['stdout'] = kw['stderr'] = None
return super(tex, self).exec_command(cmd, **kw)
2011-11-06 13:06:54 +01:00
def scan_aux(self, node):
"""
A recursive regex-based scanner that finds included auxiliary files.
"""
nodes = [node]
re_aux = re.compile(r'\\@input{(?P<file>[^{}]*)}', re.M)
def parse_node(node):
code = node.read()
for match in re_aux.finditer(code):
path = match.group('file')
found = node.parent.find_or_declare(path)
if found and found not in nodes:
2016-03-19 14:46:22 +01:00
Logs.debug('tex: found aux node %r', found)
2011-11-06 13:06:54 +01:00
nodes.append(found)
parse_node(found)
parse_node(node)
return nodes
2011-09-10 11:13:51 +02:00
def scan(self):
"""
A recursive regex-based scanner that finds latex dependencies. It uses :py:attr:`waflib.Tools.tex.re_tex`
Depending on your needs you might want:
* to change re_tex::
from waflib.Tools import tex
tex.re_tex = myregex
* or to change the method scan from the latex tasks::
from waflib.Task import classes
classes['latex'].scan = myscanfunction
"""
node = self.inputs[0]
nodes = []
names = []
seen = []
if not node: return (nodes, names)
def parse_node(node):
if node in seen:
return
seen.append(node)
code = node.read()
global re_tex
for match in re_tex.finditer(code):
multibib = match.group('type')
if multibib and multibib.startswith('bibliography'):
multibib = multibib[len('bibliography'):]
if multibib.startswith('style'):
continue
else:
multibib = None
2011-09-10 11:13:51 +02:00
for path in match.group('file').split(','):
if path:
add_name = True
found = None
for k in exts_deps_tex:
2014-09-20 19:27:27 +02:00
# issue 1067, scan in all texinputs folders
for up in self.texinputs_nodes:
2016-03-19 14:46:22 +01:00
Logs.debug('tex: trying %s%s', path, k)
2014-09-20 19:27:27 +02:00
found = up.find_resource(path + k)
if found:
break
2013-02-01 23:01:53 +01:00
for tsk in self.generator.tasks:
if not found or found in tsk.outputs:
break
else:
2011-09-10 11:13:51 +02:00
nodes.append(found)
add_name = False
2011-11-06 12:58:26 +01:00
for ext in exts_tex:
if found.name.endswith(ext):
parse_node(found)
break
# multibib stuff
if found and multibib and found.name.endswith('.bib'):
try:
self.multibibs.append(found)
except AttributeError:
self.multibibs = [found]
2011-09-10 11:13:51 +02:00
# no break, people are crazy
if add_name:
names.append(path)
parse_node(node)
for x in nodes:
x.parent.get_bld().mkdir()
2016-03-19 14:46:22 +01:00
Logs.debug("tex: found the following : %s and names %s", nodes, names)
2011-09-10 11:13:51 +02:00
return (nodes, names)
def check_status(self, msg, retcode):
"""
Check an exit status and raise an error with a particular message
:param msg: message to display if the code is non-zero
:type msg: string
:param retcode: condition
:type retcode: boolean
"""
if retcode != 0:
raise Errors.WafError("%r command exit status %r" % (msg, retcode))
def bibfile(self):
"""
2013-04-27 09:19:16 +02:00
Parse the *.aux* files to find bibfiles to process.
2011-09-10 11:13:51 +02:00
If yes, execute :py:meth:`waflib.Tools.tex.tex.bibtex_fun`
"""
2013-04-27 09:19:16 +02:00
for aux_node in self.aux_nodes:
try:
2011-11-06 13:06:54 +01:00
ct = aux_node.read()
2014-10-30 19:09:53 +01:00
except EnvironmentError:
2013-04-27 09:19:16 +02:00
Logs.error('Error reading %s: %r' % aux_node.abspath())
continue
if g_bibtex_re.findall(ct):
Logs.info('calling bibtex')
2011-09-10 11:13:51 +02:00
self.env.env = {}
self.env.env.update(os.environ)
2014-09-20 19:27:27 +02:00
self.env.env.update({'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()})
2013-04-27 09:19:16 +02:00
self.env.SRCFILE = aux_node.name[:-4]
2011-09-10 11:13:51 +02:00
self.check_status('error when calling bibtex', self.bibtex_fun())
for node in getattr(self, 'multibibs', []):
self.env.env = {}
self.env.env.update(os.environ)
2014-09-20 19:27:27 +02:00
self.env.env.update({'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()})
self.env.SRCFILE = node.name[:-4]
self.check_status('error when calling bibtex', self.bibtex_fun())
2011-09-10 11:13:51 +02:00
def bibunits(self):
"""
Parse the *.aux* file to find bibunit files. If there are bibunit files,
execute :py:meth:`waflib.Tools.tex.tex.bibtex_fun`.
"""
try:
bibunits = bibunitscan(self)
except OSError:
Logs.error('error bibunitscan')
2011-09-10 11:13:51 +02:00
else:
if bibunits:
2013-12-24 15:31:45 +01:00
fn = ['bu' + str(i) for i in range(1, len(bibunits) + 1)]
2011-09-10 11:13:51 +02:00
if fn:
Logs.info('calling bibtex on bibunits')
2011-09-10 11:13:51 +02:00
for f in fn:
2014-09-20 19:27:27 +02:00
self.env.env = {'BIBINPUTS': self.texinputs(), 'BSTINPUTS': self.texinputs()}
2011-09-10 11:13:51 +02:00
self.env.SRCFILE = f
self.check_status('error when calling bibtex', self.bibtex_fun())
def makeindex(self):
"""
Look on the filesystem if there is a *.idx* file to process. If yes, execute
:py:meth:`waflib.Tools.tex.tex.makeindex_fun`
"""
2014-09-20 21:51:54 +02:00
self.idx_node = self.inputs[0].change_ext('.idx')
2011-09-10 11:13:51 +02:00
try:
idx_path = self.idx_node.abspath()
os.stat(idx_path)
except OSError:
2016-03-19 14:46:22 +01:00
Logs.info('index file %s absent, not calling makeindex', idx_path)
2011-09-10 11:13:51 +02:00
else:
Logs.info('calling makeindex')
2011-09-10 11:13:51 +02:00
self.env.SRCFILE = self.idx_node.name
self.env.env = {}
self.check_status('error when calling makeindex %s' % idx_path, self.makeindex_fun())
2013-04-27 09:19:16 +02:00
def bibtopic(self):
"""
Additional .aux files from the bibtopic package
"""
p = self.inputs[0].parent.get_bld()
if os.path.exists(os.path.join(p.abspath(), 'btaux.aux')):
self.aux_nodes += p.ant_glob('*[0-9].aux')
def makeglossaries(self):
src_file = self.inputs[0].abspath()
base_file = os.path.basename(src_file)
base, _ = os.path.splitext(base_file)
for aux_node in self.aux_nodes:
try:
ct = aux_node.read()
2014-10-30 19:09:53 +01:00
except EnvironmentError:
Logs.error('Error reading %s: %r' % aux_node.abspath())
continue
if g_glossaries_re.findall(ct):
if not self.env.MAKEGLOSSARIES:
raise Errors.WafError("The program 'makeglossaries' is missing!")
Logs.warn('calling makeglossaries')
self.env.SRCFILE = base
self.check_status('error when calling makeglossaries %s' % base, self.makeglossaries_fun())
return
2014-09-20 19:27:27 +02:00
def texinputs(self):
return os.pathsep.join([k.abspath() for k in self.texinputs_nodes]) + os.pathsep
2011-09-10 11:13:51 +02:00
def run(self):
"""
Runs the TeX build process.
2011-11-06 13:10:06 +01:00
It may require multiple passes, depending on the usage of cross-references,
bibliographies, content susceptible of needing such passes.
The appropriate TeX compiler is called until the *.aux* files stop changing.
2011-09-10 11:13:51 +02:00
Makeindex and bibtex are called if necessary.
"""
env = self.env
if not env['PROMPT_LATEX']:
env.append_value('LATEXFLAGS', '-interaction=batchmode')
env.append_value('PDFLATEXFLAGS', '-interaction=batchmode')
env.append_value('XELATEXFLAGS', '-interaction=batchmode')
# important, set the cwd for everybody
2016-01-02 01:54:52 +01:00
self.cwd = self.inputs[0].parent.get_bld()
2011-09-10 11:13:51 +02:00
2016-03-19 14:46:22 +01:00
Logs.info('first pass on %s', self.__class__.__name__)
2011-09-10 11:13:51 +02:00
2014-09-20 21:51:54 +02:00
# Hash .aux files before even calling the LaTeX compiler
cur_hash = self.hash_aux_nodes()
self.call_latex()
2011-09-10 11:13:51 +02:00
2014-09-20 21:51:54 +02:00
# Find the .aux files again since bibtex processing can require it
self.hash_aux_nodes()
2011-11-06 13:06:54 +01:00
2013-04-27 09:19:16 +02:00
self.bibtopic()
2011-09-10 11:13:51 +02:00
self.bibfile()
self.bibunits()
self.makeindex()
self.makeglossaries()
2011-09-10 11:13:51 +02:00
for i in range(10):
2014-09-20 21:51:54 +02:00
# There is no need to call latex again if the .aux hash value has not changed
prev_hash = cur_hash
cur_hash = self.hash_aux_nodes()
if not cur_hash:
Logs.error('No aux.h to process')
if cur_hash and cur_hash == prev_hash:
2011-09-10 11:13:51 +02:00
break
# run the command
2016-03-19 14:46:22 +01:00
Logs.info('calling %s', self.__class__.__name__)
2014-09-20 21:51:54 +02:00
self.call_latex()
def hash_aux_nodes(self):
try:
self.aux_nodes
2014-09-20 21:51:54 +02:00
except AttributeError:
try:
self.aux_nodes = self.scan_aux(self.inputs[0].change_ext('.aux'))
except IOError:
return None
return Utils.h_list([Utils.h_file(x.abspath()) for x in self.aux_nodes])
def call_latex(self):
self.env.env = {}
self.env.env.update(os.environ)
self.env.env.update({'TEXINPUTS': self.texinputs()})
self.env.SRCFILE = self.inputs[0].abspath()
self.check_status('error when calling latex', self.texfun())
2011-09-10 11:13:51 +02:00
class latex(tex):
texfun, vars = Task.compile_fun('${LATEX} ${LATEXFLAGS} ${SRCFILE}', shell=False)
class pdflatex(tex):
texfun, vars = Task.compile_fun('${PDFLATEX} ${PDFLATEXFLAGS} ${SRCFILE}', shell=False)
class xelatex(tex):
texfun, vars = Task.compile_fun('${XELATEX} ${XELATEXFLAGS} ${SRCFILE}', shell=False)
class dvips(Task.Task):
run_str = '${DVIPS} ${DVIPSFLAGS} ${SRC} -o ${TGT}'
color = 'BLUE'
after = ['latex', 'pdflatex', 'xelatex']
class dvipdf(Task.Task):
run_str = '${DVIPDF} ${DVIPDFFLAGS} ${SRC} ${TGT}'
color = 'BLUE'
after = ['latex', 'pdflatex', 'xelatex']
class pdf2ps(Task.Task):
run_str = '${PDF2PS} ${PDF2PSFLAGS} ${SRC} ${TGT}'
color = 'BLUE'
after = ['latex', 'pdflatex', 'xelatex']
@feature('tex')
@before_method('process_source')
def apply_tex(self):
"""
Create :py:class:`waflib.Tools.tex.tex` objects, and dvips/dvipdf/pdf2ps tasks if necessary (outs='ps', etc).
"""
if not getattr(self, 'type', None) in ('latex', 'pdflatex', 'xelatex'):
2011-09-10 11:13:51 +02:00
self.type = 'pdflatex'
outs = Utils.to_list(getattr(self, 'outs', []))
# prompt for incomplete files (else the batchmode is used)
self.env['PROMPT_LATEX'] = getattr(self, 'prompt', 1)
deps_lst = []
if getattr(self, 'deps', None):
deps = self.to_list(self.deps)
for dep in deps:
if isinstance(dep, str):
n = self.path.find_resource(dep)
if not n:
2014-01-12 19:49:31 +01:00
self.bld.fatal('Could not find %r for %r' % (dep, self))
if not n in deps_lst:
deps_lst.append(n)
elif isinstance(dep, Node.Node):
deps_lst.append(dep)
2011-09-10 11:13:51 +02:00
for node in self.to_nodes(self.source):
if self.type == 'latex':
task = self.create_task('latex', node, node.change_ext('.dvi'))
elif self.type == 'pdflatex':
task = self.create_task('pdflatex', node, node.change_ext('.pdf'))
elif self.type == 'xelatex':
task = self.create_task('xelatex', node, node.change_ext('.pdf'))
task.env = self.env
# add the manual dependencies
if deps_lst:
for n in deps_lst:
if not n in task.dep_nodes:
task.dep_nodes.append(n)
2011-09-10 11:13:51 +02:00
2014-09-20 19:27:27 +02:00
# texinputs is a nasty beast
2014-10-23 22:22:18 +02:00
if hasattr(self, 'texinputs_nodes'):
2014-09-20 19:27:27 +02:00
task.texinputs_nodes = self.texinputs_nodes
else:
task.texinputs_nodes = [node.parent, node.parent.get_bld(), self.path, self.path.get_bld()]
lst = os.environ.get('TEXINPUTS', '')
2014-10-23 22:22:18 +02:00
if self.env.TEXINPUTS:
lst += os.pathsep + self.env.TEXINPUTS
2014-09-20 19:27:27 +02:00
if lst:
lst = lst.split(os.pathsep)
for x in lst:
if x:
if os.path.isabs(x):
p = self.bld.root.find_node(x)
if p:
task.texinputs_nodes.append(p)
else:
Logs.error('Invalid TEXINPUTS folder %s' % x)
else:
Logs.error('Cannot resolve relative paths in TEXINPUTS %s' % x)
2013-02-01 23:45:30 +01:00
2011-09-10 11:13:51 +02:00
if self.type == 'latex':
if 'ps' in outs:
tsk = self.create_task('dvips', task.outputs, node.change_ext('.ps'))
2014-09-20 19:27:27 +02:00
tsk.env.env = dict(os.environ)
2011-09-10 11:13:51 +02:00
if 'pdf' in outs:
tsk = self.create_task('dvipdf', task.outputs, node.change_ext('.pdf'))
2014-09-20 19:27:27 +02:00
tsk.env.env = dict(os.environ)
2011-09-10 11:13:51 +02:00
elif self.type == 'pdflatex':
if 'ps' in outs:
self.create_task('pdf2ps', task.outputs, node.change_ext('.ps'))
self.source = []
def configure(self):
"""
Try to find the programs tex, latex and others. Do not raise any error if they
are not found.
"""
v = self.env
for p in 'tex latex pdflatex xelatex bibtex dvips dvipdf ps2pdf makeindex pdf2ps makeglossaries'.split():
2011-09-10 11:13:51 +02:00
try:
self.find_program(p, var=p.upper())
except self.errors.ConfigurationError:
pass
v['DVIPSFLAGS'] = '-Ppdf'