Coverage for python/lsst/sconsUtils/scripts.py: 7%

278 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-22 11:01 +0000

1"""Convenience functions to do the work of standard LSST SConstruct/SConscript 

2files. 

3""" 

4 

5import os.path 

6import warnings 

7import re 

8import pipes 

9from stat import ST_MODE 

10from SCons.Script import SConscript, File, Dir, Glob, BUILD_TARGETS 

11from distutils.spawn import find_executable 

12 

13from . import dependencies 

14from . import state 

15from . import tests 

16from . import utils 

17 

18DEFAULT_TARGETS = ("lib", "python", "shebang", "tests", "examples", "doc") 

19 

20 

21def _getFileBase(node): 

22 name, ext = os.path.splitext(os.path.basename(str(node))) 

23 return name 

24 

25 

26class BasicSConstruct: 

27 """A scope-only class for SConstruct-replacement convenience functions. 

28 

29 The boilerplate for a standard LSST SConstruct file is replaced by two 

30 static methods: ``initialize()`` and ``finish()``. The former configures 

31 dependencies, sets up package-dependent environment variables, and calls 

32 any SConscript files found in subdirectories, while the latter sets up 

33 installation paths, default targets, and explicit dependencies. 

34 

35 Calling ``BasicSConstruct`` as a function invokes its ``__new__`` method, 

36 which calls both `~lsst.sconsUtils.BasicSConstruct.initialize` and 

37 `~lsst.sconsUtils.BasicSConstruct.finish`, and should be used when the 

38 SConstruct file doesn't need to do anything other than what they provide. 

39 When called this way it returns the `~lsst.sconsUtils.env` Environment 

40 object rather than a `~lsst.sconsUtils.scripts.BasicSConstruct` instance 

41 (which would be useless). 

42 """ 

43 

44 _initializing = False 

45 

46 def __new__(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None, cleanExt=None, 

47 defaultTargets=DEFAULT_TARGETS, 

48 subDirList=None, ignoreRegex=None, 

49 versionModuleName="python/lsst/%s/version.py", noCfgFile=False, 

50 sconscriptOrder=None, disableCc=False): 

51 cls.initialize(packageName, versionString, eupsProduct, eupsProductPath, cleanExt, 

52 versionModuleName, noCfgFile=noCfgFile, sconscriptOrder=sconscriptOrder, 

53 disableCc=disableCc) 

54 cls.finish(defaultTargets, subDirList, ignoreRegex) 

55 return state.env 

56 

57 @classmethod 

58 def initialize(cls, packageName, versionString=None, eupsProduct=None, eupsProductPath=None, 

59 cleanExt=None, versionModuleName="python/lsst/%s/version.py", noCfgFile=False, 

60 sconscriptOrder=None, disableCc=False): 

61 """Convenience function to replace standard SConstruct boilerplate 

62 (step 1). 

63 

64 This function: 

65 

66 - Calls all SConscript files found in subdirectories. 

67 - Configures dependencies. 

68 - Sets how the ``--clean`` option works. 

69 

70 Parameters 

71 ---------- 

72 packageName : `str` 

73 Name of the package being built; must correspond to a .cfg 

74 file in ``ups/``. 

75 versionString : `str`, optional 

76 Version-control system string to be parsed for version information 

77 (``$HeadURL$`` for SVN). Defaults to "git" if not set or `None`. 

78 eupsProduct : `str`, optional 

79 Name of the EUPS product being built. Defaults to and is almost 

80 always the name of the package. 

81 eupsProductPath : `str`, optional 

82 An alternate directory where the package should be installed. 

83 cleanExt : `list`, optional 

84 Whitespace delimited sequence of globs for files to remove with 

85 ``--clean``. 

86 versionModuleName : `str`, optional 

87 If non-None, builds a ``version.py`` module as this file; ``'%s'`` 

88 is replaced with the name of the package. 

89 noCfgFile : `bool`, optional 

90 If True, this package has no .cfg file 

91 sconscriptOrder : `list`, optional 

92 A sequence of directory names that set the order for processing 

93 SConscript files discovered in nested directories. Full 

94 directories need not be specified, but paths must begin at the 

95 root. For example, ``["lib", "python"]`` will ensure that 

96 ``lib/SConscript`` is run before both ``python/foo/SConscript`` 

97 and ``python/bar/SConscript``. The default order should work for 

98 most LSST SCons builds, as it provides the correct ordering for 

99 the ``lib``, ``python``, ``tests``, ``examples``, and ``doc`` 

100 targets. If this argument is provided, it must include the subset 

101 of that list that is valid for the package, in that order. 

102 disableCC : `bool`, optional 

103 Should the C++ compiler check be disabled? Disabling this checks 

104 allows a faster startup and permits building on systems that don't 

105 meet the requirements for the C++ compilter (e.g., for 

106 pure-python packages). 

107 

108 Returns 

109 ------- 

110 env : `lsst.sconsUtils.env` 

111 A SCons Environment object. 

112 """ 

