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