idf-build-apps 2.4.3__py3-none-any.whl → 2.5.0rc1__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 +1022 -0
- idf_build_apps/config.py +19 -2
- idf_build_apps/constants.py +17 -28
- idf_build_apps/finder.py +34 -54
- idf_build_apps/log.py +2 -0
- idf_build_apps/main.py +116 -629
- idf_build_apps/manifest/manifest.py +112 -24
- idf_build_apps/utils.py +22 -0
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0rc1.dist-info}/METADATA +3 -4
- idf_build_apps-2.5.0rc1.dist-info/RECORD +26 -0
- idf_build_apps/build_apps_args.py +0 -64
- idf_build_apps-2.4.3.dist-info/RECORD +0 -26
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0rc1.dist-info}/LICENSE +0 -0
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0rc1.dist-info}/WHEEL +0 -0
- {idf_build_apps-2.4.3.dist-info → idf_build_apps-2.5.0rc1.dist-info}/entry_points.txt +0 -0
idf_build_apps/args.py
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""
|
|
4
|
+
Arguments used in the CLI, and functions.
|
|
5
|
+
|
|
6
|
+
The reason that does not use pydantic models, but dataclasses
|
|
7
|
+
|
|
8
|
+
- poor autocomplete in IDE when using pydantic custom Fields with extra metadata
|
|
9
|
+
- pydantic Field alias is nice, but hard to customize, when
|
|
10
|
+
- the deprecated field has a different nargs
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import inspect
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import typing as t
|
|
19
|
+
from copy import deepcopy
|
|
20
|
+
from dataclasses import InitVar, asdict, dataclass, field, fields
|
|
21
|
+
from enum import Enum
|
|
22
|
+
|
|
23
|
+
from . import SESSION_ARGS, setup_logging
|
|
24
|
+
from .app import App
|
|
25
|
+
from .config import get_valid_config
|
|
26
|
+
from .constants import ALL_TARGETS
|
|
27
|
+
from .manifest.manifest import FolderRule, Manifest
|
|
28
|
+
from .utils import (
|
|
29
|
+
InvalidCommand,
|
|
30
|
+
Self,
|
|
31
|
+
drop_none_kwargs,
|
|
32
|
+
files_matches_patterns,
|
|
33
|
+
semicolon_separated_str_to_list,
|
|
34
|
+
to_absolute_path,
|
|
35
|
+
to_list,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
LOGGER = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Field(Enum):
|
|
42
|
+
UNSET = 'UNSET'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class FieldMetadata:
|
|
47
|
+
"""
|
|
48
|
+
dataclass field metadata. All fields are optional.
|
|
49
|
+
Some fields are used in argparse while running :func:`add_args_to_parser`.
|
|
50
|
+
|
|
51
|
+
:param description: description of the field
|
|
52
|
+
:param deprecates: deprecates field names, used in argparse
|
|
53
|
+
:param shorthand: shorthand for the argument, used in argparse
|
|
54
|
+
:param action: action for the argument, used in argparse
|
|
55
|
+
:param nargs: nargs for the argument, used in argparse
|
|
56
|
+
:param choices: choices for the argument, used in argparse
|
|
57
|
+
:param type: type for the argument, used in argparse
|
|
58
|
+
:param required: whether the argument is required, used in argparse
|
|
59
|
+
:param default: default value, used in argparse
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
description: t.Optional[str] = None
|
|
63
|
+
# the field description will be copied from the deprecates field if not specified
|
|
64
|
+
deprecates: t.Optional[t.Dict[str, t.Dict[str, t.Any]]] = None
|
|
65
|
+
shorthand: t.Optional[str] = None
|
|
66
|
+
# argparse_kwargs
|
|
67
|
+
action: t.Optional[str] = None
|
|
68
|
+
nargs: t.Optional[str] = None
|
|
69
|
+
choices: t.Optional[t.List[str]] = None
|
|
70
|
+
type: t.Optional[t.Callable] = None
|
|
71
|
+
required: bool = False
|
|
72
|
+
# usually default is not needed. only set it when different from the default value of the field
|
|
73
|
+
default: t.Any = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class GlobalArguments:
|
|
78
|
+
"""
|
|
79
|
+
Global arguments used in all commands
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
config_file: t.Optional[str] = field(
|
|
83
|
+
default=None,
|
|
84
|
+
metadata=asdict(
|
|
85
|
+
FieldMetadata(
|
|
86
|
+
description='Path to the configuration file',
|
|
87
|
+
shorthand='-c',
|
|
88
|
+
)
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
verbose: int = field(
|
|
92
|
+
default=0,
|
|
93
|
+
metadata=asdict(
|
|
94
|
+
FieldMetadata(
|
|
95
|
+
description='Verbosity level. By default set to WARNING. Specify -v for INFO, -vv for DEBUG',
|
|
96
|
+
shorthand='-v',
|
|
97
|
+
action='count',
|
|
98
|
+
)
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
log_file: t.Optional[str] = field(
|
|
102
|
+
default=None,
|
|
103
|
+
metadata=asdict(
|
|
104
|
+
FieldMetadata(
|
|
105
|
+
description='Path to the log file, if not specified logs will be printed to stderr',
|
|
106
|
+
)
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
no_color: bool = field(
|
|
110
|
+
default=False,
|
|
111
|
+
metadata=asdict(
|
|
112
|
+
FieldMetadata(
|
|
113
|
+
description='Disable colored output',
|
|
114
|
+
action='store_true',
|
|
115
|
+
)
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
_depr_name_to_new_name_dict: t.ClassVar[t.Dict[str, str]] = {} # record deprecated field <-> new field
|
|
120
|
+
|
|
121
|
+
def __new__(cls, *args, **kwargs): # noqa: ARG003
|
|
122
|
+
for f in fields(cls):
|
|
123
|
+
_metadata = FieldMetadata(**f.metadata)
|
|
124
|
+
if _metadata.deprecates:
|
|
125
|
+
for depr_name in _metadata.deprecates:
|
|
126
|
+
cls._depr_name_to_new_name_dict[depr_name] = f.name
|
|
127
|
+
|
|
128
|
+
return super().__new__(cls)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_dict(cls, d: t.Dict[str, t.Any]) -> Self:
|
|
132
|
+
"""
|
|
133
|
+
Create an instance from a dictionary. Ignore unknown keys.
|
|
134
|
+
|
|
135
|
+
:param d: dictionary
|
|
136
|
+
:return: instance
|
|
137
|
+
"""
|
|
138
|
+
return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
|
|
139
|
+
|
|
140
|
+
def __post_init__(self):
|
|
141
|
+
self.apply_config()
|
|
142
|
+
|
|
143
|
+
def apply_config(self) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Apply the configuration file to the arguments
|
|
146
|
+
"""
|
|
147
|
+
config_dict = get_valid_config(custom_path=self.config_file) or {}
|
|
148
|
+
|
|
149
|
+
# set log fields first
|
|
150
|
+
self.verbose = config_dict.pop('verbose', self.verbose)
|
|
151
|
+
self.log_file = config_dict.pop('log_file', self.log_file)
|
|
152
|
+
self.no_color = config_dict.pop('no_color', self.no_color)
|
|
153
|
+
setup_logging(self.verbose, self.log_file, not self.no_color)
|
|
154
|
+
|
|
155
|
+
if config_dict:
|
|
156
|
+
for name, value in config_dict.items():
|
|
157
|
+
if hasattr(self, name):
|
|
158
|
+
setattr(self, name, value)
|
|
159
|
+
|
|
160
|
+
if name in self._depr_name_to_new_name_dict:
|
|
161
|
+
self.set_deprecated_field(self._depr_name_to_new_name_dict[name], name, value)
|
|
162
|
+
|
|
163
|
+
def set_deprecated_field(self, new_k: str, depr_k: str, depr_v: t.Any) -> None:
|
|
164
|
+
if depr_v == _Field.UNSET:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
LOGGER.warning(
|
|
168
|
+
f'Field `{depr_k}` is deprecated. Will be removed in the next major release. '
|
|
169
|
+
f'Use field `{new_k}` instead.'
|
|
170
|
+
)
|
|
171
|
+
if getattr(self, new_k) is not None:
|
|
172
|
+
LOGGER.warning(f'Field `{new_k}` is already set. Ignoring deprecated field `{depr_k}`')
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
setattr(self, new_k, depr_v)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class DependencyDrivenBuildArguments(GlobalArguments):
|
|
180
|
+
"""
|
|
181
|
+
Arguments used in the dependency-driven build feature.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
manifest_file: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
|
|
185
|
+
manifest_files: t.Optional[t.List[str]] = field(
|
|
186
|
+
default=None,
|
|
187
|
+
metadata=asdict(
|
|
188
|
+
FieldMetadata(
|
|
189
|
+
deprecates={
|
|
190
|
+
'manifest_file': {
|
|
191
|
+
'nargs': '+',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
description='Path to the manifest files which contains the build test rules of the apps',
|
|
195
|
+
nargs='+',
|
|
196
|
+
)
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
manifest_rootpath: str = field(
|
|
200
|
+
default=os.curdir,
|
|
201
|
+
metadata=asdict(
|
|
202
|
+
FieldMetadata(
|
|
203
|
+
description='Root path to resolve the relative paths defined in the manifest files. '
|
|
204
|
+
'By default set to the current directory',
|
|
205
|
+
)
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
modified_components: t.Optional[t.List[str]] = field(
|
|
209
|
+
default=None,
|
|
210
|
+
metadata=asdict(
|
|
211
|
+
FieldMetadata(
|
|
212
|
+
description='semicolon-separated list of modified components',
|
|
213
|
+
type=semicolon_separated_str_to_list,
|
|
214
|
+
)
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
modified_files: t.Optional[t.List[str]] = field(
|
|
218
|
+
default=None,
|
|
219
|
+
metadata=asdict(
|
|
220
|
+
FieldMetadata(
|
|
221
|
+
description='semicolon-separated list of modified files',
|
|
222
|
+
type=semicolon_separated_str_to_list,
|
|
223
|
+
)
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
ignore_app_dependencies_components: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
|
|
227
|
+
deactivate_dependency_driven_build_by_components: t.Optional[t.List[str]] = field(
|
|
228
|
+
default=None,
|
|
229
|
+
metadata=asdict(
|
|
230
|
+
FieldMetadata(
|
|
231
|
+
deprecates={
|
|
232
|
+
'ignore_app_dependencies_components': {
|
|
233
|
+
'type': semicolon_separated_str_to_list,
|
|
234
|
+
'shorthand': '-ic',
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
description='semicolon-separated list of components. '
|
|
238
|
+
'dependency-driven build feature will be deactivated when any of these components are modified',
|
|
239
|
+
type=semicolon_separated_str_to_list,
|
|
240
|
+
shorthand='-dc',
|
|
241
|
+
)
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
ignore_app_dependencies_filepatterns: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
|
|
245
|
+
deactivate_dependency_driven_build_by_filepatterns: t.Optional[t.List[str]] = field(
|
|
246
|
+
default=None,
|
|
247
|
+
metadata=asdict(
|
|
248
|
+
FieldMetadata(
|
|
249
|
+
deprecates={
|
|
250
|
+
'ignore_app_dependencies_filepatterns': {
|
|
251
|
+
'type': semicolon_separated_str_to_list,
|
|
252
|
+
'shorthand': '-if',
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
description='semicolon-separated list of file patterns. '
|
|
256
|
+
'dependency-driven build feature will be deactivated when any of matched files are modified',
|
|
257
|
+
type=semicolon_separated_str_to_list,
|
|
258
|
+
shorthand='-df',
|
|
259
|
+
)
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
check_manifest_rules: bool = field(
|
|
263
|
+
default=False,
|
|
264
|
+
metadata=asdict(
|
|
265
|
+
FieldMetadata(
|
|
266
|
+
description='Check if all folders defined in the manifest files exist. Fail if not',
|
|
267
|
+
action='store_true',
|
|
268
|
+
)
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
compare_manifest_sha_filepath: t.Optional[str] = field(
|
|
272
|
+
default=None,
|
|
273
|
+
metadata=asdict(
|
|
274
|
+
FieldMetadata(
|
|
275
|
+
description='Path to the file containing the sha256 hash of the manifest rules. '
|
|
276
|
+
'Compare the hash with the current manifest rules. '
|
|
277
|
+
'All matched apps will be built if the cooresponding manifest rule is modified',
|
|
278
|
+
)
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def __post_init__(
|
|
283
|
+
self,
|
|
284
|
+
manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
285
|
+
ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
286
|
+
ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
287
|
+
):
|
|
288
|
+
super().__post_init__()
|
|
289
|
+
|
|
290
|
+
self.set_deprecated_field('manifest_files', 'manifest_file', manifest_file)
|
|
291
|
+
self.set_deprecated_field(
|
|
292
|
+
'deactivate_dependency_driven_build_by_components',
|
|
293
|
+
'ignore_app_dependencies_components',
|
|
294
|
+
ignore_app_dependencies_components,
|
|
295
|
+
)
|
|
296
|
+
self.set_deprecated_field(
|
|
297
|
+
'deactivate_dependency_driven_build_by_filepatterns',
|
|
298
|
+
'ignore_app_dependencies_filepatterns',
|
|
299
|
+
ignore_app_dependencies_filepatterns,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self.manifest_files = to_list(self.manifest_files)
|
|
303
|
+
self.modified_components = to_list(self.modified_components)
|
|
304
|
+
self.modified_files = to_list(self.modified_files)
|
|
305
|
+
self.deactivate_dependency_driven_build_by_components = to_list(
|
|
306
|
+
self.deactivate_dependency_driven_build_by_components
|
|
307
|
+
)
|
|
308
|
+
self.deactivate_dependency_driven_build_by_filepatterns = to_list(
|
|
309
|
+
self.deactivate_dependency_driven_build_by_filepatterns
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Validation
|
|
313
|
+
Manifest.CHECK_MANIFEST_RULES = self.check_manifest_rules
|
|
314
|
+
if self.manifest_files:
|
|
315
|
+
App.MANIFEST = Manifest.from_files(
|
|
316
|
+
self.manifest_files,
|
|
317
|
+
root_path=to_absolute_path(self.manifest_rootpath),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if self.deactivate_dependency_driven_build_by_components is not None:
|
|
321
|
+
if self.modified_components is None:
|
|
322
|
+
raise InvalidCommand(
|
|
323
|
+
'Must specify --deactivate-dependency-driven-build-by-components '
|
|
324
|
+
'together with --modified-components'
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if self.deactivate_dependency_driven_build_by_filepatterns is not None:
|
|
328
|
+
if self.modified_files is None:
|
|
329
|
+
raise InvalidCommand(
|
|
330
|
+
'Must specify --deactivate-dependency-driven-build-by-filepatterns together with --modified-files'
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def dependency_driven_build_enabled(self) -> bool:
|
|
335
|
+
"""
|
|
336
|
+
Check if the dependency-driven build feature is enabled
|
|
337
|
+
|
|
338
|
+
:return: True if enabled, False otherwise
|
|
339
|
+
"""
|
|
340
|
+
# not check since modified_components and modified_files are not passed
|
|
341
|
+
if self.modified_components is None and self.modified_files is None:
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
# not check since deactivate_dependency_driven_build_by_components is passed and matched
|
|
345
|
+
if (
|
|
346
|
+
self.deactivate_dependency_driven_build_by_components
|
|
347
|
+
and self.modified_components is not None
|
|
348
|
+
and set(self.modified_components).intersection(self.deactivate_dependency_driven_build_by_components)
|
|
349
|
+
):
|
|
350
|
+
LOGGER.info(
|
|
351
|
+
'Build all apps since modified components %s matches ignored components %s',
|
|
352
|
+
', '.join(self.modified_components),
|
|
353
|
+
', '.join(self.deactivate_dependency_driven_build_by_components),
|
|
354
|
+
)
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
# not check since deactivate_dependency_driven_build_by_filepatterns is passed and matched
|
|
358
|
+
if (
|
|
359
|
+
self.deactivate_dependency_driven_build_by_filepatterns
|
|
360
|
+
and self.modified_files is not None
|
|
361
|
+
and files_matches_patterns(
|
|
362
|
+
self.modified_files, self.deactivate_dependency_driven_build_by_filepatterns, self.manifest_rootpath
|
|
363
|
+
)
|
|
364
|
+
):
|
|
365
|
+
LOGGER.info(
|
|
366
|
+
'Build all apps since modified files %s matches ignored file patterns %s',
|
|
367
|
+
', '.join(self.modified_files),
|
|
368
|
+
', '.join(self.deactivate_dependency_driven_build_by_filepatterns),
|
|
369
|
+
)
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def modified_manifest_rules_folders(self) -> t.Optional[t.Set[str]]:
|
|
376
|
+
if self.compare_manifest_sha_filepath and App.MANIFEST is not None:
|
|
377
|
+
return App.MANIFEST.diff_sha_with_filepath(self.compare_manifest_sha_filepath, use_abspath=True)
|
|
378
|
+
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _os_curdir_as_list() -> t.List[str]:
|
|
383
|
+
return [os.curdir]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclass
|
|
387
|
+
class FindBuildArguments(DependencyDrivenBuildArguments):
|
|
388
|
+
"""
|
|
389
|
+
Arguments used in both find and build commands
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
paths: t.List[str] = field(
|
|
393
|
+
default_factory=_os_curdir_as_list,
|
|
394
|
+
metadata=asdict(
|
|
395
|
+
FieldMetadata(
|
|
396
|
+
default=os.curdir,
|
|
397
|
+
description='Paths to the directories containing the apps. By default set to the current directory',
|
|
398
|
+
shorthand='-p',
|
|
399
|
+
nargs='*',
|
|
400
|
+
)
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
target: str = field(
|
|
404
|
+
default='all',
|
|
405
|
+
metadata=asdict(
|
|
406
|
+
FieldMetadata(
|
|
407
|
+
description='Filter the apps by target. By default set to "all"',
|
|
408
|
+
shorthand='-t',
|
|
409
|
+
)
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
build_system: t.Union[str, t.Type[App]] = field(
|
|
413
|
+
default='cmake',
|
|
414
|
+
metadata=asdict(
|
|
415
|
+
FieldMetadata(
|
|
416
|
+
description='Filter the apps by build system. By default set to "cmake"',
|
|
417
|
+
choices=['cmake', 'make'],
|
|
418
|
+
)
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
recursive: bool = field(
|
|
422
|
+
default=False,
|
|
423
|
+
metadata=asdict(
|
|
424
|
+
FieldMetadata(
|
|
425
|
+
description='Search for apps recursively under the specified paths',
|
|
426
|
+
action='store_true',
|
|
427
|
+
)
|
|
428
|
+
),
|
|
429
|
+
)
|
|
430
|
+
exclude_list: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
|
|
431
|
+
exclude: t.Optional[t.List[str]] = field(
|
|
432
|
+
default=None,
|
|
433
|
+
metadata=asdict(
|
|
434
|
+
FieldMetadata(
|
|
435
|
+
description='Ignore the specified directories while searching recursively',
|
|
436
|
+
nargs='+',
|
|
437
|
+
)
|
|
438
|
+
),
|
|
439
|
+
)
|
|
440
|
+
work_dir: t.Optional[str] = field(
|
|
441
|
+
default=None,
|
|
442
|
+
metadata=asdict(
|
|
443
|
+
FieldMetadata(
|
|
444
|
+
description='Copy the app to this directory before building. '
|
|
445
|
+
'By default set to the app directory. Can expand placeholders',
|
|
446
|
+
)
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
build_dir: str = field(
|
|
450
|
+
default='build',
|
|
451
|
+
metadata=asdict(
|
|
452
|
+
FieldMetadata(
|
|
453
|
+
description='Build directory for the app. By default set to "build". '
|
|
454
|
+
'When set to relative path, it will be treated as relative to the app directory. '
|
|
455
|
+
'Can expand placeholders',
|
|
456
|
+
)
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
build_log: InitVar[t.Optional[str]] = _Field.UNSET
|
|
460
|
+
build_log_filename: t.Optional[str] = field(
|
|
461
|
+
default=None,
|
|
462
|
+
metadata=asdict(
|
|
463
|
+
FieldMetadata(
|
|
464
|
+
deprecates={'build_log': {}},
|
|
465
|
+
description='Log filename under the build directory instead of stdout. Can expand placeholders',
|
|
466
|
+
)
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
size_file: InitVar[t.Optional[str]] = _Field.UNSET
|
|
470
|
+
size_json_filename: t.Optional[str] = field(
|
|
471
|
+
default=None,
|
|
472
|
+
metadata=asdict(
|
|
473
|
+
FieldMetadata(
|
|
474
|
+
deprecates={'size_file': {}},
|
|
475
|
+
description='`idf.py size` output file under the build directory when specified. '
|
|
476
|
+
'Can expand placeholders',
|
|
477
|
+
)
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
config: InitVar[t.Union[t.List[str], str, None]] = _Field.UNSET # cli # type: ignore
|
|
481
|
+
config_rules_str: InitVar[t.Union[t.List[str], str, None]] = _Field.UNSET # func # type: ignore
|
|
482
|
+
config_rules: t.Optional[t.List[str]] = field(
|
|
483
|
+
default=None,
|
|
484
|
+
metadata=asdict(
|
|
485
|
+
FieldMetadata(
|
|
486
|
+
deprecates={
|
|
487
|
+
'config': {'nargs': '+'},
|
|
488
|
+
},
|
|
489
|
+
description='Defines the rules of building the project with pre-set sdkconfig files. '
|
|
490
|
+
'Supports FILENAME[=NAME] or FILEPATTERN format. '
|
|
491
|
+
'FILENAME is the filename of the sdkconfig file, relative to the app directory. '
|
|
492
|
+
'Optional NAME is the name of the configuration. '
|
|
493
|
+
'if not specified, the filename is used as the name. '
|
|
494
|
+
'FILEPATTERN is the filename of the sdkconfig file with a single wildcard character (*). '
|
|
495
|
+
'The NAME is the value matched by the wildcard',
|
|
496
|
+
nargs='+',
|
|
497
|
+
)
|
|
498
|
+
),
|
|
499
|
+
)
|
|
500
|
+
override_sdkconfig_items: t.Optional[str] = field(
|
|
501
|
+
default=None,
|
|
502
|
+
metadata=asdict(
|
|
503
|
+
FieldMetadata(
|
|
504
|
+
description='A comma-separated list of key=value pairs to override the sdkconfig items',
|
|
505
|
+
)
|
|
506
|
+
),
|
|
507
|
+
)
|
|
508
|
+
override_sdkconfig_files: t.Optional[str] = field(
|
|
509
|
+
default=None,
|
|
510
|
+
metadata=asdict(
|
|
511
|
+
FieldMetadata(
|
|
512
|
+
description='A comma-separated list of sdkconfig files to override the sdkconfig items. '
|
|
513
|
+
'When set to relative path, it will be treated as relative to the current directory',
|
|
514
|
+
)
|
|
515
|
+
),
|
|
516
|
+
)
|
|
517
|
+
sdkconfig_defaults: t.Optional[str] = field(
|
|
518
|
+
default=os.getenv('SDKCONFIG_DEFAULTS', None),
|
|
519
|
+
metadata=asdict(
|
|
520
|
+
FieldMetadata(
|
|
521
|
+
description='A semicolon-separated list of sdkconfig files passed to `idf.py -DSDKCONFIG_DEFAULTS`. '
|
|
522
|
+
'SDKCONFIG_DEFAULTS environment variable is used when not specified',
|
|
523
|
+
)
|
|
524
|
+
),
|
|
525
|
+
)
|
|
526
|
+
check_warnings: bool = field(
|
|
527
|
+
default=False,
|
|
528
|
+
metadata=asdict(
|
|
529
|
+
FieldMetadata(
|
|
530
|
+
description='Check for warnings in the build output. Fail if any warnings are found',
|
|
531
|
+
action='store_true',
|
|
532
|
+
)
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
default_build_targets: t.Optional[t.List[str]] = field(
|
|
536
|
+
default=None,
|
|
537
|
+
metadata=asdict(
|
|
538
|
+
FieldMetadata(
|
|
539
|
+
description='space-separated list of the default enabled build targets for the apps. '
|
|
540
|
+
'When not specified, the default value is the targets listed by `idf.py --list-targets`',
|
|
541
|
+
)
|
|
542
|
+
),
|
|
543
|
+
)
|
|
544
|
+
enable_preview_targets: bool = field(
|
|
545
|
+
default=False,
|
|
546
|
+
metadata=asdict(
|
|
547
|
+
FieldMetadata(
|
|
548
|
+
description='When enabled, the default build targets will be set to all apps, '
|
|
549
|
+
'including the preview targets. As the targets defined in `idf.py --list-targets --preview`',
|
|
550
|
+
action='store_true',
|
|
551
|
+
)
|
|
552
|
+
),
|
|
553
|
+
)
|
|
554
|
+
include_skipped_apps: bool = field(
|
|
555
|
+
default=False,
|
|
556
|
+
metadata=asdict(
|
|
557
|
+
FieldMetadata(
|
|
558
|
+
description='Include the skipped apps in the output, together with the enabled ones',
|
|
559
|
+
action='store_true',
|
|
560
|
+
)
|
|
561
|
+
),
|
|
562
|
+
)
|
|
563
|
+
include_disabled_apps: bool = field(
|
|
564
|
+
default=False,
|
|
565
|
+
metadata=asdict(
|
|
566
|
+
FieldMetadata(
|
|
567
|
+
description='Include the disabled apps in the output, together with the enabled ones',
|
|
568
|
+
action='store_true',
|
|
569
|
+
)
|
|
570
|
+
),
|
|
571
|
+
)
|
|
572
|
+
include_all_apps: bool = field(
|
|
573
|
+
default=False,
|
|
574
|
+
metadata=asdict(
|
|
575
|
+
FieldMetadata(
|
|
576
|
+
description='Include skipped, and disabled apps in the output, together with the enabled ones',
|
|
577
|
+
action='store_true',
|
|
578
|
+
)
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def __post_init__( # type: ignore
|
|
583
|
+
self,
|
|
584
|
+
manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
585
|
+
ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
586
|
+
ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
587
|
+
exclude_list: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
588
|
+
build_log: t.Optional[str] = _Field.UNSET, # type: ignore
|
|
589
|
+
size_file: t.Optional[str] = _Field.UNSET, # type: ignore
|
|
590
|
+
config: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
591
|
+
config_rules_str: t.Union[t.List[str], str, None] = _Field.UNSET, # type: ignore
|
|
592
|
+
):
|
|
593
|
+
super().__post_init__(
|
|
594
|
+
manifest_file=manifest_file,
|
|
595
|
+
ignore_app_dependencies_components=ignore_app_dependencies_components,
|
|
596
|
+
ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
self.set_deprecated_field('exclude', 'exclude_list', exclude_list)
|
|
600
|
+
self.set_deprecated_field('build_log_filename', 'build_log', build_log)
|
|
601
|
+
self.set_deprecated_field('size_json_filename', 'size_file', size_file)
|
|
602
|
+
self.set_deprecated_field('config_rules', 'config', config)
|
|
603
|
+
self.set_deprecated_field('config_rules', 'config_rules_str', config_rules_str)
|
|
604
|
+
|
|
605
|
+
self.paths = to_list(self.paths)
|
|
606
|
+
self.config_rules = to_list(self.config_rules)
|
|
607
|
+
self.exclude = to_list(self.exclude)
|
|
608
|
+
|
|
609
|
+
# Validation
|
|
610
|
+
if not self.paths:
|
|
611
|
+
raise InvalidCommand('At least one path must be specified')
|
|
612
|
+
|
|
613
|
+
if not self.target:
|
|
614
|
+
LOGGER.debug('--target is missing. Set --target as "all".')
|
|
615
|
+
self.target = 'all'
|
|
616
|
+
|
|
617
|
+
if self.default_build_targets:
|
|
618
|
+
default_build_targets = []
|
|
619
|
+
for target in self.default_build_targets:
|
|
620
|
+
if target not in ALL_TARGETS:
|
|
621
|
+
LOGGER.warning(
|
|
622
|
+
f'Ignoring... Unrecognizable target {target} specified with "--default-build-targets". '
|
|
623
|
+
f'Current ESP-IDF available targets: {ALL_TARGETS}'
|
|
624
|
+
)
|
|
625
|
+
elif target not in default_build_targets:
|
|
626
|
+
default_build_targets.append(target)
|
|
627
|
+
self.default_build_targets = default_build_targets
|
|
628
|
+
LOGGER.info('Overriding default build targets to %s', self.default_build_targets)
|
|
629
|
+
FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
|
|
630
|
+
elif self.enable_preview_targets:
|
|
631
|
+
self.default_build_targets = deepcopy(ALL_TARGETS)
|
|
632
|
+
LOGGER.info('Overriding default build targets to %s', self.default_build_targets)
|
|
633
|
+
FolderRule.DEFAULT_BUILD_TARGETS = self.default_build_targets
|
|
634
|
+
|
|
635
|
+
if self.override_sdkconfig_items or self.override_sdkconfig_items:
|
|
636
|
+
SESSION_ARGS.set(self)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@dataclass
|
|
640
|
+
class FindArguments(FindBuildArguments):
|
|
641
|
+
"""
|
|
642
|
+
Arguments used in the find command
|
|
643
|
+
"""
|
|
644
|
+
|
|
645
|
+
output: t.Optional[str] = field(
|
|
646
|
+
default=None,
|
|
647
|
+
metadata=asdict(
|
|
648
|
+
FieldMetadata(
|
|
649
|
+
description='Record the found apps to the specified file instead of stdout',
|
|
650
|
+
shorthand='-o',
|
|
651
|
+
)
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
output_format: str = field(
|
|
655
|
+
default='raw',
|
|
656
|
+
metadata=asdict(
|
|
657
|
+
FieldMetadata(
|
|
658
|
+
description='Output format of the found apps. '
|
|
659
|
+
'In "raw" format, each line is a json string serialized from the app model. '
|
|
660
|
+
'In "json" format, the output is a json list of the serialized app models',
|
|
661
|
+
choices=['raw', 'json'],
|
|
662
|
+
)
|
|
663
|
+
),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
def __post_init__( # type: ignore
|
|
667
|
+
self,
|
|
668
|
+
manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
669
|
+
ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
670
|
+
ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
671
|
+
exclude_list: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
672
|
+
build_log: t.Optional[str] = _Field.UNSET, # type: ignore
|
|
673
|
+
size_file: t.Optional[str] = _Field.UNSET, # type: ignore
|
|
674
|
+
config: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
675
|
+
config_rules_str: t.Union[t.List[str], str, None] = _Field.UNSET, # type: ignore
|
|
676
|
+
):
|
|
677
|
+
super().__post_init__(
|
|
678
|
+
manifest_file=manifest_file,
|
|
679
|
+
ignore_app_dependencies_components=ignore_app_dependencies_components,
|
|
680
|
+
ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
|
|
681
|
+
exclude_list=exclude_list,
|
|
682
|
+
build_log=build_log,
|
|
683
|
+
size_file=size_file,
|
|
684
|
+
config=config,
|
|
685
|
+
config_rules_str=config_rules_str,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
if self.include_all_apps:
|
|
689
|
+
self.include_skipped_apps = True
|
|
690
|
+
self.include_disabled_apps = True
|
|
691
|
+
|
|
692
|
+
if self.output and self.output.endswith('.json') and self.output_format in ['raw', None]:
|
|
693
|
+
LOGGER.debug('Detecting output file ends with ".json", writing as json file.')
|
|
694
|
+
self.output_format = 'json'
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
@dataclass
|
|
698
|
+
class BuildArguments(FindBuildArguments):
|
|
699
|
+
build_verbose: bool = field(
|
|
700
|
+
default=False,
|
|
701
|
+
metadata=asdict(
|
|
702
|
+
FieldMetadata(
|
|
703
|
+
description='Enable verbose output of the build system',
|
|
704
|
+
action='store_true',
|
|
705
|
+
)
|
|
706
|
+
),
|
|
707
|
+
)
|
|
708
|
+
parallel_count: int = field(
|
|
709
|
+
default=1,
|
|
710
|
+
metadata=asdict(
|
|
711
|
+
FieldMetadata(
|
|
712
|
+
description='Number of parallel build jobs in total. '
|
|
713
|
+
'Specified together with --parallel-index. '
|
|
714
|
+
'The given apps will be divided into parallel_count parts, '
|
|
715
|
+
'and the current run will build the parallel_index-th part',
|
|
716
|
+
type=int,
|
|
717
|
+
)
|
|
718
|
+
),
|
|
719
|
+
)
|
|
720
|
+
parallel_index: int = field(
|
|
721
|
+
default=1,
|
|
722
|
+
metadata=asdict(
|
|
723
|
+
FieldMetadata(
|
|
724
|
+
description='Index (1-based) of the parallel build job. '
|
|
725
|
+
'Specified together with --parallel-count. '
|
|
726
|
+
'The given apps will be divided into parallel_count parts, '
|
|
727
|
+
'and the current run will build the parallel_index-th part',
|
|
728
|
+
type=int,
|
|
729
|
+
)
|
|
730
|
+
),
|
|
731
|
+
)
|
|
732
|
+
dry_run: bool = field(
|
|
733
|
+
default=False,
|
|
734
|
+
metadata=asdict(
|
|
735
|
+
FieldMetadata(
|
|
736
|
+
description='Skip the actual build, only print the build process',
|
|
737
|
+
action='store_true',
|
|
738
|
+
)
|
|
739
|
+
),
|
|
740
|
+
)
|
|
741
|
+
keep_going: bool = field(
|
|
742
|
+
default=False,
|
|
743
|
+
metadata=asdict(
|
|
744
|
+
FieldMetadata(
|
|
745
|
+
description='Continue building the next app when the current build fails',
|
|
746
|
+
action='store_true',
|
|
747
|
+
)
|
|
748
|
+
),
|
|
749
|
+
)
|
|
750
|
+
no_preserve: bool = field(
|
|
751
|
+
default=False,
|
|
752
|
+
metadata=asdict(
|
|
753
|
+
FieldMetadata(
|
|
754
|
+
description='Do not preserve the build directory after a successful build',
|
|
755
|
+
action='store_true',
|
|
756
|
+
)
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
collect_size_info: t.Optional[str] = field(
|
|
760
|
+
default=None,
|
|
761
|
+
metadata=asdict(
|
|
762
|
+
FieldMetadata(
|
|
763
|
+
description='Record size json filepath of the built apps to the specified file. '
|
|
764
|
+
'Each line is a json string. Can expand placeholders @p',
|
|
765
|
+
)
|
|
766
|
+
),
|
|
767
|
+
)
|
|
768
|
+
_collect_size_info: t.Optional[str] = field(init=False, repr=False, default=None)
|
|
769
|
+
collect_app_info: t.Optional[str] = field(
|
|
770
|
+
default=None,
|
|
771
|
+
metadata=asdict(
|
|
772
|
+
FieldMetadata(
|
|
773
|
+
description='Record serialized app model of the built apps to the specified file. '
|
|
774
|
+
'Each line is a json string. Can expand placeholders @p',
|
|
775
|
+
)
|
|
776
|
+
),
|
|
777
|
+
)
|
|
778
|
+
_collect_app_info: t.Optional[str] = field(init=False, repr=False, default=None)
|
|
779
|
+
ignore_warning_str: InitVar[t.Optional[t.List[str]]] = _Field.UNSET
|
|
780
|
+
ignore_warning_strs: t.Optional[t.List[str]] = field(
|
|
781
|
+
default=None,
|
|
782
|
+
metadata=asdict(
|
|
783
|
+
FieldMetadata(
|
|
784
|
+
deprecates={
|
|
785
|
+
'ignore_warning_str': {'nargs': '+'},
|
|
786
|
+
},
|
|
787
|
+
description='space-separated list of patterns. '
|
|
788
|
+
'Ignore the warnings in the build output that match the patterns',
|
|
789
|
+
nargs='+',
|
|
790
|
+
)
|
|
791
|
+
),
|
|
792
|
+
)
|
|
793
|
+
ignore_warning_file: InitVar[t.Optional[str]] = _Field.UNSET
|
|
794
|
+
ignore_warning_files: t.Optional[t.List[str]] = field(
|
|
795
|
+
default=None,
|
|
796
|
+
metadata=asdict(
|
|
797
|
+
FieldMetadata(
|
|
798
|
+
deprecates={'ignore_warning_file': {}},
|
|
799
|
+
description='Path to the files containing the patterns to ignore the warnings in the build output',
|
|
800
|
+
nargs='+',
|
|
801
|
+
)
|
|
802
|
+
),
|
|
803
|
+
)
|
|
804
|
+
copy_sdkconfig: bool = field(
|
|
805
|
+
default=False,
|
|
806
|
+
metadata=asdict(
|
|
807
|
+
FieldMetadata(
|
|
808
|
+
description='Copy the sdkconfig file to the build directory',
|
|
809
|
+
action='store_true',
|
|
810
|
+
)
|
|
811
|
+
),
|
|
812
|
+
)
|
|
813
|
+
junitxml: t.Optional[str] = field(
|
|
814
|
+
default=None,
|
|
815
|
+
metadata=asdict(
|
|
816
|
+
FieldMetadata(
|
|
817
|
+
description='Path to the junitxml file to record the build results. Can expand placeholder @p',
|
|
818
|
+
)
|
|
819
|
+
),
|
|
820
|
+
)
|
|
821
|
+
_junitxml: t.Optional[str] = field(init=False, repr=False, default=None)
|
|
822
|
+
|
|
823
|
+
# used for expanding placeholders
|
|
824
|
+
PARALLEL_INDEX_PLACEHOLDER: t.ClassVar[str] = '@p' # replace it with the parallel index
|
|
825
|
+
|
|
826
|
+
def __post_init__( # type: ignore
|
|
827
|
+
self,
|
|
828
|
+
manifest_file: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
829
|
+
ignore_app_dependencies_components: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
830
|
+
ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
831
|
+
exclude_list: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
832
|
+
build_log: t.Optional[str] = _Field.UNSET, # type: ignore
|
|
833
|
+
size_file: t.Optional[str] = _Field.UNSET, # type: ignore
|
|
834
|
+
config: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
835
|
+
config_rules_str: t.Union[t.List[str], str, None] = _Field.UNSET, # type: ignore
|
|
836
|
+
ignore_warning_str: t.Optional[t.List[str]] = _Field.UNSET, # type: ignore
|
|
837
|
+
ignore_warning_file: t.Optional[str] = _Field.UNSET, # type: ignore
|
|
838
|
+
):
|
|
839
|
+
super().__post_init__(
|
|
840
|
+
manifest_file=manifest_file,
|
|
841
|
+
ignore_app_dependencies_components=ignore_app_dependencies_components,
|
|
842
|
+
ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
|
|
843
|
+
exclude_list=exclude_list,
|
|
844
|
+
build_log=build_log,
|
|
845
|
+
size_file=size_file,
|
|
846
|
+
config=config,
|
|
847
|
+
config_rules_str=config_rules_str,
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
self.set_deprecated_field('ignore_warning_strs', 'ignore_warning_str', ignore_warning_str)
|
|
851
|
+
self.set_deprecated_field('ignore_warning_files', 'ignore_warning_file', ignore_warning_file)
|
|
852
|
+
|
|
853
|
+
self.ignore_warning_strs = to_list(self.ignore_warning_strs) or []
|
|
854
|
+
|
|
855
|
+
ignore_warnings_regexes = []
|
|
856
|
+
if self.ignore_warning_strs:
|
|
857
|
+
for s in self.ignore_warning_strs:
|
|
858
|
+
ignore_warnings_regexes.append(re.compile(s))
|
|
859
|
+
if self.ignore_warning_files:
|
|
860
|
+
for s in self.ignore_warning_files:
|
|
861
|
+
ignore_warnings_regexes.append(re.compile(s.strip()))
|
|
862
|
+
App.IGNORE_WARNS_REGEXES = ignore_warnings_regexes
|
|
863
|
+
|
|
864
|
+
if not isinstance(BuildArguments.collect_size_info, property):
|
|
865
|
+
self._collect_size_info = self.collect_size_info
|
|
866
|
+
BuildArguments.collect_size_info = property( # type: ignore
|
|
867
|
+
BuildArguments._get_collect_size_info,
|
|
868
|
+
BuildArguments._set_collect_size_info,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
if not isinstance(BuildArguments.collect_app_info, property):
|
|
872
|
+
self._collect_app_info = self.collect_app_info
|
|
873
|
+
BuildArguments.collect_app_info = property( # type: ignore
|
|
874
|
+
BuildArguments._get_collect_app_info,
|
|
875
|
+
BuildArguments._set_collect_app_info,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
if not isinstance(BuildArguments.junitxml, property):
|
|
879
|
+
self._junitxml = self.junitxml
|
|
880
|
+
BuildArguments.junitxml = property( # type: ignore
|
|
881
|
+
BuildArguments._get_junitxml,
|
|
882
|
+
BuildArguments._set_junitxml,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def _get_collect_size_info(self) -> t.Optional[str]:
|
|
886
|
+
return (
|
|
887
|
+
self._collect_size_info.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
|
|
888
|
+
if self._collect_size_info
|
|
889
|
+
else None
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
def _set_collect_size_info(self, k: str) -> None:
|
|
893
|
+
self._collect_size_info = k
|
|
894
|
+
|
|
895
|
+
def _get_collect_app_info(self) -> t.Optional[str]:
|
|
896
|
+
return (
|
|
897
|
+
self._collect_app_info.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
|
|
898
|
+
if self._collect_app_info
|
|
899
|
+
else None
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
def _set_collect_app_info(self, k: str) -> None:
|
|
903
|
+
self._collect_app_info = k
|
|
904
|
+
|
|
905
|
+
def _get_junitxml(self) -> t.Optional[str]:
|
|
906
|
+
return (
|
|
907
|
+
self._junitxml.replace(self.PARALLEL_INDEX_PLACEHOLDER, str(self.parallel_index))
|
|
908
|
+
if self._junitxml
|
|
909
|
+
else None
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
def _set_junitxml(self, k: str) -> None:
|
|
913
|
+
self._junitxml = k
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@dataclass
|
|
917
|
+
class DumpManifestShaArguments(GlobalArguments):
|
|
918
|
+
"""
|
|
919
|
+
Arguments used in the dump-manifest-sha command
|
|
920
|
+
"""
|
|
921
|
+
|
|
922
|
+
manifest_files: t.Optional[t.List[str]] = field(
|
|
923
|
+
default=None,
|
|
924
|
+
metadata=asdict(
|
|
925
|
+
FieldMetadata(
|
|
926
|
+
description='Path to the manifest files which contains the build test rules of the apps',
|
|
927
|
+
nargs='+',
|
|
928
|
+
required=True,
|
|
929
|
+
)
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
output: t.Optional[str] = field(
|
|
933
|
+
default=None,
|
|
934
|
+
metadata=asdict(
|
|
935
|
+
FieldMetadata(
|
|
936
|
+
description='Record the sha256 hash of the manifest rules to the specified file',
|
|
937
|
+
shorthand='-o',
|
|
938
|
+
required=True,
|
|
939
|
+
)
|
|
940
|
+
),
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
def __post_init__(self):
|
|
944
|
+
super().__post_init__()
|
|
945
|
+
|
|
946
|
+
# Validation
|
|
947
|
+
self.manifest_files = to_list(self.manifest_files)
|
|
948
|
+
if not self.manifest_files:
|
|
949
|
+
raise InvalidCommand('Manifest files are required to dump the SHA values.')
|
|
950
|
+
if not self.output:
|
|
951
|
+
raise InvalidCommand('Output file is required to dump the SHA values.')
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def add_arguments_to_parser(argument_cls: t.Type[GlobalArguments], parser: argparse.ArgumentParser) -> None:
|
|
955
|
+
"""
|
|
956
|
+
Add arguments to the parser from the argument class.
|
|
957
|
+
|
|
958
|
+
FieldMetadata is used to set the argparse options.
|
|
959
|
+
|
|
960
|
+
:param argument_cls: argument class
|
|
961
|
+
:param parser: argparse parser
|
|
962
|
+
"""
|
|
963
|
+
name_fields_dict = {f.name: f for f in fields(argument_cls)}
|
|
964
|
+
|
|
965
|
+
def _snake_case_to_cli_arg_name(s: str) -> str:
|
|
966
|
+
return f'--{s.replace("_", "-")}'
|
|
967
|
+
|
|
968
|
+
for name, f in name_fields_dict.items():
|
|
969
|
+
_meta = FieldMetadata(**f.metadata)
|
|
970
|
+
|
|
971
|
+
desp = _meta.description
|
|
972
|
+
# add deprecated fields
|
|
973
|
+
if _meta.deprecates:
|
|
974
|
+
for depr_k, depr_kwargs in _meta.deprecates.items():
|
|
975
|
+
depr_kwargs['help'] = f'[DEPRECATED by {_snake_case_to_cli_arg_name(name)}] {desp}'
|
|
976
|
+
short_name = depr_kwargs.pop('shorthand', None)
|
|
977
|
+
_names = [_snake_case_to_cli_arg_name(depr_k)]
|
|
978
|
+
if short_name:
|
|
979
|
+
_names.append(short_name)
|
|
980
|
+
parser.add_argument(*_names, **depr_kwargs)
|
|
981
|
+
|
|
982
|
+
# args
|
|
983
|
+
args = [_snake_case_to_cli_arg_name(name)]
|
|
984
|
+
if _meta.shorthand:
|
|
985
|
+
args.append(_meta.shorthand)
|
|
986
|
+
|
|
987
|
+
# kwargs passed to add_argument
|
|
988
|
+
kwargs = drop_none_kwargs(
|
|
989
|
+
{
|
|
990
|
+
'help': desp,
|
|
991
|
+
'action': _meta.action,
|
|
992
|
+
'nargs': _meta.nargs,
|
|
993
|
+
'choices': _meta.choices,
|
|
994
|
+
'type': _meta.type,
|
|
995
|
+
'required': _meta.required,
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
# default None is important for argparse
|
|
999
|
+
kwargs['default'] = _meta.default or getattr(f, 'default', None)
|
|
1000
|
+
|
|
1001
|
+
parser.add_argument(*args, **kwargs)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def add_arguments_to_obj_doc_as_params(argument_cls: t.Type[GlobalArguments], obj: t.Any = None) -> None:
|
|
1005
|
+
"""
|
|
1006
|
+
Add arguments to the function as parameters.
|
|
1007
|
+
|
|
1008
|
+
:param argument_cls: argument class
|
|
1009
|
+
:param obj: object to add the docstring to
|
|
1010
|
+
"""
|
|
1011
|
+
_obj = obj or argument_cls
|
|
1012
|
+
_docs_s = _obj.__doc__ or ''
|
|
1013
|
+
_docs_s += '\n'
|
|
1014
|
+
|
|
1015
|
+
for f in fields(argument_cls):
|
|
1016
|
+
# typing generic alias is not a class
|
|
1017
|
+
_annotation = f.type.__name__ if inspect.isclass(f.type) else f.type
|
|
1018
|
+
|
|
1019
|
+
_docs_s += f' :param {f.name}: {f.metadata.get("description", "")}\n'
|
|
1020
|
+
_docs_s += f' :type {f.name}: {_annotation}\n'
|
|
1021
|
+
|
|
1022
|
+
_obj.__doc__ = _docs_s
|