113 if not disableCc: 

114 state._configureCommon() 

115 state._saveState() 

116 if cls._initializing: 

117 state.log.fail("Recursion detected; an SConscript file should not call BasicSConstruct.") 

118 cls._initializing = True 

119 dependencies.configure(packageName, versionString, eupsProduct, eupsProductPath, noCfgFile) 

120 state.env.BuildETags() 

121 if cleanExt is None: 

122 cleanExt = r"*~ core core.[1-9]* *.so *.os *.o *.pyc *.pkgc" 

123 state.env.CleanTree(cleanExt, "__pycache__ .pytest_cache") 

124 if versionModuleName is not None: 

125 try: 

126 versionModuleName = versionModuleName % "/".join(packageName.split("_")) 

127 except TypeError: 

128 pass 

129 state.targets["version"] = state.env.VersionModule(versionModuleName) 

130 scripts = [] 

131 for root, dirs, files in os.walk("."): 

132 if "SConstruct" in files and root != ".": 

133 dirs[:] = [] 

134 continue 

135 dirs[:] = [d for d in dirs if not d.startswith('.')] 

136 dirs.sort() # os.walk order is not specified, but we want builds to be deterministic 

137 if "SConscript" in files: 

138 scripts.append(os.path.join(root, "SConscript")) 

139 if sconscriptOrder is None: 

140 sconscriptOrder = DEFAULT_TARGETS 

141 

142 # directory for shebang target is bin.src 

143 sconscriptOrder = [t if t != "shebang" else "bin.src" for t in sconscriptOrder] 

144 

145 def key(path): 

146 for i, item in enumerate(sconscriptOrder): 

147 if path.lstrip("./").startswith(item): 

148 return i 

149 return len(sconscriptOrder) 

150 scripts.sort(key=key) 

151 for script in scripts: 

152 state.log.info("Using SConscript at %s" % script) 

153 SConscript(script) 

154 cls._initializing = False 

155 return state.env 

156 

157 @staticmethod 

158 def finish(defaultTargets=DEFAULT_TARGETS, 

159 subDirList=None, ignoreRegex=None): 

160 """Convenience function to replace standard SConstruct boilerplate 

161 (step 2). 

162 

163 This function: 

164 

165 - Sets up installation paths. 

166 - Tells SCons to only do MD5 checks when timestamps have changed. 

167 - Sets the "include", "lib", "python", and "tests" targets as the 

168 defaults to be built when scons is run with no target arguments. 

169 

170 Parameters 

171 ---------- 

172 subDirList : `list` 

173 An explicit list of subdirectories that should be installed. 

174 By default, all non-hidden subdirectories will be installed. 

175 defaultTargets : `list` 

176 A sequence of targets (see `lsst.sconsUtils.state.targets`) 

177 that should be built when scons is run with no arguments. 

178 ignoreRegex : `str` 

179 Regular expression that matches files that should not be installed. 

180 

181 Returns 

182 ------- 

183 env : `lsst.sconsUtils.env` 

184 A SCons Environment. 

185 """ 

186 if ignoreRegex is None: 

187 ignoreRegex = r"(~$|\.pyc$|^\.svn$|\.o|\.os$)" 

188 if subDirList is None: 

189 subDirList = [] 

190 for path in os.listdir("."): 

191 if os.path.isdir(path) and not path.startswith("."): 

192 subDirList.append(path) 

193 if "bin.src" in subDirList and "shebang" in state.targets and state.targets["shebang"]: 

194 # shebang makes a directory that should be installed 

195 subDirList += ["bin"] 

196 install = state.env.InstallLSST(state.env["prefix"], 

197 [subDir for subDir in subDirList], 

198 ignoreRegex=ignoreRegex) 

199 for name, target in state.targets.items(): 

200 state.env.Requires(install, target) 

