# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 """This file is used for generating the child pipeline for build jobs.""" import argparse import os import typing as t import __init__ # noqa: F401 # inject the system path import yaml from dynamic_pipelines.constants import DEFAULT_APPS_BUILD_PER_JOB from dynamic_pipelines.constants import DEFAULT_BUILD_CHILD_PIPELINE_FILEPATH from dynamic_pipelines.constants import DEFAULT_TEST_PATHS from dynamic_pipelines.constants import NON_TEST_RELATED_APPS_FILENAME from dynamic_pipelines.constants import NON_TEST_RELATED_BUILD_JOB_NAME from dynamic_pipelines.constants import TEST_RELATED_APPS_FILENAME from dynamic_pipelines.constants import TEST_RELATED_BUILD_JOB_NAME from dynamic_pipelines.models import BuildJob from dynamic_pipelines.models import EmptyJob from dynamic_pipelines.utils import dump_jobs_to_yaml from idf_build_apps.utils import semicolon_separated_str_to_list from idf_ci.app import dump_apps_to_txt from idf_ci_utils import IDF_PATH from idf_pytest.constants import CollectMode from idf_pytest.constants import DEFAULT_CONFIG_RULES_STR from idf_pytest.constants import DEFAULT_FULL_BUILD_TEST_COMPONENTS from idf_pytest.constants import DEFAULT_FULL_BUILD_TEST_FILEPATTERNS from idf_pytest.script import get_all_apps def _separate_str_to_list(s: str) -> t.List[str]: """ Gitlab env file will escape the doublequotes in the env file, so we need to remove them For example, in pipeline.env file we have MR_MODIFIED_COMPONENTS="app1;app2" MR_MODIFIED_FILES="main/app1.c;main/app2.c" gitlab will load the doublequotes as well, so we need to remove the doublequotes """ return semicolon_separated_str_to_list(s.strip('"')) # type: ignore def main(arguments: argparse.Namespace) -> None: # load from default build test rules config file extra_default_build_targets: t.List[str] = [] if arguments.default_build_test_rules: with open(arguments.default_build_test_rules) as fr: configs = yaml.safe_load(fr) if configs: extra_default_build_targets = configs.get('extra_default_build_targets') or [] build_jobs = [] ########################################### # special case with -k, ignore other args # ########################################### if arguments.filter_expr: # build only test related apps test_related_apps, _ = get_all_apps( arguments.paths, target=CollectMode.ALL, config_rules_str=DEFAULT_CONFIG_RULES_STR, filter_expr=arguments.filter_expr, marker_expr='not host_test', extra_default_build_targets=extra_default_build_targets, ) dump_apps_to_txt(sorted(test_related_apps), TEST_RELATED_APPS_FILENAME) print(f'Generate test related apps file {TEST_RELATED_APPS_FILENAME} with {len(test_related_apps)} apps') test_apps_build_job = BuildJob( name=TEST_RELATED_BUILD_JOB_NAME, parallel=len(test_related_apps) // DEFAULT_APPS_BUILD_PER_JOB + 1, variables={ 'APP_LIST_FILE': TEST_RELATED_APPS_FILENAME, }, ) build_jobs.append(test_apps_build_job) else: ############# # all cases # ############# test_related_apps, non_test_related_apps = get_all_apps( arguments.paths, CollectMode.ALL, marker_expr='not host_test', config_rules_str=DEFAULT_CONFIG_RULES_STR, extra_default_build_targets=extra_default_build_targets, modified_components=arguments.modified_components, modified_files=arguments.modified_files, ignore_app_dependencies_filepatterns=arguments.ignore_app_dependencies_filepatterns, ) dump_apps_to_txt(sorted(test_related_apps), TEST_RELATED_APPS_FILENAME) print(f'Generate test related apps file {TEST_RELATED_APPS_FILENAME} with {len(test_related_apps)} apps') dump_apps_to_txt(sorted(non_test_related_apps), NON_TEST_RELATED_APPS_FILENAME) print( f'Generate non-test related apps file {NON_TEST_RELATED_APPS_FILENAME} with {len(non_test_related_apps)} apps' ) if test_related_apps: test_apps_build_job = BuildJob( name=TEST_RELATED_BUILD_JOB_NAME, parallel=len(test_related_apps) // DEFAULT_APPS_BUILD_PER_JOB + 1, variables={ 'APP_LIST_FILE': TEST_RELATED_APPS_FILENAME, }, ) build_jobs.append(test_apps_build_job) if non_test_related_apps: non_test_apps_build_job = BuildJob( name=NON_TEST_RELATED_BUILD_JOB_NAME, parallel=len(non_test_related_apps) // DEFAULT_APPS_BUILD_PER_JOB + 1, variables={ 'APP_LIST_FILE': NON_TEST_RELATED_APPS_FILENAME, }, ) build_jobs.append(non_test_apps_build_job) if mr_labels := os.getenv('CI_MERGE_REQUEST_LABELS'): print(f'MR labels: {mr_labels}') # check if there's no jobs if not build_jobs: print('No apps need to be built. Create one empty job instead') build_jobs.append(EmptyJob()) extra_include_yml = [] else: extra_include_yml = ['tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml'] dump_jobs_to_yaml(build_jobs, arguments.yaml_output, extra_include_yml) print(f'Generate child pipeline yaml file {arguments.yaml_output} with {sum(j.parallel for j in build_jobs)} jobs') if __name__ == '__main__': parser = argparse.ArgumentParser( description='Generate build child pipeline', formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( '-o', '--yaml-output', default=DEFAULT_BUILD_CHILD_PIPELINE_FILEPATH, help='Output YAML path', ) parser.add_argument( '-p', '--paths', nargs='+', default=DEFAULT_TEST_PATHS, help='Paths to the apps to build.', ) parser.add_argument( '-k', '--filter-expr', help='only build tests matching given filter expression. For example: -k "test_hello_world". Works only' 'for pytest', ) parser.add_argument( '--default-build-test-rules', default=os.path.join(IDF_PATH, '.gitlab', 'ci', 'default-build-test-rules.yml'), help='default build test rules config file', ) parser.add_argument( '--modified-components', type=_separate_str_to_list, default=os.getenv('MR_MODIFIED_COMPONENTS'), help='semicolon-separated string which specifies the modified components. ' 'app with `depends_components` set in the corresponding manifest files would only be built ' 'if depends on any of the specified components. ' 'If set to "", the value would be considered as None. ' 'If set to ";", the value would be considered as an empty list', ) parser.add_argument( '--modified-files', type=_separate_str_to_list, default=os.getenv('MR_MODIFIED_FILES'), help='semicolon-separated string which specifies the modified files. ' 'app with `depends_filepatterns` set in the corresponding manifest files would only be built ' 'if any of the specified file pattern matches any of the specified modified files. ' 'If set to "", the value would be considered as None. ' 'If set to ";", the value would be considered as an empty list', ) parser.add_argument( '-ic', '--ignore-app-dependencies-components', type=_separate_str_to_list, help='semicolon-separated string which specifies the modified components used for ' 'ignoring checking the app dependencies. ' 'The `depends_components` and `depends_filepatterns` set in the manifest files will be ignored ' 'when any of the specified components matches any of the modified components. ' 'Must be used together with --modified-components. ' 'If set to "", the value would be considered as None. ' 'If set to ";", the value would be considered as an empty list', ) parser.add_argument( '-if', '--ignore-app-dependencies-filepatterns', type=_separate_str_to_list, help='semicolon-separated string which specifies the file patterns used for ' 'ignoring checking the app dependencies. ' 'The `depends_components` and `depends_filepatterns` set in the manifest files will be ignored ' 'when any of the specified file patterns matches any of the modified files. ' 'Must be used together with --modified-files. ' 'If set to "", the value would be considered as None. ' 'If set to ";", the value would be considered as an empty list', ) args = parser.parse_args() if os.getenv('IS_MR_PIPELINE') == '0' or os.getenv('BUILD_AND_TEST_ALL_APPS') == '1': print('Build and run all test cases, and compile all cmake apps') args.modified_components = None args.modified_files = None args.ignore_app_dependencies_filepatterns = None elif args.filter_expr is not None: print('Build and run only test cases matching "%s"' % args.filter_expr) args.modified_components = None args.modified_files = None args.ignore_app_dependencies_filepatterns = None else: print( f'Build and run only test cases matching:\n' f'- modified components: {args.modified_components}\n' f'- modified files: {args.modified_files}' ) if args.modified_components is not None and not args.ignore_app_dependencies_components: # setting default values args.ignore_app_dependencies_components = DEFAULT_FULL_BUILD_TEST_COMPONENTS if args.modified_files is not None and not args.ignore_app_dependencies_filepatterns: # setting default values args.ignore_app_dependencies_filepatterns = DEFAULT_FULL_BUILD_TEST_FILEPATTERNS main(args)