LSSTApplications  1.1.2+25,10.0+13,10.0+132,10.0+133,10.0+224,10.0+41,10.0+8,10.0-1-g0f53050+14,10.0-1-g4b7b172+19,10.0-1-g61a5bae+98,10.0-1-g7408a83+3,10.0-1-gc1e0f5a+19,10.0-1-gdb4482e+14,10.0-11-g3947115+2,10.0-12-g8719d8b+2,10.0-15-ga3f480f+1,10.0-2-g4f67435,10.0-2-gcb4bc6c+26,10.0-28-gf7f57a9+1,10.0-3-g1bbe32c+14,10.0-3-g5b46d21,10.0-4-g027f45f+5,10.0-4-g86f66b5+2,10.0-4-gc4fccf3+24,10.0-40-g4349866+2,10.0-5-g766159b,10.0-5-gca2295e+25,10.0-6-g462a451+1
LSSTDataManagementBasePackage
builders.py
Go to the documentation of this file.
1 #
2 # @file builders.py
3 #
4 # Extra builders and methods to be injected into the SConsEnvironment class.
5 ##
6 
7 import os
8 import re
9 import fnmatch
10 
11 import SCons.Script
12 from SCons.Script.SConscript import SConsEnvironment
13 
14 from .utils import memberOf
15 from .installation import determineVersion, getFingerprint
16 from . import state
17 
18 ## @brief Like SharedLibrary, but don't insist that all symbols are resolved
19 @memberOf(SConsEnvironment)
20 def SharedLibraryIncomplete(self, target, source, **keywords):
21  myenv = self.Clone()
22  if myenv['PLATFORM'] == 'darwin':
23  myenv['SHLINKFLAGS'] += ["-undefined", "suppress", "-flat_namespace"]
24  return myenv.SharedLibrary(target, source, **keywords)
25 
26 ## @brief Like LoadableModule, but don't insist that all symbols are resolved, and set
27 ## some SWIG-specific flags.
28 @memberOf(SConsEnvironment)
29 def SwigLoadableModule(self, target, source, **keywords):
30  myenv = self.Clone()
31  if myenv['PLATFORM'] == 'darwin':
32  myenv.Append(LDMODULEFLAGS = ["-undefined", "suppress", "-flat_namespace",])
33  #
34  # Swig-generated .cc files cast pointers to long longs and back,
35  # which is illegal. This flag tells g++ about the sin
36  #
37  try:
38  if myenv.whichCc == "gcc":
39  myenv.Append(CCFLAGS = ["-fno-strict-aliasing",])
40  except AttributeError:
41  pass
42  return myenv.LoadableModule(target, source, **keywords)
43 
44 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
45 
46 ##
47 # @brief Prepare the list of files to be passed to a SharedLibrary constructor
48 #
49 # In particular, ensure that any files listed in env.NoOptFiles (set by the command line option
50 # noOptFile="file1 file2") are built without optimisation and files listed in env.optFiles are
51 # built with optimisation
52 #
53 # The usage pattern in an SConscript file is:
54 # ccFiles = env.SourcesForSharedLibrary(Glob("../src/*/*.cc"))
55 # env.SharedLibrary('afw', ccFiles, LIBS=env.getLibs("self")))
56 #
57 # This is automatically used by scripts.BasicSConscript.lib().
58 ##
59 @memberOf(SConsEnvironment)
60 def SourcesForSharedLibrary(self, files):
61 
62  files = [SCons.Script.File(file) for file in files]
63 
64  if not (self.get("optFiles") or self.get("noOptFiles")):
65  files.sort()
66  return files
67 
68  if self.get("optFiles"):
69  optFiles = self["optFiles"].replace(".", r"\.") # it'll be used in an RE
70  optFiles = SCons.Script.Split(optFiles.replace(",", " "))
71  optFilesRe = "/(%s)$" % "|".join(optFiles)
72  else:
73  optFilesRe = None
74 
75  if self.get("noOptFiles"):
76  noOptFiles = self["noOptFiles"].replace(".", r"\.") # it'll be used in an RE
77  noOptFiles = SCons.Script.Split(noOptFiles.replace(",", " "))
78  noOptFilesRe = "/(%s)$" % "|".join(noOptFiles)
79  else:
80  noOptFilesRe = None
81 
82  if self.get("opt"):
83  opt = int(self["opt"])
84  else:
85  opt = 0
86 
87  if opt == 0:
88  opt = 3
89 
90  CCFLAGS_OPT = re.sub(r"-O(\d|s)\s*", "-O%d " % opt, " ".join(self["CCFLAGS"]))
91  CCFLAGS_NOOPT = re.sub(r"-O(\d|s)\s*", "-O0 ", " ".join(self["CCFLAGS"])) # remove -O flags from CCFLAGS
92 
93  sources = []
94  for ccFile in files:
95  if optFilesRe and re.search(optFilesRe, ccFile.abspath):
96  self.SharedObject(ccFile, CCFLAGS=CCFLAGS_OPT)
97  ccFile = os.path.splitext(ccFile.abspath)[0] + self["SHOBJSUFFIX"]
98  elif noOptFilesRe and re.search(noOptFilesRe, ccFile.abspath):
99  self.SharedObject(ccFile, CCFLAGS=CCFLAGS_NOOPT)
100  ccFile = os.path.splitext(ccFile.abspath)[0] + self["SHOBJSUFFIX"]
101 
102  sources.append(ccFile)
103 
104  sources.sort()
105  return sources
106 
107 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
108 
109 ##
110 # @brief Return a list of files that need to be scanned for tags, starting at directory root
111 #
112 # These tags are for advanced Emacs users, and should not be confused with SVN tags or Doxygen tags.
113 #
114 # Files are chosen if they match fileRegex; toplevel directories in list ignoreDirs are ignored
115 # This routine won't do anything unless you specified a "TAGS" target
116 ##
117 def filesToTag(root=None, fileRegex=None, ignoreDirs=None):
118  if root is None: root = "."
119  if fileRegex is None: fileRegex = r"^[a-zA-Z0-9_].*\.(cc|h(pp)?|py)$"
120  if ignoreDirs is None: ignoreDirs = ["examples", "tests"]
121 
122  if len(filter(lambda t: t == "TAGS", SCons.Script.COMMAND_LINE_TARGETS)) == 0:
123  return []
124 
125  files = []
126  for dirpath, dirnames, filenames in os.walk(root):
127  if dirpath == ".":
128  dirnames[:] = [d for d in dirnames if not re.search(r"^(%s)$" % "|".join(ignoreDirs), d)]
129 
130  dirnames[:] = [d for d in dirnames if not re.search(r"^(\.svn)$", d)] # ignore .svn tree
131  #
132  # List of possible files to tag, but there's some cleanup required for machine-generated files
133  #
134  candidates = [f for f in filenames if re.search(fileRegex, f)]
135  #
136  # Remove files generated by swig
137  #
138  for swigFile in [f for f in filenames if re.search(r"\.i$", f)]:
139  name = os.path.splitext(swigFile)[0]
140  candidates = [f for f in candidates if not re.search(r"%s(_wrap\.cc?|\.py)$" % name, f)]
141 
142  files += [os.path.join(dirpath, f) for f in candidates]
143 
144  return files
145 
146 ##
147 # @brief Build Emacs tags (see man etags for more information).
148 #
149 # Files are chosen if they match fileRegex; toplevel directories in list ignoreDirs are ignored
150 # This routine won't do anything unless you specified a "TAGS" target
151 ##
152 @memberOf(SConsEnvironment)
153 def BuildETags(env, root=None, fileRegex=None, ignoreDirs=None):
154  toTag = filesToTag(root, fileRegex, ignoreDirs)
155  if toTag:
156  return env.Command("TAGS", toTag, "etags -o $TARGET $SOURCES")
157 
158 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
159 
160 ##
161 # @brief Remove files matching the argument list starting at dir
162 # when scons is invoked with -c/--clean and no explicit targets are listed
163 #
164 # E.g. CleanTree(r"*~ core")
165 #
166 # If recurse is True, recursively descend the file system; if
167 # verbose is True, print each filename after deleting it
168 ##
169 @memberOf(SConsEnvironment)
170 def CleanTree(self, files, dir=".", recurse=True, verbose=False):
171  #
172  # Generate command that we may want to execute
173  #
174  files_expr = ""
175  for file in SCons.Script.Split(files):
176  if files_expr:
177  files_expr += " -o "
178 
179  files_expr += "-name %s" % re.sub(r"(^|[^\\])([[*])", r"\1\\\2",file) # quote unquoted * and []
180  #
181  # don't use xargs --- who knows what needs quoting?
182  #
183  action = "find %s" % dir
184  action += r" \( -name .svn -prune -o -name \* \) "
185  if not recurse:
186  action += " ! -name . -prune"
187 
188  file_action = "rm -f"
189 
190  action += r" \( %s \) -exec %s {} \;" % \
191  (files_expr, file_action)
192 
193  if verbose:
194  action += " -print"
195  #
196  # Clean up scons files --- users want to be able to say scons -c and get a clean copy
197  # We can't delete .sconsign.dblite if we use "scons clean" instead of "scons --clean",
198  # so the former is no longer supported.
199  #
200  action += " ; rm -rf .sconf_temp .sconsign.dblite .sconsign.tmp config.log"
201  #
202  # Do we actually want to clean up? We don't if the command is e.g. "scons -c install"
203  #
204  if "clean" in SCons.Script.COMMAND_LINE_TARGETS:
205  state.log.fail("'scons clean' is no longer supported; please use 'scons --clean'.")
206  elif not SCons.Script.COMMAND_LINE_TARGETS and self.GetOption("clean"):
207  self.Execute(self.Action([action]))
208 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
209 
210 ## @brief Return a product's PRODUCT_DIR, or None
211 @memberOf(SConsEnvironment)
212 def ProductDir(env, product):
213  import eupsForScons
214  global _productDirs
215  try:
216  _productDirs
217  except:
218  try:
219  _productDirs = eupsForScons.productDir(eupsenv=eupsForScons.getEups())
220  except TypeError: # old version of eups (pre r18588)
221  _productDirs = None
222  if _productDirs:
223  pdir = _productDirs.get(product)
224  else:
225  pdir = eupsForScons.productDir(product)
226  if pdir == "none":
227  pdir = None
228  return pdir
229 
230 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
231 
232 ##
233 # @brief A callable to be used as an SCons Action to run Doxygen.
234 #
235 # This should only be used by the env.Doxygen pseudo-builder method.
236 #
237 class DoxygenBuilder(object):
238 
239  def __init__(self, **kw):
240  self.__dict__.update(kw)
241  self.results = []
242  self.sources = []
243  self.targets = []
244  self.useTags = list(SCons.Script.File(item).abspath for item in self.useTags)
245  self.inputs = list(SCons.Script.Entry(item).abspath for item in self.inputs)
246  self.excludes = list(SCons.Script.Entry(item).abspath for item in self.excludes)
247  self.outputPaths = list(SCons.Script.Dir(item) for item in self.outputs)
248 
249  def __call__(self, env, config):
250  self.findSources()
251  self.findTargets()
252  inConfigNode = SCons.Script.File(config)
253  outConfigName, ext = os.path.splitext(inConfigNode.abspath)
254  outConfigNode = SCons.Script.File(outConfigName)
255  if self.makeTag:
256  tagNode = SCons.Script.File(self.makeTag)
257  self.makeTag = tagNode.abspath
258  self.targets.append(tagNode)
259  config = env.Command(target=outConfigNode, source=inConfigNode if os.path.exists(config) else None,
260  action=self.buildConfig)
261  env.AlwaysBuild(config)
262  doc = env.Command(target=self.targets, source=self.sources,
263  action="doxygen %s" % outConfigNode.abspath)
264  for path in self.outputPaths:
265  env.Clean(doc, path)
266  env.Depends(doc, config)
267  self.results.extend(config)
268  self.results.extend(doc)
269  return self.results
270 
271  def findSources(self):
272  for path in self.inputs:
273  if os.path.isdir(path):
274  for root, dirs, files in os.walk(path):
275  if os.path.abspath(root) in self.excludes:
276  dirs[:] = []
277  continue
278  if not self.recursive:
279  dirs[:] = []
280  else:
281  toKeep = []
282  for relDir in dirs:
283  if relDir.startswith("."):
284  continue
285  absDir = os.path.abspath(os.path.join(root, relDir))
286  if absDir not in self.excludes:
287  toKeep.append(relDir)
288  dirs[:] = toKeep
289  if self.excludeSwig:
290  for relFile in files:
291  base, ext = os.path.splitext(relFile)
292  if ext == ".i":
293  self.excludes.append(os.path.join(root, base + ".py"))
294  self.excludes.append(os.path.join(root, base + "_wrap.cc"))
295  for relFile in files:
296  absFile = os.path.abspath(os.path.join(root, relFile))
297  if absFile in self.excludes:
298  continue
299  for pattern in self.patterns:
300  if fnmatch.fnmatch(relFile, pattern):
301  self.sources.append(SCons.Script.File(absFile))
302  break
303  elif os.path.isfile(path):
304  self.sources.append(SCons.Script.File(path))
305 
306  def findTargets(self):
307  for item in self.outputs:
308  self.targets.append(SCons.Script.Dir(item))
309 
310  def buildConfig(self, target, source, env):
311  outConfigFile = open(target[0].abspath, "w")
312  for tagPath in self.useTags:
313  docDir, tagFile = os.path.split(tagPath)
314  htmlDir = os.path.join(docDir, "html")
315  outConfigFile.write('TAGFILES += "%s=%s"\n' % (tagPath, htmlDir))
316  self.sources.append(SCons.Script.Dir(docDir))
317  docPaths = []
318  incFiles = []
319  for incPath in self.includes:
320  docDir, incFile = os.path.split(incPath)
321  docPaths.append('"%s"' % docDir)
322  incFiles.append('"%s"' % incFile)
323  self.sources.append(SCons.Script.File(incPath))
324  if docPaths:
325  outConfigFile.write('@INCLUDE_PATH = %s\n' % " ".join(docPaths))
326  for incFile in incFiles:
327  outConfigFile.write('@INCLUDE = %s\n' % incFile)
328  if self.projectName is not None:
329  outConfigFile.write("PROJECT_NAME = %s\n" % self.projectName)
330  if self.projectNumber is not None:
331  outConfigFile.write("PROJECT_NUMBER = %s\n" % self.projectNumber)
332  outConfigFile.write("INPUT = %s\n" % " ".join(self.inputs))
333  outConfigFile.write("EXCLUDE = %s\n" % " ".join(self.excludes))
334  outConfigFile.write("FILE_PATTERNS = %s\n" % " ".join(self.patterns))
335  outConfigFile.write("RECURSIVE = YES\n" if self.recursive else "RECURSIVE = NO\n")
336  allOutputs = set(("html", "latex", "man", "rtf", "xml"))
337  for output, path in zip(self.outputs, self.outputPaths):
338  try:
339  allOutputs.remove(output.lower())
340  except:
341  state.log.fail("Unknown Doxygen output format '%s'." % output)
342  state.log.finish()
343  outConfigFile.write("GENERATE_%s = YES\n" % output.upper())
344  outConfigFile.write("%s_OUTPUT = %s\n" % (output.upper(), path.abspath))
345  for output in allOutputs:
346  outConfigFile.write("GENERATE_%s = NO\n" % output.upper())
347  if self.makeTag is not None:
348  outConfigFile.write("GENERATE_TAGFILE = %s\n" % self.makeTag)
349  #
350  # Append the local overrides (usually doxygen.conf.in)
351  #
352  if len(source) > 0:
353  with open(source[0].abspath, "r") as inConfigFile:
354  outConfigFile.write(inConfigFile.read())
355 
356  outConfigFile.close()
357 
358 ##
359 # @brief Generate a Doxygen config file and run Doxygen on it.
360 #
361 # Rather than parse a complete Doxygen config file for SCons sources
362 # and targets, this Doxygen builder builds a Doxygen config file,
363 # adding INPUT, FILE_PATTERNS, RECUSRIVE, EXCLUDE, XX_OUTPUT and
364 # GENERATE_XX options (and possibly others) to an existing
365 # proto-config file. Generated settings will override those in
366 # the proto-config file.
367 #
368 # @param config A Doxygen config file, usually with the
369 # extension .conf.in; a new file with the .in
370 # removed will be generated and passed to
371 # Doxygen. Settings in the original config
372 # file will be overridden by those generated
373 # by this method.
374 # @param inputs A sequence of folders or files to be passed
375 # as the INPUT setting for Doxygen. This list
376 # will be turned into absolute paths by SCons,
377 # so the "#folder" syntax will work.
378 # Otherwise, the list is passed in as-is, but
379 # the builder will also examine those
380 # directories to find which source files the
381 # Doxygen output actually depends on.
382 # @param patterns A sequence of glob patterns for the
383 # FILE_PATTERNS Doxygen setting. This will be
384 # passed directly to Doxygen, but it is also
385 # used to determine which source files should
386 # be considered dependencies.
387 # @param recursive Whether the inputs should be searched
388 # recursively (used for the Doxygen RECURSIVE
389 # setting).
390 # @param outputs A sequence of output formats which will also
391 # be used as output directories.
392 # @param exclude A sequence of folders or files (not globs)
393 # to be ignored by Doxygen (the Doxygen
394 # EXCLUDE setting). Hidden directories are
395 # automatically ignored.
396 # @param includes A sequence of Doxygen config files to
397 # include. These will automatically be
398 # separated into paths and files to fill in
399 # the \@INCLUDE_PATH and \@INCLUDE settings.
400 # @param useTags A sequence of Doxygen tag files to use. It
401 # will be assumed that the html directory for
402 # each tag file is in an "html" subdirectory
403 # in the same directory as the tag file.
404 # @param makeTag A string indicating the name of a tag file
405 # to be generated.
406 # @param projectName Sets the Doxygen PROJECT_NAME setting.
407 # @param projectNumber Sets the Doxygen PROJECT_NUMBER setting.
408 # @param excludeSwig If True (default), looks for SWIG .i files
409 # in the input directories and adds Python
410 # and C++ files generated by SWIG to the
411 # list of files to exclude. For this to work,
412 # the SWIG-generated filenames must be the
413 # default ones ("module.i" generates "module.py"
414 # and "moduleLib_wrap.cc").
415 #
416 # @note When building documentation from a clean source tree,
417 # generated source files (like headers generated with M4)
418 # will not be included among the dependencies, because
419 # they aren't present when we walk the input folders.
420 # The workaround is just to build the docs after building
421 # the source.
422 ##
423 @memberOf(SConsEnvironment)
424 def Doxygen(self, config, **kw):
425  inputs = [d for d in ["#doc", "#include", "#python", "#src"]
426  if os.path.exists(SCons.Script.Entry(d).abspath)]
427  defaults = {
428  "inputs": inputs,
429  "recursive": True,
430  "patterns": ["*.h", "*.cc", "*.py", "*.dox"],
431  "outputs": ["html",],
432  "excludes": [],
433  "includes": [],
434  "useTags": [],
435  "makeTag": None,
436  "projectName": None,
437  "projectNumber": None,
438  "excludeSwig": True
439  }
440  for k in defaults:
441  if kw.get(k) is None:
442  kw[k] = defaults[k]
443  builder = DoxygenBuilder(**kw)
444  return builder(self, config)
445 
446 @memberOf(SConsEnvironment)
447 def VersionModule(self, filename, versionString=None):
448  if versionString is None:
449  for n in ("git", "hg", "svn",):
450  if os.path.isdir(".%s" % n):
451  versionString = n
452 
453  if not versionString:
454  versionString = "git"
455 
456  def calcMd5(filename):
457  try:
458  import hashlib
459  md5 = hashlib.md5("\n".join(open(filename).readlines())).hexdigest()
460  except IOError:
461  md5 = None
462 
463  return md5
464 
465  oldMd5 = calcMd5(filename)
466 
467  def makeVersionModule(target, source, env):
468  try:
469  version = determineVersion(state.env, versionString)
470  except RuntimeError:
471  version = "unknown"
472  parts = version.split("+")
473 
474  names = []
475  with open(target[0].abspath, "w") as outFile:
476  outFile.write("#--------- This file is automatically generated by LSST's sconsUtils ---------#\n")
477 
478  what = "__version__"
479  outFile.write("%s = '%s'\n" % (what, version))
480  names.append(what)
481 
482  what = "__repo_version__"
483  outFile.write("%s = '%s'\n" % (what, parts[0]))
484  names.append(what)
485 
486  what = "__repo_version__"
487  outFile.write("%s = '%s'\n" % (what, parts[0]))
488  names.append(what)
489 
490  what = "__fingerprint__"
491  outFile.write("%s = '%s'\n" % (what, getFingerprint(versionString)))
492  names.append(what)
493 
494  try:
495  info = tuple(int(v) for v in parts[0].split("."))
496  what = "__version_info__"
497  names.append(what)
498  outFile.write("%s = %r\n" % (what, info))
499  except ValueError:
500  pass
501 
502  if len(parts) > 1:
503  try:
504  what = "__rebuild_version__"
505  outFile.write("%s = %s\n" % (what, int(parts[1])))
506  names.append(what)
507  except ValueError:
508  pass
509 
510  what = "__dependency_versions__"
511  names.append(what)
512  outFile.write("%s = {\n" % (what))
513  for name, mod in env.dependencies.packages.iteritems():
514  if mod is None:
515  outFile.write(" '%s': None,\n" % name)
516  elif hasattr(mod.config, "version"):
517  outFile.write(" '%s': '%s',\n" % (name, mod.config.version))
518  else:
519  outFile.write(" '%s': 'unknown',\n" % name)
520  outFile.write("}\n")
521 
522  outFile.write("__all__ = %r\n" % (tuple(names),))
523 
524  if calcMd5(target[0].abspath) != oldMd5: # only print if something's changed
525  print "makeVersionModule([\"%s\"], [])" % str(target[0])
526 
527  result = self.Command(filename, [], self.Action(makeVersionModule, strfunction=lambda *args: None))
528 
529  self.AlwaysBuild(result)
530  return result
def getFingerprint
Return a unique fingerprint for a version (e.g.
Definition: installation.py:73
def memberOf
A Python decorator that injects functions into a class.
Definition: utils.py:84
def filesToTag
Return a list of files that need to be scanned for tags, starting at directory root.
Definition: builders.py:117
def CleanTree
Remove files matching the argument list starting at dir when scons is invoked with -c/–clean and no e...
Definition: builders.py:170
def ProductDir
Return a product's PRODUCT_DIR, or None.
Definition: builders.py:212
def determineVersion
Set a version ID from env, or a version control ID string ($name$ or $HeadURL$)
Definition: installation.py:46
def Doxygen
Generate a Doxygen config file and run Doxygen on it.
Definition: builders.py:424
def SharedLibraryIncomplete
Like SharedLibrary, but don't insist that all symbols are resolved.
Definition: builders.py:20
def BuildETags
Build Emacs tags (see man etags for more information).
Definition: builders.py:153
def SwigLoadableModule
Like LoadableModule, but don't insist that all symbols are resolved, and set some SWIG-specific flags...
Definition: builders.py:29
def SourcesForSharedLibrary
Prepare the list of files to be passed to a SharedLibrary constructor.
Definition: builders.py:60
A callable to be used as an SCons Action to run Doxygen.
Definition: builders.py:237