idf-build-apps 2.10.1__tar.gz → 2.10.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.pre-commit-config.yaml +1 -1
  2. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/CHANGELOG.md +18 -0
  3. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/PKG-INFO +1 -1
  4. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/__init__.py +1 -1
  5. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/app.py +3 -3
  6. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/args.py +20 -13
  7. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/finder.py +12 -1
  8. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/main.py +4 -3
  9. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/manifest/__init__.py +2 -2
  10. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/manifest/manifest.py +85 -6
  11. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/pyproject.toml +1 -1
  12. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/setup.py +1 -1
  13. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/tests/conftest.py +2 -3
  14. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/tests/test_args.py +129 -1
  15. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/tests/test_finder.py +16 -2
  16. idf_build_apps-2.10.3/tests/test_manifest.py +561 -0
  17. idf_build_apps-2.10.1/tests/test_manifest.py +0 -568
  18. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.editorconfig +0 -0
  19. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.git-blame-ignore-revs +0 -0
  20. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.gitattributes +0 -0
  21. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.github/dependabot.yml +0 -0
  22. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.github/workflows/publish-pypi.yml +0 -0
  23. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.github/workflows/sync-jira.yml +0 -0
  24. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.github/workflows/test-build-docs.yml +0 -0
  25. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.github/workflows/test-build-idf-apps.yml +0 -0
  26. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.gitignore +0 -0
  27. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/.readthedocs.yml +0 -0
  28. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/CONTRIBUTING.md +0 -0
  29. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/LICENSE +0 -0
  30. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/README.md +0 -0
  31. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/_apidoc_templates/module.rst_t +0 -0
  32. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/_apidoc_templates/package.rst_t +0 -0
  33. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/_apidoc_templates/toc.rst_t +0 -0
  34. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/_static/espressif-logo.svg +0 -0
  35. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/_static/theme_overrides.css +0 -0
  36. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/_templates/layout.html +0 -0
  37. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/conf_common.py +0 -0
  38. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/Makefile +0 -0
  39. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/conf.py +0 -0
  40. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/explanations/build.rst +0 -0
  41. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/explanations/config_rules.rst +0 -0
  42. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/explanations/dependency_driven_build.rst +0 -0
  43. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/explanations/find.rst +0 -0
  44. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/guides/1.x_to_2.x.md +0 -0
  45. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/guides/custom_app.md +0 -0
  46. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/index.rst +0 -0
  47. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/others/CHANGELOG.md +0 -0
  48. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/others/CONTRIBUTING.md +0 -0
  49. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/references/cli.rst +0 -0
  50. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/references/config_file.rst +0 -0
  51. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/docs/en/references/manifest.rst +0 -0
  52. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/__main__.py +0 -0
  53. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/autocompletions.py +0 -0
  54. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/constants.py +0 -0
  55. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/junit/__init__.py +0 -0
  56. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/junit/report.py +0 -0
  57. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/junit/utils.py +0 -0
  58. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/log.py +0 -0
  59. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/manifest/soc_header.py +0 -0
  60. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/py.typed +0 -0
  61. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/session_args.py +0 -0
  62. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/utils.py +0 -0
  63. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/vendors/__init__.py +0 -0
  64. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/vendors/pydantic_sources.py +0 -0
  65. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/yaml/__init__.py +0 -0
  66. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/idf_build_apps/yaml/parser.py +0 -0
  67. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/license_header.txt +0 -0
  68. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/tests/test_app.py +0 -0
  69. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/tests/test_build.py +0 -0
  70. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/tests/test_cmd.py +0 -0
  71. {idf_build_apps-2.10.1 → idf_build_apps-2.10.3}/tests/test_utils.py +0 -0
@@ -17,7 +17,7 @@ repos:
17
17
  - --use-current-year
18
18
  exclude: 'idf_build_apps/vendors/'
19
19
  - repo: https://github.com/astral-sh/ruff-pre-commit
20
- rev: 'v0.11.6'
20
+ rev: 'v0.11.10'
21
21
  hooks:
