idf-build-apps 2.6.3__tar.gz → 2.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.github/workflows/test-build-docs.yml +1 -1
  2. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.pre-commit-config.yaml +1 -0
  3. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/CHANGELOG.md +12 -0
  4. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/PKG-INFO +2 -1
  5. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/__init__.py +1 -1
  6. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/app.py +32 -54
  7. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/constants.py +0 -11
  8. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/junit/report.py +2 -2
  9. idf_build_apps-2.7.0/idf_build_apps/log.py +119 -0
  10. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/main.py +23 -9
  11. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/pyproject.toml +4 -2
  12. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/setup.py +3 -2
  13. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/test_build.py +17 -1
  14. idf_build_apps-2.6.3/idf_build_apps/log.py +0 -88
  15. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.editorconfig +0 -0
  16. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.git-blame-ignore-revs +0 -0
  17. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.gitattributes +0 -0
  18. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.github/dependabot.yml +0 -0
  19. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.github/workflows/check-pre-commit.yml +0 -0
  20. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.github/workflows/publish-pypi.yml +0 -0
  21. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.github/workflows/sync-jira.yml +0 -0
  22. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.github/workflows/test-build-idf-apps.yml +0 -0
  23. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.gitignore +0 -0
  24. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/.readthedocs.yml +0 -0
  25. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/CONTRIBUTING.md +0 -0
  26. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/LICENSE +0 -0
  27. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/README.md +0 -0
  28. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/_apidoc_templates/module.rst_t +0 -0
  29. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/_apidoc_templates/package.rst_t +0 -0
  30. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/_apidoc_templates/toc.rst_t +0 -0
  31. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/_static/espressif-logo.svg +0 -0
  32. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/_static/theme_overrides.css +0 -0
  33. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/_templates/layout.html +0 -0
  34. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/conf_common.py +0 -0
  35. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/Makefile +0 -0
  36. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/conf.py +0 -0
  37. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/explanations/build.rst +0 -0
  38. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/explanations/config_rules.rst +0 -0
  39. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/explanations/dependency_driven_build.rst +0 -0
  40. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/explanations/find.rst +0 -0
  41. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/guides/1.x_to_2.x.md +0 -0
  42. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/index.rst +0 -0
  43. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/others/CHANGELOG.md +0 -0
  44. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/others/CONTRIBUTING.md +0 -0
  45. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/references/cli.rst +0 -0
  46. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/references/config_file.rst +0 -0
  47. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/docs/en/references/manifest.rst +0 -0
  48. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/__main__.py +0 -0
  49. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/args.py +0 -0
  50. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/autocompletions.py +0 -0
  51. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/finder.py +0 -0
  52. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/junit/__init__.py +0 -0
  53. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/junit/utils.py +0 -0
  54. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/manifest/__init__.py +0 -0
  55. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/manifest/manifest.py +0 -0
  56. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/manifest/soc_header.py +0 -0
  57. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/py.typed +0 -0
  58. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/session_args.py +0 -0
  59. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/utils.py +0 -0
  60. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/vendors/__init__.py +0 -0
  61. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/vendors/pydantic_sources.py +0 -0
  62. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/yaml/__init__.py +0 -0
  63. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/idf_build_apps/yaml/parser.py +0 -0
  64. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/license_header.txt +0 -0
  65. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/conftest.py +0 -0
  66. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/test_app.py +0 -0
  67. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/test_args.py +0 -0
  68. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/test_cmd.py +0 -0
  69. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/test_finder.py +0 -0
  70. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/test_manifest.py +0 -0
  71. {idf_build_apps-2.6.3 → idf_build_apps-2.7.0}/tests/test_utils.py +0 -0
@@ -22,4 +22,4 @@ jobs:
22
22
  cd docs
23
23
  pushd en && make html && popd
24
24
  - name: markdown-link-check
25
- uses: gaurav-nelson/github-action-markdown-link-check@1.0.15
25
+ uses: gaurav-nelson/github-action-markdown-link-check@1.0.16
@@ -38,6 +38,7 @@ repos:
38
38
  - pytest
39
39
  - argcomplete>=3
40
40
  - esp-bool-parser>=0.1.2,<1
41
+ - rich
41
42
  - repo: https://github.com/hfudev/rstfmt
42
43
  rev: v0.1.4
43
44
  hooks:
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## v2.7.0 (2025-02-18)
6
+
7
+ ### Feat
8
+
9
+ - improve debug info with rich
10
+
11
+ ## v2.6.4 (2025-02-14)
12
+
13
+ ### Fix
14
+
15
+ - collect file not created when no apps built
16
+
5
17
  ## v2.6.3 (2025-02-11)