201 state.env.Alias(name, target) 

202 state.env.Requires(state.targets["python"], state.targets["version"]) 

203 declarer = state.env.Declare() 

204 state.env.Requires(declarer, install) # Ensure declaration fires after installation available 

205 

206 # shebang should be in the list if bin.src exists but the location 

207 # matters so we can not append it afterwards. 

208 state.env.Default([t for t in defaultTargets 

209 if os.path.exists(t) or (t == "shebang" and os.path.exists("bin.src"))]) 

210 if "version" in state.targets: 

211 state.env.Default(state.targets["version"]) 

212 state.env.Requires(state.targets["tests"], state.targets["version"]) 

213 state.env.Decider("MD5-timestamp") # if timestamps haven't changed, don't do MD5 checks 

214 # 

215 # Check if any of the tests failed by looking for *.failed files. 

216 # Perform this test just before scons exits 

217 # 

218 # N.b. the test is written in sh not python as then we can use @ to 

219 # suppress output 

220 # 

221 if "tests" in [str(t) for t in BUILD_TARGETS]: 

222 testsDir = pipes.quote(os.path.join(os.getcwd(), "tests", ".tests")) 

223 checkTestStatus_command = state.env.Command('checkTestStatus', [], """ 

224 @ if [ -d {0} ]; then \ 

225 nfail=`find {0} -name "*.failed" | wc -l | sed -e 's/ //g'`; \ 

226 if [ $$nfail -gt 0 ]; then \ 

227 echo "Failed test output:" >&2; \ 

228 for f in `find {0} -name "*.failed"`; do \ 

229 case "$$f" in \ 

230 *.xml.failed) \ 

231 echo "Global pytest output is in $$f" >&2; \ 

232 ;; \ 

233 *.failed) \ 

234 cat $$f >&2; \ 

235 ;; \ 

236 esac; \ 

237 done; \ 

238 echo "The following tests failed:" >&2;\ 

239 find {0} -name "*.failed" >&2; \ 

240 echo "$$nfail tests failed" >&2; exit 1; \ 

241 fi; \ 

242 fi; \ 

243 """.format(testsDir)) 

244 

245 state.env.Depends(checkTestStatus_command, BUILD_TARGETS) # this is why the check runs last 

246 BUILD_TARGETS.extend(checkTestStatus_command) 

247 state.env.AlwaysBuild(checkTestStatus_command) 

248 

249 

250class BasicSConscript: 

251 """A scope-only class for SConscript-replacement convenience functions. 

252 

253 All methods of `~lsst.sconsUtils.scripts.BasicSConscript` are static. All 

254 of these functions update the `~lsst.sconsUtils.state.targets` dictionary 

255 of targets used to set default targets and fix build dependencies; if you 

256 build anything without using BasicSConscript methods, be sure to manually 

257 add it to the `~lsst.sconsUtils.state.state.targets` `dict`. 

258 """ 

259 

260 @staticmethod 

261 def lib(libName=None, src=None, libs="self", noBuildList=None): 

262 """Convenience function to replace standard lib/SConscript boilerplate. 

263 

264 With no arguments, this will build a shared library with the same name 

265 as the package. This uses 

266 `lsst.sconsUtils.env.SourcesForSharedLibrary` to support the 

267 ``optFiles``/``noOptFiles`` command-line variables. 

268 

269 Parameters 

270 ---------- 

271 libName : `str` 

272 Name of the shared libray to be built (defaults to 

273 ``env["packageName"]``). 

274 src : `str` or `Glob` 

275 Source to compile into the library. Defaults to a 4-directory 

276 deep glob of all ``*.cc`` files in ``#src``. 

277 libs : `str` or `list` 

278 Libraries to link against, either as a string argument to be 

279 passed to `lsst.sconsUtils.env.getLibs` or a sequence of actual 

280 libraries to pass in. 

281 noBuildList : `list` 

282 List of source files to exclude from building. 

283 

284 Returns 

285 ------- 

286 result : ??? 

287 ??? 

288 """ 

289 if libName is None: 

290 libName = state.env["packageName"] 

291 if src is None: 

292 src = Glob("#src/*.cc") + Glob("#src/*/*.cc") + Glob("#src/*/*/*.cc") + Glob("#src/*/*/*/*.cc") 

293 if noBuildList is not None: 

294 src = [node for node in src if os.path.basename(str(node)) not in noBuildList] 

