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 3603 2008/10/10 05:46:45 scons"
31
32 import os.path
33 import string
34 import re
35
36 import SCons.Scanner
37
39 """Return a prototype Scanner instance for scanning LaTeX source files
40 when built with latex.
41 """
42 ds = LaTeX(name = "LaTeXScanner",
43 suffixes = '$LATEXSUFFIXES',
44
45 graphics_extensions = ['.eps', '.ps'],
46 recursive = 0)
47 return ds
48
50 """Return a prototype Scanner instance for scanning LaTeX source files
51 when built with pdflatex.
52 """
53 ds = LaTeX(name = "PDFLaTeXScanner",
54 suffixes = '$LATEXSUFFIXES',
55
56 graphics_extensions = ['.pdf', '.png', '.jpg', '.gif', '.tif'],
57 recursive = 0)
58 return ds
59
60 -class LaTeX(SCons.Scanner.Base):
61 """Class for scanning LaTeX files for included files.
62
63 Unlike most scanners, which use regular expressions that just
64 return the included file name, this returns a tuple consisting
65 of the keyword for the inclusion ("include", "includegraphics",
66 "input", or "bibliography"), and then the file name itself.
67 Based on a quick look at LaTeX documentation, it seems that we
68 should append .tex suffix for the "include" keywords, append .tex if
69 there is no extension for the "input" keyword, and need to add .bib
70 for the "bibliography" keyword that does not accept extensions by itself.
71
72 Finally, if there is no extension for an "includegraphics" keyword
73 latex will append .ps or .eps to find the file, while pdftex may use .pdf,
74 .jpg, .tif, .mps, or .png.
75
76 The actual subset and search order may be altered by
77 DeclareGraphicsExtensions command. This complication is ignored.
78 The default order corresponds to experimentation with teTeX
79 $ latex --version
80 pdfeTeX 3.141592-1.21a-2.2 (Web2C 7.5.4)
81 kpathsea version 3.5.4
82 The order is:
83 ['.eps', '.ps'] for latex
84 ['.png', '.pdf', '.jpg', '.tif'].
85
86 Another difference is that the search path is determined by the type
87 of the file being searched:
88 env['TEXINPUTS'] for "input" and "include" keywords
89 env['TEXINPUTS'] for "includegraphics" keyword
90 env['BIBINPUTS'] for "bibliography" keyword
91 env['BSTINPUTS'] for "bibliographystyle" keyword
92
93 FIXME: also look for the class or style in document[class|style]{}
94 FIXME: also look for the argument of bibliographystyle{}
95 """
96 keyword_paths = {'include': 'TEXINPUTS',
97 'input': 'TEXINPUTS',
98 'includegraphics': 'TEXINPUTS',
99 'bibliography': 'BIBINPUTS',
100 'bibliographystyle': 'BSTINPUTS',
101 'usepackage': 'TEXINPUTS'}
102 env_variables = SCons.Util.unique(keyword_paths.values())
103
104 - def __init__(self, name, suffixes, graphics_extensions, *args, **kw):
105
106
107
108
109
110
111 regex = r'^[^%\n]*\\(include|includegraphics(?:\[[^\]]+\])?|input|bibliography|usepackage){([^}]*)}'
112 self.cre = re.compile(regex, re.M)
113 self.graphics_extensions = graphics_extensions
114
115 def _scan(node, env, path=(), self=self):
116 node = node.rfile()
117 if not node.exists():
118 return []
119 return self.scan(node, path)
120
121 class FindMultiPathDirs:
122 """The stock FindPathDirs function has the wrong granularity:
123 it is called once per target, while we need the path that depends
124 on what kind of included files is being searched. This wrapper
125 hides multiple instances of FindPathDirs, one per the LaTeX path
126 variable in the environment. When invoked, the function calculates
127 and returns all the required paths as a dictionary (converted into
128 a tuple to become hashable). Then the scan function converts it
129 back and uses a dictionary of tuples rather than a single tuple
130 of paths.
131 """
132 def __init__(self, dictionary):
133 self.dictionary = {}
134 for k,n in dictionary.items():
135 self.dictionary[k] = SCons.Scanner.FindPathDirs(n)
136
137 def __call__(self, env, dir=None, target=None, source=None,
138 argument=None):
139 di = {}
140 for k,c in self.dictionary.items():
141 di[k] = c(env, dir=None, target=None, source=None,
142 argument=None)
143
144 return tuple(di.items())
145
146 class LaTeXScanCheck:
147 """Skip all but LaTeX source files, i.e., do not scan *.eps,
148 *.pdf, *.jpg, etc.
149 """
150 def __init__(self, suffixes):
151 self.suffixes = suffixes
152 def __call__(self, node, env):
153 current = not node.has_builder() or node.is_up_to_date()
154 scannable = node.get_suffix() in env.subst_list(self.suffixes)[0]
155
156 return scannable and current
157
158 kw['function'] = _scan
159 kw['path_function'] = FindMultiPathDirs(LaTeX.keyword_paths)
160 kw['recursive'] = 1
161 kw['skeys'] = suffixes
162 kw['scan_check'] = LaTeXScanCheck(suffixes)
163 kw['name'] = name
164
165 apply(SCons.Scanner.Base.__init__, (self,) + args, kw)
166
168 filename = include[1]
169 if include[0] == 'input':
170 base, ext = os.path.splitext( filename )
171 if ext == "":
172 return [filename + '.tex']
173 if (include[0] == 'include'):
174 return [filename + '.tex']
175 if include[0] == 'bibliography':
176 base, ext = os.path.splitext( filename )
177 if ext == "":
178 return [filename + '.bib']
179 if include[0] == 'usepackage':
180 base, ext = os.path.splitext( filename )
181 if ext == "":
182 return [filename + '.sty']
183 if include[0] == 'includegraphics':
184 base, ext = os.path.splitext( filename )
185 if ext == "":
186
187 return map(lambda e, f=filename: f+e, self.graphics_extensions)
188 return [filename]
189
192
194 try:
195 sub_path = path[include[0]]
196 except (IndexError, KeyError):
197 sub_path = ()
198 try_names = self._latex_names(include)
199 for n in try_names:
200 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path)
201 if i:
202 return i, include
203 return i, include
204
205 - def scan(self, node, path=()):
206
207
208
209
210
211 path_dict = dict(list(path))
212 noopt_cre = re.compile('\[.*$')
213 if node.includes != None:
214 includes = node.includes
215 else:
216 includes = self.cre.findall(node.get_contents())
217
218
219
220
221
222
223
224
225
226 split_includes = []
227 for include in includes:
228 inc_type = noopt_cre.sub('', include[0])
229 inc_list = string.split(include[1],',')
230 for j in range(len(inc_list)):
231 split_includes.append( (inc_type, inc_list[j]) )
232
233 includes = split_includes
234 node.includes = includes
235
236
237
238
239
240
241
242
243 nodes = []
244 source_dir = node.get_dir()
245 for include in includes:
246
247
248
249 n, i = self.find_include(include, source_dir, path_dict)
250 if n is None:
251
252
253 if include[0] != 'usepackage':
254 SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
255 "No dependency generated for file: %s (included from: %s) -- file not found" % (i, node))
256 else:
257 sortkey = self.sort_key(n)
258 nodes.append((sortkey, n))
259
260 nodes.sort()
261 nodes = map(lambda pair: pair[1], nodes)
262 return nodes
263