diff --git a/examples/peripherals/temp_sensor_esp32s2/README.md b/examples/peripherals/temp_sensor_esp32s2/README.md index 161798695..e0e328f63 100644 --- a/examples/peripherals/temp_sensor_esp32s2/README.md +++ b/examples/peripherals/temp_sensor_esp32s2/README.md @@ -1,3 +1,6 @@ +| Supported Targets | ESP32-S2 | +| ----------------- | -------- | + # ESP32-S2 Temperature Sensor Example The ESP32-S2 has a built-in temperature sensor. The temperature sensor module contains an 8-bit Sigma-Delta ADC and a temperature offset DAC. diff --git a/tools/build_apps.py b/tools/build_apps.py index 6a49ab06a..c18e259c9 100755 --- a/tools/build_apps.py +++ b/tools/build_apps.py @@ -5,8 +5,10 @@ # import argparse -import sys import logging +import shutil +import sys + from find_build_apps import BuildItem, BuildError, setup_logging, BUILD_SYSTEMS @@ -33,8 +35,8 @@ def main(): default=1, type=int, help="Number of parallel build jobs. Note that this script doesn't start the jobs, " + - "it needs to be executed multiple times with same value of --parallel-count and " + - "different values of --parallel-index.", + "it needs to be executed multiple times with same value of --parallel-count and " + + "different values of --parallel-index.", ) parser.add_argument( "--parallel-index", @@ -75,10 +77,9 @@ def main(): setup_logging(args) build_items = [BuildItem.from_json(line) for line in args.build_list] - if not build_items: - logging.error("Empty build list!") - raise SystemExit(1) + logging.warning("Empty build list") + SystemExit(0) num_builds = len(build_items) num_jobs = args.parallel_count @@ -117,6 +118,11 @@ def main(): failed_builds.append(build_info) else: raise SystemExit(1) + else: + if not build_info.preserve: + logging.info("Removing build directory {}".format(build_info.build_dir)) + # we only remove binaries here, log files are still needed by check_build_warnings.py + shutil.rmtree(build_info.build_dir, ignore_errors=True) if failed_builds: logging.error("The following build have failed:") diff --git a/tools/ci/build_examples.sh b/tools/ci/build_examples.sh index 7308861f7..df7e4745a 100755 --- a/tools/ci/build_examples.sh +++ b/tools/ci/build_examples.sh @@ -31,6 +31,7 @@ die() { [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set" [ -z ${IDF_TARGET} ] && die "IDF_TARGET is not set" [ -z ${EXAMPLE_TEST_BUILD_SYSTEM} ] && die "EXAMPLE_TEST_BUILD_SYSTEM is not set" +[ -z ${SCAN_EXAMPLE_TEST_JSON} ] && die "SCAN_EXAMPLE_TEST_JSON is not set" [ -d ${LOG_PATH} ] || mkdir -p ${LOG_PATH} [ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_PATH} @@ -71,13 +72,9 @@ cd ${IDF_PATH} # If changing the work-dir or build-dir format, remember to update the "artifacts" in gitlab-ci configs, and IDFApp.py. -${IDF_PATH}/tools/find_apps.py examples \ +${IDF_PATH}/tools/find_apps.py \ -vv \ --format json \ - --build-system ${EXAMPLE_TEST_BUILD_SYSTEM} \ - --target ${IDF_TARGET} \ - --recursive \ - --exclude examples/build_system/idf_as_lib \ --work-dir "${BUILD_PATH}/@f/@w/@t" \ --build-dir build \ --build-log "${LOG_PATH}/@f_@w.txt" \ @@ -85,6 +82,7 @@ ${IDF_PATH}/tools/find_apps.py examples \ --config 'sdkconfig.ci=default' \ --config 'sdkconfig.ci.*=' \ --config '=default' \ + --app-list ${SCAN_EXAMPLE_TEST_JSON} # --config rules above explained: # 1. If sdkconfig.ci exists, use it build the example with configuration name "default" diff --git a/tools/ci/build_test_apps.sh b/tools/ci/build_test_apps.sh index cce9c2dfe..7909044f0 100755 --- a/tools/ci/build_test_apps.sh +++ b/tools/ci/build_test_apps.sh @@ -29,6 +29,7 @@ die() { [ -z ${LOG_PATH} ] && die "LOG_PATH is not set" [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set" [ -z ${IDF_TARGET} ] && die "IDF_TARGET is not set" +[ -z ${SCAN_CUSTOM_TEST_JSON} ] && die "SCAN_CUSTOM_TEST_JSON is not set" [ -d ${LOG_PATH} ] || mkdir -p ${LOG_PATH} [ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_PATH} @@ -61,12 +62,9 @@ cd ${IDF_PATH} # If changing the work-dir or build-dir, remember to update the "artifacts" in gitlab-ci configs, and IDFApp.py. -${IDF_PATH}/tools/find_apps.py tools/test_apps \ +${IDF_PATH}/tools/find_apps.py \ -vv \ --format json \ - --build-system cmake \ - --target ${IDF_TARGET} \ - --recursive \ --work-dir "${BUILD_PATH}/@f/@w/@t" \ --build-dir build \ --build-log "${LOG_PATH}/@f_@w.txt" \ @@ -74,6 +72,7 @@ ${IDF_PATH}/tools/find_apps.py tools/test_apps \ --config 'sdkconfig.ci=default' \ --config 'sdkconfig.ci.*=' \ --config '=default' \ + --app-list ${SCAN_CUSTOM_TEST_JSON} # --config rules above explained: # 1. If sdkconfig.ci exists, use it build the example with configuration name "default" diff --git a/tools/ci/build_unit_test.sh b/tools/ci/build_unit_test.sh index dd4402f1d..deeb0e8cd 100755 --- a/tools/ci/build_unit_test.sh +++ b/tools/ci/build_unit_test.sh @@ -63,7 +63,8 @@ cd ${IDF_PATH} # This part of the script produces the same result for all the unit test app build jobs. It may be moved to a separate stage # (pre-build) later, then the build jobs will receive ${BUILD_LIST_JSON} file as an artifact. -${IDF_PATH}/tools/find_apps.py tools/unit-test-app \ +${IDF_PATH}/tools/find_apps.py \ + -p tools/unit-test-app \ -vv \ --format json \ --build-system cmake \ diff --git a/tools/ci/check_build_warnings.py b/tools/ci/check_build_warnings.py index e64cc4066..d8b359111 100755 --- a/tools/ci/check_build_warnings.py +++ b/tools/ci/check_build_warnings.py @@ -6,11 +6,11 @@ # log files for every build. # Exits with a non-zero exit code if any warning is found. -import os -import sys import argparse import logging +import os import re +import sys try: from find_build_apps import BuildItem, setup_logging @@ -70,10 +70,9 @@ def main(): setup_logging(args) build_items = [BuildItem.from_json(line) for line in args.build_list] - if not build_items: - logging.error("Empty build list!") - raise SystemExit(1) + logging.warning("Empty build list") + SystemExit(0) found_warnings = 0 for build_item in build_items: diff --git a/tools/ci/config/build.yml b/tools/ci/config/build.yml index 7133ac2a4..d09977aef 100644 --- a/tools/ci/config/build.yml +++ b/tools/ci/config/build.yml @@ -82,6 +82,8 @@ build_esp_idf_tests_cmake_esp32s2: artifacts: when: always expire_in: 4 days + variables: + SCAN_EXAMPLE_TEST_JSON: ${CI_PROJECT_DIR}/examples/test_configs/scan_${IDF_TARGET}_${EXAMPLE_TEST_BUILD_SYSTEM}.json only: # Here both 'variables' and 'refs' conditions are given. They are combined with "AND" logic. variables: @@ -96,9 +98,6 @@ build_esp_idf_tests_cmake_esp32s2: - mkdir ${BUILD_PATH} - mkdir -p ${LOG_PATH} - ${IDF_PATH}/tools/ci/build_examples.sh - # Check if the tests demand Make built binaries. If not, delete them - - if [ ${EXAMPLE_TEST_BUILD_SYSTEM} == "cmake" ]; then exit 0; fi - - rm -rf ${BUILD_PATH} build_examples_make: extends: .build_examples_template @@ -126,6 +125,8 @@ build_examples_make: # same as above, but for CMake .build_examples_cmake: &build_examples_cmake extends: .build_examples_template + dependencies: + - scan_tests artifacts: paths: - build_examples/list.json @@ -156,6 +157,8 @@ build_examples_cmake_esp32s2: .build_test_apps: &build_test_apps extends: .build_template stage: build + dependencies: + - scan_tests artifacts: when: always paths: @@ -171,8 +174,10 @@ build_examples_cmake_esp32s2: - $LOG_PATH expire_in: 3 days variables: - LOG_PATH: "$CI_PROJECT_DIR/log_test_apps" - BUILD_PATH: "$CI_PROJECT_DIR/build_test_apps" + LOG_PATH: "${CI_PROJECT_DIR}/log_test_apps" + BUILD_PATH: "${CI_PROJECT_DIR}/build_test_apps" + CUSTOM_TEST_BUILD_SYSTEM: "cmake" + SCAN_CUSTOM_TEST_JSON: ${CI_PROJECT_DIR}/tools/test_apps/test_configs/scan_${IDF_TARGET}_${CUSTOM_TEST_BUILD_SYSTEM}.json only: variables: - $BOT_TRIGGER_WITH_LABEL == null diff --git a/tools/ci/config/pre_check.yml b/tools/ci/config/pre_check.yml index 13b1dd8d1..d986503d8 100644 --- a/tools/ci/config/pre_check.yml +++ b/tools/ci/config/pre_check.yml @@ -191,3 +191,33 @@ check_public_headers: script: - python tools/ci/check_public_headers.py --jobs 4 --prefix xtensa-esp32-elf- +.scan_build_tests: + stage: pre_check + image: $CI_DOCKER_REGISTRY/ubuntu-test-env$BOT_DOCKER_IMAGE_TAG + tags: + - assign_test + variables: + CI_SCAN_TESTS_PY: ${CI_PROJECT_DIR}/tools/ci/python_packages/ttfw_idf/CIScanTests.py + TEST_CONFIG_FILE: ${CI_PROJECT_DIR}/tools/ci/config/target-test.yml + +scan_tests: + extends: .scan_build_tests + only: + variables: + - $BOT_TRIGGER_WITH_LABEL == null + - $BOT_LABEL_REGULAR_TEST + - $BOT_LABEL_EXAMPLE_TEST + - $BOT_LABEL_CUSTOM_TEST + artifacts: + paths: + - $EXAMPLE_TEST_OUTPUT_DIR + - $TEST_APPS_OUTPUT_DIR + variables: + EXAMPLE_TEST_DIR: ${CI_PROJECT_DIR}/examples + EXAMPLE_TEST_OUTPUT_DIR: ${CI_PROJECT_DIR}/examples/test_configs + TEST_APPS_TEST_DIR: ${CI_PROJECT_DIR}/tools/test_apps + TEST_APPS_OUTPUT_DIR: ${CI_PROJECT_DIR}/tools/test_apps/test_configs + script: + - python $CI_SCAN_TESTS_PY example_test -b make $EXAMPLE_TEST_DIR --exclude examples/build_system/idf_as_lib -c $TEST_CONFIG_FILE -o $EXAMPLE_TEST_OUTPUT_DIR + - python $CI_SCAN_TESTS_PY example_test -b cmake $EXAMPLE_TEST_DIR --exclude examples/build_system/idf_as_lib -c $TEST_CONFIG_FILE -o $EXAMPLE_TEST_OUTPUT_DIR + - python $CI_SCAN_TESTS_PY test_apps $TEST_APPS_TEST_DIR -c $TEST_CONFIG_FILE -o $TEST_APPS_OUTPUT_DIR diff --git a/tools/ci/python_packages/tiny_test_fw/Utility/CIAssignTest.py b/tools/ci/python_packages/tiny_test_fw/Utility/CIAssignTest.py index 6277f0781..240c7fbc4 100644 --- a/tools/ci/python_packages/tiny_test_fw/Utility/CIAssignTest.py +++ b/tools/ci/python_packages/tiny_test_fw/Utility/CIAssignTest.py @@ -189,16 +189,15 @@ class AssignTest(object): job_list.sort(key=lambda x: x["name"]) return job_list - def _search_cases(self, test_case_path, case_filter=None, test_case_file_pattern=None): + def search_cases(self, case_filter=None): """ - :param test_case_path: path contains test case folder :param case_filter: filter for test cases. the filter to use is default filter updated with case_filter param. :return: filtered test case list """ _case_filter = self.DEFAULT_FILTER.copy() if case_filter: _case_filter.update(case_filter) - test_methods = SearchCases.Search.search_test_cases(test_case_path, test_case_file_pattern) + test_methods = SearchCases.Search.search_test_cases(self.test_case_path, self.test_case_file_pattern) return CaseConfig.filter_test_cases(test_methods, _case_filter) def _group_cases(self): @@ -287,7 +286,7 @@ class AssignTest(object): failed_to_assign = [] assigned_groups = [] case_filter = self._apply_bot_filter() - self.test_cases = self._search_cases(self.test_case_path, case_filter, self.test_case_file_pattern) + self.test_cases = self.search_cases(case_filter) self._apply_bot_test_count() test_groups = self._group_cases() diff --git a/tools/ci/python_packages/tiny_test_fw/Utility/SearchCases.py b/tools/ci/python_packages/tiny_test_fw/Utility/SearchCases.py index 699bb57ec..d85283b76 100644 --- a/tools/ci/python_packages/tiny_test_fw/Utility/SearchCases.py +++ b/tools/ci/python_packages/tiny_test_fw/Utility/SearchCases.py @@ -50,6 +50,7 @@ class Search(object): for i, test_function in enumerate(test_functions_out): print("\t{}. ".format(i + 1) + test_function.case_info["name"]) + test_function.case_info['app_dir'] = os.path.dirname(file_name) return test_functions_out @classmethod @@ -124,6 +125,7 @@ class Search(object): search all test cases from a folder or file, and then do case replicate. :param test_case: test case file(s) path + :param test_case_file_pattern: unix filename pattern :return: a list of replicated test methods """ test_case_files = cls._search_test_case_files(test_case, test_case_file_pattern or cls.TEST_CASE_FILE_PATTERN) diff --git a/tools/ci/python_packages/ttfw_idf/CIAssignUnitTest.py b/tools/ci/python_packages/ttfw_idf/CIAssignUnitTest.py index bde9a102d..e9c012e6d 100644 --- a/tools/ci/python_packages/ttfw_idf/CIAssignUnitTest.py +++ b/tools/ci/python_packages/ttfw_idf/CIAssignUnitTest.py @@ -139,7 +139,7 @@ class UnitTestAssignTest(CIAssignTest.AssignTest): def __init__(self, test_case_path, ci_config_file): CIAssignTest.AssignTest.__init__(self, test_case_path, ci_config_file, case_group=Group) - def _search_cases(self, test_case_path, case_filter=None, test_case_file_pattern=None): + def search_cases(self, case_filter=None): """ For unit test case, we don't search for test functions. The unit test cases is stored in a yaml file which is created in job build-idf-test. @@ -164,11 +164,11 @@ class UnitTestAssignTest(CIAssignTest.AssignTest): return test_cases test_cases = [] - if os.path.isdir(test_case_path): - for yml_file in find_by_suffix('.yml', test_case_path): + if os.path.isdir(self.test_case_path): + for yml_file in find_by_suffix('.yml', self.test_case_path): test_cases.extend(get_test_cases_from_yml(yml_file)) - elif os.path.isfile(test_case_path): - test_cases.extend(get_test_cases_from_yml(test_case_path)) + elif os.path.isfile(self.test_case_path): + test_cases.extend(get_test_cases_from_yml(self.test_case_path)) else: print("Test case path is invalid. Should only happen when use @bot to skip unit test.") diff --git a/tools/ci/python_packages/ttfw_idf/CIScanTests.py b/tools/ci/python_packages/ttfw_idf/CIScanTests.py new file mode 100644 index 000000000..f78f3ede5 --- /dev/null +++ b/tools/ci/python_packages/ttfw_idf/CIScanTests.py @@ -0,0 +1,175 @@ +import argparse +import errno +import json +import logging +import os +import re +from collections import defaultdict + +from find_apps import find_apps +from find_build_apps import BUILD_SYSTEMS, BUILD_SYSTEM_CMAKE +from ttfw_idf.CIAssignExampleTest import CIExampleAssignTest, TestAppsGroup, ExampleGroup + +VALID_TARGETS = [ + 'esp32', + 'esp32s2', +] + +TEST_LABELS = { + 'example_test': 'BOT_LABEL_EXAMPLE_TEST', + 'test_apps': 'BOT_LABEL_CUSTOM_TEST', +} + +BUILD_ALL_LABELS = [ + 'BOT_LABEL_BUILD_ALL_APPS', + 'BOT_LABEL_REGULAR_TEST', +] + + +def _has_build_all_label(): + for label in BUILD_ALL_LABELS: + if os.getenv(label): + return True + return False + + +def _judge_build_or_not(action, build_all): # type: (str, bool) -> (bool, bool) + """ + :return: (build_or_not_for_test_related_apps, build_or_not_for_non_related_apps) + """ + if build_all or _has_build_all_label() or (not os.getenv('BOT_TRIGGER_WITH_LABEL')): + logging.info('Build all apps') + return True, True + + if os.getenv(TEST_LABELS[action]): + logging.info('Build test cases apps') + return True, False + else: + logging.info('Skip all') + return False, False + + +def output_json(apps_dict_list, target, build_system, output_dir): + output_path = os.path.join(output_dir, 'scan_{}_{}.json'.format(target.lower(), build_system)) + with open(output_path, 'w') as fw: + fw.writelines([json.dumps(app) + '\n' for app in apps_dict_list]) + + +def main(): + parser = argparse.ArgumentParser(description='Scan the required build tests') + parser.add_argument('test_type', + choices=TEST_LABELS.keys(), + help='Scan test type') + parser.add_argument('paths', + nargs='+', + help='One or more app paths') + parser.add_argument('-b', '--build-system', + choices=BUILD_SYSTEMS.keys(), + default=BUILD_SYSTEM_CMAKE) + parser.add_argument('-c', '--ci-config-file', + required=True, + help="gitlab ci config target-test file") + parser.add_argument('-o', '--output-path', + required=True, + help="output path of the scan result") + parser.add_argument("--exclude", + action="append", + help='Ignore specified directory. Can be used multiple times.') + parser.add_argument('--preserve', action="store_true", + help='add this flag to preserve artifacts for all apps') + parser.add_argument('--build-all', action="store_true", + help='add this flag to build all apps') + + args = parser.parse_args() + build_test_case_apps, build_standalone_apps = _judge_build_or_not(args.test_type, args.build_all) + + if not os.path.exists(args.output_path): + try: + os.makedirs(args.output_path) + except OSError as e: + if e.errno != errno.EEXIST: + raise e + + if (not build_standalone_apps) and (not build_test_case_apps): + for target in VALID_TARGETS: + output_json([], target, args.build_system, args.output_path) + SystemExit(0) + + test_cases = [] + for path in set(args.paths): + if args.test_type == 'example_test': + assign = CIExampleAssignTest(path, args.ci_config_file, ExampleGroup) + elif args.test_type == 'test_apps': + CIExampleAssignTest.CI_TEST_JOB_PATTERN = re.compile(r'^test_app_test_.+') + assign = CIExampleAssignTest(path, args.ci_config_file, TestAppsGroup) + else: + raise SystemExit(1) # which is impossible + + test_cases.extend(assign.search_cases()) + + ''' + { + : { + 'test_case_apps': [], # which is used in target tests + 'standalone_apps': [], # which is not + }, + ... + } + ''' + scan_info_dict = defaultdict(dict) + # store the test cases dir, exclude these folders when scan for standalone apps + default_exclude = args.exclude if args.exclude else [] + exclude_apps = default_exclude + + build_system = args.build_system.lower() + build_system_class = BUILD_SYSTEMS[build_system] + + if build_test_case_apps: + for target in VALID_TARGETS: + target_dict = scan_info_dict[target] + test_case_apps = target_dict['test_case_apps'] = set() + for case in test_cases: + app_dir = case.case_info['app_dir'] + app_target = case.case_info['target'] + if app_target.lower() != target.lower(): + continue + test_case_apps.update(find_apps(build_system_class, app_dir, True, default_exclude, target.lower())) + exclude_apps.append(app_dir) + else: + for target in VALID_TARGETS: + scan_info_dict[target]['test_case_apps'] = set() + + if build_standalone_apps: + for target in VALID_TARGETS: + target_dict = scan_info_dict[target] + standalone_apps = target_dict['standalone_apps'] = set() + for path in args.paths: + standalone_apps.update(find_apps(build_system_class, path, True, exclude_apps, target.lower())) + else: + for target in VALID_TARGETS: + scan_info_dict[target]['standalone_apps'] = set() + + test_case_apps_preserve_default = True if build_system == 'cmake' else False + for target in VALID_TARGETS: + apps = [] + for app_dir in scan_info_dict[target]['test_case_apps']: + apps.append({ + 'app_dir': app_dir, + 'build_system': args.build_system, + 'target': target, + 'preserve': args.preserve or test_case_apps_preserve_default + }) + for app_dir in scan_info_dict[target]['standalone_apps']: + apps.append({ + 'app_dir': app_dir, + 'build_system': args.build_system, + 'target': target, + 'preserve': args.preserve + }) + output_path = os.path.join(args.output_path, 'scan_{}_{}.json'.format(target.lower(), build_system)) + with open(output_path, 'w') as fw: + fw.writelines([json.dumps(app) + '\n' for app in apps]) + + +if __name__ == '__main__': + main() diff --git a/tools/find_apps.py b/tools/find_apps.py index 478ed5f42..6fdaf178f 100755 --- a/tools/find_apps.py +++ b/tools/find_apps.py @@ -5,12 +5,15 @@ # Produces the list of builds. The list can be consumed by build_apps.py, which performs the actual builds. import argparse -import os -import sys -import re import glob +import json import logging +import os +import re +import sys + import typing + from find_build_apps import ( BUILD_SYSTEMS, BUILD_SYSTEM_CMAKE, @@ -22,8 +25,8 @@ from find_build_apps import ( DEFAULT_TARGET, ) -# Helper functions +# Helper functions def dict_from_sdkconfig(path): """ @@ -45,9 +48,9 @@ def dict_from_sdkconfig(path): # Main logic: enumerating apps and builds -def find_builds_for_app( - app_path, work_dir, build_dir, build_log, target_arg, build_system, - config_rules): # type: (str, str, str, str, str, str, typing.List[ConfigRule]) -> typing.List[BuildItem] +def find_builds_for_app(app_path, work_dir, build_dir, build_log, target_arg, + build_system, config_rules, preserve_artifacts=True): + # type: (str, str, str, str, str, str, typing.List[ConfigRule], bool) -> typing.List[BuildItem] """ Find configurations (sdkconfig file fragments) for the given app, return them as BuildItem objects :param app_path: app directory (can be / usually will be a relative path) @@ -60,6 +63,7 @@ def find_builds_for_app( a different CONFIG_IDF_TARGET value. :param build_system: name of the build system, index into BUILD_SYSTEMS dictionary :param config_rules: mapping of sdkconfig file name patterns to configuration names + :param preserve_artifacts: determine if the built binary will be uploaded as artifacts. :return: list of BuildItems representing build configuration of the app """ build_items = [] # type: typing.List[BuildItem] @@ -104,6 +108,7 @@ def find_builds_for_app( sdkconfig_path, config_name, build_system, + preserve_artifacts, )) if not build_items: @@ -118,14 +123,15 @@ def find_builds_for_app( None, default_config_name, build_system, + preserve_artifacts, ) ] return build_items -def find_apps(build_system_class, path, recursive, exclude_list, - target): # type: (typing.Type[BuildSystem], str, bool, typing.List[str], str) -> typing.List[str] +def find_apps(build_system_class, path, recursive, exclude_list, target): + # type: (typing.Type[BuildSystem], str, bool, typing.List[str], str) -> typing.List[str] """ Find app directories in path (possibly recursively), which contain apps for the given build system, compatible with the given target. @@ -189,26 +195,29 @@ def main(): action="store_true", help="Look for apps in the specified directories recursively.", ) - parser.add_argument("--build-system", choices=BUILD_SYSTEMS.keys(), default=BUILD_SYSTEM_CMAKE) + parser.add_argument( + "--build-system", + choices=BUILD_SYSTEMS.keys() + ) parser.add_argument( "--work-dir", help="If set, the app is first copied into the specified directory, and then built." + - "If not set, the work directory is the directory of the app.", + "If not set, the work directory is the directory of the app.", ) parser.add_argument( "--config", action="append", help="Adds configurations (sdkconfig file names) to build. This can either be " + - "FILENAME[=NAME] or FILEPATTERN. FILENAME is the name of the sdkconfig file, " + - "relative to the project directory, to be used. Optional NAME can be specified, " + - "which can be used as a name of this configuration. FILEPATTERN is the name of " + - "the sdkconfig file, relative to the project directory, with at most one wildcard. " + - "The part captured by the wildcard is used as the name of the configuration.", + "FILENAME[=NAME] or FILEPATTERN. FILENAME is the name of the sdkconfig file, " + + "relative to the project directory, to be used. Optional NAME can be specified, " + + "which can be used as a name of this configuration. FILEPATTERN is the name of " + + "the sdkconfig file, relative to the project directory, with at most one wildcard. " + + "The part captured by the wildcard is used as the name of the configuration.", ) parser.add_argument( "--build-dir", help="If set, specifies the build directory name. Can expand placeholders. Can be either a " + - "name relative to the work directory, or an absolute path.", + "name relative to the work directory, or an absolute path.", ) parser.add_argument( "--build-log", @@ -232,52 +241,84 @@ def main(): type=argparse.FileType("w"), help="Output the list of builds to the specified file", ) - parser.add_argument("paths", nargs="+", help="One or more app paths.") + parser.add_argument( + "--app-list", + default=None, + help="Scan tests results. Restrict the build/artifacts preservation behavior to apps need to be built. " + "If the file does not exist, will build all apps and upload all artifacts." + ) + parser.add_argument( + "-p", "--paths", + nargs="+", + help="One or more app paths." + ) args = parser.parse_args() setup_logging(args) - build_system_class = BUILD_SYSTEMS[args.build_system] + # Arguments Validation + if args.app_list: + conflict_args = [args.recursive, args.build_system, args.target, args.exclude, args.paths] + if any(conflict_args): + raise ValueError('Conflict settings. "recursive", "build_system", "target", "exclude", "paths" should not ' + 'be specified with "app_list"') + if not os.path.exists(args.app_list): + raise OSError("File not found {}".format(args.app_list)) + else: + # If the build target is not set explicitly, get it from the environment or use the default one (esp32) + if not args.target: + env_target = os.environ.get("IDF_TARGET") + if env_target: + logging.info("--target argument not set, using IDF_TARGET={} from the environment".format(env_target)) + args.target = env_target + else: + logging.info("--target argument not set, using IDF_TARGET={} as the default".format(DEFAULT_TARGET)) + args.target = DEFAULT_TARGET + if not args.build_system: + logging.info("--build-system argument not set, using {} as the default".format(BUILD_SYSTEM_CMAKE)) + args.build_system = BUILD_SYSTEM_CMAKE + required_args = [args.build_system, args.target, args.paths] + if not all(required_args): + raise ValueError('If app_list not set, arguments "build_system", "target", "paths" are required.') - # If the build target is not set explicitly, get it from the environment or use the default one (esp32) - if not args.target: - env_target = os.environ.get("IDF_TARGET") - if env_target: - logging.info("--target argument not set, using IDF_TARGET={} from the environment".format(env_target)) - args.target = env_target - else: - logging.info("--target argument not set, using IDF_TARGET={} as the default".format(DEFAULT_TARGET)) - args.target = DEFAULT_TARGET + # Prepare the list of app paths, try to read from the scan_tests result. + # If the file exists, then follow the file's app_dir and build/artifacts behavior, won't do find_apps() again. + # If the file not exists, will do find_apps() first, then build all apps and upload all artifacts. + if args.app_list: + apps = [json.loads(line) for line in open(args.app_list)] + else: + app_dirs = [] + build_system_class = BUILD_SYSTEMS[args.build_system] + for path in args.paths: + app_dirs += find_apps(build_system_class, path, args.recursive, args.exclude or [], args.target) + apps = [{"app_dir": app_dir, "build": True, "preserve": True} for app_dir in app_dirs] - # Prepare the list of app paths - app_paths = [] # type: typing.List[str] - for path in args.paths: - app_paths += find_apps(build_system_class, path, args.recursive, args.exclude or [], args.target) + if not apps: + logging.warning("No apps found") + SystemExit(0) - if not app_paths: - logging.critical("No {} apps found".format(build_system_class.NAME)) - raise SystemExit(1) - logging.info("Found {} apps".format(len(app_paths))) - - app_paths = sorted(app_paths) + logging.info("Found {} apps".format(len(apps))) + apps.sort(key=lambda x: x["app_dir"]) # Find compatible configurations of each app, collect them as BuildItems build_items = [] # type: typing.List[BuildItem] config_rules = config_rules_from_str(args.config or []) - for app_path in app_paths: + for app in apps: build_items += find_builds_for_app( - app_path, + app["app_dir"], args.work_dir, args.build_dir, args.build_log, - args.target, - args.build_system, + args.target or app["target"], + args.build_system or app["build_system"], config_rules, + app["preserve"], ) logging.info("Found {} builds".format(len(build_items))) # Write out the BuildItems. Only JSON supported now (will add YAML later). if args.format != "json": raise NotImplementedError() + out = args.output or sys.stdout out.writelines([item.to_json() + "\n" for item in build_items]) diff --git a/tools/find_build_apps/cmake.py b/tools/find_build_apps/cmake.py index ab4a842ad..56b57c864 100644 --- a/tools/find_build_apps/cmake.py +++ b/tools/find_build_apps/cmake.py @@ -93,3 +93,33 @@ class CMakeBuildSystem(BuildSystem): if CMAKE_PROJECT_LINE not in cmakelists_file_content: return False return True + + @staticmethod + def supported_targets(app_path): + formal_to_usual = { + 'ESP32': 'esp32', + 'ESP32-S2': 'esp32s2', + } + + readme_file_content = BuildSystem._read_readme(app_path) + if not readme_file_content: + return None + match = re.findall(BuildSystem.SUPPORTED_TARGETS_REGEX, readme_file_content) + if not match: + return None + if len(match) > 1: + raise NotImplementedError("Can't determine the value of SUPPORTED_TARGETS in {}".format(app_path)) + support_str = match[0].strip() + + targets = [] + for part in support_str.split('|'): + for inner in part.split(' '): + inner = inner.strip() + if not inner: + continue + elif inner in formal_to_usual: + targets.append(formal_to_usual[inner]) + else: + raise NotImplementedError("Can't recognize value of target {} in {}, now we only support '{}'" + .format(inner, app_path, ', '.join(formal_to_usual.keys()))) + return targets diff --git a/tools/find_build_apps/common.py b/tools/find_build_apps/common.py index 81b61859a..a068f5f5b 100644 --- a/tools/find_build_apps/common.py +++ b/tools/find_build_apps/common.py @@ -71,6 +71,7 @@ class BuildItem(object): sdkconfig_path, config_name, build_system, + preserve_artifacts, ): # These internal variables store the paths with environment variables and placeholders; # Public properties with similar names use the _expand method to get the actual paths. @@ -84,6 +85,8 @@ class BuildItem(object): self.target = target self.build_system = build_system + self.preserve = preserve_artifacts + self._app_name = os.path.basename(os.path.normpath(app_path)) # Some miscellaneous build properties which are set later, at the build stage @@ -155,6 +158,7 @@ class BuildItem(object): "config": self.config_name, "target": self.target, "verbose": self.verbose, + "preserve": self.preserve, }) @staticmethod @@ -172,6 +176,7 @@ class BuildItem(object): config_name=d["config"], target=d["target"], build_system=d["build_system"], + preserve_artifacts=d["preserve"] ) result.verbose = d["verbose"] return result @@ -332,34 +337,9 @@ class BuildSystem(object): return readme_file.read() @staticmethod + @abstractmethod def supported_targets(app_path): - formal_to_usual = { - 'ESP32': 'esp32', - 'ESP32-S2': 'esp32s2', - } - - readme_file_content = BuildSystem._read_readme(app_path) - if not readme_file_content: - return None - match = re.findall(BuildSystem.SUPPORTED_TARGETS_REGEX, readme_file_content) - if not match: - return None - if len(match) > 1: - raise NotImplementedError("Can't determine the value of SUPPORTED_TARGETS in {}".format(app_path)) - support_str = match[0].strip() - - targets = [] - for part in support_str.split('|'): - for inner in part.split(' '): - inner = inner.strip() - if not inner: - continue - elif inner in formal_to_usual: - targets.append(formal_to_usual[inner]) - else: - raise NotImplementedError("Can't recognize value of target {} in {}, now we only support '{}'" - .format(inner, app_path, ', '.join(formal_to_usual.keys()))) - return targets + pass class BuildError(RuntimeError): diff --git a/tools/find_build_apps/make.py b/tools/find_build_apps/make.py index a8b153fd0..e7b885732 100644 --- a/tools/find_build_apps/make.py +++ b/tools/find_build_apps/make.py @@ -1,8 +1,8 @@ import logging import os +import shlex import subprocess import sys -import shlex from .common import BuildSystem, BuildError @@ -58,3 +58,7 @@ class MakeBuildSystem(BuildSystem): if MAKE_PROJECT_LINE not in makefile_content: return False return True + + @staticmethod + def supported_targets(app_path): + return ['esp32']