Coverage for python/lsst/sconsUtils/utils.py: 33%

103 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-09 02:55 -0800

1"""Internal utilities for sconsUtils.""" 

2 

3__all__ = ("Log", "_has_OSX_SIP", "libraryPathPassThrough", "whichPython", 

4 "needShebangRewrite", "libraryLoaderEnvironment", "runExternal", 

5 "memberOf", "get_conda_prefix") 

6 

7import os 

8import sys 

9import warnings 

10import subprocess 

11import platform 

12from typing import Optional 

13import SCons.Script 

14 

15 

16class Log: 

17 """A dead-simple logger for all messages. 

18 

19 Centralizes decisions about whether to throw exceptions or print 

20 user-friendly messages (the traceback variable) and whether to print 

21 extra debug info (the verbose variable). These are set from command-line 

22 options in `lsst.sconsUtils.state`. 

23 """ 

24 

25 def __init__(self): 

26 self.traceback = False 

27 self.verbose = True 

28 

29 def info(self, message): 

30 if self.verbose: 30 ↛ exitline 30 didn't return from function 'info', because the condition on line 30 was never false

31 print(message) 

32 

33 def warn(self, message): 

34 if self.traceback: 

35 warnings.warn(message, stacklevel=2) 

36 else: 

37 print(message, file=sys.stderr) 

38 

39 def fail(self, message): 

40 if self.traceback: 

41 raise RuntimeError(message) 

42 else: 

43 if message: 

44 print(message, file=sys.stderr) 

45 SCons.Script.Exit(1) 

46 

47 def flush(self): 

48 sys.stderr.flush() 

49 

50 

51def _has_OSX_SIP(): 

52 """Internal function indicating that the OS has System 

53 Integrity Protection. 

54 

55 Returns 

56 ------- 

57 hasSIP : `bool` 

58 `True` if SIP is present in this operating system version. 

59 """ 

60 hasSIP = False 

61 os_platform = SCons.Platform.platform_default() 

62 # SIP is enabled on OS X >=10.11 equivalent to darwin >= 15 

63 if os_platform == 'darwin': 

64 release_str = platform.release() 

65 release_major = int(release_str.split('.')[0]) 

66 if release_major >= 15: 

67 hasSIP = True 

68 return hasSIP 

69 

70 

71def libraryPathPassThrough(): 

72 """Name of library path environment variable to be passed throughself. 

73 

74 Returns 

75 ------- 

76 library : `str` 

77 Name of library path environment variable. `None` if no pass through 

78 is required. 

79 """ 

80 if _has_OSX_SIP(): 

81 return "DYLD_LIBRARY_PATH" 

82 return None 

83 

84 

85# Cache variable for whichPython() function 

86_pythonPath = None 

87 

88 

89def whichPython(): 

90 """Path of Python executable to use. 

91 

92 Returns 

93 ------- 

94 pythonPath : `str` 

95 Full path to the Python executable as determined 

96 from the PATH. Does not return the full path of the Python running 

97 SCons. Caches result and assumes the PATH does not change between 

98 calls. Runs the "python" command and asks where it is rather than 

99 scanning the PATH. 

100 """ 

101 global _pythonPath 

102 if _pythonPath is None: 

103 _pythonPath = runExternal(["python", "-c", "import sys; print(sys.executable)"], 

104 fatal=True, msg="Error getting python path") 

105 return _pythonPath 

106 

107 

108def needShebangRewrite(): 

109 """Is shebang rewriting required? 

110 

111 Returns 

112 ------- 

113 rewrite : `bool` 

114 Returns True if the shebang lines of executables should be rewritten. 

115 """ 

116 return _has_OSX_SIP() 

117 

118 

119def libraryLoaderEnvironment(): 

120 """Calculate library loader path environment string to be prepended to 

121 external commands. 

122 

123 Returns 

124 ------- 

125 loader : `str` 

126 If we have an macOS with System Integrity Protection enabled or similar 

127 we need to pass through both DYLD_LIBRARY_PATH and LSST_LIBRARY_PATH 

128 to the external command: DYLD_LIBRARY_PATH for Python code 

129 LSST_LIBRARY_PATH for shell scripts 

130 

131 If both are already defined then pass them each through. 

132 If only one is defined, then set both to the defined env variable 

133 If neither is defined then pass through nothing. 

134 """ 

135 libpathstr = "" 

136 lib_pass_through_var = libraryPathPassThrough() 

137 aux_pass_through_var = "LSST_LIBRARY_PATH" 

138 if lib_pass_through_var is not None: 

139 for varname in (lib_pass_through_var, aux_pass_through_var): 

140 if varname in os.environ: 

