Coverage for python/lsst/sconsUtils/utils.py: 35%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Internal utilities for sconsUtils."""
3__all__ = ("Log", "_has_OSX_SIP", "libraryPathPassThrough", "whichPython",
4 "needShebangRewrite", "libraryLoaderEnvironment", "runExternal",
5 "memberOf", "get_conda_prefix")
7import os
8import sys
9import warnings
10import subprocess
11import platform
12from typing import Optional
13import SCons.Script
16class Log:
17 """A dead-simple logger for all messages.
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 """
25 def __init__(self):
26 self.traceback = False
27 self.verbose = True
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)
33 def warn(self, message):
34 if self.traceback:
35 warnings.warn(message, stacklevel=2)
36 else:
37 print(message, file=sys.stderr)
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)
47 def flush(self):
48 sys.stderr.flush()
51def _has_OSX_SIP():
52 """Internal function indicating that the OS has System
53 Integrity Protection.
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
71def libraryPathPassThrough():
72 """Name of library path environment variable to be passed throughself.
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
85# Cache variable for whichPython() function
86_pythonPath = None
89def whichPython():
90 """Path of Python executable to use.
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
108def needShebangRewrite():
109 """Is shebang rewriting required?
111 Returns
112 -------
113 rewrite : `bool`
114 Returns True if the shebang lines of executables should be rewritten.
115 """
116 return _has_OSX_SIP()
119def libraryLoaderEnvironment():
120 """Calculate library loader path environment string to be prepended to
121 external commands.
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
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])
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])
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])
151 return libpathstr
154def runExternal(cmd, fatal=False, msg=None):
155 """Safe wrapper for running external programs, reading stdout, and
156 sanitizing error messages.
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.
168 Returns
169 -------
170 output : `str`
171 Entire program output is returned, not just a single line.
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"
184 # Run with shell unless given a list of options
185 shell = True
186 if isinstance(cmd, (list, tuple)):
187 shell = False
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()
201def memberOf(cls, name=None):
202 """A Python decorator that injects functions into a class.
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.
212 Notes
213 -----
214 For example:
216 .. code-block:: python
218 class test_class:
219 pass
221 @memberOf(test_class):
222 def test_method(self):
223 print("test_method!")
225 ...will cause ``test_method`` to appear as as if it were defined within
226 ``test_class``.
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}
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
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": 250 ↛ 254line 250 didn't jump to line 254, because the condition on line 250 was never true
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
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 ↛ 265line 263 didn't jump to line 265, because the condition on line 263 was never false
264 return True
265 if os.environ.get('CONDA_BUILD', "0") == "1":
266 return True
267 return False