idf-build-apps 2.8.1__tar.gz → 2.10.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 (71) hide show
  1. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.pre-commit-config.yaml +2 -2
  2. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/CHANGELOG.md +13 -0
  3. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/PKG-INFO +1 -1
  4. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/conf_common.py +1 -1
  5. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/conf.py +1 -1
  6. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/explanations/config_rules.rst +6 -0
  7. idf_build_apps-2.10.0/docs/en/guides/custom_app.md +80 -0
  8. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/index.rst +2 -1
  9. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/references/config_file.rst +12 -0
  10. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/__init__.py +1 -1
  11. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/__main__.py +1 -1
  12. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/args.py +35 -8
  13. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/autocompletions.py +1 -1
  14. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/finder.py +1 -1
  15. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/junit/__init__.py +1 -1
  16. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/junit/utils.py +1 -1
  17. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/main.py +14 -2
  18. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/manifest/manifest.py +8 -1
  19. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/session_args.py +1 -1
  20. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/yaml/__init__.py +1 -1
  21. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/yaml/parser.py +1 -1
  22. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/pyproject.toml +1 -1
  23. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/setup.py +1 -1
  24. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/test_args.py +10 -0
  25. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/test_build.py +103 -0
  26. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/test_cmd.py +1 -1
  27. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/test_finder.py +0 -3
  28. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/test_manifest.py +22 -0
  29. idf_build_apps-2.8.1/.github/workflows/check-pre-commit.yml +0 -18
  30. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.editorconfig +0 -0
  31. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.git-blame-ignore-revs +0 -0
  32. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.gitattributes +0 -0
  33. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.github/dependabot.yml +0 -0
  34. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.github/workflows/publish-pypi.yml +0 -0
  35. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.github/workflows/sync-jira.yml +0 -0
  36. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.github/workflows/test-build-docs.yml +0 -0
  37. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.github/workflows/test-build-idf-apps.yml +0 -0
  38. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.gitignore +0 -0
  39. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/.readthedocs.yml +0 -0
  40. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/CONTRIBUTING.md +0 -0
  41. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/LICENSE +0 -0
  42. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/README.md +0 -0
  43. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/_apidoc_templates/module.rst_t +0 -0
  44. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/_apidoc_templates/package.rst_t +0 -0
  45. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/_apidoc_templates/toc.rst_t +0 -0
  46. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/_static/espressif-logo.svg +0 -0
  47. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/_static/theme_overrides.css +0 -0
  48. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/_templates/layout.html +0 -0
  49. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/Makefile +0 -0
  50. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/explanations/build.rst +0 -0
  51. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/explanations/dependency_driven_build.rst +0 -0
  52. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/explanations/find.rst +0 -0
  53. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/guides/1.x_to_2.x.md +0 -0
  54. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/others/CHANGELOG.md +0 -0
  55. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/others/CONTRIBUTING.md +0 -0
  56. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/references/cli.rst +0 -0
  57. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/docs/en/references/manifest.rst +0 -0
  58. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/app.py +0 -0
  59. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/constants.py +0 -0
  60. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/junit/report.py +0 -0
  61. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/log.py +0 -0
  62. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/manifest/__init__.py +0 -0
  63. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/manifest/soc_header.py +0 -0
  64. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/py.typed +0 -0
  65. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/utils.py +0 -0
  66. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/vendors/__init__.py +0 -0
  67. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/idf_build_apps/vendors/pydantic_sources.py +0 -0
  68. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/license_header.txt +0 -0
  69. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/conftest.py +0 -0
  70. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/test_app.py +0 -0
  71. {idf_build_apps-2.8.1 → idf_build_apps-2.10.0}/tests/test_utils.py +0 -0
@@ -16,8 +16,8 @@ repos:
16
16
  - license_header.txt # defaults to: LICENSE.txt
17
17
  - --use-current-year
18
18
  exclude: 'idf_build_apps/vendors/'
19
- - repo: https://github.com/charliermarsh/ruff-pre-commit
20
- rev: 'v0.9.5'
19
+ - repo: https://github.com/astral-sh/ruff-pre-commit
20
+ rev: 'v0.11.6'
21
21
  hooks:
22
22
  - id: ruff
23
23
  args: ['--fix']
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v2.10.0 (2025-04-22)
6
+
7
+ ### Feat
8
+
9
+ - support custom class load from CLI
10
+
11
+ ## v2.9.0 (2025-04-16)
12
+
13
+ ### Feat
14
+
15
+ - record manifest_path that introduced the folder rule
16
+ - support env var expansion in some fields
17
+
5
18
  ## v2.8.1 (2025-03-04)
6
19
 
