Coverage for python/lsst/sconsUtils/dependencies.py: 11%

290 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-01 00:45 -0700

1 

2"""Dependency configuration and definition.""" 

3 

4__all__ = ("Configuration", "ExternalConfiguration", "PackageTree", "configure") 

5 

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 

14 

15from . import installation 

16from . import state 

17from .utils import get_conda_prefix, use_conda_compilers 

18 

19 

20def configure(packageName, versionString=None, eupsProduct=None, eupsProductPath=None, noCfgFile=False): 

21 """Recursively configure a package using ups/.cfg files. 

22 

23 Aliased as `lsst.sconsUtils.configure()`. 

24 

25 Usually, LSST packages will call this function through 

26 `~lsst.sconsUtils.scripts.BasicSConstruct`. 

27 

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. 

43 

44 Returns 

45 ------- 

46 env : `SCons.Environment` 

47 An SCons Environment object (which is also available as 

48 `lsst.sconsUtils.env`). 

49 """ 

50 

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'] = [] 

82 

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'] = [] 

95 

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'] = [] 

100 

101 if use_conda_compilers(): 

102 state.env.Append(XCPPPATH=["%s/include" % _conda_prefix]) 

103 

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 " 

109 

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") 

116 

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'] 

122 

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() 

129 

130 

131class Configuration: 

132 """Base class for defining how to configure an LSST sconsUtils package. 

133 

134 Aliased as `lsst.sconsUtils.Configuration`. 

135 

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. 

139 

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(). 

144 

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 """ 

179 

180 @staticmethod 

181 def parseFilename(cfgFile): 

182 """Parse the name of a .cfg file and return package name and root. 

183 

184 Parameters 

185 ---------- 

186 cfgFile : `str` 

187 Name of a ``.cfg`` file. 

188 

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, "..")) 

199 

200 @staticmethod 

201 def getEupsData(eupsProduct): 

202 """Get EUPS version and product directory for named product. 

203 

204 Parameters 

205 ---------- 

206 eupsProduct : `str` 

207 EUPS product name. 

208 

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 

220 

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"] = [] 

265 

266 for pathName, subDirs in [("CPPPATH", includeFileDirs), 

267 ("LIBPATH", libFileDirs)]: 

268 self.paths[pathName] = [] 

269 

270 if state.env.linkFarmDir: 

271 continue 

272 

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) 

277 

278 self.provides = { 

279 "headers": tuple(headers), 

280 "libs": tuple(self.libs["main"]) 

281 } 

282 

283 def addCustomTests(self, tests): 

284 """Add custom SCons configuration tests to the Configure Context 

285 passed to the configure() method. 

286 

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. 

290 

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 

298 

299 def configure(self, conf, packages, check=False, build=True): 

300 """Update an SCons environment to make use of the package. 

301 

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 

343 

344 

345class ExternalConfiguration(Configuration): 

346 """A Configuration subclass for external (third-party) packages. 

347 

348 Aliased as `lsst.sconsUtils.ExternalConfiguration`. 

349 

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. 

356 

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"] 

383 

384 

385def CustomCFlagCheck(context, flag, append=True): 

386 """A configuration test that checks whether a C compiler supports 

387 a particular flag. 

388 

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? 

398 

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 

412 

413 

414def CustomCppFlagCheck(context, flag, append=True): 

415 """A configuration test that checks whether a C++ compiler supports 

416 a particular flag. 

417 

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? 

427 

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 

441 

442 

443def CustomCompileCheck(context, message, source, extension=".cc"): 

444 """A configuration test that checks whether the given source code 

445 compiles. 

446 

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). 

458 

459 Returns 

460 ------- 

461 result : `bool` 

462 Did the code compile? 

463 """ 

464 context.Message(message) 

465 

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) 

471 

472 context.Result(result) 

473 

474 return result 

475 

476 

477def CustomLinkCheck(context, message, source, extension=".cc"): 

478 """A configuration test that checks whether the given source code 

479 compiles and links. 

480 

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). 

492 

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 

502 

503 

504class PackageTree: 

505 """A class for loading and managing the dependency tree of a package, 

506 as defined by its configuration module (.cfg) file. 

507 

508 This tree isn't actually stored as a tree; it's flattened into an ordered 

509 dictionary as it is recursively loaded. 

510 

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``. 

514 

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. 

519 

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 

526 

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 

547 

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) 

551 

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)) 

558 

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)) 

565 

566 for dependency in self.primary.dependencies.get("optional", ()): 

567 self._recurse(dependency) 

568 

569 for dependency in self.primary.dependencies.get("buildOptional", ()): 

570 self._recurse(dependency) 

571 

572 name = property(lambda self: self.primary.config.name) 572 ↛ exitline 572 didn't run the lambda on line 572

573 

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 

595 

596 def __contains__(self, name): 

597 return name == self.name or name in self.packages 

598 

599 has_key = __contains__ 

600 

601 def __getitem__(self, name): 

602 if name == self.name: 

603 return self.primary 

604 else: 

605 return self.packages[name] 

606 

607 def get(self, name, default=None): 

608 if name == self.name: 

609 return self.primary 

610 else: 

611 return self.packages.get(name) 

612 

613 def keys(self): 

614 k = list(self.packages.keys()) 

615 k.append(self.name) 

616 return k 

617 

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) 

649 

650 def _recurse(self, name): 

651 """Recursively load a dependency. 

652 

653 Parameters 

654 ---------- 

655 name : `str` 

656 Name of dependent package. 

657 

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 

691 

692 

693def getLibs(env, categories="main"): 

694 """Get the libraries the package should be linked with. 

695 

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. 

703 

704 Returns 

705 ------- 

706 libs : `list` 

707 Libraries to use. 

708 

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 

730 

731 

732SConsEnvironment.getLibs = getLibs