ci_build_apps.py 14 KB

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