295 src = state.env.SourcesForSharedLibrary(src) 

296 if isinstance(libs, str): 

297 libs = state.env.getLibs(libs) 

298 elif libs is None: 

299 libs = [] 

300 result = state.env.SharedLibrary(libName, src, LIBS=libs) 

301 state.targets["lib"].extend(result) 

302 return result 

303 

304 @staticmethod 

305 def shebang(src=None): 

306 """Handles shebang rewriting. 

307 

308 With no arguments looks in ``bin.src/`` and copies to ``bin/`` 

309 If `~lsst.sconsUtils.utils.needShebangRewrite()` is `False` the 

310 shebang will not be modified. 

311 

312 Only Python files requiring a shebang rewrite should be placed 

313 in ``bin.src/``. Do not place executable binaries in this directory. 

314 

315 Parameters 

316 ---------- 

317 src : `str` or `Glob`, optional 

318 Glob to use to search for files. 

319 """ 

320 # check if Python is called on the first line with this expression 

321 # This comes from distutils copy_scripts 

322 FIRST_LINE_RE = re.compile(r'^#!.*python[0-9.]*([ \t].*)?$') 

323 doRewrite = utils.needShebangRewrite() 

324 

325 def rewrite_shebang(target, source, env): 

326 """Copy source to target, rewriting the shebang""" 

327 # Currently just use this python 

328 usepython = utils.whichPython() 

329 for targ, src in zip(target, source): 

330 with open(str(src), "r") as srcfd: 

331 with open(str(targ), "w") as outfd: 

332 first_line = srcfd.readline() 

333 # Always match the first line so we can warn people 

334 # if an attempt is being made to rewrite a file that 

335 # should not be rewritten 

336 match = FIRST_LINE_RE.match(first_line) 

337 if match and doRewrite: 

338 post_interp = match.group(1) or '' 

339 # Paths can be long so ensure that flake8 won't 

340 # complain 

341 outfd.write("#!{}{} # noqa\n".format(usepython, post_interp)) 

342 else: 

343 if not match: 

344 state.log.warn("Could not rewrite shebang of {}. Please check" 

345 " file or move it to bin directory.".format(str(src))) 

346 outfd.write(first_line) 

347 for line in srcfd.readlines(): 

348 outfd.write(line) 

349 # Ensure the bin/ file is executable 

350 oldmode = os.stat(str(targ))[ST_MODE] & 0o7777 

351 newmode = (oldmode | 0o555) & 0o7777 

352 if newmode != oldmode: 

353 state.log.info("changing mode of {} from {} to {}".format( 

354 str(targ), oldmode, newmode)) 

355 os.chmod(str(targ), newmode) 

356 

357 if src is None: 

358 src = Glob("#bin.src/*") 

359 for s in src: 

360 filename = str(s) 

361 # Do not try to rewrite files starting with non-letters 

362 if filename != "SConscript" and re.match("[A-Za-z]", filename): 

363 result = state.env.Command(target=os.path.join(Dir("#bin").abspath, filename), 

364 source=s, action=rewrite_shebang) 

365 state.targets["shebang"].extend(result) 

366 

367 @staticmethod 

368 def python(module=None, src=None, extra=(), libs="main python"): 

369 """Convenience function to replace standard ``python/*/SConscript`` 

370 boilerplate. 

371 

372 With no arguments, this will build a pybind11 module with the name 

373 determined according to our current pseudo-convention: last part of 

374 ``env["packageName"]`` (split by underscores), with an underscore 

375 prepended. All .cc files in the same directory will be linked into a 

376 single module. 

377 

378 Parameters 

379 ---------- 

380 module : `str`, optional 

381 Name of the module to build (does not include the file extension). 

382 Defaults to ``"_" + env["packageName"].split("_")[-1]``. 

383 src : `list`, optional 

384 A list of source files to build the module from; defaults to all 

385 .cc files in the directory. 

386 extra : `list`, optional 

387 A list of additional source files to add to ``src``; should only 

388 be used if ``src`` is defaulted. 

389 libs : `str` or `list`, optional 

390 Libraries to link against, either as a string argument to be 

391 passed to `lsst.sconsUtils.env.getLibs` or a sequence of actual 

392 libraries to pass in. 

393 

394 Returns 

395 ------- 

396 result : `lsst.sconsUtils.env.Pybin11LoadbleModule` 

397 A Pybind11LoadableModule instance. 

398 """ 

