idf-build-apps 2.9.0__tar.gz → 2.10.1__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.9.0 → idf_build_apps-2.10.1}/.pre-commit-config.yaml +1 -1
  2. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/CHANGELOG.md +12 -0
  3. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/PKG-INFO +3 -2
  4. idf_build_apps-2.10.1/docs/en/guides/custom_app.md +80 -0
  5. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/index.rst +2 -1
  6. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/__init__.py +3 -1
  7. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/app.py +8 -0
  8. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/args.py +56 -6
  9. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/main.py +33 -18
  10. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/pyproject.toml +3 -2
  11. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/setup.py +1 -1
  12. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/tests/test_app.py +1 -2
  13. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/tests/test_build.py +103 -0
  14. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.editorconfig +0 -0
  15. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.git-blame-ignore-revs +0 -0
  16. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.gitattributes +0 -0
  17. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.github/dependabot.yml +0 -0
  18. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.github/workflows/publish-pypi.yml +0 -0
  19. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.github/workflows/sync-jira.yml +0 -0
  20. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.github/workflows/test-build-docs.yml +0 -0
  21. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.github/workflows/test-build-idf-apps.yml +0 -0
  22. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.gitignore +0 -0
  23. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/.readthedocs.yml +0 -0
  24. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/CONTRIBUTING.md +0 -0
  25. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/LICENSE +0 -0
  26. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/README.md +0 -0
  27. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/_apidoc_templates/module.rst_t +0 -0
  28. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/_apidoc_templates/package.rst_t +0 -0
  29. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/_apidoc_templates/toc.rst_t +0 -0
  30. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/_static/espressif-logo.svg +0 -0
  31. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/_static/theme_overrides.css +0 -0
  32. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/_templates/layout.html +0 -0
  33. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/conf_common.py +0 -0
  34. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/Makefile +0 -0
  35. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/conf.py +0 -0
  36. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/explanations/build.rst +0 -0
  37. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/explanations/config_rules.rst +0 -0
  38. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/explanations/dependency_driven_build.rst +0 -0
  39. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/explanations/find.rst +0 -0
  40. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/guides/1.x_to_2.x.md +0 -0
  41. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/others/CHANGELOG.md +0 -0
  42. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/others/CONTRIBUTING.md +0 -0
  43. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/references/cli.rst +0 -0
  44. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/references/config_file.rst +0 -0
  45. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/docs/en/references/manifest.rst +0 -0
  46. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/__main__.py +0 -0
  47. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/autocompletions.py +0 -0
  48. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/constants.py +0 -0
  49. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/finder.py +0 -0
  50. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/junit/__init__.py +0 -0
  51. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/junit/report.py +0 -0
  52. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/junit/utils.py +0 -0
  53. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/log.py +0 -0
  54. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/manifest/__init__.py +0 -0
  55. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/manifest/manifest.py +0 -0
  56. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/manifest/soc_header.py +0 -0
  57. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/py.typed +0 -0
  58. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/session_args.py +0 -0
  59. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/utils.py +0 -0
  60. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/vendors/__init__.py +0 -0
  61. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/vendors/pydantic_sources.py +0 -0
  62. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/yaml/__init__.py +0 -0
  63. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/idf_build_apps/yaml/parser.py +0 -0
  64. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/license_header.txt +0 -0
  65. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/tests/conftest.py +0 -0
  66. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/tests/test_args.py +0 -0
  67. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/tests/test_cmd.py +0 -0
  68. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/tests/test_finder.py +0 -0
  69. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/tests/test_manifest.py +0 -0
  70. {idf_build_apps-2.9.0 → idf_build_apps-2.10.1}/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.5'
20
+ rev: 'v0.11.6'
21
21
  hooks:
22
22
  - id: ruff
23
23
  args: ['--fix']
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v2.10.1 (2025-05-05)
6
+
7
+ ### Fix
8
+
9
+ - cache custom app classes
10
+
11
+ ## v2.10.0 (2025-04-22)
12
+
13
+ ### Feat
14
+
15
+ - support custom class load from CLI
16
+
5
17
  ## v2.9.0 (2025-04-16)