6
18
 
7
19
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: idf-build-apps
3
- Version: 2.6.3
3
+ Version: 2.7.0
4
4
  Summary: Tools for building ESP-IDF related apps.
5
5
  Author-email: Fu Hanxi <fuhanxi@espressif.com>
6
6
  Requires-Python: >=3.7
@@ -22,6 +22,7 @@ Requires-Dist: pydantic_settings
22
22
  Requires-Dist: argcomplete>=3
23
23
  Requires-Dist: typing-extensions; python_version < '3.11'
24
24
  Requires-Dist: esp-bool-parser>=0.1.2,<1
25
+ Requires-Dist: rich
25
26
  Requires-Dist: sphinx ; extra == "doc"
26
27
  Requires-Dist: sphinx-rtd-theme ; extra == "doc"
27
28
  Requires-Dist: sphinx_copybutton ; extra == "doc"
@@ -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.6.3'
11
+ __version__ = '2.7.0'
12
12
 
13
13
  from .session_args import (
14
14
  SessionArgs,
@@ -36,7 +36,6 @@ from .constants import (
36
36
  IDF_VERSION_PATCH,
37
37
  PREVIEW_TARGETS,
38
38
  PROJECT_DESCRIPTION_JSON,
39
- BuildStage,
40
39
  BuildStatus,
41
40
  )
42
41
  from .manifest.manifest import (
@@ -55,17 +54,7 @@ from .utils import (
55
54
  to_list,
56
55
  )
57
56
 
58
-
59
- class _AppBuildStageFilter(logging.Filter):
60
- def __init__(self, *args, app, **kwargs):
61
- super().__init__(*args, **kwargs)
62
- self.app = app
63
-
64
- def filter(self, record: logging.LogRecord) -> bool:
65
- if self.app._build_stage:
66
- record.build_stage = self.app._build_stage.value
67
-
68
- return True
57
+ LOGGER = logging.getLogger(__name__)
69
58
 
70
59
 
71
60
  class App(BaseModel):
@@ -120,7 +109,6 @@ class App(BaseModel):
120
109
  build_status: BuildStatus = BuildStatus.UNKNOWN
121
110
  build_comment: t.Optional[str] = None
122
111
 
123
- _build_stage: t.Optional[BuildStage] = None
124
112
  _build_duration: float = 0
125
113
  _build_timestamp: t.Optional[datetime] = None
126
114
 
@@ -176,9 +164,6 @@ class App(BaseModel):
176
164
  # private attrs, won't be dumped to json
177
165
  self._checked_should_build = False
178
166
 
179
- self._logger = logging.getLogger(f'{__name__}.{hash(self)}')
180
- self._logger.addFilter(_AppBuildStageFilter(app=self))
181
-
182
167
  self._sdkconfig_files, self._sdkconfig_files_defined_target = self._process_sdkconfig_files()
183
168
 
184
169
  @classmethod
@@ -346,10 +331,10 @@ class App(BaseModel):
346
331
  # use filepath if abs/rel already point to itself
347
332
  if not os.path.isfile(f):
348
333
  # find it in the app_dir
349
- self._logger.debug('sdkconfig file %s not found, checking under app_dir...', f)
334
+ LOGGER.debug('sdkconfig file %s not found, checking under app_dir...', f)
350
335
  f = os.path.join(self.app_dir, f)
351
336
  if not os.path.isfile(f):
352
- self._logger.debug('sdkconfig file %s not found, skipping...', f)
337
+ LOGGER.debug('sdkconfig file %s not found, skipping...', f)
353
338
  continue
354
339
 
355
340
  expanded_fp = os.path.join(expanded_dir, os.path.basename(f))
@@ -377,14 +362,14 @@ class App(BaseModel):
377
362
  with open(f) as fr:
378
363
  with open(expanded_fp) as new_fr:
379
364
  if fr.read() == new_fr.read():
380
- self._logger.debug('Use sdkconfig file %s', f)
365
+ LOGGER.debug('Use sdkconfig file %s', f)
381
366
  try:
382
367
  os.unlink(expanded_fp)
383
368
  except OSError:
384
- self._logger.debug('Failed to remove file %s', expanded_fp)
369
+ LOGGER.debug('Failed to remove file %s', expanded_fp)
385
370
  real_sdkconfig_files.append(f)
386
371
  else:
387
- self._logger.debug('Expand sdkconfig file %s to %s', f, expanded_fp)
372
+ LOGGER.debug('Expand sdkconfig file %s to %s', f, expanded_fp)
388
373
  real_sdkconfig_files.append(expanded_fp)
389
374
  # copy the related target-specific sdkconfig files
390
375
  par_dir = os.path.abspath(os.path.join(f, '..'))
@@ -392,7 +377,7 @@ class App(BaseModel):
392
377
  os.path.join(par_dir, str(p))
393
378
  for p in Path(par_dir).glob(os.path.basename(f) + f'.{self.target}')
394
379
  ):
395
- self._logger.debug(
380
+ LOGGER.debug(
396
381
  'Copy target-specific sdkconfig file %s to %s', target_specific_file, expanded_dir
397
382
  )
398
383
  shutil.copy(target_specific_file, expanded_dir)
@@ -474,21 +459,16 @@ class App(BaseModel):
474
459
  return wrapper
475
460
 
476
461
  def _pre_build(self) -> None:
477
- if self.dry_run:
478
- self._build_stage = BuildStage.DRY_RUN
479
- else:
480
- self._build_stage = BuildStage.PRE_BUILD
481
-
482
462
  if self.build_status == BuildStatus.SKIPPED:
483
463
  return
484
464
 
485
465
  if self.work_dir != self.app_dir:
486
466
  if os.path.exists(self.work_dir):
487
- self._logger.debug('Removed existing work dir: %s', self.work_dir)
467
+ LOGGER.debug('Removed existing work dir: %s', self.work_dir)
488
468
  if not self.dry_run:
489
469
  shutil.rmtree(self.work_dir)
490
470
 
491
- self._logger.debug('Copied app from %s to %s', self.app_dir, self.work_dir)
471
+ LOGGER.debug('Copied app from %s to %s', self.app_dir, self.work_dir)
492
472
  if not self.dry_run:
493
473
  # if the new directory inside the original directory,
494
474
  # make sure not to go into recursion.
@@ -502,7 +482,7 @@ class App(BaseModel):
502
482
  shutil.copytree(self.app_dir, self.work_dir, ignore=ignore, symlinks=True)
503
483
 
504
484
  if os.path.exists(self.build_path):
505
- self._logger.debug('Removed existing build dir: %s', self.build_path)
485
+ LOGGER.debug('Removed existing build dir: %s', self.build_path)
506
486
  if not self.dry_run:
507
487
  shutil.rmtree(self.build_path)
508
488
 
@@ -511,17 +491,17 @@ class App(BaseModel):
511
491
 
512
492
  sdkconfig_file = os.path.join(self.work_dir, 'sdkconfig')
513
493
  if os.path.exists(sdkconfig_file):
514
- self._logger.debug('Removed existing sdkconfig file: %s', sdkconfig_file)
494
+ LOGGER.debug('Removed existing sdkconfig file: %s', sdkconfig_file)
515
495
  if not self.dry_run:
516
496
  os.unlink(sdkconfig_file)
517
497
 
518
498
  if os.path.isfile(self.build_log_path):
519
- self._logger.debug('Removed existing build log file: %s', self.build_log_path)
499
+ LOGGER.debug('Removed existing build log file: %s', self.build_log_path)
520
500
  if not self.dry_run:
521
501
  os.unlink(self.build_log_path)
522
502
  elif not self.dry_run:
523
503
  os.makedirs(os.path.dirname(self.build_log_path), exist_ok=True)
524
- self._logger.info('Writing build log to %s', self.build_log_path)
504
+ LOGGER.info('Writing build log to %s', self.build_log_path)
525
505
 
526
506
  if self.dry_run:
527
507
  self.build_status = BuildStatus.SKIPPED
@@ -570,8 +550,6 @@ class App(BaseModel):
570
550
  ):
571
551
  return
572
552
 
573
- self._build_stage = BuildStage.POST_BUILD
574
-
575
553
  # both status applied
576
554
  if self.copy_sdkconfig:
577
555
  try:
@@ -580,9 +558,9 @@ class App(BaseModel):
580
558
  os.path.join(self.build_path, 'sdkconfig'),
581
559
  )
