1 """SCons.Scanner.LaTeX
2
3 This module implements the dependency scanner for LaTeX code.
4
5 """
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 __revision__ = "src/engine/SCons/Scanner/LaTeX.py 5110 2010/07/25 16:14:38 bdeegan"
31
32 import os.path
33 import string
34 import re
35
36 import SCons.Scanner
37 import SCons.Util
38
39
40 TexGraphics = ['.eps', '.ps']
41 LatexGraphics = ['.pdf', '.png', '.jpg', '.gif', '.tif']
42
43
46 _null = _Null
47
48
49
50
51
52
54 try:
55 save = env['ENV'][var]
56 except KeyError:
57 save = _null
58 env.PrependENVPath(var, abspath)
59 try:
60 if SCons.Util.is_List(env[var]):
61
62
63 env.PrependENVPath(var, map(lambda p: os.path.abspath(str(p)), env[var]))
64 else:
65
66
67 env.PrependENVPath(var, map(lambda p: os.path.abspath(p), string.split(str(env[var]), os.pathsep)))
68 except KeyError:
69 pass
70
71
72
73
74
75 if SCons.Util.is_List(env['ENV'][var]):
76
77
78 env['ENV'][var] = string.join(env['ENV'][var], os.pathsep)
79
80 env['ENV'][var] = env['ENV'][var] + os.pathsep
81
82 return save
83
85 """A class to bind a specific *PATH variable name to a function that
86 will return all of the *path directories."""
88 self.variable = variable
89 - def __call__(self, env, dir=None, target=None, source=None, argument=None):
99
100
101
103 """Return a prototype Scanner instance for scanning LaTeX source files
104 when built with latex.
105 """
106 ds = LaTeX(name = "LaTeXScanner",
107 suffixes = '$LATEXSUFFIXES',
108
109 graphics_extensions = TexGraphics,
110 recursive = 0)
111 return ds
112
114 """Return a prototype Scanner instance for scanning LaTeX source files
115 when built with pdflatex.
116 """
117 ds = LaTeX(name = "PDFLaTeXScanner",
118 suffixes = '$LATEXSUFFIXES',
119
120 graphics_extensions = LatexGraphics,
121 recursive = 0)
122 return ds
123
124 -class LaTeX(SCons.Scanner.Base):
125 """Class for scanning LaTeX files for included files.
126
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.
135
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.
139
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
143 $ latex --version
144 pdfeTeX 3.141592-1.21a-2.2 (Web2C 7.5.4)
145 kpathsea version 3.5.4
146 The order is:
147 ['.eps', '.ps'] for latex
148 ['.png', '.pdf', '.jpg', '.tif'].
149
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['TEXINPUTS'] for "lstinputlisting" keyword
155 env['BIBINPUTS'] for "bibliography" keyword
156 env['BSTINPUTS'] for "bibliographystyle" keyword
157
158 FIXME: also look for the class or style in document[class|style]{}
159 FIXME: also look for the argument of bibliographystyle{}
160 """
161 keyword_paths = {'include': 'TEXINPUTS',
162 'input': 'TEXINPUTS',
163 'includegraphics': 'TEXINPUTS',
164 'bibliography': 'BIBINPUTS',
165 'bibliographystyle': 'BSTINPUTS',
166 'usepackage': 'TEXINPUTS',
167 'lstinputlisting': 'TEXINPUTS'}
168 env_variables = SCons.Util.unique(keyword_paths.values())
169
170 - def __init__(self, name, suffixes, graphics_extensions, *args, **kw):
171
172
173
174
175
176
177 regex = r'^[^%\n]*\\(include|includegraphics(?:\[[^\]]+\])?|lstinputlisting(?:\[[^\]]+\])?|input|bibliography|usepackage){([^}]*)}'
178 self.cre = re.compile(regex, re.M)
179 self.graphics_extensions = graphics_extensions
180
181 def _scan(node, env, path=(), self=self):
182 node = node.rfile()
183 if not node.exists():
184 return []
185 return self.scan_recurse(node, path)
186
187 class FindMultiPathDirs:
188 """The stock FindPathDirs function has the wrong granularity:
189 it is called once per target, while we need the path that depends
190 on what kind of included files is being searched. This wrapper
191 hides multiple instances of FindPathDirs, one per the LaTeX path
192 variable in the environment. When invoked, the function calculates
193 and returns all the required paths as a dictionary (converted into
194 a tuple to become hashable). Then the scan function converts it
195 back and uses a dictionary of tuples rather than a single tuple
196 of paths.
197 """
198 def __init__(self, dictionary):
199 self.dictionary = {}
200 for k,n in dictionary.items():
201 self.dictionary[k] = ( SCons.Scanner.FindPathDirs(n),
202 FindENVPathDirs(n) )
203
204 def __call__(self, env, dir=None, target=None, source=None,
205 argument=None):
206 di = {}
207 for k,(c,cENV) in self.dictionary.items():
208 di[k] = ( c(env, dir=None, target=None, source=None,
209 argument=None) ,
210 cENV(env, dir=None, target=None, source=None,
211 argument=None) )
212
213 return tuple(di.items())
214
215 class LaTeXScanCheck:
216 """Skip all but LaTeX source files, i.e., do not scan *.eps,
217 *.pdf, *.jpg, etc.
218 """
219 def __init__(self, suffixes):
220 self.suffixes = suffixes
221 def __call__(self, node, env):
222 current = not node.has_builder() or node.is_up_to_date()
223 scannable = node.get_suffix() in env.subst_list(self.suffixes)[0]
224
225 return scannable and current
226
227 kw['function'] = _scan
228 kw['path_function'] = FindMultiPathDirs(LaTeX.keyword_paths)
229 kw['recursive'] = 0
230 kw['skeys'] = suffixes
231 kw['scan_check'] = LaTeXScanCheck(suffixes)
232 kw['name'] = name
233
234 apply(SCons.Scanner.Base.__init__, (self,) + args, kw)
235
237 filename = include[1]
238 if include[0] == 'input':
239 base, ext = os.path.splitext( filename )
240 if ext == "":
241 return [filename + '.tex']
242 if (include[0] == 'include'):
243 return [filename + '.tex']
244 if include[0] == 'bibliography':
245 base, ext = os.path.splitext( filename )
246 if ext == "":
247 return [filename + '.bib']
248 if include[0] == 'usepackage':
249 base, ext = os.path.splitext( filename )
250 if ext == "":
251 return [filename + '.sty']
252 if include[0] == 'includegraphics':
253 base, ext = os.path.splitext( filename )
254 if ext == "":
255
256
257
258
259 return map(lambda e, f=filename: f+e, self.graphics_extensions)
260 return [filename]
261
264
266 try:
267 sub_path = path[include[0]]
268 except (IndexError, KeyError):
269 sub_path = ()
270 try_names = self._latex_names(include)
271 for n in try_names:
272
273 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[0])
274 if i:
275 return i, include
276
277 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[1])
278 if i:
279 return i, include
280 return i, include
281
282 - def scan(self, node):
283
284
285
286
287
288
289 noopt_cre = re.compile('\[.*$')
290 if node.includes != None:
291 includes = node.includes
292 else:
293 includes = self.cre.findall(node.get_text_contents())
294
295
296
297
298
299
300
301
302
303 split_includes = []
304 for include in includes:
305 inc_type = noopt_cre.sub('', include[0])
306 inc_list = string.split(include[1],',')
307 for j in range(len(inc_list)):
308 split_includes.append( (inc_type, inc_list[j]) )
309
310 includes = split_includes
311 node.includes = includes
312
313 return includes
314
316 """ do a recursive scan of the top level target file
317 This lets us search for included files based on the
318 directory of the main file just as latex does"""
319
320 path_dict = dict(list(path))
321
322 queue = []
323 queue.extend( self.scan(node) )
324 seen = {}
325
326
327
328
329
330
331
332
333 nodes = []
334 source_dir = node.get_dir()
335
336 while queue:
337
338 include = queue.pop()
339
340
341
342
343
344
345 try:
346 already_seen = seen[include[1]]
347 except KeyError:
348 seen[include[1]] = 1
349 already_seen = False
350 if already_seen:
351 continue
352
353
354
355
356 n, i = self.find_include(include, source_dir, path_dict)
357 if n is None:
358
359
360 if include[0] != 'usepackage':
361 SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
362 "No dependency generated for file: %s (included from: %s) -- file not found" % (i, node))
363 else:
364 sortkey = self.sort_key(n)
365 nodes.append((sortkey, n))
366
367 queue.extend( self.scan(n) )
368
369
370 nodes.sort()
371 nodes = map(lambda pair: pair[1], nodes)
372 return nodes
373
374
375
376
377
378
379