common.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. # coding=utf-8
  2. import re
  3. import shutil
  4. import sys
  5. import os
  6. from abc import abstractmethod
  7. from collections import namedtuple
  8. import logging
  9. import json
  10. import typing
  11. from io import open
  12. DEFAULT_TARGET = "esp32"
  13. TARGET_PLACEHOLDER = "@t"
  14. WILDCARD_PLACEHOLDER = "@w"
  15. NAME_PLACEHOLDER = "@n"
  16. FULL_NAME_PLACEHOLDER = "@f"
  17. INDEX_PLACEHOLDER = "@i"
  18. SDKCONFIG_LINE_REGEX = re.compile(r"^([^=]+)=\"?([^\"\n]*)\"?\n*$")
  19. # If these keys are present in sdkconfig.defaults, they will be extracted and passed to CMake
  20. SDKCONFIG_TEST_OPTS = [
  21. "EXCLUDE_COMPONENTS",
  22. "TEST_EXCLUDE_COMPONENTS",
  23. "TEST_COMPONENTS",
  24. "TEST_GROUPS"
  25. ]
  26. # ConfigRule represents one --config argument of find_apps.py.
  27. # file_name is the name of the sdkconfig file fragment, optionally with a single wildcard ('*' character).
  28. # file_name can also be empty to indicate that the default configuration of the app should be used.
  29. # config_name is the name of the corresponding build configuration, or None if the value of wildcard is to be used.
  30. # For example:
  31. # filename='', config_name='default' — represents the default app configuration, and gives it a name 'default'
  32. # filename='sdkconfig.*', config_name=None - represents the set of configurations, names match the wildcard value
  33. ConfigRule = namedtuple("ConfigRule", ["file_name", "config_name"])
  34. def config_rules_from_str(rule_strings): # type: (typing.List[str]) -> typing.List[ConfigRule]
  35. """
  36. Helper function to convert strings like 'file_name=config_name' into ConfigRule objects
  37. :param rule_strings: list of rules as strings
  38. :return: list of ConfigRules
  39. """
  40. rules = [] # type: typing.List[ConfigRule]
  41. for rule_str in rule_strings:
  42. items = rule_str.split("=", 2)
  43. rules.append(ConfigRule(items[0], items[1] if len(items) == 2 else None))
  44. return rules
  45. class BuildItem(object):
  46. """
  47. Instance of this class represents one build of an application.
  48. The parameters which distinguish the build are passed to the constructor.
  49. """
  50. def __init__(
  51. self,
  52. app_path,
  53. work_dir,
  54. build_path,
  55. build_log_path,
  56. target,
  57. sdkconfig_path,
  58. config_name,
  59. build_system,
  60. ):
  61. # These internal variables store the paths with environment variables and placeholders;
  62. # Public properties with similar names use the _expand method to get the actual paths.
  63. self._app_dir = app_path
  64. self._work_dir = work_dir
  65. self._build_dir = build_path
  66. self._build_log_path = build_log_path
  67. self.sdkconfig_path = sdkconfig_path
  68. self.config_name = config_name
  69. self.target = target
  70. self.build_system = build_system
  71. self._app_name = os.path.basename(os.path.normpath(app_path))
  72. # Some miscellaneous build properties which are set later, at the build stage
  73. self.index = None
  74. self.verbose = False
  75. self.dry_run = False
  76. self.keep_going = False
  77. @property
  78. def app_dir(self):
  79. """
  80. :return: directory of the app
  81. """
  82. return self._expand(self._app_dir)
  83. @property
  84. def work_dir(self):
  85. """
  86. :return: directory where the app should be copied to, prior to the build. Can be None, which means that the app
  87. directory should be used.
  88. """
  89. return self._expand(self._work_dir)
  90. @property
  91. def build_dir(self):
  92. """
  93. :return: build directory, either relative to the work directory (if relative path is used) or absolute path.
  94. """
  95. return self._expand(self._build_dir)
  96. @property
  97. def build_log_path(self):
  98. """
  99. :return: path of the build log file
  100. """
  101. return self._expand(self._build_log_path)
  102. def __repr__(self):
  103. return "Build app {} for target {}, sdkconfig {} in {}".format(
  104. self.app_dir,
  105. self.target,
  106. self.sdkconfig_path or "(default)",
  107. self.build_dir,
  108. )
  109. def to_json(self): # type: () -> str
  110. """
  111. :return: JSON string representing this object
  112. """
  113. return self._to_json(self._app_dir, self._work_dir, self._build_dir, self._build_log_path)
  114. def to_json_expanded(self): # type: () -> str
  115. """
  116. :return: JSON string representing this object, with all placeholders in paths expanded
  117. """
  118. return self._to_json(self.app_dir, self.work_dir, self.build_dir, self.build_log_path)
  119. def _to_json(self, app_dir, work_dir, build_dir, build_log_path): # type: (str, str, str, str) -> str
  120. """
  121. Internal function, called by to_json and to_json_expanded
  122. """
  123. return json.dumps({
  124. "build_system": self.build_system,
  125. "app_dir": app_dir,
  126. "work_dir": work_dir,
  127. "build_dir": build_dir,
  128. "build_log_path": build_log_path,
  129. "sdkconfig": self.sdkconfig_path,
  130. "config": self.config_name,
  131. "target": self.target,
  132. "verbose": self.verbose,
  133. })
  134. @staticmethod
  135. def from_json(json_str): # type: (typing.Text) -> BuildItem
  136. """
  137. :return: Get the BuildItem from a JSON string
  138. """
  139. d = json.loads(str(json_str))
  140. result = BuildItem(
  141. app_path=d["app_dir"],
  142. work_dir=d["work_dir"],
  143. build_path=d["build_dir"],
  144. build_log_path=d["build_log_path"],
  145. sdkconfig_path=d["sdkconfig"],
  146. config_name=d["config"],
  147. target=d["target"],
  148. build_system=d["build_system"],
  149. )
  150. result.verbose = d["verbose"]
  151. return result
  152. def _expand(self, path): # type: (str) -> str
  153. """
  154. Internal method, expands any of the placeholders in {app,work,build} paths.
  155. """
  156. if not path:
  157. return path
  158. if self.index is not None:
  159. path = path.replace(INDEX_PLACEHOLDER, str(self.index))
  160. path = path.replace(TARGET_PLACEHOLDER, self.target)
  161. path = path.replace(NAME_PLACEHOLDER, self._app_name)
  162. if (FULL_NAME_PLACEHOLDER in path): # to avoid recursion to the call to app_dir in the next line:
  163. path = path.replace(FULL_NAME_PLACEHOLDER, self.app_dir.replace(os.path.sep, "_"))
  164. wildcard_pos = path.find(WILDCARD_PLACEHOLDER)
  165. if wildcard_pos != -1:
  166. if self.config_name:
  167. # if config name is defined, put it in place of the placeholder
  168. path = path.replace(WILDCARD_PLACEHOLDER, self.config_name)
  169. else:
  170. # otherwise, remove the placeholder and one character on the left
  171. # (which is usually an underscore, dash, or other delimiter)
  172. left_of_wildcard = max(0, wildcard_pos - 1)
  173. right_of_wildcard = wildcard_pos + len(WILDCARD_PLACEHOLDER)
  174. path = path[0:left_of_wildcard] + path[right_of_wildcard:]
  175. path = os.path.expandvars(path)
  176. return path
  177. class BuildSystem(object):
  178. """
  179. Class representing a build system.
  180. Derived classes implement the methods below.
  181. Objects of these classes aren't instantiated, instead the class (type object) is used.
  182. """
  183. NAME = "undefined"
  184. SUPPORTED_TARGETS_REGEX = re.compile(r'Supported [Tt]argets((?:[\s|]+(?:ESP[0-9A-Z\-]+))+)')
  185. @classmethod
  186. def build_prepare(cls, build_item):
  187. app_path = build_item.app_dir
  188. work_path = build_item.work_dir or app_path
  189. if not build_item.build_dir:
  190. build_path = os.path.join(work_path, "build")
  191. elif os.path.isabs(build_item.build_dir):
  192. build_path = build_item.build_dir
  193. else:
  194. build_path = os.path.join(work_path, build_item.build_dir)
  195. if work_path != app_path:
  196. if os.path.exists(work_path):
  197. logging.debug("Work directory {} exists, removing".format(work_path))
  198. if not build_item.dry_run:
  199. shutil.rmtree(work_path)
  200. logging.debug("Copying app from {} to {}".format(app_path, work_path))
  201. if not build_item.dry_run:
  202. shutil.copytree(app_path, work_path)
  203. if os.path.exists(build_path):
  204. logging.debug("Build directory {} exists, removing".format(build_path))
  205. if not build_item.dry_run:
  206. shutil.rmtree(build_path)
  207. if not build_item.dry_run:
  208. os.makedirs(build_path)
  209. # Prepare the sdkconfig file, from the contents of sdkconfig.defaults (if exists) and the contents of
  210. # build_info.sdkconfig_path, i.e. the config-specific sdkconfig file.
  211. #
  212. # Note: the build system supports taking multiple sdkconfig.defaults files via SDKCONFIG_DEFAULTS
  213. # CMake variable. However here we do this manually to perform environment variable expansion in the
  214. # sdkconfig files.
  215. sdkconfig_defaults_list = ["sdkconfig.defaults", "sdkconfig.defaults." + build_item.target]
  216. if build_item.sdkconfig_path:
  217. sdkconfig_defaults_list.append(build_item.sdkconfig_path)
  218. sdkconfig_file = os.path.join(work_path, "sdkconfig")
  219. if os.path.exists(sdkconfig_file):
  220. logging.debug("Removing sdkconfig file: {}".format(sdkconfig_file))
  221. if not build_item.dry_run:
  222. os.unlink(sdkconfig_file)
  223. logging.debug("Creating sdkconfig file: {}".format(sdkconfig_file))
  224. extra_cmakecache_items = {}
  225. if not build_item.dry_run:
  226. with open(sdkconfig_file, "w") as f_out:
  227. for sdkconfig_name in sdkconfig_defaults_list:
  228. sdkconfig_path = os.path.join(work_path, sdkconfig_name)
  229. if not sdkconfig_path or not os.path.exists(sdkconfig_path):
  230. continue
  231. logging.debug("Appending {} to sdkconfig".format(sdkconfig_name))
  232. with open(sdkconfig_path, "r") as f_in:
  233. for line in f_in:
  234. if not line.endswith("\n"):
  235. line += "\n"
  236. if cls.NAME == 'cmake':
  237. m = SDKCONFIG_LINE_REGEX.match(line)
  238. if m and m.group(1) in SDKCONFIG_TEST_OPTS:
  239. extra_cmakecache_items[m.group(1)] = m.group(2)
  240. continue
  241. f_out.write(os.path.expandvars(line))
  242. else:
  243. for sdkconfig_name in sdkconfig_defaults_list:
  244. sdkconfig_path = os.path.join(app_path, sdkconfig_name)
  245. if not sdkconfig_path:
  246. continue
  247. logging.debug("Considering sdkconfig {}".format(sdkconfig_path))
  248. if not os.path.exists(sdkconfig_path):
  249. continue
  250. logging.debug("Appending {} to sdkconfig".format(sdkconfig_name))
  251. # The preparation of build is finished. Implement the build part in sub classes.
  252. if cls.NAME == 'cmake':
  253. return build_path, work_path, extra_cmakecache_items
  254. else:
  255. return build_path, work_path
  256. @staticmethod
  257. @abstractmethod
  258. def build(build_item):
  259. pass
  260. @staticmethod
  261. @abstractmethod
  262. def is_app(path):
  263. pass
  264. @staticmethod
  265. def _read_readme(app_path):
  266. # Markdown supported targets should be:
  267. # e.g. | Supported Targets | ESP32 |
  268. # | ----------------- | ----- |
  269. # reStructuredText supported targets should be:
  270. # e.g. ================= =====
  271. # Supported Targets ESP32
  272. # ================= =====
  273. def get_md_or_rst(app_path):
  274. readme_path = os.path.join(app_path, 'README.md')
  275. if not os.path.exists(readme_path):
  276. readme_path = os.path.join(app_path, 'README.rst')
  277. if not os.path.exists(readme_path):
  278. return None
  279. return readme_path
  280. readme_path = get_md_or_rst(app_path)
  281. # Handle sub apps situation, e.g. master-slave
  282. if not readme_path:
  283. readme_path = get_md_or_rst(os.path.dirname(app_path))
  284. if not readme_path:
  285. return None
  286. with open(readme_path, "r", encoding='utf8') as readme_file:
  287. return readme_file.read()
  288. @staticmethod
  289. def supported_targets(app_path):
  290. formal_to_usual = {
  291. 'ESP32': 'esp32',
  292. 'ESP32-S2': 'esp32s2',
  293. }
  294. readme_file_content = BuildSystem._read_readme(app_path)
  295. if not readme_file_content:
  296. return None
  297. match = re.findall(BuildSystem.SUPPORTED_TARGETS_REGEX, readme_file_content)
  298. if not match:
  299. return None
  300. if len(match) > 1:
  301. raise NotImplementedError("Can't determine the value of SUPPORTED_TARGETS in {}".format(app_path))
  302. support_str = match[0].strip()
  303. targets = []
  304. for part in support_str.split('|'):
  305. for inner in part.split(' '):
  306. inner = inner.strip()
  307. if not inner:
  308. continue
  309. elif inner in formal_to_usual:
  310. targets.append(formal_to_usual[inner])
  311. else:
  312. raise NotImplementedError("Can't recognize value of target {} in {}, now we only support '{}'"
  313. .format(inner, app_path, ', '.join(formal_to_usual.keys())))
  314. return targets
  315. class BuildError(RuntimeError):
  316. pass
  317. def setup_logging(args):
  318. """
  319. Configure logging module according to the number of '--verbose'/'-v' arguments and the --log-file argument.
  320. :param args: namespace obtained from argparse
  321. """
  322. if not args.verbose:
  323. log_level = logging.WARNING
  324. elif args.verbose == 1:
  325. log_level = logging.INFO
  326. else:
  327. log_level = logging.DEBUG
  328. logging.basicConfig(
  329. format="%(levelname)s: %(message)s",
  330. stream=args.log_file or sys.stderr,
  331. level=log_level,
  332. )