141 libpathstr += '{}="{}" '.format(varname, os.environ[varname]) 

142 

143 if aux_pass_through_var in os.environ and \ 

144 lib_pass_through_var not in os.environ: 

145 libpathstr += '{}="{}" '.format(lib_pass_through_var, os.environ[aux_pass_through_var]) 

146 

147 if lib_pass_through_var in os.environ and \ 

148 aux_pass_through_var not in os.environ: 

149 libpathstr += '{}="{}" '.format(aux_pass_through_var, os.environ[lib_pass_through_var]) 

150 

151 return libpathstr 

152 

153 

154def runExternal(cmd, fatal=False, msg=None): 

155 """Safe wrapper for running external programs, reading stdout, and 

156 sanitizing error messages. 

157 

158 Parameters 

159 ---------- 

160 cmd : `str` or `list` or `tuple` 

161 Command to execute. Shell usage is disabled if a sequence is given. 

162 Shell is used if a single command string is given. 

163 fatal : `bool`, optional 

164 Control whether command failure is fatal or not. 

165 msg : `str` 

166 Message to report on command failure. 

167 

168 Returns 

169 ------- 

170 output : `str` 

171 Entire program output is returned, not just a single line. 

172 

173 Raises 

174 ------ 

175 RuntimeError 

176 If the command fails and ``fatal`` is `True`. 

177 """ 

178 if msg is None: 

179 try: 

180 msg = "Error running %s" % cmd.split()[0] 

181 except Exception: 

182 msg = "Error running external command" 

183 

184 # Run with shell unless given a list of options 

185 shell = True 

186 if isinstance(cmd, (list, tuple)): 

187 shell = False 

188 

189 try: 

190 retval = subprocess.run(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 

191 check=True) 

192 except subprocess.CalledProcessError as e: 

193 if fatal: 

194 raise RuntimeError(f"{msg}: {e.stderr.decode()}") from e 

195 else: 

196 from . import state # can't import at module scope due to circular dependency 

197 state.log.warn(f"{msg}: {e.stderr}") 

198 return retval.stdout.decode().strip() 

199 

200 

201def memberOf(cls, name=None): 

202 """A Python decorator that injects functions into a class. 

203 

204 Parameters 

205 ---------- 

206 cls : `class` 

207 Class in which to inject this method. 

208 name : `str`, optional 

209 Name of the method. Will be determined from function name if not 

210 define. 

211 

212 Notes 

213 ----- 

214 For example: 

215 

216 .. code-block:: python 

217 

218 class test_class: 

219 pass 

220 

221 @memberOf(test_class): 

222 def test_method(self): 

223 print("test_method!") 

224 

225 ...will cause ``test_method`` to appear as as if it were defined within 

226 ``test_class``. 

227 

228 The function or method will still be added to the module scope as well, 

229 replacing any existing module-scope function with that name; this appears 

230 to be unavoidable. 

231 """ 

232 if isinstance(cls, type): 232 ↛ 235line 232 didn't jump to line 235, because the condition on line 232 was never false

233 classes = (cls,) 

234 else: 

235 classes = tuple(cls) 

236 kw = {"name": name} 

237 

238 def nested(member): 

239 if kw["name"] is None: 239 ↛ 241line 239 didn't jump to line 241, because the condition on line 239 was never false

240 kw["name"] = member.__name__ 

241 for scope in classes: 

242 setattr(scope, kw["name"], member) 

243 return member 

244 return nested 

245 

246 

247def get_conda_prefix() -> Optional[str]: 

248 """Returns a copy of the current conda prefix, if available.""" 

249 _conda_prefix = os.environ.get('CONDA_PREFIX') 

250 if os.environ.get('CONDA_BUILD', "0") == "1": 

251 # when running conda-build, the right prefix to use is PREFIX 

252 # however, this appears to be absent in some builds - but we 

253 # already set the fallback 

254 if 'PREFIX' in os.environ: 

255 _conda_prefix = os.environ['PREFIX'] 

256 return _conda_prefix 

257 

258 

259def use_conda_compilers(): 

260 """Returns True if we should use conda compilers""" 

261 if "SCONSUTILS_AVOID_CONDA_COMPILERS" in os.environ: 261 ↛ 262line 261 didn't jump to line 262, because the condition on line 261 was never true

262 return False 

263 if "CONDA_BUILD_SYSROOT" in os.environ or "CONDA_PREFIX" in os.environ: 263 ↛ 264line 263 didn't jump to line 264, because the condition on line 263 was never true

264 return True 

265 if os.environ.get('CONDA_BUILD', "0") == "1": 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true

266 return True 

267 return False