22
22
  - id: ruff
23
23
  args: ['--fix']
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v2.10.3 (2025-06-03)
6
+
7
+ ### Fix
8
+
9
+ - app.target have higher precedence than target while `find_apps`
10
+ - respect FolderRule.DEFAULT_BUILD_TARGETS while validating app
11
+
12
+ ### Refactor
13
+
14
+ - move `FolderRule.DEFAULT_BUILD_TARGET` into contextvar
15
+
16
+ ## v2.10.2 (2025-05-22)
17
+
18
+ ### Perf
19
+
20
+ - `most_suitable_rule` stop searching till reached root dir
21
+ - pre-compute rules folder, reduced 50% time on `most_suitable_rule`
22
+
5
23
  ## v2.10.1 (2025-05-05)
6
24
 
7
25
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idf-build-apps
3
- Version: 2.10.1
3
+ Version: 2.10.3
4
4
  Summary: Tools for building ESP-IDF related apps.
5
5
  Author-email: Fu Hanxi <fuhanxi@espressif.com>
6
6
  Requires-Python: >=3.7
@@ -8,7 +8,7 @@ Tools for building ESP-IDF related apps.
8
8
  # ruff: noqa: E402
9
9
  # avoid circular imports
10
10
 
11
- __version__ = '2.10.1'
11
+ __version__ = '2.10.3'
12
12
 
