blob: cd3a9f1fdfff689090e37273df5f249f1d3925f5 [file] [log] [blame]
#!/usr/bin/env python3
# Lint as: python3
# Copyright 2019 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
# pylint: disable=missing-docstring
"""submodule_versions.
Synchronizes the tracked SUBMODULE_VERSIONS.txt file with the submodule state
in git.
Typical usage:
--------------
Exporting current git submodule state to SUBMODULE_VERSIONS.txt:
Syntax: ./scripts/git/submodule_versions.py export
Importing versions in SUBMODULE_VERSIONS.txt to git submodule state:
Syntax: ./scripts/git/submodule_versions.py import
Checking whether SUBMODULE_VERSIONS.txt and git state are in sync:
Syntax: ./scripts/git/submodule_versions.py check
"""
import argparse
import os
import re
import sys
import utils
VERSIONS_FILE = "SUBMODULE_VERSIONS.txt"
def get_submodule_versions(repo_dir):
raw_status = utils.execute(["git", "submodule", "status"],
cwd=repo_dir,
silent=True,
capture_output=True).stdout
status_lines = []
for line in raw_status.splitlines():
# Format is a status char followed by revision, space and path.
m = re.match(r"""^.([0-9a-z]+)\s+([^\s]+)""", line)
if m:
# Output as just the commit hash followed by space and path.
status_lines.append(m.group(1) + " " + m.group(2))
return "\n".join(status_lines) + "\n"
def export_versions(repo_dir):
current_versions = get_submodule_versions(repo_dir)
versions_file_path = os.path.join(repo_dir, VERSIONS_FILE)
print("*** Exporting current submodule versions to:", versions_file_path)
with open(versions_file_path, "w", encoding="UTF-8") as f:
f.write(current_versions)
utils.execute(["git", "add", VERSIONS_FILE], cwd=repo_dir)
def parse_versions(versions_text):
versions = dict()
for line in versions_text.splitlines():
comps = line.split(" ", maxsplit=2)
if len(comps) != 2:
continue
versions[comps[1]] = comps[0]
return versions
def get_diff_versions(repo_dir):
current_versions = parse_versions(get_submodule_versions(repo_dir))
with open(os.path.join(repo_dir, VERSIONS_FILE), "r", encoding="UTF-8") as f:
written_versions = parse_versions(f.read())
diff_versions = current_versions.items() ^ written_versions.items()
return {
k: (current_versions.get(k), written_versions.get(k))
for k, _ in diff_versions
}
def sync_and_update_submodules(repo_dir):
print("*** Synchronizing/updating submodules")
utils.execute(["git", "submodule", "sync"], cwd=repo_dir)
utils.execute(["git", "submodule", "update"], cwd=repo_dir)
def import_versions(repo_dir):
print("*** Importing versions to git submodule state")
diff_versions = get_diff_versions(repo_dir)
if not diff_versions:
print("*** No submodule updates required")
return
for path, (current, written) in diff_versions.items():
if current is None:
print(("Warning: Submodule %s does not exist but is "
"still in the version file") % (path,))
continue
if written is None:
print(f"Warning: Submodule '{current}' is not in the version file")
continue
# Directly update the submodule commit hash in the index.
# See: https://stackoverflow.com/questions/33514642
command = ["git", "update-index", "--cacheinfo", "160000", written, path]
print("Updating", path, "to", written)
utils.execute(command, cwd=repo_dir)
def init_submodules(repo_dir):
print("*** Initializing submodules")
utils.execute(["git", "submodule", "init"], cwd=repo_dir)
def parallel_shallow_update_submodules(repo_dir):
print("*** Making shallow clone of submodules")
utils.execute(["git", "submodule", "update", "--jobs", "8", "--depth", "1"],
cwd=repo_dir)
def check_submodule_versions(repo_dir):
diff_versions = get_diff_versions(repo_dir)
if diff_versions:
print("Submodule state differs from SUBMODULE_VERSIONS.txt file."
" Run (and commit) one of:")
print(" ./scripts/git/submodule_versions.py import"
" # Use version in SUBMODULE_VERSIONS.txt ('written')")
print(" ./scripts/git/submodule_versions.py export"
" # Use version in git state ('actual')")
for k, (current, written) in diff_versions.items():
print(f"{k} : actual={current} written={written}")
return False
return True
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("--repo", help="Repository root directory")
parser.add_argument("command",
help="Command to run (show|import|export|check|init)")
args = parser.parse_args()
# Default repo path.
if args.repo is None:
args.repo = utils.find_git_toplevel()
return args
def main(args):
if args.command == "show":
print(get_submodule_versions(args.repo))
elif args.command == "export":
sync_and_update_submodules(args.repo)
export_versions(args.repo)
elif args.command == "check":
if not check_submodule_versions(args.repo):
sys.exit(1)
elif args.command == "import":
import_versions(args.repo)
sync_and_update_submodules(args.repo)
elif args.command == "init":
init_submodules(args.repo)
# Redundant, since import_versions will only update if they differ,
# but good to only print output about the import if it's actually
# needed.
if not check_submodule_versions(args.repo):
print(
"Warning: git submodule state does not match SUBMODULE_VERSIONS.txt. "
"Using state in SUBMODULE_VERSIONS.txt")
import_versions(args.repo)
parallel_shallow_update_submodules(args.repo)
else:
print("Unrecognized command:", args.command)
sys.exit(1)
if __name__ == "__main__":
main(parse_arguments())