idf-build-apps 2.0.0b5__py3-none-any.whl → 2.0.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.
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  """
@@ -8,7 +8,7 @@ Tools for building ESP-IDF related apps.
8
8
  # ruff: noqa: E402
9
9
  # avoid circular imports
10
10
 
11
- __version__ = '2.0.0b5'
11
+ __version__ = '2.0.0rc1'
12
12
 
13
13
  from .session_args import (
14
14
  SessionArgs,
@@ -28,6 +28,7 @@ from .log import (
28
28
  from .main import (
29
29
  build_apps,
30
30
  find_apps,
31
+ json_to_app,
31
32
  )
32
33
 
33
34
  __all__ = [
@@ -37,6 +38,6 @@ __all__ = [
37
38
  'MakeApp',
38
39
  'find_apps',
39
40
  'build_apps',
41
+ 'json_to_app',
40
42
  'setup_logging',
41
- 'SESSION_ARGS',
42
43
  ]
idf_build_apps/app.py CHANGED
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import functools
@@ -8,17 +8,10 @@ import os
8
8
  import re
9
9
  import shutil
10
10
  import sys
11
- import tempfile
12
11
  import typing as t
13
- from copy import (
14
- deepcopy,
15
- )
16
12
  from datetime import (
17
13
  datetime,
18
14
  )
19
- from functools import (
20
- lru_cache,
21
- )
22
15
  from pathlib import (
23
16
  Path,
24
17
  )
@@ -31,10 +24,9 @@ from pydantic import (
31
24
  computed_field,
32
25
  )
33
26
 
34
- from idf_build_apps import (
27
+ from . import (
35
28
  SESSION_ARGS,
36
29
  )
37
-
38
30
  from .build_apps_args import (
39
31
  BuildAppsArgs,
40
32
  )
@@ -92,8 +84,8 @@ class App(BaseModel):
92
84
  WILDCARD_PLACEHOLDER: t.ClassVar[str] = '@w' # replace it with the wildcard, usually the sdkconfig
93
85
  NAME_PLACEHOLDER: t.ClassVar[str] = '@n' # replace it with self.name
94
86
  FULL_NAME_PLACEHOLDER: t.ClassVar[str] = '@f' # replace it with escaped self.app_dir
95
- INDEX_PLACEHOLDER: t.ClassVar[str] = '@i' # replace it with the build index
96
87
  IDF_VERSION_PLACEHOLDER: t.ClassVar[str] = '@v' # replace it with the IDF version
88
+ INDEX_PLACEHOLDER: t.ClassVar[str] = '@i' # replace it with the build index (while build_apps)
97
89
 
98
90
  SDKCONFIG_LINE_REGEX: t.ClassVar[t.Pattern] = re.compile(r"^([^=]+)=\"?([^\"\n]*)\"?\n*$")
99
91
 
@@ -115,8 +107,7 @@ class App(BaseModel):
115
107
  target: str
116
108
  sdkconfig_path: t.Optional[str] = None
117
109
  config_name: t.Optional[str] = None
118
-
119
- build_status: BuildStatus = BuildStatus.UNKNOWN
110
+ sdkconfig_defaults_str: t.Optional[str] = None
120
111
 
121
112
  # Attrs that support placeholders
122
113
  _work_dir: t.Optional[str] = None
@@ -125,48 +116,46 @@ class App(BaseModel):
125
116
  _build_log_filename: t.Optional[str] = None
126
117
  _size_json_filename: t.Optional[str] = None
127
118
 
128
- # Build related
129
119
  dry_run: bool = False
130
- index: t.Union[int, None] = None
131
120
  verbose: bool = False
132
121
  check_warnings: bool = False
133
122
  preserve: bool = True
134
123
 
135
- # logging
136
- build_apps_args: t.Optional[BuildAppsArgs] = BuildAppsArgs()
124
+ # build_apps() related
125
+ build_apps_args: t.Optional[BuildAppsArgs] = None
126
+ index: t.Optional[int] = None
127
+
128
+ # build status related
129
+ build_status: BuildStatus = BuildStatus.UNKNOWN
130
+ build_comment: t.Optional[str] = None
137
131
 
138
- _build_comment: t.Optional[str] = None
139
132
  _build_stage: t.Optional[BuildStage] = None
140
133
  _build_duration: float = 0
141
134
  _build_timestamp: t.Optional[datetime] = None
142
135
 
136
+ __EQ_IGNORE_FIELDS__ = [
137
+ 'build_comment',
138
+ ]
139
+
143
140
  def __init__(
144
141
  self,
145
142
  app_dir: str,
146
143
  target: str,
147
144
  *,
148
- sdkconfig_path: t.Optional[str] = None,
149
- config_name: t.Optional[str] = None,
150
145
  work_dir: t.Optional[str] = None,
151
146
  build_dir: str = 'build',
152
147
  build_log_filename: t.Optional[str] = None,
153
148
  size_json_filename: t.Optional[str] = None,
154
- check_warnings: bool = False,
155
- preserve: bool = True,
156
- sdkconfig_defaults_str: t.Optional[str] = None,
157
149
  **kwargs: t.Any,
158
150
  ) -> None:
159
151
  kwargs.update(
160
152
  {
161
153
  'app_dir': app_dir,
162
154
  'target': target,
163
- 'sdkconfig_path': sdkconfig_path,
164
- 'config_name': config_name,
165
- 'check_warnings': check_warnings,
166
- 'preserve': preserve,
167
155
  }
168
156
  )
169
157
  super().__init__(**kwargs)
158
+
170
159
  # These internal variables store the paths with environment variables and placeholders;
171
160
  # Public properties with similar names use the _expand method to get the actual paths.
172
161
  self._work_dir = work_dir or app_dir
@@ -174,31 +163,26 @@ class App(BaseModel):
174
163
 
175
164
  self._build_log_filename = build_log_filename
176
165
  self._size_json_filename = size_json_filename
177
-
178
- # should be built or not
179
- self._checked_should_build = False
180
-
181
- # sdkconfig attrs, use properties instead
182
- self._sdkconfig_defaults = self._get_sdkconfig_defaults(sdkconfig_defaults_str)
183
- self._sdkconfig_files: t.List[str] = None # type: ignore
184
- self._sdkconfig_files_defined_target: str = None # type: ignore
166
+ self._is_build_log_path_temp = not bool(build_log_filename)
185
167
 
186
168
  # pass all parameters to initialize hook method
187
169
  kwargs.update(
188
170
  {
189
- 'work_dir': work_dir,
190
- 'build_dir': build_dir,
171
+ 'work_dir': self._work_dir,
172
+ 'build_dir': self._build_dir,
191
173
  'build_log_filename': build_log_filename,
192
174
  'size_json_filename': size_json_filename,
193
- 'sdkconfig_defaults_str': sdkconfig_defaults_str,
194
175
  }
195
176
  )
196
177
  self._initialize_hook(**kwargs)
197
178
 
179
+ # private attrs, won't be dumped to json
180
+ self._checked_should_build = False
181
+
198
182
  self._logger = logging.getLogger(f'{__name__}.{hash(self)}')
199
183
  self._logger.addFilter(_AppBuildStageFilter(app=self))
200
184
 
201
- self._process_sdkconfig_files()
185
+ self._sdkconfig_files, self._sdkconfig_files_defined_target = self._process_sdkconfig_files()
202
186
 
203
187
  def _initialize_hook(self, **kwargs):
204
188
  """