13
13
  from .session_args import (
14
14
  SessionArgs,
@@ -39,7 +39,7 @@ from .constants import (
39
39
  BuildStatus,
40
40
  )
41
41
  from .manifest.manifest import (
42
- FolderRule,
42
+ DEFAULT_BUILD_TARGETS,
43
43
  Manifest,
44
44
  )
45
45
  from .utils import (
@@ -436,7 +436,7 @@ class App(BaseModel):
436
436
  if self.sdkconfig_files_defined_idf_target:
437
437
  return [self.sdkconfig_files_defined_idf_target]
438
438
 
439
- return FolderRule.DEFAULT_BUILD_TARGETS
439
+ return DEFAULT_BUILD_TARGETS.get()
440
440
 
441
441
  @property
442
442
  def verified_targets(self) -> t.List[str]:
@@ -820,7 +820,7 @@ class MakeApp(App):
820
820
  if self.sdkconfig_files_defined_idf_target:
821
821
  return [self.sdkconfig_files_defined_idf_target]
822
822
 
823
- return ['esp8266', *FolderRule.DEFAULT_BUILD_TARGETS]
823
+ return ['esp8266', *DEFAULT_BUILD_TARGETS.get()]
824
824
 
825
825
  def _build(
826
826
  self,
@@ -29,8 +29,8 @@ from pydantic_settings import (
29
29
  from typing_extensions import Concatenate, ParamSpec
30
30
 
31
31
  from . import SESSION_ARGS, App, CMakeApp, MakeApp, setup_logging
32
- from .constants import ALL_TARGETS, IDF_BUILD_APPS_TOML_FN, SUPPORTED_TARGETS
33
- from .manifest.manifest import FolderRule, Manifest
32
+ from .constants import ALL_TARGETS, IDF_BUILD_APPS_TOML_FN
33
+ from .manifest.manifest import DEFAULT_BUILD_TARGETS, Manifest, reset_default_build_targets
34
34
  from .utils import InvalidCommand, files_matches_patterns, semicolon_separated_str_to_list, to_absolute_path, to_list
35
35
  from .vendors.pydantic_sources import PyprojectTomlConfigSettingsSource, TomlConfigSettingsSource
36
36
 
@@ -519,15 +519,17 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
519
519
  nargs='+',
520
520
  ),
521
521
  description='space-separated list of the default enabled build targets for the apps. '
522
- 'When not specified, the default value is the targets listed by `idf.py --list-targets`',
522
+ 'When not specified, the default value is the targets listed by `idf.py --list-targets`. '
523
+ 'Cannot be used together with --enable-preview-targets',
523
524
  default=None, # type: ignore
524
525
  )
525
526
  enable_preview_targets: bool = field(
526
527
  FieldMetadata(
527
528
  action='store_true',
528
529
  ),
529
- description='When enabled, the default build targets will be set to all apps, '
530
- 'including the preview targets. As the targets defined in `idf.py --list-targets --preview`',
530
+ description='When enabled, all targets will be enabled by default, '
531
+ 'including the preview targets. As the targets defined in `idf.py --list-targets --preview`. '
532
+ 'Cannot be used together with --default-build-targets',
531
533
  default=False, # type: ignore
532
534
  )
533
535
  disable_targets: t.Optional[t.List[str]] = field(
@@ -570,6 +572,14 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
570
572
  LOGGER.debug('--target is missing. Set --target as "all".')
571
573
  self.target = 'all'
572
574
 
575
+ # Validate mutual exclusivity of enable_preview_targets and default_build_targets
576
+ if self.enable_preview_targets and self.default_build_targets:
577
+ raise InvalidCommand(
578
+ 'Cannot specify both --enable-preview-targets and --default-build-targets at the same time. '
579
+ 'Please use only one of these options.'
580
+ )
581
+
582
+ reset_default_build_targets() # reset first then judge again
573
583
  if self.default_build_targets:
574
584
  default_build_targets = []
575
585
  for target in self.default_build_targets:
@@ -582,21 +592,18 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
582
592
  default_build_targets.append(target)
583
593
  self.default_build_targets = default_build_targets
584
594
  LOGGER.info('Overriding default build targets to %s', self.default_build_targets)
585
- FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
595
+ DEFAULT_BUILD_TARGETS.set(self.default_build_targets)
586
596
  elif self.enable_preview_targets:
587
597
  self.default_build_targets = deepcopy(ALL_TARGETS)
588
598
  LOGGER.info('Overriding default build targets to %s', self.default_build_targets)
589
- FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
590
- else:
591
- # restore default build targets
592
- FolderRule.DEFAULT_BUILD_TARGETS = SUPPORTED_TARGETS
599
+ DEFAULT_BUILD_TARGETS.set(self.default_build_targets) # type: ignore
593
600
 
594
- if self.disable_targets and FolderRule.DEFAULT_BUILD_TARGETS:
601
+ if self.disable_targets and DEFAULT_BUILD_TARGETS.get():
595
602
  LOGGER.info('Disable targets: %s', self.disable_targets)
596
603
  self.default_build_targets = [
597
- _target for _target in FolderRule.DEFAULT_BUILD_TARGETS if _target not in self.disable_targets
604
+ _target for _target in DEFAULT_BUILD_TARGETS.get() if _target not in self.disable_targets
598
605
  ]
599
- FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
606
+ DEFAULT_BUILD_TARGETS.set(self.default_build_targets)
600
607
 
601
608
  if self.override_sdkconfig_files or self.override_sdkconfig_items:
602
609
  SESSION_ARGS.set(self)
@@ -18,6 +18,7 @@ from .args import FindArguments
18
18
  from .constants import (
19
19
  BuildStatus,
20
20
  )
21
+ from .manifest.manifest import DEFAULT_BUILD_TARGETS
21
22
  from .utils import (
22
23
  config_rules_from_str,
23
24
  to_absolute_path,
@@ -33,13 +34,23 @@ def _get_apps_from_path(
33
34
  app_cls: t.Type[App] = CMakeApp,
34
35
  args: FindArguments,
35
36
  ) -> t.List[App]:
36
- # trigger test
37
37
  def _validate_app(_app: App) -> bool:
38
38
  if target not in _app.supported_targets:
39
39
  LOGGER.debug('=> Ignored. %s only supports targets: %s', _app, ', '.join(_app.supported_targets))
40
40
  _app.build_status = BuildStatus.DISABLED
41
41
  return args.include_disabled_apps
42
42
 
43
+ if target == 'all' and _app.target not in DEFAULT_BUILD_TARGETS.get():
44
+ LOGGER.debug(
45
+ '=> Ignored. %s is not in the default build targets: %s', _app.target, DEFAULT_BUILD_TARGETS.get()
46
+ )
47
+ _app.build_status = BuildStatus.DISABLED
48
+ return args.include_disabled_apps
49
+ elif _app.target != target:
50
+ LOGGER.debug('=> Ignored. %s is not for target %s', _app, target)
51
+ _app.build_status = BuildStatus.DISABLED
52
+ return args.include_disabled_apps
53
+
43
54
  _app.check_should_build(
44
55
  manifest_rootpath=args.manifest_rootpath,
45
56
  modified_manifest_rules_folders=args.modified_manifest_rules_folders,
@@ -31,7 +31,7 @@ from .app import (
31
31
  AppDeserializer,
32
32
  )
33
33
  from .autocompletions import activate_completions
34
- from .constants import ALL_TARGETS, BuildStatus, completion_instructions
34
+ from .constants import BuildStatus, completion_instructions
35
35
  from .finder import (
36
36
  _find_apps,
37
37
  )
@@ -41,6 +41,7 @@ from .junit import (
41
41
  TestSuite,
42
42
  )
43
43
  from .manifest.manifest import (
44
+ DEFAULT_BUILD_TARGETS,
44
45
  Manifest,
45
46
  )
46
47
  from .utils import (
@@ -88,8 +89,8 @@ def find_apps(
88
89
 
89
90
  apps: t.Set[App] = set()
90
91
  if find_arguments.target == 'all':
91
- targets = ALL_TARGETS
92
- LOGGER.info('Searching for apps by all targets')
92
+ targets = DEFAULT_BUILD_TARGETS.get()
93
+ LOGGER.info('Searching for apps by default build targets: %s', targets)
93
94
  else:
94
95
  targets = [find_arguments.target]
95
96
  LOGGER.info('Searching for apps by target: %s', find_arguments.target)
@@ -7,11 +7,11 @@ Manifest file
7
7
 
8
8
  from esp_bool_parser import register_addition_attribute
9
9
 
10
- from .manifest import FolderRule
10
+ from .manifest import DEFAULT_BUILD_TARGETS
11
11
 
12
12
 
13
13
  def folder_rule_attr(target, **kwargs):
14
- return 1 if target in FolderRule.DEFAULT_BUILD_TARGETS else 0
14
+ return 1 if target in DEFAULT_BUILD_TARGETS.get() else 0
15
15
 
16
16
 
17
17
  register_addition_attribute('INCLUDE_DEFAULT', folder_rule_attr)
@@ -1,8 +1,10 @@
1
1
  # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
+ import contextvars
3
4
  import logging
4
5
  import os
5
6
  import typing as t
7
+ import warnings
6
8
  from hashlib import sha512
7
9
 
8
10
  from esp_bool_parser import BoolStmt, parse_bool_expr
@@ -26,6 +28,16 @@ from ..yaml import (
26
28
 
27
29
  LOGGER = logging.getLogger(__name__)
28
30
 
31
+ # Context variable for default build targets
32
+ DEFAULT_BUILD_TARGETS: contextvars.ContextVar[t.List[str]] = contextvars.ContextVar(
33
+ 'default_build_targets', default=SUPPORTED_TARGETS
34
+ )
35
+
36
+
37
+ def reset_default_build_targets() -> None:
38
+ """Reset DEFAULT_BUILD_TARGETS to the default value (SUPPORTED_TARGETS)"""
39
+ DEFAULT_BUILD_TARGETS.set(SUPPORTED_TARGETS)
40
+
29
41
 
30
42
  class IfClause:
31
43
  def __init__(self, stmt: str, temporary: bool = False, reason: t.Optional[str] = None) -> None:
@@ -78,8 +90,65 @@ class SwitchClause:
78
90
  )
79
91
 
80
92
 
81
- class FolderRule:
82
- DEFAULT_BUILD_TARGETS = SUPPORTED_TARGETS
93
+ def _getattr_default_build_targets(name: str) -> t.Any:
94
+ if name == 'DEFAULT_BUILD_TARGETS':
95
+ warnings.warn(
96
+ 'FolderRule.DEFAULT_BUILD_TARGETS is deprecated. Use DEFAULT_BUILD_TARGETS.get() directly.',
97
+ DeprecationWarning,
98
+ stacklevel=2,
99
+ )
100
+ return DEFAULT_BUILD_TARGETS.get()
101
+ return None
102
+
103
+
104
+ def _setattr_default_build_targets(name: str, value: t.Any) -> bool:
105
+ if name == 'DEFAULT_BUILD_TARGETS':
106
+ warnings.warn(
107
+ 'FolderRule.DEFAULT_BUILD_TARGETS is deprecated. Use DEFAULT_BUILD_TARGETS.set() directly.',
108
+ DeprecationWarning,
109
+ stacklevel=2,
110
+ )
111
+ if not isinstance(value, list):
112
+ raise TypeError('Default build targets must be a list')
113
+ DEFAULT_BUILD_TARGETS.set(value)
114
+ return True
115
+ return False
116
+
117
+
118
+ class _FolderRuleMeta(type):
119
+ """Metaclass to handle class-level assignments to DEFAULT_BUILD_TARGETS"""
120
+
121
+ def __getattribute__(cls, name):
122
+ result = _getattr_default_build_targets(name)
123
+ if result is not None:
124
+ return result
125
+ return super().__getattribute__(name)
126
+
127
+ def __setattr__(cls, name, value):
128
+ if _setattr_default_build_targets(name, value):
129
+ return
130
+ super().__setattr__(name, value)
131
+
132
+ def __delattr__(cls, name):
133
+ if name == 'DEFAULT_BUILD_TARGETS':
134
+ # Don't actually delete anything, just ignore the deletion
135
+ # This handles monkeypatch teardown issues
136
+ pass
137
+ else:
138
+ super().__delattr__(name)
139
+
140
+
141
+ class FolderRule(metaclass=_FolderRuleMeta):
142
+ def __getattribute__(self, name): # instance attr
143
+ result = _getattr_default_build_targets(name)
144
+ if result is not None:
145
+ return result
146
+ return super().__getattribute__(name)
147
+
148
+ def __setattr__(self, name, value):
149
+ if _setattr_default_build_targets(name, value):
150
+ return
151
+ super().__setattr__(name, value)
83
152
 
84
153
  def __init__(
85
154
  self,
@@ -254,9 +323,11 @@ class Manifest:
254
323
 
255
324
  def __init__(self, rules: t.Iterable[FolderRule], *, root_path: str = os.curdir) -> None:
256
325
  self.rules = sorted(rules, key=lambda x: x.folder)
257
-
258
326
  self._root_path = to_absolute_path(root_path)
259
327
 
328
+ # Pre-compute rule paths
329
+ self._rule_paths = {rule.folder: rule for rule in self.rules}
330
+
260
331
  @classmethod
261
332
  def from_files(cls, paths: t.Iterable[PathLike], *, root_path: str = os.curdir) -> 'Manifest':
262
333
  """
@@ -383,9 +454,17 @@ class Manifest:
383
454
 
384
455
  def most_suitable_rule(self, _folder: str) -> FolderRule:
385
456
  folder = to_absolute_path(_folder)
386
- for rule in self.rules[::-1]:
387
- if os.path.commonpath([folder, rule.folder]) == rule.folder:
388
- return rule
457
+
458
+ while True:
459
+ if folder in self._rule_paths:
460
+ return self._rule_paths[folder]
461
+ folder = os.path.dirname(folder)
462
+
463
+ # reached the root path, stop searching
464
+ if folder == self._root_path:
465
+ if folder in self._rule_paths:
466
+ return self._rule_paths[folder]
467
+ break
389
468
 
390
469
  return DefaultRule(folder)
391
470
 
@@ -65,7 +65,7 @@ idf-build-apps = "idf_build_apps:main.main"
65
65
 
66
66
  [tool.commitizen]
67
67
  name = "cz_conventional_commits"
68
- version = "2.10.1"
68
+ version = "2.10.3"
69
69
  tag_format = "v$version"
70
70
  version_files = [
71
71
  "idf_build_apps/__init__.py",
@@ -38,7 +38,7 @@ entry_points = \
38
38
  {'console_scripts': ['idf-build-apps = idf_build_apps:main.main']}
39
39
 
40
40
  setup(name='idf-build-apps',
41
- version='2.10.1',
41
+ version='2.10.3',
42
42
  description='Tools for building ESP-IDF related apps.',
43
43
  author=None,
44
44
  author_email='Fu Hanxi <fuhanxi@espressif.com>',
@@ -10,14 +10,13 @@ from idf_build_apps import (
10
10
  setup_logging,
11
11
  )
12
12
  from idf_build_apps.args import apply_config_file
13
- from idf_build_apps.constants import SUPPORTED_TARGETS
14
- from idf_build_apps.manifest.manifest import FolderRule
13
+ from idf_build_apps.manifest.manifest import FolderRule, reset_default_build_targets
15
14
 
16
15
 
17
16
  @pytest.fixture(autouse=True)
18
17
  def clean_cls_attr(tmp_path):
19
18
  App.MANIFEST = None
20
- FolderRule.DEFAULT_BUILD_TARGETS = SUPPORTED_TARGETS
19
+ reset_default_build_targets()
21
20
  idf_build_apps.SESSION_ARGS.clean()
22
21
  apply_config_file(reset=True)
23
22
  os.chdir(tmp_path)
@@ -17,8 +17,10 @@ from idf_build_apps.args import (
17
17
  FindBuildArguments,
18
18
  expand_vars,
19
19
  )
20
- from idf_build_apps.constants import IDF_BUILD_APPS_TOML_FN, PREVIEW_TARGETS, SUPPORTED_TARGETS
20
+ from idf_build_apps.constants import ALL_TARGETS, IDF_BUILD_APPS_TOML_FN, PREVIEW_TARGETS, SUPPORTED_TARGETS
21
21
  from idf_build_apps.main import main
22
+ from idf_build_apps.manifest.manifest import DEFAULT_BUILD_TARGETS, FolderRule
23
+ from idf_build_apps.utils import InvalidCommand
22
24
 
23
25
 
24
26
  def test_init_attr_deprecated_by():
@@ -162,6 +164,15 @@ modified_files = [
162
164
  assert args.deactivate_dependency_driven_build_by_components == ['baz']
163
165
 
164
166
 
167
+ def test_mutual_exclusivity_validation():
168
+ # Test that both options together raise InvalidCommand
169
+ with pytest.raises(InvalidCommand) as exc_info:
170
+ FindBuildArguments(enable_preview_targets=True, default_build_targets=['esp32'], paths=['.'])
171
+
172
+ assert 'Cannot specify both --enable-preview-targets and --default-build-targets' in str(exc_info.value)
173
+ assert 'Please use only one of these options' in str(exc_info.value)
174
+
175
+
165
176
  def test_build_targets_cli(tmp_path, monkeypatch):
166
177
  create_project('foo', tmp_path)
167
178
  with open(IDF_BUILD_APPS_TOML_FN, 'w') as fw:
@@ -411,6 +422,123 @@ dry_run = false
411
422
  assert test_suite.findall('testcase')[0].attrib['name'] == 'bar/build'
412
423
 
413
424
 
425
+ class TestDefaultBuildTargetsContextVar:
426
+ def test_direct_contextvar_access(self):
427
+ # Test initial value
428
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
429
+
430
+ # Test setting new values
431
+ test_targets = ['esp32', 'esp32s2']
432
+ DEFAULT_BUILD_TARGETS.set(test_targets)
433
+ assert DEFAULT_BUILD_TARGETS.get() == test_targets
434
+
435
+ # Test setting to ALL_TARGETS
436
+ DEFAULT_BUILD_TARGETS.set(ALL_TARGETS)
437
+ assert DEFAULT_BUILD_TARGETS.get() == ALL_TARGETS
438
+ assert len(DEFAULT_BUILD_TARGETS.get()) == len(SUPPORTED_TARGETS) + len(PREVIEW_TARGETS)
439
+
440
+ def test_folder_rule_backward_compatibility(self):
441
+ # Test initial access
442
+ assert FolderRule.DEFAULT_BUILD_TARGETS == SUPPORTED_TARGETS
443
+
444
+ # Test setting via contextvar
445
+ other_targets = ['esp32h2', 'esp32p4']
446
+ DEFAULT_BUILD_TARGETS.set(other_targets)
447
+ assert FolderRule.DEFAULT_BUILD_TARGETS == other_targets
448
+ assert DEFAULT_BUILD_TARGETS.get() == other_targets
449
+
450
+ # Test setting via FolderRule
451
+ test_targets = ['esp32c3', 'esp32c6']
452
+ FolderRule.DEFAULT_BUILD_TARGETS = test_targets
453
+ assert DEFAULT_BUILD_TARGETS.get() == test_targets
454
+ assert FolderRule.DEFAULT_BUILD_TARGETS == test_targets
455
+
456
+ def test_default_build_targets_option(self):
457
+ """Test that --default-build-targets option works correctly"""
458
+ test_targets = ['esp32', 'esp32s2', 'esp32c3']
459
+
460
+ args = FindBuildArguments(default_build_targets=test_targets, paths=['.'])
461
+
462
+ assert args.default_build_targets == test_targets
463
+ assert DEFAULT_BUILD_TARGETS.get() == test_targets
464
+ assert FolderRule.DEFAULT_BUILD_TARGETS == test_targets
465
+
466
+ def test_enable_preview_targets_option(self):
467
+ """Test that --enable-preview-targets option works correctly"""
468
+ args = FindBuildArguments(enable_preview_targets=True, paths=['.'])
469
+
470
+ assert args.enable_preview_targets is True
471
+ assert args.default_build_targets == ALL_TARGETS
472
+ assert DEFAULT_BUILD_TARGETS.get() == ALL_TARGETS
473
+ assert FolderRule.DEFAULT_BUILD_TARGETS == ALL_TARGETS
474
+ assert len(DEFAULT_BUILD_TARGETS.get()) == len(SUPPORTED_TARGETS) + len(PREVIEW_TARGETS)
475
+
476
+ def test_default_behavior(self):
477
+ """Test default behavior when no special options are provided"""
478
+ args = FindBuildArguments(paths=['.'])
479
+
480
+ assert args.enable_preview_targets is False
481
+ assert args.default_build_targets is None
482
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
483
+ assert FolderRule.DEFAULT_BUILD_TARGETS == SUPPORTED_TARGETS
484
+
485
+ def test_disable_targets_with_default_build_targets(self):
486
+ """Test --disable-targets option works with --default-build-targets"""
487
+ args = FindBuildArguments(
488
+ default_build_targets=['esp32', 'esp32s2', 'esp32c3'], disable_targets=['esp32s2'], paths=['.']
489
+ )
490
+
491
+ expected_targets = ['esp32', 'esp32c3']
492
+ assert args.default_build_targets == expected_targets
493
+ assert DEFAULT_BUILD_TARGETS.get() == expected_targets
494
+ assert FolderRule.DEFAULT_BUILD_TARGETS == expected_targets
495
+
496
+ def test_disable_targets_with_enable_preview_targets(self):
497
+ """Test --disable-targets option works with --enable-preview-targets"""
498
+ disabled_target = PREVIEW_TARGETS[0] # Disable first preview target
499
+
500
+ args = FindBuildArguments(enable_preview_targets=True, disable_targets=[disabled_target], paths=['.'])
501
+
502
+ expected_targets = [t for t in ALL_TARGETS if t != disabled_target]
503
+ assert args.default_build_targets == expected_targets
504
+ assert DEFAULT_BUILD_TARGETS.get() == expected_targets
505
+ assert len(DEFAULT_BUILD_TARGETS.get()) == len(ALL_TARGETS) - 1
506
+
507
+ def test_invalid_targets_filtering(self):
508
+ """Test that invalid targets are filtered out and warnings are logged"""
509
+ invalid_targets = ['esp32', 'invalid_target', 'esp32s2', 'another_invalid']
510
+
511
+ args = FindBuildArguments(default_build_targets=invalid_targets, paths=['.'])
512
+
513
+ # Only valid targets should remain
514
+ expected_targets = ['esp32', 'esp32s2']
515
+ assert args.default_build_targets == expected_targets
516
+ assert DEFAULT_BUILD_TARGETS.get() == expected_targets
517
+
518
+ def test_contextvar_isolation_between_instances(self):
519
+ """Test that the contextvar behaves correctly across multiple argument instances"""
520
+ # First instance sets default_build_targets
521
+ FindBuildArguments(default_build_targets=['esp32', 'esp32s2'])
522
+ assert DEFAULT_BUILD_TARGETS.get() == ['esp32', 'esp32s2']
523
+
524
+ # Second instance sets enable_preview_targets
525
+ FindBuildArguments(enable_preview_targets=True)
526
+ assert DEFAULT_BUILD_TARGETS.get() == ALL_TARGETS
527
+
528
+ # Third instance uses default behavior
529
+ FindBuildArguments()
530
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
531
+
532
+ def test_empty_default_build_targets(self):
533
+ """Test behavior with empty default_build_targets list"""
534
+ args = FindBuildArguments(default_build_targets=[])
535
+
536
+ # Empty list is treated as falsy, so it falls back to default behavior
537
+ assert args.default_build_targets == []
538
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
539
+ assert FolderRule.DEFAULT_BUILD_TARGETS == SUPPORTED_TARGETS
540
+
541
+
414
542
  def test_expand_vars(monkeypatch):
415
543
  assert expand_vars('Value is $TEST_VAR') == 'Value is '
416
544
  monkeypatch.setenv('TEST_VAR', 'test_value')
@@ -12,6 +12,7 @@ from conftest import (
12
12
  create_project,
13
13
  )
14
14
 
15
+ import idf_build_apps
15
16
  from idf_build_apps.constants import (
16
17
  DEFAULT_SDKCONFIG,
17
18
  IDF_PATH,
@@ -457,6 +458,21 @@ class TestFindWithSdkconfigFiles:
457
458
  except: # noqa
458
459
  pass
459
460
 
461
+ def test_build_preview_but_sdkconfig_default(self, tmp_path):
462
+ create_project('foo', tmp_path)
463
+
464
+ apps = find_apps(str(tmp_path / 'foo'), 'all', default_build_targets=['esp32'])
465
+ assert len(apps) == 1
466
+
467
+ with open(tmp_path / 'foo' / 'sdkconfig.defaults', 'w') as f:
468
+ f.write('CONFIG_IDF_TARGET="esp32p4"\n')
469
+
470
+ apps = find_apps(str(tmp_path / 'foo'), 'all', default_build_targets=['esp32'])
471
+ assert len(apps) == 0
472
+
473
+ apps = find_apps(str(tmp_path / 'foo'), 'esp32p4', default_build_targets=['esp32p4'])
474
+ assert len(apps) == 1
475
+
460
476
  def test_with_sdkconfig_defaults_idf_target_but_disabled(self, tmp_path):
461
477
  manifest_file = tmp_path / 'manifest.yml'
462
478
  manifest_file.write_text(
@@ -540,8 +556,6 @@ class TestFindWithSdkconfigFiles:
540
556
  assert len(apps) == 0
541
557
 
542
558
  def test_with_sdkconfig_override(self, tmp_path):
543
- import idf_build_apps
544
-
545
559
  create_project('test1', tmp_path)
546
560
  (tmp_path / 'test1' / 'sdkconfig.defaults').write_text(
547
561
  """