Package SCons :: Package Scanner :: Module LaTeX
[hide private]
[frames] | no frames]

Source Code for Module SCons.Scanner.LaTeX

  1  """SCons.Scanner.LaTeX 
  2   
  3  This module implements the dependency scanner for LaTeX code. 
  4   
  5  """ 
  6   
  7  # 
  8  # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 The SCons Foundation 
  9  # 
 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: 
 17  # 
 18  # The above copyright notice and this permission notice shall be included 
 19  # in all copies or substantial portions of the Software. 
 20  # 
 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. 
 28  # 
 29   
 30  __revision__ = "src/engine/SCons/Scanner/LaTeX.py 4720 2010/03/24 03:14:11 jars" 
 31   
 32  import os.path 
 33  import string 
 34  import re 
 35   
 36  import SCons.Scanner 
 37  import SCons.Util 
 38   
 39  # list of graphics file extensions for TeX and LaTeX 
 40  TexGraphics   = ['.eps', '.ps'] 
 41  LatexGraphics = ['.pdf', '.png', '.jpg', '.gif', '.tif'] 
 42   
 43  # Used as a return value of modify_env_var if the variable is not set. 
44 -class _Null:
45 pass
46 _null = _Null 47 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):
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 #TODO(1.5) 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])) 64 else: 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))) 68 except KeyError: 69 pass 70 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). 74 75 if SCons.Util.is_List(env['ENV'][var]): 76 # TODO(1.5) 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 81 82 return save
83
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):
90 import SCons.PathList 91 try: 92 path = env['ENV'][self.variable] 93 except KeyError: 94 return () 95 96 dir = dir or env.fs._cwd 97 path = SCons.PathList.PathList(path).subst_path(env, target, source) 98 return tuple(dir.Rfindalldirs(path))
99 100 101
102 -def LaTeXScanner():
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 # in the search order, see below in LaTeX class docstring 109 graphics_extensions = TexGraphics, 110 recursive = 0) 111 return ds
112
113 -def PDFLaTeXScanner():
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 # in the search order, see below in LaTeX class docstring 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 # We have to include \n with the % we exclude from the first part 173 # part of the regex because the expression is compiled with re.M. 174 # Without the \n, the ^ could match the beginning of a *previous* 175 # line followed by one or more newline characters (i.e. blank 176 # lines), interfering with a match on the next line. 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 # To prevent "dict is not hashable error" 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 # Returning false means that the file is not scanned. 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
236 - def _latex_names(self, include):
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 #TODO(1.5) return [filename + e for e in self.graphics_extensions] 256 #return map(lambda e, f=filename: f+e, self.graphics_extensions + TexGraphics) 257 # use the line above to find dependency for PDF builder when only .eps figure is present 258 # Since it will be found if the user tell scons how to make the pdf figure leave it out for now. 259 return map(lambda e, f=filename: f+e, self.graphics_extensions) 260 return [filename]
261
262 - def sort_key(self, include):
263 return SCons.Node.FS._my_normcase(str(include))
264
265 - def find_include(self, include, source_dir, path):
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 # see if we find it using the path in env[var] 273 i = SCons.Node.FS.find_file(n, (source_dir,) + sub_path[0]) 274 if i: 275 return i, include 276 # see if we find it using the path in env['ENV'][var] 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 # Modify the default scan function to allow for the regular 284 # expression to return a comma separated list of file names 285 # as can be the case with the bibliography keyword. 286 287 # Cache the includes list in node so we only scan it once: 288 # path_dict = dict(list(path)) 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 # 1. Split comma-separated lines, e.g. 295 # ('bibliography', 'phys,comp') 296 # should become two entries 297 # ('bibliography', 'phys') 298 # ('bibliography', 'comp') 299 # 2. Remove the options, e.g., such as 300 # ('includegraphics[clip,width=0.7\\linewidth]', 'picture.eps') 301 # should become 302 # ('includegraphics', 'picture.eps') 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
315 - def scan_recurse(self, node, path=()):
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 # This is a hand-coded DSU (decorate-sort-undecorate, or 327 # Schwartzian transform) pattern. The sort key is the raw name 328 # of the file as specifed on the \include, \input, etc. line. 329 # TODO: what about the comment in the original Classic scanner: 330 # """which lets 331 # us keep the sort order constant regardless of whether the file 332 # is actually found in a Repository or locally.""" 333 nodes = [] 334 source_dir = node.get_dir() 335 #for include in includes: 336 while queue: 337 338 include = queue.pop() 339 # TODO(1.5): more compact: 340 #try: 341 # if seen[include[1]] == 1: 342 # continue 343 #except KeyError: 344 # seen[include[1]] = 1 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 # Handle multiple filenames in include[1] 355 # 356 n, i = self.find_include(include, source_dir, path_dict) 357 if n is None: 358 # Do not bother with 'usepackage' warnings, as they most 359 # likely refer to system-level files 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 # recurse down 367 queue.extend( self.scan(n) ) 368 369 # 370 nodes.sort() 371 nodes = map(lambda pair: pair[1], nodes) 372 return nodes
373 374 # Local Variables: 375 # tab-width:4 376 # indent-tabs-mode:nil 377 # End: 378 # vim: set expandtab tabstop=4 shiftwidth=4: 379