@@ -231,16 +215,15 @@ class App(BaseModel):
231
215
 
232
216
  return default_fmt.format(*default_args)
233
217
 
234
- @staticmethod
235
- def _get_sdkconfig_defaults(sdkconfig_defaults_str: t.Optional[str] = None) -> t.List[str]:
236
- if sdkconfig_defaults_str is not None:
237
- candidates = sdkconfig_defaults_str.split(';')
238
- elif os.getenv('SDKCONFIG_DEFAULTS', None) is not None:
239
- candidates = os.getenv('SDKCONFIG_DEFAULTS', '').split(';')
240
- else:
241
- candidates = [DEFAULT_SDKCONFIG]
218
+ @property
219
+ def sdkconfig_defaults_candidates(self) -> t.List[str]:
220
+ if self.sdkconfig_defaults_str is not None:
221
+ return self.sdkconfig_defaults_str.split(';')
242
222
 
243
- return candidates
223
+ if os.getenv('SDKCONFIG_DEFAULTS', None) is not None:
224
+ return os.getenv('SDKCONFIG_DEFAULTS', '').split(';')
225
+
226
+ return [DEFAULT_SDKCONFIG]
244
227
 
245
228
  @t.overload
246
229
  def _expand(self, path: None) -> None:
@@ -259,7 +242,8 @@ class App(BaseModel):
259
242
 
260
243
  if self.index is not None:
261
244
  path = path.replace(self.INDEX_PLACEHOLDER, str(self.index))
262
- path = self.build_apps_args.expand(path)
245
+ if self.build_apps_args:
246
+ path = self.build_apps_args.expand(path)
263
247
  path = path.replace(
264
248
  self.IDF_VERSION_PLACEHOLDER, f'{IDF_VERSION_MAJOR}_{IDF_VERSION_MINOR}_{IDF_VERSION_PATCH}'
265
249
  )
@@ -283,7 +267,12 @@ class App(BaseModel):
283
267
 
284
268
  @property
285
269
  def name(self) -> str:
286
- return os.path.basename(os.path.realpath(self.app_dir))
270
+ base_name = os.path.basename(self.app_dir)
271
+ # '.' for relative path like '.'
272
+ # '' for path endswith '/'
273
+ if base_name in ['.', '']:
274
+ return os.path.basename(os.path.abspath(self.app_dir))
275
+ return base_name
287
276
 
288
277
  @computed_field # type: ignore
289
278
  @property
@@ -308,25 +297,18 @@ class App(BaseModel):
308
297
 
309
298
  return os.path.join(self.work_dir, self.build_dir)
310
299
 
311
- @property
312
- def build_comment(self) -> str:
313
- return self._build_comment or ''
314
-
315
- @build_comment.setter
316
- def build_comment(self, value: str) -> None:
317
- self._build_comment = value
318
-
319
300
  @computed_field # type: ignore
320
301
  @property
321
302
  def build_log_filename(self) -> t.Optional[str]:
322
303
  return self._expand(self._build_log_filename)
323
304
 
324
305
  @property
325
- def build_log_path(self) -> t.Optional[str]:
306
+ def build_log_path(self) -> str:
326
307
  if self.build_log_filename:
327
308
  return os.path.join(self.build_path, self.build_log_filename)
328
309
 
329
- return None
310
+ # use a temp file if build log path is not specified
311
+ return os.path.join(self.build_path, f'.temp.build.{hash(self)}.log')
330
312
 
331
313
  @computed_field # type: ignore
332
314
  @property
@@ -344,28 +326,28 @@ class App(BaseModel):
344
326
 
345
327
  return None
346
328
 
347
- @computed_field # type: ignore
348
- @property
349
- def config(self) -> t.Optional[str]:
350
- return self.config_name
351
-
352
- def _process_sdkconfig_files(self):
329
+ def _process_sdkconfig_files(self) -> t.Tuple[t.List[str], t.Optional[str]]:
353
330
  """
354
331
  Expand environment variables in default sdkconfig files and remove some CI related settings.
355
332
  """
356
- res = []
333
+ real_sdkconfig_files: t.List[str] = []
334
+ sdkconfig_files_defined_target: t.Optional[str] = None
357
335
 
336
+ # put the expanded variable files in a temporary directory
337
+ # will remove if the content is the same as the original one
358
338
  expanded_dir = os.path.join(self.work_dir, 'expanded_sdkconfig_files', os.path.basename(self.build_dir))
359
339
  if not os.path.isdir(expanded_dir):
360
340
  os.makedirs(expanded_dir)
361
341
 
362
- for f in self._sdkconfig_defaults + ([self.sdkconfig_path] if self.sdkconfig_path else []):
363
- if not os.path.isabs(f):
364
- f = os.path.join(self.work_dir, f)
365
-
342
+ for f in self.sdkconfig_defaults_candidates + ([self.sdkconfig_path] if self.sdkconfig_path else []):
343
+ # use filepath if abs/rel already point to itself
366
344
  if not os.path.isfile(f):
367
- self._logger.debug('sdkconfig file %s not exists, skipping...', f)
368
- continue
345
+ # find it in the app_dir
346
+ self._logger.debug('sdkconfig file %s not found, checking under app_dir...', f)
347
+ f = os.path.join(self.app_dir, f)
348
+ if not os.path.isfile(f):
349
+ self._logger.debug('sdkconfig file %s not found, skipping...', f)
350
+ continue
369
351
 
370
352
  expanded_fp = os.path.join(expanded_dir, os.path.basename(f))
371
353
  with open(f) as fr:
@@ -377,7 +359,7 @@ class App(BaseModel):
377
359
  if m:
378
360
  key = m.group(1)
379
361
  if key == 'CONFIG_IDF_TARGET':
380
- self._sdkconfig_files_defined_target = m.group(2)
362
+ sdkconfig_files_defined_target = m.group(2)
381
363
 
382
364
  if isinstance(self, CMakeApp):
383
365
  if key in self.SDKCONFIG_TEST_OPTS:
@@ -397,12 +379,16 @@ class App(BaseModel):
397
379
  os.unlink(expanded_fp)
398
380
  except OSError:
399
381
  self._logger.debug('Failed to remove file %s', expanded_fp)
400
- res.append(f)
382
+ real_sdkconfig_files.append(f)
401
383
  else:
402
384
  self._logger.debug('Expand sdkconfig file %s to %s', f, expanded_fp)
403
- res.append(expanded_fp)
385
+ real_sdkconfig_files.append(expanded_fp)
404
386
  # copy the related target-specific sdkconfig files
405
- for target_specific_file in Path(f).parent.glob(os.path.basename(f) + f'.{self.target}'):
387
+ par_dir = os.path.abspath(os.path.join(f, '..'))
388
+ for target_specific_file in (
389
+ os.path.join(par_dir, str(p))
390
+ for p in Path(par_dir).glob(os.path.basename(f) + f'.{self.target}')
391
+ ):
406
392
  self._logger.debug(
407
393
  'Copy target-specific sdkconfig file %s to %s', target_specific_file, expanded_dir
408
394
  )
@@ -419,24 +405,20 @@ class App(BaseModel):
419
405
  except OSError:
420
406
  pass
421
407
 
