1# Copyright 2019 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14""" 15bloat is a script which generates a size report card for binary files. 16""" 17 18import argparse 19import logging 20import os 21import subprocess 22import sys 23from typing import List, Iterable, Optional 24 25import pw_cli.log 26 27from pw_bloat.binary_diff import BinaryDiff 28from pw_bloat import bloat_output 29 30_LOG = logging.getLogger(__name__) 31 32 33def parse_args() -> argparse.Namespace: 34 """Parses the script's arguments.""" 35 def delimited_list(delimiter: str, items: Optional[int] = None): 36 def _parser(arg: str): 37 args = arg.split(delimiter) 38 39 if items and len(args) != items: 40 raise argparse.ArgumentTypeError( 41 'Argument must be a ' 42 f'{delimiter}-delimited list with {items} items: "{arg}"') 43 44 return args 45 46 return _parser 47 48 parser = argparse.ArgumentParser( 49 'Generate a size report card for binaries') 50 parser.add_argument('--bloaty-config', 51 type=delimited_list(';'), 52 required=True, 53 help='Data source configuration for Bloaty') 54 parser.add_argument('--full', 55 action='store_true', 56 help='Display full bloat breakdown by symbol') 57 parser.add_argument('--labels', 58 type=delimited_list(';'), 59 default='', 60 help='Labels for output binaries') 61 parser.add_argument('--out-dir', 62 type=str, 63 required=True, 64 help='Directory in which to write output files') 65 parser.add_argument('--target', 66 type=str, 67 required=True, 68 help='Build target name') 69 parser.add_argument('--title', 70 type=str, 71 default='pw_bloat', 72 help='Report title') 73 parser.add_argument('--source-filter', 74 type=str, 75 help='Bloaty data source filter') 76 parser.add_argument('diff_targets', 77 type=delimited_list(';', 2), 78 nargs='+', 79 metavar='DIFF_TARGET', 80 help='Binary;base pairs to process') 81 82 return parser.parse_args() 83 84 85def run_bloaty( 86 filename: str, 87 config: str, 88 base_file: Optional[str] = None, 89 data_sources: Iterable[str] = (), 90 extra_args: Iterable[str] = () 91) -> bytes: 92 """Executes a Bloaty size report on some binary file(s). 93 94 Args: 95 filename: Path to the binary. 96 config: Path to Bloaty config file. 97 base_file: Path to a base binary. If provided, a size diff is performed. 98 data_sources: List of Bloaty data sources for the report. 99 extra_args: Additional command-line arguments to pass to Bloaty. 100 101 Returns: 102 Binary output of the Bloaty invocation. 103 104 Raises: 105 subprocess.CalledProcessError: The Bloaty invocation failed. 106 """ 107 108 # TODO(frolv): Point the default bloaty path to a prebuilt in Pigweed. 109 default_bloaty = 'bloaty' 110 bloaty_path = os.getenv('BLOATY_PATH', default_bloaty) 111 112 # yapf: disable 113 cmd = [ 114 bloaty_path, 115 '-c', config, 116 '-d', ','.join(data_sources), 117 '--domain', 'vm', 118 filename, 119 *extra_args 120 ] 121 # yapf: enable 122 123 if base_file is not None: 124 cmd.extend(['--', base_file]) 125 126 return subprocess.check_output(cmd) 127 128 129def main() -> int: 130 """Program entry point.""" 131 132 args = parse_args() 133 134 base_binaries: List[str] = [] 135 diff_binaries: List[str] = [] 136 137 try: 138 for binary, base in args.diff_targets: 139 diff_binaries.append(binary) 140 base_binaries.append(base) 141 except RuntimeError as err: 142 _LOG.error('%s: %s', sys.argv[0], err) 143 return 1 144 145 data_sources = ['segment_names'] 146 if args.full: 147 data_sources.append('fullsymbols') 148 149 # TODO(frolv): CSV output is disabled for full reports as the default Bloaty 150 # breakdown is printed. This script should be modified to print a custom 151 # symbol breakdown in full reports. 152 extra_args = [] if args.full else ['--csv'] 153 if args.source_filter: 154 extra_args.extend(['--source-filter', args.source_filter]) 155 156 diffs: List[BinaryDiff] = [] 157 report = [] 158 159 for i, binary in enumerate(diff_binaries): 160 binary_name = (args.labels[i] 161 if i < len(args.labels) else os.path.basename(binary)) 162 try: 163 output = run_bloaty(binary, args.bloaty_config[i], 164 base_binaries[i], data_sources, extra_args) 165 if not output: 166 continue 167 168 # TODO(frolv): Remove when custom output for full mode is added. 169 if args.full: 170 report.append(binary_name) 171 report.append('-' * len(binary_name)) 172 report.append(output.decode()) 173 continue 174 175 # Ignore the first row as it displays column names. 176 bloaty_csv = output.decode().splitlines()[1:] 177 diffs.append(BinaryDiff.from_csv(binary_name, bloaty_csv)) 178 except subprocess.CalledProcessError: 179 _LOG.error('%s: failed to run diff on %s', sys.argv[0], binary) 180 return 1 181 182 def write_file(filename: str, contents: str) -> None: 183 path = os.path.join(args.out_dir, filename) 184 with open(path, 'w') as output_file: 185 output_file.write(contents) 186 _LOG.debug('Output written to %s', path) 187 188 # TODO(frolv): Remove when custom output for full mode is added. 189 if not args.full: 190 out = bloat_output.TableOutput(args.title, 191 diffs, 192 charset=bloat_output.LineCharset) 193 report.append(out.diff()) 194 195 rst = bloat_output.RstOutput(diffs) 196 write_file(f'{args.target}', rst.diff()) 197 198 complete_output = '\n'.join(report) + '\n' 199 write_file(f'{args.target}.txt', complete_output) 200 print(complete_output) 201 202 return 0 203 204 205if __name__ == '__main__': 206 pw_cli.log.install() 207 sys.exit(main()) 208