6
18
 
7
19
  ### Feat
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idf-build-apps
3
- Version: 2.9.0
3
+ Version: 2.10.1
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
7
7
  Description-Content-Type: text/markdown
8
- Classifier: Development Status :: 2 - Pre-Alpha
8
+ Classifier: Development Status :: 5 - Production/Stable
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Programming Language :: Python :: 3.7
11
11
  Classifier: Programming Language :: Python :: 3.8
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
16
17
  License-File: LICENSE
17
18
  Requires-Dist: pyparsing
18
19
  Requires-Dist: pyyaml
@@ -0,0 +1,80 @@
1
+ # Custom App Classes
2
+
3
+ `idf-build-apps` allows you to create custom app classes by subclassing the base `App` class. This is useful when you need to implement custom build logic or handle special project types.
4
+
5
+ ## Creating a Custom App Class
6
+
7
+ Here's an example of creating a custom app class:
8
+
9
+ ```python
10
+ from idf_build_apps import App
11
+ from idf_build_apps.constants import BuildStatus
12
+ import os
13
+ from typing import Literal # Python 3.8+ only. from typing_extensions import Literal for earlier versions
14
+
15
+ class CustomApp(App):
16
+ build_system: Literal['custom'] = 'custom' # Must be unique to identify your custom app type
17
+
18
+ def build(self, *args, **kwargs):
19
+ # Implement your custom build logic here
20
+ os.makedirs(self.build_path, exist_ok=True)
21
+ with open(os.path.join(self.build_path, 'dummy.txt'), 'w') as f:
22
+ f.write('Custom build successful')
23
+ self.build_status = BuildStatus.SUCCESS
24
+ print('Custom build successful')
25
+
26
+ @classmethod
27
+ def is_app(cls, path: str) -> bool:
28
+ # Implement logic to determine if a path contains your custom app type
29
+ return True
30
+ ```
31
+
32
+ ## Using Custom App Classes
33
+
34
+ You can use custom app classes in two ways:
35
+
36
+ ### Via CLI
37
+
38
+ ```shell
39
+ idf-build-apps build -p /path/to/app --target esp32 --build-system custom:CustomApp
40
+ ```
41
+
42
+ Where `custom:CustomApp` is in the format `module:class`. The module must be in your Python path.
43
+
44
+ ### Via Python API
45
+
46
+ ```python
47
+ from idf_build_apps import find_apps
48
+
49
+ apps = find_apps(
50
+ paths=['/path/to/app'],
51
+ target='esp32',
52
+ build_system=CustomApp,
53
+ )
54
+
55
+ for app in apps:
56
+ app.build()
57
+ ```
58
+
59
+ ## Important Notes
60
+
61
+ - Your custom app class must subclass `App`
62
+ - The `build_system` attribute must be unique to identify your app type
63
+ - You must implement the `is_app()` class method to identify your app type
64
+ - For JSON serialization support, you need to pass your custom class to `json_to_app()` when deserializing
65
+
66
+ ## Example: JSON Serialization
67
+
68
+ ```python
69
+ from idf_build_apps import json_to_app
70
+
71
+ # Serialize
72
+ json_str = custom_app.to_json()
73
+
74
+ # Deserialize
75
+ deserialized_app = json_to_app(json_str, extra_classes=[CustomApp])
76
+ ```
77
+
78
+ ## Available Methods and Properties
79
+
80
+ Please refer to the [API reference of the class `App`](https://docs.espressif.com/projects/idf-build-apps/en/latest/references/api/idf_build_apps.html#idf_build_apps.app.App)
@@ -16,8 +16,9 @@ This documentation is for idf-build-apps. idf-build-apps is a tool that allows d
16
16
  .. toctree::
17
17
  :maxdepth: 1
18
18
  :caption: Guides
19
+ :glob:
19
20
 
20
- guides/1.x_to_2.x
21
+ guides/*
21
22
 
22
23
  .. toctree::
23
24
  :maxdepth: 1
@@ -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.9.0'
11
+ __version__ = '2.10.1'
12
12
 
13
13
  from .session_args import (
14
14
  SessionArgs,
@@ -28,6 +28,7 @@ from .log import (
28
28
  from .main import (
29
29
  build_apps,
30
30
  find_apps,
31
+ json_list_files_to_apps,
31
32
  json_to_app,
32
33
  )
33
34
 
@@ -38,6 +39,7 @@ __all__ = [
38
39
  'MakeApp',
39
40
  'build_apps',
40
41
  'find_apps',
42
+ 'json_list_files_to_apps',
41
43
  'json_to_app',
42
44
  'setup_logging',
43
45
  ]
@@ -1011,3 +1011,11 @@ class AppDeserializer(BaseModel):
1011
1011
  def from_json(cls, json_data: t.Union[str, bytes, bytearray]) -> App:
1012
1012
  json_dict = json.loads(json_data.strip())
1013
1013
  return cls.model_validate({'app': json_dict}).app
1014
+
1015
+ @classmethod
1016
+ def from_json_list(cls, json_list: t.Sequence[t.Union[str, bytes, bytearray]]) -> t.List[App]:
1017
+ apps = []
1018
+ for app_json in json_list:
1019
+ apps.append(cls.from_json(app_json))
1020
+
1021
+ return apps
@@ -4,6 +4,7 @@
4
4
  import argparse
5
5
  import enum
6
6
  import glob
7
+ import importlib
7
8
  import inspect
8
9
  import logging
9
10
  import os
@@ -27,7 +28,7 @@ from pydantic_settings import (
27
28
  )
28
29
  from typing_extensions import Concatenate, ParamSpec
29
30
 
30
- from . import SESSION_ARGS, App, setup_logging
31
+ from . import SESSION_ARGS, App, CMakeApp, MakeApp, setup_logging
31
32
  from .constants import ALL_TARGETS, IDF_BUILD_APPS_TOML_FN, SUPPORTED_TARGETS
32
33
  from .manifest.manifest import FolderRule, Manifest
33
34
  from .utils import InvalidCommand, files_matches_patterns, semicolon_separated_str_to_list, to_absolute_path, to_list
@@ -397,6 +398,12 @@ class DependencyDrivenBuildArguments(GlobalArguments):
397
398
 
398
399
 
399
400
  class FindBuildArguments(DependencyDrivenBuildArguments):
401
+ _KNOWN_APP_CLASSES: t.ClassVar[t.Dict[str, t.Type[App]]] = {
402
+ 'cmake': CMakeApp,
403
+ 'make': MakeApp,
404
+ }
405
+ _LOADED_MODULE_APPS: t.ClassVar[t.Dict[str, t.Type[App]]] = {}
406
+
400
407
  paths: t.List[str] = field(
401
408
  FieldMetadata(
402
409
  validate_method=[ValidateMethod.TO_LIST],
@@ -414,10 +421,9 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
414
421
  default='all', # type: ignore
415
422
  )
416
423
  build_system: t.Union[str, t.Type[App]] = field(
417
- FieldMetadata(
418
- choices=['cmake', 'make'],
419
- ),
420
- description='Filter the apps by build system. By default set to "cmake"',
424
+ None,
425
+ description='Filter the apps by build system. By default set to "cmake". '
426
+ 'Can be either "cmake", "make" or a custom App class path in format "module:class"',
421
427
  default='cmake', # type: ignore
422
428
  )
423
429
  recursive: bool = field(
@@ -587,12 +593,56 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
587
593
 
588
594
  if self.disable_targets and FolderRule.DEFAULT_BUILD_TARGETS:
589
595
  LOGGER.info('Disable targets: %s', self.disable_targets)
590
- self.default_build_targets = [t for t in FolderRule.DEFAULT_BUILD_TARGETS if t not in self.disable_targets]
596
+ self.default_build_targets = [
597
+ _target for _target in FolderRule.DEFAULT_BUILD_TARGETS if _target not in self.disable_targets
598
+ ]
591
599
  FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
592
600
 
593
601
  if self.override_sdkconfig_files or self.override_sdkconfig_items:
594
602
  SESSION_ARGS.set(self)
595
603
 
604
+ # load build system
605
+ # here could be a string or a class of type App
606
+ if not isinstance(self.build_system, str):
607
+ # do nothing, only cache
608
+ self._KNOWN_APP_CLASSES[self.build_system('', '').build_system] = self.build_system
609
+ return
610
+
611
+ # here could only be a string
612
+ if self.build_system in self._KNOWN_APP_CLASSES:
613
+ self.build_system = self._KNOWN_APP_CLASSES[self.build_system]
614
+ return
615
+
616
+ if ':' not in self.build_system:
617
+ raise ValueError(
618
+ f'Invalid build system: {self.build_system}. '
619
+ f'Known build systems: {", ".join(self._KNOWN_APP_CLASSES.keys())}'
620
+ )
621
+
622
+ # here could only be a string in format "module:class"
623
+ if self.build_system in self._LOADED_MODULE_APPS:
624
+ self.build_system = self._LOADED_MODULE_APPS[self.build_system]
625
+ return
626
+
627
+ # here could only be a string in format "module:class", and not loaded yet
628
+ module_path, class_name = self.build_system.split(':')
629
+ try:
630
+ module = importlib.import_module(module_path)
631
+ except ImportError as e:
632
+ raise ImportError(f'Failed to import module {module_path}. Error: {e!s}')
633
+
634
+ try:
635
+ app_cls = getattr(module, class_name)
636
+ if not issubclass(app_cls, App):
637
+ raise ValueError(f'Class {class_name} must be a subclass of App')
638
+ except (ValueError, AttributeError):
639
+ raise ValueError(f'Class {class_name} not found in module {module_path}')
640
+
641
+ self._LOADED_MODULE_APPS[self.build_system] = app_cls
642
+ self._KNOWN_APP_CLASSES[app_cls('', '').build_system] = app_cls
643
+
644
+ self.build_system = app_cls
645
+
596
646
 
597
647
  class FindArguments(FindBuildArguments):
598
648
  output: t.Optional[str] = field(
@@ -29,8 +29,6 @@ from idf_build_apps.args import (
29
29
  from .app import (
30
30
  App,
31
31
  AppDeserializer,
32
- CMakeApp,
33
- MakeApp,
34
32
  )
35
33
  from .autocompletions import activate_completions
36
34
  from .constants import ALL_TARGETS, BuildStatus, completion_instructions
@@ -88,18 +86,6 @@ def find_apps(
88
86
  **kwargs,
89
87
  )
90
88
 
91
- app_cls: t.Type[App]
92
- if isinstance(find_arguments.build_system, str):
93
- # backwards compatible
94
- if find_arguments.build_system == 'cmake':
95
- app_cls = CMakeApp
96
- elif find_arguments.build_system == 'make':
97
- app_cls = MakeApp
98
- else:
99
- raise ValueError('Only Support "make" and "cmake"')
100
- else:
101
- app_cls = find_arguments.build_system
102
-
103
89
  apps: t.Set[App] = set()
104
90
  if find_arguments.target == 'all':
105
91
  targets = ALL_TARGETS
@@ -115,7 +101,7 @@ def find_apps(
115
101
  _find_apps(
116
102
  _p,
117
103
  _t,
118
- app_cls=app_cls,
104
+ app_cls=find_arguments.build_system, # type: ignore
119
105
  args=find_arguments,
120
106
  )
121
107
  )
@@ -487,14 +473,43 @@ def json_to_app(json_str: str, extra_classes: t.Optional[t.List[t.Type[App]]] =
487
473
  :param extra_classes: extra App class
488
474
  :return: App object
489
475
  """
490
- types = [App, CMakeApp, MakeApp]
476
+ _known_classes = list(FindArguments()._KNOWN_APP_CLASSES.values())
491
477
  if extra_classes:
492
- types.extend(extra_classes)
478
+ _known_classes.extend(extra_classes)
493
479
 
494
480
  custom_deserializer = create_model(
495
481
  '_CustomDeserializer',
496
- app=(t.Union[tuple(types)], Field(discriminator='build_system')),
482
+ app=(t.Union[tuple(_known_classes)], Field(discriminator='build_system')),
497
483
  __base__=AppDeserializer,
498
484
  )
499
485
 
500
486
  return custom_deserializer.from_json(json_str)
487
+
488
+
489
+ def json_list_files_to_apps(
490
+ json_list_filepaths: t.List[str],
491
+ extra_classes: t.Optional[t.List[t.Type[App]]] = None,
492
+ ) -> t.List[App]:
493
+ """
494
+ Deserialize a list of json strings to App objects
495
+
496
+ :param json_list_filepaths: filepath to the files, each line is a json string
497
+ :param extra_classes: extra App class
498
+ :return: list of App object
499
+ """
500
+ _known_classes = list(FindArguments()._KNOWN_APP_CLASSES.values())
501
+ if extra_classes:
502
+ _known_classes.extend(extra_classes)
503
+
504
+ custom_deserializer = create_model(
505
+ '_CustomDeserializer',
506
+ app=(t.Union[tuple(_known_classes)], Field(discriminator='build_system')),
507
+ __base__=AppDeserializer,
508
+ )
509
+
510
+ jsons = []
511
+ for fp in json_list_filepaths:
512
+ with open(fp) as fr:
513
+ jsons.extend(fr.readlines())
514
+
515
+ return custom_deserializer.from_json_list(jsons)
@@ -10,7 +10,7 @@ authors = [
10
10
  readme = "README.md"
11
11
  license = {file = "LICENSE"}
12
12
  classifiers = [
13
- "Development Status :: 2 - Pre-Alpha",
13
+ "Development Status :: 5 - Production/Stable",
14
14
  "License :: OSI Approved :: Apache Software License",
15
15
  "Programming Language :: Python :: 3.7",
16
16
  "Programming Language :: Python :: 3.8",
@@ -18,6 +18,7 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3.10",
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
21
22
  ]
22
23
  dynamic = ["version", "description"]
23
24
  requires-python = ">=3.7"
@@ -64,7 +65,7 @@ idf-build-apps = "idf_build_apps:main.main"
64
65
 
65
66
  [tool.commitizen]
66
67
  name = "cz_conventional_commits"
67
- version = "2.9.0"
68
+ version = "2.10.1"
68
69
  tag_format = "v$version"
69
70
  version_files = [
70
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.9.0',
41
+ version='2.10.1',
42
42
  description='Tools for building ESP-IDF related apps.',
43
43
  author=None,
44
44
  author_email='Fu Hanxi <fuhanxi@espressif.com>',
@@ -79,8 +79,7 @@ def test_app_deserializer():
79
79
 
80
80
  with pytest.raises(
81
81
  ValidationError,
82
- match="Input tag 'custom' found using 'build_system' does not match "
83
- "any of the expected tags: 'unknown', 'cmake', 'make'",
82
+ match="Input tag 'custom' found using 'build_system' does not match any of the expected tags: 'cmake', 'make'",
84
83
  ):
85
84
  assert json_to_app(c.to_json()) == c
86
85
 
@@ -3,9 +3,11 @@
3
3
 
4
4
  import os
5
5
  import shutil
6
+ import subprocess
6
7
  from copy import (
7
8
  deepcopy,
8
9
  )
10
+ from pathlib import Path
9
11
  from xml.etree import (
10
12
  ElementTree,
11
13
  )
@@ -20,6 +22,7 @@ from idf_build_apps import (
20
22
  find_apps,
21
23
  )
22
24
  from idf_build_apps.app import (
25
+ App,
23
26
  CMakeApp,
24
27
  )
25
28
  from idf_build_apps.args import BuildArguments
@@ -27,6 +30,7 @@ from idf_build_apps.constants import (
27
30
  IDF_PATH,
28
31
  BuildStatus,
29
32
  )
33
+ from idf_build_apps.utils import Literal
30
34
 
31
35
 
32
36
  @pytest.mark.skipif(not shutil.which('idf.py'), reason='idf.py not found')
@@ -205,6 +209,105 @@ class TestBuild:
205
209
  assert test_suite.findall('testcase')[0].attrib['name'] == 'foo/bar/build'
206
210
 
207
211
 
212
+ class CustomClassApp(App):
213
+ build_system: Literal['custom_class'] = 'custom_class' # type: ignore
214
+
215
+ def build(self, *args, **kwargs):
216
+ # For testing, we'll just create a dummy build directory
217
+ if not self.dry_run:
218
+ os.makedirs(self.build_path, exist_ok=True)
219
+ with open(os.path.join(self.build_path, 'dummy.txt'), 'w') as f:
220
+ f.write('Custom build successful')
221
+ self.build_status = BuildStatus.SUCCESS
222
+ print('Custom build successful')
223
+
224
+ @classmethod
225
+ def is_app(cls, path: str) -> bool: # noqa: ARG003
226
+ return True
227
+
228
+
229
+ @pytest.mark.skipif(not shutil.which('idf.py'), reason='idf.py not found')
230
+ class TestBuildWithCustomApp:
231
+ custom_app_code = """
232
+ from idf_build_apps import App
233
+ import os
234
+ from idf_build_apps.constants import BuildStatus
235
+ from idf_build_apps.utils import Literal
236
+
237
+ class CustomApp(App):
238
+ build_system: Literal['custom'] = 'custom'
239
+
240
+ def build(self, *args, **kwargs):
241
+ # For testing, we'll just create a dummy build directory
242
+ if not self.dry_run:
243
+ os.makedirs(self.build_path, exist_ok=True)
244
+ with open(os.path.join(self.build_path, 'dummy.txt'), 'w') as f:
245
+ f.write('Custom build successful')
246
+ self.build_status = BuildStatus.SUCCESS
247
+ print('Custom build successful')
248
+
249
+ @classmethod
250
+ def is_app(cls, path: str) -> bool:
251
+ return True
252
+ """
253
+
254
+ @pytest.fixture(autouse=True)
255
+ def _setup(self, tmp_path: Path, monkeypatch):
256
+ os.chdir(tmp_path)
257
+
258
+ test_app = tmp_path / 'test_app'
259
+
260
+ test_app.mkdir()
261
+ (test_app / 'main' / 'main.c').parent.mkdir(parents=True)
262
+ (test_app / 'main' / 'main.c').write_text('void app_main() {}')
263
+
264
+ # Create a custom app module
265
+ custom_module = tmp_path / 'custom.py'
266
+ custom_module.write_text(self.custom_app_code)
267
+
268
+ monkeypatch.setenv('PYTHONPATH', os.getenv('PYTHONPATH', '') + os.pathsep + str(tmp_path))
269
+
270
+ return test_app
271
+
272
+ def test_custom_app_cli(self, tmp_path):
273
+ subprocess.run(
274
+ [
275
+ 'idf-build-apps',
276
+ 'build',
277
+ '-p',
278
+ 'test_app',
279
+ '--target',
280
+ 'esp32',
281
+ '--build-system',
282
+ 'custom:CustomApp',
283
+ ],
284
+ check=True,
285
+ )
286
+
287
+ assert (tmp_path / 'test_app' / 'build' / 'dummy.txt').exists()
288
+ assert (tmp_path / 'test_app' / 'build' / 'dummy.txt').read_text() == 'Custom build successful'
289
+
290
+ def test_custom_app_function(self, tmp_path):
291
+ # Import the custom app class
292
+ # Find and build the app using the imported CustomApp class
293
+ apps = find_apps(
294
+ paths=['test_app'],
295
+ target='esp32',
296
+ build_system=CustomClassApp,
297
+ )
298
+
299
+ assert len(apps) == 1
300
+ app = apps[0]
301
+ assert isinstance(app, CustomClassApp)
302
+ assert app.build_system == 'custom_class'
303
+
304
+ # Build the app
305
+ app.build()
306
+ assert app.build_status == BuildStatus.SUCCESS
307
+ assert (tmp_path / 'test_app' / 'build' / 'dummy.txt').exists()
308
+ assert (tmp_path / 'test_app' / 'build' / 'dummy.txt').read_text() == 'Custom build successful'
309
+
310
+
208
311
  def test_build_apps_collect_files_when_no_apps_built(tmp_path):
209
312
  os.chdir(tmp_path)
210
313
 
File without changes