find_apps.py 11 KB

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