blob: 67d45188b66494885e74d26b0c6e10a3a4509fbd [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 pathlib import Path
from typing import Optional
import cuj_catalog
import util
from util import BuildType
@dataclasses.dataclass(frozen=True)
class UserInput:
build_types: tuple[BuildType, ...]
chosen_cujgroups: tuple[int, ...]
tag: Optional[str]
log_dir: Path
no_warmup: bool
targets: tuple[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]
logging.critical(
f"Invalid input: {input_str}. "
f"Expected an index between 1 and {len(cujgroups)}. "
"Try --help to view the list of available CUJs"
)
raise argparse.ArgumentTypeError()
else:
pattern = re.compile(input_str)
matching_cuj_groups = [
i
for i, cujgroup in enumerate(cujgroups)
if pattern.search(cujgroup.description)
]
if len(matching_cuj_groups):
return matching_cuj_groups
logging.critical(
f'Invalid input: "{input_str}" does not match any CUJ. '
"Try --help to view the list of available CUJs"
)
raise argparse.ArgumentTypeError()
# 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.description}" 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(
"--no-warmup",
default=False,
action="store_true",
help="skip warmup builds; this can skew your results for the first CUJ you run.",
)
p.add_argument(
"-t",
"--tag",
type=str,
default="",
help="Any additional tag for this set of builds, this helps "
"distinguish the new data from previously collected data, "
"useful for comparative analysis",
)
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_default_log_dir()
p.add_argument(
"-l",
"--log-dir",
type=Path,
default=default_log_dir,
help=textwrap.dedent(
"""\
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:
{}
3 To view column headers:
{}
"""
).format(
textwrap.indent(
util.get_cmd_to_display_tabulated_metrics(default_log_dir, False),
" " * 4,
),
textwrap.indent(util.get_csv_columns_cmd(default_log_dir), " " * 4),
),
)
def_build_types = [
BuildType.SOONG_ONLY,
]
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)
chosen_cujgroups: tuple[int, ...] = (
tuple(int(i) for sublist in options.cujs for i in sublist)
if options.cujs
else tuple()
)
bazel_labels: list[str] = [
target for target in options.targets if target.startswith("//")
]
if 0 < len(bazel_labels) < len(options.targets):
logging.critical(
f"Don't mix bazel labels {bazel_labels} with soong targets "
f"{[t for t in options.targets if t not in bazel_labels]}"
)
sys.exit(1)
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: tuple[BuildType, ...] = tuple(
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_BUILD 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].description}" 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():
logging.critical(error_message)
sys.exit(1)
logging.error(error_message)
response = input("Continue?[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."
"Consider --tag to your new runs"
)
if not util.is_interactive_shell():
logging.critical(error_message)
sys.exit(1)
logging.error(error_message)
response = input("Continue?[Y/n]")
if response.upper() != "Y":
sys.exit(1)
if log_dir.is_relative_to(util.get_top_dir()):
logging.critical(
f"choose a log_dir outside the source tree; "
f"'{options.log_dir}' resolves to {log_dir}"
)
sys.exit(1)
if options.ci_mode:
if len(chosen_cujgroups) > 1:
logging.critical(
"CI mode can only allow one cuj group. "
"Remove --ci-mode flag to skip this check."
)
sys.exit(1)
if len(build_types) > 1:
logging.critical(
"CI mode can only allow one build type. "
"Remove --ci-mode flag to skip this check."
)
sys.exit(1)
if options.no_warmup:
logging.warning(
"WARMUP runs will be skipped. Note this is not advised "
"as it gives unreliable results."
)
return UserInput(
build_types=build_types,
chosen_cujgroups=chosen_cujgroups,
tag=options.tag,
log_dir=Path(options.log_dir).resolve(),
no_warmup=options.no_warmup,
targets=options.targets,
ci_mode=options.ci_mode,
)