blob: 094dfeab03c0417d7598968eaa3a6cbb49706764 [file] [log] [blame]
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import dataclasses
import functools
import logging
import os
import re
import sys
import textwrap
from datetime import date
from enum import Enum
from pathlib import Path
from typing import Optional
import cuj_catalog
import util
class BuildType(Enum):
_ignore_ = '_soong_cmd'
_soong_cmd = ['build/soong/soong_ui.bash',
'--make-mode',
'--skip-soong-tests']
SOONG_ONLY = [*_soong_cmd, 'BUILD_BROKEN_DISABLE_BAZEL=true']
MIXED_PROD = [*_soong_cmd, '--bazel-mode']
MIXED_STAGING = [*_soong_cmd, '--bazel-mode-staging']
MIXED_DEV = [*_soong_cmd, '--bazel-mode-dev']
B = ['build/bazel/bin/b', 'build']
B_ANDROID = [*B, '--config=android']
@staticmethod
def from_flag(s: str) -> list['BuildType']:
chosen: list[BuildType] = []
for e in BuildType:
if s.lower() in e.name.lower():
chosen.append(e)
if len(chosen) == 0:
raise RuntimeError(f'no such build type: {s}')
return chosen
def to_flag(self):
return self.name.lower()
@dataclasses.dataclass(frozen=True)
class UserInput:
build_types: list[BuildType]
chosen_cujgroups: list[int]
description: Optional[str]
log_dir: Path
targets: list[str]
ci_mode: bool
@functools.cache
def get_user_input() -> UserInput:
cujgroups = cuj_catalog.get_cujgroups()
def validate_cujgroups(input_str: str) -> list[int]:
if input_str.isnumeric():
i = int(input_str)
if 0 <= i < len(cujgroups):
return [i]
else:
pattern = re.compile(input_str)
def matches(cujgroup: cuj_catalog.CujGroup) -> bool:
for cujstep in cujgroup.steps:
# because we should run all cujsteps in a group we will select
# a group if any of its steps match the pattern
if pattern.search(f'{cujstep.verb} {cujgroup.description}'):
return True
return False
matching_cuj_groups = [i for i, cujgroup in enumerate(cujgroups) if
matches(cujgroup)]
if len(matching_cuj_groups):
return matching_cuj_groups
raise argparse.ArgumentError(
argument=None,
message=f'Invalid input: "{input_str}" '
f'expected an index <= {len(cujgroups)} '
'or a regex pattern for a CUJ descriptions')
# importing locally here to avoid chances of cyclic import
import incremental_build
p = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
description='' +
textwrap.dedent(incremental_build.__doc__) +
textwrap.dedent(incremental_build.main.__doc__))
cuj_list = '\n'.join(
[f'{i:2}: {cujgroup}' for i, cujgroup in enumerate(cujgroups)])
p.add_argument('-c', '--cujs', nargs='+',
type=validate_cujgroups,
help='Index number(s) for the CUJ(s) from the following list. '
'Or substring matches for the CUJ description.'
f'Note the ordering will be respected:\n{cuj_list}')
p.add_argument('-C', '--exclude-cujs', nargs='+',
type=validate_cujgroups,
help='Index number(s) or substring match(es) for the CUJ(s) '
'to be excluded')
p.add_argument('-d', '--description', type=str, default='',
help='Any additional tag/description for the set of builds')
log_levels = dict(getattr(logging, '_levelToName')).values()
p.add_argument('-v', '--verbosity', choices=log_levels, default='INFO',
help='Log level. Defaults to %(default)s')
default_log_dir = util.get_top_dir().parent.joinpath(
f'timing-{date.today().strftime("%b%d")}')
p.add_argument('-l', '--log-dir', type=Path, default=default_log_dir,
help=textwrap.dedent(f'''
Directory for timing logs. Defaults to %(default)s
TIPS:
1 Specify a directory outside of the source tree
2 To view key metrics in metrics.csv:
{util.get_cmd_to_display_tabulated_metrics(default_log_dir,
False)}
3 To view column headers:
{util.get_csv_columns_cmd(default_log_dir)}''').strip())
def_build_types = [BuildType.SOONG_ONLY,
BuildType.MIXED_PROD,
BuildType.MIXED_STAGING]
p.add_argument('-b', '--build-types', nargs='+',
type=BuildType.from_flag,
default=[def_build_types],
help=f'Defaults to {[b.to_flag() for b in def_build_types]}. '
f'Choose from {[e.name.lower() for e in BuildType]}')
p.add_argument('--ignore-repo-diff', default=False, action='store_true',
help='Skip "repo status" check')
p.add_argument('--append-csv', default=False, action='store_true',
help='Add results to existing spreadsheet')
p.add_argument('targets', nargs='*', default=['nothing'],
help='Targets to run, e.g. "libc adbd". '
'Defaults to %(default)s')
p.add_argument('--ci-mode', default=False, action='store_true',
help='Only use it for CI runs.It will copy the '
'first metrics after warmup to the logs directory in CI')
options = p.parse_args()
if options.verbosity:
logging.root.setLevel(options.verbosity)
if options.cujs and options.exclude_cujs:
sys.exit('specify either --cujs or --exclude-cujs not both')
chosen_cujgroups: list[int]
if options.exclude_cujs:
exclusions: list[int] = [i for sublist in options.exclude_cujs for i in
sublist]
chosen_cujgroups = [i for i in range(0, len(cujgroups)) if
i not in exclusions]
elif options.cujs:
chosen_cujgroups = [i for sublist in options.cujs for i in sublist]
else:
chosen_cujgroups = [i for i in range(0, len(cujgroups))]
bazel_labels: list[str] = [target for target in options.targets if
target.startswith('//')]
if 0 < len(bazel_labels) < len(options.targets):
sys.exit(f'Don\'t mix bazel labels {bazel_labels} with soong targets '
f'{[t for t in options.targets if t not in bazel_labels]}')
if os.getenv('BUILD_BROKEN_DISABLE_BAZEL') is not None:
raise RuntimeError(f'use -b {BuildType.SOONG_ONLY.to_flag()} '
f'instead of BUILD_BROKEN_DISABLE_BAZEL')
build_types: list[BuildType] = [i for sublist in options.build_types for i in
sublist]
if len(bazel_labels) > 0:
non_b = [b.name for b in build_types if
b != BuildType.B and b != BuildType.B_ANDROID]
if len(non_b):
raise RuntimeError(f'bazel labels can not be used with {non_b}')
pretty_str = '\n'.join(
[f'{i:2}: {cujgroups[i]}' for i in chosen_cujgroups])
logging.info(f'%d CUJs chosen:\n%s', len(chosen_cujgroups), pretty_str)
if not options.ignore_repo_diff and util.has_uncommitted_changes():
error_message = 'THERE ARE UNCOMMITTED CHANGES (TIP: repo status).' \
'Use --ignore-repo-diff to skip this check.'
if not util.is_interactive_shell():
sys.exit(error_message)
response = input(f'{error_message}\nContinue?[Y/n]')
if response.upper() != 'Y':
sys.exit(1)
log_dir = Path(options.log_dir).resolve()
if not options.append_csv and log_dir.exists():
error_message = f'{log_dir} already exists. ' \
'Use --append-csv to skip this check.'
if not util.is_interactive_shell():
sys.exit(error_message)
response = input(f'{error_message}\nContinue?[Y/n]')
if response.upper() != 'Y':
sys.exit(1)
if log_dir.is_relative_to(util.get_top_dir()):
sys.exit(f" choose a log_dir outside the source tree; "
f"{options.log_dir}' resolves to {log_dir}")
if options.ci_mode:
if len(chosen_cujgroups) > 1:
sys.exit('CI mode can only allow one cuj group. '
'Remove --ci-mode flag to skip this check.')
if len(build_types) > 1:
sys.exit('CI mode can only allow one build type. '
'Remove --ci-mode flag to skip this check.')
return UserInput(
build_types=build_types,
chosen_cujgroups=chosen_cujgroups,
description=options.description,
log_dir=Path(options.log_dir).resolve(),
targets=options.targets,
ci_mode=options.ci_mode)