Coverage for python/lsst/sconsUtils/dependencies.py: 10%
290 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-24 23:29 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-24 23:29 -0700
2"""Dependency configuration and definition."""
4__all__ = ("Configuration", "ExternalConfiguration", "PackageTree", "configure")
6import os
7import os.path
8import collections
9import importlib
10from sys import platform
11import SCons.Script
12from . import eupsForScons
13from SCons.Script.SConscript import SConsEnvironment
15from . import installation
16from . import state
17from .utils import get_conda_prefix, use_conda_compilers
20def configure(packageName, versionString=None, eupsProduct=None, eupsProductPath=None, noCfgFile=False):
21 """Recursively configure a package using ups/.cfg files.
23 Aliased as `lsst.sconsUtils.configure()`.
25 Usually, LSST packages will call this function through
26 `~lsst.sconsUtils.scripts.BasicSConstruct`.
28 Parameters
29 ----------
30 packageName : `str`
31 Name of the package being built; must correspond to a ``.cfg`` file
32 in the ``ups`` directory.
33 versionString : `str`, optional
34 Version-control system string to be parsed for version information
35 (``$HeadURL$`` for SVN).
36 eupsProduct : `str`, optional
37 Name of the EUPS product being built. Defaults to and is almost
38 always the name of the package.
39 eupsProductPath : `str`, optional
40 An alternate directory where the package should be installed.
41 noCfgFile : `bool`
42 If True, this package has no ``.cfg`` file.
44 Returns
45 -------
46 env : `SCons.Environment`
47 An SCons Environment object (which is also available as
48 `lsst.sconsUtils.env`).
49 """
51 if not state.env.GetOption("no_progress"):
52 state.log.info("Setting up environment to build package '%s'." % packageName)
53 if eupsProduct is None:
54 eupsProduct = packageName
55 if versionString is None:
56 versionString = "git"
57 state.env['eupsProduct'] = eupsProduct
58 state.env['packageName'] = packageName
59 #
60 # Setup installation directories and variables
61 #
62 SCons.Script.Help(state.opts.GenerateHelpText(state.env))
63 state.env.installing = [t for t in SCons.Script.BUILD_TARGETS if t == "install"]
64 state.env.declaring = [t for t in SCons.Script.BUILD_TARGETS if t == "declare" or t == "current"]
65 state.env.linkFarmDir = state.env.GetOption("linkFarmDir")
66 if state.env.linkFarmDir:
67 state.env.linkFarmDir = os.path.abspath(os.path.expanduser(state.env.linkFarmDir))
68 prefix = installation.setPrefix(state.env, versionString, eupsProductPath)
69 state.env['prefix'] = prefix
70 state.env["libDir"] = "%s/lib" % prefix
71 state.env["pythonDir"] = "%s/python" % prefix
72 #
73 # Process dependencies
74 #
75 state.log.traceback = state.env.GetOption("traceback")
76 state.log.verbose = state.env.GetOption("verbose")
77 packages = PackageTree(packageName, noCfgFile=noCfgFile)
78 state.log.flush() # if we've already hit a fatal error, die now.
79 state.env.libs = {"main": [], "python": [], "test": []}
80 state.env.doxygen = {"tags": [], "includes": []}
81 state.env['CPPPATH'] = []
83 _conda_prefix = get_conda_prefix()
84 # _conda_prefix is usually around, even if not using conda compilers
85 if use_conda_compilers():
86 # if using the conda-force conda compilers, they handle rpath for us
87 _conda_lib = f"{_conda_prefix}/lib"
88 state.env['LIBPATH'] = [_conda_lib]
89 if platform == "darwin":
90 state.env["_RPATH"] = f"-rpath {_conda_lib}"
91 else:
92 state.env.AppendUnique(RPATH=[_conda_lib])
93 else:
94 state.env['LIBPATH'] = []
96 # XCPPPATH is a new variable defined by sconsUtils - it's like CPPPATH,
97 # but the headers found there aren't treated as dependencies. This can
98 # make scons a lot faster.
99 state.env['XCPPPATH'] = []
101 if use_conda_compilers():
102 state.env.Append(XCPPPATH=["%s/include" % _conda_prefix])
104 # XCPPPPREFIX is a replacement for SCons' built-in INCPREFIX. It is used
105 # when compiling headers in XCPPPATH directories. Here, we set it to
106 # `-isystem`, so that those are regarded as "system headers" and warnings
107 # are suppressed.
108 state.env['XCPPPREFIX'] = "-isystem "
110 state.env['_CPPINCFLAGS'] = \
111 "$( ${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE)}"\
112 " ${_concat(XCPPPREFIX, XCPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE)} $)"
113 state.env['_SWIGINCFLAGS'] = state.env['_CPPINCFLAGS'] \
114 .replace("CPPPATH", "SWIGPATH") \
115 .replace("XCPPPREFIX", "SWIGINCPREFIX")
117 if state.env.linkFarmDir:
118 for d in [state.env.linkFarmDir, "#"]:
119 state.env.Append(CPPPATH=os.path.join(d, "include"))
120 state.env.Append(LIBPATH=os.path.join(d, "lib"))
121 state.env['SWIGPATH'] = state.env['CPPPATH']
123 if not state.env.GetOption("clean") and not state.env.GetOption("help"):
124 packages.configure(state.env, check=state.env.GetOption("checkDependencies"))
125 for target in state.env.libs:
126 state.log.info("Libraries in target '%s': %s" % (target, state.env.libs[target]))
127 state.env.dependencies = packages
128 state.log.flush()
131class Configuration:
132 """Base class for defining how to configure an LSST sconsUtils package.
134 Aliased as `lsst.sconsUtils.Configuration`.
136 An ``ups/*.cfg`` file should contain an instance of this class called
137 "config". Most LSST packages will be able to use this class directly
138 instead of subclassing it.
140 The only important method is configure(), which modifies an SCons
141 environment to use the package. If a subclass overrides configure,
142 it may not need to call the base class ``__init__()``, whose only
143 purpose is to define a number of instance variables used by configure().
145 Parameters
146 ----------
147 cfgFile : `str`
148 The name of the calling .cfg file, usually just passed in with the
149 special variable ``__file__``. This will be parsed to extract the
150 package name and root.
151 headers : `list` of `str`, optional
152 A list of headers provided by the package, to be used in autoconf-style
153 tests.
154 libs : `list` or `dict`, optional
155 A list or dictionary of libraries provided by the package. If a
156 dictionary is provided, ``libs["main"]`` should contain a list of
157 regular libraries provided by the library. Other keys are "python"
158 and "test", which refer to libraries that are only linked against
159 compiled Python modules and unit tests, respectively. If a list is
160 provided, the list is used as "main". These are used both for
161 autoconf-style tests and to support ``env.getLibs(...)``, which
162 recursively computes the libraries a package must be linked with.
163 hasSwigFiles : `bool`, optional
164 If True, the package provides SWIG interface files in
165 ``<root>/python``.
166 hasDoxygenInclude : `bool`, optional
167 If True, the package provides a Doxygen include file with the
168 name ``<root>/doc/<name>.inc``.
169 hasDoxygenTag : `bool`, optional
170 If True, the package generates a Doxygen TAG file.
171 includeFileDirs : `list`, optional
172 List of directories that should be searched for include files.
173 libFileDirs : `list`, optional
174 List of directories that should be searched for libraries.
175 eupsProduct : `str`
176 Name of the EUPS product for the package, if different from the name
177 of the ``.cfg`` file.
178 """
180 @staticmethod
181 def parseFilename(cfgFile):
182 """Parse the name of a .cfg file and return package name and root.
184 Parameters
185 ----------
186 cfgFile : `str`
187 Name of a ``.cfg`` file.
189 Returns
190 -------
191 name : `str`
192 Package name
193 root : `str`
194 Root directory.
195 """
196 dir, file = os.path.split(cfgFile)
197 name, ext = os.path.splitext(file)
198 return name, os.path.abspath(os.path.join(dir, ".."))
200 @staticmethod
201 def getEupsData(eupsProduct):
202 """Get EUPS version and product directory for named product.
204 Parameters
205 ----------
206 eupsProduct : `str`
207 EUPS product name.
209 Returns
210 -------
211 version : `str`
212 EUPS product version.
213 productDir : `str`
214 EUPS product root directory.
215 """
216 version, eupsPathDir, productDir, table, flavor = eupsForScons.getEups().findSetupVersion(eupsProduct)
217 if productDir is None:
218 productDir = eupsForScons.productDir(eupsProduct)
219 return version, productDir
221 def __init__(self, cfgFile, headers=(), libs=None, hasSwigFiles=True,
222 includeFileDirs=["include"], libFileDirs=["lib"],
223 hasDoxygenInclude=False, hasDoxygenTag=True, eupsProduct=None):
224 self.name, self.root = self.parseFilename(cfgFile)
225 if eupsProduct is None:
226 eupsProduct = self.name
227 self.eupsProduct = eupsProduct
228 version, productDir = self.getEupsData(self.eupsProduct)
229 if version is not None:
230 self.version = version
231 if productDir is None:
232 state.log.info("Could not find EUPS product dir for '%s'; using %s."
233 % (self.eupsProduct, self.root))
234 else:
235 self.root = os.path.realpath(productDir)
236 self.doxygen = {
237 # Doxygen tag files generated by this package
238 "tags": ([os.path.join(self.root, "doc", "%s.tag" % self.name)]
239 if hasDoxygenTag else []),
240 # Doxygen include files to include in the configuration of
241 # dependent products
242 "includes": ([os.path.join(self.root, "doc", "%s.inc" % self.name)]
243 if hasDoxygenInclude else [])
244 }
245 if libs is None:
246 self.libs = {
247 # Normal libraries provided by this package
248 "main": [self.name],
249 # Libraries provided that should only be linked with Python
250 # modules
251 "python": [],
252 # Libraries provided that should only be linked with unit
253 # test code
254 "test": [],
255 }
256 elif "main" in libs:
257 self.libs = libs
258 else:
259 self.libs = {"main": libs, "python": [], "test": []}
260 self.paths = {}
261 if hasSwigFiles:
262 self.paths["SWIGPATH"] = [os.path.join(self.root, "python")]
263 else:
264 self.paths["SWIGPATH"] = []
266 for pathName, subDirs in [("CPPPATH", includeFileDirs),
267 ("LIBPATH", libFileDirs)]:
268 self.paths[pathName] = []
270 if state.env.linkFarmDir:
271 continue
273 for subDir in subDirs:
274 pathDir = os.path.join(self.root, subDir)
275 if os.path.isdir(pathDir):
276 self.paths[pathName].append(pathDir)
278 self.provides = {
279 "headers": tuple(headers),
280 "libs": tuple(self.libs["main"])
281 }
283 def addCustomTests(self, tests):
284 """Add custom SCons configuration tests to the Configure Context
285 passed to the configure() method.
287 This needs to be done up-front so we can pass in a dictionary of
288 custom tests when calling ``env.Configure()``, and use the same
289 configure context for all packages.
291 Parameters
292 ----------
293 tests : `dict`
294 A dictionary to add custom tests to. This will be passed as the
295 custom_tests argument to ``env.Configure()``.
296 """
297 pass
299 def configure(self, conf, packages, check=False, build=True):
300 """Update an SCons environment to make use of the package.
302 Parameters
303 ----------
304 conf : `SCons.Configure`
305 An SCons Configure context. The SCons Environment conf.env
306 should be updated by the configure function.
307 packages : `dict`
308 A dictionary containing the configuration modules of all
309 dependencies (or `None` if the dependency was optional and was not
310 found). The ``<module>.config.configure(...)`` method will have
311 already been called on all dependencies.
312 check : `bool`, optional
313 If True, perform autoconf-style tests to verify that key
314 components are in fact in place.
315 build : `bool`, optional
316 If True, this is the package currently being built, and packages in
317 "buildRequired" and "buildOptional" dependencies will also be
318 present in the packages dict.
319 """
320 assert(not (check and build))
321 conf.env.PrependUnique(**self.paths)
322 state.log.info("Configuring package '%s'." % self.name)
323 conf.env.doxygen["includes"].extend(self.doxygen["includes"])
324 if not build:
325 conf.env.doxygen["tags"].extend(self.doxygen["tags"])
326 for target in self.libs:
327 if target not in conf.env.libs:
328 conf.env.libs[target] = self.libs[target].copy()
329 state.log.info("Adding '%s' libraries to target '%s'." % (self.libs[target], target))
330 else:
331 for lib in self.libs[target]:
332 if lib not in conf.env.libs[target]:
333 conf.env.libs[target].append(lib)
334 state.log.info("Adding '%s' library to target '%s'." % (lib, target))
335 if check:
336 for header in self.provides["headers"]:
337 if not conf.CheckCXXHeader(header):
338 return False
339 for lib in self.libs["main"]:
340 if not conf.CheckLib(lib, autoadd=False, language="C++"):
341 return False
342 return True
345class ExternalConfiguration(Configuration):
346 """A Configuration subclass for external (third-party) packages.
348 Aliased as `lsst.sconsUtils.ExternalConfiguration`.
350 ExternalConfiguration doesn't assume the package uses SWIG or Doxygen,
351 and tells SCons not to consider header files this package provides as
352 dependencies (by setting XCPPPATH instead of CPPPATH). This means things
353 SCons won't waste time looking for changes in it every time you build.
354 Header files in external packages are treated as "system headers": that
355 is, most warnings generated when they are being compiled are suppressed.
357 Parameters
358 ----------
359 cfgFile : `str`
360 The name of the calling ``.cfg`` file, usually just passed in with the
361 special variable ``__file__``. This will be parsed to extract the
362 package name and root.
363 headers : `list`, optional
364 A list of headers provided by the package, to be used in
365 autoconf-style tests.
366 libs : `list` or `dict`, optional
367 A list or dictionary of libraries provided by the package. If a
368 dictionary is provided, ``libs["main"]`` should contain a list of
369 regular libraries provided by the library. Other keys are "python"
370 and "test", which refer to libraries that are only linked against
371 compiled Python modules and unit tests, respectively. If a list is
372 provided, the list is used as "main". These are used both for
373 autoconf-style tests and to support env.getLibs(...), which
374 recursively computes the libraries a package must be linked with.
375 eupsProduct : `str`, optional
376 The EUPS product being built.
377 """
378 def __init__(self, cfgFile, headers=(), libs=None, eupsProduct=None):
379 Configuration.__init__(self, cfgFile, headers, libs, eupsProduct=eupsProduct, hasSwigFiles=False,
380 hasDoxygenTag=False, hasDoxygenInclude=False)
381 self.paths["XCPPPATH"] = self.paths["CPPPATH"]
382 del self.paths["CPPPATH"]
385def CustomCFlagCheck(context, flag, append=True):
386 """A configuration test that checks whether a C compiler supports
387 a particular flag.
389 Parameters
390 ----------
391 context :
392 Configuration context.
393 flag : `str`
394 Flag to test, e.g., ``-fvisibility-inlines-hidden``.
395 append : `bool`, optional
396 Automatically append the flag to ``context.env["CCFLAGS"]``
397 if the compiler supports it?
399 Returns
400 -------
401 result : `bool`
402 Did the flag work?
403 """
404 context.Message("Checking if C compiler supports " + flag + " flag ")
405 ccflags = context.env["CCFLAGS"]
406 context.env.Append(CCFLAGS=flag)
407 result = context.TryCompile("int main(int argc, char **argv) { return 0; }", ".c")
408 context.Result(result)
409 if not append or not result:
410 context.env.Replace(CCFLAGS=ccflags)
411 return result
414def CustomCppFlagCheck(context, flag, append=True):
415 """A configuration test that checks whether a C++ compiler supports
416 a particular flag.
418 Parameters
419 ----------
420 context :
421 Configuration context.
422 flag : `str`
423 Flag to test, e.g., ``-fvisibility-inlines-hidden``.
424 append : `bool`, optional
425 Automatically append the flag to ``context.env["CXXFLAGS"]``
426 if the compiler supports it?
428 Returns
429 -------
430 result : `bool`
431 Did the flag work?
432 """
433 context.Message("Checking if C++ compiler supports " + flag + " flag ")
434 cxxflags = context.env["CXXFLAGS"]
435 context.env.Append(CXXFLAGS=flag)
436 result = context.TryCompile("int main(int argc, char **argv) { return 0; }", ".cc")
437 context.Result(result)
438 if not append or not result:
439 context.env.Replace(CXXFLAGS=cxxflags)
440 return result
443def CustomCompileCheck(context, message, source, extension=".cc"):
444 """A configuration test that checks whether the given source code
445 compiles.
447 Parameters
448 ----------
449 context :
450 Configuration context.
451 message : `str`
452 Message displayed on console prior to running the test.
453 source : `str`
454 Source code to compile.
455 extension : `str`, optional
456 Identifies the language of the source code. Use ".c" for C, and ".cc"
457 for C++ (the default).
459 Returns
460 -------
461 result : `bool`
462 Did the code compile?
463 """
464 context.Message(message)
466 env = context.env
467 if (env.GetOption("clean") or env.GetOption("help") or env.GetOption("no_exec")):
468 result = True
469 else:
470 result = context.TryCompile(source, extension)
472 context.Result(result)
474 return result
477def CustomLinkCheck(context, message, source, extension=".cc"):
478 """A configuration test that checks whether the given source code
479 compiles and links.
481 Parameters
482 ----------
483 context :
484 Configuration context.
485 message : `str`
486 Message displayed on console prior to running the test.
487 source : `str`
488 Source code to compile.
489 extension : `str`, optional
490 Identifies the language of the source code. Use ".c" for C, and ".cc"
491 for C++ (the default).
493 Returns
494 -------
495 result : `bool`
496 Did the code compile and link?
497 """
498 context.Message(message)
499 result = context.TryLink(source, extension)
500 context.Result(result)
501 return result
504class PackageTree:
505 """A class for loading and managing the dependency tree of a package,
506 as defined by its configuration module (.cfg) file.
508 This tree isn't actually stored as a tree; it's flattened into an ordered
509 dictionary as it is recursively loaded.
511 The main SCons produced by configure() and available as
512 `lsst.sconsUtils.env` will contain an instance of this class as
513 ``env.dependencies``.
515 Its can be used like a read-only dictionary to check whether an optional
516 package has been configured; a package that was not found will have a
517 value of None, while a configured package's value will be its imported
518 .cfg module.
520 Parameters
521 ----------
522 primaryName : `str`
523 The name of the primary package being built.
524 noCfgFile : `bool`, optional
525 If True, this package has no .cfg file
527 Notes
528 -----
529 After ``__init__``, ``self.primary`` will be set to the configuration
530 module for the primary package, and ``self.packages`` will be an
531 `OrderedDict` of dependencies (excluding ``self.primary``), ordered
532 such that configuration can proceed in iteration order.
533 """
534 def __init__(self, primaryName, noCfgFile=False):
535 self.cfgPath = state.env.cfgPath
536 self.packages = collections.OrderedDict()
537 self.customTests = {
538 "CustomCFlagCheck": CustomCFlagCheck,
539 "CustomCppFlagCheck": CustomCppFlagCheck,
540 "CustomCompileCheck": CustomCompileCheck,
541 "CustomLinkCheck": CustomLinkCheck,
542 }
543 self._current = set([primaryName])
544 if noCfgFile:
545 self.primary = None
546 return
548 self.primary = self._tryImport(primaryName)
549 if self.primary is None:
550 state.log.fail("Failed to load primary package configuration for %s." % primaryName)
552 missingDeps = []
553 for dependency in self.primary.dependencies.get("required", ()):
554 if not self._recurse(dependency):
555 missingDeps.append(dependency)
556 if missingDeps:
557 state.log.fail("Failed to load required dependencies: \"%s\"" % '", "'.join(missingDeps))
559 missingDeps = []
560 for dependency in self.primary.dependencies.get("buildRequired", ()):
561 if not self._recurse(dependency):
562 missingDeps.append(dependency)
563 if missingDeps:
564 state.log.fail("Failed to load required build dependencies: \"%s\"" % '", "'.join(missingDeps))
566 for dependency in self.primary.dependencies.get("optional", ()):
567 self._recurse(dependency)
569 for dependency in self.primary.dependencies.get("buildOptional", ()):
570 self._recurse(dependency)
572 name = property(lambda self: self.primary.config.name) 572 ↛ exitline 572 didn't run the lambda on line 572
574 def configure(self, env, check=False):
575 """Configure the entire dependency tree in order. and return an
576 updated environment."""
577 conf = env.Configure(custom_tests=self.customTests)
578 for name, module in self.packages.items():
579 if module is None:
580 state.log.info("Skipping missing optional package %s." % name)
581 continue
582 if not module.config.configure(conf, packages=self.packages, check=check, build=False):
583 state.log.fail("%s was found but did not pass configuration checks." % name)
584 if self.primary:
585 self.primary.config.configure(conf, packages=self.packages, check=False, build=True)
586 env.AppendUnique(SWIGPATH=env["CPPPATH"])
587 env.AppendUnique(XSWIGPATH=env["XCPPPATH"])
588 # reverse the order of libraries in env.libs, so libraries that
589 # fulfill a dependency of another appear after it. required by the
590 # linker to successfully resolve symbols in static libraries.
591 for target in env.libs:
592 env.libs[target].reverse()
593 env = conf.Finish()
594 return env
596 def __contains__(self, name):
597 return name == self.name or name in self.packages
599 has_key = __contains__
601 def __getitem__(self, name):
602 if name == self.name:
603 return self.primary
604 else:
605 return self.packages[name]
607 def get(self, name, default=None):
608 if name == self.name:
609 return self.primary
610 else:
611 return self.packages.get(name)
613 def keys(self):
614 k = list(self.packages.keys())
615 k.append(self.name)
616 return k
618 def _tryImport(self, name):
619 """Search for and import an individual configuration module from
620 file."""
621 for path in self.cfgPath:
622 filename = os.path.join(path, name + ".cfg")
623 if os.path.exists(filename):
624 try:
625 # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
626 # Need to specify SourceFileLoader since the files do not
627 # have a .py extension.
628 module_name = f"{name}_cfg"
629 loader = importlib.machinery.SourceFileLoader(module_name, filename)
630 spec = importlib.util.spec_from_file_location(module_name, filename,
631 submodule_search_locations=None,
632 loader=loader)
633 module = importlib.util.module_from_spec(spec)
634 spec.loader.exec_module(module)
635 except Exception as e:
636 state.log.warn("Error loading configuration %s (%s)" % (filename, e))
637 continue
638 state.log.info("Using configuration for package '%s' at '%s'." % (name, filename))
639 if not hasattr(module, "dependencies") or not isinstance(module.dependencies, dict):
640 state.log.warn("Configuration module for package '%s' lacks a dependencies dict." % name)
641 return None
642 if not hasattr(module, "config") or not isinstance(module.config, Configuration):
643 state.log.warn("Configuration module for package '%s' lacks a config object." % name)
644 return None
645 else:
646 module.config.addCustomTests(self.customTests)
647 return module
648 state.log.info("Failed to import configuration for optional package '%s'." % name)
650 def _recurse(self, name):
651 """Recursively load a dependency.
653 Parameters
654 ----------
655 name : `str`
656 Name of dependent package.
658 Returns
659 -------
660 loaded : `bool`
661 Was the dependency loaded?
662 """
663 if name in self._current:
664 state.log.fail("Detected recursive dependency involving package '%s'" % name)
665 else:
666 self._current.add(name)
667 if name in self.packages:
668 self._current.remove(name)
669 return self.packages[name] is not None
670 module = self._tryImport(name)
671 if module is None:
672 self.packages[name] = None
673 self._current.remove(name)
674 return False
675 for dependency in module.dependencies.get("required", ()):
676 if not self._recurse(dependency):
677 # We can't configure this package because a required
678 # dependency wasn't found. But this package might itself be
679 # optional, so we don't die yet.
680 self.packages[name] = None
681 self._current.remove(name)
682 state.log.warn("Could not load all dependencies for package '%s' (missing %s)." %
683 (name, dependency))
684 return False
685 for dependency in module.dependencies.get("optional", ()):
686 self._recurse(dependency)
687 # This comes last to ensure the ordering puts all dependencies first.
688 self.packages[name] = module
689 self._current.remove(name)
690 return True
693def getLibs(env, categories="main"):
694 """Get the libraries the package should be linked with.
696 Parameters
697 ----------
698 categories : `str`
699 A string containing whitespace-delimited categories. Standard
700 categories are "main", "python", and "test". Default is "main".
701 A special virtual category "self" can be provided, returning
702 the results of targets="main" with the ``env["packageName"]`` removed.
704 Returns
705 -------
706 libs : `list`
707 Libraries to use.
709 Notes
710 -----
711 Typically, main libraries will be linked with ``LIBS=getLibs("self")``,
712 Python modules will be linked with ``LIBS=getLibs("main python")`` and
713 C++-coded test programs will be linked with ``LIBS=getLibs("main test")``.
714 """
715 libs = []
716 removeSelf = False
717 for category in categories.split():
718 if category == "self":
719 category = "main"
720 removeSelf = True
721 for lib in env.libs[category]:
722 if lib not in libs:
723 libs.append(lib)
724 if removeSelf:
725 try:
726 libs.remove(env["packageName"])
727 except ValueError:
728 pass
729 return libs
732SConsEnvironment.getLibs = getLibs