LSSTApplications  10.0+286,10.0+36,10.0+46,10.0-2-g4f67435,10.1+152,10.1+37,11.0,11.0+1,11.0-1-g47edd16,11.0-1-g60db491,11.0-1-g7418c06,11.0-2-g04d2804,11.0-2-g68503cd,11.0-2-g818369d,11.0-2-gb8b8ce7
LSSTDataManagementBasePackage
dependencies.py
Go to the documentation of this file.
1 ##
2 # @file dependencies.py
3 #
4 # Dependency configuration and definition.
5 #
6 # @defgroup sconsUtilsDependencies Dependencies and Configuration
7 # @{
8 ##
9 
10 from __future__ import absolute_import
11 import os.path
12 import collections
13 import imp
14 import sys
15 import SCons.Script
16 from . import eupsForScons
17 from SCons.Script.SConscript import SConsEnvironment
18 
19 from . import installation
20 from . import state
21 
22 ##
23 # @brief Recursively configure a package using ups/.cfg files.
24 #
25 # Aliased as lsst.sconsUtils.configure().
26 #
27 # Usually, LSST packages will call this function through scripts.BasicSConstruct.
28 #
29 # @param packageName Name of the package being built; must correspond to a .cfg file in ups/.
30 # @param versionString Version-control system string to be parsed for version information
31 # ($HeadURL$ for SVN).
32 # @param eupsProduct Name of the EUPS product being built. Defaults to and is almost always
33 # the name of the package.
34 # @param eupsProductPath An alternate directory where the package should be installed.
35 # @param noCfgFile If True, this package has no .cfg file
36 #
37 # @return an SCons Environment object (which is also available as lsst.sconsUtils.env).
38 ##
39 def configure(packageName, versionString=None, eupsProduct=None, eupsProductPath=None, noCfgFile=False):
40  if not state.env.GetOption("no_progress"):
41  state.log.info("Setting up environment to build package '%s'." % packageName)
42  if eupsProduct is None:
43  eupsProduct = packageName
44  if versionString is None:
45  versionString = "git"
46  state.env['eupsProduct'] = eupsProduct
47  state.env['packageName'] = packageName
48  #
49  # Setup installation directories and variables
50  #
51  SCons.Script.Help(state.opts.GenerateHelpText(state.env))
52  state.env.installing = [t for t in SCons.Script.BUILD_TARGETS if t == "install"]
53  state.env.declaring = [t for t in SCons.Script.BUILD_TARGETS if t == "declare" or t == "current"]
54  state.env.linkFarmDir = state.env.GetOption("linkFarmDir")
55  if state.env.linkFarmDir:
56  state.env.linkFarmDir = os.path.abspath(os.path.expanduser(state.env.linkFarmDir))
57  prefix = installation.setPrefix(state.env, versionString, eupsProductPath)
58  state.env['prefix'] = prefix
59  state.env["libDir"] = "%s/lib" % prefix
60  state.env["pythonDir"] = "%s/python" % prefix
61  #
62  # Process dependencies
63  #
64  state.log.traceback = state.env.GetOption("traceback")
65  state.log.verbose = state.env.GetOption("verbose")
66  packages = PackageTree(packageName, noCfgFile=noCfgFile)
67  state.log.flush() # if we've already hit a fatal error, die now.
68  state.env.libs = {"main":[], "python":[], "test":[]}
69  state.env.doxygen = {"tags":[], "includes":[]}
70  state.env['CPPPATH'] = []
71  state.env['LIBPATH'] = []
72 
73  # XCPPPATH is a new variable defined by sconsUtils - it's like CPPPATH, but the headers
74  # found there aren't treated as dependencies. This can make scons a lot faster.
75  state.env['XCPPPATH'] = []
76  state.env['_CPPINCFLAGS'] = \
77  "$( ${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE)}"\
78  " ${_concat(INCPREFIX, XCPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE)} $)"
79  state.env['_SWIGINCFLAGS'] = state.env['_CPPINCFLAGS'].replace("CPPPATH", "SWIGPATH")
80 
81  if state.env.linkFarmDir:
82  for d in [state.env.linkFarmDir, "#"]:
83  state.env.Append(CPPPATH=os.path.join(d, "include"))
84  state.env.Append(LIBPATH=os.path.join(d, "lib"))
85  state.env['SWIGPATH'] = state.env['CPPPATH']
86 
87  if not state.env.GetOption("clean") and not state.env.GetOption("help"):
88  packages.configure(state.env, check=state.env.GetOption("checkDependencies"))
89  for target in state.env.libs:
90  state.log.info("Libraries in target '%s': %s" % (target, state.env.libs[target]))
91  state.env.dependencies = packages
92  state.log.flush()
93 
94 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
95 
96 ##
97 # @brief Base class for defining how to configure an LSST sconsUtils package.
98 #
99 # Aliased as lsst.sconsUtils.Configuration.
100 #
101 # An ups/*.cfg file should contain an instance of this class called
102 # "config". Most LSST packages will be able to use this class directly
103 # instead of subclassing it.
104 #
105 # The only important method is configure(), which modifies an SCons
106 # environment to use the package. If a subclass overrides configure,
107 # it may not need to call the base class __init__(), whose only
108 # purpose is to define a number of instance variables used by configure().
109 ##
110 class Configuration(object):
111 
112  ## @brief Parse the name of a .cfg file, returning the package name and root directory.
113  @staticmethod
114  def parseFilename(cfgFile):
115  dir, file = os.path.split(cfgFile)
116  name, ext = os.path.splitext(file)
117  return name, os.path.abspath(os.path.join(dir, ".."))
118 
119  @staticmethod
120  def getEupsData(eupsProduct):
121  version, eupsPathDir, productDir, table, flavor = eupsForScons.getEups().findSetupVersion(eupsProduct)
122  if productDir is None:
123  productDir = eupsForScons.productDir(eupsProduct)
124  return version, productDir
125 
126  ##
127  # @brief Initialize the configuration object.
128  #
129  # @param cfgFile The name of the calling .cfg file, usually just passed in with the special
130  # variable __file__. This will be parsed to extract the package name and
131  # root.
132  # @param headers A list of headers provided by the package, to be used in autoconf-style
133  # tests.
134  # @param libs A list or dictionary of libraries provided by the package. If a dictionary
135  # is provided, libs["main"] should contain a list of regular libraries
136  # provided
137  # by the library. Other keys are "python" and "test", which refer to
138  # libraries that are only linked against compiled Python modules and unit
139  # tests, respectively. If a list is provided, the list is used as "main".
140  # These are used both for autoconf-style tests and to support
141  # env.getLibs(...), which recursively computes the libraries a package
142  # must be linked with.
143  # @param hasSwigFiles If True, the package provides SWIG interface files in "<root>/python".
144  # @param hasDoxygenInclude If True, the package provides a Doxygen include file with the
145  # name "<root>/doc/<name>.inc".
146  # @param hasDoxygenTag If True, the package generates a Doxygen TAG file.
147  # @param includeFileDirs List of directories that should be searched for include files
148  # @param libFileDirs List of directories that should be searched for libraries
149  # @param eupsProduct Name of the EUPS product for the package, if different from the name of the
150  # .cfg file.
151  ##
152  def __init__(self, cfgFile, headers=(), libs=None, hasSwigFiles=True,
153  includeFileDirs=["include",], libFileDirs=["lib",],
154  hasDoxygenInclude=False, hasDoxygenTag=True, eupsProduct=None):
155  self.name, self.root = self.parseFilename(cfgFile)
156  if eupsProduct is None:
157  eupsProduct = self.name
158  self.eupsProduct = eupsProduct
159  version, productDir = self.getEupsData(self.eupsProduct)
160  if version is not None:
161  self.version = version
162  if productDir is None:
163  state.log.warn("Could not find EUPS product dir for '%s'; using %s."
164  % (self.eupsProduct, self.root))
165  else:
166  self.root = os.path.realpath(productDir)
167  self.doxygen = {
168  # Doxygen tag files generated by this package
169  "tags": ([os.path.join(self.root, "doc", "%s.tag" % self.name)]
170  if hasDoxygenTag else []),
171  # Doxygen include files to include in the configuration of dependent products
172  "includes": ([os.path.join(self.root, "doc", "%s.inc" % self.name)]
173  if hasDoxygenInclude else [])
174  }
175  if libs is None:
176  self.libs = {
177  # Normal libraries provided by this package
178  "main": [self.name],
179  # Libraries provided that should only be linked with Python modules
180  "python":[],
181  # Libraries provided that should only be linked with unit test code
182  "test":[],
183  }
184  elif "main" in libs:
185  self.libs = libs
186  else:
187  self.libs = {"main": libs, "python": [], "test": []}
188  self.paths = {}
189  if hasSwigFiles:
190  self.paths["SWIGPATH"] = [os.path.join(self.root, "python")]
191  else:
192  self.paths["SWIGPATH"] = []
193 
194  for pathName, subDirs in [("CPPPATH", includeFileDirs),
195  ("LIBPATH", libFileDirs),]:
196  self.paths[pathName] = []
197 
198  if state.env.linkFarmDir:
199  continue
200 
201  for subDir in subDirs:
202  pathDir = os.path.join(self.root, subDir)
203  if os.path.isdir(pathDir):
204  self.paths[pathName].append(pathDir)
205 
206  self.provides = {
207  "headers": tuple(headers),
208  "libs": tuple(self.libs["main"])
209  }
210 
211  ##
212  # @brief Add custom SCons configuration tests to the Configure Context passed to the
213  # configure() method.
214  #
215  # This needs to be done up-front so we can pass in a dictionary of custom tests when
216  # calling env.Configure(), and use the same configure context for all packages.
217  #
218  # @param tests A dictionary to add custom tests to. This will be passed as the
219  # custom_tests argument to env.Configure().
220  ##
221  def addCustomTests(self, tests):
222  pass
223 
224  ##
225  # @brief Update an SCons environment to make use of the package.
226  #
227  # @param conf An SCons Configure context. The SCons Environment conf.env should be updated
228  # by the configure function.
229  # @param packages A dictionary containing the configuration modules of all dependencies (or None if
230  # the dependency was optional and was not found). The <module>.config.configure(...)
231  # method will have already been called on all dependencies.
232  # @param check If True, perform autoconf-style tests to verify that key components are in
233  # fact in place.
234  # @param build If True, this is the package currently being built, and packages in
235  # "buildRequired" and "buildOptional" dependencies will also be present in
236  # the packages dict.
237  ##
238  def configure(self, conf, packages, check=False, build=True):
239  assert(not (check and build))
240  conf.env.PrependUnique(**self.paths)
241  state.log.info("Configuring package '%s'." % self.name)
242  conf.env.doxygen["includes"].extend(self.doxygen["includes"])
243  if not build:
244  conf.env.doxygen["tags"].extend(self.doxygen["tags"])
245  for target in self.libs:
246  if target not in conf.env.libs:
247  conf.env.libs[target] = lib[target].copy()
248  state.log.info("Adding '%s' libraries to target '%s'." % (self.libs[target], target))
249  else:
250  for lib in self.libs[target]:
251  if lib not in conf.env.libs[target]:
252  conf.env.libs[target].append(lib)
253  state.log.info("Adding '%s' library to target '%s'." % (lib, target))
254  if check:
255  for header in self.provides["headers"]:
256  if not conf.CheckCXXHeader(header): return False
257  for lib in self.libs["main"]:
258  if not conf.CheckLib(lib, autoadd=False, language="C++"): return False
259  return True
260 
261 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
262 
263 ##
264 # @brief A Configuration subclass for external (third-party) packages.
265 #
266 # Aliased as lsst.sconsUtils.ExternalConfiguration.
267 #
268 # ExternalConfiguration doesn't assume the package uses SWIG or Doxygen,
269 # and tells SCons not to consider header files this package provides as dependencies
270 # (by setting XCPPPATH instead of CPPPATH). This means things SCons won't waste time
271 # looking for changes in it every time you build.
272 ##
274 
275  ##
276  # @brief Initialize the configuration object.
277  #
278  # @param cfgFile The name of the calling .cfg file, usually just passed in with the special
279  # variable __file__. This will be parsed to extract the package name and root.
280  # @param headers A list of headers provided by the package, to be used in autoconf-style tests.
281  # @param libs A list or dictionary of libraries provided by the package. If a dictionary
282  # is provided, libs["main"] should contain a list of regular libraries provided
283  # by the library. Other keys are "python" and "test", which refer to libraries
284  # that are only linked against compiled Python modules and unit tests, respectively.
285  # If a list is provided, the list is used as "main". These are used both for
286  # autoconf-style tests and to support env.getLibs(...), which recursively computes
287  # the libraries a package must be linked with.
288  ##
289  def __init__(self, cfgFile, headers=(), libs=None, eupsProduct=None):
290  Configuration.__init__(self, cfgFile, headers, libs, eupsProduct=eupsProduct, hasSwigFiles=False,
291  hasDoxygenTag=False, hasDoxygenInclude=False)
292  self.paths["XCPPPATH"] = self.paths["CPPPATH"]
293  del self.paths["CPPPATH"]
294 
295 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
296 
297 ##
298 # @brief A configuration test that checks whether a C compiler supports
299 # a particular flag.
300 #
301 # @param context Configuration context.
302 # @param flag Flag to test, e.g. "-fvisibility-inlines-hidden".
303 # @param append Automatically append the flag to context.env["CCFLAGS"]
304 # if the compiler supports it?
305 ##
306 def CustomCFlagCheck(context, flag, append=True):
307  context.Message("Checking if C compiler supports " + flag + " flag ")
308  ccflags = context.env["CCFLAGS"];
309  context.env.Append(CCFLAGS = flag)
310  result = context.TryCompile("int main(int argc, char **argv) { return 0; }", ".c")
311  context.Result(result)
312  if not append or not result:
313  context.env.Replace(CCFLAGS = ccflags)
314  return result
315 
316 ##
317 # @brief A configuration test that checks whether a C++ compiler supports
318 # a particular flag.
319 #
320 # @param context Configuration context.
321 # @param flag Flag to test, e.g. "-fvisibility-inlines-hidden".
322 # @param append Automatically append the flag to context.env["CXXFLAGS"]
323 # if the compiler supports it?
324 ##
325 def CustomCppFlagCheck(context, flag, append=True):
326  context.Message("Checking if C++ compiler supports " + flag + " flag ")
327  cxxflags = context.env["CXXFLAGS"];
328  context.env.Append(CXXFLAGS = flag)
329  result = context.TryCompile("int main(int argc, char **argv) { return 0; }", ".cc")
330  context.Result(result)
331  if not append or not result:
332  context.env.Replace(CXXFLAGS = cxxflags)
333  return result
334 
335 ##
336 # @brief A configuration test that checks whether the given source code
337 # compiles.
338 # @param context Configuration context.
339 # @param message Message disaplyed on console prior to running the test.
340 # @param source Source code to compile.
341 # param extension Identifies the language of the source code. Use ".c" for C, and ".cc"
342 # for C++ (the default).
343 ##
344 def CustomCompileCheck(context, message, source, extension=".cc"):
345  context.Message(message)
346 
347  env = context.env
348  if (env.GetOption("clean") or env.GetOption("help") or env.GetOption("no_exec")):
349  result = True
350  else:
351  result = context.TryCompile(source, extension)
352 
353  context.Result(result)
354 
355  return result
356 
357 ##
358 # @brief A configuration test that checks whether the given source code
359 # compiles and links.
360 # @param context Configuration context.
361 # @param message Message disaplyed on console prior to running the test.
362 # @param source Source code to compile.
363 # param extension Identifies the language of the source code. Use ".c" for C, and ".cc"
364 # for C++ (the default).
365 ##
366 def CustomLinkCheck(context, message, source, extension=".cc"):
367  context.Message(message)
368  result = context.TryLink(source, extension)
369  context.Result(result)
370  return result
371 
372 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
373 
374 ##
375 # @brief A class for loading and managing the dependency tree of a package, as defined by its
376 # configuration module (.cfg) file.
377 #
378 # This tree isn't actually stored as a tree; it's flattened into an ordered dictionary
379 # as it is recursively loaded.
380 #
381 # The main SCons produced by configure() and available as sconsUtils.env will contain
382 # an instance of this class as env.dependencies.
383 #
384 # Its can be used like a read-only dictionary to check whether an optional package has been
385 # configured; a package that was not found will have a value of None, while a configured
386 # package's value will be its imported .cfg module.
387 ##
388 class PackageTree(object):
389 
390  ##
391  # @brief Recursively load *.cfg files for packageName and all its dependencies.
392  #
393  # @param primaryName The name of the primary package being built.
394  # @param noCfgFile If True, this package has no .cfg file
395  #
396  # After __init__, self.primary will be set to the configuration module for the primary package,
397  # and self.packages will be an OrderedDict of dependencies (excluding self.primary), ordered
398  # such that configuration can proceed in iteration order.
399  ##
400  def __init__(self, primaryName, noCfgFile=False):
401  self.cfgPath = state.env.cfgPath
402  self.packages = collections.OrderedDict()
403  self.customTests = {
404  "CustomCFlagCheck" : CustomCFlagCheck,
405  "CustomCppFlagCheck" : CustomCppFlagCheck,
406  "CustomCompileCheck" : CustomCompileCheck,
407  "CustomLinkCheck" : CustomLinkCheck,
408  }
409  self._current = set([primaryName])
410  if noCfgFile:
411  self.primary = None
412  return
413 
414  self.primary = self._tryImport(primaryName)
415  if self.primary is None:
416  state.log.fail("Failed to load primary package configuration for %s." % primaryName)
417 
418  missingDeps = []
419  for dependency in self.primary.dependencies.get("required", ()):
420  if not self._recurse(dependency):
421  missingDeps.append(dependency)
422  if missingDeps:
423  state.log.fail("Failed to load required dependencies: \"%s\"" % '", "'.join(missingDeps))
424 
425  missingDeps = []
426  for dependency in self.primary.dependencies.get("buildRequired", ()):
427  if not self._recurse(dependency):
428  missingDeps.append(dependency)
429  if missingDeps:
430  state.log.fail("Failed to load required build dependencies: \"%s\"" % '", "'.join(missingDeps))
431 
432  for dependency in self.primary.dependencies.get("optional", ()):
433  self._recurse(dependency)
434 
435  for dependency in self.primary.dependencies.get("buildOptional", ()):
436  self._recurse(dependency)
437 
438  name = property(lambda self: self.primary.config.name)
439 
440  ## @brief Configure the entire dependency tree in order. and return an updated environment."""
441  def configure(self, env, check=False):
442  conf = env.Configure(custom_tests=self.customTests)
443  for name, module in self.packages.items():
444  if module is None:
445  state.log.info("Skipping missing optional package %s." % name)
446  continue
447  if not module.config.configure(conf, packages=self.packages, check=check, build=False):
448  state.log.fail("%s was found but did not pass configuration checks." % name)
449  if self.primary:
450  self.primary.config.configure(conf, packages=self.packages, check=False, build=True)
451  env.AppendUnique(SWIGPATH=env["CPPPATH"])
452  env.AppendUnique(XSWIGPATH=env["XCPPPATH"])
453  # reverse the order of libraries in env.libs, so libraries that fulfill a dependency
454  # of another appear after it. required by the linker to successfully resolve symbols
455  # in static libraries.
456  for target in env.libs:
457  env.libs[target].reverse()
458  env = conf.Finish()
459  return env
460 
461  def __contains__(self, name):
462  return name == self.name or name in self.packages
463 
464  has_key = __contains__
465 
466  def __getitem__(self, name):
467  if name == self.name:
468  return self.primary
469  else:
470  return self.packages[name]
471 
472  def get(self, name, default=None):
473  if name == self.name:
474  return self.primary
475  else:
476  return self.packages.get(name)
477 
478  def keys(self):
479  k = self.packages.keys()
480  k.append(self.name)
481  return k
482 
483  def _tryImport(self, name):
484  """Search for and import an individual configuration module from file."""
485  for path in self.cfgPath:
486  filename = os.path.join(path, name + ".cfg")
487  if os.path.exists(filename):
488  try:
489  module = imp.load_source(name + "_cfg", filename)
490  except Exception as e:
491  state.log.warn("Error loading configuration %s (%s)" % (filename, e))
492  continue
493  state.log.info("Using configuration for package '%s' at '%s'." % (name, filename))
494  if not hasattr(module, "dependencies") or not isinstance(module.dependencies, dict):
495  state.log.warn("Configuration module for package '%s' lacks a dependencies dict." % name)
496  return None
497  if not hasattr(module, "config") or not isinstance(module.config, Configuration):
498  state.log.warn("Configuration module for package '%s' lacks a config object." % name)
499  return None
500  else:
501  module.config.addCustomTests(self.customTests)
502  return module
503  state.log.info("Failed to import configuration for optional package '%s'." % name)
504 
505  def _recurse(self, name):
506  """Recursively load a dependency."""
507  if name in self._current:
508  state.log.fail("Detected recursive dependency involving package '%s'" % name)
509  else:
510  self._current.add(name)
511  if name in self.packages:
512  self._current.remove(name)
513  return self.packages[name] is not None
514  module = self._tryImport(name)
515  if module is None:
516  self.packages[name] = None
517  self._current.remove(name)
518  return False
519  for dependency in module.dependencies.get("required", ()):
520  if not self._recurse(dependency):
521  # We can't configure this package because a required dependency wasn't found.
522  # But this package might itself be optional, so we don't die yet.
523  self.packages[name] = None
524  self._current.remove(name)
525  state.log.warn("Could not load all dependencies for package '%s' (missing %s)." %
526  (name, dependency))
527  return False
528  for dependency in module.dependencies.get("optional", ()):
529  self._recurse(dependency)
530  # This comes last to ensure the ordering puts all dependencies first.
531  self.packages[name] = module
532  self._current.remove(name)
533  return True
534 
535 #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
536 
537 ##
538 # @brief Get the libraries the package should be linked with.
539 #
540 # @param categories A string containing whitespace-delimited categories. Standard
541 # categories are "main", "python", and "test". Default is "main".
542 # A special virtual category "self" can be provided, returning
543 # the results of targets="main" with the env["packageName"] removed.
544 #
545 # Typically, main libraries will be linked with LIBS=getLibs("self"),
546 # Python modules will be linked with LIBS=getLibs("main python") and
547 # C++-coded test programs will be linked with LIBS=getLibs("main test").
548 # """
549 def getLibs(env, categories="main"):
550  libs = []
551  removeSelf = False
552  for category in categories.split():
553  if category == "self":
554  category = "main"
555  removeSelf = True
556  for lib in env.libs[category]:
557  if lib not in libs:
558  libs.append(lib)
559  if removeSelf:
560  try:
561  libs.remove(env["packageName"])
562  except ValueError:
563  pass
564  return libs
565 
566 SConsEnvironment.getLibs = getLibs
567 
568 ## @}
Base class for defining how to configure an LSST sconsUtils package.
def CustomCppFlagCheck
A configuration test that checks whether a C++ compiler supports a particular flag.
def CustomLinkCheck
A configuration test that checks whether the given source code compiles and links.
SelectEigenView< T >::Type copy(Eigen::EigenBase< T > const &other)
Copy an arbitrary Eigen expression into a new EigenView.
Definition: eigen.h:390
def configure
Recursively configure a package using ups/.cfg files.
Definition: dependencies.py:39
def getLibs
Get the libraries the package should be linked with.
def configure
Update an SCons environment to make use of the package.
def __init__
Recursively load *.cfg files for packageName and all its dependencies.
def CustomCompileCheck
A configuration test that checks whether the given source code compiles.
def __init__
Initialize the configuration object.
def CustomCFlagCheck
A configuration test that checks whether a C compiler supports a particular flag. ...
def __init__
Initialize the configuration object.
A class for loading and managing the dependency tree of a package, as defined by its configuration mo...
A Configuration subclass for external (third-party) packages.
def configure
Configure the entire dependency tree in order.
def addCustomTests
Add custom SCons configuration tests to the Configure Context passed to the configure() method...
def parseFilename
Parse the name of a .cfg file, returning the package name and root directory.