• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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