generate_vcs_info.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. # coding: utf-8
  2. import json
  3. import locale
  4. import re
  5. import os
  6. import subprocess
  7. import sys
  8. import time
  9. import six as six_
  10. INDENT = " " * 4
  11. def _get_vcs_dictionary(vcs_type, *arg):
  12. if vcs_type == 'git':
  13. return _GitVersion.parse(*arg)
  14. else:
  15. raise Exception("Unknown VCS type {}".format(str(vcs_type)))
  16. def _get_user_locale():
  17. try:
  18. if six_.PY3:
  19. return [locale.getencoding()]
  20. else:
  21. return [locale.getdefaultlocale()[1]]
  22. except Exception:
  23. return []
  24. class _GitVersion:
  25. @classmethod
  26. def parse(cls, commit_hash, author_info, summary_info, body_info, tag_info, branch_info, depth=None):
  27. r"""Parses output of
  28. git rev-parse HEAD
  29. git log -1 --format='format:%an <%ae>'
  30. git log -1 --format='format:%s'
  31. git log -1 --grep='^git-svn-id: ' --format='format:%b' or
  32. git log -1 --grep='^Revision: r?\d*' --format='format:%b
  33. git describe --exact-match --tags HEAD
  34. git describe --exact-match --all HEAD
  35. and depth as computed by _get_git_depth
  36. '"""
  37. info = {}
  38. info['hash'] = commit_hash
  39. info['commit_author'] = _SystemInfo._to_text(author_info)
  40. info['summary'] = _SystemInfo._to_text(summary_info)
  41. if 'svn_commit_revision' not in info:
  42. url = re.search("git?-svn?-id: (.*)@(\\d*).*", body_info)
  43. if url:
  44. info['svn_url'] = url.group(1)
  45. info['svn_commit_revision'] = int(url.group(2))
  46. if 'svn_commit_revision' not in info:
  47. rev = re.search('Revision: r?(\\d*).*', body_info)
  48. if rev:
  49. info['svn_commit_revision'] = int(rev.group(1))
  50. info['tag'] = tag_info
  51. info['branch'] = branch_info
  52. info['scm_text'] = cls._format_scm_data(info)
  53. info['vcs'] = 'git'
  54. if depth:
  55. info['patch_number'] = int(depth)
  56. return info
  57. @staticmethod
  58. def _format_scm_data(info):
  59. scm_data = "Git info:\n"
  60. scm_data += INDENT + "Commit: " + info['hash'] + "\n"
  61. scm_data += INDENT + "Branch: " + info['branch'] + "\n"
  62. scm_data += INDENT + "Author: " + info['commit_author'] + "\n"
  63. scm_data += INDENT + "Summary: " + info['summary'] + "\n"
  64. if 'svn_commit_revision' in info or 'svn_url' in info:
  65. scm_data += INDENT + "git-svn info:\n"
  66. if 'svn_url' in info:
  67. scm_data += INDENT + "URL: " + info['svn_url'] + "\n"
  68. if 'svn_commit_revision' in info:
  69. scm_data += INDENT + "Last Changed Rev: " + str(info['svn_commit_revision']) + "\n"
  70. return scm_data
  71. @staticmethod
  72. def external_data(arc_root):
  73. env = os.environ.copy()
  74. env['TZ'] = ''
  75. hash_args = ['rev-parse', 'HEAD']
  76. author_args = ['log', '-1', '--format=format:%an <%ae>']
  77. summary_args = ['log', '-1', '--format=format:%s']
  78. svn_args = ['log', '-1', '--grep=^git-svn-id: ', '--format=format:%b']
  79. svn_args_alt = ['log', '-1', '--grep=^Revision: r\\?\\d*', '--format=format:%b']
  80. tag_args = ['describe', '--exact-match', '--tags', 'HEAD']
  81. branch_args = ['describe', '--exact-match', '--all', 'HEAD']
  82. # using local 'Popen' wrapper
  83. commit = _SystemInfo._system_command_call(['git'] + hash_args, env=env, cwd=arc_root).rstrip()
  84. author = _SystemInfo._system_command_call(['git'] + author_args, env=env, cwd=arc_root)
  85. commit = _SystemInfo._system_command_call(['git'] + hash_args, env=env, cwd=arc_root).rstrip()
  86. author = _SystemInfo._system_command_call(['git'] + author_args, env=env, cwd=arc_root)
  87. summary = _SystemInfo._system_command_call(['git'] + summary_args, env=env, cwd=arc_root)
  88. svn_id = _SystemInfo._system_command_call(['git'] + svn_args, env=env, cwd=arc_root)
  89. if not svn_id:
  90. svn_id = _SystemInfo._system_command_call(['git'] + svn_args_alt, env=env, cwd=arc_root)
  91. try:
  92. tag_info = _SystemInfo._system_command_call(['git'] + tag_args, env=env, cwd=arc_root).splitlines()
  93. except Exception:
  94. tag_info = [''.encode('utf-8')]
  95. try:
  96. branch_info = _SystemInfo._system_command_call(['git'] + branch_args, env=env, cwd=arc_root).splitlines()
  97. except Exception:
  98. branch_info = [''.encode('utf-8')]
  99. depth = six_.text_type(_GitVersion._get_git_depth(env, arc_root)).encode('utf-8')
  100. # logger.debug('Git info commit:{}, author:{}, summary:{}, svn_id:{}'.format(commit, author, summary, svn_id))
  101. return [commit, author, summary, svn_id, tag_info[0], branch_info[0], depth]
  102. # YT's patch number.
  103. @staticmethod
  104. def _get_git_depth(env, arc_root):
  105. graph = {}
  106. full_history_args = ["log", "--full-history", "--format=%H %P", "HEAD"]
  107. history = _SystemInfo._system_command_call(['git'] + full_history_args, env=env, cwd=arc_root).decode('utf-8')
  108. head = None
  109. for line in history.splitlines():
  110. values = line.split()
  111. if values:
  112. if head is None:
  113. head = values[0]
  114. graph[values[0]] = values[1:]
  115. assert head
  116. cache = {}
  117. stack = [(head, None, False)]
  118. while stack:
  119. commit, child, calculated = stack.pop()
  120. if commit in cache:
  121. calculated = True
  122. if calculated:
  123. if child is not None:
  124. cache[child] = max(cache.get(child, 0), cache[commit] + 1)
  125. else:
  126. stack.append((commit, child, True))
  127. parents = graph[commit]
  128. if not parents:
  129. cache[commit] = 0
  130. else:
  131. for parent in parents:
  132. stack.append((parent, commit, False))
  133. return cache[head]
  134. class _SystemInfo:
  135. LOCALE_LIST = _get_user_locale() + [sys.getfilesystemencoding(), 'utf-8']
  136. @classmethod
  137. def get_locale(cls):
  138. import codecs
  139. for i in cls.LOCALE_LIST:
  140. if not i:
  141. continue
  142. try:
  143. codecs.lookup(i)
  144. return i
  145. except LookupError:
  146. continue
  147. @staticmethod
  148. def _to_text(s):
  149. if isinstance(s, six_.binary_type):
  150. return s.decode(_SystemInfo.get_locale(), errors='replace')
  151. return s
  152. @staticmethod
  153. def get_user():
  154. sys_user = os.environ.get("USER")
  155. if not sys_user:
  156. sys_user = os.environ.get("USERNAME")
  157. if not sys_user:
  158. sys_user = os.environ.get("LOGNAME")
  159. if not sys_user:
  160. sys_user = "Unknown user"
  161. return sys_user
  162. @staticmethod
  163. def get_date(stamp=None):
  164. # Format compatible with SVN-xml format.
  165. return time.strftime("%Y-%m-%dT%H:%M:%S.000000Z", time.gmtime(stamp))
  166. @staticmethod
  167. def get_timestamp():
  168. # Unix timestamp.
  169. return int(time.time())
  170. @staticmethod
  171. def get_other_data(src_dir, data_file='local.ymake'):
  172. other_data = "Other info:\n"
  173. other_data += INDENT + "Build by: " + _SystemInfo.get_user() + "\n"
  174. other_data += INDENT + "Top src dir: {}\n".format(src_dir)
  175. # logger.debug("Other data: %s", other_data)
  176. return other_data
  177. @staticmethod
  178. def _get_host_info(fake_build_info=False):
  179. if fake_build_info:
  180. host_info = '*sys localhost 1.0.0 #dummy information '
  181. elif not on_win():
  182. host_info = ' '.join(os.uname())
  183. else:
  184. host_info = _SystemInfo._system_command_call("VER") # XXX: check shell from cygwin to call VER this way!
  185. return INDENT + INDENT + host_info.strip() + "\n" if host_info else ""
  186. @staticmethod
  187. def _system_command_call(command, **kwargs):
  188. if isinstance(command, list):
  189. command = subprocess.list2cmdline(command)
  190. try:
  191. process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, **kwargs)
  192. stdout, stderr = process.communicate()
  193. if process.returncode != 0:
  194. # logger.debug('{}\nRunning {} failed with exit code {}\n'.format(stderr, command, process.returncode))
  195. raise get_svn_exception()(stdout=stdout, stderr=stderr, rc=process.returncode, cmd=[command])
  196. return stdout
  197. except OSError as e:
  198. msg = e.strerror
  199. errcodes = 'error {}'.format(e.errno)
  200. if on_win() and isinstance(e, WindowsError):
  201. errcodes += ', win-error {}'.format(e.winerror)
  202. try:
  203. import ctypes
  204. msg = six_.text_type(ctypes.FormatError(e.winerror), _SystemInfo.get_locale()).encode('utf-8')
  205. except ImportError:
  206. pass
  207. # logger.debug('System command call {} failed [{}]: {}\n'.format(command, errcodes, msg))
  208. return None
  209. def _get_raw_data(vcs_type, vcs_root):
  210. lines = []
  211. if vcs_type == 'git':
  212. lines = _GitVersion.external_data(vcs_root)
  213. return [l.decode('utf-8') for l in lines]
  214. def _get_json(vcs_root):
  215. try:
  216. vcs_type = "git"
  217. info = _get_vcs_dictionary(vcs_type, *_get_raw_data(vcs_type, vcs_root))
  218. return info, vcs_root
  219. except Exception:
  220. return None, ""
  221. def _dump_json(
  222. arc_root,
  223. info,
  224. other_data=None,
  225. build_user=None,
  226. build_date=None,
  227. build_timestamp=0,
  228. custom_version='',
  229. ):
  230. j = {}
  231. j['PROGRAM_VERSION'] = info['scm_text'] + "\n" + _SystemInfo._to_text(other_data)
  232. j['CUSTOM_VERSION'] = str(_SystemInfo._to_text(custom_version))
  233. j['SCM_DATA'] = info['scm_text']
  234. j['ARCADIA_SOURCE_PATH'] = _SystemInfo._to_text(arc_root)
  235. j['ARCADIA_SOURCE_URL'] = info.get('url', info.get('svn_url', ''))
  236. j['ARCADIA_SOURCE_REVISION'] = info.get('revision', -1)
  237. j['ARCADIA_SOURCE_HG_HASH'] = info.get('hash', '')
  238. j['ARCADIA_SOURCE_LAST_CHANGE'] = info.get('commit_revision', info.get('svn_commit_revision', -1))
  239. j['ARCADIA_SOURCE_LAST_AUTHOR'] = info.get('commit_author', '')
  240. j['ARCADIA_PATCH_NUMBER'] = info.get('patch_number', 0)
  241. j['BUILD_USER'] = _SystemInfo._to_text(build_user)
  242. j['VCS'] = info.get('vcs', '')
  243. j['BRANCH'] = info.get('branch', '')
  244. j['ARCADIA_TAG'] = info.get('tag', '')
  245. j['DIRTY'] = info.get('dirty', '')
  246. if 'url' in info or 'svn_url' in info:
  247. j['SVN_REVISION'] = info.get('svn_commit_revision', info.get('revision', -1))
  248. j['SVN_ARCROOT'] = info.get('url', info.get('svn_url', ''))
  249. j['SVN_TIME'] = info.get('commit_date', info.get('svn_commit_date', ''))
  250. j['BUILD_DATE'] = build_date
  251. j['BUILD_TIMESTAMP'] = build_timestamp
  252. return json.dumps(j, sort_keys=True, indent=4, separators=(',', ': '))
  253. def get_version_info(arc_root, custom_version=""):
  254. info, vcs_root = _get_json(arc_root)
  255. if info is None:
  256. return ""
  257. return _dump_json(
  258. vcs_root,
  259. info,
  260. other_data=_SystemInfo.get_other_data(
  261. src_dir=vcs_root,
  262. ),
  263. build_user=_SystemInfo.get_user(),
  264. build_date=_SystemInfo.get_date(None),
  265. build_timestamp=_SystemInfo.get_timestamp(),
  266. custom_version=custom_version,
  267. )
  268. if __name__ == '__main__':
  269. with open(sys.argv[1], 'w') as f:
  270. f.write(get_version_info(sys.argv[2]))