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