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 3842 2008/12/20 22:59:52 scons"
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
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['BIBINPUTS'] for "bibliography" keyword
155 env['BSTINPUTS'] for "bibliographystyle" keyword
156
157 FIXME: also look for the class or style in document[class|style]{}
158 FIXME: also look for the argument of bibliographystyle{}
159 """
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())
167
168 - def __init__(self, name, suffixes, graphics_extensions, *args, **kw):
169
170
171
172
173
174
175 regex = r'^[^%\n]*\\(include|includegraphics(?:\[[^\]]+\])?|input|bibliography|usepackage){([^}]*)}'
176 self.cre = re.compile(regex, re.M)
177 self.graphics_extensions = graphics_extensions
178
179 def _scan(node, env, path=(), self=self):
180 node = node.rfile()
181 if not node.exists():
182 return []
183 return self.scan(node, path)
184
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
194 of paths.
195 """
196 def __init__(self, dictionary):
197 self.dictionary = {}
198 for k,n in dictionary.items():
199 self.dictionary[k] = ( SCons.Scanner.FindPathDirs(n),
200 FindENVPathDirs(n) )
201
202 def __call__(self, env, dir=None, target=None, source=None,
203 argument=None):
204 di = {}
205 for k,(c,cENV) in self.dictionary.items():
206 di[k] = ( c(env, dir=None, target=None, source=None,
207 argument=None) ,
208 cENV(env, dir=None, target=None, source=None,
209 argument=None) )
210
211 return tuple(di.items())
212
213 class LaTeXScanCheck:
214 """Skip all but LaTeX source files, i.e., do not scan *.eps,
215 *.pdf, *.jpg, etc.
216 """
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
223 return scannable and current
224
225 kw['function'] = _scan
226 kw['path_function'] = FindMultiPathDirs(LaTeX.keyword_paths)
227 kw['recursive'] = 1
228 kw['skeys'] = suffixes
229 kw['scan_check'] = LaTeXScanCheck(suffixes)
230 kw['name'] = name
231
232 apply(SCons.Scanner.Base.__init__, (self,) + args, kw)
233
235 filename = include[1]
236 if include[0] == 'input':
237 base, ext = os.path.splitext( filename )
238 if ext == "":
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 )
244 if ext == "":
245 return [filename + '.bib']
246 if include[0] == 'usepackage':
247 base, ext = os.path.splitext( filename )
248 if ext == "":
249 return [filename + '.sty']
250 if include[0] == 'includegraphics':
251 base, ext = os.path.splitext( filename )
252 if ext == "":
253
254 return map(lambda e, f=filename: f+e, self.graphics_extensions)
255 return [filename]
256
259
261 try:
262 sub_path = path[include[0]]
263 except (IndexError, KeyError):
264 sub_path = ()
265 try_names = self._latex_names(include)
266 for n in try_names:
267
268 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[0])
269 if i:
270 return i, include
271
272 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[1])
273 if i:
274 return i, include
275 return i, include
276
277 - def scan(self, node, path=()):
278
279
280
281
282
283 path_dict = dict(list(path))
284 noopt_cre = re.compile('\[.*$')
285 if node.includes != None:
286 includes = node.includes
287 else:
288 includes = self.cre.findall(node.get_contents())
289
290
291
292
293
294
295
296
297
298 split_includes = []
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]) )
304
305 includes = split_includes
306 node.includes = includes
307
308
309
310
311
312
313
314
315 nodes = []
316 source_dir = node.get_dir()
317 for include in includes:
318
319
320
321 n, i = self.find_include(include, source_dir, path_dict)
322 if n is None:
323
324
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))
328 else:
329 sortkey = self.sort_key(n)
330 nodes.append((sortkey, n))
331
332 nodes.sort()
333 nodes = map(lambda pair: pair[1], nodes)
334 return nodes
335