399 if module is None: 

400 module = "_" + state.env["packageName"].split("_")[-1] 

401 if src is None: 

402 src = state.env.Glob("*.cc") 

403 src.extend(extra) 

404 if isinstance(libs, str): 

405 libs = state.env.getLibs(libs) 

406 elif libs is None: 

407 libs = [] 

408 result = state.env.Pybind11LoadableModule(module, src, LIBS=libs) 

409 state.targets["python"].append(result) 

410 return result 

411 

412 @staticmethod 

413 def pybind11(nameList=[], libs="main python", extraSrc=None, addUnderscore=True): 

414 """Convenience function to replace standard ``python/*/SConscript`` 

415 boilerplate. 

416 

417 .. note:: Deprecated 

418 

419 Use `lsst.sconsUtils.scripts.BasicSConscript.python` for new code 

420 with all source compiled into a single module. Will be removed 

421 once all LSST code has been converted to use the new approach. 

422 

423 Parameters 

424 ---------- 

425 nameList : `list`, optional 

426 Sequence of pybind11 modules to be built (does not include the 

427 file extensions). 

428 libs : `str` or `list`, optional 

429 Libraries to link against, either as a string argument to be 

430 passed to `lsst.sconsUtils.env.getLibs` or a sequence of actual 

431 libraries to pass in. 

432 extraSrc : `dict`, optional 

433 A dictionary of additional source files that go into the modules. 

434 Each key should be an entry in nameList, and each value should be 

435 a list of additional C++ source files. 

436 addUnderscore : `bool`, optional 

437 Add an underscore to each library name (if the source file name 

438 does not already start with underscore)? If False the library name 

439 is always the same as the source file name 

440 **DEPRECATED**: always use `False` for new code. 

441 

442 Returns 

443 ------- 

444 result : `lsst.sconsUtils.env.Pybin11LoadbleModule` 

445 A Pybind11LoadableModule instance. 

446 """ 

447 warnings.warn("sconsUtils.scripts.pybind11() is deprecated; please use scripts.python() instead", 

448 DeprecationWarning, stacklevel=2) 

449 srcList = extraSrc 

450 if srcList is None: 

451 srcList = dict([(name, []) for name in nameList]) 

452 for name in nameList: 

453 srcList[name].append(name + ".cc") 

454 if isinstance(libs, str): 

455 libs = state.env.getLibs(libs) 

456 elif libs is None: 

457 libs = [] 

458 result = [] 

459 for name in nameList: 

460 # TODO remove this block and the `addUnderscore` argument and 

461 # always use pyLibName = name; but we can't do that until all our 

462 # pybind11 SConscript files have been converted 

463 if addUnderscore: 

464 if name.startswith("_"): 

465 pyLibName = name 

466 else: 

467 pyLibName = "_" + name 

468 else: 

469 pyLibName = name 

470 result.extend(state.env.Pybind11LoadableModule(pyLibName, srcList[name], LIBS=libs)) 

471 state.targets["python"].extend(result) 

472 return result 

473 

474 @staticmethod 

475 def doc(config="doxygen.conf.in", projectName=None, projectNumber=None, **kwargs): 

476 """Convenience function to replace standard doc/SConscript 

477 boilerplate. 

478 

479 With no arguments, this will generate a Doxygen config file and run 

480 Doxygen with `lsst.sconsUtils.env.Doxygen`, using the projectName 

481 and projectNumber from ``env["packageName"]`` and ``env["version"]``, 

482 respectively. 

483 

484 This essentially just forwards all arguments (which should be passed as 

485 keyword arguments) to `lsst.sconsUtils.env.Doxygen()``. 

486 

487 Parameters 

488 ---------- 

489 config : `str`, optional 

490 Configuration .in file to use. 

491 projectName : `str`, optional 

492 Project name. 

493 projectNumber : `str`, optional 

494 Project version number. 

495 kwargs 

496 

497 Returns 

498 ------- 

499 result : ??? 

500 ??? 

501 """ 

502 if not find_executable("doxygen"): 

503 state.log.warn("doxygen executable not found; skipping documentation build.") 

504 return [] 

505 if projectName is None: 

506 projectName = ".".join(["lsst"] + state.env["packageName"].split("_")) 

507 if projectNumber is None: 

508 projectNumber = state.env["version"] 