582
560
  except Exception as e:
583
- self._logger.warning('Copy sdkconfig file from failed: %s', e)
561
+ LOGGER.warning('Copy sdkconfig file from failed: %s', e)
584
562
  else:
585
- self._logger.debug('Copied sdkconfig file from %s to %s', self.work_dir, self.build_path)
563
+ LOGGER.debug('Copied sdkconfig file from %s to %s', self.work_dir, self.build_path)
586
564
 
587
565
  # for originally success builds generate size.json if enabled
588
566
  #
@@ -592,7 +570,7 @@ class App(BaseModel):
592
570
  self.write_size_json()
593
571
 
594
572
  if not os.path.isfile(self.build_log_path):
595
- self._logger.warning(f'{self.build_log_path} does not exist. Skipping post build actions...')
573
+ LOGGER.warning(f'{self.build_log_path} does not exist. Skipping post build actions...')
596
574
  return
597
575
 
598
576
  # check warnings
@@ -603,21 +581,21 @@ class App(BaseModel):
603
581
  is_error_or_warning, ignored = self.is_error_or_warning(line)
604
582
  if is_error_or_warning:
605
583
  if ignored:
606
- self._logger.info('[Ignored warning] %s', line)
584
+ LOGGER.info('[Ignored warning] %s', line)
607
585
  else:
