Skip to content

Commit 6ce2234

Browse files
committed
Merge branch 'fix/kconfig_build_system' into 'main'
fix: Kconfig build system Closes PACMAN-1204 See merge request espressif/idf-component-manager!563
2 parents d3e4af5 + 104dc72 commit 6ce2234

File tree

4 files changed

+299
-9
lines changed

4 files changed

+299
-9
lines changed

idf_component_manager/core.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,7 @@ def inject_requirements(
805805
self,
806806
component_requires_file: t.Union[Path, str],
807807
component_list_file: t.Union[Path, str],
808+
cm_run_counter: int,
808809
):
809810
"""Set build dependencies for components with manifests"""
810811
requirements_manager = CMakeRequirementsManager(component_requires_file)
@@ -884,6 +885,14 @@ def add_req(key: str) -> None:
884885
handle_project_requirements(new_requirements)
885886
requirements_manager.dump(new_requirements)
886887

888+
# In case of the CM second run when dealing with KConfig Variables
889+
# We need to overwrite the retried to 0 to run CM the third time
890+
# It's needed due to reason that in sdkconfig.json there will be
891+
# correct values for configs of downloaded deps
892+
if cm_run_counter == 1:
893+
with open(requirements_manager.path, 'a', encoding='utf-8') as f:
894+
f.write('set(retried 0 PARENT_SCOPE)\n')
895+
887896
@staticmethod
888897
def _override_requirements_by_component_sources(
889898
requirements: t.OrderedDict[ComponentName, t.Dict[str, t.Union[t.List[str], str]]],

idf_component_manager/prepare_components/prepare.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,43 @@ def _component_list_file(build_dir):
2525
return os.path.join(build_dir, 'components_with_manifests_list.temp')
2626

2727

28+
class RunCounter:
29+
def __init__(self, build_dir: t.Union[str, Path]):
30+
self._file_path = Path(build_dir) / f'component_manager_run_counter.{os.getppid()}'
31+
32+
if not self._file_path.exists():
33+
self._file_path.write_text('0')
34+
35+
@property
36+
def value(self) -> int:
37+
"""
38+
Current value of the counter.
39+
"""
40+
try:
41+
return int(self._file_path.read_text().strip())
42+
except (FileNotFoundError, ValueError):
43+
# Fallback if file was deleted externally or contains garbage
44+
return 0
45+
46+
def increase(self) -> None:
47+
"""
48+
Increments the counter by 1.
49+
"""
50+
# Since __init__ guarantees creation, we can simply update logic.
51+
# We still catch FileNotFoundError in case it was deleted externally.
52+
if not self._file_path.exists():
53+
return
54+
55+
self._file_path.write_text(str(self.value + 1))
56+
57+
def cleanup(self) -> None:
58+
"""
59+
Removes the counter file.
60+
"""
61+
if self._file_path.exists():
62+
self._file_path.unlink()
63+
64+
2865
def _get_ppid_file_path(local_component_list_file: t.Optional[str]) -> Path:
2966
return Path(f'{local_component_list_file}.{os.getppid()}')
3067

@@ -53,28 +90,28 @@ def _get_component_list_file(local_components_list_file):
5390
return local_components_list_file
5491

5592

56-
def _get_sdkconfig_json_file_path(args) -> t.Optional[Path]:
93+
def _get_sdkconfig_json_file_path(args, build_dir) -> t.Optional[Path]:
5794
"""
5895
Returns the path to the sdkconfig.json file if found, None otherwise.
5996
`sdkconfig_json_file` argument is not provided in some ESP-IDF versions (5.5.0, 5.5.1,...) when injecting deps
6097
so there's a fallback to the default known location.
6198
"""
6299
if args.sdkconfig_json_file:
63100
return Path(args.sdkconfig_json_file)
64-
elif args.interface_version >= 4 and args.build_dir:
65-
return Path(args.build_dir) / 'config' / 'sdkconfig.json'
101+
elif args.interface_version >= 4 and build_dir:
102+
return Path(build_dir) / 'config' / 'sdkconfig.json'
66103

67104
return None
68105

69106

70107
def prepare_dep_dirs(args):
71-
sdk_config_json_path = _get_sdkconfig_json_file_path(args)
108+
build_dir = args.build_dir or os.path.dirname(args.managed_components_list_file)
72109

73-
if sdk_config_json_path:
110+
# If the Component Manager has been run before, we need to update the Kconfig context with the sdkconfig.json file
111+
sdk_config_json_path = _get_sdkconfig_json_file_path(args, build_dir)
112+
if sdk_config_json_path and RunCounter(build_dir).value > 0:
74113
KCONFIG_CONTEXT.get().update_from_file(sdk_config_json_path)
75114

76-
build_dir = args.build_dir or os.path.dirname(args.managed_components_list_file)
77-
78115
local_components_list_file = _get_component_list_file(args.local_components_list_file)
79116

80117
ComponentManager(
@@ -128,6 +165,7 @@ def debug_message(req: ComponentRequirement) -> str:
128165
raise FatalError(
129166
f"Failed to copy '{args.local_components_list_file}' → '{ppid_file}': {e}"
130167
) from e
168+
131169
# Exiting with code 10 to signal CMake to re-run component discovery due to missing KConfig options
132170
sys.exit(10)
133171

@@ -139,9 +177,9 @@ def debug_message(req: ComponentRequirement) -> str:
139177

140178

141179
def inject_requirements(args):
142-
sdk_config_json_path = _get_sdkconfig_json_file_path(args)
180+
sdk_config_json_path = _get_sdkconfig_json_file_path(args, args.build_dir)
143181

144-
if sdk_config_json_path:
182+
if sdk_config_json_path and RunCounter(args.build_dir).value > 0:
145183
KCONFIG_CONTEXT.get().update_from_file(sdk_config_json_path)
146184

147185
ComponentManager(
@@ -151,8 +189,16 @@ def inject_requirements(args):
151189
).inject_requirements(
152190
component_requires_file=args.component_requires_file,
153191
component_list_file=_component_list_file(args.build_dir),
192+
cm_run_counter=RunCounter(args.build_dir).value,
154193
)
155194

195+
# Last run of prepare_dep_dirs was successful
196+
# Clean up CM Run counter
197+
if not Path(_get_ppid_file_path(f'{args.build_dir}/local_components_list.temp.yml')).exists():
198+
RunCounter(args.build_dir).cleanup()
199+
else:
200+
RunCounter(args.build_dir).increase()
201+
156202

157203
def main():
158204
setup_logging()

integration_tests/test_kconfig.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: Apache-2.0
3+
import os
4+
from pathlib import Path
5+
6+
import pytest
7+
from ruamel.yaml import YAML
8+
9+
from idf_component_tools.semver.base import Version
10+
from integration_tests.integration_test_helpers import fixtures_path, project_action
11+
12+
idf_version = Version.coerce(os.getenv('ESP_IDF_VERSION'))
13+
14+
15+
@pytest.mark.skipif(
16+
idf_version < Version.coerce('5.3'),
17+
reason='KConfig variables in the manifest are not supported in ESP-IDF < 5.3',
18+
)
19+
@pytest.mark.parametrize(
20+
'project',
21+
[
22+
{
23+
'components': {
24+
'main': {
25+
'dependencies': {
26+
'cmp': {
27+
'override_path': fixtures_path(
28+
'components', 'cmp_with_kconfig_var', 'cmp'
29+
),
30+
},
31+
},
32+
},
33+
},
34+
},
35+
],
36+
indirect=True,
37+
)
38+
def test_prepare_dep_dirs_with_kconfig(project):
39+
res = project_action(
40+
project,
41+
'reconfigure',
42+
)
43+
44+
# Count how many times "Processing X dependencies" appears in the output
45+
# This indicates how many times Component Manager has been run
46+
processing_count = res.count('NOTICE: Processing')
47+
assert processing_count == 2
48+
49+
# Verify that valid Kconfig options are resolved correctly
50+
lock = YAML().load(Path(project) / 'dependencies.lock')
51+
assert 'cmp' in lock['dependencies']
52+
assert 'espressif/esp_codec_dev' in lock['dependencies']
53+
assert (
54+
'$CONFIG{ESP_BOARD_DEV_AUDIO_CODEC_SUPPORT} == True'
55+
in lock['dependencies']['cmp']['dependencies'][0]['matches'][0]['if']
56+
)
57+
assert 'service' in lock['dependencies']['espressif/esp_codec_dev']['source']['type']
58+
59+
60+
@pytest.mark.skipif(
61+
idf_version < Version.coerce('5.3'),
62+
reason='KConfig variables in the manifest are not supported in ESP-IDF < 5.3',
63+
)
64+
@pytest.mark.parametrize(
65+
'project',
66+
[
67+
{
68+
'components': {
69+
'main': {
70+
'dependencies': {
71+
'cmp': {
72+
'matches': [{'if': '$CONFIG{ADC_ENABLE_DEBUG_LOG} == True'}],
73+
'override_path': fixtures_path(
74+
'components', 'cmp_with_kconfig_var', 'cmp'
75+
),
76+
},
77+
},
78+
},
79+
},
80+
},
81+
],
82+
indirect=True,
83+
)
84+
def test_three_runs_cm_kconfig(project):
85+
(Path(project) / 'sdkconfig').write_text('CONFIG_ADC_ENABLE_DEBUG_LOG=y')
86+
87+
res = project_action(
88+
project,
89+
'reconfigure',
90+
)
91+
92+
# Count how many times "Processing X dependencies" appears in the output
93+
# This indicates how many times Component Manager has been run
94+
processing_count = res.count('NOTICE: Processing')
95+
assert processing_count == 3
96+
97+
assert 'Configuring done' in res
98+
lock = YAML().load(Path(project) / 'dependencies.lock')
99+
assert 'cmp' in lock['dependencies']
100+
assert 'espressif/esp_codec_dev' in lock['dependencies']
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-License-Identifier: Apache-2.0
3+
import os
4+
5+
from idf_component_manager.prepare_components.prepare import RunCounter
6+
7+
8+
def test_create_counter_file_with_zero(tmp_path):
9+
counter = RunCounter(tmp_path)
10+
assert counter.value == 0
11+
# Verify the file was created
12+
assert counter._file_path.exists()
13+
14+
15+
def test_file_path_includes_ppid(tmp_path):
16+
counter = RunCounter(tmp_path)
17+
expected_filename = f'component_manager_run_counter.{os.getppid()}'
18+
assert counter._file_path.name == expected_filename
19+
20+
21+
def test_value_handles_missing_file(tmp_path):
22+
counter = RunCounter(tmp_path)
23+
assert counter.value == 0
24+
25+
26+
def test_increase_increments_counter_by_one(tmp_path):
27+
counter = RunCounter(tmp_path)
28+
assert counter.value == 0
29+
counter.increase()
30+
assert counter.value == 1
31+
32+
33+
def test_increase_multiple_times(tmp_path):
34+
counter = RunCounter(tmp_path)
35+
counter.increase()
36+
counter.increase()
37+
counter.increase()
38+
assert counter.value == 3
39+
40+
41+
def test_increase_does_nothing_if_file_deleted(tmp_path):
42+
counter = RunCounter(tmp_path)
43+
counter.cleanup()
44+
# Should not raise an exception
45+
counter.increase()
46+
assert counter.value == 0 # Fallback
47+
48+
49+
def test_cleanup_removes_counter_file(tmp_path):
50+
counter = RunCounter(tmp_path)
51+
assert counter._file_path.exists()
52+
counter.cleanup()
53+
assert not counter._file_path.exists()
54+
55+
56+
def test_cleanup_value_after_cleanup(tmp_path):
57+
counter = RunCounter(tmp_path)
58+
counter.increase()
59+
assert counter.value == 1
60+
counter.cleanup()
61+
assert counter.value == 0
62+
63+
64+
def test_cleanup_called_multiple_times(tmp_path):
65+
counter = RunCounter(tmp_path)
66+
counter.cleanup()
67+
counter.cleanup() # Should not raise
68+
assert not counter._file_path.exists()
69+
70+
71+
def test_single_run_typical_cmake_workflow(tmp_path):
72+
# First CMake run: prepare_dependencies
73+
assert not RunCounter(tmp_path).value > 0
74+
75+
# First CMake run: inject_requirements
76+
assert not RunCounter(tmp_path).value > 0
77+
RunCounter(tmp_path).increase()
78+
79+
# Cleanup after successful build
80+
cnt = RunCounter(tmp_path)
81+
cnt.cleanup()
82+
83+
assert not cnt._file_path.exists()
84+
85+
86+
def test_two_run_typical_cmake_workflow(tmp_path):
87+
# First CMake run: prepare_dependencies
88+
assert not RunCounter(tmp_path).value > 0
89+
90+
# First CMake run: inject_requirements
91+
assert not RunCounter(tmp_path).value > 0
92+
RunCounter(tmp_path).increase()
93+
94+
# Second CMake run: prepare_dependencies checks if run before
95+
assert RunCounter(tmp_path).value > 0 # Was run before
96+
97+
# Second CMake run: inject_requirements
98+
assert RunCounter(tmp_path).value > 0
99+
RunCounter(tmp_path).increase()
100+
101+
# Cleanup after successful build
102+
cnt = RunCounter(tmp_path)
103+
cnt.cleanup()
104+
105+
assert not cnt._file_path.exists()
106+
107+
108+
def test_three_run_typical_cmake_workflow(tmp_path):
109+
# First CMake run: prepare_dependencies
110+
assert not RunCounter(tmp_path).value > 0
111+
112+
# First CMake run: inject_requirements
113+
assert not RunCounter(tmp_path).value > 0
114+
RunCounter(tmp_path).increase()
115+
116+
# Second CMake run: prepare_dependencies checks if run before
117+
assert RunCounter(tmp_path).value > 0 # Was run before
118+
119+
# Second CMake run: inject_requirements
120+
assert RunCounter(tmp_path).value > 0
121+
122+
RunCounter(tmp_path).increase()
123+
124+
# Third CMake run: prepare_dependencies checks if run before
125+
assert RunCounter(tmp_path).value > 0 # Was run before
126+
127+
# Third CMake run: inject_requirements
128+
assert RunCounter(tmp_path).value > 0
129+
RunCounter(tmp_path).increase()
130+
131+
# Cleanup after successful build
132+
cnt = RunCounter(tmp_path)
133+
cnt.cleanup()
134+
135+
assert not cnt._file_path.exists()

0 commit comments

Comments
 (0)