509 result = state.env.Doxygen( 

510 config, projectName=projectName, projectNumber=projectNumber, 

511 includes=state.env.doxygen["includes"], 

512 useTags=state.env.doxygen["tags"], 

513 makeTag=(state.env["packageName"] + ".tag"), 

514 **kwargs 

515 ) 

516 state.targets["doc"].extend(result) 

517 return result 

518 

519 @staticmethod 

520 def tests(pyList=None, ccList=None, swigNameList=None, swigSrc=None, 

521 ignoreList=None, noBuildList=None, pySingles=None, 

522 args=None): 

523 """Convenience function to replace standard tests/SConscript 

524 boilerplate. 

525 

526 With no arguments, will attempt to figure out which files should be 

527 run as tests and which are support code (like SWIG modules). 

528 

529 Python tests will be marked as dependent on the entire ``#python`` 

530 directory and any SWIG modules built in the tests directory. This 

531 should ensure tests are always run when their results might have 

532 changed, but may result in them being re-run more often 

533 than necessary. 

534 

535 Parameters 

536 ---------- 

537 pyList : `list`, optional 

538 A sequence of Python tests to run (including .py extensions). 

539 Defaults to a ``*.py`` glob of the tests directory, minus any 

540 files corresponding to the SWIG modules in swigFileList. 

541 An empty list will enable automated test discovery. 

542 ccList : `list`, optional 

543 A sequence of C++ unit tests to run (including .cc extensions). 

544 Defaults to a ``*.cc`` glob of the tests directory, minus any 

545 files that end with ``*_wrap.cc`` and files present in swigSrc. 

546 swigNameList : `list`, optional 

547 A sequence of SWIG modules to build (NOT including .i extensions). 

548 swigSrc : `dict`, optional 

549 Additional source files to be compiled into SWIG modules, as a 

550 dictionary; each key must be an entry in swigNameList, and each 

551 value a list of additional source files. 

552 ignoreList : `list`, optional 

553 List of ignored tests to be passed to 

554 `lsst.sconsUtils.tests.Control` (note that ignored tests will be 

555 built, but not run). 

556 noBuildList : `list`, optional 

557 List of tests that should not even be built. 

558 pySingles : `list`, optional 

559 A sequence of Python tests to run (including .py extensions) 

560 as independent single tests. By default this list is empty 

561 and all tests are run in a single pytest call. 

562 Items specified here will not appear in the default pyList 

563 and should not start with ``test_`` (such that they will not 

564 be auto-discoverable by pytest). 

565 args : `dict`, optional 

566 A dictionary of program arguments for tests, passed directly 

567 to `lsst.sconsUtils.tests.Control`. 

568 

569 Returns 

570 ------- 

571 result : ??? 

572 ??? 

573 """ 

574 if noBuildList is None: 

575 noBuildList = [] 

576 if pySingles is None: 

577 pySingles = [] 

578 if swigNameList is None: 

579 swigFileList = Glob("*.i") 

580 swigNameList = [_getFileBase(node) for node in swigFileList] 

581 else: 

582 swigFileList = [File(name + ".i") for name in swigNameList] 

583 if swigSrc is None: 

584 swigSrc = {} 

585 allSwigSrc = set() 

586 for name, node in zip(swigNameList, swigFileList): 

587 src = swigSrc.setdefault(name, []) 

588 allSwigSrc.update(str(element) for element in src) 

589 src.append(node) 

590 if pyList is None: 

591 pyList = [node for node in Glob("*.py") 

592 if _getFileBase(node) not in swigNameList 

593 and os.path.basename(str(node)) not in noBuildList] 

594 # if we got no matches, reset to None so we do not enabled 

595 # auto test detection in pytest 

596 if not pyList: 

597 pyList = None 

598 if ccList is None: 

599 ccList = [node for node in Glob("*.cc") 

600 if (not str(node).endswith("_wrap.cc")) and str(node) not in allSwigSrc 

601 and os.path.basename(str(node)) not in noBuildList] 

602 if ignoreList is None: 

603 ignoreList = [] 

604 

605 def s(ll): 

606 if ll is None: 

607 return ['None'] 

608 return [str(i) for i in ll] 

609 

610 state.log.info("SWIG modules for tests: %s" % s(swigFileList)) 

611 state.log.info("Python tests: %s" % s(pyList)) 

612 state.log.info("C++ tests: %s" % s(ccList)) 

613 state.log.info("Files that will not be built: %s" % noBuildList) 