422
- if SESSION_ARGS.override_sdkconfig_items:
423
- res.append(SESSION_ARGS.override_sdkconfig_file_path)
408
+ if SESSION_ARGS.override_sdkconfig_file_path:
409
+ real_sdkconfig_files.append(SESSION_ARGS.override_sdkconfig_file_path)
424
410
  if 'CONFIG_IDF_TARGET' in SESSION_ARGS.override_sdkconfig_items:
425
- self._sdkconfig_files_defined_target = SESSION_ARGS.override_sdkconfig_items['CONFIG_IDF_TARGET']
411
+ sdkconfig_files_defined_target = SESSION_ARGS.override_sdkconfig_items['CONFIG_IDF_TARGET']
426
412
 
427
- self._sdkconfig_files = res
413
+ return real_sdkconfig_files, sdkconfig_files_defined_target
428
414
 
429
415
  @property
430
- @lru_cache()
431
- # @cached_property requires python 3.8
432
416
  def sdkconfig_files_defined_idf_target(self) -> t.Optional[str]:
433
417
  return self._sdkconfig_files_defined_target
434
418
 
435
419
  @property
436
- @lru_cache()
437
- # @cached_property requires python 3.8
438
420
  def sdkconfig_files(self) -> t.List[str]:
439
- return [os.path.realpath(file) for file in self._sdkconfig_files]
421
+ return [os.path.abspath(file) for file in self._sdkconfig_files]
440
422
 
441
423
  @property
442
424
  def depends_components(self) -> t.List[str]:
@@ -484,14 +466,7 @@ class App(BaseModel):
484
466
 
485
467
  return wrapper
486
468
 
487
- @record_build_duration # type: ignore
488
- def build(
489
- self,
490
- manifest_rootpath: t.Optional[str] = None,
491
- modified_components: t.Union[t.List[str], str, None] = None,
492
- modified_files: t.Union[t.List[str], str, None] = None,
493
- check_app_dependencies: bool = False,
494
- ) -> None:
469
+ def _pre_build(self) -> None:
495
470
  if self.dry_run:
496
471
  self._build_stage = BuildStage.DRY_RUN
497
472
  else:
@@ -533,26 +508,32 @@ class App(BaseModel):
533
508
  if not self.dry_run:
534
509
  os.unlink(sdkconfig_file)
535
510
 
536
- if self.build_log_path:
537
- self._logger.info('Writing build log to %s', self.build_log_path)
511
+ if os.path.isfile(self.build_log_path):
512
+ self._logger.debug('Removed existing build log file: %s', self.build_log_path)
513
+ if not self.dry_run:
514
+ os.unlink(self.build_log_path)
515
+ elif not self.dry_run:
516
+ os.makedirs(os.path.dirname(self.build_log_path), exist_ok=True)
517
+ self._logger.info('Writing build log to %s', self.build_log_path)
538
518
 
539
519
  if self.dry_run:
540
520
  self.build_status = BuildStatus.SKIPPED
541
521
  self.build_comment = 'dry run'
542
522
  return
543
523
 
544
- if self.build_log_path:
545
- logfile: t.IO[str] = open(self.build_log_path, 'w')
546
- keep_logfile = True
547
- else:
548
- # delete manually later, used for tracking debugging info
549
- logfile = tempfile.NamedTemporaryFile('w', delete=False)
550
- keep_logfile = False
524
+ @record_build_duration # type: ignore
525
+ def build(
526
+ self,
527
+ *,
528
+ manifest_rootpath: t.Optional[str] = None,
529
+ modified_components: t.Union[t.List[str], str, None] = None,
530
+ modified_files: t.Union[t.List[str], str, None] = None,
531
+ check_app_dependencies: bool = False,
532
+ ) -> None:
533
+ self._pre_build()
551
534
 
552
- self._build_stage = BuildStage.BUILD
553
535
  try:
