build_apps.py 6.0 KB

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