123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- #!/usr/bin/env python3
- #
- # Licensed to the Apache Software Foundation (ASF) under one
- # or more contributor license agreements. See the NOTICE file
- # distributed with this work for additional information
- # regarding copyright ownership. The ASF licenses this file
- # to you 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.
- # Script which checks Java API compatibility between two revisions of the
- # Java client.
- #
- # Originally sourced from Apache Kudu, which was based on the
- # compatibility checker from the Apache HBase project, but ported to
- # Python for better readability.
- import logging
- import os
- import re
- import shutil
- import subprocess
- import sys
- import urllib.request
- import argparse
- # Various relative paths
- REPO_DIR = os.getcwd()
- def check_output(*popenargs, **kwargs):
- """ Run command with arguments and return its output as a string. """
- return subprocess.check_output(*popenargs, **kwargs, encoding='utf-8')
- def get_repo_dir():
- """ Return the path to the top of the repo. """
- dirname, _ = os.path.split(os.path.abspath(__file__))
- return os.path.join(dirname, "../..")
- def get_scratch_dir():
- """ Return the path to the scratch dir that we build within. """
- scratch_dir = os.path.join(get_repo_dir(), "target", "compat-check")
- if not os.path.exists(scratch_dir):
- os.makedirs(scratch_dir)
- return scratch_dir
- def get_java_acc_dir():
- """ Return the path where we check out the Java API Compliance Checker. """
- return os.path.join(get_repo_dir(), "target", "java-acc")
- def clean_scratch_dir(scratch_dir):
- """ Clean up and re-create the scratch directory. """
- if os.path.exists(scratch_dir):
- logging.info("Removing scratch dir %s...", scratch_dir)
- shutil.rmtree(scratch_dir)
- logging.info("Creating empty scratch dir %s...", scratch_dir)
- os.makedirs(scratch_dir)
- def checkout_java_tree(rev, path):
- """ Check out the Java source tree for the given revision into
- the given path. """
- logging.info("Checking out %s in %s", rev, path)
- os.makedirs(path)
- # Extract java source
- subprocess.check_call(["bash", '-o', 'pipefail', "-c",
- ("git archive --format=tar %s | " +
- "tar -C \"%s\" -xf -") % (rev, path)],
- cwd=get_repo_dir())
- def get_git_hash(revname):
- """ Convert 'revname' to its SHA-1 hash. """
- return check_output(["git", "rev-parse", revname],
- cwd=get_repo_dir()).strip()
- def get_repo_name():
- """Get the name of the repo based on the git remote."""
- remotes = check_output(["git", "remote", "-v"],
- cwd=get_repo_dir()).strip().split("\n")
- # Example output:
- # origin https://github.com/apache/hadoop.git (fetch)
- # origin https://github.com/apache/hadoop.git (push)
- remote_url = remotes[0].split("\t")[1].split(" ")[0]
- remote = remote_url.split("/")[-1]
- if remote.endswith(".git"):
- remote = remote[:-4]
- return remote
- def build_tree(java_path):
- """ Run the Java build within 'path'. """
- logging.info("Building in %s...", java_path)
- subprocess.check_call(["mvn", "-DskipTests", "-Dmaven.javadoc.skip=true",
- "package"],
- cwd=java_path)
- def checkout_java_acc(force):
- """
- Check out the Java API Compliance Checker. If 'force' is true, will
- re-download even if the directory exists.
- """
- acc_dir = get_java_acc_dir()
- if os.path.exists(acc_dir):
- logging.info("Java ACC is already downloaded.")
- if not force:
- return
- logging.info("Forcing re-download.")
- shutil.rmtree(acc_dir)
- logging.info("Downloading Java ACC...")
- url = "https://github.com/lvc/japi-compliance-checker/archive/1.8.tar.gz"
- scratch_dir = get_scratch_dir()
- path = os.path.join(scratch_dir, os.path.basename(url))
- jacc = urllib.request.urlopen(url)
- with open(path, 'wb') as w:
- w.write(jacc.read())
- subprocess.check_call(["tar", "xzf", path],
- cwd=scratch_dir)
- shutil.move(os.path.join(scratch_dir, "japi-compliance-checker-1.8"),
- os.path.join(acc_dir))
- def find_jars(path):
- """ Return a list of jars within 'path' to be checked for compatibility. """
- all_jars = set(check_output(["find", path, "-name", "*.jar"]).splitlines())
- return [j for j in all_jars if (
- "-tests" not in j and
- "-sources" not in j and
- "-with-dependencies" not in j)]
- def write_xml_file(path, version, jars):
- """Write the XML manifest file for JACC."""
- with open(path, "wt") as f:
- f.write("<version>" + version + "</version>\n")
- f.write("<archives>")
- for j in jars:
- f.write(j + "\n")
- f.write("</archives>")
- def run_java_acc(src_name, src_jars, dst_name, dst_jars, annotations):
- """ Run the compliance checker to compare 'src' and 'dst'. """
- logging.info("Will check compatibility between original jars:\n\t%s\n" +
- "and new jars:\n\t%s",
- "\n\t".join(src_jars),
- "\n\t".join(dst_jars))
- java_acc_path = os.path.join(get_java_acc_dir(), "japi-compliance-checker.pl")
- src_xml_path = os.path.join(get_scratch_dir(), "src.xml")
- dst_xml_path = os.path.join(get_scratch_dir(), "dst.xml")
- write_xml_file(src_xml_path, src_name, src_jars)
- write_xml_file(dst_xml_path, dst_name, dst_jars)
- out_path = os.path.join(get_scratch_dir(), "report.html")
- args = ["perl", java_acc_path,
- "-l", get_repo_name(),
- "-d1", src_xml_path,
- "-d2", dst_xml_path,
- "-report-path", out_path]
- if annotations is not None:
- annotations_path = os.path.join(get_scratch_dir(), "annotations.txt")
- with open(annotations_path, "w") as f:
- for ann in annotations:
- print(ann, file=f)
- args += ["-annotations-list", annotations_path]
- subprocess.check_call(args)
- def filter_jars(jars, include_filters, exclude_filters):
- """Filter the list of JARs based on include and exclude filters."""
- filtered = []
- # Apply include filters
- for j in jars:
- found = False
- basename = os.path.basename(j)
- for f in include_filters:
- if f.match(basename):
- found = True
- break
- if found:
- filtered += [j]
- else:
- logging.debug("Ignoring JAR %s", j)
- # Apply exclude filters
- exclude_filtered = []
- for j in filtered:
- basename = os.path.basename(j)
- found = False
- for f in exclude_filters:
- if f.match(basename):
- found = True
- break
- if found:
- logging.debug("Ignoring JAR %s", j)
- else:
- exclude_filtered += [j]
- return exclude_filtered
- def main():
- """Main function."""
- logging.basicConfig(level=logging.INFO)
- parser = argparse.ArgumentParser(
- description="Run Java API Compliance Checker.")
- parser.add_argument("-f", "--force-download",
- action="store_true",
- help="Download dependencies (i.e. Java JAVA_ACC) " +
- "even if they are already present")
- parser.add_argument("-i", "--include-file",
- action="append",
- dest="include_files",
- help="Regex filter for JAR files to be included. " +
- "Applied before the exclude filters. " +
- "Can be specified multiple times.")
- parser.add_argument("-e", "--exclude-file",
- action="append",
- dest="exclude_files",
- help="Regex filter for JAR files to be excluded. " +
- "Applied after the include filters. " +
- "Can be specified multiple times.")
- parser.add_argument("-a", "--annotation",
- action="append",
- dest="annotations",
- help="Fully-qualified Java annotation. " +
- "Java ACC will only check compatibility of " +
- "annotated classes. Can be specified multiple times.")
- parser.add_argument("--skip-clean",
- action="store_true",
- help="Skip cleaning the scratch directory.")
- parser.add_argument("--skip-build",
- action="store_true",
- help="Skip building the projects.")
- parser.add_argument("src_rev", nargs=1, type=str, help="Source revision.")
- parser.add_argument("dst_rev", nargs="?", type=str, default="HEAD",
- help="Destination revision. " +
- "If not specified, will use HEAD.")
- if len(sys.argv) == 1:
- parser.print_help()
- sys.exit(1)
- args = parser.parse_args()
- src_rev, dst_rev = args.src_rev[0], args.dst_rev
- logging.info("Source revision: %s", src_rev)
- logging.info("Destination revision: %s", dst_rev)
- # Construct the JAR regex patterns for filtering.
- include_filters = []
- if args.include_files is not None:
- for f in args.include_files:
- logging.info("Applying JAR filename include filter: %s", f)
- include_filters += [re.compile(f)]
- else:
- include_filters = [re.compile(".*")]
- exclude_filters = []
- if args.exclude_files is not None:
- for f in args.exclude_files:
- logging.info("Applying JAR filename exclude filter: %s", f)
- exclude_filters += [re.compile(f)]
- # Construct the annotation list
- annotations = args.annotations
- if annotations is not None:
- logging.info("Filtering classes using %d annotation(s):", len(annotations))
- for a in annotations:
- logging.info("\t%s", a)
- # Download deps.
- checkout_java_acc(args.force_download)
- # Set up the build.
- scratch_dir = get_scratch_dir()
- src_dir = os.path.join(scratch_dir, "src")
- dst_dir = os.path.join(scratch_dir, "dst")
- if args.skip_clean:
- logging.info("Skipping cleaning the scratch directory")
- else:
- clean_scratch_dir(scratch_dir)
- # Check out the src and dst source trees.
- checkout_java_tree(get_git_hash(src_rev), src_dir)
- checkout_java_tree(get_git_hash(dst_rev), dst_dir)
- # Run the build in each.
- if args.skip_build:
- logging.info("Skipping the build")
- else:
- build_tree(src_dir)
- build_tree(dst_dir)
- # Find the JARs.
- src_jars = find_jars(src_dir)
- dst_jars = find_jars(dst_dir)
- # Filter the JARs.
- src_jars = filter_jars(src_jars, include_filters, exclude_filters)
- dst_jars = filter_jars(dst_jars, include_filters, exclude_filters)
- if len(src_jars) == 0 or len(dst_jars) == 0:
- logging.error("No JARs found! Are your filters too strong?")
- sys.exit(1)
- run_java_acc(src_rev, src_jars,
- dst_rev, dst_jars, annotations)
- if __name__ == "__main__":
- main()
|