| #!/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()) |