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,requireExt=False):
131 name,ext = SCons.Util.splitext(name)
132 # if the user gave an extension use it.
136 print " searching for '%s' with extensions: " % name,suffixes
139 testName = os.path.join(path,name)
141 print " look for '%s'" % testName
142 if os.path.exists(testName):
144 print " found '%s'" % testName
145 return env.fs.File(testName)
147 name_ext = SCons.Util.splitext(testName)[1]
151 # if no suffix try adding those passed in
152 for suffix in suffixes:
153 testNameExt = testName + suffix
155 print " look for '%s'" % testNameExt
157 if os.path.exists(testNameExt):
159 print " found '%s'" % testNameExt
160 return env.fs.File(testNameExt)
162 print " did not find '%s'" % name
165 def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None):
166 """A builder for LaTeX files that checks the output in the aux file
167 and decides how many times to use LaTeXAction, and BibTeXAction."""
169 global must_rerun_latex
171 # This routine is called with two actions. In this file for DVI builds
172 # with LaTeXAction and from the pdflatex.py with PDFLaTeXAction
173 # set this up now for the case where the user requests a different extension
174 # for the target filename
175 if (XXXLaTeXAction == LaTeXAction):
176 callerSuffix = ".dvi"
178 callerSuffix = env['PDFSUFFIX']
180 basename = SCons.Util.splitext(str(source[0]))[0]
181 basedir = os.path.split(str(source[0]))[0]
182 basefile = os.path.split(str(basename))[1]
183 abspath = os.path.abspath(basedir)
185 targetext = os.path.splitext(str(target[0]))[1]
186 targetdir = os.path.split(str(target[0]))[0]
189 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
190 saved_env[var] = modify_env_var(env, var, abspath)
192 # Create base file names with the target directory since the auxiliary files
193 # will be made there. That's because the *COM variables have the cd
194 # command in the prolog. We check
195 # for the existence of files before opening them--even ones like the
196 # aux file that TeX always creates--to make it possible to write tests
197 # with stubs that don't necessarily generate all of the same files.
199 targetbase = os.path.join(targetdir, basefile)
201 # if there is a \makeindex there will be a .idx and thus
202 # we have to run makeindex at least once to keep the build
203 # happy even if there is no index.
204 # Same for glossaries and nomenclature
205 src_content = source[0].get_text_contents()
206 run_makeindex = makeindex_re.search(src_content) and not os.path.exists(targetbase + '.idx')
207 run_nomenclature = makenomenclature_re.search(src_content) and not os.path.exists(targetbase + '.nlo')
208 run_glossary = makeglossary_re.search(src_content) and not os.path.exists(targetbase + '.glo')
213 for suffix in all_suffixes:
214 theNode = env.fs.File(targetbase + suffix)
215 suffix_nodes[suffix] = theNode
216 saved_hashes[suffix] = theNode.get_csig()
219 print "hashes: ",saved_hashes
221 must_rerun_latex = True
224 # routine to update MD5 hash and compare
226 # TODO(1.5): nested scopes
227 def check_MD5(filenode, suffix, saved_hashes=saved_hashes, targetbase=targetbase):
228 global must_rerun_latex
229 # two calls to clear old csig
230 filenode.clear_memoized_values()
231 filenode.ninfo = filenode.new_ninfo()
232 new_md5 = filenode.get_csig()
234 if saved_hashes[suffix] == new_md5:
236 print "file %s not changed" % (targetbase+suffix)
237 return False # unchanged
238 saved_hashes[suffix] = new_md5
239 must_rerun_latex = True
241 print "file %s changed, rerunning Latex, new hash = " % (targetbase+suffix), new_md5
242 return True # changed
244 # generate the file name that latex will generate
245 resultfilename = targetbase + callerSuffix
249 while (must_rerun_latex and count < int(env.subst('$LATEXRETRIES'))) :
250 result = XXXLaTeXAction(target, source, env)
256 must_rerun_latex = False
257 # Decide if various things need to be run, or run again.
259 # Read the log file to find all .aux files
260 logfilename = targetbase + '.log'
263 if os.path.exists(logfilename):
264 logContent = open(logfilename, "rb").read()
265 auxfiles = openout_aux_re.findall(logContent)
267 # Now decide if bibtex will need to be run.
268 # The information that bibtex reads from the .aux file is
269 # pass-independent. If we find (below) that the .bbl file is unchanged,
270 # then the last latex saw a correct bibliography.
271 # Therefore only do this on the first pass
273 for auxfilename in auxfiles:
274 target_aux = os.path.join(targetdir, auxfilename)
275 if os.path.exists(target_aux):
276 content = open(target_aux, "rb").read()
277 if string.find(content, "bibdata") != -1:
279 print "Need to run bibtex"
280 bibfile = env.fs.File(targetbase)
281 result = BibTeXAction(bibfile, bibfile, env)
284 must_rerun_latex = check_MD5(suffix_nodes['.bbl'],'.bbl')
287 # Now decide if latex will need to be run again due to index.
288 if check_MD5(suffix_nodes['.idx'],'.idx') or (count == 1 and run_makeindex):
289 # We must run makeindex
291 print "Need to run makeindex"
292 idxfile = suffix_nodes['.idx']
293 result = MakeIndexAction(idxfile, idxfile, env)
297 # TO-DO: need to add a way for the user to extend this list for whatever
298 # auxiliary files they create in other (or their own) packages
299 # Harder is case is where an action needs to be called -- that should be rare (I hope?)
301 for index in check_suffixes:
302 check_MD5(suffix_nodes[index],index)
304 # Now decide if latex will need to be run again due to nomenclature.
305 if check_MD5(suffix_nodes['.nlo'],'.nlo') or (count == 1 and run_nomenclature):
306 # We must run makeindex
308 print "Need to run makeindex for nomenclature"
309 nclfile = suffix_nodes['.nlo']
310 result = MakeNclAction(nclfile, nclfile, env)
314 # Now decide if latex will need to be run again due to glossary.
315 if check_MD5(suffix_nodes['.glo'],'.glo') or (count == 1 and run_glossary):
316 # We must run makeindex
318 print "Need to run makeindex for glossary"
319 glofile = suffix_nodes['.glo']
320 result = MakeGlossaryAction(glofile, glofile, env)
324 # Now decide if latex needs to be run yet again to resolve warnings.
325 if warning_rerun_re.search(logContent):
326 must_rerun_latex = True
328 print "rerun Latex due to latex or package rerun warning"
330 if rerun_citations_re.search(logContent):
331 must_rerun_latex = True
333 print "rerun Latex due to 'Rerun to get citations correct' warning"
335 if undefined_references_re.search(logContent):
336 must_rerun_latex = True
338 print "rerun Latex due to undefined references or citations"
340 if (count >= int(env.subst('$LATEXRETRIES')) and must_rerun_latex):
341 print "reached max number of retries on Latex ,",int(env.subst('$LATEXRETRIES'))
344 # rename Latex's output to what the target name is
345 if not (str(target[0]) == resultfilename and os.path.exists(resultfilename)):
346 if os.path.exists(resultfilename):
347 print "move %s to %s" % (resultfilename, str(target[0]), )
348 shutil.move(resultfilename,str(target[0]))
350 # Original comment (when TEXPICTS was not restored):
351 # The TEXPICTS enviroment variable is needed by a dvi -> pdf step
352 # later on Mac OSX so leave it
354 # It is also used when searching for pictures (implicit dependencies).
355 # Why not set the variable again in the respective builder instead
356 # of leaving local modifications in the environment? What if multiple
357 # latex builds in different directories need different TEXPICTS?
358 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
359 if var == 'TEXPICTS':
361 if saved_env[var] is _null:
367 env['ENV'][var] = saved_env[var]
371 def LaTeXAuxAction(target = None, source= None, env=None):
372 result = InternalLaTeXAuxAction( LaTeXAction, target, source, env )
375 LaTeX_re = re.compile("\\\\document(style|class)")
378 # Scan a file list to decide if it's TeX- or LaTeX-flavored.
380 content = f.get_text_contents()
381 if LaTeX_re.search(content):
385 def TeXLaTeXFunction(target = None, source= None, env=None):
386 """A builder for TeX and LaTeX that scans the source file to
387 decide the "flavor" of the source and then executes the appropriate
390 result = LaTeXAuxAction(target,source,env)
392 result = TeXAction(target,source,env)
395 def TeXLaTeXStrFunction(target = None, source= None, env=None):
396 """A strfunction for TeX and LaTeX that scans the source file to
397 decide the "flavor" of the source and then returns the appropriate
399 if env.GetOption("no_exec"):
401 result = env.subst('$LATEXCOM',0,target,source)+" ..."
403 result = env.subst("$TEXCOM",0,target,source)+" ..."
408 def tex_eps_emitter(target, source, env):
409 """An emitter for TeX and LaTeX sources when
410 executing tex or latex. It will accept .ps and .eps
413 (target, source) = tex_emitter_core(target, source, env, TexGraphics)
415 return (target, source)
417 def tex_pdf_emitter(target, source, env):
418 """An emitter for TeX and LaTeX sources when
419 executing pdftex or pdflatex. It will accept graphics
420 files of types .pdf, .jpg, .png, .gif, and .tif
422 (target, source) = tex_emitter_core(target, source, env, LatexGraphics)
424 return (target, source)
426 def ScanFiles(theFile, target, paths, file_tests, file_tests_search, env, graphics_extensions, targetdir):
427 # for theFile (a Node) update any file_tests and search for graphics files
428 # then find all included files and call ScanFiles for each of them
429 content = theFile.get_text_contents()
431 print " scanning ",str(theFile)
433 for i in range(len(file_tests_search)):
434 if file_tests[i][0] is None:
435 file_tests[i][0] = file_tests_search[i].search(content)
437 # recursively call this on each of the included files
439 inc_files.extend( include_re.findall(content) )
441 print "files included by '%s': "%str(theFile),inc_files
442 # inc_files is list of file names as given. need to find them
443 # using TEXINPUTS paths.
445 for src in inc_files:
446 srcNode = srcNode = FindFile(src,['.tex','.ltx','.latex'],paths,env,requireExt=False)
447 if srcNode is not None:
448 file_test = ScanFiles(srcNode, target, paths, file_tests, file_tests_search, env, graphics_extensions, targetdir)
450 print " done scanning ",str(theFile)
453 def tex_emitter_core(target, source, env, graphics_extensions):
454 """An emitter for TeX and LaTeX sources.
455 For LaTeX sources we try and find the common created files that
456 are needed on subsequent runs of latex to finish tables of contents,
457 bibliographies, indices, lists of figures, and hyperlink references.
459 targetbase = SCons.Util.splitext(str(target[0]))[0]
460 basename = SCons.Util.splitext(str(source[0]))[0]
461 basefile = os.path.split(str(basename))[1]
463 basedir = os.path.split(str(source[0]))[0]
464 targetdir = os.path.split(str(target[0]))[0]
465 abspath = os.path.abspath(basedir)
466 target[0].attributes.path = abspath
469 # file names we will make use of in searching the sources and log file
471 emit_suffixes = ['.aux', '.log', '.ilg', '.blg', '.nls', '.nlg', '.gls', '.glg'] + all_suffixes
472 auxfilename = targetbase + '.aux'
473 logfilename = targetbase + '.log'
475 env.SideEffect(auxfilename,target[0])
476 env.SideEffect(logfilename,target[0])
477 env.Clean(target[0],auxfilename)
478 env.Clean(target[0],logfilename)
480 content = source[0].get_text_contents()
482 idx_exists = os.path.exists(targetbase + '.idx')
483 nlo_exists = os.path.exists(targetbase + '.nlo')
484 glo_exists = os.path.exists(targetbase + '.glo')
486 # set up list with the regular expressions
487 # we use to find features used
488 file_tests_search = [auxfile_re,
498 # set up list with the file suffixes that need emitting
499 # when a feature is found
500 file_tests_suff = [['.aux'],
501 ['.idx', '.ind', '.ilg'],
507 ['.nlo', '.nls', '.nlg'],
508 ['.glo', '.gls', '.glg'],
509 ['.nav', '.snm', '.out', '.toc'] ]
510 # build the list of lists
512 for i in range(len(file_tests_search)):
513 file_tests.append( [None, file_tests_suff[i]] )
515 # TO-DO: need to add a way for the user to extend this list for whatever
516 # auxiliary files they create in other (or their own) packages
518 # get path list from both env['TEXINPUTS'] and env['ENV']['TEXINPUTS']
519 savedpath = modify_env_var(env, 'TEXINPUTS', abspath)
520 paths = env['ENV']['TEXINPUTS']
521 if SCons.Util.is_List(paths):
524 # Split at os.pathsep to convert into absolute path
526 #paths = paths.split(os.pathsep)
527 paths = string.split(paths, os.pathsep)
529 # now that we have the path list restore the env
530 if savedpath is _null:
532 del env['ENV']['TEXINPUTS']
536 env['ENV']['TEXINPUTS'] = savedpath
538 print "search path ",paths
540 file_tests = ScanFiles(source[0], target, paths, file_tests, file_tests_search, env, graphics_extensions, targetdir)
542 for (theSearch,suffix_list) in file_tests:
544 for suffix in suffix_list:
545 env.SideEffect(targetbase + suffix,target[0])
546 env.Clean(target[0],targetbase + suffix)
548 # read log file to get all other files that latex creates and will read on the next pass
549 if os.path.exists(logfilename):
550 content = open(logfilename, "rb").read()
551 out_files = openout_re.findall(content)
552 env.SideEffect(out_files,target[0])
553 env.Clean(target[0],out_files)
555 return (target, source)
558 TeXLaTeXAction = None
561 """Add Builders and construction variables for TeX to an Environment."""
563 # A generic tex file Action, sufficient for all tex files.
565 if TeXAction is None:
566 TeXAction = SCons.Action.Action("$TEXCOM", "$TEXCOMSTR")
568 # An Action to build a latex file. This might be needed more
569 # than once if we are dealing with labels and bibtex.
571 if LaTeXAction is None:
572 LaTeXAction = SCons.Action.Action("$LATEXCOM", "$LATEXCOMSTR")
574 # Define an action to run BibTeX on a file.
576 if BibTeXAction is None:
577 BibTeXAction = SCons.Action.Action("$BIBTEXCOM", "$BIBTEXCOMSTR")
579 # Define an action to run MakeIndex on a file.
580 global MakeIndexAction
581 if MakeIndexAction is None:
582 MakeIndexAction = SCons.Action.Action("$MAKEINDEXCOM", "$MAKEINDEXCOMSTR")
584 # Define an action to run MakeIndex on a file for nomenclatures.
586 if MakeNclAction is None:
587 MakeNclAction = SCons.Action.Action("$MAKENCLCOM", "$MAKENCLCOMSTR")
589 # Define an action to run MakeIndex on a file for glossaries.
590 global MakeGlossaryAction
591 if MakeGlossaryAction is None:
592 MakeGlossaryAction = SCons.Action.Action("$MAKEGLOSSARYCOM", "$MAKEGLOSSARYCOMSTR")
594 global TeXLaTeXAction
595 if TeXLaTeXAction is None:
596 TeXLaTeXAction = SCons.Action.Action(TeXLaTeXFunction,
597 strfunction=TeXLaTeXStrFunction)
602 bld = env['BUILDERS']['DVI']
603 bld.add_action('.tex', TeXLaTeXAction)
604 bld.add_emitter('.tex', tex_eps_emitter)
607 env['TEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
608 env['TEXCOM'] = 'cd ${TARGET.dir} && $TEX $TEXFLAGS ${SOURCE.file}'
610 # Duplicate from latex.py. If latex.py goes away, then this is still OK.
611 env['LATEX'] = 'latex'
612 env['LATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
613 env['LATEXCOM'] = 'cd ${TARGET.dir} && $LATEX $LATEXFLAGS ${SOURCE.file}'
614 env['LATEXRETRIES'] = 3
616 env['BIBTEX'] = 'bibtex'
617 env['BIBTEXFLAGS'] = SCons.Util.CLVar('')
618 env['BIBTEXCOM'] = 'cd ${TARGET.dir} && $BIBTEX $BIBTEXFLAGS ${SOURCE.filebase}'
620 env['MAKEINDEX'] = 'makeindex'
621 env['MAKEINDEXFLAGS'] = SCons.Util.CLVar('')
622 env['MAKEINDEXCOM'] = 'cd ${TARGET.dir} && $MAKEINDEX $MAKEINDEXFLAGS ${SOURCE.file}'
624 env['MAKEGLOSSARY'] = 'makeindex'
625 env['MAKEGLOSSARYSTYLE'] = '${SOURCE.filebase}.ist'
626 env['MAKEGLOSSARYFLAGS'] = SCons.Util.CLVar('-s ${MAKEGLOSSARYSTYLE} -t ${SOURCE.filebase}.glg')
627 env['MAKEGLOSSARYCOM'] = 'cd ${TARGET.dir} && $MAKEGLOSSARY ${SOURCE.filebase}.glo $MAKEGLOSSARYFLAGS -o ${SOURCE.filebase}.gls'
629 env['MAKENCL'] = 'makeindex'
630 env['MAKENCLSTYLE'] = '$nomencl.ist'
631 env['MAKENCLFLAGS'] = '-s ${MAKENCLSTYLE} -t ${SOURCE.filebase}.nlg'
632 env['MAKENCLCOM'] = 'cd ${TARGET.dir} && $MAKENCL ${SOURCE.filebase}.nlo $MAKENCLFLAGS -o ${SOURCE.filebase}.nls'
634 # Duplicate from pdflatex.py. If latex.py goes away, then this is still OK.
635 env['PDFLATEX'] = 'pdflatex'
636 env['PDFLATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
637 env['PDFLATEXCOM'] = 'cd ${TARGET.dir} && $PDFLATEX $PDFLATEXFLAGS ${SOURCE.file}'
640 return env.Detect('tex')
644 # indent-tabs-mode:nil
646 # vim: set expandtab tabstop=4 shiftwidth=4: