From abc963d6ca646c6728619d6df07c175b0b02fbec Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Mon, 15 Jun 2026 10:17:52 +0200 Subject: [PATCH 1/6] ci(buildv2): add build/QEMU/Linux jobs for Build system v2 Add the parallel set of CI jobs that exercise the cmakev2 path end-to-end via the IDF_BUILD_V2 shim. All jobs are gated on the `buildv2` MR label with `allow_failure: true` so unrelated MRs do not pay for the extra build matrix. - `.gitlab/ci/build.yml`: add the buildv2 child build pipeline that re-runs the full app build matrix with IDF_BUILD_V2=y, plus the Windows buildv2 build job. The generator script injects IDF_BUILD_V2 and a PIPELINE_COMMIT_SHA suffix into each child job and broadens target_test job filters so v1 and buildv2 artifacts upload to distinct s3 cache paths and each child pipeline tests its own binary. - `.gitlab/ci/host-test.yml`: add the QEMU (esp32, esp32c3) and Linux pytest buildv2 jobs. - `.idf_build_apps.toml`: parameterize build_dir with BUILDV2_DIR_SUFFIX (set to _v2 only on the buildv2 pipeline). --- .gitlab/ci/build.yml | 71 +++++++++++++++++ .gitlab/ci/host-test.yml | 58 +++++++++++--- .idf_build_apps.toml | 5 +- .../scripts/patch_buildv2_child_pipeline.py | 76 +++++++++++++++++++ .../patch_buildv2_target_test_pipeline.py | 56 ++++++++++++++ 5 files changed, 254 insertions(+), 12 deletions(-) create mode 100644 tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py create mode 100644 tools/ci/dynamic_pipelines/scripts/patch_buildv2_target_test_pipeline.py diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index d0ce66c0c5b..55d4823f429 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -128,6 +128,77 @@ build_child_pipeline: job: generate_build_child_pipeline strategy: depend +# Generates a separate child-pipeline YAML for the cmakev2 build system. +# The build_dir stays identical to cmakev1 (build__); v1/v2 +# binaries are kept separate in s3 by suffixing PIPELINE_COMMIT_SHA — see +# the python injection below — which idf-ci uses unmodified in its s3 +# prefix (project//...). Keeping build_dir identical preserves the +# upstream pytest matching that uses app.build_path to filter test cases. +generate_build_child_pipeline_buildv2: + extends: + - .build_template + - .rules:labels:buildv2 + tags: [fast_run, shiny] + variables: + # The build child pipeline's app list is fixed here, at generation time. + # Set IDF_BUILD_V2 so that manifest `disable: if IDF_BUILD_V2 == "1"` rules + # take effect during app discovery; otherwise those apps (e.g. kasan_test, + # IDF-15864) are discovered, built and target-tested even though the build + # jobs run with IDF_BUILD_V2 injected by patch_buildv2_child_pipeline.py. + IDF_BUILD_V2: "1" + dependencies: # set dependencies to null to avoid missing artifacts issue + needs: + - pipeline_variables + - job: pre_check_complete + artifacts: false + - job: baseline_manifest_sha + optional: true + artifacts: + paths: + - buildv2_child_pipeline.yml + - test_related_apps.txt + - non_test_related_apps.txt + expire_in: 1 week + when: always + script: + - idf-ci --debug gitlab build-child-pipeline + -p components + -p examples + -p tools/test_apps + --modified-files "$MR_MODIFIED_FILES" + buildv2_child_pipeline.yml > idfci_buildv2_gen.log 2>&1 + - tail -20 idfci_buildv2_gen.log + - shuf test_related_apps.txt -o test_related_apps.txt + - shuf non_test_related_apps.txt -o non_test_related_apps.txt + # Patch the generated yaml to activate the cmakev2 shim and route v2 build + # and target_test artifacts to a distinct s3 namespace. See the script + # docstring for the rationale. + - python tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py buildv2_child_pipeline.yml + - 'echo "=== buildv2_child_pipeline.yml (post-injection) ==="; cat buildv2_child_pipeline.yml; echo "=== end ==="' + +build_child_pipeline_buildv2: + stage: build + extends: + - .rules:labels:buildv2 + needs: + - job: fast_build_check + optional: true + artifacts: false + - pipeline_variables + - generate_build_child_pipeline_buildv2 + variables: + MR_MODIFIED_COMPONENTS: $MR_MODIFIED_COMPONENTS + MR_MODIFIED_FILES: $MR_MODIFIED_FILES + PARENT_PIPELINE_ID: $CI_PIPELINE_ID + inherit: + variables: false + trigger: + include: + - artifact: buildv2_child_pipeline.yml + job: generate_build_child_pipeline_buildv2 + strategy: depend + allow_failure: true + generate_prebuild_report: extends: - .build_template diff --git a/.gitlab/ci/host-test.yml b/.gitlab/ci/host-test.yml index bdac7f2a340..8699aa14070 100644 --- a/.gitlab/ci/host-test.yml +++ b/.gitlab/ci/host-test.yml @@ -353,7 +353,7 @@ build_docker: ############################ # Host test with test apps # ############################ -test_pytest_qemu: +.test_pytest_qemu_template: extends: - .host_test_template artifacts: @@ -363,15 +363,6 @@ test_pytest_qemu: - "**/build*/*.bin" reports: junit: XUNIT_RESULT.xml - parallel: - matrix: - - IDF_TARGET: "esp32" - INSTALL_EXTRA_TOOLS: "qemu-xtensa" - # Skip Clang + Xtensa tests due to bootloader size issue - IDF_TOOLCHAIN: [gcc] - - IDF_TARGET: "esp32c3" - INSTALL_EXTRA_TOOLS: "qemu-riscv32" - IDF_TOOLCHAIN: [gcc, clang] script: - run_cmd idf-ci build run --build-system cmake @@ -386,7 +377,20 @@ test_pytest_qemu: --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} --qemu-extra-args \"-global driver=timer.$IDF_TARGET.timg,property=wdt_disable,value=true\" -test_pytest_linux: +test_pytest_qemu: + extends: + - .test_pytest_qemu_template + parallel: + matrix: + - IDF_TARGET: "esp32" + INSTALL_EXTRA_TOOLS: "qemu-xtensa" + # Skip Clang + Xtensa tests due to bootloader size issue + IDF_TOOLCHAIN: [gcc] + - IDF_TARGET: "esp32c3" + INSTALL_EXTRA_TOOLS: "qemu-riscv32" + IDF_TOOLCHAIN: [gcc, clang] + +.test_pytest_linux_template: extends: - .host_test_template artifacts: @@ -410,6 +414,10 @@ test_pytest_linux: --junitxml=XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} +test_pytest_linux: + extends: + - .test_pytest_linux_template + test_pytest_macos: extends: - .host_test_template @@ -516,6 +524,34 @@ pytest_buildv2_system: --junitxml ${CI_PROJECT_DIR}/XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} +# Run QEMU tests with the cmakev2 build system (via the IDF_BUILD_V2 shim). +# Triggered only when the 'buildv2' MR label is set. +test_pytest_qemu_buildv2: + extends: + - .test_pytest_qemu_template + - .rules:labels:buildv2 + allow_failure: true + parallel: + matrix: + - IDF_TARGET: "esp32" + INSTALL_EXTRA_TOOLS: "qemu-xtensa" + IDF_TOOLCHAIN: [gcc] + - IDF_TARGET: "esp32c3" + INSTALL_EXTRA_TOOLS: "qemu-riscv32" + IDF_TOOLCHAIN: [gcc] + variables: + IDF_BUILD_V2: "1" + +# Run Linux-target pytest tests with the cmakev2 build system (via the +# IDF_BUILD_V2 shim). Triggered only when the 'buildv2' MR label is set. +test_pytest_linux_buildv2: + extends: + - .test_pytest_linux_template + - .rules:labels:buildv2 + allow_failure: true + variables: + IDF_BUILD_V2: "1" + pytest_build_system_macos: extends: - .test_build_system_template diff --git a/.idf_build_apps.toml b/.idf_build_apps.toml index e299567bfc9..59ecf1c658a 100644 --- a/.idf_build_apps.toml +++ b/.idf_build_apps.toml @@ -19,7 +19,10 @@ ignore_warning_files = [ '$IDF_PATH/tools/ci/ignore_build_warnings.txt', ] -build_dir = "build_@t_@w" +# BUILDV2_DIR_SUFFIX is expanded by idf-build-apps from the environment; +# empty for buildv1 builds and set to "_v2" by the buildv2 CI pipelines so +# buildv1 and buildv2 build artifacts do not share the same s3 cache path. +build_dir = "build${BUILDV2_DIR_SUFFIX}_@t_@w" build_log_filename = "build_log.txt" size_json_filename = "size_${CI_JOB_ID}.json" diff --git a/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py new file mode 100644 index 00000000000..2a791ecfc37 --- /dev/null +++ b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +"""Patch the generated Build system v2 build child pipeline in place. + +The buildv2 build child pipeline is generated by ``idf-ci gitlab build-child-pipeline`` +from the same manifest as the default pipeline. This script post-processes that +generated YAML so the cmakev2 path is exercised end to end: + +1. Inject ``IDF_BUILD_V2`` into each child build job so cmake activates the + cmakev2 shim. + +2. Inject ``PIPELINE_COMMIT_SHA: ${PIPELINE_COMMIT_SHA}_buildv2`` into each child + build job. idf-ci reads ``PIPELINE_COMMIT_SHA`` when computing the s3 + upload/download prefix (``project//...``). Suffixing it for v2 jobs only + routes v2 binaries to a distinct s3 key namespace without renaming build_dir. + +3. Redefine ``generate_pytest_child_pipeline`` so its script also runs + ``patch_buildv2_target_test_pipeline.py`` against the emitted + ``target_test_child_pipeline.yml`` and injects the same + ``PIPELINE_COMMIT_SHA`` override into each target_test job. Without this, the + v2 target_test pipeline would still download from the default pipeline's s3 + keys (race-determined, mostly cmakev1 binaries). +""" + +import argparse + +import yaml + +PIPELINE_COMMIT_SHA_V2 = '${PIPELINE_COMMIT_SHA}_buildv2' + + +def patch(path: str) -> None: + with open(path) as f: + d = yaml.safe_load(f) + + injected = [] + for k, v in d.items(): + if isinstance(v, dict) and 'extends' in v: + v.setdefault('variables', {})['IDF_BUILD_V2'] = '1' + v['variables']['PIPELINE_COMMIT_SHA'] = PIPELINE_COMMIT_SHA_V2 + injected.append(k) + + # Redefine generate_pytest_child_pipeline (included from + # tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml) so its script + # also rewrites target_test_child_pipeline.yml. GitLab CI merges values at the + # keyword level, so only `script` is overridden; `needs`, `artifacts`, + # `image`, etc. come from the included version. + d['generate_pytest_child_pipeline'] = { + 'script': [ + 'python tools/ci/dynamic_pipelines/scripts/generate_target_test_child_pipeline.py', + 'python tools/ci/dynamic_pipelines/scripts/patch_buildv2_target_test_pipeline.py ' + 'target_test_child_pipeline.yml', + ], + } + + with open(path, 'w') as f: + yaml.safe_dump(d, f, sort_keys=False) + + print('Injected IDF_BUILD_V2 + PIPELINE_COMMIT_SHA into build jobs:', injected) + print('Redefined generate_pytest_child_pipeline with post-processing step') + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + 'pipeline_yaml', + nargs='?', + default='buildv2_child_pipeline.yml', + help='Path to the generated buildv2 build child pipeline YAML to patch in place.', + ) + args = parser.parse_args() + patch(args.pipeline_yaml) + + +if __name__ == '__main__': + main() diff --git a/tools/ci/dynamic_pipelines/scripts/patch_buildv2_target_test_pipeline.py b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_target_test_pipeline.py new file mode 100644 index 00000000000..bb449f0ac7b --- /dev/null +++ b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_target_test_pipeline.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +"""Patch the generated Build system v2 target_test child pipeline in place. + +Inject ``PIPELINE_COMMIT_SHA: ${PIPELINE_COMMIT_SHA}_buildv2`` into every +target_test job so idf-ci downloads the cmakev2 binaries from the distinct s3 +key namespace produced by the v2 build jobs, instead of racing with the default +pipeline's binaries. + +Run from generate_pytest_child_pipeline (see patch_buildv2_child_pipeline.py), +after generate_target_test_child_pipeline.py has emitted the YAML. +""" + +import argparse + +import yaml + +PIPELINE_COMMIT_SHA_V2 = '${PIPELINE_COMMIT_SHA}_buildv2' + +# Target test jobs use extends: .dynamic_target_test_template, so their script: +# comes from the template, not their own definition. These top-level keys are +# pipeline configuration rather than jobs and must be skipped. +SKIP_KEYS = {'include', 'workflow', 'default', 'variables', 'stages'} + + +def patch(path: str) -> None: + with open(path) as f: + d = yaml.safe_load(f) + + modified = 0 + for k, v in d.items(): + if k in SKIP_KEYS or not isinstance(v, dict): + continue + v.setdefault('variables', {})['PIPELINE_COMMIT_SHA'] = PIPELINE_COMMIT_SHA_V2 + modified += 1 + + with open(path, 'w') as f: + yaml.safe_dump(d, f, sort_keys=False) + + print(f'Injected PIPELINE_COMMIT_SHA override into {modified} target_test jobs') + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + 'pipeline_yaml', + nargs='?', + default='target_test_child_pipeline.yml', + help='Path to the generated target_test child pipeline YAML to patch in place.', + ) + args = parser.parse_args() + patch(args.pipeline_yaml) + + +if __name__ == '__main__': + main() From 2119b3c7d2cb00cdffe9afa413fa30b0aa67b9f4 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Mon, 15 Jun 2026 11:03:26 +0200 Subject: [PATCH 2/6] fix(cmakev2/kconfig): re-resolve SDKCONFIG_DEFAULTS lazily under Build system v1 shim The cmakev2 path snapshots SDKCONFIG_DEFAULTS once during early init. Build system v1 apps that mutate SDKCONFIG_DEFAULTS after `include(project.cmake)` but before `project()` (e.g. `list(PREPEND SDKCONFIG_DEFAULTS ...)` to inject tools/test_apps/configs/sdkconfig.debug_helpers) miss those mutations when the shim drives the cmakev2 path, because the snapshot was taken before the mutation. Re-resolve SDKCONFIG_DEFAULTS at sdkconfig-generation time when __V1_COMPAT_SHIM is active so the late-mutated value is honored. Native cmakev2 apps still use the property-based contract via idf_build_set_property(SDKCONFIG_DEFAULTS ... APPEND). --- tools/cmakev2/kconfig.cmake | 87 +++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/tools/cmakev2/kconfig.cmake b/tools/cmakev2/kconfig.cmake index 58fa411d1f9..e79a2124d48 100644 --- a/tools/cmakev2/kconfig.cmake +++ b/tools/cmakev2/kconfig.cmake @@ -36,35 +36,21 @@ set(MENUCONFIG_INLINE_MIN_KCONFIG_VERSION "3.9.0") function(__init_kconfig) idf_build_get_property(idf_path IDF_PATH) - # Initialize SDKCONFIG and SDKCONFIG_DEFAULTS build properties using environment - # variables, CMake cache variables, or default values. - if(EXISTS "${CMAKE_SOURCE_DIR}/sdkconfig.defaults") - set(sdkconfig_defaults "${CMAKE_SOURCE_DIR}/sdkconfig.defaults") - else() - set(sdkconfig_defaults "") - endif() - + # Initialize the SDKCONFIG file path build property. __get_default_value(VARIABLE SDKCONFIG DEFAULT "${CMAKE_SOURCE_DIR}/sdkconfig" OUTPUT sdkconfig) - __get_default_value(VARIABLE SDKCONFIG_DEFAULTS - DEFAULT "${sdkconfig_defaults}" - OUTPUT sdkconfig_defaults) - __get_absolute_paths(PATHS "${sdkconfig}" OUTPUT sdkconfig) - __get_absolute_paths(PATHS "${sdkconfig_defaults}" OUTPUT sdkconfig_defaults) - - set(sdkconfig_defaults_checked "") - foreach(sdkconfig_default ${sdkconfig_defaults}) - if(NOT EXISTS "${sdkconfig_default}") - idf_die("SDKCONFIG_DEFAULTS '${sdkconfig_default}' does not exist.") - endif() - list(APPEND sdkconfig_defaults_checked ${sdkconfig_default}) - endforeach() - idf_build_set_property(SDKCONFIG "${sdkconfig}") idf_build_set_property(__SDKCONFIG_ORIG "${sdkconfig}") - idf_build_set_property(SDKCONFIG_DEFAULTS "${sdkconfig_defaults_checked}") + + # Initialize the SDKCONFIG_DEFAULTS build property. __init_idf_target() + # reads this property to detect the target from the defaults files, so it + # must be resolved here. It is resolved again from __generate_sdkconfig() + # to pick up a SDKCONFIG_DEFAULTS variable assigned after + # include(project.cmake) but before project(). + __resolve_sdkconfig_defaults() + idf_build_set_property(GENERATE_SDKCONFIG 1) # Setup ESP-IDF root Kconfig and sdkconfig.rename files. @@ -78,6 +64,54 @@ function(__init_kconfig) idf_build_set_property(CONFIG_DIR "${config_dir}") endfunction() +#[[ + __resolve_sdkconfig_defaults() + + Resolve SDKCONFIG_DEFAULTS into a list of absolute, existing paths + and store it on the SDKCONFIG_DEFAULTS build property. + + A SDKCONFIG_DEFAULTS variable (or environment variable) takes precedence + and is (re)resolved onto the build property. Otherwise a value already + accumulated on the build property (e.g. via + ``idf_build_set_property(SDKCONFIG_DEFAULTS "" APPEND)``) is + preserved, and only when nothing has been set does it fall back to the + project's sdkconfig.defaults file. This makes the helper idempotent and + safe to call both early (from __init_kconfig) and again at + sdkconfig-generation time. +#]] +function(__resolve_sdkconfig_defaults) + if(NOT DEFINED SDKCONFIG_DEFAULTS AND NOT DEFINED ENV{SDKCONFIG_DEFAULTS}) + # No SDKCONFIG_DEFAULTS variable override: keep a value already set on + # the build property; only fall back to the default when unset. + idf_build_get_property(existing SDKCONFIG_DEFAULTS) + if(existing) + return() + endif() + endif() + + if(EXISTS "${CMAKE_SOURCE_DIR}/sdkconfig.defaults") + set(sdkconfig_defaults_default "${CMAKE_SOURCE_DIR}/sdkconfig.defaults") + else() + set(sdkconfig_defaults_default "") + endif() + + __get_default_value(VARIABLE SDKCONFIG_DEFAULTS + DEFAULT "${sdkconfig_defaults_default}" + OUTPUT sdkconfig_defaults) + + __get_absolute_paths(PATHS "${sdkconfig_defaults}" OUTPUT sdkconfig_defaults) + + set(sdkconfig_defaults_checked "") + foreach(sdkconfig_default IN LISTS sdkconfig_defaults) + if(NOT EXISTS "${sdkconfig_default}") + idf_die("SDKCONFIG_DEFAULTS '${sdkconfig_default}' does not exist.") + endif() + list(APPEND sdkconfig_defaults_checked "${sdkconfig_default}") + endforeach() + + idf_build_set_property(SDKCONFIG_DEFAULTS "${sdkconfig_defaults_checked}") +endfunction() + #[[ __create_sdkconfig_orig_copy() @@ -187,6 +221,13 @@ endfunction() 4. Generate all output files (sdkconfig.h, sdkconfig.cmake, etc.) #]] function(__generate_sdkconfig) + # Re-resolve SDKCONFIG_DEFAULTS so that a value assigned to the + # SDKCONFIG_DEFAULTS variable after include(project.cmake) but before + # project() is honored. The helper is idempotent: it only re-reads when + # the SDKCONFIG_DEFAULTS variable (or environment variable) is set, and + # otherwise leaves a value accumulated on the build property untouched. + __resolve_sdkconfig_defaults() + # Collect Kconfig files from discovered components __consolidate_component_kconfig_files() From fc7eaf85696aca3b4cd7bfdbd507d0b4cc581438 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Fri, 12 Jun 2026 07:52:59 +0200 Subject: [PATCH 3/6] test(gcov): resolve component CMakeFiles dir dynamically pytest_gcov hardcoded `__idf_.dir/` as the per-component CMake intermediate directory where each .gcda is expected to land. The directory name is build-system specific (Build system v1 uses `__idf_.dir`, Build system v2 uses `_idf_.dir`). Asserting on a single literal made every test_gcov_uart run fail under Build system v2 even though the chip was streaming valid coverage data and the host-side UartGcovCapture was writing the files to the build's actual layout. Probe `CMakeFiles/*_.dir/` and prefer the candidate whose .gcno files were emitted by the active build, so the assertion follows the build output rather than dictating it. --- examples/system/gcov/pytest_gcov.py | 82 +++++++++++++++++++---------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/examples/system/gcov/pytest_gcov.py b/examples/system/gcov/pytest_gcov.py index 10c37b7138e..fa67e5b28d1 100644 --- a/examples/system/gcov/pytest_gcov.py +++ b/examples/system/gcov/pytest_gcov.py @@ -25,44 +25,68 @@ except ImportError: from gcov_capture import get_coverage_data +def _candidate_target_dirs(binary_path: str, component: str) -> list: + """Per-component CMakeFiles/.dir/ directories used by either build system. + + Build system v1 emits ``__idf_.dir`` (double underscore); Build system + v2 emits ``_idf_.dir`` (single underscore). Both candidates are returned + so the caller can probe either layout without knowing which build system produced + the binary. + """ + parent = os.path.join(binary_path, 'esp-idf', component, 'CMakeFiles') + return [ + os.path.join(parent, f'__idf_{component}.dir'), + os.path.join(parent, f'_idf_{component}.dir'), + ] + + +def _resolve_gcda_path(binary_path: str, component: str, basename: str) -> str: + """Return the .gcda path, preferring whichever build-system layout already has the file. + + Before the chip dumps, neither layout has a .gcda yet, so the chip-side FOPEN will + create the directory of whichever path the binary embedded. Call this again after + the dump to get the materialized path. + """ + for d in _candidate_target_dirs(binary_path, component): + candidate = os.path.join(d, basename) + if os.path.isfile(candidate): + return candidate + # Default to v2 layout; the post-dump call will return the actual path. + return os.path.join(_candidate_target_dirs(binary_path, component)[1], basename) + + def get_expected_gcda_paths(dut: IdfDut) -> list: """Get list of expected .gcda file paths for this example.""" return [ - os.path.join( - dut.app.binary_path, 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir', 'gcov_example_main.c.gcda' - ), - os.path.join( - dut.app.binary_path, 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir', 'gcov_example_func.c.gcda' - ), - os.path.join(dut.app.binary_path, 'esp-idf', 'sample', 'CMakeFiles', '__idf_sample.dir', 'some_funcs.c.gcda'), + _resolve_gcda_path(dut.app.binary_path, 'main', 'gcov_example_main.c.gcda'), + _resolve_gcda_path(dut.app.binary_path, 'main', 'gcov_example_func.c.gcda'), + _resolve_gcda_path(dut.app.binary_path, 'sample', 'some_funcs.c.gcda'), ] def prepare_test(dut: IdfDut) -> list: - """Prepare test environment: create directories and clean up old .gcda files. - - Returns list of expected .gcda file paths. - """ - # Create the generated .gcda folders - # Normally created via `idf.py build`, but in CI non-related files aren't preserved - os.makedirs(os.path.join(dut.app.binary_path, 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir'), exist_ok=True) - os.makedirs(os.path.join(dut.app.binary_path, 'esp-idf', 'sample', 'CMakeFiles', '__idf_sample.dir'), exist_ok=True) - - # Get expected paths and clean up old files - expected_gcda_paths = get_expected_gcda_paths(dut) - for gcda_path in expected_gcda_paths: - if os.path.isfile(gcda_path): + """Prepare test environment: create both candidate directories and clean stale .gcda.""" + # Pre-create both build-system layouts so the chip-side FOPEN can write to whichever + # the binary embedded, and clean stale .gcda from either layout. + for component in ('main', 'sample'): + for d in _candidate_target_dirs(dut.app.binary_path, component): + os.makedirs(d, exist_ok=True) try: - os.remove(gcda_path) - print(f'Removed old .gcda file: {os.path.basename(gcda_path)}') - except OSError as e: - print(f'Warning: Could not remove {gcda_path}: {e}') + for entry in os.listdir(d): + if entry.endswith('.gcda'): + try: + os.remove(os.path.join(d, entry)) + print(f'Removed old .gcda file: {entry}') + except OSError as e: + print(f'Warning: Could not remove {os.path.join(d, entry)}: {e}') + except OSError: + pass - return expected_gcda_paths + return get_expected_gcda_paths(dut) def _test_gcov(openocd_dut: 'OpenOCD', dut: IdfDut) -> None: - expected_gcda_paths = prepare_test(dut) + prepare_test(dut) def expect_counter_output(loop: int, timeout: int = 10) -> None: dut.expect_exact( @@ -95,6 +119,8 @@ def _test_gcov(openocd_dut: 'OpenOCD', dut: IdfDut) -> None: # Verify execution counts if expected and coverage data is available if expected_counts: + # Re-resolve gcda paths after the dump so we observe the layout the chip actually used. + expected_gcda_paths = get_expected_gcda_paths(dut) coverage = get_coverage_data(dut.app.binary_path, expected_gcda_paths) if coverage: # Only verify if detailed data is available print(f'Coverage data: {coverage}') @@ -161,7 +187,7 @@ def _test_gcov_uart(dut: IdfDut) -> None: # Close console port to free up the port for the gcov capture dut.serial.close() - expected_gcda_paths = prepare_test(dut) + prepare_test(dut) log_file = os.path.join(dut.logdir, 'gcov_uart.log') uart_port = dut.serial.port baud = dut.app.sdkconfig.get('APPTRACE_UART_BAUDRATE') @@ -178,6 +204,8 @@ def _test_gcov_uart(dut: IdfDut) -> None: # Verify each dump for dump_num, expected in enumerate(expected_counts, start=1): if uart_capture.wait_for_fstop(timeout=10.0): + # Re-resolve gcda paths after the dump so we observe the layout the chip actually used. + expected_gcda_paths = get_expected_gcda_paths(dut) coverage = get_coverage_data(dut.app.binary_path, expected_gcda_paths) if coverage: # Only verify details if coverage data is available From 0e662bfa7d6b4f3bbfdd972ffb4100278705e8a8 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Fri, 12 Jun 2026 09:28:30 +0200 Subject: [PATCH 4/6] fix(gdbinit): build elf path from EXECUTABLE_NAME, not the target name The EXECUTABLE build property holds a CMake target name, not a filesystem path. Under Build system v1 the target happens to be created as ".elf", so concatenating BUILD_DIR with EXECUTABLE produced a valid elf path by coincidence. Under Build system v2 the target is just "" and the elf is written as ".elf" via CMake's executable suffix, so the same concatenation pointed gdb at a non-existent file. Construct the path explicitly from EXECUTABLE_NAME plus the .elf suffix so the gdbinit consumer no longer depends on the producer naming its target after the output file. --- tools/cmake/gdbinit.cmake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/cmake/gdbinit.cmake b/tools/cmake/gdbinit.cmake index ceda997694f..41b7a889401 100644 --- a/tools/cmake/gdbinit.cmake +++ b/tools/cmake/gdbinit.cmake @@ -38,8 +38,9 @@ function(__generate_gdbinit) set(symbols_gdbinit_path ${gdbinit_dir}/symbols) set(py_extensions_gdbinit_path ${gdbinit_dir}/py_extensions) set(connect_gdbinit_path ${gdbinit_dir}/connect) - idf_build_get_property(PROJECT_EXECUTABLE EXECUTABLE) - set(application_elf ${BUILD_DIR}/${PROJECT_EXECUTABLE}) + # EXECUTABLE is a CMake target name; the on-disk elf is EXECUTABLE_NAME + ".elf". + idf_build_get_property(PROJECT_EXECUTABLE_NAME EXECUTABLE_NAME) + set(application_elf ${BUILD_DIR}/${PROJECT_EXECUTABLE_NAME}.elf) file(MAKE_DIRECTORY ${gdbinit_dir}) From fc4d31252648afa6702eb7e2532fff2c61243cca Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Mon, 15 Jun 2026 15:41:51 +0200 Subject: [PATCH 5/6] fix(bootloader/cmakev2): exclude all paths matching BOOTLOADER_IGNORE_EXTRA_COMPONENT Walk every directory contributed via BOOTLOADER_EXTRA_COMPONENT_DIRS (both shared and direct component paths, not only those under the application's bootloader_components/) and exclude any whose component name matches BOOTLOADER_IGNORE_EXTRA_COMPONENT. --- .../bootloader/subproject/CMakeLists_v2.txt | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/components/bootloader/subproject/CMakeLists_v2.txt b/components/bootloader/subproject/CMakeLists_v2.txt index 50bda3faa2c..b684c8bddbd 100644 --- a/components/bootloader/subproject/CMakeLists_v2.txt +++ b/components/bootloader/subproject/CMakeLists_v2.txt @@ -27,10 +27,29 @@ if(EXISTS ${PROJECT_EXTRA_COMPONENTS}) endif() if(IGNORE_EXTRA_COMPONENT) - # Prefix all entries of the list with ${PROJECT_EXTRA_COMPONENTS} absolute path - list(TRANSFORM IGNORE_EXTRA_COMPONENT - PREPEND "${PROJECT_EXTRA_COMPONENTS}/" - OUTPUT_VARIABLE EXTRA_COMPONENT_EXCLUDE_DIRS) + foreach(extra_component_dir ${EXTRA_COMPONENT_DIRS}) + # Strip trailing slash so get_filename_component(NAME) returns the + # component name rather than an empty string. + string(REGEX REPLACE "/+$" "" extra_component_dir "${extra_component_dir}") + if(EXISTS "${extra_component_dir}/CMakeLists.txt") + # BOOTLOADER_EXTRA_COMPONENT_DIRS may point directly to a single component. + get_filename_component(component "${extra_component_dir}" NAME) + if(component AND ${component} IN_LIST IGNORE_EXTRA_COMPONENT) + list(APPEND EXTRA_COMPONENT_EXCLUDE_DIRS "${extra_component_dir}") + endif() + else() + # BOOTLOADER_EXTRA_COMPONENT_DIRS may also point to a directory containing + # multiple component directories. + file(GLOB proj_components RELATIVE ${extra_component_dir} ${extra_component_dir}/*) + foreach(component ${proj_components}) + # Only directories are considered components. + if(IS_DIRECTORY "${extra_component_dir}/${component}" + AND ${component} IN_LIST IGNORE_EXTRA_COMPONENT) + list(APPEND EXTRA_COMPONENT_EXCLUDE_DIRS "${extra_component_dir}/${component}") + endif() + endforeach() + endif() + endforeach() endif() # --------------------------------------------------------------------------- From 0a89c2e432f62e62bac64ecff147d3c32ad8b9f7 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Thu, 18 Jun 2026 16:30:39 +0200 Subject: [PATCH 6/6] fix(test_apps): override config defaults unused by these tests These tests enable features they do not use -- the VFS console, Wi-Fi task core pinning, and the DS peripheral -- which shift memory layout, interrupt allocation, and peripheral access enough to fail them. Override the unused options in each test's sdkconfig and ignore the resulting unknown-symbol build warnings. --- components/esp_driver_dma/test_apps/dma/sdkconfig.defaults | 2 ++ .../mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.hmac_opaque | 3 +++ .../spi_flash/test_apps/flash_suspend/sdkconfig.ci.release | 3 +++ tools/ci/ignore_build_warnings.txt | 6 ++++++ tools/test_apps/system/esp_intr_dump/sdkconfig.defaults | 2 ++ tools/test_apps/system/panic/coredump/sdkconfig.defaults | 3 +++ 6 files changed, 19 insertions(+) create mode 100644 tools/test_apps/system/esp_intr_dump/sdkconfig.defaults diff --git a/components/esp_driver_dma/test_apps/dma/sdkconfig.defaults b/components/esp_driver_dma/test_apps/dma/sdkconfig.defaults index 1ee5d718bc2..653efb4d754 100644 --- a/components/esp_driver_dma/test_apps/dma/sdkconfig.defaults +++ b/components/esp_driver_dma/test_apps/dma/sdkconfig.defaults @@ -1,3 +1,5 @@ CONFIG_FREERTOS_HZ=1000 CONFIG_ESP_TASK_WDT_EN=n CONFIG_IDF_EXPERIMENTAL_FEATURES=y +# CONFIG_VFS_SUPPORT_IO resolves to its default (y) in this test's Kconfig closure; the resulting console I/O reduces the internal DMA heap and fails the 200 KB allocation in the "GDMA M2M Weighted Arbitration Test SRAM->SRAM" case. Disable it to free that heap. +CONFIG_VFS_SUPPORT_IO=n diff --git a/components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.hmac_opaque b/components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.hmac_opaque index b0fdf33c199..0b905e6110f 100644 --- a/components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.hmac_opaque +++ b/components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.hmac_opaque @@ -13,3 +13,6 @@ CONFIG_PARTITION_TABLE_OFFSET=0x9000 # HMAC opaque driver test CONFIG_MBEDTLS_TEST_HMAC_OPAQUE_EFUSE_KEY=y CONFIG_MBEDTLS_TEST_HMAC_OPAQUE_EFUSE_KEY_ID=0 + +# CONFIG_ESP_TLS_USE_DS_PERIPHERAL resolves to its default (y) in this test's Kconfig closure, which fails the test_mbedtls_hmac_opaque test on esp32c3. Disable it to pass; root cause still under investigation. TODO: IDF-15856 +CONFIG_ESP_TLS_USE_DS_PERIPHERAL=n diff --git a/components/spi_flash/test_apps/flash_suspend/sdkconfig.ci.release b/components/spi_flash/test_apps/flash_suspend/sdkconfig.ci.release index fba9378c3d6..60587a0a919 100644 --- a/components/spi_flash/test_apps/flash_suspend/sdkconfig.ci.release +++ b/components/spi_flash/test_apps/flash_suspend/sdkconfig.ci.release @@ -5,3 +5,6 @@ CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y # Now the runners are massively using xmc-c chips, to be removed when xmc-d goes massive production. CONFIG_SPI_FLASH_FORCE_ENABLE_XMC_C_SUSPEND=y +# The options below resolve to their defaults (enabled) in this test's Kconfig closure, which fails the test_flash_auto_suspend_generic test on esp32c5 and esp32c61. Disable them to pass; root cause still under investigation. TODO: IDF-15858 +CONFIG_VFS_SUPPORT_IO=n +CONFIG_ESP_WIFI_SLP_IRAM_OPT=n diff --git a/tools/ci/ignore_build_warnings.txt b/tools/ci/ignore_build_warnings.txt index 2dcb94a0120..7751092aa50 100644 --- a/tools/ci/ignore_build_warnings.txt +++ b/tools/ci/ignore_build_warnings.txt @@ -24,6 +24,7 @@ warning: unknown kconfig symbol 'CTRL_BTDM_MODEM_SLEEP' assigned to* warning: unknown kconfig symbol 'ESP_DEFAULT_CPU_FREQ_MHZ_80' assigned to 'y' in .*/examples/system/deep_sleep/* warning: unknown kconfig symbol 'ESP_DEFAULT_CPU_FREQ_MHZ_80' assigned to 'y' in .*/examples/system/light_sleep/* warning: unknown kconfig symbol 'ESP_DEFAULT_CPU_FREQ_MHZ_80' assigned to 'y' in .*/examples/wifi/power_save/* +warning: unknown kconfig symbol 'ESP_WIFI_SLP_IRAM_OPT' assigned to 'n' in .*/components/spi_flash/test_apps/flash_suspend/* warning: unknown kconfig symbol 'ESP32_REV_MIN_3' assigned to 'y' in .*/examples/system/ota/simple_ota_example/sdkconfig.ci.on_update_no_sb_rsa warning: unknown kconfig symbol 'ESP32_REV_MIN' assigned to '3' in .*/examples/system/ota/simple_ota_example/sdkconfig.ci.on_update_no_sb_rsa warning: unknown kconfig symbol 'ESP32H4_RTC_CLK_CAL_CYCLES' assigned to '576' in .*/examples/bluetooth/nimble/blecent/sdkconfig.defaults.esp32h4 @@ -49,3 +50,8 @@ warning: unknown kconfig symbol 'ETHERNET_PHY_RST_GPIO' assigned to '5' in .*/ex warning: unknown kconfig symbol 'ETHERNET_PHY_ADDR' assigned to '1' in .*/examples/protocols/esp_http_client/sdkconfig.ci.* warning: \S+ changed binding to STB_WEAK warning: unknown kconfig symbol 'VFS_SUPPORT_IO' assigned to 'n' in .*/tools/test_apps/system/g1_components/sdkconfig.defaults +warning: unknown kconfig symbol 'VFS_SUPPORT_IO' assigned to 'n' in .*/components/esp_driver_dma/test_apps/dma/sdkconfig\S* +warning: unknown kconfig symbol 'VFS_SUPPORT_IO' assigned to 'n' in .*/tools/test_apps/system/panic/coredump/sdkconfig\S* +warning: unknown kconfig symbol 'ESP_WIFI_TASK_PINNED_TO_CORE_1' assigned to 'y' in .*/tools/test_apps/system/esp_intr_dump/sdkconfig\S* +warning: unknown kconfig symbol 'VFS_SUPPORT_IO' assigned to 'n' in .*/components/spi_flash/test_apps/flash_suspend/sdkconfig\S* +warning: unknown kconfig symbol 'ESP_TLS_USE_DS_PERIPHERAL' assigned to 'n' in .*/components/mbedtls/test_apps/mbedtls_ut/sdkconfig\S* diff --git a/tools/test_apps/system/esp_intr_dump/sdkconfig.defaults b/tools/test_apps/system/esp_intr_dump/sdkconfig.defaults new file mode 100644 index 00000000000..7653df9c5c7 --- /dev/null +++ b/tools/test_apps/system/esp_intr_dump/sdkconfig.defaults @@ -0,0 +1,2 @@ +# The Wi-Fi task otherwise runs on core 0, which reserves CPU0 interrupt 0 for WMAC, so the "test_esp_intr_dump_expected_output" case no longer finds "Used: RTC_CORE" on interrupt 0 and fails. Pin the Wi-Fi task to core 1 to keep interrupt 0 free. +CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y diff --git a/tools/test_apps/system/panic/coredump/sdkconfig.defaults b/tools/test_apps/system/panic/coredump/sdkconfig.defaults index 722718ce589..760817fe9b0 100644 --- a/tools/test_apps/system/panic/coredump/sdkconfig.defaults +++ b/tools/test_apps/system/panic/coredump/sdkconfig.defaults @@ -16,3 +16,6 @@ CONFIG_FREERTOS_USE_TRACE_FACILITY=y # Increase main task stack size CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096 + +# CONFIG_VFS_SUPPORT_IO resolves to its default (y) in this test's Kconfig closure; the extra console I/O task makes the core dump header declare more bytes than are emitted, so espcoredump.py info_corefile fails to parse it in the "test_hw_stack_guard_cpu0" case. Disable it. +CONFIG_VFS_SUPPORT_IO=n