Merge branch 'feature/scan_test_at_pre_check' into 'master'

ci: scan_tests at pre_check stage to determine build/artifact behavior for example_test and custom_test

Closes IDF-1376

See merge request espressif/esp-idf!8447
This commit is contained in:
Ivan Grokhotkov
2020-07-07 23:43:03 +08:00
16 changed files with 377 additions and 105 deletions

View File

@@ -1,3 +1,6 @@
| Supported Targets | ESP32-S2 |
| ----------------- | -------- |
# ESP32-S2 Temperature Sensor Example # 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. 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.

View File

@@ -5,8 +5,10 @@
# #
import argparse import argparse
import sys
import logging import logging
import shutil
import sys
from find_build_apps import BuildItem, BuildError, setup_logging, BUILD_SYSTEMS from find_build_apps import BuildItem, BuildError, setup_logging, BUILD_SYSTEMS
@@ -75,10 +77,9 @@ def main():
setup_logging(args) setup_logging(args)
build_items = [BuildItem.from_json(line) for line in args.build_list] build_items = [BuildItem.from_json(line) for line in args.build_list]
if not build_items: if not build_items:
logging.error("Empty build list!") logging.warning("Empty build list")
raise SystemExit(1) SystemExit(0)
num_builds = len(build_items) num_builds = len(build_items)
num_jobs = args.parallel_count num_jobs = args.parallel_count
@@ -117,6 +118,11 @@ def main():
failed_builds.append(build_info) failed_builds.append(build_info)
else: else:
raise SystemExit(1) 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: if failed_builds:
logging.error("The following build have failed:") logging.error("The following build have failed:")

View File

