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 5357 2011/09/09 21:31:03 bdeegan"
31
32 import os.path
33 import re
34
35 import SCons.Scanner
36 import SCons.Util
37
38
39 TexGraphics = ['.eps', '.ps']
40 LatexGraphics = ['.pdf', '.png', '.jpg', '.gif', '.tif']
41
42
45 _null = _Null
46
47
48
49
50
51
53 try:
54 save = env['ENV'][var]
55 except KeyError:
56 save = _null
57 env.PrependENVPath(var, abspath)
58 try:
59 if SCons.Util.is_List(env[var]):
60 env.PrependENVPath(var, [os.path.abspath(str(p)) for p in env[var]])
61 else:
62
63 env.PrependENVPath(var, [os.path.abspath(p) for p in str(env[var]).split(os.pathsep)])
64 except KeyError:
65 pass
66
67
68
69
70
71 if SCons.Util.is_List(env['ENV'][var]):
72 env['ENV'][var] = os.pathsep.join(env['ENV'][var])
73
74 env['ENV'][var] = env['ENV'][var] + os.pathsep
75
76 return save
77
79 """A class to bind a specific *PATH variable name to a function that
80 will return all of the *path directories."""
82 self.variable = variable
83 - def __call__(self, env, dir=None, target=None, source=None, argument=None):
93
94
95
97 """Return a prototype Scanner instance for scanning LaTeX source files
98 when built with latex.
99 """
100 ds = LaTeX(name = "LaTeXScanner",
101 suffixes = '$LATEXSUFFIXES',
102
103 graphics_extensions = TexGraphics,
104 recursive = 0)
105 return ds
106
108 """Return a prototype Scanner instance for scanning LaTeX source files
109 when built with pdflatex.
110 """
111 ds = LaTeX(name = "PDFLaTeXScanner",
112 suffixes = '$LATEXSUFFIXES',
113
114 graphics_extensions = LatexGraphics,
115 recursive = 0)
116 return ds
117
118 -class LaTeX(SCons.Scanner.Base):
119 """Class for scanning LaTeX files for included files.
120
121 Unlike most scanners, which use regular expressions that just
122 return the included file name, this returns a tuple consisting
123 of the keyword for the inclusion ("include", "includegraphics",
124 "input", or "bibliography"), and then the file name itself.
125 Based on a quick look at LaTeX documentation, it seems that we
126 should append .tex suffix for the "include" keywords, append .tex if
127 there is no extension for the "input" keyword, and need to add .bib
128 for the "bibliography" keyword that does not accept extensions by itself.
129
130 Finally, if there is no extension for an "includegraphics" keyword
131 latex will append .ps or .eps to find the file, while pdftex may use .pdf,
132 .jpg, .tif, .mps, or .png.
133
134 The actual subset and search order may be altered by
135 DeclareGraphicsExtensions command. This complication is ignored.
136 The default order corresponds to experimentation with teTeX
137 $ latex --version
138 pdfeTeX 3.141592-1.21a-2.2 (Web2C 7.5.4)
139 kpathsea version 3.5.4
140 The order is:
141 ['.eps', '.ps'] for latex
142 ['.png', '.pdf', '.jpg', '.tif'].
143
144 Another difference is that the search path is determined by the type
145 of the file being searched:
146 env['TEXINPUTS'] for "input" and "include" keywords
147 env['TEXINPUTS'] for "includegraphics" keyword
148 env['TEXINPUTS'] for "lstinputlisting" keyword
149 env['BIBINPUTS'] for "bibliography" keyword
150 env['BSTINPUTS'] for "bibliographystyle" keyword
151 env['INDEXSTYLE'] for "makeindex" keyword, no scanning support needed
152 just allows user to set it if needed.
153
154 FIXME: also look for the class or style in document[class|style]{}
155 FIXME: also look for the argument of bibliographystyle{}
156 """
157 keyword_paths = {'include': 'TEXINPUTS',
158 'input': 'TEXINPUTS',
159 'includegraphics': 'TEXINPUTS',
160 'bibliography': 'BIBINPUTS',
161 'bibliographystyle': 'BSTINPUTS',
162 'makeindex': 'INDEXSTYLE',
163 'usepackage': 'TEXINPUTS',
164 'lstinputlisting': 'TEXINPUTS'}
165 env_variables = SCons.Util.unique(list(keyword_paths.values()))
166
167 - def __init__(self, name, suffixes, graphics_extensions, *args, **kw):
168
169
170
171
172
173
174
175 regex = r'^[^%\n]*\\(include|includegraphics(?:\s*\[[^\]]+\])?|lstinputlisting(?:\[[^\]]+\])?|input|bibliography|usepackage)\s*{([^}]*)}'
176 self.cre = re.compile(regex, re.M)
177 self.comment_re = re.compile(r'^((?:(?:\\%)|[^%\n])*)(.*)$', re.M)
178
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(object):
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(object):
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 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
260 return [filename+e for e in self.graphics_extensions]
261 return [filename]
262
265
267 try:
268 sub_path = path[include[0]]
269 except (IndexError, KeyError):
270 sub_path = ()
271 try_names = self._latex_names(include)
272 for n in try_names:
273
274 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[0])
275 if i:
276 return i, include
277
278 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[1])
279 if i:
280 return i, include
281 return i, include
282
283 - def canonical_text(self, text):
284 """Standardize an input TeX-file contents.
285
286 Currently:
287 * removes comments, unwrapping comment-wrapped lines.
288 """
289 out = []
290 line_continues_a_comment = False
291 for line in text.splitlines():
292 line,comment = self.comment_re.findall(line)[0]
293 if line_continues_a_comment == True:
294 out[-1] = out[-1] + line.lstrip()
295 else:
296 out.append(line)
297 line_continues_a_comment = len(comment) > 0
298 return '\n'.join(out).rstrip()+'\n'
299
300 - def scan(self, node):
301
302
303
304
305
306
307
308 noopt_cre = re.compile('\s*\[.*$')
309 if node.includes != None:
310 includes = node.includes
311 else:
312 text = self.canonical_text(node.get_text_contents())
313 includes = self.cre.findall(text)
314
315
316
317
318
319
320
321
322
323 split_includes = []
324 for include in includes:
325 inc_type = noopt_cre.sub('', include[0])
326 inc_list = include[1].split(',')
327 for j in range(len(inc_list)):
328 split_includes.append( (inc_type, inc_list[j]) )
329
330 includes = split_includes
331 node.includes = includes
332
333 return includes
334
336 """ do a recursive scan of the top level target file
337 This lets us search for included files based on the
338 directory of the main file just as latex does"""
339
340 path_dict = dict(list(path))
341
342 queue = []
343 queue.extend( self.scan(node) )
344 seen = {}
345
346
347
348
349
350
351
352
353 nodes = []
354 source_dir = node.get_dir()
355
356 while queue:
357
358 include = queue.pop()
359 try:
360 if seen[include[1]] == 1:
361 continue
362 except KeyError:
363 seen[include[1]] = 1
364
365
366
367
368 n, i = self.find_include(include, source_dir, path_dict)
369 if n is None:
370
371
372 if include[0] != 'usepackage':
373 SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
374 "No dependency generated for file: %s (included from: %s) -- file not found" % (i, node))
375 else:
376 sortkey = self.sort_key(n)
377 nodes.append((sortkey, n))
378
379 queue.extend( self.scan(n) )
380
381 return [pair[1] for pair in sorted(nodes)]
382
383
384
385
386
387
388