batchbuild.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. from __future__ import print_function
  2. import collections
  3. import itertools
  4. import json
  5. import os
  6. import os.path
  7. import re
  8. import shutil
  9. import string
  10. import subprocess
  11. import sys
  12. import cgi
  13. class BuildDesc:
  14. def __init__(self, prepend_envs=None, variables=None, build_type=None, generator=None):
  15. self.prepend_envs = prepend_envs or [] # [ { "var": "value" } ]
  16. self.variables = variables or []
  17. self.build_type = build_type
  18. self.generator = generator
  19. def merged_with(self, build_desc):
  20. """Returns a new BuildDesc by merging field content.
  21. Prefer build_desc fields to self fields for single valued field.
  22. """
  23. return BuildDesc(self.prepend_envs + build_desc.prepend_envs,
  24. self.variables + build_desc.variables,
  25. build_desc.build_type or self.build_type,
  26. build_desc.generator or self.generator)
  27. def env(self):
  28. environ = os.environ.copy()
  29. for values_by_name in self.prepend_envs:
  30. for var, value in list(values_by_name.items()):
  31. var = var.upper()
  32. if type(value) is unicode:
  33. value = value.encode(sys.getdefaultencoding())
  34. if var in environ:
  35. environ[var] = value + os.pathsep + environ[var]
  36. else:
  37. environ[var] = value
  38. return environ
  39. def cmake_args(self):
  40. args = ["-D%s" % var for var in self.variables]
  41. # skip build type for Visual Studio solution as it cause warning
  42. if self.build_type and 'Visual' not in self.generator:
  43. args.append("-DCMAKE_BUILD_TYPE=%s" % self.build_type)
  44. if self.generator:
  45. args.extend(['-G', self.generator])
  46. return args
  47. def __repr__(self):
  48. return "BuildDesc(%s, build_type=%s)" % (" ".join(self.cmake_args()), self.build_type)
  49. class BuildData:
  50. def __init__(self, desc, work_dir, source_dir):
  51. self.desc = desc
  52. self.work_dir = work_dir
  53. self.source_dir = source_dir
  54. self.cmake_log_path = os.path.join(work_dir, 'batchbuild_cmake.log')
  55. self.build_log_path = os.path.join(work_dir, 'batchbuild_build.log')
  56. self.cmake_succeeded = False
  57. self.build_succeeded = False
  58. def execute_build(self):
  59. print('Build %s' % self.desc)
  60. self._make_new_work_dir()
  61. self.cmake_succeeded = self._generate_makefiles()
  62. if self.cmake_succeeded:
  63. self.build_succeeded = self._build_using_makefiles()
  64. return self.build_succeeded
  65. def _generate_makefiles(self):
  66. print(' Generating makefiles: ', end=' ')
  67. cmd = ['cmake'] + self.desc.cmake_args() + [os.path.abspath(self.source_dir)]
  68. succeeded = self._execute_build_subprocess(cmd, self.desc.env(), self.cmake_log_path)
  69. print('done' if succeeded else 'FAILED')
  70. return succeeded
  71. def _build_using_makefiles(self):
  72. print(' Building:', end=' ')
  73. cmd = ['cmake', '--build', self.work_dir]
  74. if self.desc.build_type:
  75. cmd += ['--config', self.desc.build_type]
  76. succeeded = self._execute_build_subprocess(cmd, self.desc.env(), self.build_log_path)
  77. print('done' if succeeded else 'FAILED')
  78. return succeeded
  79. def _execute_build_subprocess(self, cmd, env, log_path):
  80. process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.work_dir,
  81. env=env)
  82. stdout, _ = process.communicate()
  83. succeeded = (process.returncode == 0)
  84. with open(log_path, 'wb') as flog:
  85. log = ' '.join(cmd) + '\n' + stdout + '\nExit code: %r\n' % process.returncode
  86. flog.write(fix_eol(log))
  87. return succeeded
  88. def _make_new_work_dir(self):
  89. if os.path.isdir(self.work_dir):
  90. print(' Removing work directory', self.work_dir)
  91. shutil.rmtree(self.work_dir, ignore_errors=True)
  92. if not os.path.isdir(self.work_dir):
  93. os.makedirs(self.work_dir)
  94. def fix_eol(stdout):
  95. """Fixes wrong EOL produced by cmake --build on Windows (\r\r\n instead of \r\n).
  96. """
  97. return re.sub('\r*\n', os.linesep, stdout)
  98. def load_build_variants_from_config(config_path):
  99. with open(config_path, 'rb') as fconfig:
  100. data = json.load(fconfig)
  101. variants = data[ 'cmake_variants' ]
  102. build_descs_by_axis = collections.defaultdict(list)
  103. for axis in variants:
  104. axis_name = axis["name"]
  105. build_descs = []
  106. if "generators" in axis:
  107. for generator_data in axis["generators"]:
  108. for generator in generator_data["generator"]:
  109. build_desc = BuildDesc(generator=generator,
  110. prepend_envs=generator_data.get("env_prepend"))
  111. build_descs.append(build_desc)
  112. elif "variables" in axis:
  113. for variables in axis["variables"]:
  114. build_desc = BuildDesc(variables=variables)
  115. build_descs.append(build_desc)
  116. elif "build_types" in axis:
  117. for build_type in axis["build_types"]:
  118. build_desc = BuildDesc(build_type=build_type)
  119. build_descs.append(build_desc)
  120. build_descs_by_axis[axis_name].extend(build_descs)
  121. return build_descs_by_axis
  122. def generate_build_variants(build_descs_by_axis):
  123. """Returns a list of BuildDesc generated for the partial BuildDesc for each axis."""
  124. axis_names = list(build_descs_by_axis.keys())
  125. build_descs = []
  126. for axis_name, axis_build_descs in list(build_descs_by_axis.items()):
  127. if len(build_descs):
  128. # for each existing build_desc and each axis build desc, create a new build_desc
  129. new_build_descs = []
  130. for prototype_build_desc, axis_build_desc in itertools.product(build_descs, axis_build_descs):
  131. new_build_descs.append(prototype_build_desc.merged_with(axis_build_desc))
  132. build_descs = new_build_descs
  133. else:
  134. build_descs = axis_build_descs
  135. return build_descs
  136. HTML_TEMPLATE = string.Template('''<html>
  137. <head>
  138. <title>$title</title>
  139. <style type="text/css">
  140. td.failed {background-color:#f08080;}
  141. td.ok {background-color:#c0eec0;}
  142. </style>
  143. </head>
  144. <body>
  145. <table border="1">
  146. <thead>
  147. <tr>
  148. <th>Variables</th>
  149. $th_vars
  150. </tr>
  151. <tr>
  152. <th>Build type</th>
  153. $th_build_types
  154. </tr>
  155. </thead>
  156. <tbody>
  157. $tr_builds
  158. </tbody>
  159. </table>
  160. </body></html>''')
  161. def generate_html_report(html_report_path, builds):
  162. report_dir = os.path.dirname(html_report_path)
  163. # Vertical axis: generator
  164. # Horizontal: variables, then build_type
  165. builds_by_generator = collections.defaultdict(list)
  166. variables = set()
  167. build_types_by_variable = collections.defaultdict(set)
  168. build_by_pos_key = {} # { (generator, var_key, build_type): build }
  169. for build in builds:
  170. builds_by_generator[build.desc.generator].append(build)
  171. var_key = tuple(sorted(build.desc.variables))
  172. variables.add(var_key)
  173. build_types_by_variable[var_key].add(build.desc.build_type)
  174. pos_key = (build.desc.generator, var_key, build.desc.build_type)
  175. build_by_pos_key[pos_key] = build
  176. variables = sorted(variables)
  177. th_vars = []
  178. th_build_types = []
  179. for variable in variables:
  180. build_types = sorted(build_types_by_variable[variable])
  181. nb_build_type = len(build_types_by_variable[variable])
  182. th_vars.append('<th colspan="%d">%s</th>' % (nb_build_type, cgi.escape(' '.join(variable))))
  183. for build_type in build_types:
  184. th_build_types.append('<th>%s</th>' % cgi.escape(build_type))
  185. tr_builds = []
  186. for generator in sorted(builds_by_generator):
  187. tds = [ '<td>%s</td>\n' % cgi.escape(generator) ]
  188. for variable in variables:
  189. build_types = sorted(build_types_by_variable[variable])
  190. for build_type in build_types:
  191. pos_key = (generator, variable, build_type)
  192. build = build_by_pos_key.get(pos_key)
  193. if build:
  194. cmake_status = 'ok' if build.cmake_succeeded else 'FAILED'
  195. build_status = 'ok' if build.build_succeeded else 'FAILED'
  196. cmake_log_url = os.path.relpath(build.cmake_log_path, report_dir)
  197. build_log_url = os.path.relpath(build.build_log_path, report_dir)
  198. td = '<td class="%s"><a href="%s" class="%s">CMake: %s</a>' % ( build_status.lower(), cmake_log_url, cmake_status.lower(), cmake_status)
  199. if build.cmake_succeeded:
  200. td += '<br><a href="%s" class="%s">Build: %s</a>' % ( build_log_url, build_status.lower(), build_status)
  201. td += '</td>'
  202. else:
  203. td = '<td></td>'
  204. tds.append(td)
  205. tr_builds.append('<tr>%s</tr>' % '\n'.join(tds))
  206. html = HTML_TEMPLATE.substitute( title='Batch build report',
  207. th_vars=' '.join(th_vars),
  208. th_build_types=' '.join(th_build_types),
  209. tr_builds='\n'.join(tr_builds))
  210. with open(html_report_path, 'wt') as fhtml:
  211. fhtml.write(html)
  212. print('HTML report generated in:', html_report_path)
  213. def main():
  214. usage = r"""%prog WORK_DIR SOURCE_DIR CONFIG_JSON_PATH [CONFIG2_JSON_PATH...]
  215. Build a given CMake based project located in SOURCE_DIR with multiple generators/options.dry_run
  216. as described in CONFIG_JSON_PATH building in WORK_DIR.
  217. Example of call:
  218. python devtools\batchbuild.py e:\buildbots\jsoncpp\build . devtools\agent_vmw7.json
  219. """
  220. from optparse import OptionParser
  221. parser = OptionParser(usage=usage)
  222. parser.allow_interspersed_args = True
  223. # parser.add_option('-v', '--verbose', dest="verbose", action='store_true',
  224. # help="""Be verbose.""")
  225. parser.enable_interspersed_args()
  226. options, args = parser.parse_args()
  227. if len(args) < 3:
  228. parser.error("Missing one of WORK_DIR SOURCE_DIR CONFIG_JSON_PATH.")
  229. work_dir = args[0]
  230. source_dir = args[1].rstrip('/\\')
  231. config_paths = args[2:]
  232. for config_path in config_paths:
  233. if not os.path.isfile(config_path):
  234. parser.error("Can not read: %r" % config_path)
  235. # generate build variants
  236. build_descs = []
  237. for config_path in config_paths:
  238. build_descs_by_axis = load_build_variants_from_config(config_path)
  239. build_descs.extend(generate_build_variants(build_descs_by_axis))
  240. print('Build variants (%d):' % len(build_descs))
  241. # assign build directory for each variant
  242. if not os.path.isdir(work_dir):
  243. os.makedirs(work_dir)
  244. builds = []
  245. with open(os.path.join(work_dir, 'matrix-dir-map.txt'), 'wt') as fmatrixmap:
  246. for index, build_desc in enumerate(build_descs):
  247. build_desc_work_dir = os.path.join(work_dir, '%03d' % (index+1))
  248. builds.append(BuildData(build_desc, build_desc_work_dir, source_dir))
  249. fmatrixmap.write('%s: %s\n' % (build_desc_work_dir, build_desc))
  250. for build in builds:
  251. build.execute_build()
  252. html_report_path = os.path.join(work_dir, 'batchbuild-report.html')
  253. generate_html_report(html_report_path, builds)
  254. print('Done')
  255. if __name__ == '__main__':
  256. main()