utilities.py
Go to the documentation of this file.
1 # ============================================================================
2 # Copyright (c) 2011-2012 University of Pennsylvania
3 # Copyright (c) 2013-2016 Andreas Schuh
4 # All rights reserved.
5 #
6 # See COPYING file for license information or visit
7 # https://cmake-basis.github.io/download.html#license
8 # ============================================================================
9 
10 ##############################################################################
11 # @file utilities.py
12 # @brief Main module of project-independent BASIS utilities.
13 #
14 # This module defines the BASIS Utilities whose implementations are not
15 # project-specific, i.e., do not make use of particular project attributes such
16 # as the name or version of the project. The utility functions defined by this
17 # module are intended for use in Python scripts and modules that are not build
18 # as part of a particular BASIS project. Otherwise, the project-specific
19 # implementations should be used instead, i.e., those defined by the basis.py
20 # module of the project. The basis.py module and the submodules imported by
21 # it are generated from template modules which are customized for the particular
22 # project that is being build.
23 #
24 # @ingroup BasisPythonUtilities
25 ##############################################################################
26 
27 from __future__ import unicode_literals
28 
29 __all__ = [] # use of import * is discouraged
30 
31 import os
32 import sys
33 import re
34 import shlex
35 import subprocess
36 
37 from . import which
38 
39 # ============================================================================
40 # Python 2 and 3 compatibility
41 # ============================================================================
42 
43 if sys.version_info < (3,):
44  text_type = unicode
45  binary_type = str
46 else:
47  text_type = str
48  binary_type = bytes
49 
50 # ============================================================================
51 # constants
52 # ============================================================================
53 
54 ## @brief Default copyright of executables.
55 COPYRIGHT = """2011-12 University of Pennsylvania, 2013-14 Carnegie Mellon University, 2013-16 Andreas Schuh"""
56 ## @brief Default license of executables.
57 LICENSE = """See https://cmake-basis.github.io/download.html#license or COPYING file."""
58 ## @brief Default contact to use for help output of executables.
59 CONTACT = """andreas.schuh.84@gmail.com"""
60 
61 
62 # used to make base argument of functions absolute
63 _MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
64 
65 # ============================================================================
66 # Python 2 and 3 compatibility
67 # ============================================================================
68 
69 def decode(s):
70  if isinstance(s, binary_type): return s.decode()
71  elif isinstance(s, text_type): return s.decode()
72  else: return s
73 
74 # ============================================================================
75 # executable information
76 # ============================================================================
77 
78 # ----------------------------------------------------------------------------
79 ## @brief Print contact information.
80 #
81 # @param [in] contact Name of contact.
82 def print_contact(contact=CONTACT):
83  sys.stdout.write('Contact:\n ' + contact + '\n')
84 
85 # ----------------------------------------------------------------------------
86 ## @brief Print version information including copyright and license notices.
87 #
88 # @param [in] name Name of executable. Should not be set programmatically
89 # to the first argument of the @c __main__ module, but
90 # a string literal instead.
91 # @param [in] version Version of executable, e.g., release of project
92 # this executable belongs to.
93 # @param [in] project Name of project this executable belongs to.
94 # If @c None, or an empty string, no project
95 # information is printed.
96 # @param [in] copyright The copyright notice, excluding the common prefix
97 # "Copyright (c) " and suffix ". All rights reserved.".
98 # If @c None, or an empty string, no copyright notice
99 # is printed.
100 # @param [in] license Information regarding licensing. If @c None or an
101 # empty string, no license information is printed.
102 def print_version(name, version=None, project=None, copyright=COPYRIGHT, license=LICENSE):
103  if not version: raise Exception('print_version(): Missing version argument')
104  # program identification
105  sys.stdout.write(name)
106  if project:
107  sys.stdout.write(' (')
108  sys.stdout.write(project)
109  sys.stdout.write(')')
110  sys.stdout.write(' ')
111  sys.stdout.write(version)
112  sys.stdout.write('\n')
113  # copyright notice
114  if copyright:
115  sys.stdout.write("Copyright (c) ");
116  sys.stdout.write(copyright)
117  sys.stdout.write(". All rights reserved.\n")
118  # license information
119  if license:
120  sys.stdout.write(license)
121  sys.stdout.write('\n')
122 
123 # ----------------------------------------------------------------------------
124 ## @brief Get UID of build target.
125 #
126 # The UID of a build target is its name prepended by a namespace identifier
127 # which should be unique for each project.
128 #
129 # @param [in] name Name of build target.
130 # @param [in] prefix Common prefix of targets belonging to this project.
131 # @param [in] targets Dictionary mapping target UIDs to executable paths.
132 #
133 # @returns UID of named build target.
134 def targetuid(name, prefix=None, targets=None):
135  # handle invalid arguments
136  if not name: return None
137  # in case of a leading namespace separator, do not modify target name
138  if name.startswith('.'): return name
139  # common target UID prefix of project
140  if prefix is None or not targets: return name
141  # try prepending namespace or parts of it until target is known
142  separator = '.'
143  while True:
144  if separator.join([prefix, name]) in targets:
145  return separator.join([prefix, name])
146  parts = prefix.split(separator, 1)
147  if len(parts) == 1: break
148  prefix = parts[0]
149  # otherwise, return target name unchanged
150  return name
151 
152 # ----------------------------------------------------------------------------
153 ## @brief Determine whether a given build target is known.
154 #
155 # @param [in] name Name of build target.
156 # @param [in] prefix Common prefix of targets belonging to this project.
157 # @param [in] targets Dictionary mapping target UIDs to executable paths.
158 #
159 # @returns Whether the named target is a known executable target.
160 def istarget(name, prefix=None, targets=None):
161  uid = targetuid(name, prefix=prefix, targets=targets)
162  if not uid or not targets: return False
163  if uid.startswith('.'): uid = uid[1:]
164  return uid in targets
165 
166 # ----------------------------------------------------------------------------
167 ## @brief Get absolute path of executable file.
168 #
169 # This function determines the absolute file path of an executable. If no
170 # arguments are given, the absolute path of this executable is returned.
171 # If the command names a known executable build target, the absolute path to
172 # the corresonding built (and installed) executable file is returned.
173 # Otherwise, the named command is searched in the system @c PATH and its
174 # absolute path returned if found. If the executable is not found, @c None
175 # is returned.
176 #
177 # @param [in] name Name of command or @c None.
178 # @param [in] prefix Common prefix of targets belonging to this project.
179 # @param [in] targets Dictionary mapping target UIDs to executable paths.
180 # @param [in] base Base directory for relative paths in @p targets.
181 #
182 # @returns Absolute path of executable or @c None if not found.
183 # If @p name is @c None, the path of this executable is returned.
184 def exepath(name=None, prefix=None, targets=None, base='.'):
185  path = None
186  if name is None:
187  path = os.path.realpath(sys.argv[0])
188  elif istarget(name, prefix=prefix, targets=targets):
189  uid = targetuid(name, prefix=prefix, targets=targets)
190  if uid.startswith('.'): uid = uid[1:]
191  path = os.path.normpath(os.path.join(os.path.join(_MODULE_DIR, base), targets[uid]))
192  if '$<CONFIG>' in path:
193  for config in ['Release', 'Debug', 'RelWithDebInfo', 'MinSizeRel']:
194  tmppath = path.replace('$<CONFIG>', config)
195  if os.path.isfile(tmppath):
196  path = tmppath
197  break
198  path = path.replace('$<CONFIG>', '')
199  else:
200  try:
201  path = which.which(name)
202  except which.WhichError:
203  pass
204  return path
205 
206 # ----------------------------------------------------------------------------
207 ## @brief Get name of executable file.
208 #
209 # @param [in] name Name of command or @c None.
210 # @param [in] prefix Common prefix of targets belonging to this project.
211 # @param [in] targets Dictionary mapping target UIDs to executable paths.
212 # @param [in] base Base directory for relative paths in @p targets.
213 #
214 # @returns Name of executable file or @c None if not found.
215 # If @p name is @c None, the name of this executable is returned.
216 def exename(name=None, prefix=None, targets=None, base='.'):
217  path = exepath(name, prefix, targets, base)
218  if path is None: return None
219  name = os.path.basename(path)
220  if os.name == 'nt' and (name.endswith('.exe') or name.endswith('.com')):
221  name = name[:-4]
222  return name
223 
224 # ----------------------------------------------------------------------------
225 ## @brief Get directory of executable file.
226 #
227 # @param [in] name Name of command or @c None.
228 # @param [in] prefix Common prefix of targets belonging to this project.
229 # @param [in] targets Dictionary mapping target UIDs to executable paths.
230 # @param [in] base Base directory for relative paths in @p targets.
231 #
232 # @returns Absolute path to directory containing executable or @c None if not found.
233 # If @p name is @c None, the directory of this executable is returned.
234 def exedir(name=None, prefix=None, targets=None, base='.'):
235  path = exepath(name, prefix, targets, base)
236  if path is None: return None
237  return os.path.dirname(path)
238 
239 # ============================================================================
240 # command execution
241 # ============================================================================
242 
243 # ----------------------------------------------------------------------------
244 ## @brief Exception thrown when command execution failed.
245 class SubprocessError(Exception):
246  ## @brief Initialize exception, i.e., set message describing failure.
247  def __init__(self, msg):
248  self._message = msg
249  ## @brief Return string representation of exception message.
250  def __str__(self):
251  return self._message
252 
253 # ----------------------------------------------------------------------------
254 ## @brief Convert array of arguments to quoted string.
255 #
256 # @param [in] args Array of arguments, bytes, or non-string type.
257 #
258 # @returns Double quoted string, i.e., string where arguments are separated
259 # by a space character and surrounded by double quotes if necessary.
260 # Double quotes within an argument are escaped with a backslash.
261 #
262 # @sa split_quoted_string()
263 def tostring(args):
264  if type(args) is list:
265  qargs = []
266  re_quote_or_not = re.compile(r"'|\s|^$")
267  for arg in args:
268  # escape double quotes
269  arg = arg.replace('"', '\\"')
270  # surround element by double quotes if necessary
271  if re_quote_or_not.search(arg): qargs.append(''.join(['"', arg, '"']))
272  else: qargs.append(arg)
273  return ' '.join(qargs)
274  elif type(args) is binary_type:
275  return args.decode()
276  elif type(args) is text_type:
277  return args
278  else:
279  return text_type(args)
280 
281 # ----------------------------------------------------------------------------
282 ## @brief Split quoted string of arguments.
283 #
284 # @param [in] args Quoted string of arguments.
285 #
286 # @returns Array of arguments.
287 #
288 # @sa to_quoted_string()
289 def qsplit(args):
290  return shlex.split(args)
291 
292 # ----------------------------------------------------------------------------
293 ## @brief Execute command as subprocess.
294 #
295 # @param [in] args Command with arguments given either as quoted string
296 # or array of command name and arguments. In the latter
297 # case, the array elements are converted to strings
298 # using the built-in str() function. Hence, any type
299 # which can be converted to a string is permitted.
300 # The first argument must be the name or path of the
301 # executable of the command.
302 # @param [in] quiet Turns off output of @c stdout of child process to
303 # stdout of parent process.
304 # @param [in] stdout Whether to return the command output.
305 # @param [in] allow_fail If true, does not raise an exception if return
306 # value is non-zero. Otherwise, a @c SubprocessError is
307 # raised by this function.
308 # @param [in] verbose Verbosity of output messages.
309 # Does not affect verbosity of executed command.
310 # @param [in] simulate Whether to simulate command execution only.
311 # @param [in] prefix Common prefix of targets belonging to this project.
312 # @param [in] targets Dictionary mapping target UIDs to executable paths.
313 # @param [in] base Base directory for relative paths in @p targets.
314 #
315 # @return The exit code of the subprocess if @p stdout is false (the default).
316 # Otherwise, if @p stdout is true, a tuple consisting of exit code
317 # and binary/encoded command output is returned. Note that if
318 # @p allow_fail is false, the returned exit code will always be 0.
319 #
320 # @throws SubprocessError If command execution failed. This exception is not
321 # raised if the command executed with non-zero exit
322 # code but @p allow_fail set to @c True.
323 def execute(args, quiet=False, stdout=False, allow_fail=False, verbose=0, simulate=False,
324  prefix=None, targets=None, base='.'):
325  # convert args to list of strings
326  if type(args) is list: args = [tostring(i) for i in args]
327  else: args = qsplit(tostring(args))
328  if len(args) == 0: raise Exception("execute(): No command specified for execution")
329  # get absolute path of executable
330  path = exepath(args[0], prefix=prefix, targets=targets, base=base)
331  if not path: raise SubprocessError(args[0] + ": Command not found")
332  args[0] = path
333  # some verbose output
334  if verbose > 0 or simulate:
335  sys.stdout.write('$ ')
336  sys.stdout.write(tostring(args))
337  if simulate: sys.stdout.write(' (simulated)')
338  sys.stdout.write('\n')
339  # execute command
340  status = 0
341  output = b''
342  if not simulate:
343  try:
344  # open subprocess
345  process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
346  # read stdout until EOF
347  if hasattr(sys.stdout, 'buffer'):
348  for line in process.stdout:
349  if stdout:
350  output = b''.join([output, line])
351  if not quiet:
352  sys.stdout.buffer.write(line)
353  sys.stdout.flush()
354  else:
355  for line in process.stdout:
356  if stdout:
357  output = b''.join([output, line])
358  if not quiet:
359  if type(line) is text_type:
360  line = line.encode(sys.stdout.encoding)
361  elif type(line) is not binary_type:
362  line = binary_type(line)
363  sys.stdout.write(line)
364  sys.stdout.flush()
365  # wait until subprocess terminated and set exit code
366  _, err = process.communicate()
367  # print error messages of subprocess
368  if hasattr(sys.stderr, 'buffer'):
369  sys.stderr.buffer.write(err)
370  else:
371  for line in err:
372  if type(line) is text_type:
373  line = line.encode(sys.stderr.encoding)
374  elif type(line) is not binary_type:
375  line = binary_type(line)
376  sys.stderr.write(line)
377  # get exit code
378  status = process.returncode
379  except OSError as e:
380  raise SubprocessError(args[0] + ': ' + text_type(e))
381  except Exception as e:
382  msg = "Exception while executing \"" + args[0] + "\"!\n"
383  msg += "\tArguments: " + tostring(args[1:]) + '\n'
384  msg += '\t' + text_type(e)
385  raise SubprocessError(msg)
386  # if command failed, throw an exception
387  if status != 0 and not allow_fail:
388  raise SubprocessError("** Failed: " + tostring(args))
389  # return
390  if stdout: return (status, output)
391  else: return status
392 
393 
394 ## @}
395 # end of Doxygen group
def __init__(self, msg)
Initialize exception, i.e., set message describing failure.
Definition: utilities.py:247
def exename(name=None, prefix=None, targets=None, base='.')
Get name of executable file.
Definition: utilities.py:216
std::string join(const std::string &base, const std::string &path)
Join two paths, e.g., base path and relative path.
Definition: path.cxx:432
def exedir(name=None, prefix=None, targets=None, base='.')
Get directory of executable file.
Definition: utilities.py:234
def decode(s)
Definition: utilities.py:69
def qsplit(args)
Split quoted string of arguments.
Definition: utilities.py:289
def exepath(name=None, prefix=None, targets=None, base='.')
Get absolute path of executable file.
Definition: utilities.py:184
def print_contact(contact=CONTACT)
Print contact information.
Definition: utilities.py:82
def execute(args, quiet=False, stdout=False, allow_fail=False, verbose=0, simulate=False, prefix=None, targets=None, base='.')
Execute command as subprocess.
Definition: utilities.py:324
def targetuid(name, prefix=None, targets=None)
Get UID of build target.
Definition: utilities.py:134
Exception thrown when command execution failed.
Definition: utilities.py:245
def tostring(args)
Convert array of arguments to quoted string.
Definition: utilities.py:263
def istarget(name, prefix=None, targets=None)
Determine whether a given build target is known.
Definition: utilities.py:160
def print_version(name, version=None, project=None, copyright=COPYRIGHT, license=LICENSE)
Print version information including copyright and license notices.
Definition: utilities.py:102
def __str__(self)
Return string representation of exception message.
Definition: utilities.py:250