build_configs.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. #!/usr/bin/env python3
  2. '''
  3. Configuration builder.
  4. Generate a large number of valid configurations, build them concurrently,
  5. and report.
  6. '''
  7. import argparse
  8. import itertools
  9. import multiprocessing
  10. import os
  11. import re
  12. import shutil
  13. import subprocess
  14. import sys
  15. import tempfile
  16. def quote_if_needed(value):
  17. if not value.isdigit() and value != 'y' and value != 'n':
  18. value = '"%s"' % value
  19. return value
  20. def gen_configs_list(options_dict):
  21. 'Generate a list of all possible combinations of options.'
  22. names = options_dict.keys()
  23. product = itertools.product(*options_dict.values())
  24. return [dict(zip(names, x)) for x in product]
  25. def gen_configs_values_str(options_dict):
  26. 'Generate a list of all possible combinations of options as strings.'
  27. return [' '.join(x.values()) for x in gen_configs_list(options_dict)]
  28. def gen_cc_options_list(options_dict):
  29. '''
  30. Does the same as gen_configs_values_str() but adds the -Werror option
  31. to all generated strings.
  32. '''
  33. return ['{} -Werror'.format(x) for x in gen_configs_values_str(options_dict)]
  34. def gen_exclusive_boolean_filter(args):
  35. enabled_option, options_list = args
  36. option_filter = dict()
  37. for option in options_list:
  38. if option == enabled_option:
  39. value = [True, 'y']
  40. else:
  41. value = [True, 'n']
  42. option_filter.update({option: value})
  43. return option_filter
  44. def gen_exclusive_boolean_filters_list(options_list, all_disabled=False):
  45. '''
  46. Generate a list of passing filters on a list of boolean options.
  47. The resulting filters match configurations that have only one of the given
  48. options enabled, unless all_disabled is true, in which case an additional
  49. filter is generated to match configurations where none of the options
  50. are enabled.
  51. '''
  52. option_and_options = [(x, options_list) for x in options_list]
  53. if all_disabled:
  54. option_and_options.append((None, options_list))
  55. return list(map(gen_exclusive_boolean_filter, option_and_options))
  56. # Dictionary of compiler options.
  57. #
  58. # Each entry describes a single compiler option. The key is mostly ignored
  59. # and serves as description, but must be present in order to reuse the
  60. # gen_configs_list() function. The value is a list of all values that may
  61. # be used for this compiler option when building a configuration.
  62. all_cc_options_dict = {
  63. 'O' : ['-O0', '-O2', '-Os'],
  64. 'LTO' : ['-flto', '-fno-lto'],
  65. 'SSP' : ['-fno-stack-protector', '-fstack-protector'],
  66. }
  67. # Dictionaries of options.
  68. #
  69. # Each entry describes a single option. The key matches an option name
  70. # whereas the value is a list of all values that may be used for this
  71. # option when building a configuration.
  72. small_options_dict = {
  73. 'CONFIG_CC_EXE' : ['gcc', 'clang'],
  74. 'CONFIG_64BITS' : ['y', 'n'],
  75. 'CONFIG_CC_OPTIONS' : gen_cc_options_list(all_cc_options_dict),
  76. 'CONFIG_SMP' : ['y', 'n'],
  77. 'CONFIG_MAX_CPUS' : ['1', '128'],
  78. 'CONFIG_ASSERT' : ['y', 'n'],
  79. }
  80. large_options_dict = dict(small_options_dict)
  81. large_options_dict.update({
  82. 'CONFIG_X86_PAE' : ['y', 'n'],
  83. 'CONFIG_MUTEX_ADAPTIVE' : ['y', 'n'],
  84. 'CONFIG_MUTEX_PI' : ['y', 'n'],
  85. 'CONFIG_MUTEX_PLAIN' : ['y', 'n'],
  86. 'CONFIG_SHELL' : ['y', 'n'],
  87. 'CONFIG_THREAD_STACK_GUARD' : ['y', 'n'],
  88. })
  89. # TODO Generate this list from test/test_*.c
  90. test_list = [
  91. 'CONFIG_TEST_MODULE_ATOMIC',
  92. 'CONFIG_TEST_MODULE_BULLETIN',
  93. 'CONFIG_TEST_MODULE_MUTEX',
  94. 'CONFIG_TEST_MODULE_MUTEX_PI',
  95. 'CONFIG_TEST_MODULE_PMAP_UPDATE_MP',
  96. 'CONFIG_TEST_MODULE_RCU_DEFER',
  97. 'CONFIG_TEST_MODULE_SREF_DIRTY_ZEROES',
  98. 'CONFIG_TEST_MODULE_SREF_NOREF',
  99. 'CONFIG_TEST_MODULE_SREF_WEAKREF',
  100. 'CONFIG_TEST_MODULE_VM_PAGE_FILL',
  101. 'CONFIG_TEST_MODULE_XCALL',
  102. ]
  103. test_options_dict = dict(small_options_dict)
  104. for test in test_list:
  105. test_options_dict.update({test: ['y', 'n']})
  106. all_options_sets = {
  107. 'small' : small_options_dict,
  108. 'large' : large_options_dict,
  109. 'test' : test_options_dict,
  110. }
  111. # Filters.
  112. #
  113. # A filter is a list of dictionaries of options. For each dictionary, the
  114. # key matches an option name whereas the value is a
  115. # [match_flag, string/regular expression] list. The match flag is true if
  116. # the matching expression must match, false otherwise.
  117. #
  118. # Passing filters are used to allow configurations that match the filters,
  119. # whereras blocking filters allow configurations that do not match.
  120. passing_filters_list = gen_exclusive_boolean_filters_list([
  121. 'CONFIG_MUTEX_ADAPTIVE',
  122. 'CONFIG_MUTEX_PI',
  123. 'CONFIG_MUTEX_PLAIN',
  124. ])
  125. passing_filters_list += gen_exclusive_boolean_filters_list(test_list,
  126. all_disabled=True)
  127. blocking_filters_list = [
  128. # XXX Clang currently cannot build the kernel with LTO.
  129. {
  130. 'CONFIG_CC_EXE' : [True, 'clang'],
  131. 'CONFIG_CC_OPTIONS' : [True, re.compile('-flto')],
  132. },
  133. {
  134. 'CONFIG_SMP' : [True, 'y'],
  135. 'CONFIG_MAX_CPUS' : [True, '1'],
  136. },
  137. {
  138. 'CONFIG_SMP' : [True, 'n'],
  139. 'CONFIG_MAX_CPUS' : [False, '1'],
  140. },
  141. {
  142. 'CONFIG_64BITS' : [True, 'y'],
  143. 'CONFIG_X86_PAE' : [True, 'y'],
  144. },
  145. ]
  146. def gen_config_line(config_entry):
  147. name, value = config_entry
  148. return '%s=%s\n' % (name, quote_if_needed(value))
  149. def gen_config_content(config_dict):
  150. return map(gen_config_line, config_dict.items())
  151. def test_config_run(command, check, buildlog):
  152. buildlog.writelines(['$ %s\n' % command])
  153. buildlog.flush()
  154. if check:
  155. return subprocess.check_call(command.split(), stdout=buildlog,
  156. stderr=subprocess.STDOUT)
  157. else:
  158. return subprocess.call(command.split(), stdout=buildlog,
  159. stderr=subprocess.STDOUT)
  160. def test_config(args):
  161. 'This function is run in multiprocessing.Pool workers.'
  162. topbuilddir, config_dict = args
  163. srctree = os.path.abspath(os.getcwd())
  164. buildtree = tempfile.mkdtemp(dir=topbuilddir)
  165. os.chdir(buildtree)
  166. buildlog = open('build.log', 'w')
  167. config_file = open('.testconfig', 'w')
  168. config_file.writelines(gen_config_content(config_dict))
  169. config_file.close()
  170. try:
  171. test_config_run('%s/tools/kconfig/merge_config.sh'
  172. ' -m -f %s/Makefile .testconfig' % (srctree, srctree),
  173. True, buildlog)
  174. test_config_run('make -f %s/Makefile V=1 olddefconfig' % srctree,
  175. True, buildlog)
  176. retval = test_config_run('make -f %s/Makefile V=1 x15' % srctree,
  177. False, buildlog)
  178. except KeyboardInterrupt:
  179. buildlog.close()
  180. return
  181. except:
  182. retval = 1
  183. buildlog.close()
  184. os.chdir(srctree)
  185. if retval == 0:
  186. shutil.rmtree(buildtree)
  187. return [retval, buildtree]
  188. def check_filter(config_dict, filter_dict):
  189. 'Return true if a filter completely matches a configuration.'
  190. for name, value in filter_dict.items():
  191. if name not in config_dict:
  192. return False
  193. if isinstance(value[1], str):
  194. if value[0] != (config_dict[name] == value[1]):
  195. return False
  196. else:
  197. if value[0] != bool(value[1].search(config_dict[name])):
  198. return False
  199. return True
  200. def check_filter_relevant(config_dict, filter_dict):
  201. for name in filter_dict.keys():
  202. if name in config_dict:
  203. return True
  204. return False
  205. def check_filters_list_relevant(config_dict, filters_list):
  206. for filter_dict in filters_list:
  207. if check_filter_relevant(config_dict, filter_dict):
  208. return True
  209. return False
  210. def check_passing_filters(args):
  211. '''
  212. If the given filters list is irrelevant, i.e. it applies to none of
  213. the options in the given configuration, the filters are considered
  214. to match.
  215. @return true if a configuration doesn't pass any given filter.
  216. '''
  217. config_dict, filters_list = args
  218. if not check_filters_list_relevant(config_dict, filters_list):
  219. return True
  220. for filter_dict in filters_list:
  221. if check_filter(config_dict, filter_dict):
  222. return True
  223. return False
  224. def check_blocking_filters(args):
  225. 'Return true if a configuration passes all the given filters.'
  226. config_dict, filters_list = args
  227. for filter_dict in filters_list:
  228. if check_filter(config_dict, filter_dict):
  229. return False
  230. return True
  231. def filter_configs_list(configs_list, passing_filters, blocking_filters):
  232. configs_and_filters = [(x, passing_filters) for x in configs_list]
  233. configs_list = [x[0] for x in filter(check_passing_filters,
  234. configs_and_filters)]
  235. configs_and_filters = [(x, blocking_filters) for x in configs_list]
  236. configs_list = [x[0] for x in filter(check_blocking_filters,
  237. configs_and_filters)]
  238. return configs_list
  239. def find_options_dict(options_sets, name):
  240. if name not in options_sets:
  241. return None
  242. return options_sets[name]
  243. class BuildConfigListSetsAction(argparse.Action):
  244. def __init__(self, nargs=0, **kwargs):
  245. if nargs != 0:
  246. raise ValueError("nargs not allowed")
  247. super(BuildConfigListSetsAction, self).__init__(nargs=nargs, **kwargs)
  248. def __call__(self, parser, namespace, values, option_string=None):
  249. for key in sorted(all_options_sets.keys()):
  250. print(key)
  251. for option in sorted(all_options_sets[key]):
  252. print(' ' + option)
  253. sys.exit(0)
  254. def main():
  255. parser = argparse.ArgumentParser(description=__doc__,
  256. formatter_class=argparse.RawDescriptionHelpFormatter)
  257. parser.add_argument('-s', '--set', default='small',
  258. help='select a set of options (default=small)')
  259. parser.add_argument('-l', '--list-sets', action=BuildConfigListSetsAction,
  260. help='print the list of options sets')
  261. args = parser.parse_args()
  262. options_dict = find_options_dict(all_options_sets, args.set)
  263. if not options_dict:
  264. print('error: invalid set')
  265. sys.exit(2)
  266. print('set: {}'.format(args.set))
  267. configs_list = filter_configs_list(gen_configs_list(options_dict),
  268. passing_filters_list,
  269. blocking_filters_list)
  270. nr_configs = len(configs_list)
  271. print('total: {:d}'.format(nr_configs))
  272. # This tool performs out-of-tree builds and requires a clean source tree
  273. print('cleaning source tree...')
  274. subprocess.check_call(['make', 'distclean'])
  275. topbuilddir = os.path.abspath(tempfile.mkdtemp(prefix='build', dir='.'))
  276. print('top build directory: {}'.format(topbuilddir))
  277. pool = multiprocessing.Pool()
  278. worker_args = [(topbuilddir, x) for x in configs_list]
  279. try:
  280. results = pool.map(test_config, worker_args)
  281. except KeyboardInterrupt:
  282. pool.terminate()
  283. pool.join()
  284. shutil.rmtree(topbuilddir)
  285. raise
  286. failures = [x[1] for x in results if x[0] != 0]
  287. for buildtree in failures:
  288. print('failed: {0}/.config ({0}/build.log)'.format(buildtree))
  289. print('passed: {:d}'.format(nr_configs - len(failures)))
  290. print('failed: {:d}'.format(len(failures)))
  291. try:
  292. os.rmdir(topbuilddir)
  293. finally:
  294. sys.exit(len(failures) != 0)
  295. if __name__ == '__main__':
  296. main()