blob: b3796b92a63687fe2788951ba244ace15681e587 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2024 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Dev package bisect script.
This connects the `git bisect` tool (https://git-scm.com/docs/git-bisect)
with IREE's package builds, allowing developers to run tests through commit
history efficiently. For example, this can be used to spot at which commit
an `iree-compile` command started failing.
Requirements:
git (https://git-scm.com/)
gh (https://cli.github.com/)
Linux (at least until IREE builds packages for other systems at each commit)
Python 3.11
Example usage:
bisect_packages.py \
--good-ref=iree-3.0.0 \
--bad-ref=iree-3.1.0rc20241122 \
--test-script=bisect_example_timestamp.sh
bisect_packages.py \
--good-ref=iree-3.0.0 \
--bad-ref=main \
--test-command="iree-compile --iree-hal-target-device=local --iree-hal-local-target-device-backends=llvm-cpu -o /dev/null /tmp/repro.mlir"
"""
from pathlib import Path
import argparse
import os
import platform
import shutil
import subprocess
import sys
THIS_DIR = Path(__file__).parent.resolve()
REPO_ROOT = THIS_DIR.parent.parent.parent
def parse_arguments():
parser = argparse.ArgumentParser(description="Git release bisect tool")
# TODO(scotttodd): add --interactive mode that prompts like git bisect does
parser.add_argument(
"--good-ref",
help="The git ref (commit hash, branch name, tag name, etc.) at the lower end of the range",
required=True,
)
parser.add_argument(
"--bad-ref",
help="The git ref (commit hash, branch name, tag name, etc.) at the upper end of the range",
required=True,
)
parser.add_argument(
"--work-dir",
help="The working directory to use. Defaults to ~/.iree/bisect/",
default=Path.home() / ".iree" / "bisect",
type=Path,
)
test_group = parser.add_mutually_exclusive_group(required=True)
test_group.add_argument(
"--test-script",
help="Path to the bash script file to run at each commit (or use --test-command)",
)
test_group.add_argument(
"--test-command",
help="The bash command to run at each commit (or use --test-script)",
)
# TODO(scotttodd): choice between manual or script (`git bisect run`) to use
# note that a "manual" mode would need developers to
# run the venv setup code themselves
parser.add_argument(
"--ignore-system-requirements",
help="Ignores system requirements like Python 3.11 and tries to run even if they are not met.",
action="store_true",
default=False,
)
# TODO(scotttodd): --clean arg to `rm -rf` the workdir
# TODO(scotttodd): control over logging
# redirect stdout/stderr from test script separate files in the workdir?
return parser.parse_args()
def check_system_requirements(ignore_system_requirements):
print("")
system_check_okay = True
# Check for Linux.
print(
f" Current platform is '{platform.platform()}', platform.system is '{platform.system()}'."
)
if "Linux" not in platform.system():
print(" ERROR! platform.system must be 'Linux'.", file=sys.stderr)
system_check_okay = False
# Check for Python 3.11.
print("")
print(f" Current Python version is '{sys.version}'. This script requires 3.11.")
if sys.version_info[:2] == (3, 11):
python311_path = "python"
else:
python311_path = shutil.which("python3.11")
if python311_path:
print(f" Found python3.11 at '{python311_path}', using that instead.")
else:
print(
" ERROR! Could not find Python version 3.11. Python version must be 3.11 to match package builds.",
file=sys.stderr,
)
print(
" See `.github/workflows/pkgci_build_packages.yml` and `build_tools/pkgci/build_linux_packages.sh`.",
file=sys.stderr,
)
system_check_okay = False
# Check for 'gh'.
print("")
gh_path = shutil.which("gh")
if not gh_path:
print(
" ERROR! Could not find 'gh'. Install by following https://github.com/cli/cli#installation.",
file=sys.stderr,
)
system_check_okay = False
else:
print(f" Found gh at '{gh_path}'.")
if not system_check_okay:
print("")
if ignore_system_requirements:
print(
"One or more configuration issues detected, but --ignore-system-requirements is set. Continuing.",
file=sys.stderr,
)
return
print(
"One or more configuration issues detected. Fix the reported issues or pass --ignore-system-requirements to try running anyways. Exiting.",
file=sys.stderr,
)
print("")
print("------------------------------------------------------------------")
sys.exit(1)
return python311_path
def main(args):
print("Welcome to bisect_packages.py!")
print("")
print("------------------------------------------------------------------")
print("--------- Configuration ------------------------------------------")
print("------------------------------------------------------------------")
print("")
print(f" Searching range : '{args.good_ref}' - '{args.bad_ref}'")
print(f" Using working directory : '{args.work_dir}'")
Path.mkdir(args.work_dir, parents=True, exist_ok=True)
if args.test_script:
print(f" Using test script : '{args.test_script}'")
elif args.test_command:
print(f" Using test command : '{args.test_command}'")
python311_path = check_system_requirements(args.ignore_system_requirements)
print("")
print("------------------------------------------------------------------")
# Create new script in working directory that:
# * downloads the packages from the release and installs them
# * runs the original test script
bisect_run_script = args.work_dir / "bisect_run_script.sh"
with open(bisect_run_script, "w") as bisect_run_script_file:
contents = ""
contents += "#!/bin/bash\n"
contents += "\n"
contents += "#########################################\n"
contents += "###### BISECT RELEASE SCRIPT SETUP ######\n"
contents += "#########################################\n"
contents += "\n"
contents += "set -xeuo pipefail\n"
contents += "\n"
# Download packages for REF_HASH and install them into REF_HASH/.venv/.
contents += "REF_HASH=$(git rev-parse BISECT_HEAD)\n"
contents += f'"{python311_path}" '
contents += str((THIS_DIR / ".." / "setup_venv.py").as_posix())
contents += f" {args.work_dir}/"
contents += "${REF_HASH}/.venv"
contents += f" --artifact-path={args.work_dir}/"
contents += "${REF_HASH} "
contents += " --fetch-git-ref=${REF_HASH}\n"
# Prepend the venv bin dir to $PATH. This is similar to running
# `source .venv/bin/activate`
# while scoped to this process. Note that this does not modify
# $PYTHONHOME or support the `deactivate` command.
contents += f'PATH="{args.work_dir}/$REF_HASH/.venv/bin:$PATH"\n'
contents += "\n"
# Controlled failure - don't immediately exit. See below.
contents += "set +e\n"
contents += "\n"
contents += "#########################################\n"
contents += "############ ORIGINAL SCRIPT ############\n"
contents += "#########################################\n"
contents += "\n"
if args.test_script:
with open(args.test_script, "r") as original_script:
contents += original_script.read()
elif args.test_command:
contents += args.test_command + "\n"
contents += "\n"
contents += "#########################################\n"
contents += "##### BISECT RELEASE SCRIPT CLEANUP #####\n"
contents += "#########################################\n"
contents += "\n"
# Controlled failure, See `set +e` above.
# `git bisect` is looking for exit values in the 1-127 range, while
# iree-compile can exit with value 245 sometimes:
# https://git-scm.com/docs/git-bisect#_bisect_run. Here we just check
# for non-zero and normalize back to 1.
contents += "RET_VALUE=$?\n"
contents += "if [ $RET_VALUE -ne 0 ]; then\n"
contents += " exit 1\n"
contents += "fi\n"
bisect_run_script_file.write(contents)
os.chmod(str(bisect_run_script), 0o744) # Set as executable.
print("")
print("------------------------------------------------------------------")
print("--------- Running git bisect -------------------------------------")
print("------------------------------------------------------------------")
print("")
subprocess.check_call(["git", "bisect", "reset"], cwd=REPO_ROOT)
subprocess.check_call(
[
"git",
"bisect",
"start",
# Just update the BISECT_HEAD reference instead of checking out the
# ref for each iteration of the bisect process. We won't be building
# from source and this script lives in the source tree, so keep the
# repository in a stable state.
# Note: scripts can access the hash via `git rev-parse BISECT_HEAD`.
"--no-checkout",
# We only care about the merge/aggregate commit when branches were
# merged. Ignore ancestors of merge commits.
"--first-parent",
],
cwd=REPO_ROOT,
)
subprocess.check_call(["git", "bisect", "good", args.good_ref], cwd=REPO_ROOT)
subprocess.check_call(["git", "bisect", "bad", args.bad_ref], cwd=REPO_ROOT)
subprocess.check_call(
["git", "bisect", "run", str(bisect_run_script)], cwd=REPO_ROOT
)
print("")
if __name__ == "__main__":
main(parse_arguments())