1#!/usr/bin/env python
2# coding=utf-8
3#
4# CI script to check build logs for warnings.
5# Reads the list of builds, in the format produced by find_apps.py or build_apps.py, and finds warnings in the
6# log files for every build.
7# Exits with a non-zero exit code if any warning is found.
8
9import argparse
10import logging
11import os
12import re
13import sys
14
15try:
16    from find_build_apps import BuildItem, setup_logging
17except ImportError:
18    sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
19    from find_build_apps import BuildItem, setup_logging
20
21WARNING_REGEX = re.compile(r'(?:error|warning)[^\w]', re.MULTILINE | re.IGNORECASE)
22
23IGNORE_WARNS = [
24    re.compile(r_str) for r_str in [
25        r'library/error\.o',
26        r'/.*error\S*\.o',
27        r'.*error.*\.c\.obj',
28        r'.*error.*\.cpp\.obj',
29        r'.*error.*\.cxx\.obj',
30        r'.*error.*\.cc\.obj',
31        r'-Werror',
32        r'error\.d',
33        r'/.*error\S*.d',
34        r'reassigning to symbol',
35        r'changes choice state',
36        r'crosstool_version_check\.cmake',
37        r'CryptographyDeprecationWarning',
38        r'Warning: \d+/\d+ app partitions are too small for binary'
39    ]
40]
41
42
43def line_has_warnings(line):  # type: (str) -> bool
44    if not WARNING_REGEX.search(line):
45        return False
46
47    has_warnings = True
48    for ignored in IGNORE_WARNS:
49        if re.search(ignored, line):
50            has_warnings = False
51            break
52
53    return has_warnings
54
55
56def main():  # type: () -> None
57    parser = argparse.ArgumentParser(description='ESP-IDF app builder')
58    parser.add_argument(
59        '-v',
60        '--verbose',
61        action='count',
62        help='Increase the logging level of the script. Can be specified multiple times.',
63    )
64    parser.add_argument(
65        '--log-file',
66        type=argparse.FileType('w'),
67        help='Write the script log to the specified file, instead of stderr',
68    )
69    parser.add_argument(
70        'build_list',
71        type=argparse.FileType('r'),
72        nargs='?',
73        default=sys.stdin,
74        help='Name of the file to read the list of builds from. If not specified, read from stdin.',
75    )
76    args = parser.parse_args()
77    setup_logging(args)
78
79    build_items = [BuildItem.from_json(line) for line in args.build_list]
80    if not build_items:
81        logging.warning('Empty build list')
82        SystemExit(0)
83
84    found_warnings = 0
85    for build_item in build_items:
86        if not build_item.build_log_path:
87            logging.debug('No log file for {}'.format(build_item.work_dir))
88            continue
89        with open(build_item.build_log_path, 'r') as log_file:
90            for line_no, line in enumerate(log_file):
91                if line_has_warnings(line):
92                    logging.error('Issue in app {}, config {}:'.format(build_item.app_dir, build_item.config_name))
93                    logging.error(line.rstrip('\n'))
94                    logging.error('See {}:{} for details'.format(os.path.basename(build_item.build_log_path),
95                                                                 line_no + 1))
96                    found_warnings += 1
97                    break
98
99    if found_warnings:
100        logging.error('Checked {} builds, found {} warnings'.format(len(build_items), found_warnings))
101        raise SystemExit(1)
102
103    logging.info('No warnings found')
104
105
106if __name__ == '__main__':
107    main()
108