|  | #!/usr/bin/env python3 | 
|  | # Copyright 2019 Google LLC | 
|  | # | 
|  | # 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 | 
|  | # | 
|  | #      https://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. | 
|  |  | 
|  | # pylint: disable=missing-docstring | 
|  | """Updates LLVM-dependent submodules based on the current LLVM commit. | 
|  |  | 
|  | Updates the third_party/llvm-bazel, third_party/tensorflow, and | 
|  | third_party/mlir-hlo submodules to commits that match the LLVM commit in the | 
|  | third_party/llvm-project submodule. We have special conditions around these | 
|  | submodules since they are synced as part of the integration of LLVM into | 
|  | Google's source repository. See | 
|  | https://google.github.io/iree/developing-iree/repository-management#the-special-relationship-with-llvm-and-tensorflow. | 
|  |  | 
|  | Typical usage: | 
|  | Syntax: ./scripts/git/update_to_llvm_syncpoint.py | 
|  |  | 
|  | By default, this will update llvm-bazel to the tag corresponding to the | 
|  | current LLVM commit and update TensorFlow and MLIR-HLO to the most recent | 
|  | commit that has a matching LLVM commit. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import re | 
|  | import os | 
|  | import sys | 
|  |  | 
|  | import submodule_versions | 
|  | import utils | 
|  |  | 
|  | REMOTE_HEAD_COMMIT_OPTION = "REMOTE" | 
|  | KEEP_COMMIT_OPTION = "KEEP" | 
|  | INTEGRATE_COMMIT_OPTION = "INTEGRATE" | 
|  | LATEST_MATCHING_COMMIT_OPTION = "LATEST_MATCH" | 
|  |  | 
|  | COMMIT_OPTIONS = { | 
|  | REMOTE_HEAD_COMMIT_OPTION: | 
|  | "Update to the HEAD commit on the remote repository default branch", | 
|  | KEEP_COMMIT_OPTION: | 
|  | "Do not modify the current commit", | 
|  | INTEGRATE_COMMIT_OPTION: | 
|  | "Update to the commit where the current version of LLVM was first " | 
|  | "integrated", | 
|  | LATEST_MATCHING_COMMIT_OPTION: | 
|  | "Update to the most recent commit with a matching version of LLVM", | 
|  | } | 
|  |  | 
|  | TF_LLVM_WORKSPACE_FILEPATH = "third_party/llvm/workspace.bzl" | 
|  | TF_WORKSPACE_LLVM_COMMIT_REGEXP = re.compile( | 
|  | r"""\s*LLVM_COMMIT\s*=\s*"(.+)"\s*""", flags=re.MULTILINE) | 
|  | MLIR_HLO_LLVM_VERSION_FILEPATH = "build_tools/llvm_version.txt" | 
|  |  | 
|  |  | 
|  | def parse_arguments(): | 
|  | parser = argparse.ArgumentParser() | 
|  | parser.add_argument("--repo", help="Repository root directory") | 
|  | parser.add_argument("--llvm_path", | 
|  | help="Path to the LLVM sources " | 
|  | "(defaults to third_party/llvm-project)", | 
|  | default=None) | 
|  | parser.add_argument("--llvm_bazel_path", | 
|  | help="Path to the LLVM Bazel BUILD files" | 
|  | "(defaults to third_party/llvm-bazel)", | 
|  | default=None) | 
|  | parser.add_argument( | 
|  | "--llvm_bazel_rev", | 
|  | "--llvm_bazel_commit", | 
|  | help=("Update llvm-bazel to this git rev, or a named option:" | 
|  | f" {COMMIT_OPTIONS}." | 
|  | f" {LATEST_MATCHING_COMMIT_OPTION} and {INTEGRATE_COMMIT_OPTION}" | 
|  | " are equivalentfor this repository."), | 
|  | default=LATEST_MATCHING_COMMIT_OPTION) | 
|  | parser.add_argument("--tensorflow_path", | 
|  | help="Path to the tensorflow sources " | 
|  | "(default to third_party/tensorflow)", | 
|  | default=None) | 
|  | parser.add_argument("--tensorflow_rev", | 
|  | "--tf_rev", | 
|  | "--tensorflow_commit", | 
|  | "--tf_commit", | 
|  | help=("Update TensorFlow to this rev, or a named option:" | 
|  | f" {COMMIT_OPTIONS}"), | 
|  | default=LATEST_MATCHING_COMMIT_OPTION) | 
|  | parser.add_argument("--mlir_hlo_path", | 
|  | help="Path to the tensorflow sources " | 
|  | "(default to third_party/tensorflow)", | 
|  | default=None) | 
|  | parser.add_argument("--mlir_hlo_rev", | 
|  | "--mlir_hlo_commit", | 
|  | help=("Update mlir-hlo to this rev, or a named option:" | 
|  | f" {COMMIT_OPTIONS}"), | 
|  | default=LATEST_MATCHING_COMMIT_OPTION) | 
|  | parser.add_argument( | 
|  | "--validate", | 
|  | help="Validate that the selected commits all match the LLVM commit", | 
|  | type=utils.str2bool, | 
|  | nargs="?", | 
|  | default=True, | 
|  | ) | 
|  |  | 
|  | args = parser.parse_args() | 
|  |  | 
|  | # Default repo path. | 
|  | if args.repo is None: | 
|  | args.repo = utils.find_git_toplevel() | 
|  |  | 
|  | # Set some defaults. | 
|  | if not args.llvm_path: | 
|  | args.llvm_path = os.path.join(args.repo, "third_party", "llvm-project") | 
|  | if not args.llvm_bazel_path: | 
|  | args.llvm_bazel_path = os.path.join(args.repo, "third_party", "llvm-bazel") | 
|  | if not args.tensorflow_path: | 
|  | args.tensorflow_path = os.path.join(args.repo, "third_party", "tensorflow") | 
|  | if not args.mlir_hlo_path: | 
|  | args.mlir_hlo_path = os.path.join(args.repo, "third_party", "mlir-hlo") | 
|  |  | 
|  | return args | 
|  |  | 
|  |  | 
|  | def main(args): | 
|  | print("IREE handy-dandy-LLVM-submodule-updater at your service...") | 
|  | print(f"  IREE Path: {args.repo}") | 
|  | print(f"  LLVM Path: {args.llvm_path}") | 
|  | print(f"  LLVM Bazel Path: {args.llvm_bazel_path}") | 
|  | print(f"  TensorFlow Path: {args.tensorflow_path}") | 
|  | print(f"  MLIR-HLO Path: {args.tensorflow_path}") | 
|  |  | 
|  | current_llvm_commit = parse_rev(args.llvm_path, "HEAD") | 
|  | current_llvm_bazel_commit = parse_rev(args.llvm_bazel_path, "HEAD") | 
|  | current_tf_commit = parse_rev(args.tensorflow_path, "HEAD") | 
|  | current_mlir_hlo_commit = parse_rev(args.mlir_hlo_path, "HEAD") | 
|  | print("Current Commits:") | 
|  | print(f"  llvm = {current_llvm_commit}") | 
|  | print(f"  llvm_bazel = {current_llvm_bazel_commit}") | 
|  | print(f"  tensorflow = {current_tf_commit}") | 
|  | print(f"  mlir-hlo = {current_mlir_hlo_commit}") | 
|  |  | 
|  | # Update LLVM-Bazel | 
|  | new_llvm_bazel_commit = find_new_llvm_bazel_commit(args.llvm_bazel_path, | 
|  | current_llvm_commit, | 
|  | args.llvm_bazel_rev) | 
|  | print(f"\n*** Updating LLVM Bazel to {new_llvm_bazel_commit} ***") | 
|  | utils.execute(["git", "checkout", new_llvm_bazel_commit], | 
|  | cwd=args.llvm_bazel_path) | 
|  | stage_path(args.repo, args.llvm_bazel_path) | 
|  |  | 
|  | validate_llvm_bazel_commit(current_llvm_commit, | 
|  | args.llvm_bazel_path, | 
|  | exit_on_failure=args.validate) | 
|  |  | 
|  | # Update TensorFlow | 
|  | new_tf_commit = find_new_commit_from_version_file(args.tensorflow_path, | 
|  | TF_LLVM_WORKSPACE_FILEPATH, | 
|  | current_llvm_commit, | 
|  | args.tensorflow_rev) | 
|  | print("\n*** Updating TensorFlow to", new_tf_commit, "***") | 
|  | utils.execute(["git", "checkout", new_tf_commit], cwd=args.tensorflow_path) | 
|  | stage_path(args.repo, args.tensorflow_path) | 
|  |  | 
|  | validate_tf_commit(current_llvm_commit, | 
|  | args.tensorflow_path, | 
|  | exit_on_failure=args.validate) | 
|  |  | 
|  | # Update MLIR-HLO | 
|  | new_mlir_hlo_commit = find_new_commit_from_version_file( | 
|  | args.mlir_hlo_path, MLIR_HLO_LLVM_VERSION_FILEPATH, current_llvm_commit, | 
|  | args.mlir_hlo_rev) | 
|  | print("\n*** Updating MLIR-HLO to", new_mlir_hlo_commit, "***") | 
|  | utils.execute(["git", "checkout", new_mlir_hlo_commit], | 
|  | cwd=args.mlir_hlo_path) | 
|  | stage_path(args.repo, args.mlir_hlo_path) | 
|  |  | 
|  | validate_mlir_hlo_commit(current_llvm_commit, | 
|  | args.mlir_hlo_path, | 
|  | exit_on_failure=args.validate) | 
|  |  | 
|  | # Export SUBMODULE_VERSIONS.txt. | 
|  | print()  # Add line break. | 
|  | submodule_versions.export_versions(args.repo) | 
|  |  | 
|  |  | 
|  | def parse_rev(path, rev): | 
|  | return utils.execute(["git", "rev-parse", rev], | 
|  | cwd=path, | 
|  | silent=True, | 
|  | capture_output=True).stdout.strip() | 
|  |  | 
|  |  | 
|  | def find_new_llvm_bazel_commit(llvm_bazel_path, llvm_commit, llvm_bazel_rev): | 
|  | # Explicitly force-fetch tags. Tags in llvm-bazel are not guaranteed to be | 
|  | # stable. | 
|  | utils.execute(["git", "fetch", "--tags", "--force"], cwd=llvm_bazel_path) | 
|  |  | 
|  | if llvm_bazel_rev not in COMMIT_OPTIONS: | 
|  | return parse_rev(llvm_bazel_path, llvm_bazel_rev) | 
|  |  | 
|  | if llvm_bazel_rev == KEEP_COMMIT_OPTION: | 
|  | return parse_rev(llvm_bazel_path, "HEAD") | 
|  |  | 
|  | if llvm_bazel_rev == REMOTE_HEAD_COMMIT_OPTION: | 
|  | return parse_rev(llvm_bazel_path, "origin/main") | 
|  |  | 
|  | if (llvm_bazel_rev == INTEGRATE_COMMIT_OPTION or | 
|  | llvm_bazel_rev == LATEST_MATCHING_COMMIT_OPTION): | 
|  | return parse_rev(llvm_bazel_path, f"llvm-project-{llvm_commit}") | 
|  |  | 
|  |  | 
|  | def validate_llvm_bazel_commit(llvm_commit, | 
|  | llvm_bazel_path, | 
|  | exit_on_failure=True): | 
|  | llvm_bazel_llvm_commit = find_llvm_bazel_llvm_commit(llvm_bazel_path) | 
|  |  | 
|  | matches = llvm_bazel_llvm_commit == llvm_commit | 
|  | if not matches: | 
|  | print("WARNING: LLVM commit in llvm-bazel does not match that in IREE" | 
|  | f" ({llvm_bazel_llvm_commit} vs {llvm_commit})") | 
|  | if exit_on_failure: | 
|  | sys.exit(1) | 
|  |  | 
|  |  | 
|  | def find_llvm_bazel_llvm_commit(llvm_bazel_path): | 
|  | return utils.execute( | 
|  | ["git", "submodule", "status", "third_party/llvm-project"], | 
|  | capture_output=True, | 
|  | cwd=llvm_bazel_path).stdout.split()[0].lstrip("+-") | 
|  |  | 
|  |  | 
|  | def find_llvm_commit_changes_to_file(repo_path, | 
|  | filepath, | 
|  | llvm_commit, | 
|  | branch="origin/master"): | 
|  | """Finds commits where the occurrence of the given LLVM commit hash changes. | 
|  |  | 
|  | In normal cases, there should be at most two commits that match this: | 
|  | 1. The commit that first introduced the new hash in the file. | 
|  | 2. The commit that changed it to a new hash afterwards. | 
|  | """ | 
|  | commits = utils.execute( | 
|  | [ | 
|  | "git", | 
|  | "log", | 
|  | # Only follow the first parent of a merge commit. We don't want to go | 
|  | # off to some random PR. | 
|  | "--first-parent", | 
|  | # Just print the commit hash | 
|  | "--format=%H", | 
|  | # Look for commits where the number of occurrences of llvm_commit | 
|  | # changed. | 
|  | # https://git-scm.com/docs/git-log#Documentation/git-log.txt--Sltstringgt | 
|  | "-S", | 
|  | llvm_commit, | 
|  | # Search along the appropriate branch | 
|  | branch, | 
|  | # Only look in the specified file where the llvm_commit is recorded | 
|  | "--", | 
|  | filepath, | 
|  | ], | 
|  | capture_output=True, | 
|  | cwd=repo_path).stdout.split() | 
|  |  | 
|  | if len(commits) > 2: | 
|  | raise RuntimeError( | 
|  | f"Expected one or two commits in {repo_path} {branch} {filepath} to " | 
|  | f" involve LLVM commit {llvm_commit}, but got {len(commits)}") | 
|  |  | 
|  | if not commits: | 
|  | raise RuntimeError( | 
|  | f"{repo_path} {branch} {filepath} does not have any references to LLVM" | 
|  | f" commit {llvm_commit}. Maybe  export is behind?") | 
|  |  | 
|  | return commits | 
|  |  | 
|  |  | 
|  | def find_new_commit_from_version_file(repo_path, | 
|  | version_filepath, | 
|  | llvm_commit, | 
|  | rev, | 
|  | branch="origin/master"): | 
|  | utils.execute(["git", "fetch"], cwd=repo_path) | 
|  |  | 
|  | if rev not in COMMIT_OPTIONS: | 
|  | return parse_rev(repo_path, rev) | 
|  |  | 
|  | if rev == KEEP_COMMIT_OPTION: | 
|  | return parse_rev(repo_path, "HEAD") | 
|  |  | 
|  | if rev == REMOTE_HEAD_COMMIT_OPTION: | 
|  | return parse_rev(repo_path, branch) | 
|  |  | 
|  | commit_options = find_llvm_commit_changes_to_file(repo_path, version_filepath, | 
|  | llvm_commit) | 
|  |  | 
|  | if rev == INTEGRATE_COMMIT_OPTION: | 
|  | return commit_options[-1] | 
|  |  | 
|  | assert rev == LATEST_MATCHING_COMMIT_OPTION | 
|  | if len(commit_options) == 1: | 
|  | # There hasn't been a subsequent integrate, use remote head. | 
|  | return parse_rev(repo_path, branch) | 
|  |  | 
|  | # Use the commit one before the one that changed away from this LLVM version. | 
|  | return parse_rev(repo_path, f"{commit_options[0]}^") | 
|  |  | 
|  |  | 
|  | def validate_tf_commit(llvm_commit, tensorflow_path, exit_on_failure=True): | 
|  | tf_llvm_commit = find_tensorflow_llvm_commit(tensorflow_path) | 
|  |  | 
|  | matches = tf_llvm_commit == llvm_commit | 
|  | if not matches: | 
|  | print("WARNING: LLVM commit in TF  does not match that in IREE" | 
|  | f" ({tf_llvm_commit} vs {llvm_commit})") | 
|  | if exit_on_failure: | 
|  | sys.exit(1) | 
|  |  | 
|  |  | 
|  | def find_tensorflow_llvm_commit(tensorflow_path): | 
|  | # TensorFlow keeps its commit in workspace.bzl on a line like: | 
|  | # LLVM_COMMIT = "..." | 
|  | # Yeah. This is how we do it. | 
|  | workspace_path = os.path.join(tensorflow_path, TF_LLVM_WORKSPACE_FILEPATH) | 
|  |  | 
|  | for line in open(workspace_path, "r", encoding="UTF-8"): | 
|  | m = re.match(TF_WORKSPACE_LLVM_COMMIT_REGEXP, line) | 
|  | if m: | 
|  | return m.group(1) | 
|  |  | 
|  | print(f"ERROR: Could not find LLVM commit in {workspace_path}.") | 
|  | print("Please file a bug)") | 
|  | print("Expected pattern match for:", TF_WORKSPACE_LLVM_COMMIT_REGEXP.pattern) | 
|  | sys.exit(1) | 
|  |  | 
|  |  | 
|  | def validate_mlir_hlo_commit(llvm_commit, mlir_hlo_path, exit_on_failure=True): | 
|  | mlir_hlo_llvm_commit = find_mlir_hlo_llvm_commit(mlir_hlo_path) | 
|  |  | 
|  | matches = mlir_hlo_llvm_commit == llvm_commit | 
|  | if not matches: | 
|  | print("WARNING: LLVM commit in mlir-hlo does not match that in IREE" | 
|  | f" ({mlir_hlo_llvm_commit} vs {llvm_commit})") | 
|  | if exit_on_failure: | 
|  | sys.exit(1) | 
|  |  | 
|  |  | 
|  | def find_mlir_hlo_llvm_commit(mlir_hlo_path): | 
|  | llvm_version_file_path = os.path.join(mlir_hlo_path, | 
|  | MLIR_HLO_LLVM_VERSION_FILEPATH) | 
|  | with open(llvm_version_file_path, "r", encoding="UTF-8") as f: | 
|  | return f.read().strip() | 
|  |  | 
|  |  | 
|  | def stage_path(repo_path, to_stage): | 
|  | # TODO(laurenzo): Move to utils.py. | 
|  | utils.execute(["git", "add", to_stage], cwd=repo_path) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main(parse_arguments()) |