build_apps.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. #
  4. # ESP-IDF helper script to build multiple applications. Consumes the input of find_apps.py.
  5. #
  6. import argparse
  7. import logging
  8. import os.path
  9. import re
  10. import sys
  11. from typing import List, Optional, TextIO
  12. from find_build_apps import BUILD_SYSTEMS, BuildError, BuildItem, setup_logging
  13. from find_build_apps.common import SIZE_JSON_FN, rmdir
  14. # This RE will match GCC errors and many other fatal build errors and warnings as well
  15. LOG_ERROR_WARNING = re.compile(r'(error|warning):', re.IGNORECASE)
  16. # Log this many trailing lines from a failed build log, also
  17. LOG_DEBUG_LINES = 25
  18. def build_apps(
  19. build_items: List[BuildItem],
  20. parallel_count: int = 1,
  21. parallel_index: int = 1,
  22. dry_run: bool = False,
  23. build_verbose: bool = False,
  24. keep_going: bool = False,
  25. output_build_list: Optional[TextIO] = None,
  26. size_info: Optional[TextIO] = None
  27. ) -> None:
  28. if not build_items:
  29. logging.warning('Empty build list')
  30. sys.exit(0)
  31. num_builds = len(build_items)
  32. num_jobs = parallel_count
  33. job_index = parallel_index - 1 # convert to 0-based index
  34. num_builds_per_job = (num_builds + num_jobs - 1) // num_jobs
  35. min_job_index = num_builds_per_job * job_index
  36. if min_job_index >= num_builds:
  37. logging.warning(
  38. f'Nothing to do for job {job_index + 1} (build total: {num_builds}, per job: {num_builds_per_job})')
  39. sys.exit(0)
  40. max_job_index = min(num_builds_per_job * (job_index + 1) - 1, num_builds - 1)
  41. logging.info('Total {} builds, max. {} builds per job, running builds {}-{}'.format(
  42. num_builds, num_builds_per_job, min_job_index + 1, max_job_index + 1))
  43. builds_for_current_job = build_items[min_job_index:max_job_index + 1]
  44. for i, build_item in enumerate(builds_for_current_job):
  45. index = i + min_job_index + 1
  46. build_item.index = index
  47. build_item.dry_run = dry_run
  48. build_item.verbose = build_verbose
  49. build_item.keep_going = keep_going
  50. logging.debug('\tBuild {}: {}'.format(index, repr(build_item)))
  51. if output_build_list:
  52. output_build_list.write(build_item.to_json_expanded() + '\n')
  53. failed_builds = []
  54. for build_item in builds_for_current_job:
  55. logging.info('Running build {}: {}'.format(build_item.index, repr(build_item)))
  56. build_system_class = BUILD_SYSTEMS[build_item.build_system]
  57. try:
  58. build_system_class.build(build_item)
  59. except BuildError as e:
  60. logging.error(str(e))
  61. if build_item.build_log_path:
  62. log_filename = os.path.basename(build_item.build_log_path)
  63. with open(build_item.build_log_path, 'r') as f:
  64. lines = [line.rstrip() for line in f.readlines() if line.rstrip()] # non-empty lines
  65. logging.debug('Error and warning lines from {}:'.format(log_filename))
  66. for line in lines:
  67. if LOG_ERROR_WARNING.search(line):
  68. logging.warning('>>> {}'.format(line))
  69. logging.debug('Last {} lines of {}:'.format(LOG_DEBUG_LINES, log_filename))
  70. for line in lines[-LOG_DEBUG_LINES:]:
  71. logging.debug('>>> {}'.format(line))
  72. if keep_going:
  73. failed_builds.append(build_item)
  74. else:
  75. sys.exit(1)
  76. else:
  77. if size_info:
  78. build_item.write_size_info(size_info)
  79. if not build_item.preserve:
  80. logging.info(f'Removing build directory {build_item.build_path}')
  81. # we only remove binaries here, log files are still needed by check_build_warnings.py
  82. rmdir(build_item.build_path, exclude_file_pattern=SIZE_JSON_FN)
  83. if failed_builds:
  84. logging.error('The following build have failed:')
  85. for build in failed_builds:
  86. logging.error('\t{}'.format(build))
  87. sys.exit(1)
  88. if __name__ == '__main__':
  89. parser = argparse.ArgumentParser(description='ESP-IDF app builder')
  90. parser.add_argument(
  91. '-v',
  92. '--verbose',
  93. action='count',
  94. help='Increase the logging level of the script. Can be specified multiple times.',
  95. )
  96. parser.add_argument(
  97. '--build-verbose',
  98. action='store_true',
  99. help='Enable verbose output from build system.',
  100. )
  101. parser.add_argument(
  102. '--log-file',
  103. type=argparse.FileType('w'),
  104. help='Write the script log to the specified file, instead of stderr',
  105. )
  106. parser.add_argument(
  107. '--parallel-count',
  108. default=1,
  109. type=int,
  110. help="Number of parallel build jobs. Note that this script doesn't start the jobs, " +
  111. 'it needs to be executed multiple times with same value of --parallel-count and ' +
  112. 'different values of --parallel-index.',
  113. )
  114. parser.add_argument(
  115. '--parallel-index',
  116. default=1,
  117. type=int,
  118. help='Index (1-based) of the job, out of the number specified by --parallel-count.',
  119. )
  120. parser.add_argument(
  121. '--format',
  122. default='json',
  123. choices=['json'],
  124. help='Format to read the list of builds',
  125. )
  126. parser.add_argument(
  127. '--dry-run',
  128. action='store_true',
  129. help="Don't actually build, only print the build commands",
  130. )
  131. parser.add_argument(
  132. '--keep-going',
  133. action='store_true',
  134. help="Don't exit immediately when a build fails.",
  135. )
  136. parser.add_argument(
  137. '--output-build-list',
  138. type=argparse.FileType('w'),
  139. help='If specified, the list of builds (with all the placeholders expanded) will be written to this file.',
  140. )
  141. parser.add_argument(
  142. '--size-info',
  143. type=argparse.FileType('a'),
  144. help='If specified, the test case name and size info json will be written to this file'
  145. )
  146. parser.add_argument(
  147. 'build_list',
  148. type=argparse.FileType('r'),
  149. nargs='?',
  150. default=sys.stdin,
  151. help='Name of the file to read the list of builds from. If not specified, read from stdin.',
  152. )
  153. args = parser.parse_args()
  154. setup_logging(args)
  155. items = [BuildItem.from_json(line) for line in args.build_list]
  156. build_apps(items, args.parallel_count, args.parallel_index, args.dry_run, args.build_verbose,
  157. args.keep_going, args.output_build_list, args.size_info)