608
- self._logger.warning('%s', line)
586
+ LOGGER.warning('%s', line)
609
587
  has_unignored_warning = True
610
588
 
611
589
  # for failed builds, print last few lines to help debug
612
590
  if self.build_status == BuildStatus.FAILED:
613
591
  # print last few lines to help debug
614
- self._logger.error(
592
+ LOGGER.error(
615
593
  'Last %s lines from the build log "%s":',
616
594
  self.LOG_DEBUG_LINES,
617
595
  self.build_log_path,
618
596
  )
619
597
  for line in lines[-self.LOG_DEBUG_LINES :]:
620
- self._logger.error('%s', line)
598
+ LOGGER.error('%s', line)
621
599
  # correct build status for originally successful builds
622
600
  elif self.build_status == BuildStatus.SUCCESS:
623
601
  if self.check_warnings and has_unignored_warning:
@@ -636,7 +614,7 @@ class App(BaseModel):
636
614
  # remove temp log file
637
615
  if self._is_build_log_path_temp:
638
616
  os.unlink(self.build_log_path)
639
- self._logger.debug('Removed success build temporary log file: %s', self.build_log_path)
617
+ LOGGER.debug('Removed success build temporary log file: %s', self.build_log_path)
640
618
 
641
619
  # Cleanup build directory if not preserving
642
620
  if not self.preserve:
@@ -649,17 +627,17 @@ class App(BaseModel):
649
627
  self.build_path,
650
628
  exclude_file_patterns=exclude_list,
651
629
  )
652
- self._logger.debug('Removed built binaries under: %s', self.build_path)
630
+ LOGGER.debug('Removed built binaries under: %s', self.build_path)
653
631
 
654
632
  def _build(
655
633
  self,
656
634
  *,
657
- manifest_rootpath: t.Optional[str] = None, # noqa: ARG002
658
- modified_components: t.Optional[t.List[str]] = None, # noqa: ARG002
659
- modified_files: t.Optional[t.List[str]] = None, # noqa: ARG002
660
- check_app_dependencies: bool = False, # noqa: ARG002
635
+ manifest_rootpath: t.Optional[str] = None,
636
+ modified_components: t.Optional[t.List[str]] = None,
637
+ modified_files: t.Optional[t.List[str]] = None,
638
+ check_app_dependencies: bool = False,
661
639
  ) -> None:
662
- self._build_stage = BuildStage.BUILD
640
+ pass
663
641
 
664
642
  def _write_size_json(self) -> None:
665
643
  if not self.size_json_path:
@@ -667,7 +645,7 @@ class App(BaseModel):
667
645
 
668
646
  map_file = find_first_match('*.map', self.build_path)
669
647
  if not map_file:
670
- self._logger.warning(
648
+ LOGGER.warning(
671
649
  '.map file not found. Cannot write size json to file: %s',
672
650
  self.size_json_path,
673
651
  )
@@ -703,18 +681,18 @@ class App(BaseModel):
703
681
  check=True,
704
682
  )
705
683
 
706
- self._logger.debug('Generated size info to %s', self.size_json_path)
684
+ LOGGER.debug('Generated size info to %s', self.size_json_path)
707
685
 
708
686
  def write_size_json(self) -> None:
709
687
  if self.target in PREVIEW_TARGETS:
710
688
  # targets in preview may not yet have support in esp-idf-size
711
- self._logger.info('Skipping generation of size json for target %s as it is in preview.', self.target)
689
+ LOGGER.info('Skipping generation of size json for target %s as it is in preview.', self.target)
712
690
  return
713
691
 
714
692
  try:
715
693
  self._write_size_json()
716
694
  except Exception as e:
