| # Copyright 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 json |
| import os |
| import subprocess |
| import shutil |
| import re |
| import requests |
| import zipfile |
| import sys |
| |
| FETCH_ARTIFACT = "/google/data/ro/projects/android/fetch_artifact" |
| BASS = "/google/data/ro/projects/android/bass" |
| DEFAULT_CLONE_DEPTH = 100 # chosen arbitrarily, may need to be adjusted |
| DEFAULT_BUILD_ARTIFACT_SEARCH_TIME_SPAN_IN_DAYS = 7 # chosen arbitrarily, may need to be adjusted |
| GIT_LOG_URL = "https://android.googlesource.com/platform/frameworks/support/+log" |
| IGNORE_PATHS = [ |
| # includes timestamps |
| "*.xml", |
| # are different because the xml files include timestamps |
| "*.xml.*", |
| "*.sha*", |
| "*.md5", |
| # is different because it references the .sha* files. |
| "*.module" |
| ] |
| |
| |
| def main(build_id): |
| """ |
| This is a script to take a given build_id, and search for a build of the previous commit on ab/ |
| If a commit is found, we download the top-of-tree-m2repository-all-{build_id}.zip file from both |
| builds and diff the contents. If an .aar / .jar is different, we will unzip those and diff the |
| contents as well. |
| """ |
| staging_dir = prep_staging_dir() |
| build_info_file_path = fetch_build_info(build_id, staging_dir) |
| |
| # presubmit BUILD_INFO files include the commit being built as well as the and parent commit |
| if is_presubmit_build(build_id): |
| previous_revision = get_previous_revision_from_build_info(build_info_file_path) |
| # other builds only include the commit being built (as far as I can tell), we need to |
| # get the previous commit from git log |
| else: |
| current_revision = get_current_revision(build_info_file_path) |
| previous_revision = get_previous_revision_from_git_history(current_revision, staging_dir) |
| previous_build_id = get_previous_build_id(previous_revision) |
| (before, after) = download_and_unzip_repos(staging_dir, build_id, previous_build_id) |
| diff_repos(before, after, staging_dir) |
| |
| def prep_staging_dir(): |
| """ |
| remove and recreate the ./download_staging which is located as a sibling of this script. |
| """ |
| current_dir = os.path.dirname(os.path.realpath(__file__)) |
| staging_dir = current_dir + "/download_staging" |
| if os.path.isdir(staging_dir): |
| shutil.rmtree(staging_dir) |
| os.makedirs(staging_dir, exist_ok=True) |
| return staging_dir |
| |
| |
| def fetch_m2repo(build_id, staging_dir): |
| file_path = f"top-of-tree-m2repository-all-{build_id}.zip" |
| if is_presubmit_build(build_id): |
| file_path = f"incremental/{file_path}" |
| return fetch_artifact(build_id, staging_dir, file_path) |
| |
| |
| def fetch_build_info(build_id, staging_dir): |
| return fetch_artifact(build_id, staging_dir, "BUILD_INFO") |
| |
| |
| def fetch_artifact(build_id, output_dir, file_path): |
| file_name = file_path.split("/")[-1] |
| print(f"fetching {file_name}") |
| |
| if is_presubmit_build(build_id): |
| target = "androidx_incremental" |
| else: |
| target = "androidx" |
| return FetchArtifactService().fetch_artifact(build_id, "aosp-androidx-main", target, output_dir, file_path) |
| |
| |
| def get_current_revision(build_info_file_path): |
| print("Getting current revision from BUILD_INFO") |
| with open(build_info_file_path) as f: |
| build_info = json.load(f) |
| support_project = next( |
| project for project in build_info["parsed_manifest"]["projects"] if |
| project["name"] == "platform/frameworks/support") |
| current_revision = support_project["revision"] |
| print(f"Found revision: {current_revision}") |
| return current_revision |
| |
| |
| def get_previous_revision_from_build_info(build_info_file_path): |
| print("Getting previous revision from BUILD_INFO") |
| with open(build_info_file_path) as f: |
| build_info = json.load(f) |
| revision = build_info["git-pull"][0]["revisions"][0]["commit"]["parents"][0]["commitId"] |
| print(f"Found previous revision: {revision}") |
| return revision |
| |
| def get_previous_revision_from_git_history(current_revision, staging_dir): |
| """ |
| Gets previous revision from git log endpoint for androidx-main. |
| """ |
| response = requests.get(git_log_url(current_revision)) |
| # endpoint returns some junk in the first line making it invalid json |
| text_with_first_line_removed = "\n".join(response.text.split("\n")[1:]) |
| response_json = json.loads(text_with_first_line_removed) |
| previous_revision = response_json["log"][0]["parents"][0] |
| print(f"Found previous revision: {previous_revision}") |
| return previous_revision |
| |
| |
| def get_previous_build_id(previous_revision): |
| print("Searching Android Build server for build matching previous revision") |
| output = BassService().search_builds( |
| DEFAULT_BUILD_ARTIFACT_SEARCH_TIME_SPAN_IN_DAYS, |
| "aosp-androidx-main", |
| "androidx", |
| "BUILD_INFO", |
| previous_revision |
| ) |
| match = re.search("BuildID\: (\d+)", output.stdout) |
| if match is None: |
| raise Exception(f"Couldn't find previous build ID for revision {previous_revision}") |
| |
| previous_build_id = match.group(1) |
| print(f"Found build matching previous revision: {previous_build_id}") |
| return previous_build_id |
| |
| def download_and_unzip_repos(staging_dir, build_id, previous_build_id): |
| before_dir = staging_dir + "/before" |
| after_dir = staging_dir + "/after" |
| os.makedirs(before_dir) |
| os.makedirs(after_dir) |
| after_zip = fetch_m2repo(build_id, staging_dir) |
| before_zip = fetch_m2repo(previous_build_id, staging_dir) |
| return (unzip(before_zip, before_dir), unzip(after_zip, after_dir)) |
| |
| def diff_repos(before, after, staging_dir): |
| output = DiffService().diff(before, after, IGNORE_PATHS) |
| for line in output.stdout.splitlines(): |
| if line.startswith("Binary files "): |
| for (before_file, after_file) in re.findall("Binary files (.+) and (.+) differ", line): |
| diff_binary(before_file, after_file, staging_dir) |
| else: |
| print(line) |
| |
| |
| def diff_binary(before, after, staging_dir): |
| file_name = before.split("/")[-1] |
| if is_unzippable(before) and is_unzippable(after): |
| before_contents = unzip(before, staging_dir + "/" + file_name + "-before") |
| after_contents = unzip(after, staging_dir + "/" + file_name + "-after") |
| output = DiffService().diff(before_contents, after_contents) |
| # sometimes the binary is "different" but the contents are identical. |
| # It might be interesting to add diff the metadata, but for now just ignore it. |
| if output.stdout.strip() != "": |
| print(output.stdout) |
| else: |
| print(f"Binary files {before} and {after} differ") |
| |
| def is_unzippable(filename): |
| return filename.endswith(".zip") or filename.endswith(".aar") or filename.endswith(".jar") |
| |
| def unzip(file, destination): |
| with zipfile.ZipFile(file, 'r') as zip: |
| zip.extractall(destination) |
| return destination |
| |
| def is_presubmit_build(build_id): |
| return build_id.startswith("P") |
| |
| def git_log_url(revision): |
| return f"{GIT_LOG_URL}/{revision}?format=JSON" |
| |
| class DiffService(): |
| @staticmethod |
| def diff(before_dir, after_dir, exclude=[]): |
| args = ["diff", "-r"] |
| for pattern in exclude: |
| args.extend(["-x", pattern]) |
| args.extend([before_dir, after_dir]) |
| return subprocess.run(args, text=True, capture_output=True) |
| |
| |
| class FetchArtifactService(): |
| @staticmethod |
| def fetch_artifact(build_id, branch, target, output_dir, file_path): |
| file_name = file_path.split("/")[-1] |
| subprocess.run( |
| [ |
| FETCH_ARTIFACT, |
| "--bid", |
| build_id, |
| "--branch", |
| branch, |
| "--target", |
| target, |
| file_path, |
| ], |
| cwd=output_dir, |
| capture_output=True, |
| check=True |
| ) |
| return f"{output_dir}/{file_name}" |
| |
| class BassService(): |
| @staticmethod |
| def search_builds(days, branch, target, file_name, query): |
| return subprocess.run([ |
| BASS, |
| "--days", |
| str(days), |
| "--successful", |
| "true", |
| "--branch", |
| branch, |
| "--target", |
| target, |
| "--filename", |
| file_name, |
| "--query", |
| query |
| ], |
| capture_output=True, |
| text=True, |
| check=True |
| ) |
| |
| if __name__ == "__main__": |
| main(sys.argv[1]) |