idf_ext.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import copy
  2. import glob
  3. import os
  4. import os.path
  5. import re
  6. import shutil
  7. def action_extensions(base_actions, project_path=os.getcwd()):
  8. """ Describes extensions for unit tests. This function expects that actions "all" and "reconfigure" """
  9. PROJECT_NAME = 'unit-test-app'
  10. # List of unit-test-app configurations.
  11. # Each file in configs/ directory defines a configuration. The format is the
  12. # same as sdkconfig file. Configuration is applied on top of sdkconfig.defaults
  13. # file from the project directory
  14. CONFIG_NAMES = os.listdir(os.path.join(project_path, 'configs'))
  15. # Build (intermediate) and output (artifact) directories
  16. BUILDS_DIR = os.path.join(project_path, 'builds')
  17. BINARIES_DIR = os.path.join(project_path, 'output')
  18. def parse_file_to_dict(path, regex):
  19. """
  20. Parse the config file at 'path'
  21. Returns a dict of name:value.
  22. """
  23. compiled_regex = re.compile(regex)
  24. result = {}
  25. with open(path) as f:
  26. for line in f:
  27. m = compiled_regex.match(line)
  28. if m:
  29. result[m.group(1)] = m.group(2)
  30. return result
  31. def parse_config(path):
  32. """
  33. Expected format with default regex is "key=value"
  34. """
  35. return parse_file_to_dict(path, r'^([^=]+)=(.+)$')
  36. def ut_apply_config(ut_apply_config_name, ctx, args):
  37. config_name = re.match(r'ut-apply-config-(.*)', ut_apply_config_name).group(1)
  38. # Make sure that define_cache_entry is list
  39. args.define_cache_entry = list(args.define_cache_entry)
  40. new_cache_values = {}
  41. sdkconfig_set = list(filter(lambda s: 'SDKCONFIG=' in s, args.define_cache_entry))
  42. sdkconfig_path = os.path.join(args.project_dir, 'sdkconfig')
  43. if sdkconfig_set:
  44. sdkconfig_path = sdkconfig_set[-1].split('=')[1]
  45. sdkconfig_path = os.path.abspath(sdkconfig_path)
  46. try:
  47. os.remove(sdkconfig_path)
  48. except OSError:
  49. pass
  50. if config_name in CONFIG_NAMES:
  51. # Parse the sdkconfig for components to be included/excluded and tests to be run
  52. config_path = os.path.join(project_path, 'configs', config_name)
  53. config = parse_config(config_path)
  54. target = config.get('CONFIG_IDF_TARGET', 'esp32').strip("'").strip('"')
  55. print('Reconfigure: config %s, target %s' % (config_name, target))
  56. # Clean up and set idf-target
  57. base_actions['actions']['fullclean']['callback']('fullclean', ctx, args)
  58. new_cache_values['EXCLUDE_COMPONENTS'] = config.get('EXCLUDE_COMPONENTS', "''")
  59. new_cache_values['TEST_EXCLUDE_COMPONENTS'] = config.get('TEST_EXCLUDE_COMPONENTS', "''")
  60. new_cache_values['TEST_COMPONENTS'] = config.get('TEST_COMPONENTS', "''")
  61. new_cache_values['TESTS_ALL'] = int(new_cache_values['TEST_COMPONENTS'] == "''")
  62. new_cache_values['IDF_TARGET'] = target
  63. new_cache_values['SDKCONFIG_DEFAULTS'] = ';'.join([os.path.join(project_path, 'sdkconfig.defaults'), config_path])
  64. args.define_cache_entry.extend(['%s=%s' % (k, v) for k, v in new_cache_values.items()])
  65. reconfigure = base_actions['actions']['reconfigure']['callback']
  66. reconfigure(None, ctx, args)
  67. # This target builds the configuration. It does not currently track dependencies,
  68. # but is good enough for CI builds if used together with clean-all-configs.
  69. # For local builds, use 'apply-config-NAME' target and then use normal 'all'
  70. # and 'flash' targets.
  71. def ut_build(ut_build_name, ctx, args):
  72. # Create a copy of the passed arguments to prevent arg modifications to accrue if
  73. # all configs are being built
  74. build_args = copy.copy(args)
  75. config_name = re.match(r'ut-build-(.*)', ut_build_name).group(1)
  76. if config_name in CONFIG_NAMES:
  77. build_args.build_dir = os.path.join(BUILDS_DIR, config_name)
  78. src = os.path.join(BUILDS_DIR, config_name)
  79. dest = os.path.join(BINARIES_DIR, config_name)
  80. try:
  81. os.makedirs(dest)
  82. except OSError:
  83. pass
  84. # Build, tweaking paths to sdkconfig and sdkconfig.defaults
  85. ut_apply_config('ut-apply-config-' + config_name, ctx, build_args)
  86. build_target = base_actions['actions']['all']['callback']
  87. build_target('all', ctx, build_args)
  88. # Copy artifacts to the output directory
  89. shutil.copyfile(
  90. os.path.join(build_args.project_dir, 'sdkconfig'),
  91. os.path.join(dest, 'sdkconfig'),
  92. )
  93. binaries = [PROJECT_NAME + x for x in ['.elf', '.bin', '.map']]
  94. for binary in binaries:
  95. shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
  96. try:
  97. os.mkdir(os.path.join(dest, 'bootloader'))
  98. except OSError:
  99. pass
  100. shutil.copyfile(
  101. os.path.join(src, 'bootloader', 'bootloader.bin'),
  102. os.path.join(dest, 'bootloader', 'bootloader.bin'),
  103. )
  104. for partition_table in glob.glob(os.path.join(src, 'partition_table', 'partition-table*.bin')):
  105. try:
  106. os.mkdir(os.path.join(dest, 'partition_table'))
  107. except OSError:
  108. pass
  109. shutil.copyfile(
  110. partition_table,
  111. os.path.join(dest, 'partition_table', os.path.basename(partition_table)),
  112. )
  113. shutil.copyfile(
  114. os.path.join(src, 'flasher_args.json'),
  115. os.path.join(dest, 'flasher_args.json'),
  116. )
  117. binaries = glob.glob(os.path.join(src, '*.bin'))
  118. binaries = [os.path.basename(s) for s in binaries]
  119. for binary in binaries:
  120. shutil.copyfile(os.path.join(src, binary), os.path.join(dest, binary))
  121. def ut_clean(ut_clean_name, ctx, args):
  122. config_name = re.match(r'ut-clean-(.*)', ut_clean_name).group(1)
  123. if config_name in CONFIG_NAMES:
  124. shutil.rmtree(os.path.join(BUILDS_DIR, config_name), ignore_errors=True)
  125. shutil.rmtree(os.path.join(BINARIES_DIR, config_name), ignore_errors=True)
  126. def test_component_callback(ctx, global_args, tasks):
  127. """ Convert the values passed to the -T and -E parameter to corresponding cache entry definitions TESTS_ALL and TEST_COMPONENTS """
  128. test_components = global_args.test_components
  129. test_exclude_components = global_args.test_exclude_components
  130. cache_entries = {}
  131. if test_components:
  132. if 'all' in test_components:
  133. cache_entries['TESTS_ALL'] = 1
  134. cache_entries['TEST_COMPONENTS'] = "''"
  135. else:
  136. cache_entries['TESTS_ALL'] = 0
  137. cache_entries['TEST_COMPONENTS'] = ' '.join(test_components)
  138. if test_exclude_components:
  139. cache_entries['TEST_EXCLUDE_COMPONENTS'] = ' '.join(test_exclude_components)
  140. if cache_entries:
  141. global_args.define_cache_entry = list(global_args.define_cache_entry)
  142. global_args.define_cache_entry.extend(['%s=%s' % (k, v) for k, v in cache_entries.items()])
  143. # Add global options
  144. extensions = {
  145. 'global_options': [{
  146. 'names': ['-T', '--test-components'],
  147. 'help': 'Specify the components to test.',
  148. 'scope': 'shared',
  149. 'multiple': True,
  150. }, {
  151. 'names': ['-E', '--test-exclude-components'],
  152. 'help': 'Specify the components to exclude from testing.',
  153. 'scope': 'shared',
  154. 'multiple': True,
  155. }],
  156. 'global_action_callbacks': [test_component_callback],
  157. 'actions': {},
  158. }
  159. # This generates per-config targets (clean, build, apply-config).
  160. build_all_config_deps = []
  161. clean_all_config_deps = []
  162. for config in CONFIG_NAMES:
  163. config_build_action_name = 'ut-build-' + config
  164. config_clean_action_name = 'ut-clean-' + config
  165. config_apply_config_action_name = 'ut-apply-config-' + config
  166. extensions['actions'][config_build_action_name] = {
  167. 'callback':
  168. ut_build,
  169. 'help':
  170. 'Build unit-test-app with configuration provided in configs/NAME. ' +
  171. 'Build directory will be builds/%s/, ' % config_build_action_name +
  172. 'output binaries will be under output/%s/' % config_build_action_name,
  173. }
  174. extensions['actions'][config_clean_action_name] = {
  175. 'callback': ut_clean,
  176. 'help': 'Remove build and output directories for configuration %s.' % config_clean_action_name,
  177. }
  178. extensions['actions'][config_apply_config_action_name] = {
  179. 'callback':
  180. ut_apply_config,
  181. 'help':
  182. 'Generates configuration based on configs/%s in sdkconfig file.' % config_apply_config_action_name +
  183. 'After this, normal all/flash targets can be used. Useful for development/debugging.',
  184. }
  185. build_all_config_deps.append(config_build_action_name)
  186. clean_all_config_deps.append(config_clean_action_name)
  187. extensions['actions']['ut-build-all-configs'] = {
  188. 'callback': ut_build,
  189. 'help': 'Build all configurations defined in configs/ directory.',
  190. 'dependencies': build_all_config_deps,
  191. }
  192. extensions['actions']['ut-clean-all-configs'] = {
  193. 'callback': ut_clean,
  194. 'help': 'Remove build and output directories for all configurations defined in configs/ directory.',
  195. 'dependencies': clean_all_config_deps,
  196. }
  197. return extensions