7
20
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idf-build-apps
3
- Version: 2.8.1
3
+ Version: 2.10.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
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import os
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import os
@@ -146,6 +146,12 @@ When building the project for the ESP32 target, `sdkconfig files`_ with the ``.e
146
146
  B --> C --> D -- "populates sdkconfig file, then build" --> F
147
147
  B --> E -- "populates sdkconfig file, then build" --> G
148
148
 
149
+ .. warning::
150
+
151
+ Standalone target-specific sdkconfig files are ignored. To make the target-specific sdkconfig files effective, the original sdkconfig file, (without the target name suffix) must be present.
152
+
153
+ For example, ``sdkconfig.ci.foo.esp32`` will only be taken into account while building with target ``esp32`` if ``sdkconfig.ci.foo`` is also present.
154
+
149
155
  Override In CLI
150
156
  ===============
151
157
 
@@ -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
@@ -97,6 +97,18 @@ This indicates that in the configuration file, you should specify it with the na
97
97
  "bar",
98
98
  ]
99
99
 
100
+ ******************************
101
+ Expand Environment Variables
102
+ ******************************
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:
105
+
106
+ .. code:: toml
107
+
108
+ collect_app_info_filename = "app_info_${CI_JOB_NAME_SLUG}"
109
+
110
+ when the environment variable ``CI_JOB_NAME_SLUG`` is set to ``my_job``, the ``collect_app_info_filename`` will be expanded to ``app_info_my_job``. When the environment variable is not set, the value will be ``app_info_``.
111
+
100
112
  *************************
101
113
  CLI Argument Precedence
102
114
  *************************
@@ -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.8.1'
11
+ __version__ = '2.10.0'
12
12
 
