ci_build_apps.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. # SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. """
  4. This file is used in CI generate binary files for different kinds of apps
  5. """
  6. import argparse
  7. import os
  8. import sys
  9. import typing as t
  10. import unittest
  11. from collections import defaultdict
  12. from pathlib import Path
  13. import yaml
  14. from idf_build_apps import LOGGER, App, build_apps, find_apps, setup_logging
  15. from idf_build_apps.constants import SUPPORTED_TARGETS
  16. from idf_ci_utils import IDF_PATH, get_ttfw_app_paths
  17. CI_ENV_VARS = {
  18. 'EXTRA_CFLAGS': '-Werror -Werror=deprecated-declarations -Werror=unused-variable '
  19. '-Werror=unused-but-set-variable -Werror=unused-function -Wstrict-prototypes',
  20. 'EXTRA_CXXFLAGS': '-Werror -Werror=deprecated-declarations -Werror=unused-variable '
  21. '-Werror=unused-but-set-variable -Werror=unused-function',
  22. 'LDGEN_CHECK_MAPPING': '1',
  23. }
  24. def get_pytest_apps(
  25. paths: t.List[str],
  26. target: str,
  27. config_rules_str: t.List[str],
  28. marker_expr: str,
  29. filter_expr: str,
  30. preserve_all: bool = False,
  31. extra_default_build_targets: t.Optional[t.List[str]] = None,
  32. modified_components: t.Optional[t.List[str]] = None,
  33. modified_files: t.Optional[t.List[str]] = None,
  34. ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = None,
  35. ) -> t.List[App]:
  36. from idf_pytest.script import get_pytest_cases
  37. pytest_cases = get_pytest_cases(paths, target, marker_expr, filter_expr)
  38. _paths: t.Set[str] = set()
  39. test_related_app_configs = defaultdict(set)
  40. for case in pytest_cases:
  41. for app in case.apps:
  42. _paths.add(app.path)
  43. test_related_app_configs[app.path].add(app.config)
  44. if not extra_default_build_targets:
  45. extra_default_build_targets = []
  46. app_dirs = list(_paths)
  47. if not app_dirs:
  48. raise RuntimeError('No apps found')
  49. LOGGER.info(f'Found {len(app_dirs)} apps')
  50. app_dirs.sort()
  51. apps = find_apps(
  52. app_dirs,
  53. target=target,
  54. build_dir='build_@t_@w',
  55. config_rules_str=config_rules_str,
  56. build_log_path='build_log.txt',
  57. size_json_path='size.json',
  58. check_warnings=True,
  59. manifest_rootpath=IDF_PATH,
  60. manifest_files=[str(p) for p in Path(IDF_PATH).glob('**/.build-test-rules.yml')],
  61. default_build_targets=SUPPORTED_TARGETS + extra_default_build_targets,
  62. modified_components=modified_components,
  63. modified_files=modified_files,
  64. ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
  65. )
  66. for app in apps:
  67. is_test_related = app.config_name in test_related_app_configs[app.app_dir]
  68. if not preserve_all and not is_test_related:
  69. app.preserve = False
  70. if app.target == 'linux':
  71. app._size_json_path = None # no esp_idf_size for linux target
  72. return apps # type: ignore
  73. def get_cmake_apps(
  74. paths: t.List[str],
  75. target: str,
  76. config_rules_str: t.List[str],
  77. preserve_all: bool = False,
  78. extra_default_build_targets: t.Optional[t.List[str]] = None,
  79. modified_components: t.Optional[t.List[str]] = None,
  80. modified_files: t.Optional[t.List[str]] = None,
  81. ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = None,
  82. ) -> t.List[App]:
  83. from idf_pytest.constants import PytestApp
  84. from idf_pytest.script import get_pytest_cases
  85. ttfw_app_dirs = get_ttfw_app_paths(paths, target)
  86. apps = find_apps(
  87. paths,
  88. recursive=True,
  89. target=target,
  90. build_dir='build_@t_@w',
  91. config_rules_str=config_rules_str,
  92. build_log_path='build_log.txt',
  93. size_json_path='size.json',
  94. check_warnings=True,
  95. preserve=False,
  96. manifest_rootpath=IDF_PATH,
  97. manifest_files=[str(p) for p in Path(IDF_PATH).glob('**/.build-test-rules.yml')],
  98. default_build_targets=SUPPORTED_TARGETS + extra_default_build_targets,
  99. modified_components=modified_components,
  100. modified_files=modified_files,
  101. ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
  102. )
  103. apps_for_build = []
  104. pytest_cases_apps = [app for case in get_pytest_cases(paths, target) for app in case.apps]
  105. for app in apps:
  106. if preserve_all or app.app_dir in ttfw_app_dirs: # relpath
  107. app.preserve = True
  108. if PytestApp(os.path.realpath(app.app_dir), app.target, app.config_name) in pytest_cases_apps:
  109. LOGGER.debug('Skipping build app with pytest scripts: %s', app)
  110. continue
  111. if app.target == 'linux':
  112. app._size_json_path = None # no esp_idf_size for linux target
  113. apps_for_build.append(app)
  114. return apps_for_build
  115. APPS_BUILD_PER_JOB = 30
  116. def main(args: argparse.Namespace) -> None:
  117. extra_default_build_targets: t.List[str] = []
  118. if args.default_build_test_rules:
  119. with open(args.default_build_test_rules) as fr:
  120. configs = yaml.safe_load(fr)
  121. if configs:
  122. extra_default_build_targets = configs.get('extra_default_build_targets') or []
  123. if args.pytest_apps:
  124. LOGGER.info('Only build apps with pytest scripts')
  125. apps = get_pytest_apps(
  126. args.paths,
  127. args.target,
  128. args.config,
  129. args.marker_expr,
  130. args.filter_expr,
  131. args.preserve_all,
  132. extra_default_build_targets,
  133. args.modified_components,
  134. args.modified_files,
  135. args.ignore_app_dependencies_filepatterns,
  136. )
  137. else:
  138. LOGGER.info('build apps. will skip pytest apps with pytest scripts')
  139. apps = get_cmake_apps(
  140. args.paths,
  141. args.target,
  142. args.config,
  143. args.preserve_all,
  144. extra_default_build_targets,
  145. args.modified_components,
  146. args.modified_files,
  147. args.ignore_app_dependencies_filepatterns,
  148. )
  149. LOGGER.info('Found %d apps after filtering', len(apps))
  150. LOGGER.info(
  151. 'Suggest setting the parallel count to %d for this build job',
  152. len(apps) // APPS_BUILD_PER_JOB + 1,
  153. )
  154. if args.extra_preserve_dirs:
  155. for app in apps:
  156. if app.preserve:
  157. continue
  158. for extra_preserve_dir in args.extra_preserve_dirs:
  159. abs_extra_preserve_dir = Path(extra_preserve_dir).resolve()
  160. abs_app_dir = Path(app.app_dir).resolve()
  161. if abs_extra_preserve_dir == abs_app_dir or abs_extra_preserve_dir in abs_app_dir.parents:
  162. app.preserve = True
  163. res = build_apps(
  164. apps,
  165. parallel_count=args.parallel_count,
  166. parallel_index=args.parallel_index,
  167. dry_run=False,
  168. build_verbose=args.build_verbose,
  169. keep_going=True,
  170. collect_size_info='size_info.txt',
  171. collect_app_info=args.collect_app_info,
  172. ignore_warning_strs=args.ignore_warning_str,
  173. ignore_warning_file=args.ignore_warning_file,
  174. copy_sdkconfig=args.copy_sdkconfig,
  175. modified_components=args.modified_components,
  176. modified_files=args.modified_files,
  177. ignore_app_dependencies_filepatterns=args.ignore_app_dependencies_filepatterns,
  178. )
  179. if isinstance(res, tuple):
  180. sys.exit(res[0])
  181. else:
  182. sys.exit(res)
  183. if __name__ == '__main__':
  184. parser = argparse.ArgumentParser(
  185. description='Build all the apps for different test types. Will auto remove those non-test apps binaries',
  186. formatter_class=argparse.ArgumentDefaultsHelpFormatter,
  187. )
  188. parser.add_argument('paths', nargs='+', help='Paths to the apps to build.')
  189. parser.add_argument(
  190. '-t',
  191. '--target',
  192. default='all',
  193. help='Build apps for given target',
  194. )
  195. parser.add_argument(
  196. '--config',
  197. default=['sdkconfig.ci=default', 'sdkconfig.ci.*=', '=default'],
  198. nargs='+',
  199. help='Adds configurations (sdkconfig file names) to build. This can either be '
  200. 'FILENAME[=NAME] or FILEPATTERN. FILENAME is the name of the sdkconfig file, '
  201. 'relative to the project directory, to be used. Optional NAME can be specified, '
  202. 'which can be used as a name of this configuration. FILEPATTERN is the name of '
  203. 'the sdkconfig file, relative to the project directory, with at most one wildcard. '
  204. 'The part captured by the wildcard is used as the name of the configuration.',
  205. )
  206. parser.add_argument(
  207. '-v',
  208. '--verbose',
  209. action='count',
  210. help='Increase the LOGGER level of the script. Can be specified multiple times.',
  211. )
  212. parser.add_argument(
  213. '--build-verbose',
  214. action='store_true',
  215. help='Enable verbose output from build system.',
  216. )
  217. parser.add_argument(
  218. '--preserve-all',
  219. action='store_true',
  220. help='Preserve the binaries for all apps when specified.',
  221. )
  222. parser.add_argument('--parallel-count', default=1, type=int, help='Number of parallel build jobs.')
  223. parser.add_argument(
  224. '--parallel-index',
  225. default=1,
  226. type=int,
  227. help='Index (1-based) of the job, out of the number specified by --parallel-count.',
  228. )
  229. parser.add_argument(
  230. '--collect-app-info',
  231. default='list_job_@p.txt',
  232. help='If specified, the test case name and app info json will be written to this file',
  233. )
  234. parser.add_argument(
  235. '--ignore-warning-str',
  236. nargs='+',
  237. help='Ignore the warning string that match the specified regex in the build output. space-separated list',
  238. )
  239. parser.add_argument(
  240. '--ignore-warning-file',
  241. default=os.path.join(IDF_PATH, 'tools', 'ci', 'ignore_build_warnings.txt'),
  242. type=argparse.FileType('r'),
  243. help='Ignore the warning strings in the specified file. Each line should be a regex string.',
  244. )
  245. parser.add_argument(
  246. '--copy-sdkconfig',
  247. action='store_true',
  248. help='Copy the sdkconfig file to the build directory.',
  249. )
  250. parser.add_argument(
  251. '--extra-preserve-dirs',
  252. nargs='+',
  253. help='also preserve binaries of the apps under the specified dirs',
  254. )
  255. parser.add_argument(
  256. '--pytest-apps',
  257. action='store_true',
  258. help='Only build apps with pytest scripts. Will build apps without pytest scripts if this flag is unspecified.',
  259. )
  260. parser.add_argument(
  261. '-m',
  262. '--marker-expr',
  263. default='not host_test', # host_test apps would be built and tested under the same job
  264. help='only build tests matching given mark expression. For example: -m "host_test and generic". Works only'
  265. 'for pytest',
  266. )
  267. parser.add_argument(
  268. '-k',
  269. '--filter-expr',
  270. help='only build tests matching given filter expression. For example: -k "test_hello_world". Works only'
  271. 'for pytest',
  272. )
  273. parser.add_argument(
  274. '--default-build-test-rules',
  275. default=os.path.join(IDF_PATH, '.gitlab', 'ci', 'default-build-test-rules.yml'),
  276. help='default build test rules config file',
  277. )
  278. parser.add_argument(
  279. '--skip-setting-flags',
  280. action='store_true',
  281. help='by default this script would set the build flags exactly the same as the CI ones. '
  282. 'Set this flag to use your local build flags.',
  283. )
  284. parser.add_argument(
  285. '--modified-components',
  286. nargs='*',
  287. default=None,
  288. help='space-separated list which specifies the modified components. app with `depends_components` set in the '
  289. 'corresponding manifest files would only be built if depends on any of the specified components.',
  290. )
  291. parser.add_argument(
  292. '--modified-files',
  293. nargs='*',
  294. default=None,
  295. help='space-separated list which specifies the modified files. app with `depends_filepatterns` set in the '
  296. 'corresponding manifest files would only be built if any of the specified file pattern matches any of the '
  297. 'specified modified files.',
  298. )
  299. parser.add_argument(
  300. '-if',
  301. '--ignore-app-dependencies-filepatterns',
  302. nargs='*',
  303. default=None,
  304. help='space-separated list which specifies the file patterns used for ignoring checking the app dependencies. '
  305. 'The `depends_components` and `depends_filepatterns` set in the manifest files will be ignored when any of the '
  306. 'specified file patterns matches any of the modified files. Must be used together with --modified-files',
  307. )
  308. arguments = parser.parse_args()
  309. setup_logging(arguments.verbose)
  310. # skip setting flags in CI
  311. if not arguments.skip_setting_flags and not os.getenv('CI_JOB_ID'):
  312. for _k, _v in CI_ENV_VARS.items():
  313. os.environ[_k] = _v
  314. LOGGER.info(f'env var {_k} set to "{_v}"')
  315. if os.getenv('IS_MR_PIPELINE') == '0' or os.getenv('BUILD_AND_TEST_ALL_APPS') == '1':
  316. # if it's not MR pipeline or env var BUILD_AND_TEST_ALL_APPS=1,
  317. # remove component dependency related arguments
  318. if 'modified_components' in arguments:
  319. arguments.modified_components = None
  320. if 'modified_files' in arguments:
  321. arguments.modified_files = None
  322. # file patterns to tigger full build
  323. if 'modified_components' in arguments and not arguments.ignore_app_dependencies_filepatterns:
  324. arguments.ignore_app_dependencies_filepatterns = [
  325. # tools
  326. 'tools/cmake/**/*',
  327. 'tools/tools.json',
  328. # components
  329. 'components/cxx/**/*',
  330. 'components/esp_common/**/*',
  331. 'components/esp_hw_support/**/*',
  332. 'components/esp_rom/**/*',
  333. 'components/esp_system/**/*',
  334. 'components/esp_timer/**/*',
  335. 'components/freertos/**/*',
  336. 'components/hal/**/*',
  337. 'components/heap/**/*',
  338. 'components/log/**/*',
  339. 'components/newlib/**/*',
  340. 'components/riscv/**/*',
  341. 'components/soc/**/*',
  342. 'components/xtensa/**/*',
  343. ]
  344. main(arguments)
  345. class TestParsingShellScript(unittest.TestCase):
  346. """
  347. This test case is run in CI jobs to make sure the CI build flags is the same as the ones recorded in CI_ENV_VARS
  348. """
  349. def test_parse_result(self) -> None:
  350. for k, v in CI_ENV_VARS.items():
  351. self.assertEqual(os.getenv(k), v)