717
- self._logger.warning('Failed to generate size json: %s', e)
695
+ LOGGER.warning('Failed to generate size json: %s', e)
718
696
 
719
697
  def to_json(self) -> str:
720
698
  return self.model_dump_json()
@@ -969,7 +947,7 @@ class CMakeApp(App):
969
947
  check=True,
970
948
  additional_env_dict=additional_env_dict,
971
949
  )
972
- self._logger.debug('generated project_description.json to check app dependencies')
950
+ LOGGER.debug('generated project_description.json to check app dependencies')
973
951
 
974
952
  with open(os.path.join(self.build_path, PROJECT_DESCRIPTION_JSON)) as fr:
975
953
  build_components = {item for item in json.load(fr)['build_components'] if item}
@@ -31,17 +31,6 @@ class BuildStatus(str, enum.Enum):
31
31
  SUCCESS = 'build success'
32
32
 
33
33
 
34
- class BuildStage(str, enum.Enum):
35
- DRY_RUN = 'Dry Run'
36
- PRE_BUILD = 'Pre Build'
37
- BUILD = 'Build'
38
- POST_BUILD = 'Post Build'
39
-
40
- @classmethod
41
- def max_length(cls) -> int:
42
- return max(len(v.value) for v in cls.__members__.values())
43
-
44
-
45
34
  completion_instructions = """
46
35
  With the `--activate` option, detect your shell type and add the appropriate commands to your shell's config file
47
36
  so that it runs on startup. You will likely have to restart.
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  """
@@ -211,4 +211,4 @@ class TestReport:
211
211
  xml.append(test_suite.to_xml_elem())
212
212
 
213
213
  ElementTree.ElementTree(xml).write(self.filepath, encoding='utf-8')
214
- LOGGER.info('Test report written to %s', self.filepath)
214
+ LOGGER.info('Generated build junit report at: %s', self.filepath)
@@ -0,0 +1,119 @@
1
+ # SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ import typing as t
6
+ from datetime import datetime
7
+
8
+ from rich import get_console
9
+ from rich._log_render import LogRender
10
+ from rich.console import Console, ConsoleRenderable
11
+ from rich.containers import Renderables
12
+ from rich.logging import RichHandler
13
+ from rich.text import Text, TextType
14
+
15
+
16
+ class _OneLineLogRender(LogRender):
17
+ def __call__( # type: ignore # the original method returns Table instead of Text
18
+ self,
19
+ console: Console,
20
+ renderables: t.Iterable[ConsoleRenderable],
21
+ log_time: t.Optional[datetime] = None,
22
+ time_format: t.Optional[t.Union[str, t.Callable[[datetime], Text]]] = None,
23
+ level: TextType = '',
24
+ path: t.Optional[str] = None,
25
+ line_no: t.Optional[int] = None,
26
+ link_path: t.Optional[str] = None,
27
+ ) -> Text:
28
+ output = Text(no_wrap=True)
29
+ if self.show_time:
30
+ log_time = log_time or console.get_datetime()
31
+ time_format = time_format or self.time_format
32
+ if callable(time_format):
33
+ log_time_display = time_format(log_time)
34
+ else:
35
+ log_time_display = Text(log_time.strftime(time_format), style='log.time')
36
+ if log_time_display == self._last_time and self.omit_repeated_times:
37
+ output.append(' ' * len(log_time_display), style='log.time')
38
+ else:
39
+ output.append(log_time_display)
40
+ self._last_time = log_time_display
41
+ output.pad_right(1)
42
+
43
+ if self.show_level:
44
+ output.append(level)
45
+ if self.level_width:
46
+ output.pad_right(max(1, self.level_width - len(level)))
47
+ else:
48
+ output.pad_right(1)
49
+
50
+ for renderable in Renderables(renderables): # type: ignore
51
+ if isinstance(renderable, Text):
52
+ renderable.stylize('log.message')
53
+
54
+ output.append(renderable)
55
+ output.pad_right(1)
56
+
57
+ if self.show_path and path:
58
+ path_text = Text(style='log.path')
59
+ path_text.append(path, style=f'link file://{link_path}' if link_path else '')
60
+ if line_no:
61
+ path_text.append(':')
62
+ path_text.append(
63
+ f'{line_no}',
64
+ style=f'link file://{link_path}#{line_no}' if link_path else '',
65
+ )
66
+ output.append(path_text)
67
+ output.pad_right(1)
68
+
69
+ output.rstrip()
70
+ return output
71
+
72
+
73
+ def get_rich_log_handler(level: int = logging.WARNING, no_color: bool = False) -> RichHandler:
74
+ console = get_console()
75
+ console.soft_wrap = True
76
+ console.no_color = no_color
77
+ console.stderr = True
78
+
79
+ handler = RichHandler(
80
+ level,
81
+ console,
82
+ )
83
+ handler._log_render = _OneLineLogRender(
84
+ show_level=True,
85
+ show_path=False,
86
+ omit_repeated_times=False,
87
+ )
88
+
89
+ return handler
90
+
91
+
92
+ def setup_logging(verbose: int = 0, log_file: t.Optional[str] = None, colored: bool = True) -> None:
93
+ """
94
+ Setup logging stream handler
95
+
96
+ :param verbose: 0 - WARNING, 1 - INFO, 2 - DEBUG
97
+ :param log_file: log file path
98
+ :param colored: colored output or not
99
+ :return: None
100
+ """
101
+ if not verbose:
102
+ level = logging.WARNING
103
+ elif verbose == 1:
104
+ level = logging.INFO
105
+ else:
106
+ level = logging.DEBUG
107
+
108
+ package_logger = logging.getLogger(__package__)
109
+ package_logger.setLevel(level)
110
+
111
+ if log_file:
112
+ handler: logging.Handler = logging.FileHandler(log_file)
113
+ else:
114
+ handler = get_rich_log_handler(level, not colored)
115
+ if package_logger.hasHandlers():
116
+ package_logger.handlers.clear()
117
+ package_logger.addHandler(handler)
118
+
119
+ package_logger.propagate = False # don't propagate to root logger
@@ -10,6 +10,7 @@ import os
10
10
  import sys
