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