1# Copyright (C) 2022 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://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, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import argparse 16import dataclasses 17import functools 18import logging 19import os 20import re 21import sys 22import textwrap 23from datetime import date 24from enum import Enum 25from pathlib import Path 26from typing import Optional 27 28import cuj_catalog 29import util 30 31 32class BuildType(Enum): 33 _ignore_ = '_soong_cmd' 34 _soong_cmd = ['build/soong/soong_ui.bash', 35 '--make-mode', 36 '--skip-soong-tests'] 37 SOONG_ONLY = [*_soong_cmd, 'BUILD_BROKEN_DISABLE_BAZEL=true'] 38 MIXED_PROD = [*_soong_cmd, '--bazel-mode'] 39 MIXED_STAGING = [*_soong_cmd, '--bazel-mode-staging'] 40 MIXED_DEV = [*_soong_cmd, '--bazel-mode-dev'] 41 B = ['build/bazel/bin/b', 'build'] 42 B_ANDROID = [*B, '--config=android'] 43 44 @staticmethod 45 def from_flag(s: str) -> list['BuildType']: 46 chosen: list[BuildType] = [] 47 for e in BuildType: 48 if s.lower() in e.name.lower(): 49 chosen.append(e) 50 if len(chosen) == 0: 51 raise RuntimeError(f'no such build type: {s}') 52 return chosen 53 54 def to_flag(self): 55 return self.name.lower() 56 57 58@dataclasses.dataclass(frozen=True) 59class UserInput: 60 build_types: list[BuildType] 61 chosen_cujgroups: list[int] 62 description: Optional[str] 63 log_dir: Path 64 targets: list[str] 65 66 67@functools.cache 68def get_user_input() -> UserInput: 69 cujgroups = cuj_catalog.get_cujgroups() 70 71 def validate_cujgroups(input_str: str) -> list[int]: 72 if input_str.isnumeric(): 73 i = int(input_str) 74 if 0 <= i < len(cujgroups): 75 return [i] 76 else: 77 pattern = re.compile(input_str) 78 79 def matches(cujgroup: cuj_catalog.CujGroup) -> bool: 80 for cujstep in cujgroup.steps: 81 # because we should run all cujsteps in a group we will select 82 # a group if any of its steps match the pattern 83 if pattern.search(f'{cujstep.verb} {cujgroup.description}'): 84 return True 85 return False 86 87 matching_cuj_groups = [i for i, cujgroup in enumerate(cujgroups) if 88 matches(cujgroup)] 89 if len(matching_cuj_groups): 90 return matching_cuj_groups 91 raise argparse.ArgumentError( 92 argument=None, 93 message=f'Invalid input: "{input_str}" ' 94 f'expected an index <= {len(cujgroups)} ' 95 'or a regex pattern for a CUJ descriptions') 96 97 # importing locally here to avoid chances of cyclic import 98 import incremental_build 99 p = argparse.ArgumentParser( 100 formatter_class=argparse.RawTextHelpFormatter, 101 description='' + 102 textwrap.dedent(incremental_build.__doc__) + 103 textwrap.dedent(incremental_build.main.__doc__)) 104 105 cuj_list = '\n'.join( 106 [f'{i:2}: {cujgroup}' for i, cujgroup in enumerate(cujgroups)]) 107 p.add_argument('-c', '--cujs', nargs='+', 108 type=validate_cujgroups, 109 help='Index number(s) for the CUJ(s) from the following list. ' 110 'Or substring matches for the CUJ description.' 111 f'Note the ordering will be respected:\n{cuj_list}') 112 p.add_argument('-C', '--exclude-cujs', nargs='+', 113 type=validate_cujgroups, 114 help='Index number(s) or substring match(es) for the CUJ(s) ' 115 'to be excluded') 116 p.add_argument('-d', '--description', type=str, default='', 117 help='Any additional tag/description for the set of builds') 118 119 log_levels = dict(getattr(logging, '_levelToName')).values() 120 p.add_argument('-v', '--verbosity', choices=log_levels, default='INFO', 121 help='Log level. Defaults to %(default)s') 122 default_log_dir = util.get_top_dir().parent.joinpath( 123 f'timing-{date.today().strftime("%b%d")}') 124 p.add_argument('-l', '--log-dir', type=Path, default=default_log_dir, 125 help=textwrap.dedent(f''' 126 Directory for timing logs. Defaults to %(default)s 127 TIPS: 128 1 Specify a directory outside of the source tree 129 2 To view key metrics in metrics.csv: 130 {util.get_cmd_to_display_tabulated_metrics(default_log_dir)} 131 3 To view column headers: 132 {util.get_csv_columns_cmd(default_log_dir)}''').strip()) 133 def_build_types = [BuildType.SOONG_ONLY, 134 BuildType.MIXED_PROD, 135 BuildType.MIXED_STAGING] 136 p.add_argument('-b', '--build-types', nargs='+', 137 type=BuildType.from_flag, 138 default=[def_build_types], 139 help=f'Defaults to {[b.to_flag() for b in def_build_types]}. ' 140 f'Choose from {[e.name.lower() for e in BuildType]}') 141 p.add_argument('--ignore-repo-diff', default=False, action='store_true', 142 help='Skip "repo status" check') 143 p.add_argument('--append-csv', default=False, action='store_true', 144 help='Add results to existing spreadsheet') 145 p.add_argument('targets', nargs='*', default=['nothing'], 146 help='Targets to run, e.g. "libc adbd". ' 147 'Defaults to %(default)s') 148 149 options = p.parse_args() 150 151 if options.verbosity: 152 logging.root.setLevel(options.verbosity) 153 154 if options.cujs and options.exclude_cujs: 155 sys.exit('specify either --cujs or --exclude-cujs not both') 156 chosen_cujgroups: list[int] 157 if options.exclude_cujs: 158 exclusions: list[int] = [i for sublist in options.exclude_cujs for i in 159 sublist] 160 chosen_cujgroups = [i for i in range(0, len(cujgroups)) if 161 i not in exclusions] 162 elif options.cujs: 163 chosen_cujgroups = [i for sublist in options.cujs for i in sublist] 164 else: 165 chosen_cujgroups = [i for i in range(0, len(cujgroups))] 166 167 bazel_labels: list[str] = [target for target in options.targets if 168 target.startswith('//')] 169 if 0 < len(bazel_labels) < len(options.targets): 170 sys.exit(f'Don\'t mix bazel labels {bazel_labels} with soong targets ' 171 f'{[t for t in options.targets if t not in bazel_labels]}') 172 if os.getenv('BUILD_BROKEN_DISABLE_BAZEL') is not None: 173 raise RuntimeError(f'use -b {BuildType.SOONG_ONLY.to_flag()} ' 174 f'instead of BUILD_BROKEN_DISABLE_BAZEL') 175 build_types: list[BuildType] = [i for sublist in options.build_types for i in 176 sublist] 177 if len(bazel_labels) > 0: 178 non_b = [b for b in build_types if 179 b != BuildType.B and b != BuildType.B_ANDROID] 180 raise RuntimeError(f'bazel labels can not be used with {non_b}') 181 182 pretty_str = '\n'.join( 183 [f'{i:2}: {cujgroups[i]}' for i in chosen_cujgroups]) 184 logging.info(f'%d CUJs chosen:\n%s', len(chosen_cujgroups), pretty_str) 185 186 if not options.ignore_repo_diff and util.has_uncommitted_changes(): 187 error_message = 'THERE ARE UNCOMMITTED CHANGES (TIP: repo status).' \ 188 'Use --ignore-repo-diff to skip this check.' 189 if not util.is_interactive_shell(): 190 sys.exit(error_message) 191 response = input(f'{error_message}\nContinue?[Y/n]') 192 if response.upper() != 'Y': 193 sys.exit(1) 194 195 log_dir = Path(options.log_dir).resolve() 196 if not options.append_csv and log_dir.exists(): 197 error_message = f'{log_dir} already exists. ' \ 198 'Use --append-csv to skip this check.' 199 if not util.is_interactive_shell(): 200 sys.exit(error_message) 201 response = input(f'{error_message}\nContinue?[Y/n]') 202 if response.upper() != 'Y': 203 sys.exit(1) 204 205 return UserInput( 206 build_types=build_types, 207 chosen_cujgroups=chosen_cujgroups, 208 description=options.description, 209 log_dir=Path(options.log_dir).resolve(), 210 targets=options.targets) 211