idf-build-apps 2.10.2__tar.gz → 2.11.0__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 (70) hide show
  1. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/CHANGELOG.md +17 -0
  2. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/PKG-INFO +1 -1
  3. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/references/config_file.rst +1 -1
  4. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/__init__.py +1 -1
  5. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/app.py +3 -3
  6. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/args.py +45 -22
  7. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/finder.py +12 -1
  8. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/main.py +4 -3
  9. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/manifest/__init__.py +2 -2
  10. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/manifest/manifest.py +71 -2
  11. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/pyproject.toml +1 -1
  12. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/setup.py +1 -1
  13. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/conftest.py +2 -3
  14. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/test_args.py +138 -3
  15. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/test_finder.py +122 -5
  16. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.editorconfig +0 -0
  17. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.git-blame-ignore-revs +0 -0
  18. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.gitattributes +0 -0
  19. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.github/dependabot.yml +0 -0
  20. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.github/workflows/publish-pypi.yml +0 -0
  21. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.github/workflows/sync-jira.yml +0 -0
  22. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.github/workflows/test-build-docs.yml +0 -0
  23. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.github/workflows/test-build-idf-apps.yml +0 -0
  24. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.gitignore +0 -0
  25. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.pre-commit-config.yaml +0 -0
  26. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/.readthedocs.yml +0 -0
  27. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/CONTRIBUTING.md +0 -0
  28. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/LICENSE +0 -0
  29. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/README.md +0 -0
  30. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/_apidoc_templates/module.rst_t +0 -0
  31. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/_apidoc_templates/package.rst_t +0 -0
  32. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/_apidoc_templates/toc.rst_t +0 -0
  33. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/_static/espressif-logo.svg +0 -0
  34. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/_static/theme_overrides.css +0 -0
  35. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/_templates/layout.html +0 -0
  36. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/conf_common.py +0 -0
  37. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/Makefile +0 -0
  38. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/conf.py +0 -0
  39. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/explanations/build.rst +0 -0
  40. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/explanations/config_rules.rst +0 -0
  41. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/explanations/dependency_driven_build.rst +0 -0
  42. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/explanations/find.rst +0 -0
  43. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/guides/1.x_to_2.x.md +0 -0
  44. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/guides/custom_app.md +0 -0
  45. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/index.rst +0 -0
  46. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/others/CHANGELOG.md +0 -0
  47. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/others/CONTRIBUTING.md +0 -0
  48. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/references/cli.rst +0 -0
  49. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/docs/en/references/manifest.rst +0 -0
  50. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/__main__.py +0 -0
  51. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/autocompletions.py +0 -0
  52. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/constants.py +0 -0
  53. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/junit/__init__.py +0 -0
  54. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/junit/report.py +0 -0
  55. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/junit/utils.py +0 -0
  56. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/log.py +0 -0
  57. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/manifest/soc_header.py +0 -0
  58. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/py.typed +0 -0
  59. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/session_args.py +0 -0
  60. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/utils.py +0 -0
  61. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/vendors/__init__.py +0 -0
  62. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/vendors/pydantic_sources.py +0 -0
  63. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/yaml/__init__.py +0 -0
  64. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/idf_build_apps/yaml/parser.py +0 -0
  65. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/license_header.txt +0 -0
  66. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/test_app.py +0 -0
  67. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/test_build.py +0 -0
  68. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/test_cmd.py +0 -0
  69. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/test_manifest.py +0 -0
  70. {idf_build_apps-2.10.2 → idf_build_apps-2.11.0}/tests/test_utils.py +0 -0
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v2.11.0 (2025-06-03)
6
+
7
+ ### Feat
8
+
9
+ - support extra_pythonpaths injection during the runtime
10
+
11
+ ## v2.10.3 (2025-06-03)
12
+
13
+ ### Fix
14
+
15
+ - app.target have higher precedence than target while `find_apps`
16
+ - respect FolderRule.DEFAULT_BUILD_TARGETS while validating app
17
+
18
+ ### Refactor
19
+
20
+ - move `FolderRule.DEFAULT_BUILD_TARGET` into contextvar
21
+
5
22
  ## v2.10.2 (2025-05-22)