@@ -31,6 +31,7 @@ die() {
[ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set" [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set"
[ -z ${IDF_TARGET} ] && die "IDF_TARGET 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 ${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 ${LOG_PATH} ] || mkdir -p ${LOG_PATH}
[ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_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. # 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 \ -vv \
--format json \ --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" \ --work-dir "${BUILD_PATH}/@f/@w/@t" \
--build-dir build \ --build-dir build \
--build-log "${LOG_PATH}/@f_@w.txt" \ --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=default' \
--config 'sdkconfig.ci.*=' \ --config 'sdkconfig.ci.*=' \
--config '=default' \ --config '=default' \
--app-list ${SCAN_EXAMPLE_TEST_JSON}
# --config rules above explained: # --config rules above explained:
# 1. If sdkconfig.ci exists, use it build the example with configuration name "default" # 1. If sdkconfig.ci exists, use it build the example with configuration name "default"

View File

@@ -29,6 +29,7 @@ die() {
[ -z ${LOG_PATH} ] && die "LOG_PATH is not set" [ -z ${LOG_PATH} ] && die "LOG_PATH is not set"
[ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set" [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set"
[ -z ${IDF_TARGET} ] && die "IDF_TARGET 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 ${LOG_PATH} ] || mkdir -p ${LOG_PATH}
[ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_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. # 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 \ -vv \
--format json \ --format json \
--build-system cmake \
--target ${IDF_TARGET} \
--recursive \
--work-dir "${BUILD_PATH}/@f/@w/@t" \ --work-dir "${BUILD_PATH}/@f/@w/@t" \
--build-dir build \ --build-dir build \
--build-log "${LOG_PATH}/@f_@w.txt" \ --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=default' \
--config 'sdkconfig.ci.*=' \ --config 'sdkconfig.ci.*=' \
--config '=default' \ --config '=default' \
--app-list ${SCAN_CUSTOM_TEST_JSON}
# --config rules above explained: # --config rules above explained:
# 1. If sdkconfig.ci exists, use it build the example with configuration name "default" # 1. If sdkconfig.ci exists, use it build the example with configuration name "default"

View File

@@ -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 # 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. # (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 \ -vv \
--format json \ --format json \
--build-system cmake \ --build-system cmake \

View File

@@ -6,11 +6,11 @@
# log files for every build. # log files for every build.
# Exits with a non-zero exit code if any warning is found. # Exits with a non-zero exit code if any warning is found.
import os
import sys
import argparse import argparse
import logging import logging
import os
import re import re
import sys
try: try:
from find_build_apps import BuildItem, setup_logging from find_build_apps import BuildItem, setup_logging
@@ -70,10 +70,9 @@ def main():
setup_logging(args) setup_logging(args)
build_items = [BuildItem.from_json(line) for line in args.build_list] build_items = [BuildItem.from_json(line) for line in args.build_list]
if not build_items: if not build_items:
logging.error("Empty build list!") logging.warning("Empty build list")
raise SystemExit(1) SystemExit(0)
found_warnings = 0 found_warnings = 0
for build_item in build_items: for build_item in build_items:

View File

@@ -82,6 +82,8 @@ build_esp_idf_tests_cmake_esp32s2:
artifacts: artifacts:
when: always when: always
expire_in: 4 days expire_in: 4 days
variables:
SCAN_EXAMPLE_TEST_JSON: ${CI_PROJECT_DIR}/examples/test_configs/scan_${IDF_TARGET}_${EXAMPLE_TEST_BUILD_SYSTEM}.json
only: only:
# Here both 'variables' and 'refs' conditions are given. They are combined with "AND" logic. # Here both 'variables' and 'refs' conditions are given. They are combined with "AND" logic.
variables: variables:
@@ -96,9 +98,6 @@ build_esp_idf_tests_cmake_esp32s2:
- mkdir ${BUILD_PATH} - mkdir ${BUILD_PATH}
- mkdir -p ${LOG_PATH} - mkdir -p ${LOG_PATH}
- ${IDF_PATH}/tools/ci/build_examples.sh - ${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: build_examples_make:
extends: .build_examples_template extends: .build_examples_template
@@ -126,6 +125,8 @@ build_examples_make:
# same as above, but for CMake # same as above, but for CMake
.build_examples_cmake: &build_examples_cmake .build_examples_cmake: &build_examples_cmake
extends: .build_examples_template extends: .build_examples_template
dependencies:
- scan_tests
artifacts: artifacts:
paths: paths:
- build_examples/list.json - build_examples/list.json
@@ -156,6 +157,8 @@ build_examples_cmake_esp32s2:
.build_test_apps: &build_test_apps .build_test_apps: &build_test_apps
extends: .build_template extends: .build_template
stage: build stage: build
dependencies:
- scan_tests
artifacts: artifacts:
when: always when: always
paths: paths:
@@ -171,8 +174,10 @@ build_examples_cmake_esp32s2:
- $LOG_PATH - $LOG_PATH
expire_in: 3 days expire_in: 3 days
variables: variables:
LOG_PATH: "$CI_PROJECT_DIR/log_test_apps" LOG_PATH: "${CI_PROJECT_DIR}/log_test_apps"
BUILD_PATH: "$CI_PROJECT_DIR/build_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: only:
variables: variables:
- $BOT_TRIGGER_WITH_LABEL == null - $BOT_TRIGGER_WITH_LABEL == null

View File

@@ -191,3 +191,33 @@ check_public_headers:
script: script:
- python tools/ci/check_public_headers.py --jobs 4 --prefix xtensa-esp32-elf- - 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

View File

@@ -189,16 +189,15 @@ class AssignTest(object):
job_list.sort(key=lambda x: x["name"]) job_list.sort(key=lambda x: x["name"])
return job_list 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. :param case_filter: filter for test cases. the filter to use is default filter updated with case_filter param.
:return: filtered test case list :return: filtered test case list
""" """
_case_filter = self.DEFAULT_FILTER.copy() _case_filter = self.DEFAULT_FILTER.copy()
if case_filter: if case_filter:
_case_filter.update(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) return CaseConfig.filter_test_cases(test_methods, _case_filter)
def _group_cases(self): def _group_cases(self):
@@ -287,7 +286,7 @@ class AssignTest(object):
failed_to_assign = [] failed_to_assign = []
assigned_groups = [] assigned_groups = []
case_filter = self._apply_bot_filter() 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() self._apply_bot_test_count()
test_groups = self._group_cases() test_groups = self._group_cases()

View File

@@ -50,6 +50,7 @@ class Search(object):
for i, test_function in enumerate(test_functions_out): for i, test_function in enumerate(test_functions_out):
print("\t{}. ".format(i + 1) + test_function.case_info["name"]) 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 return test_functions_out
@classmethod @classmethod
@@ -124,6 +125,7 @@ class Search(object):
search all test cases from a folder or file, and then do case replicate. 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: test case file(s) path
:param test_case_file_pattern: unix filename pattern
:return: a list of replicated test methods :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) test_case_files = cls._search_test_case_files(test_case, test_case_file_pattern or cls.TEST_CASE_FILE_PATTERN)

View File

@@ -139,7 +139,7 @@ class UnitTestAssignTest(CIAssignTest.AssignTest):
def __init__(self, test_case_path, ci_config_file): def __init__(self, test_case_path, ci_config_file):
CIAssignTest.AssignTest.__init__(self, test_case_path, ci_config_file, case_group=Group) 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. 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. 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 return test_cases
test_cases = [] test_cases = []
if os.path.isdir(test_case_path): if os.path.isdir(self.test_case_path):
for yml_file in find_by_suffix('.yml', 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)) test_cases.extend(get_test_cases_from_yml(yml_file))
elif os.path.isfile(test_case_path): elif os.path.isfile(self.test_case_path):
test_cases.extend(get_test_cases_from_yml(test_case_path)) test_cases.extend(get_test_cases_from_yml(self.test_case_path))
else: else:
print("Test case path is invalid. Should only happen when use @bot to skip unit test.") print("Test case path is invalid. Should only happen when use @bot to skip unit test.")

View File

@@ -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())
'''
{
<target>: {
'test_case_apps': [<app_dir>], # which is used in target tests
'standalone_apps': [<app_dir>], # 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()

View File

@@ -5,12 +5,15 @@
# Produces the list of builds. The list can be consumed by build_apps.py, which performs the actual builds. # Produces the list of builds. The list can be consumed by build_apps.py, which performs the actual builds.
import argparse import argparse
import os
import sys
import re
import glob import glob
import json
import logging import logging
import os
import re
import sys
import typing import typing
from find_build_apps import ( from find_build_apps import (
BUILD_SYSTEMS, BUILD_SYSTEMS,
BUILD_SYSTEM_CMAKE, BUILD_SYSTEM_CMAKE,
@@ -22,8 +25,8 @@ from find_build_apps import (
DEFAULT_TARGET, DEFAULT_TARGET,
) )
# Helper functions
# Helper functions
def dict_from_sdkconfig(path): def dict_from_sdkconfig(path):
""" """
@@ -45,9 +48,9 @@ def dict_from_sdkconfig(path):
# Main logic: enumerating apps and builds # Main logic: enumerating apps and builds
def find_builds_for_app( def find_builds_for_app(app_path, work_dir, build_dir, build_log, target_arg,
app_path, work_dir, build_dir, build_log, target_arg, build_system, build_system, config_rules, preserve_artifacts=True):
config_rules): # type: (str, str, str, str, str, str, typing.List[ConfigRule]) -> typing.List[BuildItem] # 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 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) :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. a different CONFIG_IDF_TARGET value.
:param build_system: name of the build system, index into BUILD_SYSTEMS dictionary :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 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 :return: list of BuildItems representing build configuration of the app
""" """
build_items = [] # type: typing.List[BuildItem] build_items = [] # type: typing.List[BuildItem]
@@ -104,6 +108,7 @@ def find_builds_for_app(
sdkconfig_path, sdkconfig_path,
config_name, config_name,
build_system, build_system,
preserve_artifacts,
)) ))
if not build_items: if not build_items:
@@ -118,14 +123,15 @@ def find_builds_for_app(
None, None,
default_config_name, default_config_name,
build_system, build_system,
preserve_artifacts,
) )
] ]
return build_items return build_items
def find_apps(build_system_class, path, recursive, exclude_list, def find_apps(build_system_class, path, recursive, exclude_list, target):
target): # type: (typing.Type[BuildSystem], str, bool, typing.List[str], str) -> typing.List[str] # 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 Find app directories in path (possibly recursively), which contain apps for the given build system, compatible
with the given target. with the given target.
@@ -189,7 +195,10 @@ def main():
action="store_true", action="store_true",
help="Look for apps in the specified directories recursively.", 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( parser.add_argument(
"--work-dir", "--work-dir",
help="If set, the app is first copied into the specified directory, and then built." + help="If set, the app is first copied into the specified directory, and then built." +
@@ -232,12 +241,29 @@ def main():
type=argparse.FileType("w"), type=argparse.FileType("w"),
help="Output the list of builds to the specified file", 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() args = parser.parse_args()
setup_logging(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 the build target is not set explicitly, get it from the environment or use the default one (esp32)
if not args.target: if not args.target:
env_target = os.environ.get("IDF_TARGET") env_target = os.environ.get("IDF_TARGET")
@@ -247,37 +273,52 @@ def main():
else: else:
logging.info("--target argument not set, using IDF_TARGET={} as the default".format(DEFAULT_TARGET)) logging.info("--target argument not set, using IDF_TARGET={} as the default".format(DEFAULT_TARGET))
args.target = 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.')
# Prepare the list of app paths # Prepare the list of app paths, try to read from the scan_tests result.
app_paths = [] # type: typing.List[str] # 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: for path in args.paths:
app_paths += find_apps(build_system_class, path, args.recursive, args.exclude or [], args.target) 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]
if not app_paths: if not apps:
logging.critical("No {} apps found".format(build_system_class.NAME)) logging.warning("No apps found")
raise SystemExit(1) SystemExit(0)
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 # Find compatible configurations of each app, collect them as BuildItems
build_items = [] # type: typing.List[BuildItem] build_items = [] # type: typing.List[BuildItem]
config_rules = config_rules_from_str(args.config or []) 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( build_items += find_builds_for_app(
app_path, app["app_dir"],
args.work_dir, args.work_dir,
args.build_dir, args.build_dir,
args.build_log, args.build_log,
args.target, args.target or app["target"],
args.build_system, args.build_system or app["build_system"],
config_rules, config_rules,
app["preserve"],
) )
logging.info("Found {} builds".format(len(build_items))) logging.info("Found {} builds".format(len(build_items)))
# Write out the BuildItems. Only JSON supported now (will add YAML later). # Write out the BuildItems. Only JSON supported now (will add YAML later).
if args.format != "json": if args.format != "json":
raise NotImplementedError() raise NotImplementedError()
out = args.output or sys.stdout out = args.output or sys.stdout
out.writelines([item.to_json() + "\n" for item in build_items]) out.writelines([item.to_json() + "\n" for item in build_items])

View File

@@ -93,3 +93,33 @@ class CMakeBuildSystem(BuildSystem):
if CMAKE_PROJECT_LINE not in cmakelists_file_content: if CMAKE_PROJECT_LINE not in cmakelists_file_content:
return False return False
return True 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

View File

@@ -71,6 +71,7 @@ class BuildItem(object):
sdkconfig_path, sdkconfig_path,
config_name, config_name,
build_system, build_system,
preserve_artifacts,
): ):
# These internal variables store the paths with environment variables and placeholders; # 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. # 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.target = target
self.build_system = build_system self.build_system = build_system
self.preserve = preserve_artifacts
self._app_name = os.path.basename(os.path.normpath(app_path)) self._app_name = os.path.basename(os.path.normpath(app_path))
# Some miscellaneous build properties which are set later, at the build stage # Some miscellaneous build properties which are set later, at the build stage
@@ -155,6 +158,7 @@ class BuildItem(object):
"config": self.config_name, "config": self.config_name,
"target": self.target, "target": self.target,
"verbose": self.verbose, "verbose": self.verbose,
"preserve": self.preserve,
}) })
@staticmethod @staticmethod
@@ -172,6 +176,7 @@ class BuildItem(object):
config_name=d["config"], config_name=d["config"],
target=d["target"], target=d["target"],
build_system=d["build_system"], build_system=d["build_system"],
preserve_artifacts=d["preserve"]
) )
result.verbose = d["verbose"] result.verbose = d["verbose"]
return result return result
@@ -332,34 +337,9 @@ class BuildSystem(object):
return readme_file.read() return readme_file.read()
@staticmethod @staticmethod
@abstractmethod
def supported_targets(app_path): def supported_targets(app_path):
formal_to_usual = { pass
'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
class BuildError(RuntimeError): class BuildError(RuntimeError):

View File

@@ -1,8 +1,8 @@
import logging import logging
import os import os
import shlex
import subprocess import subprocess
import sys import sys
import shlex
from .common import BuildSystem, BuildError from .common import BuildSystem, BuildError
@@ -58,3 +58,7 @@ class MakeBuildSystem(BuildSystem):
if MAKE_PROJECT_LINE not in makefile_content: if MAKE_PROJECT_LINE not in makefile_content:
return False return False
return True return True
@staticmethod
def supported_targets(app_path):
return ['esp32']