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()
12 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 The SCons Foundation
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__ = "src/engine/SCons/Tool/tex.py 3842 2008/12/20 22:59:52 scons"
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 = SCons.Util.splitext(name)[0]
133 print " searching for '%s' with extensions: " % name,suffixes
136 testName = os.path.join(path,name)
138 print " look for '%s'" % testName
139 if os.path.exists(testName):
141 print " found '%s'" % testName
142 return env.fs.File(testName)
144 name_ext = SCons.Util.splitext(testName)[1]
148 # if no suffix try adding those passed in
149 for suffix in suffixes:
150 testNameExt = testName + suffix
152 print " look for '%s'" % testNameExt
154 if os.path.exists(testNameExt):
156 print " found '%s'" % testNameExt
157 return env.fs.File(testNameExt)
159 print " did not find '%s'" % name
162 def InternalLaTeXAuxAction(XXXLaTeXAction, target = None, source= None, env=None):
163 """A builder for LaTeX files that checks the output in the aux file
164 and decides how many times to use LaTeXAction, and BibTeXAction."""
166 global must_rerun_latex
168 # This routine is called with two actions. In this file for DVI builds
169 # with LaTeXAction and from the pdflatex.py with PDFLaTeXAction
170 # set this up now for the case where the user requests a different extension
171 # for the target filename
172 if (XXXLaTeXAction == LaTeXAction):
173 callerSuffix = ".dvi"
175 callerSuffix = env['PDFSUFFIX']
177 basename = SCons.Util.splitext(str(source[0]))[0]
178 basedir = os.path.split(str(source[0]))[0]
179 basefile = os.path.split(str(basename))[1]
180 abspath = os.path.abspath(basedir)
181 targetext = os.path.splitext(str(target[0]))[1]
182 targetdir = os.path.split(str(target[0]))[0]
185 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
186 saved_env[var] = modify_env_var(env, var, abspath)
188 # Create base file names with the target directory since the auxiliary files
189 # will be made there. That's because the *COM variables have the cd
190 # command in the prolog. We check
191 # for the existence of files before opening them--even ones like the
192 # aux file that TeX always creates--to make it possible to write tests
193 # with stubs that don't necessarily generate all of the same files.
195 targetbase = os.path.join(targetdir, basefile)
197 # if there is a \makeindex there will be a .idx and thus
198 # we have to run makeindex at least once to keep the build
199 # happy even if there is no index.
200 # Same for glossaries and nomenclature
201 src_content = source[0].get_contents()
202 run_makeindex = makeindex_re.search(src_content) and not os.path.exists(targetbase + '.idx')
203 run_nomenclature = makenomenclature_re.search(src_content) and not os.path.exists(targetbase + '.nlo')
204 run_glossary = makeglossary_re.search(src_content) and not os.path.exists(targetbase + '.glo')
209 for suffix in all_suffixes:
210 theNode = env.fs.File(targetbase + suffix)
211 suffix_nodes[suffix] = theNode
212 saved_hashes[suffix] = theNode.get_csig()
215 print "hashes: ",saved_hashes
217 must_rerun_latex = True
220 # routine to update MD5 hash and compare
222 # TODO(1.5): nested scopes
223 def check_MD5(filenode, suffix, saved_hashes=saved_hashes, targetbase=targetbase):
224 global must_rerun_latex
225 # two calls to clear old csig
226 filenode.clear_memoized_values()
227 filenode.ninfo = filenode.new_ninfo()
228 new_md5 = filenode.get_csig()
230 if saved_hashes[suffix] == new_md5:
232 print "file %s not changed" % (targetbase+suffix)
233 return False # unchanged
234 saved_hashes[suffix] = new_md5
235 must_rerun_latex = True
237 print "file %s changed, rerunning Latex, new hash = " % (targetbase+suffix), new_md5
238 return True # changed
240 # generate the file name that latex will generate
241 resultfilename = targetbase + callerSuffix
245 while (must_rerun_latex and count < int(env.subst('$LATEXRETRIES'))) :
246 result = XXXLaTeXAction(target, source, env)
252 must_rerun_latex = False
253 # Decide if various things need to be run, or run again.
255 # Read the log file to find all .aux files
256 logfilename = targetbase + '.log'
259 if os.path.exists(logfilename):
260 logContent = open(logfilename, "rb").read()
261 auxfiles = openout_aux_re.findall(logContent)
263 # Now decide if bibtex will need to be run.
264 # The information that bibtex reads from the .aux file is
265 # pass-independent. If we find (below) that the .bbl file is unchanged,
266 # then the last latex saw a correct bibliography.
267 # Therefore only do this on the first pass
269 for auxfilename in auxfiles:
270 target_aux = os.path.join(targetdir, auxfilename)
271 if os.path.exists(target_aux):
272 content = open(target_aux, "rb").read()
273 if string.find(content, "bibdata") != -1:
275 print "Need to run bibtex"
276 bibfile = env.fs.File(targetbase)
277 result = BibTeXAction(bibfile, bibfile, env)
280 must_rerun_latex = check_MD5(suffix_nodes['.bbl'],'.bbl')
283 # Now decide if latex will need to be run again due to index.
284 if check_MD5(suffix_nodes['.idx'],'.idx') or (count == 1 and run_makeindex):
285 # We must run makeindex
287 print "Need to run makeindex"
288 idxfile = suffix_nodes['.idx']
289 result = MakeIndexAction(idxfile, idxfile, env)
293 # TO-DO: need to add a way for the user to extend this list for whatever
294 # auxiliary files they create in other (or their own) packages
295 # Harder is case is where an action needs to be called -- that should be rare (I hope?)
297 for index in check_suffixes:
298 check_MD5(suffix_nodes[index],index)
300 # Now decide if latex will need to be run again due to nomenclature.
301 if check_MD5(suffix_nodes['.nlo'],'.nlo') or (count == 1 and run_nomenclature):
302 # We must run makeindex
304 print "Need to run makeindex for nomenclature"
305 nclfile = suffix_nodes['.nlo']
306 result = MakeNclAction(nclfile, nclfile, env)
310 # Now decide if latex will need to be run again due to glossary.
311 if check_MD5(suffix_nodes['.glo'],'.glo') or (count == 1 and run_glossary):
312 # We must run makeindex
314 print "Need to run makeindex for glossary"
315 glofile = suffix_nodes['.glo']
316 result = MakeGlossaryAction(glofile, glofile, env)
320 # Now decide if latex needs to be run yet again to resolve warnings.
321 if warning_rerun_re.search(logContent):
322 must_rerun_latex = True
324 print "rerun Latex due to latex or package rerun warning"
326 if rerun_citations_re.search(logContent):
327 must_rerun_latex = True
329 print "rerun Latex due to 'Rerun to get citations correct' warning"
331 if undefined_references_re.search(logContent):
332 must_rerun_latex = True
334 print "rerun Latex due to undefined references or citations"
336 if (count >= int(env.subst('$LATEXRETRIES')) and must_rerun_latex):
337 print "reached max number of retries on Latex ,",int(env.subst('$LATEXRETRIES'))
340 # rename Latex's output to what the target name is
341 if not (str(target[0]) == resultfilename and os.path.exists(resultfilename)):
342 if os.path.exists(resultfilename):
343 print "move %s to %s" % (resultfilename, str(target[0]), )
344 shutil.move(resultfilename,str(target[0]))
346 # Original comment (when TEXPICTS was not restored):
347 # The TEXPICTS enviroment variable is needed by a dvi -> pdf step
348 # later on Mac OSX so leave it
350 # It is also used when searching for pictures (implicit dependencies).
351 # Why not set the variable again in the respective builder instead
352 # of leaving local modifications in the environment? What if multiple
353 # latex builds in different directories need different TEXPICTS?
354 for var in SCons.Scanner.LaTeX.LaTeX.env_variables:
355 if var == 'TEXPICTS':
357 if saved_env[var] is _null:
363 env['ENV'][var] = saved_env[var]
367 def LaTeXAuxAction(target = None, source= None, env=None):
368 result = InternalLaTeXAuxAction( LaTeXAction, target, source, env )
371 LaTeX_re = re.compile("\\\\document(style|class)")
374 # Scan a file list to decide if it's TeX- or LaTeX-flavored.
376 content = f.get_contents()
377 if LaTeX_re.search(content):
381 def TeXLaTeXFunction(target = None, source= None, env=None):
382 """A builder for TeX and LaTeX that scans the source file to
383 decide the "flavor" of the source and then executes the appropriate
386 result = LaTeXAuxAction(target,source,env)
388 result = TeXAction(target,source,env)
391 def TeXLaTeXStrFunction(target = None, source= None, env=None):
392 """A strfunction for TeX and LaTeX that scans the source file to
393 decide the "flavor" of the source and then returns the appropriate
395 if env.GetOption("no_exec"):
397 result = env.subst('$LATEXCOM',0,target,source)+" ..."
399 result = env.subst("$TEXCOM",0,target,source)+" ..."
404 def tex_eps_emitter(target, source, env):
405 """An emitter for TeX and LaTeX sources when
406 executing tex or latex. It will accept .ps and .eps
409 (target, source) = tex_emitter_core(target, source, env, TexGraphics)
411 return (target, source)
413 def tex_pdf_emitter(target, source, env):
414 """An emitter for TeX and LaTeX sources when
415 executing pdftex or pdflatex. It will accept graphics
416 files of types .pdf, .jpg, .png, .gif, and .tif
418 (target, source) = tex_emitter_core(target, source, env, LatexGraphics)
420 return (target, source)
422 def ScanFiles(theFile, target, paths, file_tests, file_tests_search, env, graphics_extensions, targetdir):
423 # for theFile (a Node) update any file_tests and search for graphics files
424 # then find all included files and call ScanFiles for each of them
425 content = theFile.get_contents()
427 print " scanning ",str(theFile)
429 for i in range(len(file_tests_search)):
430 if file_tests[i][0] == None:
431 file_tests[i][0] = file_tests_search[i].search(content)
433 # For each file see if any graphics files are included
434 # and set up target to create ,pdf graphic
435 # is this is in pdflatex toolchain
436 graphic_files = includegraphics_re.findall(content)
438 print "graphics files in '%s': "%str(theFile),graphic_files
439 for graphFile in graphic_files:
440 graphicNode = FindFile(graphFile,graphics_extensions,paths,env,requireExt=True)
441 # if building with pdflatex see if we need to build the .pdf version of the graphic file
442 # I should probably come up with a better way to tell which builder we are using.
443 if graphics_extensions == LatexGraphics:
444 # see if we can build this graphics file by epstopdf
445 graphicSrc = FindFile(graphFile,TexGraphics,paths,env,requireExt=True)
446 # it seems that FindFile checks with no extension added
447 # so if the extension is included in the name then both searches find it
448 # we don't want to try to build a .pdf from a .pdf so make sure src!=file wanted
449 if (graphicSrc != None) and (graphicSrc != graphicNode):
451 if graphicNode == None:
452 print "need to build '%s' by epstopdf %s -o %s" % (graphFile,graphicSrc,graphFile)
454 print "no need to build '%s', but source file %s exists" % (graphicNode,graphicSrc)
455 graphicNode = env.PDF(graphicSrc)
456 env.Depends(target[0],graphicNode)
458 # recursively call this on each of the included files
460 inc_files.extend( include_re.findall(content) )
462 print "files included by '%s': "%str(theFile),inc_files
463 # inc_files is list of file names as given. need to find them
464 # using TEXINPUTS paths.
466 for src in inc_files:
467 srcNode = srcNode = FindFile(src,['.tex','.ltx','.latex'],paths,env,requireExt=False)
469 file_test = ScanFiles(srcNode, target, paths, file_tests, file_tests_search, env, graphics_extensions, targetdir)
471 print " done scanning ",str(theFile)
474 def tex_emitter_core(target, source, env, graphics_extensions):
475 """An emitter for TeX and LaTeX sources.
476 For LaTeX sources we try and find the common created files that
477 are needed on subsequent runs of latex to finish tables of contents,
478 bibliographies, indices, lists of figures, and hyperlink references.
480 targetbase = SCons.Util.splitext(str(target[0]))[0]
481 basename = SCons.Util.splitext(str(source[0]))[0]
482 basefile = os.path.split(str(basename))[1]
484 basedir = os.path.split(str(source[0]))[0]
485 targetdir = os.path.split(str(target[0]))[0]
486 abspath = os.path.abspath(basedir)
487 target[0].attributes.path = abspath
490 # file names we will make use of in searching the sources and log file
492 emit_suffixes = ['.aux', '.log', '.ilg', '.blg', '.nls', '.nlg', '.gls', '.glg'] + all_suffixes
493 auxfilename = targetbase + '.aux'
494 logfilename = targetbase + '.log'
496 env.SideEffect(auxfilename,target[0])
497 env.SideEffect(logfilename,target[0])
498 env.Clean(target[0],auxfilename)
499 env.Clean(target[0],logfilename)
501 content = source[0].get_contents()
503 idx_exists = os.path.exists(targetbase + '.idx')
504 nlo_exists = os.path.exists(targetbase + '.nlo')
505 glo_exists = os.path.exists(targetbase + '.glo')
507 # set up list with the regular expressions
508 # we use to find features used
509 file_tests_search = [auxfile_re,
519 # set up list with the file suffixes that need emitting
520 # when a feature is found
521 file_tests_suff = [['.aux'],
522 ['.idx', '.ind', '.ilg'],
528 ['.nlo', '.nls', '.nlg'],
529 ['.glo', '.gls', '.glg'],
530 ['.nav', '.snm', '.out', '.toc'] ]
531 # build the list of lists
533 for i in range(len(file_tests_search)):
534 file_tests.append( [None, file_tests_suff[i]] )
536 # TO-DO: need to add a way for the user to extend this list for whatever
537 # auxiliary files they create in other (or their own) packages
539 # get path list from both env['TEXINPUTS'] and env['ENV']['TEXINPUTS']
540 savedpath = modify_env_var(env, 'TEXINPUTS', abspath)
541 paths = env['ENV']['TEXINPUTS']
542 if SCons.Util.is_List(paths):
545 # Split at os.pathsep to convert into absolute path
547 #paths = paths.split(os.pathsep)
548 paths = string.split(paths, os.pathsep)
550 # now that we have the path list restore the env
551 if savedpath is _null:
553 del env['ENV']['TEXINPUTS']
557 env['ENV']['TEXINPUTS'] = savedpath
559 print "search path ",paths
561 file_tests = ScanFiles(source[0], target, paths, file_tests, file_tests_search, env, graphics_extensions, targetdir)
563 for (theSearch,suffix_list) in file_tests:
565 for suffix in suffix_list:
566 env.SideEffect(targetbase + suffix,target[0])
567 env.Clean(target[0],targetbase + suffix)
569 # read log file to get all other files that latex creates and will read on the next pass
570 if os.path.exists(logfilename):
571 content = open(logfilename, "rb").read()
572 out_files = openout_re.findall(content)
573 env.SideEffect(out_files,target[0])
574 env.Clean(target[0],out_files)
576 return (target, source)
579 TeXLaTeXAction = None
582 """Add Builders and construction variables for TeX to an Environment."""
584 # A generic tex file Action, sufficient for all tex files.
586 if TeXAction is None:
587 TeXAction = SCons.Action.Action("$TEXCOM", "$TEXCOMSTR")
589 # An Action to build a latex file. This might be needed more
590 # than once if we are dealing with labels and bibtex.
592 if LaTeXAction is None:
593 LaTeXAction = SCons.Action.Action("$LATEXCOM", "$LATEXCOMSTR")
595 # Define an action to run BibTeX on a file.
597 if BibTeXAction is None:
598 BibTeXAction = SCons.Action.Action("$BIBTEXCOM", "$BIBTEXCOMSTR")
600 # Define an action to run MakeIndex on a file.
601 global MakeIndexAction
602 if MakeIndexAction is None:
603 MakeIndexAction = SCons.Action.Action("$MAKEINDEXCOM", "$MAKEINDEXCOMSTR")
605 # Define an action to run MakeIndex on a file for nomenclatures.
607 if MakeNclAction is None:
608 MakeNclAction = SCons.Action.Action("$MAKENCLCOM", "$MAKENCLCOMSTR")
610 # Define an action to run MakeIndex on a file for glossaries.
611 global MakeGlossaryAction
612 if MakeGlossaryAction is None:
613 MakeGlossaryAction = SCons.Action.Action("$MAKEGLOSSARYCOM", "$MAKEGLOSSARYCOMSTR")
615 global TeXLaTeXAction
616 if TeXLaTeXAction is None:
617 TeXLaTeXAction = SCons.Action.Action(TeXLaTeXFunction,
618 strfunction=TeXLaTeXStrFunction)
623 bld = env['BUILDERS']['DVI']
624 bld.add_action('.tex', TeXLaTeXAction)
625 bld.add_emitter('.tex', tex_eps_emitter)
628 env['TEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
629 env['TEXCOM'] = 'cd ${TARGET.dir} && $TEX $TEXFLAGS ${SOURCE.file}'
631 # Duplicate from latex.py. If latex.py goes away, then this is still OK.
632 env['LATEX'] = 'latex'
633 env['LATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
634 env['LATEXCOM'] = 'cd ${TARGET.dir} && $LATEX $LATEXFLAGS ${SOURCE.file}'
635 env['LATEXRETRIES'] = 3
637 env['BIBTEX'] = 'bibtex'
638 env['BIBTEXFLAGS'] = SCons.Util.CLVar('')
639 env['BIBTEXCOM'] = 'cd ${TARGET.dir} && $BIBTEX $BIBTEXFLAGS ${SOURCE.filebase}'
641 env['MAKEINDEX'] = 'makeindex'
642 env['MAKEINDEXFLAGS'] = SCons.Util.CLVar('')
643 env['MAKEINDEXCOM'] = 'cd ${TARGET.dir} && $MAKEINDEX $MAKEINDEXFLAGS ${SOURCE.file}'
645 env['MAKEGLOSSARY'] = 'makeindex'
646 env['MAKEGLOSSARYSTYLE'] = '${SOURCE.filebase}.ist'
647 env['MAKEGLOSSARYFLAGS'] = SCons.Util.CLVar('-s ${MAKEGLOSSARYSTYLE} -t ${SOURCE.filebase}.glg')
648 env['MAKEGLOSSARYCOM'] = 'cd ${TARGET.dir} && $MAKEGLOSSARY ${SOURCE.filebase}.glo $MAKEGLOSSARYFLAGS -o ${SOURCE.filebase}.gls'
650 env['MAKENCL'] = 'makeindex'
651 env['MAKENCLSTYLE'] = '$nomencl.ist'
652 env['MAKENCLFLAGS'] = '-s ${MAKENCLSTYLE} -t ${SOURCE.filebase}.nlg'
653 env['MAKENCLCOM'] = 'cd ${TARGET.dir} && $MAKENCL ${SOURCE.filebase}.nlo $MAKENCLFLAGS -o ${SOURCE.filebase}.nls'
655 # Duplicate from pdflatex.py. If latex.py goes away, then this is still OK.
656 env['PDFLATEX'] = 'pdflatex'
657 env['PDFLATEXFLAGS'] = SCons.Util.CLVar('-interaction=nonstopmode')
658 env['PDFLATEXCOM'] = 'cd ${TARGET.dir} && $PDFLATEX $PDFLATEXFLAGS ${SOURCE.file}'
661 return env.Detect('tex')