fix(tools): seed IDF_VERSION before idf.py parses dependencies.lock

Any idf.py invocation could hang indefinitely with no output while
spawning an unbounded chain of "idf.py --version" subprocesses,
eventually exhausting system memory.

During init_cli(), idf.py parses the project's dependencies.lock to
vet trusted component-provided idf_ext.py extensions. If the lock
contains a component whose manifest has an "if: idf_version" clause,
evaluating it calls idf-component-manager's _get_idf_version(). Outside
a CMake build the IDF_VERSION environment variable is not set, so that
function falls back to running "idf.py --version" as a subprocess,
which re-enters init_cli() and recurses without bound.

During a normal CMake build the component manager runs as a subprocess
that already has IDF_VERSION in its environment (see build/config.env),
so the fallback is never reached. The recursion happens only because
idf.py runs component-manager code in-process during its own CLI
startup, outside that context.

Seed IDF_VERSION into os.environ early in init_cli(), before any
dependencies.lock parsing, using the subprocess-free
idf_version_from_cmake() helper. This gives in-process component-manager
code the same IDF_VERSION a CMake build would provide.
This commit is contained in:
Roland Dobai
2026-05-14 13:13:20 +02:00
parent dc9276941e
commit 29dcff2cdc
3 changed files with 192 additions and 1 deletions

View File

@@ -147,6 +147,26 @@ class TestDependencyManagement(TestWithoutExtensions):
)
class TestIdfVersionSeeding(TestWithoutExtensions):
def test_idf_version_seeded_when_unset(self):
from idf_py_actions.tools import idf_version_from_cmake
with mock.patch.dict(os.environ):
os.environ.pop('IDF_VERSION', None)
idf.init_cli()(args=['--dry-run', 'build'], standalone_mode=False)
self.assertIn('IDF_VERSION', os.environ)
expected = idf_version_from_cmake()
self.assertIsNotNone(expected)
expected_stripped = expected.lstrip('v')
self.assertEqual(os.environ['IDF_VERSION'], expected_stripped)
self.assertFalse(os.environ['IDF_VERSION'].startswith('v'))
def test_idf_version_not_overwritten_when_set(self):
with mock.patch.dict(os.environ, {'IDF_VERSION': '0.0.0-sentinel'}):
idf.init_cli()(args=['--dry-run', 'build'], standalone_mode=False)
self.assertEqual(os.environ['IDF_VERSION'], '0.0.0-sentinel')
class TestVerboseFlag(TestWithoutExtensions):
def test_verbose_messages(self):
output = subprocess.check_output(