ci_build_apps.py 14 KB

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