command_wrappers.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. # vim: set fileencoding=utf-8 :
  2. #
  3. # (C) 2007,2009,2015 Guido Guenther <agx@sigxcpu.org>
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, please see
  16. # <http://www.gnu.org/licenses/>
  17. """
  18. Simple class wrappers for the various external commands needed by
  19. git-buildpackage and friends
  20. """
  21. import subprocess
  22. import os
  23. import os.path
  24. import signal
  25. import gbp.log as log
  26. class CommandExecFailed(Exception):
  27. """Exception raised by the Command class"""
  28. pass
  29. class Command(object):
  30. """
  31. Wraps a shell command, so we don't have to store any kind of command
  32. line options in one of the git-buildpackage commands
  33. """
  34. def __init__(self, cmd, args=[], shell=False, extra_env=None, cwd=None,
  35. capture_stderr=False,
  36. capture_stdout=False):
  37. self.cmd = cmd
  38. self.args = args
  39. self.run_error = "'%s' failed: {err_reason}" % (" ".join([self.cmd] + self.args))
  40. self.shell = shell
  41. self.capture_stdout = capture_stdout
  42. self.capture_stderr = capture_stderr
  43. self.cwd = cwd
  44. if extra_env is not None:
  45. self.env = os.environ.copy()
  46. self.env.update(extra_env)
  47. else:
  48. self.env = None
  49. self._reset_state()
  50. def _reset_state(self):
  51. self.retcode = 1
  52. self.stdout, self.stderr, self.err_reason = [''] * 3
  53. def __call(self, args):
  54. """
  55. Wraps subprocess.call so we can be verbose and fix Python's
  56. SIGPIPE handling
  57. """
  58. def default_sigpipe():
  59. "Restore default signal handler (http://bugs.python.org/issue1652)"
  60. signal.signal(signal.SIGPIPE, signal.SIG_DFL)
  61. log.debug("%s %s %s" % (self.cmd, self.args, args))
  62. self._reset_state()
  63. stdout_arg = subprocess.PIPE if self.capture_stdout else None
  64. stderr_arg = subprocess.PIPE if self.capture_stderr else None
  65. cmd = [ self.cmd ] + self.args + args
  66. if self.shell:
  67. # subprocess.call only cares about the first argument if shell=True
  68. cmd = " ".join(cmd)
  69. try:
  70. popen = subprocess.Popen(cmd,
  71. cwd=self.cwd,
  72. shell=self.shell,
  73. env=self.env,
  74. preexec_fn=default_sigpipe,
  75. stdout=stdout_arg,
  76. stderr=stderr_arg)
  77. (self.stdout, self.stderr) = popen.communicate()
  78. except OSError as err:
  79. self.err_reason = "execution failed: %s" % str(err)
  80. self.retcode = 1
  81. raise
  82. self.retcode = popen.returncode
  83. if self.retcode < 0:
  84. self.err_reason = "it was terminated by signal %d" % -self.retcode
  85. elif self.retcode > 0:
  86. self.err_reason = "it exited with %d" % self.retcode
  87. return self.retcode
  88. def _log_err(self):
  89. "Log an error message"
  90. log.err(self._format_err())
  91. def _format_err(self):
  92. """Log an error message
  93. This allows to replace stdout, stderr and err_reason in
  94. the self.run_error.
  95. """
  96. stdout = self.stdout.rstrip() if self.stdout else self.stdout
  97. stderr = self.stderr.rstrip() if self.stderr else self.stderr
  98. return self.run_error.format(stdout=stdout,
  99. stderr=stderr,
  100. err_reason=self.err_reason)
  101. def __call__(self, args=[], quiet=False):
  102. """Run the command and raise exception on errors
  103. If run quietly it will not print an error message via the
  104. L{gbp.log} logging API.
  105. Wether the command prints anything to stdout/stderr depends on
  106. the I{capture_stderr}, I{capture_stdout} instance variables.
  107. All errors will be reported as subclass of the
  108. L{CommandExecFailed} exception including a non zero exit
  109. status of the run command.
  110. @param args: additional command line arguments
  111. @type args: C{list} of C{strings}
  112. @param quiet: don't log failed execution to stderr. Mostly useful during
  113. unit testing
  114. @type quiet: C{bool}
  115. >>> Command("/bin/true")(["foo", "bar"])
  116. >>> Command("/foo/bar")(quiet=True)
  117. Traceback (most recent call last):
  118. ...
  119. CommandExecFailed: '/foo/bar' failed: execution failed: [Errno 2] No such file or directory
  120. """
  121. try:
  122. ret = self.__call(args)
  123. except OSError:
  124. ret = 1
  125. if ret:
  126. if not quiet:
  127. self._log_err()
  128. raise CommandExecFailed(self._format_err())
  129. def call(self, args, quiet=True):
  130. """Like L{__call__} but let the caller handle the return status.
  131. Only raise L{CommandExecFailed} if we failed to launch the command
  132. at all (i.e. if it does not exist) not if the command returned
  133. nonzero.
  134. Logs errors using L{gbp.log} by default.
  135. @param args: additional command line arguments
  136. @type args: C{list} of C{strings}
  137. @param quiet: don't log failed execution to stderr. Mostly useful during
  138. unit testing
  139. @type quiet: C{bool}
  140. @returns: the exit status of the run command
  141. @rtype: C{int}
  142. >>> Command("/bin/true").call(["foo", "bar"])
  143. 0
  144. >>> Command("/foo/bar").call(["foo", "bar"]) # doctest:+ELLIPSIS
  145. Traceback (most recent call last):
  146. ...
  147. CommandExecFailed: execution failed: ...
  148. >>> c = Command("/bin/true", capture_stdout=True,
  149. ... extra_env={'LC_ALL': 'C'})
  150. >>> c.call(["--version"])
  151. 0
  152. >>> c.stdout.decode('utf-8').startswith('true')
  153. True
  154. >>> c = Command("/bin/false", capture_stdout=True,
  155. ... extra_env={'LC_ALL': 'C'})
  156. >>> c.call(["--help"])
  157. 1
  158. >>> c.stdout.decode('utf-8').startswith('Usage:')
  159. True
  160. """
  161. try:
  162. ret = self.__call(args)
  163. except OSError:
  164. ret = 1
  165. raise CommandExecFailed(self.err_reason)
  166. finally:
  167. if ret and not quiet:
  168. self._log_err()
  169. return ret
  170. class RunAtCommand(Command):
  171. """Run a command in a specific directory"""
  172. def __call__(self, dir='.', *args):
  173. curdir = os.path.abspath(os.path.curdir)
  174. try:
  175. os.chdir(dir)
  176. Command.__call__(self, list(*args))
  177. os.chdir(curdir)
  178. except Exception:
  179. os.chdir(curdir)
  180. raise
  181. class UnpackTarArchive(Command):
  182. """Wrap tar to unpack a compressed tar archive"""
  183. def __init__(self, archive, dir, filters=[], compression=None):
  184. self.archive = archive
  185. self.dir = dir
  186. exclude = [("--exclude=%s" % _filter) for _filter in filters]
  187. if not compression:
  188. compression = '-a'
  189. Command.__init__(self, 'tar', exclude +
  190. ['-C', dir, compression, '-xf', archive ])
  191. self.run_error = 'Couldn\'t unpack "%s": {err_reason}' % self.archive
  192. class PackTarArchive(Command):
  193. """Wrap tar to pack a compressed tar archive"""
  194. def __init__(self, archive, dir, dest, filters=[], compression=None):
  195. self.archive = archive
  196. self.dir = dir
  197. exclude = [("--exclude=%s" % _filter) for _filter in filters]
  198. if not compression:
  199. compression = '-a'
  200. Command.__init__(self, 'tar', exclude +
  201. ['-C', dir, compression, '-cf', archive, dest])
  202. self.run_error = 'Couldn\'t repack "%s": {err_reason}' % self.archive
  203. class CatenateTarArchive(Command):
  204. """Wrap tar to catenate a tar file with the next"""
  205. def __init__(self, archive, **kwargs):
  206. self.archive = archive
  207. Command.__init__(self, 'tar', ['-A', '-f', archive], **kwargs)
  208. def __call__(self, target):
  209. Command.__call__(self, [target])
  210. class RemoveTree(Command):
  211. "Wrap rm to remove a whole directory tree"
  212. def __init__(self, tree):
  213. self.tree = tree
  214. Command.__init__(self, 'rm', [ '-rf', tree ])
  215. self.run_error = 'Couldn\'t remove "%s": {err_reason}' % self.tree
  216. class Dch(Command):
  217. """Wrap dch and set a specific version"""
  218. def __init__(self, version, msg):
  219. args = ['-v', version]
  220. if msg:
  221. args.append(msg)
  222. Command.__init__(self, 'debchange', args)
  223. self.run_error = "Dch failed: {err_reason}"
  224. class DpkgSourceExtract(Command):
  225. """
  226. Wrap dpkg-source to extract a Debian source package into a certain
  227. directory, this needs
  228. """
  229. def __init__(self):
  230. Command.__init__(self, 'dpkg-source', ['-x'])
  231. def __call__(self, dsc, output_dir):
  232. self.run_error = 'Couldn\'t extract "%s": {err_reason}' % dsc
  233. Command.__call__(self, [dsc, output_dir])
  234. class UnpackZipArchive(Command):
  235. """Wrap zip to Unpack a zip file"""
  236. def __init__(self, archive, dir):
  237. self.archive = archive
  238. self.dir = dir
  239. Command.__init__(self, 'unzip', [ "-q", archive, '-d', dir ])
  240. self.run_error = 'Couldn\'t unpack "%s": {err_reason}' % self.archive
  241. class CatenateZipArchive(Command):
  242. """Wrap zipmerge tool to catenate a zip file with the next"""
  243. def __init__(self, archive, **kwargs):
  244. self.archive = archive
  245. Command.__init__(self, 'zipmerge', [archive], **kwargs)
  246. def __call__(self, target):
  247. self.run_error = 'Couldn\'t append "%s" to "%s": {err_reason}' % \
  248. (target, self.archive)
  249. Command.__call__(self, [target])
  250. class GitCommand(Command):
  251. "Mother/Father of all git commands"
  252. def __init__(self, cmd, args=[], **kwargs):
  253. Command.__init__(self, 'git', [cmd] + args, **kwargs)
  254. self.run_error = "Couldn't run git %s: {err_reason}" % cmd
  255. # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: