idf-build-apps 2.4.3__py3-none-any.whl → 2.5.0__py3-none-any.whl
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.
- idf_build_apps/__init__.py +1 -1
- idf_build_apps/app.py +31 -27
- idf_build_apps/args.py +846 -0
- idf_build_apps/constants.py +18 -28
- idf_build_apps/finder.py +34 -54
- idf_build_apps/log.py +2 -0
- idf_build_apps/main.py +159 -624
- idf_build_apps/manifest/if_parser.py +14 -13
- idf_build_apps/manifest/manifest.py +124 -28
- idf_build_apps/utils.py +27 -1
- idf_build_apps/vendors/__init__.py +0 -0
- idf_build_apps/vendors/pydantic_sources.py +120 -0
- idf_build_apps/yaml/parser.py +3 -1
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0.dist-info}/METADATA +4 -4
- idf_build_apps-2.5.0.dist-info/RECORD +27 -0
- idf_build_apps/build_apps_args.py +0 -64
- idf_build_apps/config.py +0 -91
- idf_build_apps-2.4.3.dist-info/RECORD +0 -26
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0.dist-info}/LICENSE +0 -0
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0.dist-info}/WHEEL +0 -0
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0.dist-info}/entry_points.txt +0 -0
idf_build_apps/args.py
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import enum
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import typing as t
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from io import TextIOWrapper
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pydantic import AliasChoices, Field, computed_field, field_validator
|
|
19
|
+
from pydantic.fields import FieldInfo
|
|
20
|
+
from pydantic_core.core_schema import ValidationInfo
|
|
21
|
+
from pydantic_settings import (
|
|
22
|
+
BaseSettings,
|
|
23
|
+
PydanticBaseSettingsSource,
|
|
24
|
+
SettingsConfigDict,
|
|
25
|
+
)
|
|
26
|
+
from typing_extensions import Concatenate, ParamSpec
|
|
27
|
+
|
|
28
|
+
from . import SESSION_ARGS, App, setup_logging
|
|
29
|
+
from .constants import ALL_TARGETS
|
|
30
|
+
from .manifest.manifest import FolderRule, Manifest
|
|
31
|
+
from .utils import InvalidCommand, files_matches_patterns, semicolon_separated_str_to_list, to_absolute_path, to_list
|
|
32
|
+
from .vendors.pydantic_sources import PyprojectTomlConfigSettingsSource, TomlConfigSettingsSource
|
|
33
|
+
|
|
34
|
+
LOGGER = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ValidateMethod(str, enum.Enum):
|
|
38
|
+
TO_LIST = 'to_list'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class FieldMetadata:
|
|
43
|
+
"""
|
|
44
|
+
dataclass field metadata. All fields are optional.
|
|
45
|
+
Some fields are used in argparse while running :func:`add_args_to_parser`.
|
|
46
|
+
|
|
47
|
+
:param validate_method: validate method for the field
|
|
48
|
+
:param deprecates: deprecates field names, used in argparse
|
|
49
|
+
:param shorthand: shorthand for the argument, used in argparse
|
|
50
|
+
:param action: action for the argument, used in argparse
|
|
51
|
+
:param nargs: nargs for the argument, used in argparse
|
|
52
|
+
:param choices: choices for the argument, used in argparse
|
|
53
|
+
:param type: type for the argument, used in argparse
|
|
54
|
+
:param required: whether the argument is required, used in argparse
|
|
55
|
+
:param default: default value for the argument, used in argparse
|
|
56
|
+
:param hidden: whether the argument is hidden, used in argparse
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# validate method
|
|
60
|
+
validate_method: t.Optional[t.List[str]] = None
|
|
61
|
+
# the field description will be copied from the deprecates field if not specified
|
|
62
|
+
deprecates: t.Optional[t.Dict[str, t.Dict[str, t.Any]]] = None
|
|
63
|
+
shorthand: t.Optional[str] = None
|
|
64
|
+
# argparse_kwargs
|
|
65
|
+
action: t.Optional[str] = None
|
|
66
|
+
nargs: t.Optional[str] = None
|
|
67
|
+
choices: t.Optional[t.List[str]] = None
|
|
68
|
+
type: t.Optional[t.Callable] = None
|
|
69
|
+
required: bool = False
|
|
70
|
+
# usually default is not needed. only set it when different from the default value of the field
|
|
71
|
+
default: t.Any = None
|
|
72
|
+
# hidden field, use deprecated instead, or hide it in the argparse
|
|
73
|
+
hidden: bool = False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
P = ParamSpec('P')
|
|
77
|
+
T = t.TypeVar('T')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _wrap_with_metadata(
|
|
81
|
+
_: t.Callable[P, t.Any],
|
|
82
|
+
) -> t.Callable[[t.Callable[..., T]], t.Callable[Concatenate[t.Optional[FieldMetadata], P], T]]:
|
|
83
|
+
"""Patch the function signature with metadata args"""
|
|
84
|
+
|
|
85
|
+
def return_func(func: t.Callable[..., T]) -> t.Callable[Concatenate[t.Optional[FieldMetadata], P], T]:
|
|
86
|
+
return t.cast(t.Callable[Concatenate[t.Optional[FieldMetadata], P], T], func)
|
|
87
|
+
|
|
88
|
+
return return_func
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@_wrap_with_metadata(Field)
|
|
92
|
+
def field(meta: t.Optional[FieldMetadata], *args, **kwargs):
|
|
93
|
+
"""field with metadata"""
|
|
94
|
+
f = Field(*args, **kwargs)
|
|
95
|
+
f.metadata.append(meta)
|
|
96
|
+
return f
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_meta(f: FieldInfo) -> t.Optional[FieldMetadata]:
|
|
100
|
+
"""
|
|
101
|
+
Get the metadata of the field
|
|
102
|
+
|
|
103
|
+
:param f: field
|
|
104
|
+
:return: metadata of the field if exists, None otherwise
|
|
105
|
+
"""
|
|
106
|
+
for m in f.metadata:
|
|
107
|
+
if isinstance(m, FieldMetadata):
|
|
108
|
+
return m
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BaseArguments(BaseSettings):
|
|
114
|
+
"""Base settings class for all settings classes"""
|
|
115
|
+
|
|
116
|
+
model_config = SettingsConfigDict(
|
|
117
|
+
toml_file='.idf_build_apps.toml',
|
|
118
|
+
pyproject_toml_table_header=('tool', 'idf-build-apps'),
|
|
119
|
+
pyproject_toml_depth=sys.maxsize,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def settings_customise_sources(
|
|
124
|
+
cls,
|
|
125
|
+
settings_cls: t.Type[BaseSettings],
|
|
126
|
+
init_settings: PydanticBaseSettingsSource,
|
|
127
|
+
env_settings: PydanticBaseSettingsSource, # noqa: ARG003
|
|
128
|
+
dotenv_settings: PydanticBaseSettingsSource, # noqa: ARG003
|
|
129
|
+
file_secret_settings: PydanticBaseSettingsSource, # noqa: ARG003
|
|
130
|
+
) -> t.Tuple[PydanticBaseSettingsSource, ...]:
|
|
131
|
+
return (
|
|
132
|
+
init_settings,
|
|
133
|
+
TomlConfigSettingsSource(settings_cls),
|
|
134
|
+
PyprojectTomlConfigSettingsSource(settings_cls),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@field_validator('*', mode='before')
|
|
138
|
+
@classmethod
|
|
139
|
+
def validate_by_validate_methods(cls, v: t.Any, info: ValidationInfo):
|
|
140
|
+
if info.field_name and info.field_name in cls.model_fields:
|
|
141
|
+
f = cls.model_fields[info.field_name]
|
|
142
|
+
meta = get_meta(f)
|
|
143
|
+
if meta and meta.validate_method and ValidateMethod.TO_LIST in meta.validate_method:
|
|
144
|
+
return to_list(v)
|
|
145
|
+
|
|
146
|
+
return v
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class GlobalArguments(BaseArguments):
|
|
150
|
+
verbose: int = field(
|
|
151
|
+
FieldMetadata(
|
|
152
|
+
shorthand='-v',
|
|
153
|
+
action='count',
|
|
154
|
+
),
|
|
155
|
+
description='Verbosity level. By default set to WARNING. Specify -v for INFO, -vv for DEBUG',
|
|
156
|
+
default=0,
|
|
157
|
+
)
|
|
158
|
+
log_file: t.Optional[str] = field(
|
|
159
|
+
None,
|
|
160
|
+
description='Path to the log file, if not specified logs will be printed to stderr',
|
|
161
|
+
default=None,
|
|
162
|
+
)
|
|
163
|
+
no_color: bool = field(
|
|
164
|
+
FieldMetadata(
|
|
165
|
+
action='store_true',
|
|
166
|
+
),
|
|
167
|
+
description='Disable colored output',
|
|
168
|
+
default=False,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def model_post_init(self, __context: Any) -> None:
|
|
172
|
+
super().model_post_init(__context)
|
|
173
|
+
|
|
174
|
+
setup_logging(self.verbose, self.log_file, not self.no_color)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class DependencyDrivenBuildArguments(GlobalArguments):
|
|
178
|
+
manifest_files: t.Optional[t.List[t.Union[Path, str]]] = field(
|
|
179
|
+
FieldMetadata(
|
|
180
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
181
|
+
deprecates={
|
|
182
|
+
'manifest_file': {
|
|
183
|
+
'nargs': '+',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
nargs='+',
|
|
187
|
+
),
|
|
188
|
+
description='Path to the manifest files which contains the build test rules of the apps',
|
|
189
|
+
validation_alias=AliasChoices('manifest_files', 'manifest_file'),
|
|
190
|
+
default=None,
|
|
191
|
+
)
|
|
192
|
+
manifest_rootpath: str = field(
|
|
193
|
+
None,
|
|
194
|
+
description='Root path to resolve the relative paths defined in the manifest files. '
|
|
195
|
+
'By default set to the current directory',
|
|
196
|
+
default=os.curdir,
|
|
197
|
+
)
|
|
198
|
+
modified_components: t.Optional[t.List[str]] = field(
|
|
199
|
+
FieldMetadata(
|
|
200
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
201
|
+
type=semicolon_separated_str_to_list,
|
|
202
|
+
),
|
|
203
|
+
description='semicolon-separated list of modified components. '
|
|
204
|
+
'If set to "", the value would be considered as None. '
|
|
205
|
+
'If set to ";", the value would be considered as an empty list.',
|
|
206
|
+
default=None,
|
|
207
|
+
)
|
|
208
|
+
modified_files: t.Optional[t.List[str]] = field(
|
|
209
|
+
FieldMetadata(
|
|
210
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
211
|
+
type=semicolon_separated_str_to_list,
|
|
212
|
+
),
|
|
213
|
+
description='semicolon-separated list of modified files. '
|
|
214
|
+
'If set to "", the value would be considered as None. '
|
|
215
|
+
'If set to ";", the value would be considered as an empty list.',
|
|
216
|
+
default=None,
|
|
217
|
+
)
|
|
218
|
+
deactivate_dependency_driven_build_by_components: t.Optional[t.List[str]] = field(
|
|
219
|
+
FieldMetadata(
|
|
220
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
221
|
+
deprecates={
|
|
222
|
+
'ignore_app_dependencies_components': {
|
|
223
|
+
'type': semicolon_separated_str_to_list,
|
|
224
|
+
'shorthand': '-ic',
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
type=semicolon_separated_str_to_list,
|
|
228
|
+
shorthand='-dc',
|
|
229
|
+
),
|
|
230
|
+
description='semicolon-separated list of components. '
|
|
231
|
+
'dependency-driven build feature will be deactivated when any of these components are modified. '
|
|
232
|
+
'Must be specified together with --modified-components. '
|
|
233
|
+
'If set to "", the value would be considered as None. '
|
|
234
|
+
'If set to ";", the value would be considered as an empty list.',
|
|
235
|
+
validation_alias=AliasChoices(
|
|
236
|
+
'deactivate_dependency_driven_build_by_components', 'ignore_app_dependencies_components'
|
|
237
|
+
),
|
|
238
|
+
default=None,
|
|
239
|
+
)
|
|
240
|
+
deactivate_dependency_driven_build_by_filepatterns: t.Optional[t.List[str]] = field(
|
|
241
|
+
FieldMetadata(
|
|
242
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
243
|
+
deprecates={
|
|
244
|
+
'ignore_app_dependencies_filepatterns': {
|
|
245
|
+
'type': semicolon_separated_str_to_list,
|
|
246
|
+
'shorthand': '-if',
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
type=semicolon_separated_str_to_list,
|
|
250
|
+
shorthand='-df',
|
|
251
|
+
),
|
|
252
|
+
description='semicolon-separated list of file patterns. '
|
|
253
|
+
'dependency-driven build feature will be deactivated when any of matched files are modified. '
|
|
254
|
+
'Must be specified together with --modified-files. '
|
|
255
|
+
'If set to "", the value would be considered as None. '
|
|
256
|
+
'If set to ";", the value would be considered as an empty list.',
|
|
257
|
+
validation_alias=AliasChoices(
|
|
258
|
+
'deactivate_dependency_driven_build_by_filepatterns', 'ignore_app_dependencies_filepatterns'
|
|
259
|
+
),
|
|
260
|
+
default=None,
|
|
261
|
+
)
|
|
262
|
+
check_manifest_rules: bool = field(
|
|
263
|
+
FieldMetadata(
|
|
264
|
+
action='store_true',
|
|
265
|
+
),
|
|
266
|
+
description='Check if all folders defined in the manifest files exist. Fail if not',
|
|
267
|
+
default=False,
|
|
268
|
+
)
|
|
269
|
+
compare_manifest_sha_filepath: t.Optional[str] = field(
|
|
270
|
+
None,
|
|
271
|
+
description='Path to the file containing the sha256 hash of the manifest rules. '
|
|
272
|
+
'Compare the hash with the current manifest rules. '
|
|
273
|
+
'All matched apps will be built if the corresponding manifest rule is modified',
|
|
274
|
+
default=None,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def model_post_init(self, __context: Any) -> None:
|
|
278
|
+
super().model_post_init(__context)
|
|
279
|
+
|
|
280
|
+
Manifest.CHECK_MANIFEST_RULES = self.check_manifest_rules
|
|
281
|
+
if self.manifest_files:
|
|
282
|
+
App.MANIFEST = Manifest.from_files(
|
|
283
|
+
self.manifest_files,
|
|
284
|
+
root_path=to_absolute_path(self.manifest_rootpath),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if self.deactivate_dependency_driven_build_by_components is not None:
|
|
288
|
+
if self.modified_components is None:
|
|
289
|
+
raise InvalidCommand(
|
|
290
|
+
'Must specify --deactivate-dependency-driven-build-by-components '
|
|
291
|
+
'together with --modified-components'
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if self.deactivate_dependency_driven_build_by_filepatterns is not None:
|
|
295
|
+
if self.modified_files is None:
|
|
296
|
+
raise InvalidCommand(
|
|
297
|
+
'Must specify --deactivate-dependency-driven-build-by-filepatterns together with --modified-files'
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def dependency_driven_build_enabled(self) -> bool:
|
|
302
|
+
"""
|
|
303
|
+
Check if the dependency-driven build feature is enabled
|
|
304
|
+
|
|
305
|
+
:return: True if enabled, False otherwise
|
|
306
|
+
"""
|
|
307
|
+
# not check since modified_components and modified_files are not passed
|
|
308
|
+
if self.modified_components is None and self.modified_files is None:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
# not check since deactivate_dependency_driven_build_by_components is passed and matched
|
|
312
|
+
if (
|
|
313
|
+
self.deactivate_dependency_driven_build_by_components
|
|
314
|
+
and self.modified_components is not None
|
|
315
|
+
and set(self.modified_components).intersection(self.deactivate_dependency_driven_build_by_components)
|
|
316
|
+
):
|
|
317
|
+
LOGGER.info(
|
|
318
|
+
'Build all apps since modified components %s matches ignored components %s',
|
|
319
|
+
', '.join(self.modified_components),
|
|
320
|
+
', '.join(self.deactivate_dependency_driven_build_by_components),
|
|
321
|
+
)
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
# not check since deactivate_dependency_driven_build_by_filepatterns is passed and matched
|
|
325
|
+
if (
|
|
326
|
+
self.deactivate_dependency_driven_build_by_filepatterns
|
|
327
|
+
and self.modified_files is not None
|
|
328
|
+
and files_matches_patterns(
|
|
329
|
+
self.modified_files, self.deactivate_dependency_driven_build_by_filepatterns, self.manifest_rootpath
|
|
330
|
+
)
|
|
331
|
+
):
|
|
332
|
+
LOGGER.info(
|
|
333
|
+
'Build all apps since modified files %s matches ignored file patterns %s',
|
|
334
|
+
', '.join(self.modified_files),
|
|
335
|
+
', '.join(self.deactivate_dependency_driven_build_by_filepatterns),
|
|
336
|
+
)
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
return True
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def modified_manifest_rules_folders(self) -> t.Optional[t.Set[str]]:
|
|
343
|
+
if self.compare_manifest_sha_filepath and App.MANIFEST is not None:
|
|
344
|
+
return App.MANIFEST.diff_sha_with_filepath(self.compare_manifest_sha_filepath, use_abspath=True)
|
|
345
|
+
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class FindBuildArguments(DependencyDrivenBuildArguments):
|
|
350
|
+
paths: t.List[str] = field(
|
|
351
|
+
FieldMetadata(
|
|
352
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
353
|
+
shorthand='-p',
|
|
354
|
+
nargs='*',
|
|
355
|
+
),
|
|
356
|
+
description='Paths to the directories containing the apps. By default set to the current directory',
|
|
357
|
+
default=os.curdir,
|
|
358
|
+
)
|
|
359
|
+
target: str = field(
|
|
360
|
+
FieldMetadata(
|
|
361
|
+
shorthand='-t',
|
|
362
|
+
),
|
|
363
|
+
description='Filter the apps by target. By default set to "all"',
|
|
364
|
+
default='all',
|
|
365
|
+
)
|
|
366
|
+
build_system: t.Union[str, t.Type[App]] = field(
|
|
367
|
+
FieldMetadata(
|
|
368
|
+
choices=['cmake', 'make'],
|
|
369
|
+
),
|
|
370
|
+
description='Filter the apps by build system. By default set to "cmake"',
|
|
371
|
+
default='cmake',
|
|
372
|
+
)
|
|
373
|
+
recursive: bool = field(
|
|
374
|
+
FieldMetadata(
|
|
375
|
+
action='store_true',
|
|
376
|
+
),
|
|
377
|
+
description='Search for apps recursively under the specified paths',
|
|
378
|
+
default=False,
|
|
379
|
+
)
|
|
380
|
+
exclude: t.Optional[t.List[str]] = field(
|
|
381
|
+
FieldMetadata(
|
|
382
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
383
|
+
nargs='+',
|
|
384
|
+
),
|
|
385
|
+
description='Ignore the specified directories while searching recursively',
|
|
386
|
+
validation_alias=AliasChoices('exclude', 'exclude_list'),
|
|
387
|
+
default=None,
|
|
388
|
+
)
|
|
389
|
+
work_dir: t.Optional[str] = field(
|
|
390
|
+
None,
|
|
391
|
+
description='Copy the app to this directory before building. '
|
|
392
|
+
'By default set to the app directory. Can expand placeholders',
|
|
393
|
+
default=None,
|
|
394
|
+
)
|
|
395
|
+
build_dir: str = field(
|
|
396
|
+
None,
|
|
397
|
+
description='Build directory for the app. By default set to "build". '
|
|
398
|
+
'When set to relative path, it will be treated as relative to the app directory. '
|
|
399
|
+
'Can expand placeholders',
|
|
400
|
+
default='build',
|
|
401
|
+
)
|
|
402
|
+
build_log_filename: t.Optional[str] = field(
|
|
403
|
+
FieldMetadata(
|
|
404
|
+
deprecates={'build_log': {}},
|
|
405
|
+
),
|
|
406
|
+
description='Log filename under the build directory instead of stdout. Can expand placeholders',
|
|
407
|
+
validation_alias=AliasChoices('build_log_filename', 'build_log'),
|
|
408
|
+
default=None,
|
|
409
|
+
)
|
|
410
|
+
size_json_filename: t.Optional[str] = field(
|
|
411
|
+
FieldMetadata(
|
|
412
|
+
deprecates={'size_file': {}},
|
|
413
|
+
),
|
|
414
|
+
description='`idf.py size` output file under the build directory when specified. ' 'Can expand placeholders',
|
|
415
|
+
validation_alias=AliasChoices('size_json_filename', 'size_file'),
|
|
416
|
+
default=None,
|
|
417
|
+
)
|
|
418
|
+
config_rules: t.Optional[t.List[str]] = field(
|
|
419
|
+
FieldMetadata(
|
|
420
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
421
|
+
deprecates={
|
|
422
|
+
'config': {'nargs': '+'},
|
|
423
|
+
},
|
|
424
|
+
nargs='+',
|
|
425
|
+
),
|
|
426
|
+
description='Defines the rules of building the project with pre-set sdkconfig files. '
|
|
427
|
+
'Supports FILENAME[=NAME] or FILEPATTERN format. '
|
|
428
|
+
'FILENAME is the filename of the sdkconfig file, relative to the app directory. '
|
|
429
|
+
'Optional NAME is the name of the configuration. '
|
|
430
|
+
'if not specified, the filename is used as the name. '
|
|
431
|
+
'FILEPATTERN is the filename of the sdkconfig file with a single wildcard character (*). '
|
|
432
|
+
'The NAME is the value matched by the wildcard',
|
|
433
|
+
validation_alias=AliasChoices('config_rules', 'config_rules_str', 'config'),
|
|
434
|
+
default=None,
|
|
435
|
+
)
|
|
436
|
+
override_sdkconfig_items: t.Optional[str] = field(
|
|
437
|
+
None,
|
|
438
|
+
description='A comma-separated list of key=value pairs to override the sdkconfig items',
|
|
439
|
+
default=None,
|
|
440
|
+
)
|
|
441
|
+
override_sdkconfig_files: t.Optional[str] = field(
|
|
442
|
+
None,
|
|
443
|
+
description='A comma-separated list of sdkconfig files to override the sdkconfig items. '
|
|
444
|
+
'When set to relative path, it will be treated as relative to the current directory',
|
|
445
|
+
default=None,
|
|
446
|
+
)
|
|
447
|
+
sdkconfig_defaults: t.Optional[str] = field(
|
|
448
|
+
None,
|
|
449
|
+
description='A semicolon-separated list of sdkconfig files passed to `idf.py -DSDKCONFIG_DEFAULTS`. '
|
|
450
|
+
'SDKCONFIG_DEFAULTS environment variable is used when not specified',
|
|
451
|
+
default=os.getenv('SDKCONFIG_DEFAULTS', None),
|
|
452
|
+
)
|
|
453
|
+
check_warnings: bool = field(
|
|
454
|
+
FieldMetadata(
|
|
455
|
+
action='store_true',
|
|
456
|
+
),
|
|
457
|
+
description='Check for warnings in the build output. Fail if any warnings are found',
|
|
458
|
+
default=False,
|
|
459
|
+
)
|
|
460
|
+
default_build_targets: t.Optional[t.List[str]] = field(
|
|
461
|
+
FieldMetadata(
|
|
462
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
463
|
+
nargs='+',
|
|
464
|
+
),
|
|
465
|
+
description='space-separated list of the default enabled build targets for the apps. '
|
|
466
|
+
'When not specified, the default value is the targets listed by `idf.py --list-targets`',
|
|
467
|
+
default=None,
|
|
468
|
+
)
|
|
469
|
+
enable_preview_targets: bool = field(
|
|
470
|
+
FieldMetadata(
|
|
471
|
+
action='store_true',
|
|
472
|
+
),
|
|
473
|
+
description='When enabled, the default build targets will be set to all apps, '
|
|
474
|
+
'including the preview targets. As the targets defined in `idf.py --list-targets --preview`',
|
|
475
|
+
default=False,
|
|
476
|
+
)
|
|
477
|
+
include_skipped_apps: bool = field(
|
|
478
|
+
FieldMetadata(
|
|
479
|
+
action='store_true',
|
|
480
|
+
),
|
|
481
|
+
description='Include the skipped apps in the output, together with the enabled ones',
|
|
482
|
+
default=False,
|
|
483
|
+
)
|
|
484
|
+
include_disabled_apps: bool = field(
|
|
485
|
+
FieldMetadata(
|
|
486
|
+
action='store_true',
|
|
487
|
+
),
|
|
488
|
+
description='Include the disabled apps in the output, together with the enabled ones',
|
|
489
|
+
default=False,
|
|
490
|
+
)
|
|
491
|
+
include_all_apps: bool = field(
|
|
492
|
+
FieldMetadata(
|
|
493
|
+
action='store_true',
|
|
494
|
+
),
|
|
495
|
+
description='Include skipped, and disabled apps in the output, together with the enabled ones',
|
|
496
|
+
default=False,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def model_post_init(self, __context: Any) -> None:
|
|
500
|
+
super().model_post_init(__context)
|
|
501
|
+
|
|
502
|
+
if not self.paths:
|
|
503
|
+
raise InvalidCommand('At least one path must be specified')
|
|
504
|
+
|
|
505
|
+
if not self.target:
|
|
506
|
+
LOGGER.debug('--target is missing. Set --target as "all".')
|
|
507
|
+
self.target = 'all'
|
|
508
|
+
|
|
509
|
+
if self.default_build_targets:
|
|
510
|
+
default_build_targets = []
|
|
511
|
+
for target in self.default_build_targets:
|
|
512
|
+
if target not in ALL_TARGETS:
|
|
513
|
+
LOGGER.warning(
|
|
514
|
+
f'Ignoring... Unrecognizable target {target} specified with "--default-build-targets". '
|
|
515
|
+
f'Current ESP-IDF available targets: {ALL_TARGETS}'
|
|
516
|
+
)
|
|
517
|
+
elif target not in default_build_targets:
|
|
518
|
+
default_build_targets.append(target)
|
|
519
|
+
self.default_build_targets = default_build_targets
|
|
520
|
+
LOGGER.info('Overriding default build targets to %s', self.default_build_targets)
|
|
521
|
+
FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
|
|
522
|
+
elif self.enable_preview_targets:
|
|
523
|
+
self.default_build_targets = deepcopy(ALL_TARGETS)
|
|
524
|
+
LOGGER.info('Overriding default build targets to %s', self.default_build_targets)
|
|
525
|
+
FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
|
|
526
|
+
|
|
527
|
+
if self.override_sdkconfig_items or self.override_sdkconfig_items:
|
|
528
|
+
SESSION_ARGS.set(self)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class FindArguments(FindBuildArguments):
|
|
532
|
+
output: t.Optional[str] = field(
|
|
533
|
+
FieldMetadata(
|
|
534
|
+
shorthand='-o',
|
|
535
|
+
),
|
|
536
|
+
description='Record the found apps to the specified file instead of stdout',
|
|
537
|
+
default=None,
|
|
538
|
+
)
|
|
539
|
+
output_format: str = field(
|
|
540
|
+
FieldMetadata(
|
|
541
|
+
choices=['raw', 'json'],
|
|
542
|
+
),
|
|
543
|
+
description='Output format of the found apps. '
|
|
544
|
+
'In "raw" format, each line is a json string serialized from the app model. '
|
|
545
|
+
'In "json" format, the output is a json list of the serialized app models',
|
|
546
|
+
default='raw',
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
def model_post_init(self, __context: Any) -> None:
|
|
550
|
+
super().model_post_init(__context)
|
|
551
|
+
|
|
552
|
+
if self.include_all_apps:
|
|
553
|
+
self.include_skipped_apps = True
|
|
554
|
+
self.include_disabled_apps = True
|
|
555
|
+
|
|
556
|
+
if self.output and self.output.endswith('.json') and self.output_format in ['raw', None]:
|
|
557
|
+
LOGGER.debug('Detecting output file ends with ".json", writing as json file.')
|
|
558
|
+
self.output_format = 'json'
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class BuildArguments(FindBuildArguments):
|
|
562
|
+
build_verbose: bool = field(
|
|
563
|
+
FieldMetadata(
|
|
564
|
+
action='store_true',
|
|
565
|
+
),
|
|
566
|
+
description='Enable verbose output of the build system',
|
|
567
|
+
default=False,
|
|
568
|
+
)
|
|
569
|
+
parallel_count: int = field(
|
|
570
|
+
FieldMetadata(
|
|
571
|
+
type=int,
|
|
572
|
+
),
|
|
573
|
+
description='Number of parallel build jobs in total. '
|
|
574
|
+
'Specified together with --parallel-index. '
|
|
575
|
+
'The given apps will be divided into parallel_count parts, '
|
|
576
|
+
'and the current run will build the parallel_index-th part',
|
|
577
|
+
default=1,
|
|
578
|
+
)
|
|
579
|
+
parallel_index: int = field(
|
|
580
|
+
FieldMetadata(
|
|
581
|
+
type=int,
|
|
582
|
+
),
|
|
583
|
+
description='Index (1-based) of the parallel build job. '
|
|
584
|
+
'Specified together with --parallel-count. '
|
|
585
|
+
'The given apps will be divided into parallel_count parts, '
|
|
586
|
+
'and the current run will build the parallel_index-th part',
|
|
587
|
+
default=1,
|
|
588
|
+
)
|
|
589
|
+
dry_run: bool = field(
|
|
590
|
+
FieldMetadata(
|
|
591
|
+
action='store_true',
|
|
592
|
+
),
|
|
593
|
+
description='Skip the actual build, only print the build process',
|
|
594
|
+
default=False,
|
|
595
|
+
)
|
|
596
|
+
keep_going: bool = field(
|
|
597
|
+
FieldMetadata(
|
|
598
|
+
action='store_true',
|
|
599
|
+
),
|
|
600
|
+
description='Continue building the next app when the current build fails',
|
|
601
|
+
default=False,
|
|
602
|
+
)
|
|
603
|
+
no_preserve: bool = field(
|
|
604
|
+
FieldMetadata(
|
|
605
|
+
action='store_true',
|
|
606
|
+
),
|
|
607
|
+
description='Do not preserve the build directory after a successful build',
|
|
608
|
+
default=False,
|
|
609
|
+
)
|
|
610
|
+
ignore_warning_strs: t.Optional[t.List[str]] = field(
|
|
611
|
+
FieldMetadata(
|
|
612
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
613
|
+
deprecates={
|
|
614
|
+
'ignore_warning_str': {'nargs': '+'},
|
|
615
|
+
},
|
|
616
|
+
nargs='+',
|
|
617
|
+
),
|
|
618
|
+
description='space-separated list of patterns. '
|
|
619
|
+
'Ignore the warnings in the build output that match the patterns',
|
|
620
|
+
validation_alias=AliasChoices('ignore_warning_strs', 'ignore_warning_str'),
|
|
621
|
+
default=None,
|
|
622
|
+
)
|
|
623
|
+
ignore_warning_files: t.Optional[t.List[t.Union[str, TextIOWrapper]]] = field(
|
|
624
|
+
FieldMetadata(
|
|
625
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
626
|
+
deprecates={
|
|
627
|
+
'ignore_warning_file': {
|
|
628
|
+
'type': argparse.FileType('r'),
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
nargs='+',
|
|
632
|
+
type=argparse.FileType('r'),
|
|
633
|
+
),
|
|
634
|
+
description='Path to the files containing the patterns to ignore the warnings in the build output',
|
|
635
|
+
validation_alias=AliasChoices('ignore_warning_files', 'ignore_warning_file'),
|
|
636
|
+
default=None,
|
|
637
|
+
)
|
|
638
|
+
copy_sdkconfig: bool = field(
|
|
639
|
+
FieldMetadata(
|
|
640
|
+
action='store_true',
|
|
641
|
+
),
|
|
642
|
+
description='Copy the sdkconfig file to the build directory',
|
|
643
|
+
default=False,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Attrs that support placeholders
|
|
647
|
+
collect_size_info_filename: t.Optional[str] = field(
|
|
648
|
+
FieldMetadata(
|
|
649
|
+
deprecates={'collect_size_info': {}},
|
|
650
|
+
hidden=True,
|
|
651
|
+
),
|
|
652
|
+
description='Record size json filepath of the built apps to the specified file. '
|
|
653
|
+
'Each line is a json string. Can expand placeholders @p',
|
|
654
|
+
validation_alias=AliasChoices('collect_size_info_filename', 'collect_size_info'),
|
|
655
|
+
default=None,
|
|
656
|
+
exclude=True, # computed field is used
|
|
657
|
+
)
|
|
658
|
+
collect_app_info_filename: t.Optional[str] = field(
|
|
659
|
+
FieldMetadata(
|
|
660
|
+
deprecates={'collect_app_info': {}},
|
|
661
|
+
hidden=True,
|
|
662
|
+
),
|
|
663
|
+
description='Record serialized app model of the built apps to the specified file. '
|
|
664
|
+
'Each line is a json string. Can expand placeholders @p',
|
|
665
|
+
validation_alias=AliasChoices('collect_app_info_filename', 'collect_app_info'),
|
|
666
|
+
default=None,
|
|
667
|
+
exclude=True, # computed field is used
|
|
668
|
+
)
|
|
669
|
+
junitxml_filename: t.Optional[str] = field(
|
|
670
|
+
FieldMetadata(
|
|
671
|
+
deprecates={'junitxml': {}},
|
|
672
|
+
hidden=True,
|
|
673
|
+
),
|
|
674
|
+
description='Path to the junitxml file to record the build results. Can expand placeholder @p',
|
|
675
|
+
validation_alias=AliasChoices('junitxml_filename', 'junitxml'),
|
|
676
|
+
default=None,
|
|
677
|
+
exclude=True, # computed field is used
|
|
678
|
+
)
|
|
679
|
+
# used for expanding placeholders
|
|
680
|
+
PARALLEL_INDEX_PLACEHOLDER: t.ClassVar[str] = '@p' # replace it with the parallel index
|
|
681
|
+
|
|
682
|
+
def model_post_init(self, __context: Any) -> None:
|
|
683
|
+
super().model_post_init(__context)
|
|
684
|
+
|
|
685
|
+
ignore_warnings_regexes = []
|
|
686
|
+
if self.ignore_warning_strs:
|
|
687
|
+
for s in self.ignore_warning_strs:
|
|
688
|
+
ignore_warnings_regexes.append(re.compile(s))
|
|
689
|
+
if self.ignore_warning_files:
|
|
690
|
+
for f in self.ignore_warning_files:
|
|
691
|
+
if isinstance(f, str):
|
|
692
|
+
with open(f) as fr:
|
|
693
|
+
for s in fr:
|
|
694
|
+
ignore_warnings_regexes.append(re.compile(s.strip()))
|
|
695
|
+
else:
|
|
696
|
+
for s in f:
|
|
697
|
+
ignore_warnings_regexes.append(re.compile(s.strip()))
|
|
698
|
+
App.IGNORE_WARNS_REGEXES = ignore_warnings_regexes
|
|
699
|
+
|
|
700
|
+
@computed_field # type: ignore
|
|
701
|
+
@property
|
|
702
|
+
def collect_size_info(self) -> t.Optional[str]:
|
|
703
|
+
if self.collect_size_info_filename:
|
|
704
|
+
return self.collect_size_info_filename.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
|
|
705
|
+
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
@computed_field # type: ignore
|
|
709
|
+
@property
|
|
710
|
+
def collect_app_info(self) -> t.Optional[str]:
|
|
711
|
+
if self.collect_app_info_filename:
|
|
712
|
+
return self.collect_app_info_filename.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
|
|
713
|
+
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
@computed_field # type: ignore
|
|
717
|
+
@property
|
|
718
|
+
def junitxml(self) -> t.Optional[str]:
|
|
719
|
+
if self.junitxml_filename:
|
|
720
|
+
return self.junitxml_filename.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
|
|
721
|
+
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class DumpManifestShaArguments(GlobalArguments):
|
|
726
|
+
manifest_files: t.Optional[t.List[str]] = field(
|
|
727
|
+
FieldMetadata(
|
|
728
|
+
validate_method=[ValidateMethod.TO_LIST],
|
|
729
|
+
nargs='+',
|
|
730
|
+
required=True,
|
|
731
|
+
),
|
|
732
|
+
description='Path to the manifest files which contains the build test rules of the apps',
|
|
733
|
+
default=None,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
output: t.Optional[str] = field(
|
|
737
|
+
FieldMetadata(
|
|
738
|
+
shorthand='-o',
|
|
739
|
+
required=True,
|
|
740
|
+
),
|
|
741
|
+
description='Path to the output file to record the sha256 hash of the manifest rules',
|
|
742
|
+
default=None,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def model_post_init(self, __context: Any) -> None:
|
|
746
|
+
super().model_post_init(__context)
|
|
747
|
+
|
|
748
|
+
if not self.manifest_files:
|
|
749
|
+
raise InvalidCommand('Manifest files are required to dump the SHA values.')
|
|
750
|
+
if not self.output:
|
|
751
|
+
raise InvalidCommand('Output file is required to dump the SHA values.')
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _snake_case_to_cli_arg_name(s: str) -> str:
|
|
755
|
+
return f'--{s.replace("_", "-")}'
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def add_args_to_parser(argument_cls: t.Type[BaseArguments], parser: argparse.ArgumentParser) -> None:
|
|
759
|
+
"""
|
|
760
|
+
Add arguments to the parser from the argument class.
|
|
761
|
+
|
|
762
|
+
FieldMetadata is used to set the argparse options.
|
|
763
|
+
|
|
764
|
+
:param argument_cls: argument class
|
|
765
|
+
:param parser: argparse parser
|
|
766
|
+
"""
|
|
767
|
+
for f_name, f in argument_cls.model_fields.items():
|
|
768
|
+
f_meta = get_meta(f)
|
|
769
|
+
if f_meta and f_meta.deprecates:
|
|
770
|
+
for dep_f_name, dep_f_kwargs in f_meta.deprecates.items():
|
|
771
|
+
_names = [_snake_case_to_cli_arg_name(dep_f_name)]
|
|
772
|
+
_shorthand = dep_f_kwargs.pop('shorthand', None)
|
|
773
|
+
if _shorthand:
|
|
774
|
+
_names.append(_shorthand)
|
|
775
|
+
|
|
776
|
+
if f_meta.hidden: # f is hidden, use deprecated field instead
|
|
777
|
+
help_msg = f.description
|
|
778
|
+
else:
|
|
779
|
+
help_msg = f'[Deprecated] Use {_snake_case_to_cli_arg_name(f_name)} instead'
|
|
780
|
+
|
|
781
|
+
parser.add_argument(
|
|
782
|
+
*_names,
|
|
783
|
+
**dep_f_kwargs,
|
|
784
|
+
help=help_msg,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
if f_meta and f_meta.hidden:
|
|
788
|
+
continue
|
|
789
|
+
|
|
790
|
+
names = [_snake_case_to_cli_arg_name(f_name)]
|
|
791
|
+
if f_meta and f_meta.shorthand:
|
|
792
|
+
names.append(f_meta.shorthand)
|
|
793
|
+
|
|
794
|
+
kwargs: t.Dict[str, t.Any] = {}
|
|
795
|
+
if f_meta:
|
|
796
|
+
if f_meta.type:
|
|
797
|
+
kwargs['type'] = f_meta.type
|
|
798
|
+
if f_meta.required:
|
|
799
|
+
kwargs['required'] = True
|
|
800
|
+
if f_meta.action:
|
|
801
|
+
kwargs['action'] = f_meta.action
|
|
802
|
+
if f_meta.nargs:
|
|
803
|
+
kwargs['nargs'] = f_meta.nargs
|
|
804
|
+
if f_meta.choices:
|
|
805
|
+
kwargs['choices'] = f_meta.choices
|
|
806
|
+
if f_meta.default:
|
|
807
|
+
kwargs['default'] = f_meta.default
|
|
808
|
+
if 'default' not in kwargs:
|
|
809
|
+
kwargs['default'] = f.default
|
|
810
|
+
|
|
811
|
+
parser.add_argument(
|
|
812
|
+
*names,
|
|
813
|
+
**kwargs,
|
|
814
|
+
help=f.description,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def add_args_to_obj_doc_as_params(argument_cls: t.Type[GlobalArguments], obj: t.Any = None) -> None:
|
|
819
|
+
"""
|
|
820
|
+
Add arguments to the function as parameters.
|
|
821
|
+
|
|
822
|
+
:param argument_cls: argument class
|
|
823
|
+
:param obj: object to add the docstring to
|
|
824
|
+
"""
|
|
825
|
+
_obj = obj or argument_cls
|
|
826
|
+
_doc_str = _obj.__doc__ or ''
|
|
827
|
+
_doc_str += '\n'
|
|
828
|
+
|
|
829
|
+
for f_name, f in argument_cls.model_fields.items():
|
|
830
|
+
# typing generic alias is not a class
|
|
831
|
+
_annotation = f.annotation.__name__ if inspect.isclass(f.annotation) else f.annotation
|
|
832
|
+
_doc_str += f' :param {f_name}: {f.description}\n'
|
|
833
|
+
_doc_str += f' :type {f_name}: {_annotation}\n'
|
|
834
|
+
|
|
835
|
+
_obj.__doc__ = _doc_str
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def apply_config_file(config_file: t.Optional[str]) -> None:
|
|
839
|
+
def _subclasses(klass: t.Type[T]) -> t.Set[t.Type[T]]:
|
|
840
|
+
return set(klass.__subclasses__()).union([s for c in klass.__subclasses__() for s in _subclasses(c)])
|
|
841
|
+
|
|
842
|
+
if config_file:
|
|
843
|
+
BaseArguments.model_config['toml_file'] = str(config_file)
|
|
844
|
+
# modify all subclasses
|
|
845
|
+
for cls in _subclasses(BaseArguments):
|
|
846
|
+
cls.model_config['toml_file'] = str(config_file)
|