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