find_apps.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. #
  4. # ESP-IDF helper script to enumerate the builds of multiple configurations of multiple apps.
  5. # Produces the list of builds. The list can be consumed by build_apps.py, which performs the actual builds.
  6. import argparse
  7. import glob
  8. import json
  9. import logging
  10. import os
  11. import re
  12. import sys
  13. import typing
  14. from find_build_apps import (BUILD_SYSTEM_CMAKE, BUILD_SYSTEMS, DEFAULT_TARGET, BuildItem, BuildSystem, ConfigRule,
  15. config_rules_from_str, setup_logging)
  16. # Helper functions
  17. def dict_from_sdkconfig(path):
  18. """
  19. Parse the sdkconfig file at 'path', return name:value pairs as a dict
  20. """
  21. regex = re.compile(r'^([^#=]+)=(.+)$')
  22. result = {}
  23. with open(path) as f:
  24. for line in f:
  25. m = regex.match(line)
  26. if m:
  27. val = m.group(2)
  28. if val.startswith('"') and val.endswith('"'):
  29. val = val[1:-1]
  30. result[m.group(1)] = val
  31. return result
  32. # Main logic: enumerating apps and builds
  33. def find_builds_for_app(app_path, work_dir, build_dir, build_log, target_arg,
  34. build_system, config_rules, preserve_artifacts=True):
  35. # type: (str, str, str, str, str, str, typing.List[ConfigRule], bool) -> typing.List[BuildItem]
  36. """
  37. Find configurations (sdkconfig file fragments) for the given app, return them as BuildItem objects
  38. :param app_path: app directory (can be / usually will be a relative path)
  39. :param work_dir: directory where the app should be copied before building.
  40. May contain env. variables and placeholders.
  41. :param build_dir: directory where the build will be done, relative to the work_dir. May contain placeholders.
  42. :param build_log: path of the build log. May contain placeholders. May be None, in which case the log should go
  43. into stdout/stderr.
  44. :param target_arg: the value of IDF_TARGET passed to the script. Used to filter out configurations with
  45. a different CONFIG_IDF_TARGET value.
  46. :param build_system: name of the build system, index into BUILD_SYSTEMS dictionary
  47. :param config_rules: mapping of sdkconfig file name patterns to configuration names
  48. :param preserve_artifacts: determine if the built binary will be uploaded as artifacts.
  49. :return: list of BuildItems representing build configuration of the app
  50. """
  51. build_items = [] # type: typing.List[BuildItem]
  52. default_config_name = ''
  53. for rule in config_rules:
  54. if not rule.file_name:
  55. default_config_name = rule.config_name
  56. continue
  57. sdkconfig_paths = glob.glob(os.path.join(app_path, rule.file_name))
  58. sdkconfig_paths = sorted(sdkconfig_paths)
  59. for sdkconfig_path in sdkconfig_paths:
  60. # Check if the sdkconfig file specifies IDF_TARGET, and if it is matches the --target argument.
  61. sdkconfig_dict = dict_from_sdkconfig(sdkconfig_path)
  62. target_from_config = sdkconfig_dict.get('CONFIG_IDF_TARGET')
  63. if target_from_config is not None and target_from_config != target_arg:
  64. logging.debug('Skipping sdkconfig {} which requires target {}'.format(
  65. sdkconfig_path, target_from_config))
  66. continue
  67. # Figure out the config name
  68. config_name = rule.config_name or ''
  69. if '*' in rule.file_name:
  70. # convert glob pattern into a regex
  71. regex_str = r'.*' + rule.file_name.replace('.', r'\.').replace('*', r'(.*)')
  72. groups = re.match(regex_str, sdkconfig_path)
  73. assert groups
  74. config_name = groups.group(1)
  75. sdkconfig_path = os.path.relpath(sdkconfig_path, app_path)
  76. logging.debug('Adding build: app {}, sdkconfig {}, config name "{}"'.format(
  77. app_path, sdkconfig_path, config_name))
  78. build_items.append(
  79. BuildItem(
  80. app_path,
  81. work_dir,
  82. build_dir,
  83. build_log,
  84. target_arg,
  85. sdkconfig_path,
  86. config_name,
  87. build_system,
  88. preserve_artifacts,
  89. ))
  90. if not build_items:
  91. logging.debug('Adding build: app {}, default sdkconfig, config name "{}"'.format(app_path, default_config_name))
  92. return [
  93. BuildItem(
  94. app_path,
  95. work_dir,
  96. build_dir,
  97. build_log,
  98. target_arg,
  99. None,
  100. default_config_name,
  101. build_system,
  102. preserve_artifacts,
  103. )
  104. ]
  105. return build_items
  106. def find_apps(build_system_class, path, recursive, exclude_list, target):
  107. # type: (typing.Type[BuildSystem], str, bool, typing.List[str], str) -> typing.List[str]
  108. """
  109. Find app directories in path (possibly recursively), which contain apps for the given build system, compatible
  110. with the given target.
  111. :param build_system_class: class derived from BuildSystem, representing the build system in use
  112. :param path: path where to look for apps
  113. :param recursive: whether to recursively descend into nested directories if no app is found
  114. :param exclude_list: list of paths to be excluded from the recursive search
  115. :param target: desired value of IDF_TARGET; apps incompatible with the given target are skipped.
  116. :return: list of paths of the apps found
  117. """
  118. build_system_name = build_system_class.NAME
  119. logging.debug('Looking for {} apps in {}{}'.format(build_system_name, path, ' recursively' if recursive else ''))
  120. if not recursive:
  121. if exclude_list:
  122. logging.warning('--exclude option is ignored when used without --recursive')
  123. if not build_system_class.is_app(path):
  124. logging.warning('Path {} specified without --recursive flag, but no {} app found there'.format(
  125. path, build_system_name))
  126. return []
  127. return [path]
  128. # The remaining part is for recursive == True
  129. apps_found = [] # type: typing.List[str]
  130. for root, dirs, _ in os.walk(path, topdown=True):
  131. logging.debug('Entering {}'.format(root))
  132. if root in exclude_list:
  133. logging.debug('Skipping {} (excluded)'.format(root))
  134. del dirs[:]
  135. continue
  136. if build_system_class.is_app(root):
  137. logging.debug('Found {} app in {}'.format(build_system_name, root))
  138. # Don't recurse into app subdirectories
  139. del dirs[:]
  140. supported_targets = build_system_class.supported_targets(root)
  141. if supported_targets and (target in supported_targets):
  142. apps_found.append(root)
  143. else:
  144. if supported_targets:
  145. logging.debug('Skipping, app only supports targets: ' + ', '.join(supported_targets))
  146. else:
  147. logging.debug('Skipping, app has no supported targets')
  148. continue
  149. return apps_found
  150. def main():
  151. parser = argparse.ArgumentParser(description='Tool to generate build steps for IDF apps')
  152. parser.add_argument(
  153. '-v',
  154. '--verbose',
  155. action='count',
  156. help='Increase the logging level of the script. Can be specified multiple times.',
  157. )
  158. parser.add_argument(
  159. '--log-file',
  160. type=argparse.FileType('w'),
  161. help='Write the script log to the specified file, instead of stderr',
  162. )
  163. parser.add_argument(
  164. '--recursive',
  165. action='store_true',
  166. help='Look for apps in the specified directories recursively.',
  167. )
  168. parser.add_argument(
  169. '--build-system',
  170. choices=BUILD_SYSTEMS.keys()
  171. )
  172. parser.add_argument(
  173. '--work-dir',
  174. help='If set, the app is first copied into the specified directory, and then built.' +
  175. 'If not set, the work directory is the directory of the app.',
  176. )
  177. parser.add_argument(
  178. '--config',
  179. action='append',
  180. help='Adds configurations (sdkconfig file names) to build. This can either be ' +
  181. 'FILENAME[=NAME] or FILEPATTERN. FILENAME is the name of the sdkconfig file, ' +
  182. 'relative to the project directory, to be used. Optional NAME can be specified, ' +
  183. 'which can be used as a name of this configuration. FILEPATTERN is the name of ' +
  184. 'the sdkconfig file, relative to the project directory, with at most one wildcard. ' +
  185. 'The part captured by the wildcard is used as the name of the configuration.',
  186. )
  187. parser.add_argument(
  188. '--build-dir',
  189. help='If set, specifies the build directory name. Can expand placeholders. Can be either a ' +
  190. 'name relative to the work directory, or an absolute path.',
  191. )
  192. parser.add_argument(
  193. '--build-log',
  194. help='If specified, the build log will be written to this file. Can expand placeholders.',
  195. )
  196. parser.add_argument('--target', help='Build apps for given target.')
  197. parser.add_argument(
  198. '--format',
  199. default='json',
  200. choices=['json'],
  201. help='Format to write the list of builds as',
  202. )
  203. parser.add_argument(
  204. '--exclude',
  205. action='append',
  206. help='Ignore specified directory (if --recursive is given). Can be used multiple times.',
  207. )
  208. parser.add_argument(
  209. '-o',
  210. '--output',
  211. type=argparse.FileType('w'),
  212. help='Output the list of builds to the specified file',
  213. )
  214. parser.add_argument(
  215. '--app-list',
  216. default=None,
  217. help='Scan tests results. Restrict the build/artifacts preservation behavior to apps need to be built. '
  218. 'If the file does not exist, will build all apps and upload all artifacts.'
  219. )
  220. parser.add_argument(
  221. '-p', '--paths',
  222. nargs='+',
  223. help='One or more app paths.'
  224. )
  225. args = parser.parse_args()
  226. setup_logging(args)
  227. # Arguments Validation
  228. if args.app_list:
  229. conflict_args = [args.recursive, args.build_system, args.target, args.exclude, args.paths]
  230. if any(conflict_args):
  231. raise ValueError('Conflict settings. "recursive", "build_system", "target", "exclude", "paths" should not '
  232. 'be specified with "app_list"')
  233. if not os.path.exists(args.app_list):
  234. raise OSError('File not found {}'.format(args.app_list))
  235. else:
  236. # If the build target is not set explicitly, get it from the environment or use the default one (esp32)
  237. if not args.target:
  238. env_target = os.environ.get('IDF_TARGET')
  239. if env_target:
  240. logging.info('--target argument not set, using IDF_TARGET={} from the environment'.format(env_target))
  241. args.target = env_target
  242. else:
  243. logging.info('--target argument not set, using IDF_TARGET={} as the default'.format(DEFAULT_TARGET))
  244. args.target = DEFAULT_TARGET
  245. if not args.build_system:
  246. logging.info('--build-system argument not set, using {} as the default'.format(BUILD_SYSTEM_CMAKE))
  247. args.build_system = BUILD_SYSTEM_CMAKE
  248. required_args = [args.build_system, args.target, args.paths]
  249. if not all(required_args):
  250. raise ValueError('If app_list not set, arguments "build_system", "target", "paths" are required.')
  251. # Prepare the list of app paths, try to read from the scan_tests result.
  252. # If the file exists, then follow the file's app_dir and build/artifacts behavior, won't do find_apps() again.
  253. # If the file not exists, will do find_apps() first, then build all apps and upload all artifacts.
  254. if args.app_list:
  255. apps = [json.loads(line) for line in open(args.app_list)]
  256. else:
  257. app_dirs = []
  258. build_system_class = BUILD_SYSTEMS[args.build_system]
  259. for path in args.paths:
  260. app_dirs += find_apps(build_system_class, path, args.recursive, args.exclude or [], args.target)
  261. apps = [{'app_dir': app_dir, 'build': True, 'preserve': True} for app_dir in app_dirs]
  262. if not apps:
  263. logging.warning('No apps found')
  264. SystemExit(0)
  265. logging.info('Found {} apps'.format(len(apps)))
  266. apps.sort(key=lambda x: x['app_dir'])
  267. # Find compatible configurations of each app, collect them as BuildItems
  268. build_items = [] # type: typing.List[BuildItem]
  269. config_rules = config_rules_from_str(args.config or [])
  270. for app in apps:
  271. build_items += find_builds_for_app(
  272. app['app_dir'],
  273. args.work_dir,
  274. args.build_dir,
  275. args.build_log,
  276. args.target or app['target'],
  277. args.build_system or app['build_system'],
  278. config_rules,
  279. app['preserve'],
  280. )
  281. logging.info('Found {} builds'.format(len(build_items)))
  282. # Write out the BuildItems. Only JSON supported now (will add YAML later).
  283. if args.format != 'json':
  284. raise NotImplementedError()
  285. out = args.output or sys.stdout
  286. out.writelines([item.to_json() + '\n' for item in build_items])
  287. if __name__ == '__main__':
  288. main()