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