LSSTApplications  16.0-10-g0ee56ad+4,16.0-11-ga33d1f2+4,16.0-12-g3ef5c14+2,16.0-12-g71e5ef5+17,16.0-12-gbdf3636+2,16.0-13-g118c103+2,16.0-13-g8f68b0a+2,16.0-15-gbf5c1cb+3,16.0-16-gfd17674+2,16.0-17-g7c01f5c+2,16.0-18-g0a50484,16.0-20-ga20f992+7,16.0-21-g0e05fd4+5,16.0-21-g15e2d33+3,16.0-22-g62d8060+3,16.0-22-g847a80f+3,16.0-25-gf00d9b8,16.0-28-g3990c221+3,16.0-3-gf928089+2,16.0-32-g88a4f23+4,16.0-34-gd7987ad+2,16.0-37-gc7333cb+1,16.0-4-g10fc685+1,16.0-4-g18f3627+25,16.0-4-g5f3a788+25,16.0-5-gaf5c3d7+3,16.0-5-gcc1f4bb,16.0-6-g3b92700+3,16.0-6-g4412fcd+2,16.0-6-g7235603+3,16.0-69-g2562ce1b+1,16.0-7-g0913a87,16.0-8-g14ebd58+3,16.0-8-g2df868b,16.0-8-g4cec79c+5,16.0-8-gadf6c7a,16.0-82-g59ec2a54a,16.0-9-g5400cdc+1,16.0-9-ge6233d7+4,master-g2880f2d8cf+2,v17.0.rc1
LSSTDataManagementBasePackage
scripts.py
Go to the documentation of this file.
1 
9 import os.path
10 import re
11 import pipes
12 from stat import ST_MODE
13 from SCons.Script import SConscript, File, Dir, Glob, BUILD_TARGETS
14 from distutils.spawn import find_executable
15 
16 from . import dependencies
17 from . import state
18 from . import tests
19 from . import utils
20 
21 DEFAULT_TARGETS = ("lib", "python", "shebang", "tests", "examples", "doc")
22 
23 
24 def _getFileBase(node):
25  name, ext = os.path.splitext(os.path.basename(str(node)))
26  return name
27 
28 
29 
42 
43  _initializing = False
44 
45 
57  def __new__(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None, cleanExt=None,
58  defaultTargets=DEFAULT_TARGETS,
59  subDirList=None, ignoreRegex=None,
60  versionModuleName="python/lsst/%s/version.py", noCfgFile=False,
61  sconscriptOrder=None, disableCc=False):
62  cls.initialize(packageName, versionString, eupsProduct, eupsProductPath, cleanExt,
63  versionModuleName, noCfgFile=noCfgFile, sconscriptOrder=sconscriptOrder,
64  disableCc=disableCc)
65  cls.finish(defaultTargets, subDirList, ignoreRegex)
66  return state.env
67 
68 
101  @classmethod
102  def initialize(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None,
103  cleanExt=None, versionModuleName="python/lsst/%s/version.py", noCfgFile=False,
104  sconscriptOrder=None, disableCc=False):
105  if not disableCc:
106  state._configureCommon()
107  state._saveState()
108  if cls._initializing:
109  state.log.fail("Recursion detected; an SConscript file should not call BasicSConstruct.")
110  cls._initializing = True
111  dependencies.configure(packageName, versionString, eupsProduct, eupsProductPath, noCfgFile)
112  state.env.BuildETags()
113  if cleanExt is None:
114  cleanExt = r"*~ core core.[1-9]* *.so *.os *.o *.pyc *.pkgc"
115  state.env.CleanTree(cleanExt, ".cache __pycache__ .pytest_cache")
116  if versionModuleName is not None:
117  try:
118  versionModuleName = versionModuleName % "/".join(packageName.split("_"))
119  except TypeError:
120  pass
121  state.targets["version"] = state.env.VersionModule(versionModuleName)
122  scripts = []
123  for root, dirs, files in os.walk("."):
124  if "SConstruct" in files and root != ".":
125  dirs[:] = []
126  continue
127  dirs[:] = [d for d in dirs if not d.startswith('.')]
128  dirs.sort() # os.walk order is not specified, but we want builds to be deterministic
129  if "SConscript" in files:
130  scripts.append(os.path.join(root, "SConscript"))
131  if sconscriptOrder is None:
132  sconscriptOrder = DEFAULT_TARGETS
133 
134  # directory for shebang target is bin.src
135  sconscriptOrder = [t if t != "shebang" else "bin.src" for t in sconscriptOrder]
136 
137  def key(path):
138  for i, item in enumerate(sconscriptOrder):
139  if path.lstrip("./").startswith(item):
140  return i
141  return len(sconscriptOrder)
142  scripts.sort(key=key)
143  for script in scripts:
144  state.log.info("Using SConscript at %s" % script)
145  SConscript(script)
146  cls._initializing = False
147  return state.env
148 
149 
166  @staticmethod
167  def finish(defaultTargets=DEFAULT_TARGETS,
168  subDirList=None, ignoreRegex=None):
169  if ignoreRegex is None:
170  ignoreRegex = r"(~$|\.pyc$|^\.svn$|\.o|\.os$)"
171  if subDirList is None:
172  subDirList = []
173  for path in os.listdir("."):
174  if os.path.isdir(path) and not path.startswith("."):
175  subDirList.append(path)
176  install = state.env.InstallLSST(state.env["prefix"],
177  [subDir for subDir in subDirList],
178  ignoreRegex=ignoreRegex)
179  for name, target in state.targets.items():
180  state.env.Requires(install, target)
181  state.env.Alias(name, target)
182  state.env.Requires(state.targets["python"], state.targets["version"])
183  declarer = state.env.Declare()
184  state.env.Requires(declarer, install) # Ensure declaration fires after installation available
185 
186  # shebang should be in the list if bin.src exists but the location matters
187  # so we can not append it afterwards.
188  state.env.Default([t for t in defaultTargets
189  if os.path.exists(t) or (t == "shebang" and os.path.exists("bin.src"))])
190  if "version" in state.targets:
191  state.env.Default(state.targets["version"])
192  state.env.Requires(state.targets["tests"], state.targets["version"])
193  state.env.Decider("MD5-timestamp") # if timestamps haven't changed, don't do MD5 checks
194  #
195  # Check if any of the tests failed by looking for *.failed files.
196  # Perform this test just before scons exits
197  #
198  # N.b. the test is written in sh not python as then we can use @ to suppress output
199  #
200  if "tests" in [str(t) for t in BUILD_TARGETS]:
201  testsDir = pipes.quote(os.path.join(os.getcwd(), "tests", ".tests"))
202  checkTestStatus_command = state.env.Command('checkTestStatus', [], """
203  @ if [ -d {0} ]; then \
204  nfail=`find {0} -name "*.failed" | wc -l | sed -e 's/ //g'`; \
205  if [ $$nfail -gt 0 ]; then \
206  echo "Failed test output:" >&2; \
207  for f in `find {0} -name "*.failed"`; do \
208  case "$$f" in \
209  *.xml.failed) \
210  echo "Global pytest output is in $$f" >&2; \
211  ;; \
212  *.failed) \
213  cat $$f >&2; \
214  ;; \
215  esac; \
216  done; \
217  echo "The following tests failed:" >&2;\
218  find {0} -name "*.failed" >&2; \
219  echo "$$nfail tests failed" >&2; exit 1; \
220  fi; \
221  fi; \
222  """.format(testsDir))
223 
224  state.env.Depends(checkTestStatus_command, BUILD_TARGETS) # this is why the check runs last
225  BUILD_TARGETS.extend(checkTestStatus_command)
226  state.env.AlwaysBuild(checkTestStatus_command)
227 
228 
229 
237 
238 
251  @staticmethod
252  def lib(libName=None, src=None, libs="self", noBuildList=None):
253  if libName is None:
254  libName = state.env["packageName"]
255  if src is None:
256  src = Glob("#src/*.cc") + Glob("#src/*/*.cc") + Glob("#src/*/*/*.cc") + Glob("#src/*/*/*/*.cc")
257  if noBuildList is not None:
258  src = [node for node in src if os.path.basename(str(node)) not in noBuildList]
259  src = state.env.SourcesForSharedLibrary(src)
260  if isinstance(libs, str):
261  libs = state.env.getLibs(libs)
262  elif libs is None:
263  libs = []
264  result = state.env.SharedLibrary(libName, src, LIBS=libs)
265  state.targets["lib"].extend(result)
266  return result
267 
268 
280  @staticmethod
281  def shebang(src=None):
282  # check if Python is called on the first line with this expression
283  # This comes from distutils copy_scripts
284  FIRST_LINE_RE = re.compile(r'^#!.*python[0-9.]*([ \t].*)?$')
285  doRewrite = utils.needShebangRewrite()
286 
287  def rewrite_shebang(target, source, env):
288  """Copy source to target, rewriting the shebang"""
289  # Currently just use this python
290  usepython = utils.whichPython()
291  for targ, src in zip(target, source):
292  with open(str(src), "r") as srcfd:
293  with open(str(targ), "w") as outfd:
294  first_line = srcfd.readline()
295  # Always match the first line so we can warn people
296  # if an attempt is being made to rewrite a file that should
297  # not be rewritten
298  match = FIRST_LINE_RE.match(first_line)
299  if match and doRewrite:
300  post_interp = match.group(1) or ''
301  # Paths can be long so ensure that flake8 won't complain
302  outfd.write("#!{}{} # noqa\n".format(usepython, post_interp))
303  else:
304  if not match:
305  state.log.warn("Could not rewrite shebang of {}. Please check"
306  " file or move it to bin directory.".format(str(src)))
307  outfd.write(first_line)
308  for line in srcfd.readlines():
309  outfd.write(line)
310  # Ensure the bin/ file is executable
311  oldmode = os.stat(str(targ))[ST_MODE] & 0o7777
312  newmode = (oldmode | 0o555) & 0o7777
313  if newmode != oldmode:
314  state.log.info("changing mode of {} from {} to {}".format(
315  str(targ), oldmode, newmode))
316  os.chmod(str(targ), newmode)
317 
318  if src is None:
319  src = Glob("#bin.src/*")
320  for s in src:
321  filename = str(s)
322  # Do not try to rewrite files starting with non-letters
323  if filename != "SConscript" and re.match("[A-Za-z]", filename):
324  result = state.env.Command(target=os.path.join(Dir("#bin").abspath, filename),
325  source=s, action=rewrite_shebang)
326  state.targets["shebang"].extend(result)
327 
328 
348  @staticmethod
349  def python(module=None, src=None, extra=(), libs="main python"):
350  if module is None:
351  module = "_" + state.env["packageName"].split("_")[-1]
352  if src is None:
353  src = state.env.Glob("*.cc")
354  src.extend(extra)
355  if isinstance(libs, str):
356  libs = state.env.getLibs(libs)
357  elif libs is None:
358  libs = []
359  result = state.env.Pybind11LoadableModule(module, src, LIBS=libs)
360  state.targets["python"].append(result)
361  return result
362 
363 
383  @staticmethod
384  def pybind11(nameList=[], libs="main python", extraSrc=None, addUnderscore=True):
385  srcList = extraSrc
386  if srcList is None:
387  srcList = dict([(name, []) for name in nameList])
388  for name in nameList:
389  srcList[name].append(name + ".cc")
390  if isinstance(libs, str):
391  libs = state.env.getLibs(libs)
392  elif libs is None:
393  libs = []
394  result = []
395  for name in nameList:
396  # TODO remove this block and the `addUnderscore` argument and always use pyLibName = name;
397  # but we can't do that until all our pybind11 SConscript files have been converted
398  if addUnderscore:
399  if name.startswith("_"):
400  pyLibName = name
401  else:
402  pyLibName = "_" + name
403  else:
404  pyLibName = name
405  result.extend(state.env.Pybind11LoadableModule(pyLibName, srcList[name], LIBS=libs))
406  state.targets["python"].extend(result)
407  return result
408 
409 
419  @staticmethod
420  def doc(config="doxygen.conf.in", projectName=None, projectNumber=None, **kw):
421  if not find_executable("doxygen"):
422  state.log.warn("doxygen executable not found; skipping documentation build.")
423  return []
424  if projectName is None:
425  projectName = ".".join(["lsst"] + state.env["packageName"].split("_"))
426  if projectNumber is None:
427  projectNumber = state.env["version"]
428  result = state.env.Doxygen(
429  config, projectName=projectName, projectNumber=projectNumber,
430  includes=state.env.doxygen["includes"],
431  useTags=state.env.doxygen["tags"],
432  makeTag=(state.env["packageName"] + ".tag"),
433  **kw
434  )
435  state.targets["doc"].extend(result)
436  return result
437 
438 
472  @staticmethod
473  def tests(pyList=None, ccList=None, swigNameList=None, swigSrc=None,
474  ignoreList=None, noBuildList=None, pySingles=None,
475  args=None):
476  if noBuildList is None:
477  noBuildList = []
478  if pySingles is None:
479  pySingles = []
480  if swigNameList is None:
481  swigFileList = Glob("*.i")
482  swigNameList = [_getFileBase(node) for node in swigFileList]
483  else:
484  swigFileList = [File(name + ".i") for name in swigNameList]
485  if swigSrc is None:
486  swigSrc = {}
487  allSwigSrc = set()
488  for name, node in zip(swigNameList, swigFileList):
489  src = swigSrc.setdefault(name, [])
490  allSwigSrc.update(str(element) for element in src)
491  src.append(node)
492  if pyList is None:
493  pyList = [node for node in Glob("*.py")
494  if _getFileBase(node) not in swigNameList and
495  os.path.basename(str(node)) not in noBuildList]
496  # if we got no matches, reset to None so we do not enabled
497  # auto test detection in pytest
498  if not pyList:
499  pyList = None
500  if ccList is None:
501  ccList = [node for node in Glob("*.cc")
502  if (not str(node).endswith("_wrap.cc")) and str(node) not in allSwigSrc and
503  os.path.basename(str(node)) not in noBuildList]
504  if ignoreList is None:
505  ignoreList = []
506 
507  def s(l):
508  if l is None:
509  return ['None']
510  return [str(i) for i in l]
511 
512  state.log.info("SWIG modules for tests: %s" % s(swigFileList))
513  state.log.info("Python tests: %s" % s(pyList))
514  state.log.info("C++ tests: %s" % s(ccList))
515  state.log.info("Files that will not be built: %s" % noBuildList)
516  state.log.info("Ignored tests: %s" % ignoreList)
517  control = tests.Control(state.env, ignoreList=ignoreList, args=args, verbose=True)
518  for ccTest in ccList:
519  state.env.Program(ccTest, LIBS=state.env.getLibs("main test"))
520  swigMods = []
521  for name, src in swigSrc.items():
522  swigMods.extend(
523  state.env.SwigLoadableModule("_" + name, src, LIBS=state.env.getLibs("main python"))
524  )
525 
526  # Warn about insisting that a test in pySingles starts with test_ and
527  # therefore might be automatically discovered by pytest. These files
528  # should not be discovered automatically.
529  for node in pySingles:
530  if str(node).startswith("test_"):
531  state.log.warn("Warning: {} should be run independently but"
532  " can be automatically discovered".format(node))
533 
534  # Ensure that python tests listed in pySingles are not included in pyList.
535  if pyList is not None:
536  pyList = [str(node) for node in pyList if str(node) not in pySingles]
537 
538  ccList = [control.run(str(node)) for node in ccList]
539  pySingles = [control.run(str(node)) for node in pySingles]
540 
541  # If we tried to discover .py files and found none, do not then
542  # try to use auto test discovery.
543  if pyList is not None:
544  pyList = [control.runPythonTests(pyList)]
545  else:
546  pyList = []
547  pyList.extend(pySingles)
548  for pyTest in pyList:
549  state.env.Depends(pyTest, ccList)
550  state.env.Depends(pyTest, swigMods)
551  state.env.Depends(pyTest, state.targets["python"])
552  state.env.Depends(pyTest, state.targets["shebang"])
553  result = ccList + pyList
554  state.targets["tests"].extend(result)
555  return result
556 
557 
568  @staticmethod
569  def examples(ccList=None, swigNameList=None, swigSrc=None):
570  if swigNameList is None:
571  swigFileList = Glob("*.i")
572  swigNameList = [_getFileBase(node) for node in swigFileList]
573  else:
574  swigFileList = [File(name) for name in swigNameList]
575  if swigSrc is None:
576  swigSrc = {}
577  allSwigSrc = set()
578  for name, node in zip(swigNameList, swigFileList):
579  src = swigSrc.setdefault(name, [])
580  allSwigSrc.update(str(element) for element in src)
581  src.append(node)
582  if ccList is None:
583  ccList = [node for node in Glob("*.cc")
584  if (not str(node).endswith("_wrap.cc")) and str(node) not in allSwigSrc]
585  state.log.info("SWIG modules for examples: %s" % swigFileList)
586  state.log.info("C++ examples: %s" % ccList)
587  results = []
588  for src in ccList:
589  results.extend(state.env.Program(src, LIBS=state.env.getLibs("main")))
590  for name, src in swigSrc.items():
591  results.extend(
592  state.env.SwigLoadableModule("_" + name, src, LIBS=state.env.getLibs("main python"))
593  )
594  for result in results:
595  state.env.Depends(result, state.targets["lib"])
596  state.targets["examples"].extend(results)
597  return results
598 
599 
def doc(config="doxygen.conf.in", projectName=None, projectNumber=None, kw)
Convenience function to replace standard doc/SConscript boilerplate.
Definition: scripts.py:420
def examples(ccList=None, swigNameList=None, swigSrc=None)
Convenience function to replace standard examples/SConscript boilerplate.
Definition: scripts.py:569
def lib(libName=None, src=None, libs="self", noBuildList=None)
Convenience function to replace standard lib/SConscript boilerplate.
Definition: scripts.py:252
def shebang(src=None)
Handles shebang rewriting.
Definition: scripts.py:281
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def initialize(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None, cleanExt=None, versionModuleName="python/lsst/%s/version.py", noCfgFile=False, sconscriptOrder=None, disableCc=False)
Convenience function to replace standard SConstruct boilerplate (step 1).
Definition: scripts.py:104
daf::base::PropertySet * set
Definition: fits.cc:832
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:168
def python(module=None, src=None, extra=(), libs="main python")
Convenience function to replace standard python/*/SConscript boilerplate.
Definition: scripts.py:349
def pybind11(nameList=[], libs="main python", extraSrc=None, addUnderscore=True)
Convenience function to replace standard python/*/SConscript boilerplate.
Definition: scripts.py:384
A scope-only class for SConstruct-replacement convenience functions.
Definition: scripts.py:41
solver_t * s
A scope-only class for SConscript-replacement convenience functions.
Definition: scripts.py:236
Key< U > key
Definition: Schema.cc:281
def tests(pyList=None, ccList=None, swigNameList=None, swigSrc=None, ignoreList=None, noBuildList=None, pySingles=None, args=None)
Convenience function to replace standard tests/SConscript boilerplate.
Definition: scripts.py:475
def __new__(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None, cleanExt=None, defaultTargets=DEFAULT_TARGETS, subDirList=None, ignoreRegex=None, versionModuleName="python/lsst/%s/version.py", noCfgFile=False, sconscriptOrder=None, disableCc=False)
Convenience function to replace standard SConstruct boilerplate.
Definition: scripts.py:61
A class to control unit tests.
Definition: tests.py:21
def finish(defaultTargets=DEFAULT_TARGETS, subDirList=None, ignoreRegex=None)
Convenience function to replace standard SConstruct boilerplate (step 2).
Definition: scripts.py:168