6
23
 
7
24
  ### Perf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idf-build-apps
3
- Version: 2.10.2
3
+ Version: 2.11.0
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
@@ -101,7 +101,7 @@ This indicates that in the configuration file, you should specify it with the na
101
101
  Expand Environment Variables
102
102
  ******************************
103
103
 
104
- Some configuration options support environment variables. You can use environment variables in the configuration file by using the syntax ``${VAR_NAME}`` or ``$VAR_NAME``. Undeclared environment variables will be replaced with an empty string. For exmaple:
104
+ All configuration options support environment variables. You can use environment variables in the configuration file by using the syntax ``${VAR_NAME}`` or ``$VAR_NAME``. Undeclared environment variables will be replaced with an empty string. For example:
105
105
 
106
106
  .. code:: toml
107
107
 
@@ -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.2'
11
+ __version__ = '2.11.0'
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
 
@@ -39,7 +39,6 @@ LOGGER = logging.getLogger(__name__)
39
39
 
40
40
  class ValidateMethod(str, enum.Enum):
41
41
  TO_LIST = 'to_list'
42
- EXPAND_VARS = 'expand_vars'
43
42
 
44
43
 
45
44
  @dataclass
@@ -174,12 +173,17 @@ class BaseArguments(BaseSettings):
174
173
  if info.field_name and info.field_name in cls.model_fields:
175
174
  f = cls.model_fields[info.field_name]
176
175
  meta = get_meta(f)
176
+
177
+ # always expand vars for all fields
178
+ if isinstance(v, str):
179
+ v = expand_vars(v)
180
+ elif isinstance(v, list):
181
+ v = [expand_vars(item) if isinstance(item, str) else item for item in v]
182
+
177
183
  if meta and meta.validate_method:
178
184
  for method in meta.validate_method:
179
185
  if method == ValidateMethod.TO_LIST:
180
186
  v = to_list(v)
181
- elif method == ValidateMethod.EXPAND_VARS:
182
- v = expand_vars(v)
183
187
  else:
184
188
  raise NotImplementedError(f'Unknown validate method: {method}')
185
189
 
@@ -241,9 +245,7 @@ class DependencyDrivenBuildArguments(GlobalArguments):
241
245
  default=None, # type: ignore
242
246
  )
243
247
  manifest_rootpath: str = field(
244
- FieldMetadata(
245
- validate_method=[ValidateMethod.EXPAND_VARS],
246
- ),
248
+ None,
247
249
  description='Root path to resolve the relative paths defined in the manifest files. '
248
250
  'By default set to the current directory. Support environment variables.',
249
251
  default=os.curdir, # type: ignore
@@ -420,6 +422,15 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
420
422
  description='Filter the apps by target. By default set to "all"',
421
423
  default='all', # type: ignore
422
424
  )
425
+ extra_pythonpaths: t.Optional[t.List[str]] = field(
426
+ FieldMetadata(
427
+ validate_method=[ValidateMethod.TO_LIST],
428
+ nargs='+',
429
+ ),
430
+ description='space-separated list of additional Python paths to search for the app classes. '
431
+ 'Will be injected into the head of sys.path.',
432
+ default=None, # type: ignore
433
+ )
423
434
  build_system: t.Union[str, t.Type[App]] = field(
424
435
  None,
425
436
  description='Filter the apps by build system. By default set to "cmake". '
@@ -519,15 +530,17 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
519
530
  nargs='+',
520
531
  ),
521
532
  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`',
533
+ 'When not specified, the default value is the targets listed by `idf.py --list-targets`. '
534
+ 'Cannot be used together with --enable-preview-targets',
523
535
  default=None, # type: ignore
524
536
  )
525
537
  enable_preview_targets: bool = field(
526
538
  FieldMetadata(
527
539
  action='store_true',
528
540
  ),
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`',
541
+ description='When enabled, all targets will be enabled by default, '
542
+ 'including the preview targets. As the targets defined in `idf.py --list-targets --preview`. '
543
+ 'Cannot be used together with --default-build-targets',
531
544
  default=False, # type: ignore
