[ci] Fix memory impact build selecting unbuildable platform (#16788)

This commit is contained in:
J. Nick Koston
2026-06-03 22:30:36 -05:00
committed by GitHub
parent d47f6b896e
commit 3e562b9267
4 changed files with 171 additions and 74 deletions

View File

@@ -70,6 +70,7 @@ from helpers import (
get_changed_components,
get_component_from_path,
get_component_test_files,
get_component_test_platforms,
get_components_with_dependencies,
get_cpp_changed_components,
get_fixture_to_test_files,
@@ -77,7 +78,6 @@ from helpers import (
get_target_branch,
git_ls_files,
is_validate_only_file,
parse_test_filename,
root_path,
)
from split_components_for_ci import create_intelligent_batches
@@ -169,24 +169,6 @@ MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core ch
MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform
MEMORY_IMPACT_MAX_COMPONENTS = 40 # Max components before results become nonsensical
# Platform-specific components that can only be built on their respective platforms
# These components contain platform-specific code and cannot be cross-compiled
# Regular components (wifi, logger, api, etc.) are cross-platform and not listed here
PLATFORM_SPECIFIC_COMPONENTS = frozenset(
{
"esp32", # ESP32 platform implementation
"esp8266", # ESP8266 platform implementation
"rp2040", # Raspberry Pi Pico / RP2040 platform implementation
"libretiny", # LibreTiny base platform implementation
"bk72xx", # Beken BK72xx platform implementation (uses LibreTiny)
"rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny)
"ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny)
"host", # Host platform (for testing on development machine)
"nrf52", # Nordic nRF52 platform implementation (uses Zephyr)
"zephyr", # Zephyr RTOS platform implementation
}
)
# Platform preference order for memory impact analysis
# This order is used when no platform-specific hints are detected from filenames
# Priority rationale:
@@ -1006,23 +988,24 @@ def detect_memory_impact_config(
] = {} # Track which platforms each component supports
for component in sorted(changed_component_set):
# Look for test files on preferred platforms
test_files = get_component_test_files(component, all_variants=True)
if not test_files:
continue
# Check if component has tests for any preferred platform
available_platforms = [
platform
for test_file in test_files
if (platform := parse_test_filename(test_file)[1]) != "all"
and platform in MEMORY_IMPACT_PLATFORM_PREFERENCE
]
# Discover the platforms this component has BASE tests for, using the
# same logic as the build runner (get_component_test_platforms wraps the
# shared get_component_test_files + parse_test_filename helpers). Base
# tests only: the memory impact CI build runs test_build_components.py
# with --base-only, which compiles base test.<platform>.yaml files but
# never variant test-<variant>.<platform>.yaml files. Counting
# variant-only platforms here would let us select a platform the build
# then has nothing to compile for, producing no memory output.
available_platforms = {
Platform(platform)
for platform in get_component_test_platforms(component)
if platform in MEMORY_IMPACT_PLATFORM_PREFERENCE
}
if not available_platforms:
continue
component_platforms_map[component] = set(available_platforms)
component_platforms_map[component] = available_platforms
components_with_tests.append(component)
# If no components have tests, don't run memory impact
@@ -1084,20 +1067,57 @@ def detect_memory_impact_config(
)
platform = _select_platform_by_count(platform_counts)
# Filter out platform-specific components that are incompatible with selected platform
# Platform components (esp32, esp8266, rp2040, etc.) can only build on their own platform
# Other components (wifi, logger, etc.) are cross-platform and can build anywhere
compatible_components = [
component
for component in components_with_tests
if component not in PLATFORM_SPECIFIC_COMPONENTS
or platform in component_platforms_map.get(component, set())
]
# Keep only components that have a base test on the selected platform.
# The merged build runs test_build_components.py -t <platform> --base-only,
# so a component without a base test.<platform>.yaml compiles nothing and
# contributes no memory output. This also covers platform-specific
# components (esp32, esp8266, etc.), which only have tests on their own
# platform. When components don't share a common platform we build the
# largest subset that does, dropping the rest.
def components_supporting(target: Platform) -> list[str]:
return [
component
for component in components_with_tests
if target in component_platforms_map.get(component, set())
]
# If no components are compatible with the selected platform, don't run
compatible_components = components_supporting(platform)
# A platform hint (or no-common-platform fallback) can pick a platform that
# no changed component actually has a base test for, leaving nothing to
# build. In that case fall back to the platform supported by the most
# components. component_platforms_map is non-empty (guarded above) and every
# value is a non-empty platform set (components with no supported platform
# are skipped at discovery), so this always yields a buildable platform with
# at least one compatible component.
if not compatible_components:
platform = _select_platform_by_count(
Counter(
p for platforms in component_platforms_map.values() for p in platforms
)
)
compatible_components = components_supporting(platform)
# Defensive backstop: unreachable given the invariant above, but guards
# against a future regression in platform selection silently passing an
# empty component list to the build.
if not compatible_components:
return {"should_run": "false"}
# Log components dropped because they lack a base test on the selected
# platform so partial-subset builds are visible in CI logs.
dropped_components = [
component
for component in components_with_tests
if component not in compatible_components
]
if dropped_components:
print(
f"Memory impact: Dropping components without a base test on "
f"{platform}: {dropped_components}",
file=sys.stderr,
)
# Debug output
print("Memory impact analysis:", file=sys.stderr)
print(f" Changed components: {sorted(changed_component_set)}", file=sys.stderr)

View File

@@ -149,6 +149,31 @@ def get_component_test_files(
return files
def get_component_test_platforms(component: str, *, base_only: bool = True) -> set[str]:
"""Return the set of platforms a component has compilable test files for.
Uses the same discovery as ``test_build_components.py`` (``get_component_test_files``
+ ``parse_test_filename``) so callers agree with what the build runner would
actually compile. With ``base_only=True`` (the default, matching the
memory-impact build's ``--base-only``), only base ``test.<platform>.yaml``
files are considered; variant ``test-<variant>.<platform>.yaml`` files are
excluded. The ``"all"`` platform sentinel is excluded.
Args:
component: Component name (e.g. "wifi")
base_only: If True, only consider base test files (default).
Returns:
Set of platform identifiers (e.g. {"esp32-idf", "esp8266-ard"}).
"""
platforms: set[str] = set()
for test_file in get_component_test_files(component, all_variants=not base_only):
platform = parse_test_filename(test_file)[1]
if platform != "all":
platforms.add(platform)
return platforms
def is_validate_only_file(test_file: Path) -> bool:
"""Return True if the given path is a config-only validate file.

View File

@@ -42,6 +42,7 @@ from script.analyze_component_buses import (
from script.helpers import (
get_component_test_files,
is_validate_only_file,
parse_test_filename,
split_conflicting_groups,
)
from script.merge_component_configs import merge_component_configs
@@ -122,21 +123,6 @@ def find_component_tests(
return dict(component_tests)
def parse_test_filename(test_file: Path) -> tuple[str, str]:
"""Parse test filename to extract test name and platform.
Args:
test_file: Path to test file
Returns:
Tuple of (test_name, platform)
"""
parts = test_file.stem.split(".")
if len(parts) == 2:
return parts[0], parts[1] # test, platform
return parts[0], "all"
def get_platform_base_files(base_dir: Path) -> dict[str, list[Path]]:
"""Get all platform base files.