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/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