13
13
  from .session_args import (
14
14
  SessionArgs,
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  from .main import (
@@ -14,6 +14,7 @@ from copy import deepcopy
14
14
  from dataclasses import dataclass
15
15
  from io import TextIOWrapper
16
16
  from pathlib import Path
17
+ from string import Template
17
18
  from typing import Any
18
19
 
19
20
  from pydantic import AliasChoices, Field, computed_field, field_validator
@@ -112,6 +113,29 @@ def get_meta(f: FieldInfo) -> t.Optional[FieldMetadata]:
112
113
  return None
113
114
 
114
115
 
116
+ def expand_vars(v: t.Optional[str]) -> t.Optional[str]:
117
+ """
118
+ Expand environment variables in the string. If the variable is not found, use an empty string.
119
+
120
+ :param v: string to expand
121
+ :return: expanded string or None if the input is None
122
+ """
123
+ if v is None:
124
+ return None
125
+
126
+ unknown_vars: t.Dict[str, str] = dict()
127
+ while True:
128
+ try:
129
+ v = Template(v).substitute(os.environ, **unknown_vars)
130
+ except KeyError as e:
131
+ LOGGER.debug('Environment variable %s not found. use empty string', e)
132
+ unknown_vars[e.args[0]] = ''
133
+ else:
134
+ break
135
+
136
+ return v
137
+
138
+
115
139
  class BaseArguments(BaseSettings):
116
140
  """Base settings class for all settings classes"""
117
141
 
@@ -154,7 +178,7 @@ class BaseArguments(BaseSettings):
154
178
  if method == ValidateMethod.TO_LIST:
155
179
  v = to_list(v)
156
180
  elif method == ValidateMethod.EXPAND_VARS:
157
- v = os.path.expandvars(v)
181
+ v = expand_vars(v)
158
182
  else:
159
183
  raise NotImplementedError(f'Unknown validate method: {method}')
160
184
 
@@ -390,10 +414,9 @@ class FindBuildArguments(DependencyDrivenBuildArguments):
390
414
  default='all', # type: ignore
391
415
  )
392
416
  build_system: t.Union[str, t.Type[App]] = field(
393
- FieldMetadata(
394
- choices=['cmake', 'make'],
395
- ),
396
- description='Filter the apps by build system. By default set to "cmake"',
417
+ None,
418
+ description='Filter the apps by build system. By default set to "cmake". '
419
+ 'Can be either "cmake", "make" or a custom App class path in format "module:class"',
397
420
  default='cmake', # type: ignore
398
421
  )
399
422
  recursive: bool = field(
@@ -689,9 +712,10 @@ class BuildArguments(FindBuildArguments):
689
712
  FieldMetadata(
690
713
  deprecates={'collect_size_info': {}},
691
714
  hidden=True,
715
+ validate_method=[ValidateMethod.EXPAND_VARS],
692
716
  ),
693
717
  description='Record size json filepath of the built apps to the specified file. '
694
- 'Each line is a json string. Can expand placeholders @p',
718
+ 'Each line is a json string. Can expand placeholders @p. Support environment variables.',
695
719
  validation_alias=AliasChoices('collect_size_info_filename', 'collect_size_info'),
696
720
  default=None, # type: ignore
697
721
  exclude=True, # computed field is used
@@ -700,9 +724,10 @@ class BuildArguments(FindBuildArguments):
700
724
  FieldMetadata(
701
725
  deprecates={'collect_app_info': {}},
702
726
  hidden=True,
727
+ validate_method=[ValidateMethod.EXPAND_VARS],
703
728
  ),
704
729
  description='Record serialized app model of the built apps to the specified file. '
705
- 'Each line is a json string. Can expand placeholders @p',
730
+ 'Each line is a json string. Can expand placeholders @p. Support environment variables.',
706
731
  validation_alias=AliasChoices('collect_app_info_filename', 'collect_app_info'),
707
732
  default=None, # type: ignore
708
733
  exclude=True, # computed field is used
@@ -711,8 +736,10 @@ class BuildArguments(FindBuildArguments):
711
736
  FieldMetadata(
712
737
  deprecates={'junitxml': {}},
713
738
  hidden=True,
739
+ validate_method=[ValidateMethod.EXPAND_VARS],
714
740
  ),
715
- description='Path to the junitxml file to record the build results. Can expand placeholder @p',
741
+ description='Path to the junitxml file to record the build results. Can expand placeholder @p. '
742
+ 'Support environment variables.',
716
743
  validation_alias=AliasChoices('junitxml_filename', 'junitxml'),
717
744
  default=None, # type: ignore
718
745
  exclude=True, # computed field is used
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import os
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import logging
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  from .report import (
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import os
@@ -4,6 +4,7 @@
4
4
  # SPDX-License-Identifier: Apache-2.0
5
5
 
6
6
  import argparse
7
+ import importlib
7
8
  import json
8
9
  import logging
9
10
  import os
@@ -90,13 +91,24 @@ def find_apps(
90
91
 
91
92
  app_cls: t.Type[App]
92
93
  if isinstance(find_arguments.build_system, str):
93
- # backwards compatible
94
94
  if find_arguments.build_system == 'cmake':
95
95
  app_cls = CMakeApp
96
96
  elif find_arguments.build_system == 'make':
97
97
  app_cls = MakeApp
98
+ elif ':' in find_arguments.build_system:
99
+ # Custom App class in format "module:class"
100
+ try:
101
+ module_path, class_name = find_arguments.build_system.split(':')
102
+ module = importlib.import_module(module_path)
103
+ app_cls = getattr(module, class_name)
104
+ if not issubclass(app_cls, App):
105
+ raise ValueError(f'Class {class_name} must be a subclass of App')
106
+ except (ValueError, ImportError, AttributeError) as e:
107
+ raise ValueError(f'Invalid custom App class path: {find_arguments.build_system}. Error: {e!s}')
98
108
  else:
99
- raise ValueError('Only Support "make" and "cmake"')
109
+ raise ValueError(
110
+ 'build_system must be either "cmake", "make" or a custom App class path in format "module:class"'
111
+ )
100
112
  else:
101
113
  app_cls = find_arguments.build_system
102
114
 
@@ -89,7 +89,10 @@ class FolderRule:
89
89
  disable_test: t.Optional[t.List[t.Dict[str, t.Any]]] = None,
90
90
  depends_components: t.Optional[t.List[t.Union[str, t.Dict[str, t.Any]]]] = None,
91
91
  depends_filepatterns: t.Optional[t.List[t.Union[str, t.Dict[str, t.Any]]]] = None,
92
+ manifest_filepath: t.Optional[str] = None,
92
93
  ) -> None:
94
+ self._manifest_filepath = manifest_filepath
95
+
93
96
  self.folder = os.path.abspath(folder)
94
97
 
95
98
  def _clause_to_if_clause(clause: t.Dict[str, t.Any]) -> IfClause:
@@ -168,6 +171,10 @@ class FolderRule:
168
171
  def __repr__(self) -> str:
169
172
  return f'FolderRule({self.folder})'
170
173
 
174
+ @property
175
+ def by_manifest_file(self) -> t.Optional[str]:
176
+ return self._manifest_filepath
177
+
171
178
  def _enable_build(self, target: str, config_name: str) -> bool:
172
179
  if self.enable:
173
180
  res = False
@@ -309,7 +316,7 @@ class Manifest:
309
316
  LOGGER.warning(msg)
310
317
 
311
318
  try:
312
- rules.append(FolderRule(folder, **folder_rule if folder_rule else {}))
319
+ rules.append(FolderRule(folder, **folder_rule if folder_rule else {}, manifest_filepath=str(path)))
313
320
  except InvalidIfClause as e:
314
321
  raise InvalidManifest(f'Invalid manifest file {path}: {e}')
315
322
 
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import logging
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  from .parser import (
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import typing as t
@@ -64,7 +64,7 @@ idf-build-apps = "idf_build_apps:main.main"
64
64
 
65
65
  [tool.commitizen]
66
66
  name = "cz_conventional_commits"
67
- version = "2.8.1"
67
+ version = "2.10.0"
68
68
  tag_format = "v$version"
69
69
  version_files = [
70
70
  "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.8.1',
41
+ version='2.10.0',
42
42
  description='Tools for building ESP-IDF related apps.',
43
43
  author=None,
44
44
  author_email='Fu Hanxi <fuhanxi@espressif.com>',
@@ -15,6 +15,7 @@ from idf_build_apps.args import (
15
15
  DependencyDrivenBuildArguments,
16
16
  FindArguments,
17
17
  FindBuildArguments,
18
+ expand_vars,
18
19
  )
19
20
  from idf_build_apps.constants import IDF_BUILD_APPS_TOML_FN, PREVIEW_TARGETS, SUPPORTED_TARGETS
20
21
  from idf_build_apps.main import main
@@ -408,3 +409,12 @@ dry_run = false
408
409
  assert test_suite.attrib['errors'] == '0'
409
410
  assert test_suite.attrib['skipped'] == '1'
410
411
  assert test_suite.findall('testcase')[0].attrib['name'] == 'bar/build'
412
+
413
+
414
+ def test_expand_vars(monkeypatch):
415
+ assert expand_vars('Value is $TEST_VAR') == 'Value is '
416
+ monkeypatch.setenv('TEST_VAR', 'test_value')
417
+ assert expand_vars('Value is $TEST_VAR') == 'Value is test_value'
418
+ assert expand_vars('Value is $TEST_VAR and $NON_EXISTING_VAR') == 'Value is test_value and '
419
+ assert expand_vars('No variables here') == 'No variables here'
420
+ assert expand_vars('') == ''
@@ -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
 
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  import os
4
4
  import sys
@@ -1,7 +1,6 @@
1
1
  # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import logging
5
4
  import os
6
5
  import tempfile
7
6
  from pathlib import (
@@ -443,7 +442,6 @@ class TestFindWithSdkconfigFiles:
443
442
  if not os.path.isfile(default_sdkconfig_path):
444
443
  with open(default_sdkconfig_path, 'w') as fw:
445
444
  fw.write('CONFIG_IDF_TARGET="linux"')
446
- logging.info('Created temp %s %s', DEFAULT_SDKCONFIG, default_sdkconfig_path)
447
445
  _app = app
448
446
  _default_sdkconfig_path = default_sdkconfig_path
449
447
  break
@@ -456,7 +454,6 @@ class TestFindWithSdkconfigFiles:
456
454
  finally:
457
455
  try:
458
456
  os.remove(_default_sdkconfig_path)
459
- logging.info('Removed temp %s %s', DEFAULT_SDKCONFIG, _default_sdkconfig_path)
460
457
  except: # noqa
461
458
  pass
462
459
 
@@ -535,6 +535,28 @@ baz:
535
535
  }
536
536
 
537
537
 
538
+ def test_folder_rule_introduced_by(tmp_path):
539
+ yaml_file = tmp_path / 'test.yml'
540
+ yaml_file.write_text(
541
+ """
542
+ foo:
543
+ enable:
544
+ - if: IDF_TARGET == "esp32"
545
+ - if: IDF_TARGET == "esp32c3"
546
+ bar:
547
+ enable:
548
+ - if: IDF_TARGET == "esp32"
549
+ baz:
550
+ enable:
551
+ - if: IDF_TARGET == "esp32"
552
+ """,
553
+ encoding='utf8',
554
+ )
555
+
556
+ manifest = Manifest.from_file(yaml_file)
557
+ assert manifest.most_suitable_rule('baz').by_manifest_file == str(yaml_file)
558
+
559
+
538
560
  class TestIfParser:
539
561
  def test_invalid_if_statement(self):
540
562
  statement = '1'
@@ -1,18 +0,0 @@
1
- name: pre-commit check
2
-
3
- on:
4
- pull_request:
5
-
6
- jobs:
7
- pre-commit:
8
- runs-on: ubuntu-latest
9
- steps:
10
- - uses: actions/checkout@v4
11
- with:
12
- fetch-depth: 0
13
- - uses: actions/setup-python@v5
14
- - id: changed-files
15
- uses: tj-actions/changed-files@v45
16
- - uses: pre-commit/action@v3.0.1
17
- with:
18
- extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }}
File without changes