3 Tool-specific initialization for TeX.
5 There normally shouldn't be any need to import this module directly.
6 It will usually be imported through the generic SCons.Tool.Tool()
14 # Permission is hereby granted, free of charge, to any person obtaining
15 # a copy of this software and associated documentation files (the
16 # "Software"), to deal in the Software without restriction, including
17 # without limitation the rights to use, copy, modify, merge, publish,
18 # distribute, sublicense, and/or sell copies of the Software, and to
19 # permit persons to whom the Software is furnished to do so, subject to
20 # the following conditions:
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
26 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
27 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
45 import SCons.Scanner.LaTeX
49 must_rerun_latex = True
51 # these are files that just need to be checked for changes and then rerun latex
52 check_suffixes = ['.toc', '.lof', '.lot', '.out', '.nav', '.snm']
54 # these are files that require bibtex or makeindex to be run when they change
55 all_suffixes = check_suffixes + ['.bbl', '.idx', '.nlo', '.glo']
58 # regular expressions used to search for Latex features
59 # or outputs that require rerunning latex
61 # search for all .aux files opened by latex (recorded in the .log file)
62 openout_aux_re = re.compile(r"\\openout.*`(.*\.aux)'")
64 #printindex_re = re.compile(r"^[^%]*\\printindex", re.MULTILINE)
65 #printnomenclature_re = re.compile(r"^[^%]*\\printnomenclature", re.MULTILINE)
66 #printglossary_re = re.compile(r"^[^%]*\\printglossary", re.MULTILINE)
68 # search to find rerun warnings
69 warning_rerun_str = '(^LaTeX Warning:.*Rerun)|(^Package \w+ Warning:.*Rerun)'
70 warning_rerun_re = re.compile(warning_rerun_str, re.MULTILINE)
72 # search to find citation rerun warnings
73 rerun_citations_str = "^LaTeX Warning:.*\n.*Rerun to get citations correct"
74 rerun_citations_re = re.compile(rerun_citations_str, re.MULTILINE)
76 # search to find undefined references or citations warnings
77 undefined_references_str = '(^LaTeX Warning:.*undefined references)|(^Package \w+ Warning:.*undefined citations)'
78 undefined_references_re = re.compile(undefined_references_str, re.MULTILINE)
81 auxfile_re = re.compile(r".", re.MULTILINE)
82 tableofcontents_re = re.compile(r"^[^%\n]*\\tableofcontents", re.MULTILINE)
83 makeindex_re = re.compile(r"^[^%\n]*\\makeindex", re.MULTILINE)
84 bibliography_re = re.compile(r"^[^%\n]*\\bibliography", re.MULTILINE)
85 listoffigures_re = re.compile(r"^[^%\n]*\\listoffigures", re.MULTILINE)
86 listoftables_re = re.compile(r"^[^%\n]*\\listoftables", re.MULTILINE)
87 hyperref_re = re.compile(r"^[^%\n]*\\usepackage.*\{hyperref\}", re.MULTILINE)
88 makenomenclature_re = re.compile(r"^[^%\n]*\\makenomenclature", re.MULTILINE)
89 makeglossary_re = re.compile(r"^[^%\n]*\\makeglossary", re.MULTILINE)
90 beamer_re = re.compile(r"^[^%\n]*\\documentclass\{beamer\}", re.MULTILINE)
92 # search to find all files included by Latex
93 include_re = re.compile(r'^[^%\n]*\\(?:include|input){([^}]*)}', re.MULTILINE)
95 # search to find all graphics files included by Latex
96 includegraphics_re = re.compile(r'^[^%\n]*\\(?:includegraphics(?:\[[^\]]+\])?){([^}]*)}', re.MULTILINE)
98 # search to find all files opened by Latex (recorded in .log file)
99 openout_re = re.compile(r"\\openout.*`(.*)'")
101 # list of graphics file extensions for TeX and LaTeX
102 TexGraphics = SCons.Scanner.LaTeX.TexGraphics
103 LatexGraphics = SCons.Scanner.LaTeX.LatexGraphics
105 # An Action sufficient to build any generic tex file.
108 # An action to build a latex file. This action might be needed more
109 # than once if we are dealing with labels and bibtex.
112 # An action to run BibTeX on a file.
115 # An action to run MakeIndex on a file.
116 MakeIndexAction = None
118 # An action to run MakeIndex (for nomencl) on a file.
121 # An action to run MakeIndex (for glossary) on a file.
122 MakeGlossaryAction = None
124 # Used as a return value of modify_env_var if the variable is not set.
125 _null = SCons.Scanner.LaTeX._null
127 modify_env_var = SCons.Scanner.LaTeX.modify_env_var
129 def FindFile(name,suffixes,paths,env):
130 #print "FindFile: name %s, suffixes "% name,suffixes," paths ",paths
132 print " searching for '%s' with extensions: " % name,suffixes
135 testName = os.path.join(path,name)
137 print " look for '%s'" % testName
138 if os.path.exists(testName):
140 print " found '%s'" % testName
141 return env.fs.File(testName)
143 name_ext = SCons.Util.splitext(testName)[1]
147 # if no suffix try adding those passed in
148 for suffix in suffixes:
149 testNameExt = testName + suffix
151 print " look for '%s'" % testNameExt
153 if os.path.exists(testNameExt):
155 print " found '%s'" % testNameExt
156 return env.fs.File(testNameExt)
158 print " did not find '%s'" % name
161 def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None):
162 """A builder for LaTeX files that checks the output in the aux file
163 and decides how many times to use LaTeXAction, and BibTeXAction."""
165 global must_rerun_latex
167 # This routine is called with two actions. In this file for DVI builds
168 # with LaTeXAction and from the pdflatex.py with PDFLaTeXAction
169 # set this up now for the case where the user requests a different extension
170 # for the target filename
171 if (XXXLaTeXAction == LaTeXAction):
172 callerSuffix = ".dvi"
174 callerSuffix = env['PDFSUFFIX']
176 basename = SCons.Util.splitext(str(source[0]))[0]
177 basedir = os.path.split(str(source[0]))[0]
178 basefile = os.path.split(str(basename))[1]
179 abspath = os.path.abspath(basedir)
180 targetext = os.path.splitext(str(target[0]))[1]
181 targetdir = os.path.split(str(target[0]))[0]
184 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
185 saved_env[var] = modify_env_var(env, var, abspath)
187 # Create base file names with the target directory since the auxiliary files
188 # will be made there. That's because the *COM variables have the cd
189 # command in the prolog. We check
190 # for the existence of files before opening them--even ones like the
191 # aux file that TeX always creates--to make it possible to write tests
192 # with stubs that don't necessarily generate all of the same files.
194 targetbase = os.path.join(targetdir, basefile)
196 # if there is a \makeindex there will be a .idx and thus
197 # we have to run makeindex at least once to keep the build
198 # happy even if there is no index.
199 # Same for glossaries and nomenclature
200 src_content = source[0].get_contents()
201 run_makeindex = makeindex_re.search(src_content) and not os.path.exists(targetbase + '.idx')
202 run_nomenclature = makenomenclature_re.search(src_content) and not os.path.exists(targetbase + '.nlo')
203 run_glossary = makeglossary_re.search(src_content) and not os.path.exists(targetbase + '.glo')
208 for suffix in all_suffixes:
209 theNode = env.fs.File(targetbase + suffix)
210 suffix_nodes[suffix] = theNode
211 saved_hashes[suffix] = theNode.get_csig()
214 print "hashes: ",saved_hashes
216 must_rerun_latex = True
219 # routine to update MD5 hash and compare
221 def check_MD5(filenode, suffix, saved_hashes=saved_hashes):
222 global must_rerun_latex
223 # two calls to clear old csig
224 filenode.clear_memoized_values()
225 filenode.ninfo = filenode.new_ninfo()
226 new_md5 = filenode.get_csig()
228 if saved_hashes[suffix] == new_md5:
230 print "file %s not changed" % (targetbase+suffix)
231 return False # unchanged
232 saved_hashes[suffix] = new_md5
233 must_rerun_latex = True
235 print "file %s changed, rerunning Latex, new hash = " % (targetbase+suffix), new_md5
236 return True # changed
238 # generate the file name that latex will generate
239 resultfilename = targetbase + callerSuffix
243 while (must_rerun_latex and count < int(env.subst('$LATEXRETRIES'))) :
244 result = XXXLaTeXAction(target, source, env)
250 must_rerun_latex = False
251 # Decide if various things need to be run, or run again.
253 # Read the log file to find all .aux files
254 logfilename = targetbase + '.log'
257 if os.path.exists(logfilename):
258 logContent = open(logfilename, "rb").read()
259 auxfiles = openout_aux_re.findall(logContent)
261 # Now decide if bibtex will need to be run.
262 # The information that bibtex reads from the .aux file is
263 # pass-independent. If we find (below) that the .bbl file is unchanged,
264 # then the last latex saw a correct bibliography.
265 # Therefore only do this on the first pass
267 for auxfilename in auxfiles:
268 target_aux = os.path.join(targetdir, auxfilename)
269 if os.path.exists(target_aux):
270 content = open(target_aux, "rb").read()
271 if string.find(content, "bibdata") != -1:
273 print "Need to run bibtex"
274 bibfile = env.fs.File(targetbase)
275 result = BibTeXAction(bibfile, bibfile, env)
278 must_rerun_latex = check_MD5(suffix_nodes['.bbl'],'.bbl')
281 # Now decide if latex will need to be run again due to index.
282 if check_MD5(suffix_nodes['.idx'],'.idx') or (count == 1 and run_makeindex):
283 # We must run makeindex
285 print "Need to run makeindex"
286 idxfile = suffix_nodes['.idx']
287 result = MakeIndexAction(idxfile, idxfile, env)
291 # TO-DO: need to add a way for the user to extend this list for whatever
292 # auxiliary files they create in other (or their own) packages
293 # Harder is case is where an action needs to be called -- that should be rare (I hope?)
295 for index in check_suffixes:
296 check_MD5(suffix_nodes[index],index)
298 # Now decide if latex will need to be run again due to nomenclature.
299 if check_MD5(suffix_nodes['.nlo'],'.nlo') or (count == 1 and run_nomenclature):
300 # We must run makeindex
302 print "Need to run makeindex for nomenclature"
303 nclfile = suffix_nodes['.nlo']
304 result = MakeNclAction(nclfile, nclfile, env)
308 # Now decide if latex will need to be run again due to glossary.
309 if check_MD5(suffix_nodes['.glo'],'.glo') or (count == 1 and run_glossary):
310 # We must run makeindex
312 print "Need to run makeindex for glossary"
313 glofile = suffix_nodes['.glo']
314 result = MakeGlossaryAction(glofile, glofile, env)
318 # Now decide if latex needs to be run yet again to resolve warnings.
319 if warning_rerun_re.search(logContent):
320 must_rerun_latex = True
322 print "rerun Latex due to latex or package rerun warning"
324 if rerun_citations_re.search(logContent):
325 must_rerun_latex = True
327 print "rerun Latex due to 'Rerun to get citations correct' warning"
329 if undefined_references_re.search(logContent):
330 must_rerun_latex = True
332 print "rerun Latex due to undefined references or citations"
334 if (count >= int(env.subst('$LATEXRETRIES')) and must_rerun_latex):
335 print "reached max number of retries on Latex ,",int(env.subst('$LATEXRETRIES'))
338 # rename Latex's output to what the target name is
339 if not (str(target[0]) == resultfilename and os.path.exists(resultfilename)):
340 if os.path.exists(resultfilename):
341 print "move %s to %s" % (resultfilename, str(target[0]), )
342 shutil.move(resultfilename,str(target[0]))
344 # Original comment (when TEXPICTS was not restored):
345 # The TEXPICTS enviroment variable is needed by a dvi -> pdf step
346 # later on Mac OSX so leave it
348 # It is also used when searching for pictures (implicit dependencies).
349 # Why not set the variable again in the respective builder instead
350 # of leaving local modifications in the environment? What if multiple
351 # latex builds in different directories need different TEXPICTS?
352 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
353 if var == 'TEXPICTS':
355 if saved_env[var] is _null:
361 env['ENV'][var] = saved_env[var]
365 def LaTeXAuxAction(target = None, source= None, env=None):
366 result = InternalLaTeXAuxAction( LaTeXAction, target, source, env )
369 LaTeX_re = re.compile("\\\\document(style|class)")
372 # Scan a file list to decide if it's TeX- or LaTeX-flavored.
374 content = f.get_contents()
375 if LaTeX_re.search(content):
379 def TeXLaTeXFunction(target = None, source= None, env=None):
380 """A builder for TeX and LaTeX that scans the source file to
381 decide the "flavor" of the source and then executes the appropriate
384 result = LaTeXAuxAction(target,source,env)
386 result = TeXAction(target,source,env)
389 def TeXLaTeXStrFunction(target = None, source= None, env=None):
390 """A strfunction for TeX and LaTeX that scans the source file to
391 decide the "flavor" of the source and then returns the appropriate
393 if env.GetOption("no_exec"):
395 result = env.subst('$LATEXCOM',0,target,source)+" ..."
397 result = env.subst("$TEXCOM",0,target,source)+" ..."
402 def tex_eps_emitter(target, source, env):
403 """An emitter for TeX and LaTeX sources when
404 executing tex or latex. It will accept .ps and .eps
407 (target, source) = tex_emitter_core(target, source, env, TexGraphics)
409 return (target, source)
411 def tex_pdf_emitter(target, source, env):
412 """An emitter for TeX and LaTeX sources when
413 executing pdftex or pdflatex. It will accept graphics
414 files of types .pdf, .jpg, .png, .gif, and .tif
416 (target, source) = tex_emitter_core(target, source, env, LatexGraphics)
418 return (target, source)
420 def tex_emitter_core(target, source, env, graphics_extensions):
421 """An emitter for TeX and LaTeX sources.
422 For LaTeX sources we try and find the common created files that
423 are needed on subsequent runs of latex to finish tables of contents,
424 bibliographies, indices, lists of figures, and hyperlink references.
426 targetbase = SCons.Util.splitext(str(target[0]))[0]
427 basename = SCons.Util.splitext(str(source[0]))[0]
428 basefile = os.path.split(str(basename))[1]
430 basedir = os.path.split(str(source[0]))[0]
431 targetdir = os.path.split(str(target[0]))[0]
432 abspath = os.path.abspath(basedir)
433 target[0].attributes.path = abspath
436 # file names we will make use of in searching the sources and log file
438 emit_suffixes = ['.aux', '.log', '.ilg', '.blg', '.nls', '.nlg', '.gls', '.glg'] + all_suffixes
439 auxfilename = targetbase + '.aux'
440 logfilename = targetbase + '.log'
442 env.SideEffect(auxfilename,target[0])
443 env.SideEffect(logfilename,target[0])
444 env.Clean(target[0],auxfilename)
445 env.Clean(target[0],logfilename)
447 content = source[0].get_contents()
449 idx_exists = os.path.exists(targetbase + '.idx')
450 nlo_exists = os.path.exists(targetbase + '.nlo')
451 glo_exists = os.path.exists(targetbase + '.glo')
453 # set up list with the regular expressions
454 # we use to find features used
455 file_tests_search = [auxfile_re,
465 # set up list with the file suffixes that need emitting
466 # when a feature is found
467 file_tests_suff = [['.aux'],
468 ['.idx', '.ind', '.ilg'],
474 ['.nlo', '.nls', '.nlg'],
475 ['.glo', '.gls', '.glg'],
476 ['.nav', '.snm', '.out', '.toc'] ]
477 # build the list of lists
479 for i in range(len(file_tests_search)):
480 file_tests.append( [file_tests_search[i].search(content), file_tests_suff[i]] )
482 # TO-DO: need to add a way for the user to extend this list for whatever
483 # auxiliary files they create in other (or their own) packages
485 inc_files = [str(source[0]), ]
486 inc_files.extend( include_re.findall(content) )
488 print "files included by '%s': "%source[0],inc_files
489 # inc_files is list of file names as given. need to find them
490 # using TEXINPUTS paths.
492 # get path list from both env['TEXINPUTS'] and env['ENV']['TEXINPUTS']
493 savedpath = modify_env_var(env, 'TEXINPUTS', abspath)
494 paths = env['ENV']['TEXINPUTS']
495 if SCons.Util.is_List(paths):
498 # Split at os.pathsep to convert into absolute path
499 paths = paths.split(os.pathsep)
501 # now that we have the path list restore the env
502 if savedpath is _null:
504 del env['ENV']['TEXINPUTS']
508 env['ENV']['TEXINPUTS'] = savedpath
510 print "search path ",paths
512 # search all files read (including the original source file)
513 # for files that are \input or \include
514 for src in inc_files:
515 # we already did this for the base file
516 if src != inc_files[0]:
518 srcNode = FindFile(src,['.tex','.ltx','.latex'],paths,env)
520 content = srcNode.get_contents()
521 for i in range(len(file_tests_search)):
522 if file_tests[i][0] == None:
523 file_tests[i][0] = file_tests_search[i].search(content)
525 # For each file see if any graphics files are included
526 # and set up target to create ,pdf graphic
527 # is this is in pdflatex toolchain
528 src_inc_files = includegraphics_re.findall(content)
530 print "graphics files in '%s': "%basename,src_inc_files
531 for graphFile in src_inc_files:
532 graphicNode = FindFile(graphFile,graphics_extensions,paths,env)
533 # see if we can build this graphics file by epstopdf
534 graphicSrc = FindFile(graphFile,TexGraphics,paths,env)
535 if graphicSrc != None:
536 if Verbose and (graphicNode == None):
537 print "need to build '%s' by epstopdf %s -o %s" % (graphFile,graphicSrc,graphFile)
538 graphicNode = env.PDF(os.path.join(targetdir,str(graphicSrc)))
539 env.Depends(target[0],graphicNode)
541 for (theSearch,suffix_list) in file_tests:
543 for suffix in suffix_list:
544 env.SideEffect(targetbase + suffix,target[0])
545 env.Clean(target[0],targetbase + suffix)
547 # read log file to get all other files that latex creates and will read on the next pass
548 if os.path.exists(logfilename):
549 content = open(logfilename, "rb").read()
550 out_files = openout_re.findall(content)
551 env.SideEffect(out_files,target[0])
552 env.Clean(target[0],out_files)
554 return (target, source)
557 TeXLaTeXAction = None
560 """Add Builders and construction variables for TeX to an Environment."""
562 # A generic tex file Action, sufficient for all tex files.
564 if TeXAction is None:
565 TeXAction = SCons.Action.Action("$TEXCOM", "$TEXCOMSTR")
567 # An Action to build a latex file. This might be needed more
568 # than once if we are dealing with labels and bibtex.
570 if LaTeXAction is None:
571 LaTeXAction = SCons.Action.Action("$LATEXCOM", "$LATEXCOMSTR")
573 # Define an action to run BibTeX on a file.
575 if BibTeXAction is None:
576 BibTeXAction = SCons.Action.Action("$BIBTEXCOM", "$BIBTEXCOMSTR")
578 # Define an action to run MakeIndex on a file.
579 global MakeIndexAction
580 if MakeIndexAction is None:
581 MakeIndexAction = SCons.Action.Action("$MAKEINDEXCOM", "$MAKEINDEXCOMSTR")
583 # Define an action to run MakeIndex on a file for nomenclatures.
585 if MakeNclAction is None:
586 MakeNclAction = SCons.Action.Action("$MAKENCLCOM", "$MAKENCLCOMSTR")
588 # Define an action to run MakeIndex on a file for glossaries.
589 global MakeGlossaryAction
590 if MakeGlossaryAction is None:
591 MakeGlossaryAction = SCons.Action.Action("$MAKEGLOSSARYCOM", "$MAKEGLOSSARYCOMSTR")
593 global TeXLaTeXAction
594 if TeXLaTeXAction is None:
595 TeXLaTeXAction = SCons.Action.Action(TeXLaTeXFunction,
596 strfunction=TeXLaTeXStrFunction)
601 bld = env['BUILDERS']['DVI']
602 bld.add_action('.tex', TeXLaTeXAction)
603 bld.add_emitter('.tex', tex_eps_emitter)
606 env['TEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
607 env['TEXCOM'] = 'cd ${TARGET.dir} && $TEX $TEXFLAGS ${SOURCE.file}'
609 # Duplicate from latex.py. If latex.py goes away, then this is still OK.
610 env['LATEX'] = 'latex'
611 env['LATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
612 env['LATEXCOM'] = 'cd ${TARGET.dir} && $LATEX $LATEXFLAGS ${SOURCE.file}'
613 env['LATEXRETRIES'] = 3
615 env['BIBTEX'] = 'bibtex'
616 env['BIBTEXFLAGS'] = SCons.Util.CLVar('')
617 env['BIBTEXCOM'] = 'cd ${TARGET.dir} && $BIBTEX $BIBTEXFLAGS ${SOURCE.filebase}'
619 env['MAKEINDEX'] = 'makeindex'
620 env['MAKEINDEXFLAGS'] = SCons.Util.CLVar('')
621 env['MAKEINDEXCOM'] = 'cd ${TARGET.dir} && $MAKEINDEX $MAKEINDEXFLAGS ${SOURCE.file}'
623 env['MAKEGLOSSARY'] = 'makeindex'
624 env['MAKEGLOSSARYSTYLE'] = '${SOURCE.filebase}.ist'
625 env['MAKEGLOSSARYFLAGS'] = SCons.Util.CLVar('-s ${MAKEGLOSSARYSTYLE} -t ${SOURCE.filebase}.glg')
626 env['MAKEGLOSSARYCOM'] = 'cd ${TARGET.dir} && $MAKEGLOSSARY ${SOURCE.filebase}.glo $MAKEGLOSSARYFLAGS -o ${SOURCE.filebase}.gls'
628 env['MAKENCL'] = 'makeindex'
629 env['MAKENCLSTYLE'] = '$nomencl.ist'
630 env['MAKENCLFLAGS'] = '-s ${MAKENCLSTYLE} -t ${SOURCE.filebase}.nlg'
631 env['MAKENCLCOM'] = 'cd ${TARGET.dir} && $MAKENCL ${SOURCE.filebase}.nlo $MAKENCLFLAGS -o ${SOURCE.filebase}.nls'
633 # Duplicate from pdflatex.py. If latex.py goes away, then this is still OK.
634 env['PDFLATEX'] = 'pdflatex'
635 env['PDFLATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
636 env['PDFLATEXCOM'] = 'cd ${TARGET.dir} && $PDFLATEX $PDFLATEXFLAGS ${SOURCE.file}'
639 return env.Detect('tex')