waf

FORK: waf with some random patches
git clone https://git.neptards.moe/neptards/waf.git
Log | Files | Refs | README

tex.py (15360B)


      1 #!/usr/bin/env python
      2 # encoding: utf-8
      3 # Thomas Nagy, 2006-2018 (ita)
      4 
      5 """
      6 TeX/LaTeX/PDFLaTeX/XeLaTeX support
      7 
      8 Example::
      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 
     25 Notes:
     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 
     36 import os, re
     37 from waflib import Utils, Task, Errors, Logs, Node
     38 from waflib.TaskGen import feature, before_method
     39 
     40 re_bibunit = re.compile(r'\\(?P<type>putbib)\[(?P<file>[^\[\]]*)\]',re.M)
     41 def 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 
     73 exts_deps_tex = ['', '.ltx', '.tex', '.bib', '.pdf', '.png', '.eps', '.ps', '.sty']
     74 """List of typical file extensions included in latex files"""
     75 
     76 exts_tex = ['.ltx', '.tex']
     77 """List of typical file extensions that contain latex"""
     78 
     79 re_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 
     82 g_bibtex_re = re.compile('bibdata', re.M)
     83 """Regexp for bibtex files"""
     84 
     85 g_glossaries_re = re.compile('\\@newglossary', re.M)
     86 """Regexp for expressions that create glossaries"""
     87 
     88 class 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 	   :top-classes: waflib.Tools.tex.tex
     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:
    164 			return (nodes, names)
    165 
    166 		def parse_node(node):
    167 			if node in seen:
    168 				return
    169 			seen.append(node)
    170 			code = node.read()
    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'
    544