554
536
  self._build(
555
- logfile=logfile,
556
537
  manifest_rootpath=manifest_rootpath,
557
538
  modified_components=to_list(modified_components),
558
539
  modified_files=to_list(modified_files),
@@ -561,12 +542,17 @@ class App(BaseModel):
561
542
  except BuildError as e:
562
543
  self.build_status = BuildStatus.FAILED
563
544
  self.build_comment = str(e)
564
- finally:
565
- logfile.close()
566
545
 
546
+ self._post_build()
547
+
548
+ def _post_build(self) -> None:
567
549
  self._build_stage = BuildStage.POST_BUILD
550
+
551
+ if not os.path.isfile(self.build_log_path):
552
+ return
553
+
568
554
  has_unignored_warning = False
569
- with open(logfile.name) as fr:
555
+ with open(self.build_log_path) as fr:
570
556
  lines = [line.rstrip() for line in fr.readlines() if line.rstrip()]
571
557
  for line in lines:
572
558
  is_error_or_warning, ignored = self.is_error_or_warning(line)
@@ -582,15 +568,14 @@ class App(BaseModel):
582
568
  self._logger.error(
583
569
  'Last %s lines from the build log "%s":',
584
570
  self.LOG_DEBUG_LINES,
585
- logfile.name,
571
+ self.build_log_path,
586
572
  )
587
573
  for line in lines[-self.LOG_DEBUG_LINES :]:
588
574
  self._logger.error('%s', line)
589
575
 
590
- # remove the log file if not specified and build succeeded
591
- if not keep_logfile and self.build_status == BuildStatus.SUCCESS:
592
- os.unlink(logfile.name)
593
- self._logger.debug('Removed temporary build log file: %s', logfile.name)
576
+ if self._is_build_log_path_temp and self.build_status == BuildStatus.SUCCESS:
577
+ os.unlink(self.build_log_path)
578
+ self._logger.debug('Removed success build temporary log file: %s', self.build_log_path)
594
579
 
595
580
  # Generate Size Files
596
581
  if self.build_status == BuildStatus.SUCCESS:
@@ -601,8 +586,7 @@ class App(BaseModel):
601
586
  exclude_list = []
602
587
  if self.size_json_path:
603
588
  exclude_list.append(os.path.basename(self.size_json_path))
604
- if self.build_log_path:
605
- exclude_list.append(os.path.basename(self.build_log_path))
589
+ exclude_list.append(os.path.basename(self.build_log_path))
606
590
 
607
591
  rmdir(
608
592
  self.build_path,
@@ -619,13 +603,13 @@ class App(BaseModel):
619
603
 
620
604
  def _build(
621
605
  self,
622
- logfile: t.IO[str],
606
+ *,
623
607
  manifest_rootpath: t.Optional[str] = None,
624
608
  modified_components: t.Optional[t.List[str]] = None,
625
609
  modified_files: t.Optional[t.List[str]] = None,
626
610
  check_app_dependencies: bool = False,
627
611
  ) -> None:
628
- pass
612
+ self._build_stage = BuildStage.BUILD
629
613
 
630
614
  def _write_size_json(self) -> None:
631
615
  if not self.size_json_path:
@@ -701,10 +685,10 @@ class App(BaseModel):
701
685
  if modified_files:
702
686
  for f in modified_files:
703
687
  _f_fullpath = to_absolute_path(f)
704
- if _f_fullpath.parts[-1].endswith('.md'):
688
+ if os.path.basename(_f_fullpath).endswith('.md'):
705
689
  continue
706
690
 
707
- if _app_dir_fullpath in _f_fullpath.parents:
691
+ if _f_fullpath.startswith(_app_dir_fullpath):
708
692
  return True
709
693
 
710
694
  return False
@@ -794,12 +778,19 @@ class MakeApp(App):
794
778
 
795
779
  def _build(
796
780
  self,
797
- logfile: t.IO[str],
781
+ *,
798
782
  manifest_rootpath: t.Optional[str] = None,
799
783
  modified_components: t.Optional[t.List[str]] = None,
800
784
  modified_files: t.Optional[t.List[str]] = None,
801
785
  check_app_dependencies: bool = False,
802
786
  ) -> None:
787
+ super()._build(
788
+ manifest_rootpath=manifest_rootpath,
789
+ modified_components=modified_components,
790
+ modified_files=modified_files,
791
+ check_app_dependencies=check_app_dependencies,
792
+ )
793
+
803
794
  # additional env variables
804
795
  additional_env_dict = {
805
796
  'IDF_TARGET': self.target,
@@ -816,8 +807,8 @@ class MakeApp(App):
816
807
  for cmd in commands:
817
808
  subprocess_run(
818
809
  cmd,
819
- log_terminal=False if self.build_log_path else True,
820
- log_fs=logfile,
810
+ log_terminal=self._is_build_log_path_temp,
811
+ log_fs=self.build_log_path,
821
812
  check=True,
822
813
  additional_env_dict=additional_env_dict,
823
814
  cwd=self.work_dir,
@@ -861,12 +852,19 @@ class CMakeApp(App):
861
852
 
862
853
  def _build(
863
854
  self,
864
- logfile: t.IO[str],
855
+ *,
865
856
  manifest_rootpath: t.Optional[str] = None,
866
857
  modified_components: t.Optional[t.List[str]] = None,
867
858
  modified_files: t.Optional[t.List[str]] = None,
868
859
  check_app_dependencies: bool = False,
869
860
  ) -> None:
861
+ super()._build(
862
+ manifest_rootpath=manifest_rootpath,
863
+ modified_components=modified_components,
864
+ modified_files=modified_files,
865
+ check_app_dependencies=check_app_dependencies,
866
+ )
867
+
870
868
  if not self._checked_should_build:
871
869
  self._check_should_build(
872
870
  manifest_rootpath=manifest_rootpath,
@@ -898,8 +896,8 @@ class CMakeApp(App):
898
896
  if modified_components is not None and check_app_dependencies and self.build_status == BuildStatus.UNKNOWN:
899
897
  subprocess_run(
900
898
  common_args + ['reconfigure'],
901
- log_terminal=False if self.build_log_path else True,
902
- log_fs=logfile,
899
+ log_terminal=self._is_build_log_path_temp,
900
+ log_fs=self.build_log_path,
903
901
  check=True,
904
902
  additional_env_dict=additional_env_dict,
905
903
  )
@@ -920,23 +918,22 @@ class CMakeApp(App):
920
918
  return
921
919
 
922
920
  # idf.py build
923
- build_args = deepcopy(common_args)
924
921
  if self.cmake_vars:
925
922
  for key, val in self.cmake_vars.items():
926
- build_args.append(f'-D{key}={val}')
923
+ common_args.append(f'-D{key}={val}')
927
924
  if 'TEST_EXCLUDE_COMPONENTS' in self.cmake_vars and 'TEST_COMPONENTS' not in self.cmake_vars:
928
- build_args.append('-DTESTS_ALL=1')
925
+ common_args.append('-DTESTS_ALL=1')
929
926
  if 'CONFIG_APP_BUILD_BOOTLOADER' in self.cmake_vars:
930
927
  # In case if secure_boot is enabled then for bootloader build need to add `bootloader` cmd
931
- build_args.append('bootloader')
932
- build_args.append('build')
928
+ common_args.append('bootloader')
929
+ common_args.append('build')
933
930
  if self.verbose:
934
- build_args.append('-v')
931
+ common_args.append('-v')
935
932
 
936
933
  subprocess_run(
937
- build_args,
938
- log_terminal=False if self.build_log_path else True,
939
- log_fs=logfile,
934
+ common_args,
935
+ log_terminal=self._is_build_log_path_temp,
936
+ log_fs=self.build_log_path,
940
937
  check=True,
941
938
  additional_env_dict=additional_env_dict,
942
939
  )
idf_build_apps/config.py CHANGED
@@ -39,10 +39,10 @@ def load_toml(filepath: t.Union[str, Path]) -> dict:
39
39
  raise InvalidTomlError(filepath, str(e))
40
40
 
41
41
 
42
- def _get_config_from_file(filepath: Path) -> t.Tuple[t.Optional[dict], Path]:
42
+ def _get_config_from_file(filepath: str) -> t.Tuple[t.Optional[dict], str]:
43
43
  config = None
44
- if filepath.is_file():
45
- if filepath.parts[-1] == PYPROJECT_TOML_FN:
44
+ if os.path.isfile(filepath):
45
+ if os.path.basename(filepath) == PYPROJECT_TOML_FN:
46
46
  tool = load_toml(filepath).get('tool', None)
47
47
  if tool:
48
48
  config = tool.get('idf-build-apps', None)
@@ -52,14 +52,14 @@ def _get_config_from_file(filepath: Path) -> t.Tuple[t.Optional[dict], Path]:
52
52
  return config, filepath
53
53
 
54
54
 
55
- def _get_config_from_path(dirpath: Path) -> t.Tuple[t.Optional[dict], Path]:
55
+ def _get_config_from_path(dirpath: str) -> t.Tuple[t.Optional[dict], str]:
56
56
  config = None
57
57
  filepath = dirpath
58
- if (dirpath / PYPROJECT_TOML_FN).is_file():
59
- config, filepath = _get_config_from_file(dirpath / PYPROJECT_TOML_FN)
58
+ if os.path.isfile(os.path.join(dirpath, PYPROJECT_TOML_FN)):
59
+ config, filepath = _get_config_from_file(os.path.join(dirpath, PYPROJECT_TOML_FN))
60
60
 
61
- if config is None and (dirpath / IDF_BUILD_APPS_TOML_FN).is_file():
62
- config, filepath = _get_config_from_file(dirpath / IDF_BUILD_APPS_TOML_FN)
61
+ if config is None and os.path.isfile(os.path.join(dirpath, IDF_BUILD_APPS_TOML_FN)):
62
+ config, filepath = _get_config_from_file(os.path.join(dirpath, IDF_BUILD_APPS_TOML_FN))
63
63
 
64
64
  return config, filepath
65
65
 
@@ -83,9 +83,9 @@ def get_valid_config(starts_from: str = os.getcwd(), custom_path: t.Optional[str
83
83
  print(f'Using config file: {filepath}')
84
84
  return config
85
85
 
86
- if (cur_dir / '.git').exists():
86
+ if os.path.exists(os.path.join(cur_dir, '.git')):
87
87
  break
88
88
 
89
- cur_dir = cur_dir.parent
89
+ cur_dir = os.path.abspath(os.path.join(cur_dir, '..'))
90
90
 
91
91
  return None
@@ -9,9 +9,6 @@ import re
9
9
  import sys
10
10
  import tempfile
11
11
  import typing as t
12
- from pathlib import (
13
- Path,
14
- )
15
12
 
16
13
  from .utils import (
17
14
  to_version,
@@ -27,19 +24,19 @@ if _BUILDING_DOCS:
27
24
  if _BUILDING_DOCS:
28
25
  _idf_env = tempfile.gettempdir()
29
26
  else:
30
- _idf_env = os.getenv('IDF_PATH', '')
31
- if not os.path.isdir(_idf_env):
32
- raise ValueError(f'Invalid value for IDF_PATH: {_idf_env}')
27
+ _idf_env = os.getenv('IDF_PATH') or ''
28
+ if not _idf_env:
29
+ raise SystemExit('environment variable IDF_PATH must be set')
33
30
 
34
31
 
35
- IDF_PATH = Path(_idf_env).resolve()
36
- IDF_PY = IDF_PATH / 'tools' / 'idf.py'
37
- IDF_SIZE_PY = IDF_PATH / 'tools' / 'idf_size.py'
32
+ IDF_PATH = os.path.abspath(_idf_env)
33
+ IDF_PY = os.path.join(IDF_PATH, 'tools', 'idf.py')
34
+ IDF_SIZE_PY = os.path.join(IDF_PATH, 'tools', 'idf_size.py')
38
35
  PROJECT_DESCRIPTION_JSON = 'project_description.json'
39
36
  DEFAULT_SDKCONFIG = 'sdkconfig.defaults'
40
37
 
41
38
 
42
- sys.path.append(str(IDF_PATH / 'tools' / 'idf_py_actions'))
39
+ sys.path.append(os.path.join(IDF_PATH, 'tools', 'idf_py_actions'))
43
40
  if _BUILDING_DOCS:
44
41
  _idf_py_constant_py = object()
45
42
  else:
@@ -54,7 +51,7 @@ ALL_TARGETS = SUPPORTED_TARGETS + PREVIEW_TARGETS
54
51
 
55
52
 
56
53
  def _idf_version_from_cmake() -> t.Tuple[int, int, int]:
57
- version_path = str(IDF_PATH / 'tools' / 'cmake' / 'version.cmake')
54
+ version_path = os.path.join(IDF_PATH, 'tools', 'cmake', 'version.cmake')
58
55
  if not os.path.isfile(version_path):
59
56
  raise ValueError(f'File {version_path} does not exist')
60
57
 
idf_build_apps/finder.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import logging
5
5
  import os
6
+ import os.path
6
7
  import re
7
8
  import typing as t
8
9
  from pathlib import (
@@ -80,7 +81,7 @@ def _get_apps_from_path(
80
81
  default_config_name = rule.config_name
81
82
  continue
82
83
 
83
- sdkconfig_paths = sorted([str(p.relative_to(path)) for p in Path(path).glob(rule.file_name)])
84
+ sdkconfig_paths = sorted([str(p.resolve()) for p in Path(path).glob(rule.file_name)])
84
85
 
85
86
  if sdkconfig_paths:
86
87
  sdkconfig_paths_matched = True # skip the next block for no wildcard config rules
@@ -178,7 +179,7 @@ def _find_apps(
178
179
  del dirs[:]
179
180
  continue
180
181
 
181
- if root_path.parts[-1] == 'managed_components': # idf-component-manager
182
+ if os.path.basename(root_path) == 'managed_components': # idf-component-manager
182
183
  LOGGER.debug('=> Skipping %s (managed components)', root_path)
183
184
  del dirs[:]
184
185
  continue
idf_build_apps/main.py CHANGED
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import argparse
@@ -10,8 +10,10 @@ import shutil
10
10
  import sys
11
11
  import textwrap
12
12
  import typing as t
13
- from pathlib import (
14
- Path,
13
+
14
+ from pydantic import (
15
+ Field,
16
+ create_model,
15
17
  )
16
18
 
17
19
  from . import (
@@ -19,6 +21,7 @@ from . import (
19
21
  )
20
22
  from .app import (
21
23
  App,
24
+ AppDeserializer,
22
25
  CMakeApp,
23
26
  MakeApp,
24
27
  )
@@ -181,6 +184,7 @@ def find_apps(
181
184
 
182
185
  for target in targets:
183
186
  for path in to_list(paths):
187
+ path = path.strip()
184
188
  apps.extend(
185
189
  _find_apps(
186
190
  path,
@@ -295,7 +299,6 @@ def build_apps(
295
299
  if f and os.path.isfile(f):
296
300
  os.remove(f)
297
301
  LOGGER.debug('Remove existing collect file %s', f)
298
- Path(f).touch()
299
302
 
300
303
  exit_code = 0
301
304
  for i, app in enumerate(apps):
@@ -428,8 +431,9 @@ def get_parser() -> argparse.ArgumentParser:
428
431
  '- @w: would be replaced by the wildcard, usually the sdkconfig\n'
429
432
  '- @n: would be replaced by the app name\n'
430
433
  '- @f: would be replaced by the escaped app path (replaced "/" to "_")\n'
431
- '- @i: would be replaced by the build index\n'
432
- '- @p: would be replaced by the parallel index',
434
+ '- @v: Would be replaced by the ESP-IDF version like `5_3_0`\n'
435
+ '- @i: would be replaced by the build index (only available in `build` command)\n'
436
+ '- @p: would be replaced by the parallel index (only available in `build` command)',
433
437
  formatter_class=argparse.RawDescriptionHelpFormatter,
434
438
  )
435
439
  actions = parser.add_subparsers(dest='action')
@@ -743,7 +747,7 @@ def main():
743
747
  os.makedirs(os.path.dirname(os.path.realpath(args.output)), exist_ok=True)
744
748
  with open(args.output, 'w') as fw:
745
749
  for app in apps:
746
- fw.write(app.model_dump_json() + '\n')
750
+ fw.write(app.to_json() + '\n')
747
751
  else:
748
752
  for app in apps:
749
753
  print(app)
@@ -792,3 +796,37 @@ def main():
792
796
  print(f' {app}')
793
797
 
794
798
  sys.exit(res)
799
+
800
+
801
+ def json_to_app(json_str: str, extra_classes: t.Optional[t.List[t.Type[App]]] = None) -> App:
802
+ """
803
+ Deserialize json string to App object
804
+
805
+ .. note::
806
+
807
+ You can pass extra_cls to support custom App class. A custom App class must be a subclass of App, and have a
808
+ different value of `build_system`. For example, a custom CMake app
809
+
810
+ >>> class CustomApp(CMakeApp):
811
+ >>> build_system: Literal['custom_cmake'] = 'custom_cmake'
812
+
813
+ Then you can pass the CustomApp class to the `extra_cls` argument
814
+
815
+ >>> json_str = CustomApp('.', 'esp32').to_json()
816
+ >>> json_to_app(json_str, extra_classes=[CustomApp])
817
+
818
+ :param json_str: json string
819
+ :param extra_classes: extra App class
820
+ :return: App object
821
+ """
822
+ types = [App, CMakeApp, MakeApp]
823
+ if extra_classes:
824
+ types.extend(extra_classes)
825
+
826
+ custom_deserializer = create_model(
827
+ '_CustomDeserializer',
828
+ app=(t.Union[tuple(types)], Field(discriminator='build_system')),
829
+ __base__=AppDeserializer,
830
+ )
831
+
832
+ return custom_deserializer.from_json(json_str)
@@ -5,9 +5,6 @@ import logging
5
5
  import os
6
6
  import typing as t
7
7
  import warnings
8
- from pathlib import (
9
- Path,
10
- )
11
8
 
12
9
  import yaml
13
10
  from pyparsing import (
@@ -52,24 +49,26 @@ class FolderRule:
52
49
 
53
50
  def __init__(
54
51
  self,
55
- folder: Path,
52
+ folder: str,
56
53
  enable: t.Optional[t.List[t.Dict[str, t.Any]]] = None,
57
54
  disable: t.Optional[t.List[t.Dict[str, t.Any]]] = None,
58
55
  disable_test: t.Optional[t.List[t.Dict[str, t.Any]]] = None,
59
56
  depends_components: t.Optional[t.List[str]] = None,
60
57
  depends_filepatterns: t.Optional[t.List[str]] = None,
61
58
  ) -> None:
62
- self.folder = folder.resolve()
63
-
64
- for group in [enable, disable, disable_test]:
65
- if group:
66
- for d in group:
67
- d['stmt'] = d['if'] # avoid keyword `if`
68
- del d['if']
69
-
70
- self.enable = [IfClause(**clause) for clause in enable] if enable else []
71
- self.disable = [IfClause(**clause) for clause in disable] if disable else []
72
- self.disable_test = [IfClause(**clause) for clause in disable_test] if disable_test else []
59
+ self.folder = os.path.abspath(folder)
60
+
61
+ def _clause_to_if_clause(clause: t.Dict[str, t.Any]) -> IfClause:
62
+ _kwargs = {'stmt': clause['if']}
63
+ if 'temporary' in clause:
64
+ _kwargs['temporary'] = clause['temporary']
65
+ if 'reason' in clause:
66
+ _kwargs['reason'] = clause['reason']
67
+ return IfClause(**_kwargs)
68
+
69
+ self.enable = [_clause_to_if_clause(clause) for clause in enable] if enable else []
70
+ self.disable = [_clause_to_if_clause(clause) for clause in disable] if disable else []
71
+ self.disable_test = [_clause_to_if_clause(clause) for clause in disable_test] if disable_test else []
73
72
  self.depends_components = depends_components or []
74
73
  self.depends_filepatterns = depends_filepatterns or []
75
74
 
@@ -148,13 +147,13 @@ class FolderRule:
148
147
 
149
148
 
150
149
  class DefaultRule(FolderRule):
151
- def __init__(self, folder: Path) -> None:
150
+ def __init__(self, folder: str) -> None:
152
151
  super().__init__(folder)
153
152
 
154
153
 
155
154
  class Manifest:
156
155
  # could be reassigned later
157
- ROOTPATH = Path(os.curdir)
156
+ ROOTPATH = os.curdir
158
157
  CHECK_MANIFEST_RULES = False
159
158
 
160
159
  def __init__(
@@ -174,12 +173,10 @@ class Manifest:
174
173
  if folder.startswith('.'):
175
174
  continue
176
175
 
177
- if os.path.isabs(folder):
178
- folder = Path(folder)
179
- else:
180
- folder = Path(cls.ROOTPATH, folder)
176
+ if not os.path.isabs(folder):
177
+ folder = os.path.join(cls.ROOTPATH, folder)
181
178
 
182
- if not folder.exists():
179
+ if not os.path.exists(folder):
183
180
  msg = f'Folder "{folder}" does not exist. Please check your manifest file {path}'
184
181
  if cls.CHECK_MANIFEST_RULES:
185
182
  raise InvalidManifest(msg)
@@ -194,9 +191,9 @@ class Manifest:
194
191
  return Manifest(rules)
195
192
 
196
193
  def _most_suitable_rule(self, _folder: str) -> FolderRule:
197
- folder = Path(_folder).resolve()
194
+ folder = os.path.abspath(_folder)
198
195
  for rule in self.rules[::-1]:
199
- if rule.folder == folder or rule.folder in folder.parents:
196
+ if rule.folder == folder or folder.startswith(rule.folder):
200
197
  return rule
201
198
 
202
199
  return DefaultRule(folder)
@@ -1,7 +1,7 @@
1
1
  # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
-
4
3
  import logging
4
+ import os.path
5
5
  import typing as t
6
6
  from pathlib import (
7
7
  Path,
@@ -50,10 +50,10 @@ _value = Optional('(').suppress() + MatchFirst([_hex_value, _str_value, _int_val
50
50
  _define_expr = '#define' + Optional(_name)('name') + Optional(_value)
51
51
 
52
52
 
53
- def get_defines(header_path: Path) -> t.List[str]:
53
+ def get_defines(header_path: str) -> t.List[str]:
54
54
  defines = []
55
55
  LOGGER.debug('Reading macros from %s...', header_path)
56
- with open(str(header_path)) as f:
56
+ with open(header_path) as f:
57
57
  output = f.read()
58
58
 
59
59
  for line in output.split('\n'):
@@ -82,10 +82,10 @@ class SocHeader(dict):
82
82
  super().__init__(**soc_header_dict)
83
83
 
84
84
  @staticmethod
85
- def _get_dir_from_candidates(candidates: t.List[Path]) -> t.Optional[Path]:
85
+ def _get_dir_from_candidates(candidates: t.List[str]) -> t.Optional[str]:
86
86
  for d in candidates:
87
- if not d.is_dir():
88
- LOGGER.debug('folder "%s" not found. Skipping...', d.absolute())
87
+ if not os.path.isdir(d):
88
+ LOGGER.debug('folder "%s" not found. Skipping...', os.path.abspath(d))
89
89
  else:
90
90
  return d
91
91
 
@@ -96,22 +96,22 @@ class SocHeader(dict):
96
96
  soc_headers_dir = cls._get_dir_from_candidates(
97
97
  [
98
98
  # other branches
99
- IDF_PATH / 'components' / 'soc' / target / 'include' / 'soc',
99
+ os.path.abspath(os.path.join(IDF_PATH, 'components', 'soc', target, 'include', 'soc')),
100
100
  # release/v4.2
101
- IDF_PATH / 'components' / 'soc' / 'soc' / target / 'include' / 'soc',
101
+ os.path.abspath(os.path.join(IDF_PATH, 'components', 'soc', 'soc', target, 'include', 'soc')),
102
102
  ]
103
103
  )
104
104
  esp_rom_headers_dir = cls._get_dir_from_candidates(
105
105
  [
106
- IDF_PATH / 'components' / 'esp_rom' / target,
106
+ os.path.join(IDF_PATH, 'components', 'esp_rom', target),
107
107
  ]
108
108
  )
109
109
 
110
- header_files = []
110
+ header_files: t.List[str] = []
111
111
  if soc_headers_dir:
112
- header_files += list(soc_headers_dir.glob(cls.CAPS_HEADER_FILEPATTERN))
112
+ header_files += [str(p.resolve()) for p in Path(soc_headers_dir).glob(cls.CAPS_HEADER_FILEPATTERN)]
113
113
  if esp_rom_headers_dir:
114
- header_files += list(esp_rom_headers_dir.glob(cls.CAPS_HEADER_FILEPATTERN))
114
+ header_files += [str(p.resolve()) for p in Path(esp_rom_headers_dir).glob(cls.CAPS_HEADER_FILEPATTERN)]
115
115
 
116
116
  output_dict = {}
117
117
  for f in header_files:
@@ -1,10 +1,13 @@
1
1
  # SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import logging
4
5
  import os
5
6
  import re
6
7
  import typing as t
7
8
 
9
+ LOGGER = logging.getLogger(__name__)
10
+
8
11
 
9
12
  class SessionArgs:
10
13
  workdir: str = os.getcwd()
@@ -35,27 +38,32 @@ class SessionArgs:
35
38
  self.override_sdkconfig_file_path = override_sdkconfig_merged_file
36
39
 
37
40
  def _get_override_sdkconfig_files_items(self, override_sdkconfig_files: t.Tuple[str]) -> t.Dict:
38
- dct = {}
41
+ d = {}
39
42
  for f in override_sdkconfig_files:
40
- if not os.path.isabs(f):
41
- f = os.path.join(self.workdir, f)
43
+ # use filepath if abs/rel already point to itself
42
44
  if not os.path.isfile(f):
43
- continue
45
+ # find it in the workdir
46
+ LOGGER.debug('override sdkconfig file %s not found, checking under app_dir...', f)
47
+ f = os.path.join(self.workdir, f)
48
+ if not os.path.isfile(f):
49
+ LOGGER.debug('override sdkconfig file %s not found, skipping...', f)
50
+ continue
51
+
44
52
  with open(f) as fr:
45
53
  for line in fr:
46
54
  m = re.compile(r"^([^=]+)=\"?([^\"\n]*)\"?\n*$").match(line)
47
55
  if not m:
48
56
  continue
49
- dct[m.group(1)] = m.group(2)
50
- return dct
57
+ d[m.group(1)] = m.group(2)
58
+ return d
51
59
 
52
60
  def _get_override_sdkconfig_items(self, override_sdkconfig_items: t.Tuple[str]) -> t.Dict:
53
- dct = {}
61
+ d = {}
54
62
  for line in override_sdkconfig_items:
55
63
  m = re.compile(r"^([^=]+)=\"?([^\"\n]*)\"?\n*$").match(line)
56
64
  if m:
57
- dct[m.group(1)] = m.group(2)
58
- return dct
65
+ d[m.group(1)] = m.group(2)
66
+ return d
59
67
 
60
68
  def _create_override_sdkconfig_merged_file(self, override_sdkconfig_merged_items) -> t.Optional[str]:
61
69
  if not override_sdkconfig_merged_items:
idf_build_apps/utils.py CHANGED
@@ -1,7 +1,8 @@
1
- # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import fnmatch
5
+ import functools
5
6
  import glob
6
7
  import logging
7
8
  import os
@@ -12,9 +13,6 @@ import typing as t
12
13
  from copy import (
13
14
  deepcopy,
14
15
  )
15
- from pathlib import (
16
- Path,
17
- )
18
16
 
19
17
  from packaging.version import (
20
18
  Version,
@@ -137,7 +135,7 @@ def find_first_match(pattern: str, path: str) -> t.Optional[str]:
137
135
  def subprocess_run(
138
136
  cmd: t.List[str],
139
137
  log_terminal: bool = True,
140
- log_fs: t.Optional[t.IO[str]] = None,
138
+ log_fs: t.Union[t.IO[str], str, None] = None,
141
139
  check: bool = False,
142
140
  additional_env_dict: t.Optional[t.Dict[str, str]] = None,
143
141
  **kwargs,
@@ -160,16 +158,26 @@ def subprocess_run(
160
158
  subprocess_env.update(additional_env_dict)
161
159
 
162
160
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=subprocess_env, **kwargs)
163
- if p.stdout:
164
- for line in p.stdout:
165
- if isinstance(line, bytes):
166
- line = line.decode('utf-8')
167
161
 
168
- if log_terminal:
169
- sys.stdout.write(line)
162
+ def _log_stdout(fs: t.Optional[t.IO[str]] = None):
163
+ if p.stdout:
164
+ for line in p.stdout:
165
+ if isinstance(line, bytes):
166
+ line = line.decode('utf-8')
170
167
 
171
- if log_fs:
172
- log_fs.write(line)
168
+ if log_terminal:
169
+ sys.stdout.write(line)
170
+
171
+ if fs:
172
+ fs.write(line)
173
+
174
+ if p.stdout:
175
+ if log_fs:
176
+ if isinstance(log_fs, str):
177
+ with open(log_fs, 'a') as fa:
178
+ _log_stdout(fa)
179
+ else:
180
+ _log_stdout(log_fs)
173
181
 
174
182
  returncode = p.wait()
175
183
  if check and returncode != 0:
@@ -270,14 +278,14 @@ def semicolon_separated_str_to_list(s: t.Optional[str]) -> t.Optional[t.List[str
270
278
  return [p.strip() for p in s.strip().split(';') if p.strip()]
271
279
 
272
280
 
273
- def to_absolute_path(s: str, rootpath: t.Optional[str] = None) -> Path:
274
- rp = Path(os.path.expanduser(rootpath or '.')).resolve()
281
+ def to_absolute_path(s: str, rootpath: t.Optional[str] = None) -> str:
282
+ rp = os.path.abspath(os.path.expanduser(rootpath or '.'))
275
283
 
276
- sp = Path(os.path.expanduser(s))
277
- if sp.is_absolute():
278
- return sp.resolve()
284
+ sp = os.path.expanduser(s)
285
+ if os.path.isabs(sp):
286
+ return sp
279
287
  else:
280
- return (rp / sp).resolve()
288
+ return os.path.abspath(os.path.join(rp, sp))
281
289
 
282
290
 
283
291
  def to_version(s: t.Any) -> Version:
@@ -308,31 +316,49 @@ def files_matches_patterns(
308
316
  return False
309
317
 
310
318
 
319
+ @functools.total_ordering
311
320
  class BaseModel(_BaseModel):
312
321
  """
313
322
  BaseModel that is hashable
314
323
  """
315
324
 
325
+ __EQ_IGNORE_FIELDS__: t.List[str] = []
326
+
316
327
  def __lt__(self, other: t.Any) -> bool:
317
328
  if isinstance(other, self.__class__):
318
329
  for k in self.model_dump():
319
- if getattr(self, k) != getattr(other, k):
320
- return getattr(self, k) < getattr(other, k)
321
- else:
330
+ if k in self.__EQ_IGNORE_FIELDS__:
322
331
  continue
323
332
 
333
+ self_attr = getattr(self, k, '') or ''
334
+ other_attr = getattr(other, k, '') or ''
335
+
336
+ if self_attr != other_attr:
337
+ return self_attr < other_attr
338
+
339
+ continue
340
+
341
+ return False
342
+
324
343
  return NotImplemented
325
344
 
326
345
  def __eq__(self, other: t.Any) -> bool:
327
346
  if isinstance(other, self.__class__):
328
347
  # we only care the public attributes
329
- return self.__dict__ == other.__dict__
348
+ self_model_dump = self.model_dump()
349
+ other_model_dump = other.model_dump()
350
+
351
+ for _field in self.__EQ_IGNORE_FIELDS__:
352
+ self_model_dump.pop(_field, None)
353
+ other_model_dump.pop(_field, None)
354
+
355
+ return self_model_dump == other_model_dump
330
356
 
331
357
  return NotImplemented
332
358
 
333
359
  def __hash__(self) -> int:
334
360
  hash_list = []
335
- for v in self.__dict__.values():
361
+ for v in self.model_dump().values():
336
362
  if isinstance(v, list):
337
363
  hash_list.append(tuple(v))
338
364
  elif isinstance(v, dict):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: idf-build-apps
3
- Version: 2.0.0b5
3
+ Version: 2.0.0rc1
4
4
  Summary: Tools for building ESP-IDF related apps.
5
5
  Author-email: Fu Hanxi <fuhanxi@espressif.com>
6
6
  Requires-Python: >=3.7
@@ -0,0 +1,23 @@
1
+ idf_build_apps/__init__.py,sha256=WAwRTUCnGjoWIT8bl_n9qCOk7xmA8JdvjZfnYWNs1Qs,653
2
+ idf_build_apps/__main__.py,sha256=8E-5xHm2MlRun0L88XJleNh5U50dpE0Q1nK5KqomA7I,182
3
+ idf_build_apps/app.py,sha256=KwZ7xrxUs_cVqEaDxtrPE7B4bo-_-RCJAEUX3iHR9A4,34537
4
+ idf_build_apps/build_apps_args.py,sha256=r6VCJDdCzE873X8OTputYkCBZPgECaKoNlAejfcamJk,1644
5
+ idf_build_apps/config.py,sha256=I75uOQGarCWVKGi16ZYpo0qTVU25BUP4eh6-RWCtbvw,2924
6
+ idf_build_apps/constants.py,sha256=xqclwUpWE5dEByL4kxdg2HaHjbAfkJtxodFfLZuAk8A,2818
7
+ idf_build_apps/finder.py,sha256=qw5moNq7U5mHSsR0CCfGkKE9p4QsWYNcfkxzeQ73HgM,6252
8
+ idf_build_apps/log.py,sha256=4P4q8EqV68iQw0LCoNI6ibBBpAkNgetDxZ0Cgbhp0Bo,2570
9
+ idf_build_apps/main.py,sha256=jh2mZFK75OpjEeWzupNk-ZQFCIXFfyoL6-XyKi2hbnk,32896
10
+ idf_build_apps/session_args.py,sha256=LYgibgUGjHm0iAd6gBYcT9rpTCcdKfcblFMysAlv7UE,2996
11
+ idf_build_apps/utils.py,sha256=-o0yyYHAhwVD2ihRupUJdKEu0sAIOaGg4DnIRy6iJNA,9669
12
+ idf_build_apps/junit/__init__.py,sha256=GRyhJfZet00iWxe2PvUAG72CXhnhOZaV4B7Z7txKVAk,226
13
+ idf_build_apps/junit/report.py,sha256=2sBLEL5EnGW6h0ku2W-BOyEFMgQcu0OO-WcFyXH_Aqg,6341
14
+ idf_build_apps/junit/utils.py,sha256=gtibRs8WTE8IXTIAS73QR_k_jrJlOjCl2y-9KiP5_Nk,1304
15
+ idf_build_apps/manifest/__init__.py,sha256=Q2-cb3ngNjnl6_zWhUfzZZB10f_-Rv2JYNck3Lk7UkQ,133
16
+ idf_build_apps/manifest/if_parser.py,sha256=hS3khcDeFsoqfDLdRG931h91SU4bTALACxjcEiiDIBQ,6379
17
+ idf_build_apps/manifest/manifest.py,sha256=oRy3BlR_JXfQ7avJQnAZ8__ZCcuhGM_wnFDqHYCqkIQ,7316
18
+ idf_build_apps/manifest/soc_header.py,sha256=TVT7wxzcxdeIdX8WAtPYr8bFV7EqTWkVo4h18MWsD8Q,4028
19
+ idf_build_apps-2.0.0rc1.dist-info/entry_points.txt,sha256=3pVUirUEsb6jsDRikkQWNUt4hqLK2ci1HvW_Vf8b6uE,59
20
+ idf_build_apps-2.0.0rc1.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
21
+ idf_build_apps-2.0.0rc1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
22
+ idf_build_apps-2.0.0rc1.dist-info/METADATA,sha256=EU1KK7_Meebf2ZdJ6mpyB2hS_VJj4HLjXVWri1q0RwU,4453
23
+ idf_build_apps-2.0.0rc1.dist-info/RECORD,,
@@ -1,23 +0,0 @@
1
- idf_build_apps/__init__.py,sha256=BcyNvVaRq_d-XR_wSvY4kaO2XLD4sy2OajweqWTc7F8,636
2
- idf_build_apps/__main__.py,sha256=8E-5xHm2MlRun0L88XJleNh5U50dpE0Q1nK5KqomA7I,182
3
- idf_build_apps/app.py,sha256=uDo_UInS4ogLf_R44N9ZbG_-NMlofUD1rvHe0ohjd4E,34102
4
- idf_build_apps/build_apps_args.py,sha256=r6VCJDdCzE873X8OTputYkCBZPgECaKoNlAejfcamJk,1644
5
- idf_build_apps/config.py,sha256=zRmFgka3S3HKxPeJXj5ebj6I0DN4U2UNQnNFAD-u_2Y,2806
6
- idf_build_apps/constants.py,sha256=CweC0HwaACuHkrjY2zaPPrjM9rgnacesuu-FGw0SOqk,2817
7
- idf_build_apps/finder.py,sha256=0qJ0jQdTJH9HwFe9vrt_fbI8RjZmDcqzS7WMI5G_k1o,6237
8
- idf_build_apps/log.py,sha256=4P4q8EqV68iQw0LCoNI6ibBBpAkNgetDxZ0Cgbhp0Bo,2570
9
- idf_build_apps/main.py,sha256=VQjsoKvT8GPXVI4S-xByWyea6JWSHjblxoJt_CgaUnc,31615
10
- idf_build_apps/session_args.py,sha256=UOPqYPlqQBd2p0pBDkgypjqnpwFGF1YYndOoWGpBjLI,2654
11
- idf_build_apps/utils.py,sha256=V-8Wfk3Wg7ML-xpfV_aQc9HsFbpSHYLL-EN39UvUnNg,8883
12
- idf_build_apps/junit/__init__.py,sha256=GRyhJfZet00iWxe2PvUAG72CXhnhOZaV4B7Z7txKVAk,226
13
- idf_build_apps/junit/report.py,sha256=2sBLEL5EnGW6h0ku2W-BOyEFMgQcu0OO-WcFyXH_Aqg,6341
14
- idf_build_apps/junit/utils.py,sha256=gtibRs8WTE8IXTIAS73QR_k_jrJlOjCl2y-9KiP5_Nk,1304
15
- idf_build_apps/manifest/__init__.py,sha256=Q2-cb3ngNjnl6_zWhUfzZZB10f_-Rv2JYNck3Lk7UkQ,133
16
- idf_build_apps/manifest/if_parser.py,sha256=hS3khcDeFsoqfDLdRG931h91SU4bTALACxjcEiiDIBQ,6379
17
- idf_build_apps/manifest/manifest.py,sha256=X0bgE0QokqBuH9zeJo1oEzJAC4ebKhTcvMTF3RDbHm4,7215
18
- idf_build_apps/manifest/soc_header.py,sha256=MAEOUQgP6VJERbfFtVF9lnkbDAZbEkzGduHCA_eH7Pc,3879
19
- idf_build_apps-2.0.0b5.dist-info/entry_points.txt,sha256=3pVUirUEsb6jsDRikkQWNUt4hqLK2ci1HvW_Vf8b6uE,59
20
- idf_build_apps-2.0.0b5.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
21
- idf_build_apps-2.0.0b5.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
22
- idf_build_apps-2.0.0b5.dist-info/METADATA,sha256=ZBOLysnM320fDM9DkYtllmIc9PLMqjAILyFm5e6Yqe4,4452
23
- idf_build_apps-2.0.0b5.dist-info/RECORD,,