11
11
  import textwrap
12
12
  import typing as t
13
+ from pathlib import Path
13
14
 
14
15
  import argcomplete
15
16
  from pydantic import (
@@ -75,7 +76,7 @@ def find_apps(
75
76
  ## `preserve`
76
77
  if 'preserve' in kwargs:
77
78
  LOGGER.warning(
78
- 'Passing "preserve" directly is deprecated. '
79
+ 'DEPRECATED: Passing "preserve" directly is deprecated. '
79
80
  'Pass "no_preserve" instead to disable preserving the build directory'
80
81
  )
81
82
  kwargs['no_preserve'] = not kwargs.pop('preserve')
@@ -102,11 +103,14 @@ def find_apps(
102
103
  apps: t.Set[App] = set()
103
104
  if find_arguments.target == 'all':
104
105
  targets = ALL_TARGETS
106
+ LOGGER.info('Searching for apps by all targets')
105
107
  else:
106
108
  targets = [find_arguments.target]
109
+ LOGGER.info('Searching for apps by target: %s', find_arguments.target)
107
110
 
108
111
  for _t in targets:
109
112
  for _p in find_arguments.paths:
113
+ LOGGER.debug('Searching for apps in path %s for target %s', _p, _t)
110
114
  apps.update(
111
115
  _find_apps(
112
116
  _p,
@@ -116,7 +120,7 @@ def find_apps(
116
120
  )
117
121
  )
118
122
 
119
- LOGGER.info(f'Found {len(apps)} apps in total')
123
+ LOGGER.info('Found %d apps in total', len(apps))
120
124
 
121
125
  return sorted(apps)
122
126
 
@@ -140,7 +144,7 @@ def build_apps(
140
144
  ## `check_app_dependencies`
141
145
  if 'check_app_dependencies' in kwargs:
142
146
  LOGGER.warning(
143
- 'Passing "check_app_dependencies" directly is deprecated. '
147
+ 'DEPRECATED: Passing "check_app_dependencies" directly is deprecated. '
144
148
  'Pass "modified_components" instead to enable dependency-driven build feature'
145
149
  )
146
150
  kwargs.pop('check_app_dependencies')
@@ -161,15 +165,24 @@ def build_apps(
161
165
  test_suite = TestSuite('build_apps')
162
166
 
163
167
  start, stop = get_parallel_start_stop(len(apps), build_arguments.parallel_count, build_arguments.parallel_index)
164
- LOGGER.info('Total %s apps. running build for app %s-%s', len(apps), start, stop)
168
+ LOGGER.info('Processing %d total apps: building apps %d-%d', len(apps), start, stop)
165
169
 
166
170
  # cleanup collect files if exists at this early-stage
167
171
  for f in (build_arguments.collect_app_info, build_arguments.collect_size_info, build_arguments.junitxml):
168
172
  if f and os.path.isfile(f):
173
+ LOGGER.debug('Removing existing collect file: %s', f)
169
174
  os.remove(f)
170
- LOGGER.debug('Remove existing collect file %s', f)
171
175
 
172
176
  exit_code = 0
177
+
178
+ # create empty files, avoid no file when no app is built
179
+ if build_arguments.collect_app_info:
180
+ LOGGER.debug('Creating empty app info file: %s', build_arguments.collect_app_info)
181
+ Path(build_arguments.collect_app_info).touch()
182
+ if build_arguments.collect_size_info:
183
+ LOGGER.debug('Creating empty size info file: %s', build_arguments.collect_size_info)
184
+ Path(build_arguments.collect_size_info).touch()
185
+
173
186
  for i, app in enumerate(apps):
174
187
  index = i + 1 # we use 1-based
175
188
  if index < start or index > stop:
@@ -181,7 +194,7 @@ def build_apps(
181
194
  app.verbose = build_arguments.build_verbose
182
195
  app.copy_sdkconfig = build_arguments.copy_sdkconfig
183
196
 
184
- LOGGER.info('(%s/%s) Building app: %s', index, len(apps), app)
197
+ LOGGER.info('(%d/%d) Building app: %s', index, len(apps), app)
185
198
 
186
199
  app.build(
187
200
  manifest_rootpath=build_arguments.manifest_rootpath,
@@ -199,12 +212,14 @@ def build_apps(
199
212
  if build_arguments.collect_app_info:
200
213
  with open(build_arguments.collect_app_info, 'a') as fw:
201
214
  fw.write(app.to_json() + '\n')
202
- LOGGER.debug('Recorded app info in %s', build_arguments.collect_app_info)
215
+ LOGGER.debug('Recorded app info in file: %s', build_arguments.collect_app_info)
203
216
 
204
217
  if app.build_status == BuildStatus.FAILED:
205
218
  if not build_arguments.keep_going:
219
+ LOGGER.error('Build failed and keep_going=False, stopping build process')
206
220
  return 1
207
221
  else:
222
+ LOGGER.warning('Build failed but keep_going=True, continuing with next app')
208
223
  exit_code = 1
209
224
  elif app.build_status == BuildStatus.SUCCESS:
210
225
  if build_arguments.collect_size_info and app.size_json_path:
@@ -221,13 +236,12 @@ def build_apps(
221
236
  )
222
237
  + '\n'
223
238
  )
224
- LOGGER.debug('Recorded size info file path in %s', build_arguments.collect_size_info)
239
+ LOGGER.debug('Recorded size info file path: %s', build_arguments.collect_size_info)
225
240
 
226
241
  LOGGER.info('') # add one empty line for separating different builds
227
242
 
228
243
  if build_arguments.junitxml:
229
244
  TestReport([test_suite], build_arguments.junitxml).create_test_report()
230
- LOGGER.info('Generated junit report for build apps: %s', build_arguments.junitxml)
231
245
 
232
246
  return exit_code
233
247
 
@@ -31,7 +31,9 @@ dependencies = [
31
31
  "pydantic_settings",
32
32
  "argcomplete>=3",
33
33
  "typing-extensions; python_version < '3.11'",
34
- "esp-bool-parser>=0.1.2,<1"
34
+ "esp-bool-parser>=0.1.2,<1",
35
+ # debug/print
36
+ "rich",
35
37
  ]
36
38
 
37
39
  [project.optional-dependencies]
@@ -62,7 +64,7 @@ idf-build-apps = "idf_build_apps:main.main"
62
64
 
63
65
  [tool.commitizen]
64
66
  name = "cz_conventional_commits"
65
- version = "2.6.3"
67
+ version = "2.7.0"
66
68
  tag_format = "v$version"
67
69
  version_files = [
68
70
  "idf_build_apps/__init__.py",
@@ -20,7 +20,8 @@ install_requires = \
20
20
  'pydantic~=2.0',
21
21
  'pydantic_settings',
22
22
  'argcomplete>=3',
23
- 'esp-bool-parser>=0.1.2,<1']
23
+ 'esp-bool-parser>=0.1.2,<1',
24
+ 'rich']
24
25
 
25
26
  extras_require = \
26
27
  {":python_version < '3.11'": ['toml', 'typing-extensions'],
@@ -37,7 +38,7 @@ entry_points = \
37
38
  {'console_scripts': ['idf-build-apps = idf_build_apps:main.main']}
38
39
 
39
40
  setup(name='idf-build-apps',
40
- version='2.6.3',
41
+ version='2.7.0',
41
42
  description='Tools for building ESP-IDF related apps.',
42
43
  author=None,
43
44
  author_email='Fu Hanxi <fuhanxi@espressif.com>',
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
1
+ # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import os
@@ -22,6 +22,7 @@ from idf_build_apps import (
22
22
  from idf_build_apps.app import (
23
23
  CMakeApp,
24
24
  )
25
+ from idf_build_apps.args import BuildArguments
25
26
  from idf_build_apps.constants import (
26
27
  IDF_PATH,
27
28
  BuildStatus,
@@ -202,3 +203,18 @@ class TestBuild:
202
203
  assert test_suite.attrib['skipped'] == '0'
203
204
 
204
205
  assert test_suite.findall('testcase')[0].attrib['name'] == 'foo/bar/build'
206
+
207
+
208
+ def test_build_apps_collect_files_when_no_apps_built(tmp_path):
209
+ os.chdir(tmp_path)
210
+
211
+ build_apps(
212
+ build_arguments=BuildArguments(
213
+ target='esp32',
214
+ collect_app_info_filename='app_info.txt',
215
+ collect_size_info_filename='size_info.txt',
216
+ )
217
+ )
218
+
219
+ assert os.path.exists('app_info.txt')
220
+ assert os.path.exists('size_info.txt')
@@ -1,88 +0,0 @@
1
- # SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- import logging
5
- import sys
6
- import typing as t
7
-
8
- from .constants import (
9
- BuildStage,
10
- )
11
-
12
-
13
- class ColoredFormatter(logging.Formatter):
14
- grey: str = '\x1b[37;20m'
15
- yellow: str = '\x1b[33;20m'
16
- red: str = '\x1b[31;20m'
17
- bold_red: str = '\x1b[31;1m'
18
-
19
- reset: str = '\x1b[0m'
20
-
21
- fmt: str = '%(asctime)s %(levelname)8s %(message)s'
22
- app_fmt: str = f'%(asctime)s %(levelname)8s [%(build_stage){BuildStage.max_length()}s] %(message)s'
23
-
24
- datefmt: str = '%Y-%m-%d %H:%M:%S'
25
-
26
- FORMATS: t.Dict[int, str] = {
27
- logging.DEBUG: f'{grey}{{}}{reset}',
28
- logging.INFO: '{}',
29
- logging.WARNING: f'{yellow}{{}}{reset}',
30
- logging.ERROR: f'{red}{{}}{reset}',
31
- logging.CRITICAL: f'{bold_red}{{}}{reset}',
32
- }
33
-
34
- def __init__(self, colored: bool = True) -> None:
35
- self.colored = colored
36
- if sys.platform == 'win32': # does not support it
37
- self.colored = False
38
-
39
- super().__init__(datefmt=self.datefmt)
40
-
41
- def format(self, record: logging.LogRecord) -> str:
42
- if getattr(record, 'build_stage', None):
43
- base_fmt = self.app_fmt
44
- else:
45
- base_fmt = self.fmt
46
-
47
- if self.colored:
48
- log_fmt = self.FORMATS[record.levelno].format(base_fmt)
49
- else:
50
- log_fmt = base_fmt
51
-
52
- if record.levelno in [logging.WARNING, logging.ERROR]:
53
- record.msg = '>>> ' + str(record.msg)
54
- elif record.levelno == logging.CRITICAL:
55
- record.msg = '!!! ' + str(record.msg)
56
-
57
- formatter = logging.Formatter(log_fmt, datefmt=self.datefmt)
58
- return formatter.format(record)
59
-
60
-
61
- def setup_logging(verbose: int = 0, log_file: t.Optional[str] = None, colored: bool = True) -> None:
62
- """
63
- Setup logging stream handler
64
-
65
- :param verbose: 0 - WARNING, 1 - INFO, 2 - DEBUG
66
- :param log_file: log file path
67
- :param colored: colored output or not
68
- :return: None
69
- """
70
- if not verbose:
71
- level = logging.WARNING
72
- elif verbose == 1:
73
- level = logging.INFO
74
- else:
75
- level = logging.DEBUG
76
-
77
- package_logger = logging.getLogger(__package__)
78
- package_logger.setLevel(level)
79
- if log_file:
80
- handler: logging.Handler = logging.FileHandler(log_file)
81
- else:
82
- handler = logging.StreamHandler(sys.stderr)
83
- handler.setFormatter(ColoredFormatter(colored))
84
-
85
- if package_logger.hasHandlers():
86
- package_logger.handlers.clear()
87
- package_logger.addHandler(handler)
88
- package_logger.propagate = False # don't propagate to root logger
File without changes
File without changes