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
scripts.py
Go to the documentation of this file.
1 ##
2 # @file scripts.py
3 #
4 # Convenience functions to do the work of standard LSST SConstruct/SConscript files.
5 #
6 # @defgroup sconsUtilsScripts Convenience functions for SConstruct/SConscript files
7 # @{
8 ##
9 
10 import os.path
11 from SCons.Script import *
12 
13 from . import dependencies
14 from . import builders
15 from . import installation
16 from . import state
17 from . import tests
18 
19 def _getFileBase(node):
20  name, ext = os.path.splitext(os.path.basename(str(node)))
21  return name
22 
23 ##
24 # @brief A scope-only class for SConstruct-replacement convenience functions.
25 #
26 # The boilerplate for a standard LSST SConstruct file is replaced by two static methods:
27 # initialize() and finish(). The former configures dependencies, sets up package-dependent
28 # environment variables, and calls any SConscript files found in subdirectories, while the
29 # latter sets up installation paths, default targets, and explicit dependencies.
30 #
31 # Calling BasicSConstruct as a function invokes its __new__ method, which calls both
32 # initialize() and finish(), and should be used when the SConstruct file doesn't need to
33 # do anything other than what they provide.
34 ##
35 class BasicSConstruct(object):
36 
37  _initializing = False
38 
39  ##
40  # @brief Convenience function to replace standard SConstruct boilerplate.
41  #
42  # This is a shortcut for
43  # @code
44  # BasicSConstruct.initialize(...)
45  # BasicSConstruct.finalize(...)
46  # @endcode
47  #
48  # This returns the sconsUtils.env Environment object rather than
49  # a BasicSConstruct instance (which would be useless).
50  ##
51  def __new__(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None, cleanExt=None,
52  defaultTargets=("lib", "python", "tests", "examples", "doc"),
53  subDirList=None, ignoreRegex=None,
54  versionModuleName="python/lsst/%s/version.py", noCfgFile=False):
55  cls.initialize(packageName, versionString, eupsProduct, eupsProductPath, cleanExt,
56  versionModuleName, noCfgFile=noCfgFile)
57  cls.finish(defaultTargets, subDirList, ignoreRegex)
58  return state.env
59 
60  ##
61  # @brief Convenience function to replace standard SConstruct boilerplate (step 1).
62  #
63  # This function:
64  # - Calls all SConscript files found in subdirectories.
65  # - Configures dependencies.
66  # - Sets how the --clean option works.
67  #
68  # @param packageName Name of the package being built; must correspond to a .cfg file in ups/.
69  # @param versionString Version-control system string to be parsed for version information
70  # ($HeadURL$ for SVN). Defaults to "git" if not set or None.
71  # @param eupsProduct Name of the EUPS product being built. Defaults to and is almost always
72  # the name of the package.
73  # @param eupsProductPath An alternate directory where the package should be installed.
74  # @param cleanExt Whitespace delimited sequence of globs for files to remove with --clean.
75  # @param versionModuleName If non-None, builds a version.py module as this file; '%s' is replaced with
76  # the name of the package.
77  # @param noCfgFile If True, this package has no .cfg file
78  #
79  # @returns an SCons Environment object (which is also available as lsst.sconsUtils.env).
80  ##
81  @classmethod
82  def initialize(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None,
83  cleanExt=None, versionModuleName="python/lsst/%s/version.py", noCfgFile=False):
84  if cls._initializing:
85  state.log.fail("Recursion detected; an SConscript file should not call BasicSConstruct.")
86  cls._initializing = True
87  if cleanExt is None:
88  cleanExt = r"*~ core *.so *.os *.o *.pyc *.pkgc"
89  dependencies.configure(packageName, versionString, eupsProduct, eupsProductPath, noCfgFile)
90  state.env.BuildETags()
91  state.env.CleanTree(cleanExt)
92  if versionModuleName is not None:
93  try:
94  versionModuleName = versionModuleName % "/".join(packageName.split("_"))
95  except TypeError:
96  pass
97  state.targets["version"] = state.env.VersionModule(versionModuleName)
98  for root, dirs, files in os.walk("."):
99  if "SConstruct" in files and root != ".":
100  dirs[:] = []
101  continue
102  dirs[:] = [d for d in dirs if (not d.startswith('.'))]
103  dirs.sort() # happy coincidence that include < libs < python < tests
104  if "SConscript" in files:
105  state.log.info("Using Sconscript at %s/SConscript" % root)
106  SCons.Script.SConscript(os.path.join(root, "SConscript"))
107  cls._initializing = False
108  return state.env
109 
110  ##
111  # @brief Convenience function to replace standard SConstruct boilerplate (step 2).
112  #
113  # This function:
114  # - Sets up installation paths.
115  # - Tells SCons to only do MD5 checks when timestamps have changed.
116  # - Sets the "include", "lib", "python", and "tests" targets as the defaults
117  # to be built when scons is run with no target arguments.
118  #
119  # @param subDirList An explicit list of subdirectories that should be installed. By default,
120  # all non-hidden subdirectories will be installed.
121  # @param defaultTargets A sequence of targets (see state.targets) that should be built when
122  # scons is run with no arguments.
123  # @param ignoreRegex Regular expression that matches files that should not be installed.
124  #
125  # @returns an SCons Environment object (which is also available as lsst.sconsUtils.env).
126  ##
127  @staticmethod
128  def finish(defaultTargets=("lib", "python", "tests", "examples", "doc"),
129  subDirList=None, ignoreRegex=None):
130  if ignoreRegex is None:
131  ignoreRegex = r"(~$|\.pyc$|^\.svn$|\.o|\.os$)"
132  if subDirList is None:
133  subDirList = []
134  for path in os.listdir("."):
135  if os.path.isdir(path) and not path.startswith("."):
136  subDirList.append(path)
137  install = state.env.InstallLSST(state.env["prefix"],
138  [subDir for subDir in subDirList],
139  ignoreRegex=ignoreRegex)
140  for name, target in state.targets.iteritems():
141  state.env.Requires(install, target)
142  state.env.Alias(name, target)
143  state.env.Requires(state.targets["python"], state.targets["version"])
144  declarer = state.env.Declare()
145  state.env.Requires(declarer, install) # Ensure declaration fires after installation available
146  state.env.Default([t for t in defaultTargets if os.path.exists(t)])
147  if "version" in state.targets:
148  state.env.Default(state.targets["version"])
149  state.env.Requires(state.targets["tests"], state.targets["version"])
150  state.env.Decider("MD5-timestamp") # if timestamps haven't changed, don't do MD5 checks
151  #
152  # Check if any of the tests failed by looking for *.failed files.
153  # Perform this test just before scons exits
154  #
155  # N.b. the test is written in sh not python as then we can use @ to suppress output
156  #
157  if "tests" in [str(t) for t in BUILD_TARGETS]:
158  testsDir = os.path.join(os.getcwd(), "tests", ".tests")
159  checkTestStatus_command = state.env.Command('checkTestStatus', [], """
160  @ if [ -d %s ]; then \
161  nfail=`find %s -name \*.failed | wc -l | sed -e 's/ //g'`; \
162  if [ $$nfail -gt 0 ]; then \
163  echo "$$nfail tests failed" >&2; exit 1; \
164  fi; \
165  fi; \
166  """ % (testsDir, testsDir))
167 
168  state.env.Depends(checkTestStatus_command, BUILD_TARGETS) # this is why the check runs last
169  BUILD_TARGETS.extend(checkTestStatus_command)
170  state.env.AlwaysBuild(checkTestStatus_command)
171 
172 ##
173 # @brief A scope-only class for SConscript-replacement convenience functions.
174 #
175 # All methods of BasicSConscript are static. All of these functions update the state.targets
176 # dictionary of targets used to set default targets and fix build dependencies; if you build anything
177 # without using BasicSConscript methods, be sure to manually it to the state.targets dict.
178 ##
179 class BasicSConscript(object):
180 
181  ##
182  # @brief Convenience function to replace standard lib/SConscript boilerplate.
183  #
184  # With no arguments, this will build a shared library with the same name as the package.
185  # This uses env.SourcesForSharedLibrary to support the optFiles/noOptFiles command-line variables.
186  #
187  # @param libName Name of the shared libray to be built (defaults to env["packageName"]).
188  # @param src Source to compile into the library. Defaults to a 4-directory deep glob
189  # of all *.cc files in \#src.
190  # @param libs Libraries to link against, either as a string argument to be passed to
191  # env.getLibs() or a sequence of actual libraries to pass in.
192  ##
193  @staticmethod
194  def lib(libName=None, src=None, libs="self"):
195  if libName is None:
196  libName = state.env["packageName"]
197  if src is None:
198  src = Glob("#src/*.cc") + Glob("#src/*/*.cc") + Glob("#src/*/*/*.cc") + Glob("#src/*/*/*/*.cc")
199  src = state.env.SourcesForSharedLibrary(src)
200  if isinstance(libs, basestring):
201  libs = state.env.getLibs(libs)
202  elif libs is None:
203  libs = []
204  result = state.env.SharedLibrary(libName, src, LIBS=libs)
205  state.targets["lib"].extend(result)
206  return result
207 
208  ##
209  # @brief Convenience function to replace standard python/*/SConscript boilerplate.
210  #
211  # With no arguments, this will build a SWIG module with the name determined according
212  # to our current pseudo-convention: last part of env["packageName"], split by underscores,
213  # with "Lib" appended to the end.
214  #
215  # @param swigNameList Sequence of SWIG modules to be built (does not include the file extensions).
216  # @param libs Libraries to link against, either as a string argument to be passed to
217  # env.getLibs() or a sequence of actual libraries to pass in.
218  # @param swigSrc A dictionary of additional source files that go into the modules. Each
219  # key should be an entry in swigNameList, and each value should be a list
220  # of additional C++ source files not generated by SWIG.
221  ##
222  @staticmethod
223  def python(swigNameList=None, libs="main python", swigSrc=None):
224  if swigNameList is None:
225  swigNameList = [state.env["packageName"].split("_")[-1] + "Lib"]
226  swigFileList = [File(name + ".i") for name in swigNameList]
227  if swigSrc is None:
228  swigSrc = {}
229  for name, node in zip(swigNameList, swigFileList):
230  swigSrc.setdefault(name, []).append(node)
231  if isinstance(libs, basestring):
232  libs = state.env.getLibs(libs)
233  elif libs is None:
234  libs = []
235  result = []
236  for name, src in swigSrc.iteritems():
237  result.extend(state.env.SwigLoadableModule("_" + name, src, LIBS=libs))
238  state.targets["python"].extend(result)
239  return result
240 
241  ##
242  # @brief Convenience function to replace standard doc/SConscript boilerplate.
243  #
244  # With no arguments, this will generate a Doxygen config file and run Doxygen
245  # with env.Doxygen(), using the projectName and projectNumber from
246  # env["packageName"] and env["version"], respectively.
247  #
248  # This essentially just forwards all arguments (which should be passed as
249  # keyword arguments) to env.Doxygen().
250  ##
251  @staticmethod
252  def doc(config="doxygen.conf.in", projectName=None, projectNumber=None, **kw):
253  if not state.env.ProductDir("doxygen"):
254  state.log.warn("Doxygen is not setup; skipping documentation build.")
255  return []
256  if projectName is None:
257  projectName = ".".join(["lsst"] + state.env["packageName"].split("_"))
258  if projectNumber is None:
259  projectNumber = state.env["version"]
260  result = state.env.Doxygen(
261  config, projectName=projectName, projectNumber=projectNumber,
262  includes=state.env.doxygen["includes"],
263  useTags=state.env.doxygen["tags"],
264  makeTag=(state.env["packageName"] + ".tag"),
265  **kw
266  )
267  state.targets["doc"].extend(result)
268  return result
269 
270  ##
271  # @brief Convenience function to replace standard tests/SConscript boilerplate.
272  #
273  # With no arguments, will attempt to figure out which files should be run as tests
274  # and which are support code (like SWIG modules).
275  #
276  # Python tests will be marked as dependent on the entire \#python directory and
277  # any SWIG modules built in the tests directory. This should ensure tests are always
278  # run when their results might have changed, but may result in them being re-run more often
279  # than necessary.
280  #
281  # @param pyList A sequence of Python tests to run (including .py extensions).
282  # Defaults to a *.py glob of the tests directory, minus any
283  # files corresponding to the SWIG modules in swigFileList.
284  # @param ccList A sequence of C++ unit tests to run (including .cc extensions).
285  # Defaults to a *.cc glob of the tests directory, minus any
286  # files that end with *_wrap.cc and files present in swigSrc.
287  # @param swigNameList A sequence of SWIG modules to build (NOT including .i extensions).
288  # @param swigSrc Additional source files to be compiled into SWIG modules, as a
289  # dictionary; each key must be an entry in swigNameList, and each
290  # value a list of additional source files.
291  # @param ignoreList List of ignored tests to be passed to tests.Control (note that
292  # ignored tests will be built, but not run).
293  # @param nobuildList List of tests that should not even be built.
294  # @param args A dictionary of program arguments for tests, passed directly
295  # to tests.Control.
296  ##
297  @staticmethod
298  def tests(pyList=None, ccList=None, swigNameList=None, swigSrc=None,
299  ignoreList=None, noBuildList=None,
300  args=None):
301  if noBuildList is None:
302  noBuildList = []
303  if swigNameList is None:
304  swigFileList = Glob("*.i")
305  swigNameList = [_getFileBase(node) for node in swigFileList]
306  else:
307  swigFileList = [File(name + ".i") for name in swigNameList]
308  if swigSrc is None:
309  swigSrc = {}
310  allSwigSrc = set()
311  for name, node in zip(swigNameList, swigFileList):
312  src = swigSrc.setdefault(name, [])
313  allSwigSrc.update(str(element) for element in src)
314  src.append(node)
315  if pyList is None:
316  pyList = [node for node in Glob("*.py")
317  if _getFileBase(node) not in swigNameList
318  and os.path.basename(str(node)) not in noBuildList]
319  if ccList is None:
320  ccList = [node for node in Glob("*.cc")
321  if (not str(node).endswith("_wrap.cc")) and str(node) not in allSwigSrc
322  and os.path.basename(str(node)) not in noBuildList]
323  if ignoreList is None:
324  ignoreList = []
325  s = lambda l: [str(i) for i in l]
326  state.log.info("SWIG modules for tests: %s" % s(swigFileList))
327  state.log.info("Python tests: %s" % s(pyList))
328  state.log.info("C++ tests: %s" % s(ccList))
329  state.log.info("Files that will not be built: %s" % noBuildList)
330  state.log.info("Ignored tests: %s" % ignoreList)
331  control = tests.Control(state.env, ignoreList=ignoreList, args=args, verbose=True)
332  for ccTest in ccList:
333  state.env.Program(ccTest, LIBS=state.env.getLibs("main test"))
334  swigMods = []
335  for name, src in swigSrc.iteritems():
336  swigMods.extend(
337  state.env.SwigLoadableModule("_" + name, src, LIBS=state.env.getLibs("main python"))
338  )
339  ccList = [control.run(str(node)) for node in ccList]
340  pyList = [control.run(str(node)) for node in pyList]
341  for pyTest in pyList:
342  state.env.Depends(pyTest, swigMods)
343  state.env.Depends(pyTest, state.targets["python"])
344  result = ccList + pyList
345  state.targets["tests"].extend(result)
346  return result
347 
348  ##
349  # @brief Convenience function to replace standard examples/SConscript boilerplate.
350  #
351  # @param ccList A sequence of C++ examples to build (including .cc extensions).
352  # Defaults to a *.cc glob of the examples directory, minus any
353  # files that end with *_wrap.cc and files present in swigSrc.
354  # @param swigNameList A sequence of SWIG modules to build (NOT including .i extensions).
355  # @param swigSrc Additional source files to be compiled into SWIG modules, as a
356  # dictionary; each key must be an entry in swigNameList, and each
357  # value a list of additional source files.
358  ##
359  @staticmethod
360  def examples(ccList=None, swigNameList=None, swigSrc=None):
361  if swigNameList is None:
362  swigFileList = Glob("*.i")
363  swigNameList = [_getFileBase(node) for node in swigFileList]
364  else:
365  swigFileList = [File(name) for name in swigNameList]
366  if swigSrc is None:
367  swigSrc = {}
368  allSwigSrc = set()
369  for name, node in zip(swigNameList, swigFileList):
370  src = swigSrc.setdefault(name, [])
371  allSwigSrc.update(str(element) for element in src)
372  src.append(node)
373  if ccList is None:
374  ccList = [node for node in Glob("*.cc")
375  if (not str(node).endswith("_wrap.cc")) and str(node) not in allSwigSrc]
376  state.log.info("SWIG modules for examples: %s" % swigFileList)
377  state.log.info("C++ examples: %s" % ccList)
378  results = []
379  for src in ccList:
380  results.extend(state.env.Program(src, LIBS=state.env.getLibs("main")))
381  swigMods = []
382  for name, src in swigSrc.iteritems():
383  results.extend(
384  state.env.SwigLoadableModule("_" + name, src, LIBS=state.env.getLibs("main python"))
385  )
386  for result in results:
387  state.env.Depends(result, state.targets["lib"])
388  state.targets["examples"].extend(results)
389  return results
390 
391 ## @}
def lib
Convenience function to replace standard lib/SConscript boilerplate.
Definition: scripts.py:194
def initialize
Convenience function to replace standard SConstruct boilerplate (step 1).
Definition: scripts.py:83
def finish
Convenience function to replace standard SConstruct boilerplate (step 2).
Definition: scripts.py:129
def doc
Convenience function to replace standard doc/SConscript boilerplate.
Definition: scripts.py:252
A scope-only class for SConstruct-replacement convenience functions.
Definition: scripts.py:35
A scope-only class for SConscript-replacement convenience functions.
Definition: scripts.py:179
def examples
Convenience function to replace standard examples/SConscript boilerplate.
Definition: scripts.py:360
def __new__
Convenience function to replace standard SConstruct boilerplate.
Definition: scripts.py:54
A class to control unit tests.
Definition: tests.py:17
def tests
Convenience function to replace standard tests/SConscript boilerplate.
Definition: scripts.py:300
def python
Convenience function to replace standard python/*/SConscript boilerplate.
Definition: scripts.py:223