3 This module implements the dependency scanner for LaTeX code.
8 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 The SCons Foundation
10 # Permission is hereby granted, free of charge, to any person obtaining
11 # a copy of this software and associated documentation files (the
12 # "Software"), to deal in the Software without restriction, including
13 # without limitation the rights to use, copy, modify, merge, publish,
14 # distribute, sublicense, and/or sell copies of the Software, and to
15 # permit persons to whom the Software is furnished to do so, subject to
16 # the following conditions:
18 # The above copyright notice and this permission notice shall be included
19 # in all copies or substantial portions of the Software.
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
22 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
23 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 __revision__ = "src/engine/SCons/Scanner/LaTeX.py 3842 2008/12/20 22:59:52 scons"
39 # list of graphics file extensions for TeX and LaTeX
40 TexGraphics = ['.eps', '.ps']
41 LatexGraphics = ['.pdf', '.png', '.jpg', '.gif', '.tif']
43 # Used as a return value of modify_env_var if the variable is not set.
48 # The user specifies the paths in env[variable], similar to other builders.
49 # They may be relative and must be converted to absolute, as expected
50 # by LaTeX and Co. The environment may already have some paths in
51 # env['ENV'][var]. These paths are honored, but the env[var] paths have
52 # higher precedence. All changes are un-done on exit.
53 def modify_env_var(env, var, abspath):
55 save = env['ENV'][var]
58 env.PrependENVPath(var, abspath)
60 if SCons.Util.is_List(env[var]):
62 #env.PrependENVPath(var, [os.path.abspath(str(p)) for p in env[var]])
63 env.PrependENVPath(var, map(lambda p: os.path.abspath(str(p)), env[var]))
65 # Split at os.pathsep to convert into absolute path
66 #TODO(1.5) env.PrependENVPath(var, [os.path.abspath(p) for p in str(env[var]).split(os.pathsep)])
67 env.PrependENVPath(var, map(lambda p: os.path.abspath(p), string.split(str(env[var]), os.pathsep)))
71 # Convert into a string explicitly to append ":" (without which it won't search system
72 # paths as well). The problem is that env.AppendENVPath(var, ":")
73 # does not work, refuses to append ":" (os.pathsep).
75 if SCons.Util.is_List(env['ENV'][var]):
77 #env['ENV'][var] = os.pathsep.join(env['ENV'][var])
78 env['ENV'][var] = string.join(env['ENV'][var], os.pathsep)
79 # Append the trailing os.pathsep character here to catch the case with no env[var]
80 env['ENV'][var] = env['ENV'][var] + os.pathsep
84 class FindENVPathDirs:
85 """A class to bind a specific *PATH variable name to a function that
86 will return all of the *path directories."""
87 def __init__(self, variable):
88 self.variable = variable
89 def __call__(self, env, dir=None, target=None, source=None, argument=None):
92 path = env['ENV'][self.variable]
96 dir = dir or env.fs._cwd
97 path = SCons.PathList.PathList(path).subst_path(env, target, source)
98 return tuple(dir.Rfindalldirs(path))
103 """Return a prototype Scanner instance for scanning LaTeX source files
104 when built with latex.
106 ds = LaTeX(name = "LaTeXScanner",
107 suffixes = '$LATEXSUFFIXES',
108 # in the search order, see below in LaTeX class docstring
109 graphics_extensions = TexGraphics,
113 def PDFLaTeXScanner():
114 """Return a prototype Scanner instance for scanning LaTeX source files
115 when built with pdflatex.
117 ds = LaTeX(name = "PDFLaTeXScanner",
118 suffixes = '$LATEXSUFFIXES',
119 # in the search order, see below in LaTeX class docstring
120 graphics_extensions = LatexGraphics,
124 class LaTeX(SCons.Scanner.Base):
125 """Class for scanning LaTeX files for included files.
127 Unlike most scanners, which use regular expressions that just
128 return the included file name, this returns a tuple consisting
129 of the keyword for the inclusion ("include", "includegraphics",
130 "input", or "bibliography"), and then the file name itself.
131 Based on a quick look at LaTeX documentation, it seems that we
132 should append .tex suffix for the "include" keywords, append .tex if
133 there is no extension for the "input" keyword, and need to add .bib
134 for the "bibliography" keyword that does not accept extensions by itself.
136 Finally, if there is no extension for an "includegraphics" keyword
137 latex will append .ps or .eps to find the file, while pdftex may use .pdf,
138 .jpg, .tif, .mps, or .png.
140 The actual subset and search order may be altered by
141 DeclareGraphicsExtensions command. This complication is ignored.
142 The default order corresponds to experimentation with teTeX
144 pdfeTeX 3.141592-1.21a-2.2 (Web2C 7.5.4)
145 kpathsea version 3.5.4
147 ['.eps', '.ps'] for latex
148 ['.png', '.pdf', '.jpg', '.tif'].
150 Another difference is that the search path is determined by the type
151 of the file being searched:
152 env['TEXINPUTS'] for "input" and "include" keywords
153 env['TEXINPUTS'] for "includegraphics" keyword
154 env['BIBINPUTS'] for "bibliography" keyword
155 env['BSTINPUTS'] for "bibliographystyle" keyword
157 FIXME: also look for the class or style in document[class|style]{}
158 FIXME: also look for the argument of bibliographystyle{}
160 keyword_paths = {'include': 'TEXINPUTS',
161 'input': 'TEXINPUTS',
162 'includegraphics': 'TEXINPUTS',
163 'bibliography': 'BIBINPUTS',
164 'bibliographystyle': 'BSTINPUTS',
165 'usepackage': 'TEXINPUTS'}
166 env_variables = SCons.Util.unique(keyword_paths.values())
168 def __init__(self, name, suffixes, graphics_extensions, *args, **kw):
170 # We have to include \n with the % we exclude from the first part
171 # part of the regex because the expression is compiled with re.M.
172 # Without the \n, the ^ could match the beginning of a *previous*
173 # line followed by one or more newline characters (i.e. blank
174 # lines), interfering with a match on the next line.
175 regex = r'^[^%\n]*\\(include|includegraphics(?:\[[^\]]+\])?|input|bibliography|usepackage){([^}]*)}'
176 self.cre = re.compile(regex, re.M)
177 self.graphics_extensions = graphics_extensions
179 def _scan(node, env, path=(), self=self):
181 if not node.exists():
183 return self.scan(node, path)
185 class FindMultiPathDirs:
186 """The stock FindPathDirs function has the wrong granularity:
187 it is called once per target, while we need the path that depends
188 on what kind of included files is being searched. This wrapper
189 hides multiple instances of FindPathDirs, one per the LaTeX path
190 variable in the environment. When invoked, the function calculates
191 and returns all the required paths as a dictionary (converted into
192 a tuple to become hashable). Then the scan function converts it
193 back and uses a dictionary of tuples rather than a single tuple
196 def __init__(self, dictionary):
198 for k,n in dictionary.items():
199 self.dictionary[k] = ( SCons.Scanner.FindPathDirs(n),
202 def __call__(self, env, dir=None, target=None, source=None,
205 for k,(c,cENV) in self.dictionary.items():
206 di[k] = ( c(env, dir=None, target=None, source=None,
208 cENV(env, dir=None, target=None, source=None,
210 # To prevent "dict is not hashable error"
211 return tuple(di.items())
213 class LaTeXScanCheck:
214 """Skip all but LaTeX source files, i.e., do not scan *.eps,
217 def __init__(self, suffixes):
218 self.suffixes = suffixes
219 def __call__(self, node, env):
220 current = not node.has_builder() or node.is_up_to_date()
221 scannable = node.get_suffix() in env.subst_list(self.suffixes)[0]
222 # Returning false means that the file is not scanned.
223 return scannable and current
225 kw['function'] = _scan
226 kw['path_function'] = FindMultiPathDirs(LaTeX.keyword_paths)
228 kw['skeys'] = suffixes
229 kw['scan_check'] = LaTeXScanCheck(suffixes)
232 apply(SCons.Scanner.Base.__init__, (self,) + args, kw)
234 def _latex_names(self, include):
235 filename = include[1]
236 if include[0] == 'input':
237 base, ext = os.path.splitext( filename )
239 return [filename + '.tex']
240 if (include[0] == 'include'):
241 return [filename + '.tex']
242 if include[0] == 'bibliography':
243 base, ext = os.path.splitext( filename )
245 return [filename + '.bib']
246 if include[0] == 'usepackage':
247 base, ext = os.path.splitext( filename )
249 return [filename + '.sty']
250 if include[0] == 'includegraphics':
251 base, ext = os.path.splitext( filename )
253 #TODO(1.5) return [filename + e for e in self.graphics_extensions]
254 return map(lambda e, f=filename: f+e, self.graphics_extensions)
257 def sort_key(self, include):
258 return SCons.Node.FS._my_normcase(str(include))
260 def find_include(self, include, source_dir, path):
262 sub_path = path[include[0]]
263 except (IndexError, KeyError):
265 try_names = self._latex_names(include)
267 # see if we find it using the path in env[var]
268 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[0])
271 # see if we find it using the path in env['ENV'][var]
272 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[1])
277 def scan(self, node, path=()):
278 # Modify the default scan function to allow for the regular
279 # expression to return a comma separated list of file names
280 # as can be the case with the bibliography keyword.
282 # Cache the includes list in node so we only scan it once:
283 path_dict = dict(list(path))
284 noopt_cre = re.compile('\[.*$')
285 if node.includes != None:
286 includes = node.includes
288 includes = self.cre.findall(node.get_contents())
289 # 1. Split comma-separated lines, e.g.
290 # ('bibliography', 'phys,comp')
291 # should become two entries
292 # ('bibliography', 'phys')
293 # ('bibliography', 'comp')
294 # 2. Remove the options, e.g., such as
295 # ('includegraphics[clip,width=0.7\\linewidth]', 'picture.eps')
297 # ('includegraphics', 'picture.eps')
299 for include in includes:
300 inc_type = noopt_cre.sub('', include[0])
301 inc_list = string.split(include[1],',')
302 for j in range(len(inc_list)):
303 split_includes.append( (inc_type, inc_list[j]) )
305 includes = split_includes
306 node.includes = includes
308 # This is a hand-coded DSU (decorate-sort-undecorate, or
309 # Schwartzian transform) pattern. The sort key is the raw name
310 # of the file as specifed on the \include, \input, etc. line.
311 # TODO: what about the comment in the original Classic scanner:
313 # us keep the sort order constant regardless of whether the file
314 # is actually found in a Repository or locally."""
316 source_dir = node.get_dir()
317 for include in includes:
319 # Handle multiple filenames in include[1]
321 n, i = self.find_include(include, source_dir, path_dict)
323 # Do not bother with 'usepackage' warnings, as they most
324 # likely refer to system-level files
325 if include[0] != 'usepackage':
326 SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
327 "No dependency generated for file: %s (included from: %s) -- file not found" % (i, node))
329 sortkey = self.sort_key(n)
330 nodes.append((sortkey, n))
333 nodes = map(lambda pair: pair[1], nodes)