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