Change FUTURE markers to TODO(1.5) so we are all using the same style.
[scons.git] / src / engine / SCons / Tool / tex.py
1 """SCons.Tool.tex
2
3 Tool-specific initialization for TeX.
4
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()
7 selection method.
8
9 """
10
11 #
12 # __COPYRIGHT__
13 #
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:
21 #
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
24 #
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.
32 #
33
34 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
35
36 import os.path
37 import re
38 import string
39 import shutil
40
41 import SCons.Action
42 import SCons.Node
43 import SCons.Node.FS
44 import SCons.Util
45
46 Verbose = False
47
48 must_rerun_latex = True
49
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']
52
53 # these are files that require bibtex or makeindex to be run when they change
54 all_suffixes = check_suffixes + ['.bbl', '.idx', '.nlo', '.glo']
55
56 #
57 # regular expressions used to search for Latex features
58 # or outputs that require rerunning latex
59 #
60 # search for all .aux files opened by latex (recorded in the .log file)
61 openout_aux_re = re.compile(r"\\openout.*`(.*\.aux)'")
62
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)
66
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)
70
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)
74
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)
78
79 # used by the emitter
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)
90
91 # search to find all files opened by Latex (recorded in .log file)
92 openout_re = re.compile(r"\\openout.*`(.*)'")
93
94 # An Action sufficient to build any generic tex file.
95 TeXAction = None
96
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.
99 LaTeXAction = None
100
101 # An action to run BibTeX on a file.
102 BibTeXAction = None
103
104 # An action to run MakeIndex on a file.
105 MakeIndexAction = None
106
107 # An action to run MakeIndex (for nomencl) on a file.
108 MakeNclAction = None
109
110 # An action to run MakeIndex (for glossary) on a file.
111 MakeGlossaryAction = None
112
113 # Used as a return value of modify_env_var if the variable is not set.
114 class _Null:
115     pass
116 _null = _Null
117
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):
124     try:
125         save = env['ENV'][var]
126     except KeyError:
127         save = _null
128     env.PrependENVPath(var, abspath)
129     try:
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]))
133         else:
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)))
137     except KeyError:
138         pass
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
146     return save
147
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."""
151
152     global must_rerun_latex
153
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"
160     else:
161        callerSuffix = env['PDFSUFFIX']
162
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]
169
170     saved_env = {}
171     for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
172         saved_env[var] = modify_env_var(env, var, abspath)
173
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.
180
181     targetbase = os.path.join(targetdir, basefile)
182
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')
191
192     saved_hashes = {}
193     suffix_nodes = {}
194
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()
199
200     if Verbose:
201         print "hashes: ",saved_hashes
202
203     must_rerun_latex = True
204
205     #
206     # routine to update MD5 hash and compare
207     #
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()
214
215         if saved_hashes[suffix] == new_md5:
216             if Verbose:
217                 print "file %s not changed" % (targetbase+suffix)
218             return False        # unchanged
219         saved_hashes[suffix] = new_md5
220         must_rerun_latex = True
221         if Verbose:
222             print "file %s changed, rerunning Latex, new hash = " % (targetbase+suffix), new_md5
223         return True     # changed
224
225     # generate the file name that latex will generate
226     resultfilename = targetbase + callerSuffix
227
228     count = 0
229
230     while (must_rerun_latex and count < int(env.subst('$LATEXRETRIES'))) :
231         result = XXXLaTeXAction(target, source, env)
232         if result != 0:
233             return result
234
235         count = count + 1
236
237         must_rerun_latex = False
238         # Decide if various things need to be run, or run again.
239
240         # Read the log file to find all .aux files
241         logfilename = targetbase + '.log'
242         logContent = ''
243         auxfiles = []
244         if os.path.exists(logfilename):
245             logContent = open(logfilename, "rb").read()
246             auxfiles = openout_aux_re.findall(logContent)
247
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
253         if count == 1:
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:
259                         if Verbose:
260                             print "Need to run bibtex"
261                         bibfile = env.fs.File(targetbase)
262                         result = BibTeXAction(bibfile, bibfile, env)
263                         if result != 0:
264                             return result
265                         must_rerun_latex = check_MD5(suffix_nodes['.bbl'],'.bbl')
266                         break
267
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
271             if Verbose:
272                 print "Need to run makeindex"
273             idxfile = suffix_nodes['.idx']
274             result = MakeIndexAction(idxfile, idxfile, env)
275             if result != 0:
276                 return result
277
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?)
281
282         for index in check_suffixes:
283             check_MD5(suffix_nodes[index],index)
284
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
288             if Verbose:
289                 print "Need to run makeindex for nomenclature"
290             nclfile = suffix_nodes['.nlo']
291             result = MakeNclAction(nclfile, nclfile, env)
292             if result != 0:
293                 return result
294
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
298             if Verbose:
299                 print "Need to run makeindex for glossary"
300             glofile = suffix_nodes['.glo']
301             result = MakeGlossaryAction(glofile, glofile, env)
302             if result != 0:
303                 return result
304
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
308             if Verbose:
309                 print "rerun Latex due to latex or package rerun warning"
310
311         if rerun_citations_re.search(logContent):
312             must_rerun_latex = True
313             if Verbose:
314                 print "rerun Latex due to 'Rerun to get citations correct' warning"
315
316         if undefined_references_re.search(logContent):
317             must_rerun_latex = True
318             if Verbose:
319                 print "rerun Latex due to undefined references or citations"
320
321         if (count >= int(env.subst('$LATEXRETRIES')) and must_rerun_latex):
322             print "reached max number of retries on Latex ,",int(env.subst('$LATEXRETRIES'))
323 # end of while loop
324
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]))
330
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
334     #
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':
341             continue
342         if saved_env[var] is _null:
343             try:
344                 del env['ENV'][var]
345             except KeyError:
346                 pass # was never set
347         else:
348             env['ENV'][var] = saved_env[var]
349
350     return result
351
352 def LaTeXAuxAction(target = None, source= None, env=None):
353     result = InternalLaTeXAuxAction( LaTeXAction, target, source, env )
354     return result
355
356 LaTeX_re = re.compile("\\\\document(style|class)")
357
358 def is_LaTeX(flist):
359     # Scan a file list to decide if it's TeX- or LaTeX-flavored.
360     for f in flist:
361         content = f.get_contents()
362         if LaTeX_re.search(content):
363             return 1
364     return 0
365
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
369     program."""
370     if is_LaTeX(source):
371         result = LaTeXAuxAction(target,source,env)
372     else:
373         result = TeXAction(target,source,env)
374     return result
375
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
379     command string."""
380     if env.GetOption("no_exec"):
381         if is_LaTeX(source):
382             result = env.subst('$LATEXCOM',0,target,source)+" ..."
383         else:
384             result = env.subst("$TEXCOM",0,target,source)+" ..."
385     else:
386         result = ''
387     return result
388
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.
394     """
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]
398
399     #
400     # file names we will make use of in searching the sources and log file
401     #
402     emit_suffixes = ['.aux', '.log', '.ilg', '.blg', '.nls', '.nlg', '.gls', '.glg'] + all_suffixes
403     auxfilename = targetbase + '.aux'
404     logfilename = targetbase + '.log'
405
406     env.SideEffect(auxfilename,target[0])
407     env.SideEffect(logfilename,target[0])
408     env.Clean(target[0],auxfilename)
409     env.Clean(target[0],logfilename)
410
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')
415
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
429
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
432
433     for (theSearch,suffix_list) in file_tests:
434         if theSearch:
435             for suffix in suffix_list:
436                 env.SideEffect(targetbase + suffix,target[0])
437                 env.Clean(target[0],targetbase + suffix)
438
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)
445
446     return (target, source)
447
448
449 TeXLaTeXAction = None
450
451 def generate(env):
452     """Add Builders and construction variables for TeX to an Environment."""
453
454     # A generic tex file Action, sufficient for all tex files.
455     global TeXAction
456     if TeXAction is None:
457         TeXAction = SCons.Action.Action("$TEXCOM", "$TEXCOMSTR")
458
459     # An Action to build a latex file.  This might be needed more
460     # than once if we are dealing with labels and bibtex.
461     global LaTeXAction
462     if LaTeXAction is None:
463         LaTeXAction = SCons.Action.Action("$LATEXCOM", "$LATEXCOMSTR")
464
465     # Define an action to run BibTeX on a file.
466     global BibTeXAction
467     if BibTeXAction is None:
468         BibTeXAction = SCons.Action.Action("$BIBTEXCOM", "$BIBTEXCOMSTR")
469
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")
474
475     # Define an action to run MakeIndex on a file for nomenclatures.
476     global MakeNclAction
477     if MakeNclAction is None:
478         MakeNclAction = SCons.Action.Action("$MAKENCLCOM", "$MAKENCLCOMSTR")
479
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")
484
485     global TeXLaTeXAction
486     if TeXLaTeXAction is None:
487         TeXLaTeXAction = SCons.Action.Action(TeXLaTeXFunction,
488                               strfunction=TeXLaTeXStrFunction)
489
490     import dvi
491     dvi.generate(env)
492
493     bld = env['BUILDERS']['DVI']
494     bld.add_action('.tex', TeXLaTeXAction)
495     bld.add_emitter('.tex', tex_emitter)
496
497     env['TEX']      = 'tex'
498     env['TEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
499     env['TEXCOM']   = 'cd ${TARGET.dir} && $TEX $TEXFLAGS ${SOURCE.file}'
500
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
506
507     env['BIBTEX']      = 'bibtex'
508     env['BIBTEXFLAGS'] = SCons.Util.CLVar('')
509     env['BIBTEXCOM']   = 'cd ${TARGET.dir} && $BIBTEX $BIBTEXFLAGS ${SOURCE.filebase}'
510
511     env['MAKEINDEX']      = 'makeindex'
512     env['MAKEINDEXFLAGS'] = SCons.Util.CLVar('')
513     env['MAKEINDEXCOM']   = 'cd ${TARGET.dir} && $MAKEINDEX $MAKEINDEXFLAGS ${SOURCE.file}'
514
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'
519
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'
524
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}'
529
530 def exists(env):
531     return env.Detect('tex')