find_apps.py 11 KB

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