614 state.log.info("Ignored tests: %s" % ignoreList) 

615 control = tests.Control(state.env, ignoreList=ignoreList, args=args, verbose=True) 

616 for ccTest in ccList: 

617 state.env.Program(ccTest, LIBS=state.env.getLibs("main test")) 

618 swigMods = [] 

619 for name, src in swigSrc.items(): 

620 swigMods.extend( 

621 state.env.SwigLoadableModule("_" + name, src, LIBS=state.env.getLibs("main python")) 

622 ) 

623 

624 # Warn about insisting that a test in pySingles starts with test_ and 

625 # therefore might be automatically discovered by pytest. These files 

626 # should not be discovered automatically. 

627 for node in pySingles: 

628 if str(node).startswith("test_"): 

629 state.log.warn("Warning: {} should be run independently but" 

630 " can be automatically discovered".format(node)) 

631 

632 # Ensure that python tests listed in pySingles are not included in 

633 # pyList. 

634 if pyList is not None: 

635 pyList = [str(node) for node in pyList if str(node) not in pySingles] 

636 

637 ccList = [control.run(str(node)) for node in ccList] 

638 pySingles = [control.run(str(node)) for node in pySingles] 

639 

640 # If we tried to discover .py files and found none, do not then 

641 # try to use auto test discovery. 

642 if pyList is not None: 

643 pyList = [control.runPythonTests(pyList)] 

644 else: 

645 pyList = [] 

646 

647 # Run all pySingles sequentially before other tests. This ensures 

648 # that there are no race conditions collecting coverage databases. 

649 if pySingles: 

650 for i in range(len(pySingles) - 1): 

651 state.env.Depends(pySingles[i + 1], pySingles[i]) 

652 for pyTest in pyList: 

653 state.env.Depends(pyTest, pySingles[-1]) 

654 

655 pyList.extend(pySingles) 

656 for pyTest in pyList: 

657 state.env.Depends(pyTest, ccList) 

658 state.env.Depends(pyTest, swigMods) 

659 state.env.Depends(pyTest, state.targets["python"]) 

660 state.env.Depends(pyTest, state.targets["shebang"]) 

661 result = ccList + pyList 

662 state.targets["tests"].extend(result) 

663 return result 

664 

665 @staticmethod 

666 def examples(ccList=None, swigNameList=None, swigSrc=None): 

667 """Convenience function to replace standard examples/SConscript 

668 boilerplate. 

669 

670 Parameters 

671 ---------- 

672 ccList : `list`, optional 

673 A sequence of C++ examples to build (including .cc extensions). 

674 Defaults to a ``*.cc`` glob of the examples directory, minus any 

675 files that end with ``*_wrap.cc`` and files present in swigSrc. 

676 swigNameList : `list`, optional 

677 A sequence of SWIG modules to build (NOT including .i extensions). 

678 swigSrc : `list`, optional 

679 Additional source files to be compiled into SWIG modules, as a 

680 dictionary; each key must be an entry in swigNameList, and each 

681 value a list of additional source files. 

682 

683 Returns 

684 ------- 

685 result : ??? 

686 ??? 

687 """ 

688 if swigNameList is None: 

689 swigFileList = Glob("*.i") 

690 swigNameList = [_getFileBase(node) for node in swigFileList] 

691 else: 

692 swigFileList = [File(name) for name in swigNameList] 

693 if swigSrc is None: 

694 swigSrc = {} 

695 allSwigSrc = set() 

696 for name, node in zip(swigNameList, swigFileList): 

697 src = swigSrc.setdefault(name, []) 

698 allSwigSrc.update(str(element) for element in src) 

699 src.append(node) 

700 if ccList is None: 

701 ccList = [node for node in Glob("*.cc") 

702 if (not str(node).endswith("_wrap.cc")) and str(node) not in allSwigSrc] 

703 state.log.info("SWIG modules for examples: %s" % swigFileList) 

704 state.log.info("C++ examples: %s" % ccList) 

705 results = [] 

706 for src in ccList: 

707 results.extend(state.env.Program(src, LIBS=state.env.getLibs("main"))) 

708 for name, src in swigSrc.items(): 

709 results.extend( 

710 state.env.SwigLoadableModule("_" + name, src, LIBS=state.env.getLibs("main python")) 

711 ) 

712 for result in results: 

713 state.env.Depends(result, state.targets["lib"]) 

714 state.targets["examples"].extend(results) 

715 return results