| # 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) |