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__"
48 must_rerun_latex = True
50 # these are files that just need to be checked for changes and then rerun latex
51 check_suffixes = ['.toc', '.lof', '.lot', '.out', '.nav', '.snm']
53 # these are files that require bibtex or makeindex to be run when they change
54 all_suffixes = check_suffixes + ['.bbl', '.idx', '.nlo', '.glo']
57 # regular expressions used to search for Latex features
58 # or outputs that require rerunning latex
60 # search for all .aux files opened by latex (recorded in the .log file)
61 openout_aux_re = re.compile(r"\\openout.*`(.*\.aux)'")
63 #printindex_re = re.compile(r"^[^%]*\\printindex", re.MULTILINE)
64 #printnomenclature_re = re.compile(r"^[^%]*\\printnomenclature", re.MULTILINE)
65 #printglossary_re = re.compile(r"^[^%]*\\printglossary", re.MULTILINE)
67 # search to find rerun warnings
68 warning_rerun_str = '(^LaTeX Warning:.*Rerun)|(^Package \w+ Warning:.*Rerun)'
69 warning_rerun_re = re.compile(warning_rerun_str, re.MULTILINE)
71 # search to find citation rerun warnings
72 rerun_citations_str = "^LaTeX Warning:.*\n.*Rerun to get citations correct"
73 rerun_citations_re = re.compile(rerun_citations_str, re.MULTILINE)
75 # search to find undefined references or citations warnings
76 undefined_references_str = '(^LaTeX Warning:.*undefined references)|(^Package \w+ Warning:.*undefined citations)'
77 undefined_references_re = re.compile(undefined_references_str, re.MULTILINE)
80 auxfile_re = re.compile(r".", re.MULTILINE)
81 tableofcontents_re = re.compile(r"^[^%]*\\tableofcontents", re.MULTILINE)
82 makeindex_re = re.compile(r"^[^%]*\\makeindex", re.MULTILINE)
83 bibliography_re = re.compile(r"^[^%]*\\bibliography", re.MULTILINE)
84 listoffigures_re = re.compile(r"^[^%]*\\listoffigures", re.MULTILINE)
85 listoftables_re = re.compile(r"^[^%]*\\listoftables", re.MULTILINE)
86 hyperref_re = re.compile(r"^[^%]*\\usepackage.*\{hyperref\}", re.MULTILINE)
87 makenomenclature_re = re.compile(r"^[^%]*\\makenomenclature", re.MULTILINE)
88 makeglossary_re = re.compile(r"^[^%]*\\makeglossary", re.MULTILINE)
89 beamer_re = re.compile(r"^[^%]*\\documentclass\{beamer\}", re.MULTILINE)
91 # search to find all files opened by Latex (recorded in .log file)
92 openout_re = re.compile(r"\\openout.*`(.*)'")
94 # An Action sufficient to build any generic tex file.
97 # An action to build a latex file. This action might be needed more
98 # than once if we are dealing with labels and bibtex.
101 # An action to run BibTeX on a file.
104 # An action to run MakeIndex on a file.
105 MakeIndexAction = None
107 # An action to run MakeIndex (for nomencl) on a file.
110 # An action to run MakeIndex (for glossary) on a file.
111 MakeGlossaryAction = None
113 # Used as a return value of modify_env_var if the variable is not set.
118 # The user specifies the paths in env[variable], similar to other builders.
119 # They may be relative and must be converted to absolute, as expected
120 # by LaTeX and Co. The environment may already have some paths in
121 # env['ENV'][var]. These paths are honored, but the env[var] paths have
122 # higher precedence. All changes are un-done on exit.
123 def modify_env_var(env, var, abspath):
125 save = env['ENV'][var]
128 env.PrependENVPath(var, abspath)
130 if SCons.Util.is_List(env[var]):
131 #TODO(1.5) env.PrependENVPath(var, [os.path.abspath(str(p)) for p in env[var]])
132 env.PrependENVPath(var, map(lambda p: os.path.abspath(str(p)), env[var]))
134 # Split at os.pathsep to convert into absolute path
135 #TODO(1.5) env.PrependENVPath(var, [os.path.abspath(p) for p in str(env[var]).split(os.pathsep)])
136 env.PrependENVPath(var, map(lambda p: os.path.abspath(p), str(env[var]).split(os.pathsep)))
139 # Convert into a string explicitly to append ":" (without which it won't search system
140 # paths as well). The problem is that env.AppendENVPath(var, ":")
141 # does not work, refuses to append ":" (os.pathsep).
142 if SCons.Util.is_List(env['ENV'][var]):
143 env['ENV'][var] = os.pathsep.join(env['ENV'][var])
144 # Append the trailing os.pathsep character here to catch the case with no env[var]
145 env['ENV'][var] = env['ENV'][var] + os.pathsep
148 def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None):
149 """A builder for LaTeX files that checks the output in the aux file
150 and decides how many times to use LaTeXAction, and BibTeXAction."""
152 global must_rerun_latex
154 # This routine is called with two actions. In this file for DVI builds
155 # with LaTeXAction and from the pdflatex.py with PDFLaTeXAction
156 # set this up now for the case where the user requests a different extension
157 # for the target filename
158 if (XXXLaTeXAction == LaTeXAction):
159 callerSuffix = ".dvi"
161 callerSuffix = env['PDFSUFFIX']
163 basename = SCons.Util.splitext(str(source[0]))[0]
164 basedir = os.path.split(str(source[0]))[0]
165 basefile = os.path.split(str(basename))[1]
166 abspath = os.path.abspath(basedir)
167 targetext = os.path.splitext(str(target[0]))[1]
168 targetdir = os.path.split(str(target[0]))[0]
171 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
172 saved_env[var] = modify_env_var(env, var, abspath)
174 # Create base file names with the target directory since the auxiliary files
175 # will be made there. That's because the *COM variables have the cd
176 # command in the prolog. We check
177 # for the existence of files before opening them--even ones like the
178 # aux file that TeX always creates--to make it possible to write tests
179 # with stubs that don't necessarily generate all of the same files.
181 targetbase = os.path.join(targetdir, basefile)
183 # if there is a \makeindex there will be a .idx and thus
184 # we have to run makeindex at least once to keep the build
185 # happy even if there is no index.
186 # Same for glossaries and nomenclature
187 src_content = source[0].get_contents()
188 run_makeindex = makeindex_re.search(src_content) and not os.path.exists(targetbase + '.idx')
189 run_nomenclature = makenomenclature_re.search(src_content) and not os.path.exists(targetbase + '.nlo')
190 run_glossary = makeglossary_re.search(src_content) and not os.path.exists(targetbase + '.glo')
195 for suffix in all_suffixes:
196 theNode = env.fs.File(targetbase + suffix)
197 suffix_nodes[suffix] = theNode
198 saved_hashes[suffix] = theNode.get_csig()
201 print "hashes: ",saved_hashes
203 must_rerun_latex = True
206 # routine to update MD5 hash and compare
208 def check_MD5(filenode, suffix, saved_hashes=saved_hashes):
209 global must_rerun_latex
210 # two calls to clear old csig
211 filenode.clear_memoized_values()
212 filenode.ninfo = filenode.new_ninfo()
213 new_md5 = filenode.get_csig()
215 if saved_hashes[suffix] == new_md5:
217 print "file %s not changed" % (targetbase+suffix)
218 return False # unchanged
219 saved_hashes[suffix] = new_md5
220 must_rerun_latex = True
222 print "file %s changed, rerunning Latex, new hash = " % (targetbase+suffix), new_md5
223 return True # changed
225 # generate the file name that latex will generate
226 resultfilename = targetbase + callerSuffix
230 while (must_rerun_latex and count < int(env.subst('$LATEXRETRIES'))) :
231 result = XXXLaTeXAction(target, source, env)
237 must_rerun_latex = False
238 # Decide if various things need to be run, or run again.
240 # Read the log file to find all .aux files
241 logfilename = targetbase + '.log'
244 if os.path.exists(logfilename):
245 logContent = open(logfilename, "rb").read()
246 auxfiles = openout_aux_re.findall(logContent)
248 # Now decide if bibtex will need to be run.
249 # The information that bibtex reads from the .aux file is
250 # pass-independent. If we find (below) that the .bbl file is unchanged,
251 # then the last latex saw a correct bibliography.
252 # Therefore only do this on the first pass
254 for auxfilename in auxfiles:
255 target_aux = os.path.join(targetdir, auxfilename)
256 if os.path.exists(target_aux):
257 content = open(target_aux, "rb").read()
258 if string.find(content, "bibdata") != -1:
260 print "Need to run bibtex"
261 bibfile = env.fs.File(targetbase)
262 result = BibTeXAction(bibfile, bibfile, env)
265 must_rerun_latex = check_MD5(suffix_nodes['.bbl'],'.bbl')
268 # Now decide if latex will need to be run again due to index.
269 if check_MD5(suffix_nodes['.idx'],'.idx') or (count == 1 and run_makeindex):
270 # We must run makeindex
272 print "Need to run makeindex"
273 idxfile = suffix_nodes['.idx']
274 result = MakeIndexAction(idxfile, idxfile, env)
278 # TO-DO: need to add a way for the user to extend this list for whatever
279 # auxiliary files they create in other (or their own) packages
280 # Harder is case is where an action needs to be called -- that should be rare (I hope?)
282 for index in check_suffixes:
283 check_MD5(suffix_nodes[index],index)
285 # Now decide if latex will need to be run again due to nomenclature.
286 if check_MD5(suffix_nodes['.nlo'],'.nlo') or (count == 1 and run_nomenclature):
287 # We must run makeindex
289 print "Need to run makeindex for nomenclature"
290 nclfile = suffix_nodes['.nlo']
291 result = MakeNclAction(nclfile, nclfile, env)
295 # Now decide if latex will need to be run again due to glossary.
296 if check_MD5(suffix_nodes['.glo'],'.glo') or (count == 1 and run_glossary):
297 # We must run makeindex
299 print "Need to run makeindex for glossary"
300 glofile = suffix_nodes['.glo']
301 result = MakeGlossaryAction(glofile, glofile, env)
305 # Now decide if latex needs to be run yet again to resolve warnings.
306 if warning_rerun_re.search(logContent):
307 must_rerun_latex = True
309 print "rerun Latex due to latex or package rerun warning"
311 if rerun_citations_re.search(logContent):
312 must_rerun_latex = True
314 print "rerun Latex due to 'Rerun to get citations correct' warning"
316 if undefined_references_re.search(logContent):
317 must_rerun_latex = True
319 print "rerun Latex due to undefined references or citations"
321 if (count >= int(env.subst('$LATEXRETRIES')) and must_rerun_latex):
322 print "reached max number of retries on Latex ,",int(env.subst('$LATEXRETRIES'))
325 # rename Latex's output to what the target name is
326 if not (str(target[0]) == resultfilename and os.path.exists(resultfilename)):
327 if os.path.exists(resultfilename):
328 print "move %s to %s" % (resultfilename, str(target[0]), )
329 shutil.move(resultfilename,str(target[0]))
331 # Original comment (when TEXPICTS was not restored):
332 # The TEXPICTS enviroment variable is needed by a dvi -> pdf step
333 # later on Mac OSX so leave it
335 # It is also used when searching for pictures (implicit dependencies).
336 # Why not set the variable again in the respective builder instead
337 # of leaving local modifications in the environment? What if multiple
338 # latex builds in different directories need different TEXPICTS?
339 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
340 if var == 'TEXPICTS':
342 if saved_env[var] is _null:
348 env['ENV'][var] = saved_env[var]
352 def LaTeXAuxAction(target = None, source= None, env=None):
353 result = InternalLaTeXAuxAction( LaTeXAction, target, source, env )
356 LaTeX_re = re.compile("\\\\document(style|class)")
359 # Scan a file list to decide if it's TeX- or LaTeX-flavored.
361 content = f.get_contents()
362 if LaTeX_re.search(content):
366 def TeXLaTeXFunction(target = None, source= None, env=None):
367 """A builder for TeX and LaTeX that scans the source file to
368 decide the "flavor" of the source and then executes the appropriate
371 result = LaTeXAuxAction(target,source,env)
373 result = TeXAction(target,source,env)
376 def TeXLaTeXStrFunction(target = None, source= None, env=None):
377 """A strfunction for TeX and LaTeX that scans the source file to
378 decide the "flavor" of the source and then returns the appropriate
380 if env.GetOption("no_exec"):
382 result = env.subst('$LATEXCOM',0,target,source)+" ..."
384 result = env.subst("$TEXCOM",0,target,source)+" ..."
389 def tex_emitter(target, source, env):
390 """An emitter for TeX and LaTeX sources.
391 For LaTeX sources we try and find the common created files that
392 are needed on subsequent runs of latex to finish tables of contents,
393 bibliographies, indices, lists of figures, and hyperlink references.
395 targetbase = SCons.Util.splitext(str(target[0]))[0]
396 basename = SCons.Util.splitext(str(source[0]))[0]
397 basefile = os.path.split(str(basename))[1]
400 # file names we will make use of in searching the sources and log file
402 emit_suffixes = ['.aux', '.log', '.ilg', '.blg', '.nls', '.nlg', '.gls', '.glg'] + all_suffixes
403 auxfilename = targetbase + '.aux'
404 logfilename = targetbase + '.log'
406 env.SideEffect(auxfilename,target[0])
407 env.SideEffect(logfilename,target[0])
408 env.Clean(target[0],auxfilename)
409 env.Clean(target[0],logfilename)
411 content = source[0].get_contents()
412 idx_exists = os.path.exists(targetbase + '.idx')
413 nlo_exists = os.path.exists(targetbase + '.nlo')
414 glo_exists = os.path.exists(targetbase + '.glo')
416 file_tests = [(auxfile_re.search(content),['.aux']),
417 (makeindex_re.search(content) or idx_exists,['.idx', '.ind', '.ilg']),
418 (bibliography_re.search(content),['.bbl', '.blg']),
419 (tableofcontents_re.search(content),['.toc']),
420 (listoffigures_re.search(content),['.lof']),
421 (listoftables_re.search(content),['.lot']),
422 (hyperref_re.search(content),['.out']),
423 (makenomenclature_re.search(content) or nlo_exists,['.nlo', '.nls', '.nlg']),
424 (makeglossary_re.search(content) or glo_exists,['.glo', '.gls', '.glg']),
425 (beamer_re.search(content),['.nav', '.snm', '.out', '.toc']) ]
426 # Note we add the various makeindex files if the file produced by latex exists (.idx, .glo, .nlo)
427 # This covers the case where the \makeindex, \makenomenclature, or \makeglossary
428 # is not in the main file but we want to clean the files and those made by makeindex
430 # TO-DO: need to add a way for the user to extend this list for whatever
431 # auxiliary files they create in other (or their own) packages
433 for (theSearch,suffix_list) in file_tests:
435 for suffix in suffix_list:
436 env.SideEffect(targetbase + suffix,target[0])
437 env.Clean(target[0],targetbase + suffix)
439 # read log file to get all other files that latex creates and will read on the next pass
440 if os.path.exists(logfilename):
441 content = open(logfilename, "rb").read()
442 out_files = openout_re.findall(content)
443 env.SideEffect(out_files,target[0])
444 env.Clean(target[0],out_files)
446 return (target, source)
449 TeXLaTeXAction = None
452 """Add Builders and construction variables for TeX to an Environment."""
454 # A generic tex file Action, sufficient for all tex files.
456 if TeXAction is None:
457 TeXAction = SCons.Action.Action("$TEXCOM", "$TEXCOMSTR")
459 # An Action to build a latex file. This might be needed more
460 # than once if we are dealing with labels and bibtex.
462 if LaTeXAction is None:
463 LaTeXAction = SCons.Action.Action("$LATEXCOM", "$LATEXCOMSTR")
465 # Define an action to run BibTeX on a file.
467 if BibTeXAction is None:
468 BibTeXAction = SCons.Action.Action("$BIBTEXCOM", "$BIBTEXCOMSTR")
470 # Define an action to run MakeIndex on a file.
471 global MakeIndexAction
472 if MakeIndexAction is None:
473 MakeIndexAction = SCons.Action.Action("$MAKEINDEXCOM", "$MAKEINDEXCOMSTR")
475 # Define an action to run MakeIndex on a file for nomenclatures.
477 if MakeNclAction is None:
478 MakeNclAction = SCons.Action.Action("$MAKENCLCOM", "$MAKENCLCOMSTR")
480 # Define an action to run MakeIndex on a file for glossaries.
481 global MakeGlossaryAction
482 if MakeGlossaryAction is None:
483 MakeGlossaryAction = SCons.Action.Action("$MAKEGLOSSARYCOM", "$MAKEGLOSSARYCOMSTR")
485 global TeXLaTeXAction
486 if TeXLaTeXAction is None:
487 TeXLaTeXAction = SCons.Action.Action(TeXLaTeXFunction,
488 strfunction=TeXLaTeXStrFunction)
493 bld = env['BUILDERS']['DVI']
494 bld.add_action('.tex', TeXLaTeXAction)
495 bld.add_emitter('.tex', tex_emitter)
498 env['TEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
499 env['TEXCOM'] = 'cd ${TARGET.dir} && $TEX $TEXFLAGS ${SOURCE.file}'
501 # Duplicate from latex.py. If latex.py goes away, then this is still OK.
502 env['LATEX'] = 'latex'
503 env['LATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
504 env['LATEXCOM'] = 'cd ${TARGET.dir} && $LATEX $LATEXFLAGS ${SOURCE.file}'
505 env['LATEXRETRIES'] = 3
507 env['BIBTEX'] = 'bibtex'
508 env['BIBTEXFLAGS'] = SCons.Util.CLVar('')
509 env['BIBTEXCOM'] = 'cd ${TARGET.dir} && $BIBTEX $BIBTEXFLAGS ${SOURCE.filebase}'
511 env['MAKEINDEX'] = 'makeindex'
512 env['MAKEINDEXFLAGS'] = SCons.Util.CLVar('')
513 env['MAKEINDEXCOM'] = 'cd ${TARGET.dir} && $MAKEINDEX $MAKEINDEXFLAGS ${SOURCE.file}'
515 env['MAKEGLOSSARY'] = 'makeindex'
516 env['MAKEGLOSSARYSTYLE'] = '${SOURCE.filebase}.ist'
517 env['MAKEGLOSSARYFLAGS'] = SCons.Util.CLVar('-s ${MAKEGLOSSARYSTYLE} -t ${SOURCE.filebase}.glg')
518 env['MAKEGLOSSARYCOM'] = 'cd ${TARGET.dir} && $MAKEGLOSSARY ${SOURCE.filebase}.glo $MAKEGLOSSARYFLAGS -o ${SOURCE.filebase}.gls'
520 env['MAKENCL'] = 'makeindex'
521 env['MAKENCLSTYLE'] = '$nomencl.ist'
522 env['MAKENCLFLAGS'] = '-s ${MAKENCLSTYLE} -t ${SOURCE.filebase}.nlg'
523 env['MAKENCLCOM'] = 'cd ${TARGET.dir} && $MAKENCL ${SOURCE.filebase}.nlo $MAKENCLFLAGS -o ${SOURCE.filebase}.nls'
525 # Duplicate from pdflatex.py. If latex.py goes away, then this is still OK.
526 env['PDFLATEX'] = 'pdflatex'
527 env['PDFLATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
528 env['PDFLATEXCOM'] = 'cd ${TARGET.dir} && $PDFLATEX $PDFLATEXFLAGS ${SOURCE.file}'
531 return env.Detect('tex')