532
545
  )
533
546
  disable_targets: t.Optional[t.List[str]] = field(
@@ -570,6 +583,14 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
570
583
  LOGGER.debug('--target is missing. Set --target as "all".')
571
584
  self.target = 'all'
572
585
 
586
+ # Validate mutual exclusivity of enable_preview_targets and default_build_targets
587
+ if self.enable_preview_targets and self.default_build_targets:
588
+ raise InvalidCommand(
589
+ 'Cannot specify both --enable-preview-targets and --default-build-targets at the same time. '
590
+ 'Please use only one of these options.'
591
+ )
592
+
593
+ reset_default_build_targets() # reset first then judge again
573
594
  if self.default_build_targets:
574
595
  default_build_targets = []
575
596
  for target in self.default_build_targets:
@@ -582,25 +603,30 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
582
603
  default_build_targets.append(target)
583
604
  self.default_build_targets = default_build_targets
584
605
  LOGGER.info('Overriding default build targets to %s', self.default_build_targets)
585
- FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
606
+ DEFAULT_BUILD_TARGETS.set(self.default_build_targets)
586
607
  elif self.enable_preview_targets:
587
608
  self.default_build_targets = deepcopy(ALL_TARGETS)
588
609
  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
610
+ DEFAULT_BUILD_TARGETS.set(self.default_build_targets) # type: ignore
593
611
 
594
- if self.disable_targets and FolderRule.DEFAULT_BUILD_TARGETS:
612
+ if self.disable_targets and DEFAULT_BUILD_TARGETS.get():
595
613
  LOGGER.info('Disable targets: %s', self.disable_targets)
596
614
  self.default_build_targets = [
597
- _target for _target in FolderRule.DEFAULT_BUILD_TARGETS if _target not in self.disable_targets
615
+ _target for _target in DEFAULT_BUILD_TARGETS.get() if _target not in self.disable_targets
598
616
  ]
599
- FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
617
+ DEFAULT_BUILD_TARGETS.set(self.default_build_targets)
600
618
 
601
619
  if self.override_sdkconfig_files or self.override_sdkconfig_items:
602
620
  SESSION_ARGS.set(self)
603
621
 
622
+ # update PYTHONPATH
623
+ if self.extra_pythonpaths:
624
+ LOGGER.debug('Adding extra Python paths: %s', self.extra_pythonpaths)
625
+ for path in self.extra_pythonpaths:
626
+ abs_path = to_absolute_path(path)
627
+ if abs_path not in sys.path:
628
+ sys.path.insert(0, abs_path)
629
+
604
630
  # load build system
605
631
  # here could be a string or a class of type App
606
632
  if not isinstance(self.build_system, str):
@@ -763,7 +789,6 @@ class BuildArguments(FindBuildArguments):
763
789
  FieldMetadata(
764
790
  deprecates={'collect_size_info': {}},
765
791
  hidden=True,
766
- validate_method=[ValidateMethod.EXPAND_VARS],
767
792
  ),
768
793
  description='Record size json filepath of the built apps to the specified file. '
769
794
  'Each line is a json string. Can expand placeholders @p. Support environment variables.',
@@ -775,7 +800,6 @@ class BuildArguments(FindBuildArguments):
775
800
  FieldMetadata(
776
801
  deprecates={'collect_app_info': {}},
777
802
  hidden=True,
778
- validate_method=[ValidateMethod.EXPAND_VARS],
779
803
  ),
780
804
  description='Record serialized app model of the built apps to the specified file. '
781
805
  'Each line is a json string. Can expand placeholders @p. Support environment variables.',
@@ -787,7 +811,6 @@ class BuildArguments(FindBuildArguments):
787
811
  FieldMetadata(
788
812
  deprecates={'junitxml': {}},
789
813
  hidden=True,
790
- validate_method=[ValidateMethod.EXPAND_VARS],
791
814
  ),
792
815
  description='Path to the junitxml file to record the build results. Can expand placeholder @p. '
793
816
  'Support environment variables.',
@@ -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,
@@ -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.2"
68
+ version = "2.11.0"
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.2',
41
+ version='2.11.0',
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():
@@ -95,12 +97,19 @@ def test_empty_argument():
95
97
  assert args.config_rules is None
96
98
 
97
99
 
98
- def test_build_args_expansion():
100
+ def test_build_args_expansion(monkeypatch):
101
+ monkeypatch.setenv('FOO', '2')
102
+
99
103
  args = BuildArguments(
100
- parallel_index=2, collect_app_info='@p.txt', junitxml='x_@p.txt', collect_size_info='@p_@p.txt'
104
+ parallel_index=2,
105
+ parallel_count='$FOO',
106
+ collect_app_info='@p.txt',
107
+ junitxml='x_@p.txt',
108
+ collect_size_info='@p_@p.txt',
101
109
  )
102
110
  assert args.collect_app_info == '2.txt'
103
111
  assert args.junitxml == 'x_2.txt'
112
+ assert args.parallel_count == 2
104
113
 
105
114
  args.parallel_index = 3
106
115
  assert args.collect_app_info == '3.txt'
@@ -162,6 +171,15 @@ modified_files = [
162
171
  assert args.deactivate_dependency_driven_build_by_components == ['baz']
163
172
 
164
173
 
174
+ def test_mutual_exclusivity_validation():
175
+ # Test that both options together raise InvalidCommand
176
+ with pytest.raises(InvalidCommand) as exc_info:
177
+ FindBuildArguments(enable_preview_targets=True, default_build_targets=['esp32'], paths=['.'])
178
+
179
+ assert 'Cannot specify both --enable-preview-targets and --default-build-targets' in str(exc_info.value)
180
+ assert 'Please use only one of these options' in str(exc_info.value)
181
+
182
+
165
183
  def test_build_targets_cli(tmp_path, monkeypatch):
166
184
  create_project('foo', tmp_path)
167
185
  with open(IDF_BUILD_APPS_TOML_FN, 'w') as fw:
@@ -411,6 +429,123 @@ dry_run = false
411
429
  assert test_suite.findall('testcase')[0].attrib['name'] == 'bar/build'
412
430
 
413
431
 
432
+ class TestDefaultBuildTargetsContextVar:
433
+ def test_direct_contextvar_access(self):
434
+ # Test initial value
435
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
436
+
437
+ # Test setting new values
438
+ test_targets = ['esp32', 'esp32s2']
439
+ DEFAULT_BUILD_TARGETS.set(test_targets)
440
+ assert DEFAULT_BUILD_TARGETS.get() == test_targets
441
+
442
+ # Test setting to ALL_TARGETS
443
+ DEFAULT_BUILD_TARGETS.set(ALL_TARGETS)
444
+ assert DEFAULT_BUILD_TARGETS.get() == ALL_TARGETS
445
+ assert len(DEFAULT_BUILD_TARGETS.get()) == len(SUPPORTED_TARGETS) + len(PREVIEW_TARGETS)
446
+
447
+ def test_folder_rule_backward_compatibility(self):
448
+ # Test initial access
449
+ assert FolderRule.DEFAULT_BUILD_TARGETS == SUPPORTED_TARGETS
450
+
451
+ # Test setting via contextvar
452
+ other_targets = ['esp32h2', 'esp32p4']
453
+ DEFAULT_BUILD_TARGETS.set(other_targets)
454
+ assert FolderRule.DEFAULT_BUILD_TARGETS == other_targets
455
+ assert DEFAULT_BUILD_TARGETS.get() == other_targets
456
+
457
+ # Test setting via FolderRule
458
+ test_targets = ['esp32c3', 'esp32c6']
459
+ FolderRule.DEFAULT_BUILD_TARGETS = test_targets
460
+ assert DEFAULT_BUILD_TARGETS.get() == test_targets
461
+ assert FolderRule.DEFAULT_BUILD_TARGETS == test_targets
462
+
463
+ def test_default_build_targets_option(self):
464
+ """Test that --default-build-targets option works correctly"""
465
+ test_targets = ['esp32', 'esp32s2', 'esp32c3']
466
+
467
+ args = FindBuildArguments(default_build_targets=test_targets, paths=['.'])
468
+
469
+ assert args.default_build_targets == test_targets
470
+ assert DEFAULT_BUILD_TARGETS.get() == test_targets
471
+ assert FolderRule.DEFAULT_BUILD_TARGETS == test_targets
472
+
473
+ def test_enable_preview_targets_option(self):
474
+ """Test that --enable-preview-targets option works correctly"""
475
+ args = FindBuildArguments(enable_preview_targets=True, paths=['.'])
476
+
477
+ assert args.enable_preview_targets is True
478
+ assert args.default_build_targets == ALL_TARGETS
479
+ assert DEFAULT_BUILD_TARGETS.get() == ALL_TARGETS
480
+ assert FolderRule.DEFAULT_BUILD_TARGETS == ALL_TARGETS
481
+ assert len(DEFAULT_BUILD_TARGETS.get()) == len(SUPPORTED_TARGETS) + len(PREVIEW_TARGETS)
482
+
483
+ def test_default_behavior(self):
484
+ """Test default behavior when no special options are provided"""
485
+ args = FindBuildArguments(paths=['.'])
486
+
487
+ assert args.enable_preview_targets is False
488
+ assert args.default_build_targets is None
489
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
490
+ assert FolderRule.DEFAULT_BUILD_TARGETS == SUPPORTED_TARGETS
491
+
492
+ def test_disable_targets_with_default_build_targets(self):
493
+ """Test --disable-targets option works with --default-build-targets"""
494
+ args = FindBuildArguments(
495
+ default_build_targets=['esp32', 'esp32s2', 'esp32c3'], disable_targets=['esp32s2'], paths=['.']
496
+ )
497
+
498
+ expected_targets = ['esp32', 'esp32c3']
499
+ assert args.default_build_targets == expected_targets
500
+ assert DEFAULT_BUILD_TARGETS.get() == expected_targets
501
+ assert FolderRule.DEFAULT_BUILD_TARGETS == expected_targets
502
+
503
+ def test_disable_targets_with_enable_preview_targets(self):
504
+ """Test --disable-targets option works with --enable-preview-targets"""
505
+ disabled_target = PREVIEW_TARGETS[0] # Disable first preview target
506
+
507
+ args = FindBuildArguments(enable_preview_targets=True, disable_targets=[disabled_target], paths=['.'])
508
+
509
+ expected_targets = [t for t in ALL_TARGETS if t != disabled_target]
510
+ assert args.default_build_targets == expected_targets
511
+ assert DEFAULT_BUILD_TARGETS.get() == expected_targets
512
+ assert len(DEFAULT_BUILD_TARGETS.get()) == len(ALL_TARGETS) - 1
513
+
514
+ def test_invalid_targets_filtering(self):
515
+ """Test that invalid targets are filtered out and warnings are logged"""
516
+ invalid_targets = ['esp32', 'invalid_target', 'esp32s2', 'another_invalid']
517
+
518
+ args = FindBuildArguments(default_build_targets=invalid_targets, paths=['.'])
519
+
520
+ # Only valid targets should remain
521
+ expected_targets = ['esp32', 'esp32s2']
522
+ assert args.default_build_targets == expected_targets
523
+ assert DEFAULT_BUILD_TARGETS.get() == expected_targets
524
+
525
+ def test_contextvar_isolation_between_instances(self):
526
+ """Test that the contextvar behaves correctly across multiple argument instances"""
527
+ # First instance sets default_build_targets
528
+ FindBuildArguments(default_build_targets=['esp32', 'esp32s2'])
529
+ assert DEFAULT_BUILD_TARGETS.get() == ['esp32', 'esp32s2']
530
+
531
+ # Second instance sets enable_preview_targets
532
+ FindBuildArguments(enable_preview_targets=True)
533
+ assert DEFAULT_BUILD_TARGETS.get() == ALL_TARGETS
534
+
535
+ # Third instance uses default behavior
536
+ FindBuildArguments()
537
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
538
+
539
+ def test_empty_default_build_targets(self):
540
+ """Test behavior with empty default_build_targets list"""
541
+ args = FindBuildArguments(default_build_targets=[])
542
+
543
+ # Empty list is treated as falsy, so it falls back to default behavior
544
+ assert args.default_build_targets == []
545
+ assert DEFAULT_BUILD_TARGETS.get() == SUPPORTED_TARGETS
546
+ assert FolderRule.DEFAULT_BUILD_TARGETS == SUPPORTED_TARGETS
547
+
548
+
414
549
  def test_expand_vars(monkeypatch):
415
550
  assert expand_vars('Value is $TEST_VAR') == 'Value is '
416
551
  monkeypatch.setenv('TEST_VAR', 'test_value')
@@ -2,6 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import os
5
+ import sys
5
6
  import tempfile
6
7
  from pathlib import (
7
8
  Path,
@@ -12,6 +13,7 @@ from conftest import (
12
13
  create_project,
13
14
  )
14
15
 
16
+ import idf_build_apps
15
17
  from idf_build_apps.constants import (
16
18
  DEFAULT_SDKCONFIG,
17
19
  IDF_PATH,
@@ -24,8 +26,9 @@ from idf_build_apps.manifest.manifest import Manifest
24
26
 
25
27
 
26
28
  class TestFindWithManifest:
27
- def test_manifest_rootpath_chdir(self, capsys):
28
- test_dir = Path(IDF_PATH) / 'examples' / 'get-started'
29
+ def test_manifest_rootpath_chdir(self, capsys, tmp_path):
30
+ test_dir = tmp_path / 'examples' / 'get-started'
31
+ create_project('hello_world', test_dir)
29
32
 
30
33
  yaml_file = test_dir / 'test.yml'
31
34
  yaml_file.write_text(
@@ -37,7 +40,7 @@ examples/get-started:
37
40
  encoding='utf8',
38
41
  )
39
42
 
40
- os.chdir(IDF_PATH)
43
+ os.chdir(tmp_path)
41
44
  assert not find_apps(str(test_dir), 'esp32', recursive=True, manifest_files=str(yaml_file))
42
45
  assert not capsys.readouterr().err
43
46
 
@@ -457,6 +460,21 @@ class TestFindWithSdkconfigFiles:
457
460
  except: # noqa
458
461
  pass
459
462
 
463
+ def test_build_preview_but_sdkconfig_default(self, tmp_path):
464
+ create_project('foo', tmp_path)
465
+
466
+ apps = find_apps(str(tmp_path / 'foo'), 'all', default_build_targets=['esp32'])
467
+ assert len(apps) == 1
468
+
469
+ with open(tmp_path / 'foo' / 'sdkconfig.defaults', 'w') as f:
470
+ f.write('CONFIG_IDF_TARGET="esp32p4"\n')
471
+
472
+ apps = find_apps(str(tmp_path / 'foo'), 'all', default_build_targets=['esp32'])
473
+ assert len(apps) == 0
474
+
475
+ apps = find_apps(str(tmp_path / 'foo'), 'esp32p4', default_build_targets=['esp32p4'])
476
+ assert len(apps) == 1
477
+
460
478
  def test_with_sdkconfig_defaults_idf_target_but_disabled(self, tmp_path):
461
479
  manifest_file = tmp_path / 'manifest.yml'
462
480
  manifest_file.write_text(
@@ -540,8 +558,6 @@ class TestFindWithSdkconfigFiles:
540
558
  assert len(apps) == 0
541
559
 
542
560
  def test_with_sdkconfig_override(self, tmp_path):
543
- import idf_build_apps
544
-
545
561
  create_project('test1', tmp_path)
546
562
  (tmp_path / 'test1' / 'sdkconfig.defaults').write_text(
547
563
  """
@@ -755,3 +771,104 @@ def test_find_apps_with_duplicated_paths(tmp_path):
755
771
  == len(find_apps(str(tmp_path / 'folder1'), 'esp32', recursive=True))
756
772
  == 2
757
773
  )
774
+
775
+
776
+ class TestFindWithExtraPythonPaths:
777
+ custom_app_code = """
778
+ import os
779
+ import typing as t
780
+ from idf_build_apps import App
781
+ from idf_build_apps.constants import BuildStatus
782
+ from idf_build_apps.utils import Literal
783
+
784
+
785
+ class ExtraPathTestApp(App):
786
+ build_system: Literal['extra_path_test'] = 'extra_path_test' # type: ignore
787
+
788
+ @property
789
+ def supported_targets(self) -> t.List[str]:
790
+ return ['esp32', 'esp32s2', 'esp32c3']
791
+
792
+ def build(self, *args, **kwargs):
793
+ if not self.dry_run:
794
+ os.makedirs(self.build_path, exist_ok=True)
795
+ with open(os.path.join(self.build_path, 'extra_path_test_marker.txt'), 'w') as f:
796
+ f.write('Extra path test build successful')
797
+ self.build_status = BuildStatus.SUCCESS
798
+
799
+ @classmethod
800
+ def is_app(cls, path: str) -> bool:
801
+ return True
802
+ """
803
+
804
+ @pytest.fixture
805
+ def setup_custom_module_and_app(self, tmp_path):
806
+ """Set up a custom module in a separate directory and a test app"""
807
+ # Create custom module directory (separate from app directory)
808
+ custom_module_dir = tmp_path / 'custom_modules'
809
+ custom_module_dir.mkdir()
810
+
811
+ # Create the custom module file
812
+ custom_module_file = custom_module_dir / 'extra_path_test_module.py'
813
+ custom_module_file.write_text(self.custom_app_code)
814
+
815
+ # Create test app directory
816
+ test_app_dir = tmp_path / 'test_app'
817
+ test_app_dir.mkdir()
818
+
819
+ # Create basic app structure
820
+ main_dir = test_app_dir / 'main'
821
+ main_dir.mkdir()
822
+ (main_dir / 'main.c').write_text('void app_main() {}')
823
+ (main_dir / 'CMakeLists.txt').write_text('idf_component_register(SRCS "main.c")')
824
+ (test_app_dir / 'CMakeLists.txt').write_text(
825
+ 'cmake_minimum_required(VERSION 3.16)\ninclude($ENV{IDF_PATH}/tools/cmake/project.cmake)\nproject(test_app)'
826
+ )
827
+
828
+ return {
829
+ 'custom_module_dir': str(custom_module_dir),
830
+ 'test_app_dir': str(test_app_dir),
831
+ 'custom_module_file': str(custom_module_file),
832
+ }
833
+
834
+ def test_extra_pythonpaths_with_custom_build_system(self, setup_custom_module_and_app):
835
+ """Test that extra_pythonpaths allows loading custom build system classes"""
836
+ setup = setup_custom_module_and_app
837
+
838
+ original_path = sys.path.copy()
839
+
840
+ try:
841
+ # First verify that without extra_pythonpaths, the module cannot be found
842
+ with pytest.raises(ImportError, match=r'Failed to import module extra_path_test_module'):
843
+ find_apps(
844
+ paths=[setup['test_app_dir']],
845
+ target='esp32',
846
+ build_system='extra_path_test_module:ExtraPathTestApp',
847
+ )
848
+
849
+ # Now test with extra_pythonpaths - this should work
850
+ apps = find_apps(
851
+ paths=[setup['test_app_dir']],
852
+ target='esp32',
853
+ build_system='extra_path_test_module:ExtraPathTestApp',
854
+ extra_pythonpaths=[setup['custom_module_dir']],
855
+ )
856
+
857
+ # Verify we found the app
858
+ assert len(apps) == 1
859
+ app = apps[0]
860
+
861
+ # Verify it's using our custom class
862
+ assert app.build_system == 'extra_path_test'
863
+ assert app.__class__.__name__ == 'ExtraPathTestApp'
864
+
865
+ # Verify the custom module dir was added to sys.path
866
+ assert setup['custom_module_dir'] in sys.path
867
+
868
+ # Test building the app
869
+ app.build(dry_run=True)
870
+ assert app.build_status == BuildStatus.SUCCESS
871
+
872
+ finally:
873
+ # Restore original sys.path
874
+ sys.path[:] = original_path
File without changes