build_configs.py 11 KB

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