blob: 24762cc9a8d46a7ab42cea8555e75c83e34dd878 [file] [log] [blame]
#!/usr/bin/env python
#
# 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.
#
# Compare current commit against the previous commit:
# ./build-support/check_compatibility.py HEAD~1..HEAD
#
# Compare current commit against a release tag:
# ./build-support/check_compatibility.py 1.13.0..HEAD
#
# Compare current commit against a release tag:
# ./build-support/check_compatibility.py 1.13.0..HEAD
#
# Compare two releases:
# ./build-support/check_compatibility.py 1.12.0..1.13.0
#
# NOTE: You can only compare against releases which support a Gradle build (1.5+)
import argparse
import logging
import os
import re
import shutil
import subprocess
import sys
from kudu_util import check_output, init_logging
JAPICMP_VERSION="0.15.1"
JAPICMP_JAR="japicmp-" + JAPICMP_VERSION + "-jar-with-dependencies.jar"
JAPICMP_URL="https://repo1.maven.org/maven2/com/github/siom79/japicmp/japicmp/" \
+ JAPICMP_VERSION + "/" + JAPICMP_JAR
# Paths are ".../kudu/java/<artifact-name>/build/..."
ARTIFACT_NAME_PATTERN=re.compile("java/(.*)/build")
# Various relative paths
PATH_TO_REPO_DIR = "../"
PATH_TO_BUILD_DIR = "../build/compat-check"
def get_repo_dir():
""" Return the path to the top of the repo. """
dirname, _ = os.path.split(os.path.abspath(__file__))
return os.path.abspath(os.path.join(dirname, PATH_TO_REPO_DIR))
def get_scratch_dir():
""" Return the path to the scratch dir that we build within. """
dirname, _ = os.path.split(os.path.abspath(__file__))
return os.path.abspath(os.path.join(dirname, PATH_TO_BUILD_DIR))
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_source_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 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()).decode('utf-8').strip()
def build_tree(path):
""" Run the Java build within 'path'. """
java_path = os.path.join(path, "java")
logging.info("Building in %s...", java_path)
subprocess.check_call(["./gradlew", "assemble"], cwd=java_path)
def get_japicmp_path():
""" Return the path where we download the japicmp jar. """
return os.path.join(get_repo_dir(), "thirdparty/src/" + JAPICMP_JAR)
def download_japicmp(force):
""" Download the japicmp jar. """
if os.path.exists(get_japicmp_path()):
logging.info("japicmp is already downloaded.")
if not force:
return
logging.info("Forcing re-download.")
os.remove(get_japicmp_path())
subprocess.check_call(["curl", "--retry", "3", "-L", "-o",
get_japicmp_path(), JAPICMP_URL], cwd=get_repo_dir())
def find_client_jars(path):
""" Return a list of jars within 'path' to be checked for compatibility. """
all_jars = set(check_output(["find", path, "-name", "*.jar"]).decode('utf-8').splitlines())
return [j for j in all_jars if (
"-javadoc" not in j and
"-sources" not in j and
"-test-sources" not in j and
"-tests" not in j and
"-unshaded" not in j and
"buildSrc" not in j and
"gradle-wrapper" not in j and
"kudu-backup" not in j and
"kudu-hive" not in j and
"kudu-jepsen" not in j and
"kudu-proto" not in j and
"kudu-subprocess" not in j)]
def get_artifact_name(jar_path):
""" Return the artifact name given a full jar path. """
return ARTIFACT_NAME_PATTERN.search(jar_path).group(1)
def run_japicmp(src, dst, opts):
""" Run the compliance checker to compare 'src' and 'dst'. """
src_jars = find_client_jars(src)
dst_jars = find_client_jars(dst)
logging.info("Will check compatibility between original jars:\n%s\n" +
"and new jars:\n%s",
"\n".join(src_jars),
"\n".join(dst_jars))
excludes = [
# Exclude shaded/relocated packages.
"org.apache.kudu.shaded.*",
# Exclude inner Protobuf classes since the annotation filter doesn't handle these.
"org.apache.kudu.*PB*",
# Exclude generated Protobuf code.
"@javax.annotation.Generated",
# Exclude unstable code.
"@org.apache.yetus.audience.InterfaceStability$Unstable",
# Exclude private code.
"@org.apache.yetus.audience.InterfaceAudience$Private",
# Exclude limited private code.
"@org.apache.yetus.audience.InterfaceAudience$LimitedPrivate",
# Exclude Scala Generated code.
"@scala.reflect.ScalaSignature",
"*$$anon$*",
"*$$anonfun$*"
]
reports = []
for src_jar in src_jars:
src_name = get_artifact_name(src_jar)
for dst_jar in dst_jars:
dst_name = get_artifact_name(dst_jar)
if src_name == dst_name:
reports.append((src_name, src_jar, dst_jar))
# CLI tool documentation: https://siom79.github.io/japicmp/CliTool.html
for (name, src_jar, dst_jar) in reports:
out_path = os.path.join(get_scratch_dir(), name + "-report.html")
# Add the extra flags.
cmd = ["java", "-jar", get_japicmp_path(),
"--old", src_jar,
"--new", dst_jar,
"--include", "org.apache.kudu.*",
"--exclude", ";".join(excludes),
"--html-file", out_path,
"--only-modified",
"--ignore-missing-classes",
"--report-only-filename"]
if (opts.only_incompatible):
cmd.append("--only-incompatible")
if (opts.error_on_binary_incompatibility):
cmd.append("--error-on-binary-incompatibility")
if (opts.error_on_source_incompatibility):
cmd.append("--error-on-source-incompatibility")
subprocess.check_call(cmd)
def parse_args():
""" Parse command-line arguments """
parser = argparse.ArgumentParser(description='Check and report on Kudu API compatibility',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('source',
help="The source revision to compare against. Likely the previous release tag.")
parser.add_argument('destination',
help="The destination revision to compare against the source.")
parser.add_argument('--only-incompatible', action='store_true',
help='Outputs only classes/methods that are binary incompatible. '
'If not given, all classes and methods are printed.')
parser.add_argument('--error-on-binary-incompatibility', action='store_true',
help='Exit with an error if a binary incompatibility is detected.')
parser.add_argument('--error-on-source-incompatibility', action='store_true',
help='Exit with an error if a source incompatibility is detected.')
parser.add_argument("--force-download-deps", action='store_true',
help="Download dependencies (i.e. japicmp) even if they are already present")
return parser.parse_args()
def main():
opts = parse_args()
src_rev = get_git_hash(opts.source)
dst_rev = get_git_hash(opts.destination)
logging.info("Source revision: %s", src_rev)
logging.info("Destination revision: %s", dst_rev)
# Set up the build.
scratch_dir = get_scratch_dir()
clean_scratch_dir(scratch_dir)
# Download japicmp.
download_japicmp(opts.force_download_deps)
# Check out the src and dst source trees.
src_dir = os.path.join(scratch_dir, "src")
dst_dir = os.path.join(scratch_dir, "dst")
checkout_source_tree(src_rev, src_dir)
checkout_source_tree(dst_rev, dst_dir)
# Run the build in each tree.
build_tree(src_dir)
build_tree(dst_dir)
run_japicmp(src_dir, dst_dir, opts)
if __name__ == "__main__":
init_logging()
main()